

物联网 #Modbus #TCP/IP #UDP #POE供电 #腾讯云 #Wireshark #Python #InfluxDB #以太网温湿度传感器 #网口温湿度变送器 #机房监控
前面把双供电切换、Wireshark 排障、PoE 功率分配、浪涌防护、EMC 电磁兼容全部讲透了。这一篇换个角度——固件侧。当你的网口温湿度传感器不再是"只跑一个 Modbus TCP 从站"的简单设备,而是要同时响应三种协议:Modbus TCP/IP 做主数据采集、UDP 做主动报警推送、SNMP 被网管平台轮询。三个协议栈同时跑在一颗 MCU 上,资源怎么分、端口怎么管、数据怎么同步、优先级怎么排——这才是真正的嵌入式网关开发。
先说一个开发中的真实坑:
传感器固件跑 FreeRTOS,同时开了 Modbus TCP 监听 502、UDP 监听 161(SNMP)和 UDP 161 用于告警推送。 现场测试:Modbus 轮询正常,SNMP 偶尔超时,UDP 告警丢包严重。 抓包发现:Modbus TCP 连接建立后,SNMP 请求到达时,LwIP 协议栈的 TCP PCB 链表满了,新 UDP 包被丢弃。 原因:LwIP 默认
MEMP_NUM_TCP_PCB只有 5,而采集网关开了 8 个并发连接,SNMP 请求被 LwIP 直接 drop,连应用层回调都没进。
协议 | 使用方 | 用途 | 通信模式 |
|---|---|---|---|
Modbus TCP/IP | 动环采集网关 | 周期性轮询温湿度、配置参数 | 请求-响应,TCP 长连接 |
UDP(自定义) | 边缘规则引擎、本地控制器 | 越限报警主动推送、事件通知 | 无连接,单向发送 |
SNMP | 网络管理系统(NMS)、IT 运维平台 | 设备状态监控、端口流量、系统信息 | 请求-响应,UDP |
核心矛盾:动环系统要 Modbus TCP 的稳定可靠,IT 运维要 SNMP 的标准化,本地联动要 UDP 的低延迟——一台设备要同时服务三个"客户",还不能互相拖后腿。
协议 | 传输层 | 连接状态 | 内存占用(典型) | CPU 开销 |
|---|---|---|---|---|
Modbus TCP | TCP | 面向连接 | 高(每连接 ~2–4 KB PCB + 发送窗口) | 中(三次握手、重传、窗口管理) |
SNMP | UDP | 无连接 | 低(单次请求-响应) | 低 |
自定义 UDP | UDP | 无连接 | 低 | 极低 |
关键问题:TCP 是"重"协议,UDP 是"轻"协议。当 MCU 资源有限(如 STM32F4 带 LwIP,RAM 只有 192 KB),TCP 连接数一多,UDP 包就可能被协议栈丢弃——不是应用层的问题,是协议栈底层的内存池被 TCP 吃光了。
以下以 LwIP 2.1.x + FreeRTOS 为例,这是嵌入式以太网最常见的组合。
// ===== TCP 相关 =====
#define MEMP_NUM_TCP_PCB 10 // 并发 TCP 连接数(默认 5,太小!)
#define MEMP_NUM_TCP_PCB_LISTEN 4 // 监听 PCB 数
#define MEMP_NUM_TCP_SEG 20 // TCP 段数(发送+接收)
#define TCP_SND_BUF (4 * TCP_MSS) // 发送窗口
#define TCP_WND (4 * TCP_MSS) // 接收窗口
#define TCP_MSS 1460
#define TCP_SND_QUEUELEN (MEMP_NUM_TCP_SEG / 2)
// ===== UDP 相关 =====
#define MEMP_NUM_UDP_PCB 6 // UDP PCB 数(每个绑定端口占一个)
#define UDP_MAX_PAYLOAD_SIZE 1472 // UDP 最大载荷(1500 - 20 IP - 8 UDP)
// ===== RAW API / Netconn / Socket =====
#define LWIP_SOCKET 1 // 启用 socket API
#define LWIP_NETCONN 1 // 启用 netconn API
#define LWIP_SO_RCVBUF 1 // 启用接收缓冲区
#define RECV_BUFSIZE_DEFAULT 8192 // 默认接收缓冲区
// ===== 内存堆 =====
#define MEM_SIZE (48 * 1024) // 堆内存 48 KB(默认 16 KB 不够)
#define MEMP_NUM_PBUF 32 // PBUF 数量
#define PBUF_POOL_SIZE 24 // PBUF 池大小
#define PBUF_POOL_BUFSIZE 256 // 每个 PBUF 大小关键调优点:
参数 | 默认值 | 推荐值 | 原因 |
|---|---|---|---|
MEMP_NUM_TCP_PCB | 5 | 10–16 | 每个 Modbus TCP 客户端占用一个 PCB,多网关并发需要更多 |
MEMP_NUM_UDP_PCB | 4 | 6–8 | SNMP + 自定义 UDP + 可能的 DNS/NTP |
MEM_SIZE | 16 KB | 48–64 KB | TCP 窗口 + PCB + PBUF 都从堆分配 |
MEMP_NUM_PBUF | 16 | 32 | 每个收到的包需要一个 PBUF |
// FreeRTOS 任务配置
#define TCPIP_THREAD_PRIO (configMAX_PRIORITIES - 2) // LwIP 核心任务优先级
#define TCPIP_THREAD_STACKSIZE 1024 // LwIP 核心任务栈(单位:字,非字节)
#define MODBUS_TASK_PRIO (configMAX_PRIORITIES - 3) // Modbus 处理任务
#define MODBUS_TASK_STACK 512
#define SNMP_TASK_PRIO (configMAX_PRIORITIES - 4) // SNMP 处理任务
#define SNMP_TASK_STACK 384
#define UDP_ALERT_TASK_PRIO (configMAX_PRIORITIES - 5) // UDP 告警任务
#define UDP_ALERT_TASK_STACK 256设计原则:
tcpip_thread 优先级最高(网络中断处理) ┌─────────────────────────────────────────────────────────────┐
│ 传感器固件(FreeRTOS) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Modbus TCP │ │ SNMP Agent │ │ UDP Alert Sender│ │
│ │ Server Task │ │ Task │ │ Task │ │
│ │ (端口 502) │ │ (端口 161) │ │ (端口 8888) │ │
│ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └──────────────────┼────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ LwIP 协议栈 (tcpip_thread) │ │
│ │ TCP PCB 池 | UDP PCB 池 | PBUF 池 | 内存堆 │ │
│ └──────────────────────────┬──────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 以太网 MAC + PHY (RMII) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
└─────────────────────────────┼─────────────────────────────────┘
│
RJ45 (PoE)三个协议任务访问同一份传感器数据,需要无锁读取 + 写者独占:
// ===== 共享数据区(全局) =====
typedef struct {
// 传感器原始数据
float temperature; // 温度 ℃
float humidity; // 湿度 %RH
float pm25; // PM2.5 μg/m³
float pressure; // 压差 Pa
// 设备状态
uint32_t uptime; // 运行时间(秒)
uint8_t link_status; // 链路状态
uint8_t power_status; // 供电状态(PoE/DC)
// 告警状态
uint16_t alarm_flags; // 告警位图
uint32_t sample_count; // 采样计数
// 同步原语
SemaphoreHandle_t mutex; // 读写锁
} sensor_data_t;
static sensor_data_t g_sensor_data;
// 初始化
void sensor_data_init(void) {
g_sensor_data.mutex = xSemaphoreCreateMutex();
memset(&g_sensor_data, 0, sizeof(sensor_data_t));
}
// 写者(传感器采集任务)更新数据
void sensor_data_update(float temp, float humid, float pm25, float press) {
xSemaphoreTake(g_sensor_data.mutex, portMAX_DELAY);
g_sensor_data.temperature = temp;
g_sensor_data.humidity = humid;
g_sensor_data.pm25 = pm25;
g_sensor_data.pressure = press;
g_sensor_data.sample_count++;
xSemaphoreGive(g_sensor_data.mutex);
}
// 读者(协议任务)读取数据
void sensor_data_read(sensor_data_t *out) {
xSemaphoreTake(g_sensor_data.mutex, portMAX_DELAY);
memcpy(out, &g_sensor_data, sizeof(sensor_data_t));
xSemaphoreGive(g_sensor_data.mutex);
}注意:读操作加互斥锁是为了防止读到半更新状态(虽然 float 在 Cortex-M 上通常是原子读,但结构体拷贝不是)。如果追求极致性能,可以用双缓冲(ping-pong buffer)或 seqlock。
// Modbus 寄存器映射(保持寄存器,4xxxx)
typedef enum {
REG_TEMP = 0, // 温度 ×10 (int16)
REG_HUMID = 1, // 湿度 ×10 (int16)
REG_PM25 = 2, // PM2.5 (uint16)
REG_PRESSURE = 3, // 压差 (uint16)
REG_ALARM = 4, // 告警位图 (uint16)
REG_SAMPLE_CNT = 5, // 采样计数高16 (uint16)
REG_UPTIME = 7, // 运行时间 (uint32, 2 regs)
REG_CONFIG = 100, // 配置起始地址
} modbus_reg_t;
#define MODBUS_MAX_REGS 128
static uint16_t modbus_regs[MODBUS_MAX_REGS];void modbus_tcp_task(void *arg) {
struct netconn *conn, *client;
err_t err;
// 创建 TCP socket,绑定端口 502
conn = netconn_new(NETCONN_TCP);
netconn_bind(conn, IP_ADDR_ANY, 502);
netconn_listen(conn);
while (1) {
err = netconn_accept(conn, &client);
if (err != ERR_OK) continue;
// 每个客户端连接在一个循环中处理
while (1) {
struct netbuf *inbuf;
err_t recv_err = netconn_recv(client, &inbuf);
if (recv_err != ERR_OK) break;
// 解析 Modbus TCP 请求
uint8_t *req;
uint16_t req_len;
netbuf_data(inbuf, (void**)&req, &req_len);
if (req_len >= 8) { // MBAP 头 6 字节 + 功能码 1 + 数据
uint16_t tid = (req[0] << 8) | req[1]; // Transaction ID
uint8_t func = req[7]; // 功能码
uint16_t addr = (req[8] << 8) | req[9]; // 寄存器地址
uint16_t count = (req[10] << 8) | req[11]; // 寄存器数量
// 构造响应
uint8_t resp[256];
int resp_len = modbus_handle_request(req, req_len, resp);
if (resp_len > 0) {
netconn_write(client, resp, resp_len, NETCONN_COPY);
}
}
netbuf_delete(inbuf);
}
netconn_close(client);
netconn_delete(client);
}
}// 私有 MIB OID 树
// 1.3.6.1.4.1.<enterprise>.<product>.<sensor>
#define MIB_TEMP "1.3.6.1.4.1.50001.1.1.1" // 温度
#define MIB_HUMID "1.3.6.1.4.1.50001.1.1.2" // 湿度
#define MIB_PM25 "1.3.6.1.4.1.50001.1.1.3" // PM2.5
#define MIB_ALARM "1.3.6.1.4.1.50001.1.1.4" // 告警状态
#define MIB_UPTIME "1.3.6.1.4.1.50001.1.1.5" // 运行时间void snmp_task(void *arg) {
struct netconn *snmp_conn;
err_t err;
snmp_conn = netconn_new(NETCONN_UDP);
netconn_bind(snmp_conn, IP_ADDR_ANY, 161);
while (1) {
struct netbuf *inbuf;
err = netconn_recv(snmp_conn, &inbuf);
if (err != ERR_OK) continue;
uint8_t *req;
uint16_t req_len;
netbuf_data(inbuf, (void**)&req, &req_len);
// 解析 SNMP GET/GETNEXT 请求
// 简化:只处理 GET 请求
if (snmp_is_get_request(req, req_len)) {
oid_t oid;
snmp_parse_oid(req, req_len, &oid);
sensor_data_t data;
sensor_data_read(&data);
uint8_t resp[256];
int resp_len = 0;
if (strcmp(oid.name, MIB_TEMP) == 0) {
resp_len = snmp_build_response(req, req_len,
&data.temperature, SNMP_TYPE_FLOAT, resp);
} else if (strcmp(oid.name, MIB_HUMID) == 0) {
resp_len = snmp_build_response(req, req_len,
&data.humidity, SNMP_TYPE_FLOAT, resp);
}
// ... 其他 OID
if (resp_len > 0) {
netconn_send(snmp_conn, inbuf->addr, inbuf->port,
resp, resp_len);
}
}
netbuf_delete(inbuf);
}
}void udp_alert_task(void *arg) {
struct netconn *udp_conn;
uint8_t alert_buf[64];
ip_addr_t dest_ip;
uint16_t dest_port = 8888;
udp_conn = netconn_new(NETCONN_UDP);
IP4_ADDR(&dest_ip, 192, 168, 10, 100); // 告警接收服务器
while (1) {
sensor_data_t data;
sensor_data_read(&data);
// 越限检测
uint16_t new_alarms = 0;
if (data.temperature > TEMP_HIGH_THRESH) new_alarms |= ALARM_TEMP_HIGH;
if (data.temperature < TEMP_LOW_THRESH) new_alarms |= ALARM_TEMP_LOW;
if (data.humidity > HUMID_HIGH_THRESH) new_alarms |= ALARM_HUMID_HIGH;
if (data.humidity < HUMID_LOW_THRESH) new_alarms |= ALARM_HUMID_LOW;
// 告警状态变化时才发送(边沿触发,非电平)
static uint16_t last_alarms = 0;
if (new_alarms != last_alarms) {
// 构造告警报文
alert_buf[0] = 0xAA; // 帧头
alert_buf[1] = 0x55;
memcpy(&alert_buf[2], &data.temperature, 4);
memcpy(&alert_buf[6], &data.humidity, 4);
memcpy(&alert_buf[10], &new_alarms, 2);
memcpy(&alert_buf[12], &data.sample_count, 4);
netconn_sendto(udp_conn, &dest_ip, dest_port,
alert_buf, 16);
last_alarms = new_alarms;
}
vTaskDelay(pdMS_TO_TICKS(1000)); // 1 秒检测一次
}
}问题 | 现象 | 根因 | 对策 |
|---|---|---|---|
Modbus TCP 连接数多了 SNMP 超时 | SNMP 请求无响应 | LwIP TCP PCB 池满,UDP PCB 分配失败 | 增大 MEMP_NUM_TCP_PCB,限制最大连接数 |
UDP 告警丢包 | 接收端收不到告警 | LwIP 内存池不足,PBUF 分配失败 | 增大 PBUF_POOL_SIZE,告警发送加重试 |
数据读取不一致 | Modbus 返回的温度和湿度来自不同采样周期 | 读写无同步 | 加互斥锁或双缓冲 |
协议栈死锁 | 所有协议无响应 | 某个任务持有 mutex 时阻塞在 netconn API | 不在持有 mutex 时调用 LwIP API |
内存泄漏 | 运行几天后所有协议失效 | TCP 连接未正确关闭,PCB 未释放 | 实现连接超时机制,定期清理空闲连接 |
#define MAX_MODBUS_CONNECTIONS 8
static uint8_t g_conn_count = 0;
err_t modbus_accept_cb(void *arg, struct tcp_pcb *newpcb, err_t err) {
if (g_conn_count >= MAX_MODBUS_CONNECTIONS) {
tcp_abort(newpcb); // 拒绝新连接
return ERR_ABRT;
}
g_conn_count++;
return ERR_OK;
}import asyncio
from pymodbus.client import AsyncModbusTcpClient
from pysnmp.hlapi import *
async def test_modbus():
client = AsyncModbusTcpClient("192.168.10.21", port=502)
await client.connect()
for i in range(100):
resp = await client.read_holding_registers(0, count=4, slave=1)
print(f"Modbus #{i}: {resp.registers}")
await asyncio.sleep(0.1)
def test_snmp():
# SNMP GET
iterator = getCmd(
SnmpEngine(),
CommunityData('public'),
UdpTransportTarget(('192.168.10.21', 161)),
ContextData(),
ObjectType(ObjectIdentity('1.3.6.1.4.1.50001.1.1.1'))
)
errorIndication, errorStatus, errorIndex, varBinds = next(iterator)
print(f"SNMP: {varBinds}")
async def test_all():
await asyncio.gather(
test_modbus(),
asyncio.to_thread(test_snmp)
)
asyncio.run(test_all())测试项 | 指标 | 合格标准 |
|---|---|---|
Modbus TCP 并发 8 连接 | 每连接 RTT | < 50 ms |
SNMP GET | 响应时间 | < 100 ms |
UDP 告警 | 发送延迟 | < 10 ms |
内存使用 | LwIP 堆剩余 | > 8 KB |
长时间运行(24h) | 无内存泄漏 | 堆使用稳定 |
问题 | 后果 | 正确做法 |
|---|---|---|
LwIP 默认配置不调 | 多协议时丢包 | 按本文参数调整 |
共享数据无保护 | 读到脏数据 | 互斥锁或双缓冲 |
TCP 连接不限制 | 资源耗尽 | 限制最大连接数 |
UDP 发送不检查返回值 | 丢告警 | 发送失败加重试 |
SNMP 用阻塞式 API | 阻塞其他协议 | 用非阻塞或独立任务 |
不测试并发 | 现场出问题 | 并发压测 24h |
多协议同时运行不是"把三个端口都打开"那么简单——它是协议栈内存管理、任务调度、数据共享、资源隔离的综合工程。 调好 LwIP 参数、管好连接数、保护好共享数据,三个协议才能在同一颗 MCU 上和平共处。 否则,你以为的"功能丰富",其实是"互相拖垮"。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。