ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

# STM32F407 工业物联网网关:Modbus 采集 + W5500 MQTT 上云 + LCD 仪表盘

# STM32F407 工业物联网网关:Modbus 采集 + W5500 MQTT 上云 + LCD 仪表盘 完整源码https://github.com/uiuiiii1/stm32f407-iot-gateway基于 STM32F407VGT6 W5500 Modbus RTU 的工业物联网网关含 HAL 库版和标准库版两套工程共用同一套自写驱动仅库层与工程组织不同。一、项目概述这是一个工业物联网网关的完整实现温湿度变送器 ──RS485(Modbus RTU)──┐ │ USART2 ┌─────────────▼─────────┐ LCD 仪表盘 ◄──SPI3──┤ STM32F407VGT6 ├──SPI2──W25Q64 Flash(断网缓存/OTA用) └─────────────┬─────────┘ │ SPI1 ┌───────▼───────┐ │ W5500 模块 │──网线── 路由器 ── MQTT Broker └───────────────┘ (broker.emqx.io:1883)核心功能RS485 采集温湿度变送器数据Modbus RTU 主站W5500 硬件 TCP/IP 协议栈经 MQTT 上云LCD 1.54 寸仪表盘本地可视化全套健壮性机制模块在位检测、配置自愈、链路防抖、全链路超时保护两个工程版本功能完全一致HAL库工程/CubeMX 图形化配置已上板验证标准库基础工程/SPL V1.8.0手工搭建两者共用同一套自写驱动Modbus / MQTT / W5500 BSP / LCD仅库层与工程组织不同可对照阅读两种写法。二、硬件清单部件型号/说明主控鹿小班 LXB407VG-P1 核心板STM32F407VGT6HSE 8MHz板载 RTC 晶振电池座以太网USR-ES1W5500硬件 TCP/IP 协议栈温湿度RS485 导轨式变送器Modbus RTU 从站DC 5~28V 供电显示1.54 寸 TFT 240×240ST7789SPI存储W25Q64 模块SPI Flash8MB断网缓存/OTA 用RTC板载 32.768kHz 晶振 CR1220 电池座F407 内部 RTC外部 DS1307 模块经排查确认故障已弃用三、接线W5500USR-ES1SPI1模块引脚核心板说明VIN3.3 / GND×2 都要接3.3V / GND3.3V 供电峰值 200mASCLK / MISO / MOSIPA5 / PA6 / PA7SPI1AF5nSSPC4软件片选低有效nRSTPC5复位低有效拉低500µs→释放等≥50msPWDN模块内部接地无需外接nINT不接轮询方式Modbus 温湿度USART2 SP3485SP3485核心板/变送器说明VCC3.3V3.3V 版收发器RX / TXPA2(TX) / PA3(RX)交叉连接DERE短接PA4高发送低接收A / B变送器 A / B不通就对调各厂家命名不统一变送器 VCC / GND5V / GND宽压 5~28V1.54 寸 TFTSPI3FPC 8P 座屏引脚核心板屏引脚核心板SCKPB3CSPA15MOSIPB5DCPD13RST板载 RC 复位BLPD12调试串口CH340USART1CH340 TXD→PA10、RXD→PA9、GND 共地。CH340 的 3.3V/5V 不要接只留三根线。供电与注意事项重要单电源同源所有模块的 3.3V/GND 必须从核心板引脚取DCDC 同源短地线。外部面包板电源与核心板混供时地电位弹跳会导致 SPI 误码、以太网链路反复断开——本项目实测踩坑器件电平W5500/W25Q64/SP3485/TFT 均 3.3VDS1307 已弃用若接必须 5V变送器 5V 宽压3.3V 轨容量W5500 峰值 200mA建议模块 VIN3.3/GND 就近并联 470µF 电解电容网线接路由器 LAN 口MQTT 出公网需要静态 IP192.168.0.250需避开路由器 DHCP 池——换 IP 后先断电 ping 验证地址空闲再上电防 IP 冲突Modbus 通信帧内温度为 16 位补码0xFF9B-101→-10.1℃寄存器 0x0000湿度、0x0001温度×10四、编译与烧录环境Keil MDK 5.x ARM Compiler 5V5.06器件包 Keil.STM32F4xx_DFP固件库STM32F4xx_StdPeriph_DriverV1.8.0已随仓库包含勿用 V1.9——官方确认其 stm32f4xx.h 有宏定义 bug编译打开标准库基础工程/基础项目.uvprojx→ F7。已配置AC5、MicroLIB、HEX 输出、_WIZCHIP_5500、静态 IP192.168.0.250stm32f4xx_fmc.c已从编译组排除F427/429 专用F407 编不过烧录ST-LinkSWDSWDIOPA13/SWCLKPA14Keil 直接 F8 下载Flash Download 勾选 Reset and Run串口 ISPObjects/基础项目.hex FlyMcuBOOT0 按住进 ISP → 复位 → 下载 → BOOT0 回低 → 复位五、上电验证步骤LCD 出现仪表盘框架Link: UP绿——网线链路建立MQTT: ONLINE绿——MQTT 已连接 broker.emqx.ioTemp/Hum出现数值并每 2 秒刷新——Modbus 采集正常PUB: n每 5 秒 1——发布正常电脑同一局域网ping 192.168.0.250应稳定 1~5msNetAssist/MQTTX 连broker.emqx.io:1883订阅gateway/gw001/data每 5 秒收到 JSON{device:gw001,temp:22.4,hum:35.6}串口115200-8-N-1输出运行日志发送s可设置 RTC 基准时间六、代码架构主循环结构main.c的主循环结构非常清晰while(1){/* 1. 心跳灯每2秒滴一下区分启动成功和故障播报 *//* 2. 模块在位检测连续5次版本读失败才判离线 *//* 3. 配置自愈连续3次校验不一致才重下发 *//* 4. 链路状态监测防抖3次才关socket *//* 5. MQTT 状态机连接/保活/自动重连 *//* 6. 每5秒发布 JSON *//* 7. 每2秒 Modbus 读温湿度并刷新仪表盘 */Delay_ms(10);}Modbus 主站驱动modbus.cRS485 半双工 功能码 03 读保持寄存器/* CRC16多项式 0xA001初值 0xFFFF附加时低字节在前 */staticuint16_tMB_CRC16(constuint8_t*data,uint16_tlen){uint16_tcrc0xFFFF;for(uint16_ti0;ilen;i){crc^data[i];for(uint16_tj0;j8;j){if(crc0x0001){crc1;crc^0xA001;}elsecrc1;}}returncrc;}/* DE 置高发送整帧。HAL_UART_Transmit 阻塞到 TC 置位才返回 * 所以返回后立刻拉低 DE 不会截断帧尾 */staticvoidMB_SendFrame(constuint8_t*d,uint16_tlen){MB_DE_Send();HAL_UART_Transmit(huart2,(uint8_t*)d,len,MB_SEND_TIMEOUT_MS);MB_DE_Recv();}/* 读保持寄存器功能码 03失败自动重试 2 次共 3 次尝试 */uint8_tMODBUS_ReadRegs(uint16_tregStart,uint16_tcount,uint16_t*out){for(attempt0;attempt3;attempt){MB_FlushRx();MB_SendFrame(tx,8);for(i0;irxLen;i){errMB_RecvByte(rx[i]);if(err)break;}if(errMB_OK){// 校验地址、功能码、字节数、CRC// 通过则提取寄存器值}}returnerr;}/* 读温湿度寄存器 0x0000湿度(×10)0x0001温度(×10 补码) */uint8_tMODBUS_ReadTempHum(int16_t*temp10,uint16_t*hum10){uint16_tregs[2];MODBUS_ReadRegs(MB_REG_START,MB_REG_COUNT,regs);*hum10regs[0];*temp10(int16_t)regs[1];/* 0xFF9B - -101 - -10.1℃ */}关键设计3 次重试Modbus 通信不稳定单次失败不致命ORE 溢出清除MB_FlushRx顺带清 ORE 标志避免下次读取失败补码处理温度是 16 位有符号数-10.1℃ 在寄存器里是 0xFF9BW5500 BSPw5500_bsp.c硬件复位时序 配置自愈 版本检测voidW5500_BSP_Init(void){uint8_ttxSize[8]{2,2,2,2,2,2,2,2};/* 8个Socket收发缓冲各2KB共16KB */uint8_trxSize[8]{2,2,2,2,2,2,2,2};bsp_cs_deselect();HAL_GPIO_WritePin(W5500_RST_GPIO_Port,W5500_RST_Pin,GPIO_PIN_SET);/* 硬件复位时序拉低500us - 释放 - 等50msPLL稳定 */HAL_GPIO_WritePin(W5500_RST_GPIO_Port,W5500_RST_Pin,GPIO_PIN_RESET);Delay_ms(2);HAL_GPIO_WritePin(W5500_RST_GPIO_Port,W5500_RST_Pin,GPIO_PIN_SET);Delay_ms(50);reg_wizchip_cs_cbfunc(bsp_cs_select,bsp_cs_deselect);reg_wizchip_spi_cbfunc(bsp_spi_read,bsp_spi_write);wizchip_init(txSize,rxSize);}/* 配置自愈校验只读不改读回芯片的源IP寄存器SIPRCommon块0x000F与配置区比对。 * 芯片被意外复位干扰/电源抖动会丢失全部网络配置PHY不受影响仍显示Link。 * 返回1配置完好0不一致是否恢复由调用方防抖决定避免单次坏读触发重配 */uint8_tW5500_CheckConfig(void){uint8_tcur[4];constuint8_twant[4]W5500_IP;WIZCHIP_READ_BUF(SIPR,cur,4);if(cur[0]!want[0]||cur[1]!want[1]||cur[2]!want[2]||cur[3]!want[3]){return0;}return1;}/* 读版本寄存器VERSIONRCommon块偏移0x0039固定值0x04 * ⚠️ 注意地址是0x0039不是0x0000——0x0000是模式寄存器MR复位值0x00 * 曾因读错地址把芯片正常应答MR0x00误判为模块离线 * SPI帧格式[地址H0x00][地址L0x39][控制字节(Common块3 | 读 | VDM00)] */uint8_tW5500_ReadVersion(void){uint8_tver;bsp_cs_select();(void)SPI1_RW(0x00);/* 地址高字节 */(void)SPI1_RW(0x39);/* 地址低字节VERSIONR0x0039 */(void)SPI1_RW(0x00);/* 控制字节 */verSPI1_RW(0xFF);bsp_cs_deselect();returnver;}MQTT 客户端mqtt.c精简 MQTT 3.1.1 状态机uint8_tMQTT_Process(void){switch(state){caseMQ_IDLE:/* 重连间隔控制公共服务器禁止高频重连2s */if((uint32_t)(GetTick()-tRetry)2000)break;tRetryGetTick();subState0;tStepGetTick();stateMQ_TCP_OK;/* fallthrough立即进入 MQ_TCP_OK 首步节省一轮 */caseMQ_TCP_OK:if(subState0)/* 步骤1建立 TCP 连接 */{if(getSn_SR(MQTT_SOCK)SOCK_CLOSED){// DNS 解析可选localPort;if(socket(MQTT_SOCK,Sn_MR_TCP,localPort,0x00)!MQTT_SOCK){close(MQTT_SOCK);stateMQ_IDLE;break;}if(connect(MQTT_SOCK,brokerIp,MQTT_BROKER_PORT)!SOCK_OK){close(MQTT_SOCK);stateMQ_IDLE;break;}tStepGetTick();}elseif(getSn_SR(MQTT_SOCK)SOCK_ESTABLISHED){// 组装 CONNECT 包并发送// CONNECT 0x10 | 剩余长度 | 协议名 | 协议级别 | 连接标志 | KeepAlive | ClientID}elseif((uint32_t)(GetTick()-tStep)MQTT_TCP_TIMEOUT){// connect 超时close(MQTT_SOCK);stateMQ_IDLE;}}elseif(subState1)/* 步骤2等 CONNACK */{// 收 CONNACK 0x20 02 00 00// 第3字节非0拒绝}break;caseMQ_MQTT_OK:/* TCP 断开监视 */if(getSn_SR(MQTT_SOCK)!SOCK_ESTABLISHED){close(MQTT_SOCK);stateMQ_IDLE;break;}/* keepalive每 30s 发一次 PINGREQ */if((uint32_t)(GetTick()-tPing)(MQTT_KEEPALIVE*1000/2)){uint8_tping[2]{0xC0,0x00};SendAll(ping,2);}break;}return(uint8_t)state;}/* 发布 PUBLISHQoS0 */uint8_tMQTT_Publish(constchar*topic,constuint8_t*payload,uint16_tlen){// 组装包PUBLISH 0x30 | 剩余长度 | Topic | Payload// QoS0 无报文ID不需要确认}关键设计两级状态机TCP 连接 → MQTT CONNECT → 在线重连间隔 2s公共服务器禁止高频重连KeepAlive 30sMQTT_KEEPALIVE60s超 90s 不发 PINGREQ 会被踢端口递增localPort避免 TIME_WAIT 复用DNS 解析可选编译时开关可回退到静态 IP时钟配置SystemClock_ConfigvoidSystemClock_Config(void){// HSE 8MHz × PLL 168MHz// PLLM8, PLLN336, PLLP2 - SYSCLK 8MHz/8*336/2 168MHz// APB1 42MHz, APB2 84MHz/* 手工修改CubeMX 重新生成会覆盖需重打 * LSE(32.768K) 起振失败时不让整板卡死在 Error_Handler —— 标准库工程从来不开 LSE * 所以同一块板子标准库能跑、HAL 版全黑最大嫌疑就是这里。 * 失败则降级关 LSE、开 LSI 做 RTC 时钟RTC 仍可走时只是断电不保持。 */if(HAL_RCC_OscConfig(RCC_OscInitStruct)!HAL_OK){RCC_OscInitStruct.OscillatorTypeRCC_OSCILLATORTYPE_HSE|RCC_OSCILLATORTYPE_LSI;RCC_OscInitStruct.LSEStateRCC_LSE_OFF;RCC_OscInitStruct.LSIStateRCC_LSI_ON;if(HAL_RCC_OscConfig(RCC_OscInitStruct)!HAL_OK){Error_Handler();}}}错误处理Error_HandlervoidError_Handler(void){/* 初始化失败PC0 LED 快闪报错寄存器直写不依赖外设初始化结果 * 不静默死循环——避免程序没跑起来和初始化失败分不清。 */Fault_Blink();}心跳灯 vs 故障灯心跳灯每 2 秒只点一下约 80ms其余全灭——看到这个稀疏的滴—— 启动全部成功、主循环在跑故障灯0.7s 长亮 连续几下短闪——看到这个循环 故障播报去数两组各几下七、健壮性机制这个项目的亮点在于全套健壮性设计不是简单跑通就完事1. 模块在位检测防误判/* 模块在位检测W5500未供电时MISO上拉会把寄存器读成0xFF链路读数全不可信。 * 连续5次坏读才判定离线——单次SPI坏读数据收发时的电源毛刺不允许杀死TCP连接 */if(W5500_ReadVersion()!0x04){verFail;if(verFail5)/* 瞬时坏读不动socket快速重试保住TCP */{Delay_ms(10);continue;}/* 连续5次失败确认离线 */// ... 关 socket显示 OFF}踩坑记录单次状态读数毛刺就 close TCP 连接 → 加防抖连续 N 次才动作2. 配置自愈防芯片复位丢失配置/* 配置自愈防抖芯片被意外复位干扰/电源抖动会丢失IP配置但PHY仍显示Link。 * 连续3次校验不一致才重新下发——单次SPI坏读不触发重配 */if(W5500_CheckConfig()0){if(cfgFail3)cfgFail;if(cfgFail3){cfgFail0;link0xFF;/* 芯片复位过强制刷新链路显示 */W5500_NetworkInit();}}elsecfgFail0;3. 链路防抖防 PHY 毛刺/* 链路状态监测变化才刷屏/打印 */linkNowW5500_LinkStatus();if(linkNow!link){linklinkNow;LCD_DisplayLink(link);printf(PHY %s\r\n,link?LinkUp:LinkDown);}if(!link){/* 防抖连续3次(~30ms)读都断才关socket——单次PHY读数毛刺不杀TCP连接 */if(linkDownCnt3){linkDownCnt;Delay_ms(10);continue;}linkDownCnt0;close(MQTT_SOCK);/* 链路确认断开关socketMQTT状态机自会重连 */Delay_ms(200);continue;}4. 全链路超时保护SPI1 超时HAL_SPI_TransmitReceive(hspi1, tx, rx, 1, 100)100ms 超时Modbus 字节超时50msModbus 帧超时100msMQTT TCP 连接超时5sMQTT CONNACK 超时5s踩坑记录硬件等待循环必须有超时SPI 等待标志位死等 → 异常电平时整机冻结5. 离线期节拍前移/* MQTT 离线 */else{LCD_DisplayMqtt(0);/* 离线期节拍基准前移避免恢复在线瞬间连发 */tPubGetTick();}6. 错误时不静默死循环voidError_Handler(void){/* 初始化失败PC0 LED 快闪报错寄存器直写不依赖外设初始化结果 * 不静默死循环——避免程序没跑起来和初始化失败分不清。 */Fault_Blink();}八、踩坑记录这些是项目实际踩过的坑整理出来给后人参考IP 地址冲突ping 通了但延迟 300~700ms 且时断时续——断电板子后仍能 ping 通 地址被局域网其他设备DHCP 分配占用。静态 IP 设备上电前必须先断电 ping 验证地址空闲诊断代码读错寄存器W5500 VERSIONR 在 0x0039误读成 0x0000MR复位值 0x00——模块离线结论全是误报排查方向被自己的诊断代码带偏。教训诊断代码的寄存器地址必须先对照数据手册F4 外设初始化漏配 GPIO_PinAFConfig光配 GPIO_Mode_AF 不够必须写 AFR 路由到外设否则引脚停在 AF0SPI2 翻车实例硬件等待循环必须有超时SPI 等待标志位死等 → 异常电平时整机冻结模块离线时禁用 ioLibrary socket API寄存器读回 0xFF 使 close() 死等 Sn_CR 清零 → 冻结单次采样不做重大决策单次状态读数毛刺就 close TCP 连接 → 加防抖连续 N 次才动作单电源同源模块 3.3V/GND 从不同电源取电 长地线 → 地弹跳毁掉 SPI 信号、USB 靠近电脑就断链改配置时显示文本与生效数值是两处改了 IP 显示字符串、漏了数字宏 → 屏幕谎报 IP九、目录结构├── 标准库基础工程/ Keil 工程本仓库主体 │ ├── Start/ 启动文件 CMSIS │ ├── Library/ STM32F4 标准库 V1.8.0fmc.c 已排除 │ ├── Hardware/ 板级驱动 │ │ ├── usart.c/h 调试串口 printf 重定向 │ │ ├── delay.c/h SysTick 毫秒延时 GetTick() │ │ ├── lcd_spi_154.c/h 1.54寸 ST7789 驱动商家例程移植 │ │ ├── modbus.c/h Modbus RTU 主站USART2RS485 │ │ ├── spi2.c/h w25q64.c/h SPI Flash断网缓存/OTA用 │ │ ├── i2c.c/h ds1307.c/h I2C/RTCDS1307 故障已弃用保留备用 │ │ ├── rtc.c/h F407 内部 RTC当前时间源 │ │ ├── w5500_bsp.c/h W5500 BSPSPI1复位时序回调注册配置自愈 │ │ └── mqtt.c/h 精简 MQTT 3.1.1 客户端QoS0 │ ├── User/main.c 应用层仪表盘 状态机 健壮性逻辑 │ ├── Ethernet/ WIZnet ioLibrarysocket/wizchip_conf/W5500 │ └── 测试程序存档/ DS1307AT24C32 诊断程序存档 ├── HAL库工程/ HAL CubeMX 生成工程已上板验证 └── docs/ ├── 工业网关项目开发计划.md 完整开发计划阶段0~13 ├── LXB407VG-P1原理图.pdf 核心板原理图 └── 简报/ 阶段任务简报Modbus/MQTT十、相关文档开发计划阶段0~13 全流程docs/工业网关项目开发计划.md核心板原理图docs/LXB407VG-P1原理图.pdf阶段任务简报docs/简报/源码地址https://github.com/uiuiiii1/stm32f407-iot-gateway
返回列表