首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >多协议网关开发笔记:在同一台以太网温湿度传感器上同时运行 Modbus TCP/IP、UDP 与 SNMP

多协议网关开发笔记:在同一台以太网温湿度传感器上同时运行 Modbus TCP/IP、UDP 与 SNMP

原创
作者头像
盛世宏博小可
发布于 2026-09-22 14:41:12
发布于 2026-09-22 14:41:12
980
举报

多协议网关开发笔记:在同一台以太网温湿度传感器上同时运行 Modbus TCP/IP、UDP 与 SNMP

物联网 #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,连应用层回调都没进。


一、为什么需要多协议同时运行?

1. 场景驱动

协议

使用方

用途

通信模式

Modbus TCP/IP

动环采集网关

周期性轮询温湿度、配置参数

请求-响应,TCP 长连接

UDP(自定义)

边缘规则引擎、本地控制器

越限报警主动推送、事件通知

无连接,单向发送

SNMP

网络管理系统(NMS)、IT 运维平台

设备状态监控、端口流量、系统信息

请求-响应,UDP

核心矛盾:动环系统要 Modbus TCP 的稳定可靠,IT 运维要 SNMP 的标准化,本地联动要 UDP 的低延迟——一台设备要同时服务三个"客户",还不能互相拖后腿。

2. 协议栈资源需求对比

协议

传输层

连接状态

内存占用(典型)

CPU 开销

Modbus TCP

TCP

面向连接

高(每连接 ~2–4 KB PCB + 发送窗口)

中(三次握手、重传、窗口管理)

SNMP

UDP

无连接

低(单次请求-响应)

低

自定义 UDP

UDP

无连接

低

极低

关键问题:TCP 是"重"协议,UDP 是"轻"协议。当 MCU 资源有限(如 STM32F4 带 LwIP,RAM 只有 192 KB),TCP 连接数一多,UDP 包就可能被协议栈丢弃——不是应用层的问题,是协议栈底层的内存池被 TCP 吃光了。


二、LwIP 协议栈配置:多协议共存的关键参数

以下以 LwIP 2.1.x + FreeRTOS 为例,这是嵌入式以太网最常见的组合。

1. 内存池配置(lwipopts.h)

代码语言:javascript
复制
// ===== 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

2. 任务优先级与栈大小

代码语言:javascript
复制
// 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

设计原则:

  • LwIP tcpip_thread 优先级最高(网络中断处理)
  • Modbus TCP 次之(实时数据采集)
  • SNMP 再次(管理面,允许延迟)
  • UDP 告警最低(但发送路径要快,用中断直接触发)

三、多协议固件架构

1. 整体架构

代码语言:javascript
复制
┌─────────────────────────────────────────────────────────────┐
│                    传感器固件(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)

2. 共享数据区设计

三个协议任务访问同一份传感器数据,需要无锁读取 + 写者独占:

代码语言:javascript
复制
// ===== 共享数据区(全局) =====
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 TCP 从站实现

1. 寄存器映射

代码语言:javascript
复制
// 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];

2. Modbus TCP 处理任务

代码语言:javascript
复制
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);
    }
}

五、SNMP Agent 实现

1. MIB 定义(简化版)

代码语言:javascript
复制
// 私有 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"  // 运行时间

2. SNMP 请求处理

代码语言:javascript
复制
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);
    }
}

六、UDP 告警推送

1. 告警检测与发送

代码语言:javascript
复制
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 秒检测一次
    }
}

七、资源竞争与性能优化

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 未释放

实现连接超时机制,定期清理空闲连接

2. 连接数限制(防止资源耗尽)

代码语言:javascript
复制
#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;
}

八、测试与验证

1. 协议并发测试

代码语言:javascript
复制
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())

2. 性能基准

测试项

指标

合格标准

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 删除。

目录
  • 多协议网关开发笔记:在同一台以太网温湿度传感器上同时运行 Modbus TCP/IP、UDP 与 SNMP
    • 一、为什么需要多协议同时运行?
      • 1. 场景驱动
      • 2. 协议栈资源需求对比
    • 二、LwIP 协议栈配置:多协议共存的关键参数
      • 1. 内存池配置(lwipopts.h)
      • 2. 任务优先级与栈大小
    • 三、多协议固件架构
      • 1. 整体架构
      • 2. 共享数据区设计
    • 四、Modbus TCP 从站实现
      • 1. 寄存器映射
      • 2. Modbus TCP 处理任务
    • 五、SNMP Agent 实现
      • 1. MIB 定义(简化版)
      • 2. SNMP 请求处理
    • 六、UDP 告警推送
      • 1. 告警检测与发送
    • 七、资源竞争与性能优化
      • 1. 常见问题与对策
      • 2. 连接数限制(防止资源耗尽)
    • 八、测试与验证
      • 1. 协议并发测试
      • 2. 性能基准
    • 九、常见返工点
    • 十、一句话总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档