

近期在调试一套工业环境监控系统时,遇到了关于Modbus连接保持的问题。现场部署了一批网口温湿度变送器,PoE取电、Modbus TCP上云,同时利旧接入存量RS485探头,并开启了SNMP和UDP Trap服务。服务器机柜内部空间极其有限——1U PDU旁边、盲板缝隙、走线槽拐角,留给传感器的物理空间往往只有火柴盒大小。这篇从硬件选型、PCB布局、固件架构到PoE供电调优,完整记录小型RJ45温湿度变送器的嵌入式开发全过程。
维度 | 目标值 | 约束来源 |
|---|---|---|
外壳尺寸 | ≤ 40×40×18mm | 服务器机柜盲板开孔/PDU侧面 |
PCB尺寸 | ≤ 35×35mm | 外壳内腔 |
RJ45连接器 | 集成式(不可突出) | 面板安装要求 |
传感器开孔 | φ2mm | 气流通道 |
器件 | 型号 | 理由 |
|---|---|---|
MCU | STM32F103C8T6 | 64KB Flash/20KB RAM,性价比高,LWIP可跑 |
以太网PHY | LAN8720A | 单芯片10/100M,RMII接口,QFN-24(4×4mm) |
温湿度传感器 | SHT30 | I²C,±0.2℃/±2%RH,DFN-8(2.5×2.5mm) |
PoE PD | Si3402 | 集成整流桥+DC-DC,支持802.3af,TSSOP-16 |
Flash | W25Q16 | 2MB SPI NOR,SOIC-8 |
晶振 | 25MHz ±10ppm | PHY RMII参考时钟 |
RJ45 | HR911105A | 集成变压器+LED,单口,底部焊盘接地 |
关键决策:放弃外部SDRAM,用STM32F103内部20KB RAM跑LWIP;放弃外部RTC,用STM32内部RTC+温补晶振(LSE 32.768kHz);放弃RS485扩展,只做单点监测。
4层板叠构:
Layer 1: 信号(RJ45/PHY/MCU高速线)
Layer 2: 完整地平面(GND)
Layer 3: 电源平面(3.3V/1.2V/VDD_POE)
Layer 4: 信号(I²C/SPI/GPIO)分区原则:
┌─────────────────────────────────────────────┐
│ RJ45 (左) │ PHY (中左) │ MCU (中右) │
│ ───────── │ ────────── │ ─────────── │
│ 变压器内置 │ RMII走线 │ GPIO/I²C │
│ 屏蔽壳接 │ <15mm │ <10mm │
│ Layer2地 │ │ │
├─────────────┴──────────────┴───────────────┤
│ PoE PD (下) │
│ Si3402 + 整流桥 + 滤波电感 │
│ 注意:高压(48V)与低压(3.3V)间距≥2mm │
├─────────────────────────────────────────────┤
│ SHT30 (右上角) │
│ 开孔对准外壳气流通道 │
└─────────────────────────────────────────────┘RMII信号(50MHz,严格等长):
REF_CLK: MCU → PHY, 长度 ≤ 25mm
TXD0/1: MCU → PHY, 等长 ±2mm
TX_EN: MCU → PHY, 等长 ±2mm
RXD0/1: PHY → MCU, 等长 ±2mm
CRS_DV: PHY → MCU, 等长 ±2mm
MDC/MDIO: 管理接口,长度不限但远离噪声源关键:REF_CLK是50MHz源同步时钟,走线尽量短,避免过孔。所有RMII信号线宽0.15mm,间距0.15mm(3W规则)。
RJ45 Pin 1,2,3,6 (差分对)
│
├───→ 中心抽头(内置变压器)
│ │
│ ┌────▼────┐
│ │ Si3402 │ PoE PD + DC-DC
│ │ TSSOP16 │
│ └────┬────┘
│ │ 3.3V/1.2V
│ ┌────▼────┐
│ │ 滤波 │ 10μF + 0.1μF
│ │ π型 │
│ └────┬────┘
│ │
└─────────┼──→ PHY/MAC
│
┌────▼────┐
│ 3.3V LDO│ → MCU/SHT30/Flash
└─────────┘注意:Si3402的GND_PAD必须焊接到Layer2地平面,散热过孔至少4个(直径0.3mm)。
// 任务优先级(0最低,configMAX_PRIORITIES-1最高)
#define TASK_PRIO_SENSOR 3 // 传感器采集
#define TASK_PRIO_NET 4 // 网络协议栈
#define TASK_PRIO_MODBUS 3 // Modbus TCP服务
#define TASK_PRIO_UDP 2 // UDP推送
#define TASK_PRIO_SYNC 1 // 本地存储同步
#define TASK_PRIO_WATCHDOG 5 // 看门狗
// 任务栈大小
#define STACK_SENSOR 256
#define STACK_NET 512
#define STACK_MODBUS 384
#define STACK_UDP 256
#define STACK_SYNC 384
#define STACK_WATCHDOG 128// lwipopts.h - 针对20KB RAM的精简配置
#define MEM_SIZE 8192 // 堆内存 8KB
#define MEMP_NUM_PBUF 32
#define MEMP_NUM_TCP_PCB 4 // 最多4个TCP连接
#define MEMP_NUM_TCP_PCB_LISTEN 2
#define MEMP_NUM_TCP_SEG 16
#define MEMP_NUM_REASSDATA 4
#define MEMP_NUM_FRAG_PBUF 4
#define PBUF_POOL_SIZE 16
#define PBUF_POOL_BUFSIZE 256
#define TCP_MSS 1460
#define TCP_SND_BUF 2048
#define TCP_WND 2048
#define TCP_QUEUE_OOSEQ 0 // 关闭乱序队列省内存
#define LWIP_NETIF_STATUS_CALLBACK 1
#define LWIP_NETIF_LINK_CALLBACK 1// main.c
int main(void) {
// 硬件初始化
HAL_Init();
SystemClock_Config(); // 72MHz
MX_GPIO_Init();
MX_USART1_UART_Init(); // 调试串口
MX_I2C1_Init(); // SHT30
MX_SPI1_Init(); // W25Q16
MX_TIM2_Init(); // 系统tick
MX_ETH_Init(); // RMII + LAN8720A
// 外设初始化
sht30_init();
flash_init();
ntp_init();
// LWIP初始化
lwip_init();
netif_add(&g_netif, &ipaddr, &netmask, &gw, NULL, ethernetif_init, netif_input);
netif_set_default(&g_netif);
netif_set_up(&g_netif);
// FreeRTOS任务创建
xTaskCreate(sensor_task, "sensor", STACK_SENSOR, NULL, TASK_PRIO_SENSOR, NULL);
xTaskCreate(net_task, "net", STACK_NET, NULL, TASK_PRIO_NET, NULL);
xTaskCreate(modbus_task, "modbus", STACK_MODBUS, NULL, TASK_PRIO_MODBUS, NULL);
xTaskCreate(udp_task, "udp", STACK_UDP, NULL, TASK_PRIO_UDP, NULL);
xTaskCreate(sync_task, "sync", STACK_SYNC, NULL, TASK_PRIO_SYNC, NULL);
xTaskCreate(watchdog_task, "wdg", STACK_WATCHDOG, NULL, TASK_PRIO_WATCHDOG, NULL);
// 启动调度器
vTaskStartScheduler();
while (1) {}
}// sht30.c
#include "sht30.h"
#include "i2c.h"
#define SHT30_ADDR 0x44 << 1
#define CMD_MEAS_HIGH 0x2400 // 高精度模式,15ms转换时间
static float g_temperature = 0.0f;
static float g_humidity = 0.0f;
static uint8_t g_sensor_ready = 0;
int sht30_init(void) {
uint8_t cmd[2] = {0x30, 0xA2}; // 软复位
HAL_I2C_Master_Transmit(&hi2c1, SHT30_ADDR, cmd, 2, 100);
HAL_Delay(1);
// 读取状态寄存器
cmd[0] = 0xF3; cmd[1] = 0x2D;
HAL_I2C_Master_Transmit(&hi2c1, SHT30_ADDR, cmd, 2, 100);
g_sensor_ready = 1;
return 0;
}
int sht30_read(float *temp, float *hum) {
if (!g_sensor_ready) return -1;
uint8_t cmd[2] = {CMD_MEAS_HIGH >> 8, CMD_MEAS_HIGH & 0xFF};
if (HAL_I2C_Master_Transmit(&hi2c1, SHT30_ADDR, cmd, 2, 100) != HAL_OK)
return -1;
HAL_Delay(15); // 等待转换完成
uint8_t rx[6];
if (HAL_I2C_Master_Receive(&hi2c1, SHT30_ADDR, rx, 6, 100) != HAL_OK)
return -1;
// CRC校验
if (!sht30_crc_check(rx, 2, rx[2])) return -2;
if (!sht30_crc_check(rx + 3, 2, rx[5])) return -3;
// 转换
uint16_t temp_raw = (rx[0] << 8) | rx[1];
uint16_t hum_raw = (rx[3] << 8) | rx[4];
*temp = -45.0f + 175.0f * temp_raw / 65535.0f;
*hum = 100.0f * hum_raw / 65535.0f;
g_temperature = *temp;
g_humidity = *hum;
return 0;
}
static uint8_t sht30_crc_check(uint8_t *data, uint8_t len, uint8_t crc) {
uint8_t crc_calc = 0xFF;
for (int i = 0; i < len; i++) {
crc_calc ^= data[i];
for (int j = 0; j < 8; j++) {
if (crc_calc & 0x80)
crc_calc = (crc_calc << 1) ^ 0x31;
else
crc_calc <<= 1;
}
}
return (crc_calc & 0xFF) == crc;
}// sensor_task.c
void sensor_task(void *pvParameters) {
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xPeriod = pdMS_TO_TICKS(5000); // 5秒采样
float temp, hum;
env_record_t record;
while (1) {
vTaskDelayUntil(&xLastWakeTime, xPeriod);
if (sht30_read(&temp, &hum) == 0) {
record.collect_ts = get_corrected_timestamp();
record.temperature = (int16_t)(temp * 100);
record.humidity = (uint16_t)(hum * 10);
record.voltage = (uint16_t)(read_poe_voltage() * 100);
record.status = get_status_bits();
record.reserved = 0;
// 写入本地存储
storage_write_record(&record);
// 通知UDP任务推送
xQueueSend(g_udp_queue, &record, 0);
// 通知Modbus任务更新保持寄存器
xQueueSend(g_modbus_queue, &record, 0);
} else {
// 传感器故障
set_status_bit(STATUS_SENSOR_FAULT);
}
}
}Si3402引脚配置:
Pin 1-4: VNEG (整流输出负)
Pin 5-8: VPOS (整流输出正)
Pin 9: VDD (内部LDO 3.3V)
Pin 10: GND
Pin 11: CLASS (分类电流设置)
Pin 12: SLEW (浪涌限流)
Pin 13: nPWRON (电源就绪指示)
Pin 14: VDD_AUX (辅助供电输入)
Pin 15: VDD (3.3V输出)
Pin 16: GND_PAD (散热地)分类电阻:802.3af Class 1(3.84W~6.49W),R_CLASS = 133Ω
浪涌限流:R_SLEW = 100kΩ,限制浪涌电流
STM32F103: ~75mA × 3.3V = 0.25W
LAN8720A: ~50mA × 3.3V = 0.17W
SHT30: ~1mA × 3.3V = 0.003W
W25Q16: ~15mA × 3.3V = 0.05W
LED: ~10mA × 3.3V = 0.03W
LWIP协议栈: ~30mA × 3.3V = 0.10W
────────────────────────────────────
总计: ~181mA × 3.3V = 0.60WClass 1(最大6.49W)绰绰有余,即使考虑转换效率(约85%),输入功率约0.7W。
现象 | 原因 | 对策 |
|---|---|---|
上电后反复重启 | PoE交换机端口功率不足 | 确认交换机支持802.3af,端口未限流 |
运行中突然掉线 | 网线过长导致电压跌落 | 缩短距离或改用Cat6A |
发热严重 | Si3402散热不良 | 增加散热过孔,GND_PAD焊接良好 |
只有Link灯不亮 | 极性反接 | 检查RJ45线序,T568B标准 |
机柜气流方向:
前进后出 / 下进上出
推荐安装位置:
✅ 机柜后部盲板(热通道侧,监测排风温度)
✅ PDU侧面(监测供电环境)
✅ 机柜顶部(热空气聚集区)
❌ 服务器风扇正前方(气流干扰测量)
❌ 机柜底部(冷通道,不能代表热点)外壳开孔:
┌──────────────────┐
│ ░░░░░░░░░░░░░░ │ ← 顶部开孔阵列(φ1.5mm × 20个)
│ ░░░░░░░░░░░░░░ │ 确保气流通过SHT30传感器
│ ┌──────────┐ │
│ │ SHT30 │ │ ← 传感器位于开孔正下方
│ └──────────┘ │
│ RJ45 │ ← 底部出线
└──────────────────┘注意:开孔面积 ≥ 50mm²,避免形成"死腔"导致温度滞后。
现象 | 原因 | 排查方法 |
|---|---|---|
PHY不Link | RMII时钟不对 | 示波器测REF_CLK 50MHz |
LWIP初始化失败 | PHY地址错误 | 读PHY ID寄存器 |
温度偏高2℃ | 自身发热传导 | 增加隔热垫,降低采样频率 |
I²C读不到数据 | 上拉电阻未焊 | 万用表测SDA/SCL电压 |
Flash写入失败 | 写保护引脚 | 确认WP引脚拉高 |
运行中死机 | 堆栈溢出 | FreeRTOS uxTaskGetStackHighWaterMark |
SHT30出厂精度±0.2℃,但PCB自身发热会影响测量。校准方法:
// 校准系数(通过恒温箱标定)
static float g_temp_offset = 0.0f; // 从Flash读取
float calibrated_temperature(float raw) {
return raw + g_temp_offset;
}
// 标定流程:
// 1. 放入恒温箱(25℃),稳定运行30分钟
// 2. 读取10次平均值
// 3. 计算偏差:offset = 25.0 - avg
// 4. 写入Flash校准区服务器机柜、内部监测、小型RJ45温湿度变送器、嵌入式开发、STM32F103、LAN8720A、SHT30、Si3402、PoE供电、RMII、LWIP、FreeRTOS、PCB布局、机柜安装
小型RJ45温湿度变送器的嵌入式开发核心在于"小体积下的资源平衡":用STM32F103+LAN8720A+SHT30+Si3402四颗核心芯片实现PoE供电、以太网通信、温湿度采集,LWIP在20KB RAM下精简配置跑通Modbus TCP和UDP双协议,PCB 4层板紧凑布局确保RMII信号完整性,机柜内安装位置选在热通道侧避免气流干扰——最终交付的是一个火柴盒大小、即插即用、可批量部署的机柜级环境监测节点。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。