
关键词:电力中心配电柜、以太网温湿度变送器、SNMP、UDP、双协议固件、嵌入式开发、LwIP、FreeRTOS、MIB设计、协议共存 标签:#物联网 #TCP/IP #UDP #POE供电 #以太网温湿度传感器 #网口温湿度变送器 #机房监控 #嵌入式开发
电力中心配电柜场景对温湿度数据的需求是双重的:
需求维度 | 运维侧(动环监控) | 调度侧(电力监控) |
|---|---|---|
使用方式 | 平台轮询,趋势分析 | 实时告警,联动跳闸 |
通信偏好 | SNMP(标准化,易集成) | UDP 主动上报(低延迟) |
容忍延迟 | 秒级 | 亚秒级 |
可靠性要求 | 可容忍短暂中断 | 丢数据需可感知 |
协议生态 | NMS、Zabbix、PRTG | 自研采集器、SCADA |
单协议固件无法满足两类用户的部署习惯。双协议同时运行,UDP 负责"快",SNMP 负责"稳",网关侧按前文(第12篇)的融合策略消费即可。
固件层面的挑战在于:如何在资源受限的 MCU 上,让两个协议栈高效共存,互不阻塞,且对硬件资源(RAM、Flash、CPU)的消耗可控。
器件 | 型号/规格 | 说明 |
|---|---|---|
MCU | STM32F407VGT6 (Cortex-M4, 168MHz, 192KB RAM, 1MB Flash) | 主流工业级,性价比高 |
以太网 PHY | LAN8742A (RMII) | 自带魔术包唤醒 |
POE PD | TPS2375x (802.3af/at) | 集成 DC-DC 隔离 |
温湿度芯片 | SHT45 (I2C, ±0.1℃/±1.5%RH) | 高精度,长期稳定性好 |
外部 Flash | W25Q128 (16MB, SPI) | 存储配置、日志、固件备份 |
看门狗 | TPS3823 (外部硬件) | 防死机 |
┌─────────────────────────────────────────────────────────────┐
│ 应用层 (FreeRTOS Tasks) │
├────────────┬────────────┬────────────┬───────────────────────┤
│ 传感器任务 │ UDP发送任务 │ SNMP任务 │ 配置/管理任务 │
│ (100ms周期) │ (5s周期) │ (事件驱动) │ (Web/CLI/Flash) │
├────────────┴────────────┴────────────┴───────────────────────┤
│ 协议栈层 (LwIP 2.1.2) │
├───────────────────────────┬─────────────────────────────────┤
│ UDP (raw API / netconn) │ SNMP (net-snmp 移植 / 自研) │
│ - 二进制帧封装 │ - MIB-II 标准组 │
│ - 序列号 + CRC16 │ - 私有 MIB (环境数据) │
│ - 可配置目标IP/端口 │ - GET/GETNEXT/SET 支持 │
│ - QoS (DSCP) 标记 │ - Trap v1/v2c 主动上报 │
├───────────────────────────┴─────────────────────────────────┤
│ 网络接口层 (ethernetif) │
│ - RMII + DMA 描述符环 │
│ - 中断 + 零拷贝接收 │
├─────────────────────────────────────────────────────────────┤
│ 硬件抽象层 (HAL / LL) │
│ - GPIO/I2C/SPI/RCC/NVIC │
└─────────────────────────────────────────────────────────────┘/* FreeRTOS 任务配置 */
#define TASK_SENSOR_PRIO (tskIDLE_PRIORITY + 4) /* 传感器采集,最高 */
#define TASK_UDP_TX_PRIO (tskIDLE_PRIORITY + 3) /* UDP发送 */
#define TASK_SNMP_PRIO (tskIDLE_PRIORITY + 2) /* SNMP(事件驱动,大部分时间阻塞) */
#define TASK_CONFIG_PRIO (tskIDLE_PRIORITY + 1) /* 配置管理(Web/CLI) */
#define TASK_IDLE_PRIO (tskIDLE_PRIORITY + 0) /* 空闲 */
/* 栈大小 */
#define STACK_SENSOR 512
#define STACK_UDP_TX 1024
#define STACK_SNMP 2048 /* SNMP需要较大栈空间处理OID解析 */
#define STACK_CONFIG 1536/* ===== lwipopts.h - 双协议场景关键配置 ===== */
/* 内存配置 */
#define MEM_SIZE (32 * 1024) /* 堆内存 32KB */
#define MEMP_NUM_PBUF 64
#define MEMP_NUM_TCP_PCB 8
#define MEMP_NUM_TCP_PCB_LISTEN 4
#define MEMP_NUM_UDP_PCB 6 /* UDP + SNMP 各需PCB */
#define MEMP_NUM_NETBUF 32
#define MEMP_NUM_NETCONN 16
#define MEMP_NUM_TCPIP_MSG_API 32
#define MEMP_NUM_TCPIP_MSG_INPKT 32
/* UDP 配置 */
#define LWIP_UDP 1
#define UDP_TTL 128
#define LWIP_UDPLITE 0
/* SNMP 配置 */
#define LWIP_SNMP 1
#define LWIP_SNMP_V1 1
#define LWIP_SNMP_V2C 1
#define LWIP_SNMP_V3 0 /* 资源受限,暂不启用V3 */
#define LWIP_SNMP_MIB2 1
#define LWIP_SNMP_PRIVATE_MIB 1
/* TCP 配置(Web管理界面用) */
#define LWIP_TCP 1
#define TCP_MSS 1460
#define TCP_SND_BUF (4 * TCP_MSS)
#define TCP_WND (4 * TCP_MSS)
/* 网络接口 */
#define LWIP_NETIF_STATUS_CALLBACK 1
#define LWIP_NETIF_LINK_CALLBACK 1
/* 线程安全 */
#define SYS_LIGHTWEIGHT_PROT 1
#define NO_SYS 0 /* 使用RTOS */
/* 校验和 */
#define CHECKSUM_GEN_IP 0 /* 硬件加速 */
#define CHECKSUM_GEN_UDP 0
#define CHECKSUM_GEN_TCP 0
#define CHECKSUM_CHECK_IP 0
#define CHECKSUM_CHECK_UDP 0
#define CHECKSUM_CHECK_TCP 0/* ethernetif.c - 中断到任务的零拷贝路径 */
/* 接收中断 */
void ETH_IRQHandler(void)
{
HAL_ETH_IRQHandler(&heth);
}
/* 中断回调:将帧放入队列,唤醒 tcpip_thread */
void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth)
{
struct pbuf *p;
/* 从DMA描述符获取帧 */
p = low_level_input(netif);
if (p != NULL) {
/* 直接交给 tcpip_input(零拷贝) */
if (netif->input(p, netif) != ERR_OK) {
pbuf_free(p);
}
}
/* 重新激活DMA接收 */
HAL_ETH_BuildRxDescriptors(heth);
}/* udp_tx_task.c */
#include "lwip/udp.h"
#include "lwip/ip_addr.h"
static struct udp_pcb *udp_pcb;
static ip_addr_t dest_ip;
static u16_t dest_port = 9000;
/* UDP帧格式:
* 包头(2B, 0xAA55) | 设备ID(4B) | 温度(2B, ×0.1) | 湿度(2B, ×0.1)
* | 露点(2B, ×0.1) | 序列号(2B) | 状态码(1B) | CRC16(2B)
* 总计:15字节
*/
typedef struct {
u16_t header; /* 0xAA55 */
u32_t dev_id; /* 设备唯一ID(从Flash读取) */
s16_t temperature; /* 温度 ×10 */
s16_t humidity; /* 湿度 ×10 */
s16_t dewpoint; /* 露点 ×10 */
u16_t sequence; /* 序列号,每包递增 */
u8_t status; /* 状态码:bit0=传感器故障, bit1=POE欠压 */
u16_t crc; /* CRC-16/MODBUS */
} __attribute__((packed)) udp_frame_t;
static udp_frame_t tx_frame;
static u16_t seq_counter = 0;
void udp_tx_task(void *arg)
{
/* 创建UDP PCB */
udp_pcb = udp_new();
if (udp_pcb == NULL) {
vTaskDelete(NULL);
return;
}
/* 配置目标地址(从Flash读取,默认广播) */
ipaddr_aton(get_config()->udp_dest_ip, &dest_ip);
dest_port = get_config()->udp_dest_port;
/* 初始化帧头 */
tx_frame.header = 0xAA55;
tx_frame.dev_id = get_device_id(); /* 从唯一ID寄存器或Flash读取 */
tx_frame.sequence = 0;
tx_frame.status = 0;
TickType_t last_wake = xTaskGetTickCount();
const TickType_t period = pdMS_TO_TICKS(get_config()->udp_interval * 1000);
for (;;) {
vTaskDelayUntil(&last_wake, period);
/* 填充传感器数据 */
sensor_reading_t reading;
sensor_get_latest(&reading);
tx_frame.temperature = (s16_t)(reading.temperature * 10);
tx_frame.humidity = (s16_t)(reading.humidity * 10);
tx_frame.dewpoint = (s16_t)(reading.dewpoint * 10);
tx_frame.sequence = seq_counter++;
/* 状态码 */
tx_frame.status = 0;
if (!reading.valid) tx_frame.status |= 0x01;
if (reading.poe_voltage < 37) tx_frame.status |= 0x02;
/* 计算CRC */
tx_frame.crc = crc16((u8_t *)&tx_frame, sizeof(udp_frame_t) - 2);
/* 发送 */
struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, sizeof(udp_frame_t), PBUF_RAM);
if (p != NULL) {
memcpy(p->payload, &tx_frame, sizeof(udp_frame_t));
/* 设置DSCP(如果LwIP支持) */
/* ip_set_option(pcb, IP_TOS, 0xB8); DSCP 46 (EF) */
err_t err = udp_sendto(udp_pcb, p, &dest_ip, dest_port);
if (err != ERR_OK) {
/* 发送失败,记录统计 */
stats.udp_tx_err++;
} else {
stats.udp_tx_ok++;
}
pbuf_free(p);
}
}
}/* UDP接收回调 - 处理来自网关的配置命令 */
void udp_recv_callback(void *arg, struct udp_pcb *pcb, struct pbuf *p,
const ip_addr_t *addr, u16_t port)
{
if (p == NULL) return;
/* 简单命令协议:
* CMD_SET_INTERVAL (1B) | 间隔秒数 (1B)
* CMD_SET_DEST_IP (1B) | IP地址 (4B)
* CMD_REBOOT (1B)
*/
u8_t *data = (u8_t *)p->payload;
u8_t cmd = data[0];
switch (cmd) {
case 0x01: /* SET_INTERVAL */
if (p->len >= 2) {
u8_t interval = data[1];
if (interval >= 1 && interval <= 60) {
config_set_udp_interval(interval);
save_config_to_flash();
}
}
break;
case 0x02: /* SET_DEST_IP */
if (p->len >= 5) {
ip_addr_t new_ip;
IP4_ADDR(&new_ip, data[1], data[2], data[3], data[4]);
config_set_udp_dest_ip(&new_ip);
save_config_to_flash();
}
break;
case 0xFF: /* REBOOT */
NVIC_SystemReset();
break;
}
pbuf_free(p);
}私有MIB树结构:
.iso.org.dod.internet.private.enterprises (1.3.6.1.4.1)
└── <enterprise-id> (如 12345)
├── 1 (deviceInfo)
│ ├── 1.0 deviceName (OCTET STRING) 设备名称
│ ├── 2.0 deviceId (INTEGER) 设备ID
│ ├── 3.0 firmwareVersion (OCTET STRING) 固件版本
│ └── 4.0 uptime (Timeticks) 运行时间
├── 2 (environment)
│ ├── 1.0 temperature (INTEGER) 温度 ×10 (0.1℃)
│ ├── 2.0 humidity (INTEGER) 湿度 ×10 (0.1%RH)
│ ├── 3.0 dewpoint (INTEGER) 露点 ×10
│ ├── 4.0 tempStatus (INTEGER) 温度状态 (0=正常,1=超限,2=故障)
│ ├── 5.0 humStatus (INTEGER) 湿度状态
│ └── 6.0 alarmStatus (INTEGER) 综合告警 (0=正常, 1=告警)
├── 3 (network)
│ ├── 1.0 ipAddress (IpAddress) IP地址
│ ├── 2.0 macAddress (OCTET STRING) MAC地址
│ ├── 3.0 linkSpeed (INTEGER) 链路速率 (Mbps)
│ ├── 4.0 linkStatus (INTEGER) 链路状态 (1=up, 2=down)
│ └── 5.0 snmpReqCount (Counter32) SNMP请求计数
└── 4 (power)
├── 1.0 poeVoltage (INTEGER) POE输入电压 ×10
├── 2.0 poeCurrent (INTEGER) POE输入电流 ×100
└── 3.0 poePower (INTEGER) POE功率 ×10/* snmp_private_mib.c */
#include "lwip/snmp.h"
#include "lwip/snmp_structs.h"
/* OID 定义 */
static const u32_t deviceInfo_oid[] = {1,3,6,1,4,1,12345,1};
static const u32_t environment_oid[] = {1,3,6,1,4,1,12345,2};
static const u32_t network_oid[] = {1,3,6,1,4,1,12345,3};
static const u32_t power_oid[] = {1,3,6,1,4,1,12345,4};
/* 读取回调函数 */
static u16_t deviceInfo_get_value(struct snmp_node_instance* instance,
void* buf, u16_t buflen)
{
u8_t id = instance->node->oid[instance->node->oid_len - 1];
switch (id) {
case 1: /* deviceName */
return snmp_set_varbind_value(buf, buf, buflen,
(u8_t*)DEVICE_NAME, strlen(DEVICE_NAME));
case 2: /* deviceId */
*(s32_t*)buf = get_device_id();
return sizeof(s32_t);
case 3: /* firmwareVersion */
return snmp_set_varbind_value(buf, buf, buflen,
(u8_t*)FIRMWARE_VERSION, strlen(FIRMWARE_VERSION));
case 4: /* uptime */
*(u32_t*)buf = sys_now() / 10; /* sysTick → 0.01s */
return sizeof(u32_t);
}
return 0;
}
static u16_t environment_get_value(struct snmp_node_instance* instance,
void* buf, u16_t buflen)
{
u8_t id = instance->node->oid[instance->node->oid_len - 1];
sensor_reading_t reading;
sensor_get_latest(&reading);
switch (id) {
case 1: /* temperature */
*(s32_t*)buf = (s32_t)(reading.temperature * 10);
return sizeof(s32_t);
case 2: /* humidity */
*(s32_t*)buf = (s32_t)(reading.humidity * 10);
return sizeof(s32_t);
case 3: /* dewpoint */
*(s32_t*)buf = (s32_t)(reading.dewpoint * 10);
return sizeof(s32_t);
case 4: /* tempStatus */
*(s32_t*)buf = reading.temp_valid ? 0 : 2;
return sizeof(s32_t);
case 5: /* humStatus */
*(s32_t*)buf = reading.hum_valid ? 0 : 2;
return sizeof(s32_t);
case 6: /* alarmStatus */
*(s32_t*)buf = (reading.temperature > TEMP_HIGH_THRESH ||
reading.humidity > HUM_HIGH_THRESH) ? 1 : 0;
return sizeof(s32_t);
}
return 0;
}
/* SNMP Agent 初始化 */
void snmp_agent_init(void)
{
/* 设置系统信息(MIB-II sys组) */
snmp_set_sysdesr((u8_t*)"Dual-Proto Temp&Hum Sensor v1.0", 32);
snmp_set_syscontact((u8_t*)"admin@facility.local", 20);
snmp_set_sysname((u8_t*)"Sensor-01", 9);
snmp_set_syslocation((u8_t*)"Power Distribution Cabinet A", 25);
/* 注册私有MIB */
snmp_mib_add_node(deviceInfo_oid, 8, SNMP_NODE_SCALAR,
deviceInfo_get_value, NULL, NULL);
snmp_mib_add_node(environment_oid, 8, SNMP_NODE_SCALAR,
environment_get_value, NULL, NULL);
/* 启动SNMP Agent(LwIP内部已启动UDP监听161端口) */
snmp_trap_dst_enable(0, 1);
snmp_trap_dst_ip_set(0, &trap_dest_ip);
}/* snmp_trap.c - 告警时主动发送Trap */
void send_alarm_trap(u8_t alarm_type, s16_t value, s16_t threshold)
{
struct snmp_varbind *vb_head = NULL;
u32_t now = sys_now() / 10; /* 系统运行时间(0.01s) */
/* 构造Trap PDU:
* sysUpTime (1.3.6.1.2.1.1.3.0) + snmpTrapOID (1.3.6.1.6.3.1.1.4.1.0)
* + 私有变量
*/
/* sysUpTime */
vb_head = snmp_varbind_alloc(&sysUpTime_oid, 9, SNMP_ASN1_TYPE_TIMETICKS, 4);
if (vb_head) {
*(u32_t*)vb_head->value = now;
}
/* snmpTrapOID - 使用私有Trap OID */
/* 1.3.6.1.4.1.12345.0.1 = temperatureHigh */
vb_head = snmp_varbind_append(vb_head, &trapOID_oid, 11,
SNMP_ASN1_TYPE_OBJ_ID, trap_oid, trap_oid_len * 4);
/* 附加变量:告警类型、当前值、阈值 */
vb_head = snmp_varbind_append(vb_head, &alarmType_oid, 10,
SNMP_ASN1_TYPE_INTEGER, &alarm_type, 4);
vb_head = snmp_varbind_append(vb_head, ¤tValue_oid, 10,
SNMP_ASN1_TYPE_INTEGER, &value, 4);
vb_head = snmp_varbind_append(vb_head, &threshold_oid, 10,
SNMP_ASN1_TYPE_INTEGER, &threshold, 4);
/* 发送Trap v2c */
snmp_send_trap(&trap_dest_ip, SNMP_TRAP_PORT, vb_head);
/* 释放varbind链 */
snmp_varbind_free(vb_head);
}/* 内存分区:为SNMP和UDP分配独立缓冲区 */
#define UDP_BUF_SIZE 16
#define SNMP_BUF_SIZE 256
/* UDP用较小的缓冲区(帧固定15字节) */
static u8_t udp_tx_buf[UDP_BUF_SIZE] __attribute__((aligned(4)));
/* SNMP用较大缓冲区(OID解析、Trap构造) */
static u8_t snmp_buf[SNMP_BUF_SIZE] __attribute__((aligned(4)));
/* 使用FreeRTOS的heap_4,避免碎片化 */关键原则:传感器采集不能被网络任务阻塞
时间线(5秒周期):
0ms 传感器任务采集(I2C, ~2ms)
2ms 数据就绪,通知UDP任务
通知SNMP任务(更新MIB缓存)
3ms UDP任务发送(~1ms)
如果SNMP有GET请求,处理(~5ms)
5ms 空闲
...
5000ms 下一个周期/* 使用队列传递传感器数据,解耦采集和网络 */
QueueHandle_t sensor_data_queue;
void sensor_task(void *arg)
{
sensor_reading_t reading;
for (;;) {
sensor_read(&reading);
xQueueSend(sensor_data_queue, &reading, 0);
vTaskDelay(pdMS_TO_TICKS(100)); /* 100ms采样率 */
}
}
/* UDP和SNMP任务从队列读取最新数据,不阻塞采集 *//* 独立看门狗(IWDG)配置 */
void iwdg_init(void)
{
/* 超时时间:5秒 */
hiwdg.Instance = IWDG;
hiwdg.Init.Prescaler = IWDG_PRESCALER_256;
hiwdg.Init.Reload = 0x0FFF; /* ~5s */
HAL_IWDG_Init(&hiwdg);
}
/* 每个任务定期喂狗 */
void task_feed_watchdog(void)
{
static TickType_t last_feed = 0;
TickType_t now = xTaskGetTickCount();
if (now - last_feed > pdMS_TO_TICKS(1000)) {
HAL_IWDG_Refresh(&hiwdg);
last_feed = now;
}
}MEM_SIZE 只有 16KB,SNMP GETNEXT 遍历 MIB 树时内存耗尽,设备重启。解决:将 MEM_SIZE 调至 32KB 以上,限制 MIB 节点数量。 snmpwalk 验证。 sys_now() 作为时间源。 netif_set_up() 后等待 NETIF_FLAG_LINK_UP 事件再启动 SNMP。 电力中心配电柜场景下,SNMP+UDP 双协议以太网温湿度变送器的固件开发,核心挑战是在资源受限的 MCU 上让两个协议高效共存。关键设计决策:LwIP 作为统一协议栈底座,UDP 用 raw API 实现最小开销的主动上报,SNMP 用 LwIP 内置 Agent 实现标准化管理,两者通过队列解耦传感器采集任务。MIB 设计要精简实用,Trap 机制要可靠不阻塞。固件开发中最容易踩的坑是内存配置不足和时序协调不当,需要充分的压力测试和长时间运行验证。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。