简介:一份基于stm32单片机modbus无线网关系统的完整设计资料,面向嵌入式开发者、电子爱好者及毕业设计学生,解决无线温湿度采集与modbus协议网关搭建的实际需求。系统采用stm32作为核心控制,采集端搭配温湿度传感器与Lora无线模块,终端通过LCD1602实时显示数据,并支持按键设置温湿度阈值,配合声光告警电路,形成完整的监测与报警链路。除源程序外,还提供原理图与PCB图,覆盖电路设计、硬件布局与底层驱动,可直接用于理解硬件连接、调试通信时序或作为二次开发的基础框架。压缩包约440.91MB,包含全套设计文件,目录结构清晰,便于按模块查找。已有122人学习/下载,资料中系统的功能分层与模块划分,能帮助读者快速掌握无线传感器网络、modbus通信及嵌入式人机交互设计要点,适合实战学习与项目参考。
1. 从RS485总线到无线网关:这套STM32设计到底在解决什么问题
一套“STM32单片机Modbus无线网关”要解决的问题很具体:车间里二十多台变频器、温控仪和电能表挂在同一条RS485总线上,上层系统只认Modbus RTU协议,而监控中心在几公里外的机房。RS485传输距离极限约1200米,现场又没有预留网线,最省事的做法不是在每台设备上加串口服务器,而是在总线旁放一台低成本STM32单片机:它当Modbus主站轮询全部从站,再把采集结果经无线链路送出。
这套设计的交付物分三块:原理图解决RS485电平转换、供电与无线模组接法,PCB图保证总线信号完整性和天线性能,源代码负责轮询调度、RTU帧解析、CRC校验和无线AT指令调度。它适合给老旧仪表和变频器做低成本接入的现场工程师,也适合想打通硬件到固件全链路的毕业设计选题者。下文按硬件设计、Modbus协议栈、无线封装、现场验证四个环节展开,每一节都给可直接照搬的参数表和代码。
2. STM32网关硬件设计:无线链路形态、RS485电路与PCB关键约束
2.1 先定无线链路形态,再定STM32型号
网关里“无线”两个字决定整块板子的形态,常见有三种,资源占用和实现难度差别很明显。WiFi或4G直连云平台时,STM32在本地RS485总线上做Modbus主站,无线模组通过串口AT指令工作,TCP/IP协议栈、DNS、TLS全部由模组处理,MCU只关心Modbus和业务调度,固件量最小。LoRa无线延伸则是把一条RS485支线从本地延伸到几百米甚至两公里外的中心站,STM32只做RTU帧的搬移和地址过滤,不解析寄存器里的业务含义。第三种是多串口加以太网,做成标准Modbus RTU转TCP网关,本地还能接人机交互屏,资源要求最高。
对毕业设计和轻量工业应用,第一种形态最稳妥:ESP8266-12F或Air724UG都有成熟AT固件,网络协议栈被挡在MCU外面,问题域集中在Modbus本身和链路可靠性上。此时一颗STM32F103C8T6(72MHz主频、64KB Flash、20KB RAM)就够用;如果固件还要存配置表、断线缓存或OTA升级包,建议直接上F103RCT6,256KB Flash会让固件规划从容很多。
| 形态 | 无线模组 | MCU资源占用 | 典型场景 |
|---|---|---|---|
| A | ESP8266 / Air724UG | USART1+USART2,64KB Flash | 数据上云、远程监控 |
| B | SX1278 LoRa | USART1+SPI | 厂区支线无线化 |
| C | W5500+PHY | 并行/SPI接口 | Modbus TCP组网 |
三种形态的串口分配是固定的:USART1接RS485,USART2接无线模组,一个通用GPIO控制485方向,另一个GPIO做状态LED。这样后续换模组只改AT指令层,Modbus协议栈完全不动。
2.2 RS485收发电路:偏置电阻、终端电阻与方向控制
RS485是半双工差分总线,收发器选SP3485或MAX3485(3.3V供电),不要用5V的MAX485——STM32的IO电平是3.3V,3.3V收发器直接省掉一级电平转换。电路上有四个要点:A、B端子各加一路TVS管和自恢复保险丝,防止接线错误或感应雷直接打坏收发器;120Ω终端电阻只出现在总线物理两端,网关作为其中一端时,在板上预留该电阻位置;A线上拉到VCC、B线下拉到地,典型取值1k到10kΩ,节点越多取值越大,保证空闲时A-B电压差为正,收发器不会把噪声当数据;DE和RE短接后由GPIO控制,发送时拉高,接收时拉低。
2.2.1 方向控制为什么必须看TC标志
485方向切换最容易犯的错是用TXE(发送寄存器空)判断发送结束。TXE置位只代表数据进了移位寄存器,移位还没完成,此时拉低DE会把最后一个字节的高位截掉。正确做法是等TC(发送完成)标志,HAL库的发送完成回调就是TC触发点:
static void rs485_set_dir(uint8_t tx) { HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, tx ? GPIO_PIN_SET : GPIO_PIN_RESET); } void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { rs485_set_dir(0); /* 一帧发完,立刻释放总线 */ __HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE); } } static void modbus_send(uint8_t *frame, uint16_t len) { __HAL_UART_DISABLE_IT(&huart1, UART_IT_RXNE); /* 防自发自收 */ rs485_set_dir(1); HAL_UART_Transmit_IT(&huart1, frame, len); }这里用HAL_UART_Transmit_IT把发送交给中断,TXCpltCallback在最后一个字节真正离开移位寄存器时触发,回调里拉低DE并重新打开接收中断。关闭RXNE是双保险,防止轮询时把自己发出的帧又收进DMA缓存造成误判。这个细节直接决定485链路在200米以上线缆时能否稳定,是现场排查误码的第一步。
2.3 供电与无线模组的原理图要点
无线模组是板上最大的干扰源和功耗源,供电必须单独设计。常见做法是两级电源:24V经过MP1584或MP2451降到5V,再经AMS1117-3.3得到3.3V。ESP8266发射瞬间电流200到300mA,AMS1117从5V降3.3V时压差余量足够;若是从24V直降3.3V,压差过大、电流余量不足,模组发射时会反复复位,表现为WiFi时断时连。
无线模组的引脚处理:ESP8266的CH_PD(使能)必须有10k上拉到VCC;GPIO0默认悬空进入运行模式;模组电源引脚就近放10uF钽电容加100nF陶瓷电容。复位电路用10k上拉加0.1uF对地电容即可。给模组配一颗独立LDO是更稳的做法,能把射频发射时的纹波和MCU电源隔开。
2.4 PCB布局与布线的四条硬约束
原理图之外,PCB才是这套设计真正决定成败的地方,四条硬约束缺一不可。
| 区域 | 要求 | 违反后果 |
|---|---|---|
| RS485差分对 | 并行走线、等长±5mil,两侧包地 | 共模噪声大、误码率高 |
| 晶振与负载电容 | 电容贴近晶振,走线小于10mm | 频偏、起振困难 |
| 天线区域 | 顶层净空、不覆铜、正下方不布线 | 增益下降、连不上网 |
| MCU电源 | 每对VDD/VSS就近100nF | 高速开关噪声导致跑飞 |
晶振负载电容是最容易被抄错的参数。按公式 C = 2×CL − Cstray 计算,8MHz、CL=12pF的晶振,布线杂散电容按3到5pF估算,两个负载电容取16到18pF。电容选错直接导致串口波特率偏移,Modbus报文CRC明明对却收不完整时,先回来查这里。
3. Modbus RTU协议栈:CRC16、收帧状态机与主站轮询超时
3.1 从一帧RTU报文说起:地址、功能码与CRC16
Modbus RTU帧固定分成四段:地址1字节、功能码1字节、数据N字节、CRC16两字节(低字节在前)。网关做主站时最常用的功能码只有四个:0x03读保持寄存器、0x04读输入寄存器、0x06写单个寄存器、0x10写多个寄存器。
| 功能码 | 命令 | 用途 |
|---|---|---|
| 0x03 | 读保持寄存器 | 读变频器设定频率、运行电流 |
| 0x04 | 读输入寄存器 | 读仪表实时采样值 |
| 0x06 | 写单个寄存器 | 单点设定参数 |
| 0x10 | 写多个寄存器 | 批量下发电参数 |
比如主站读1号从站保持寄存器0x0000开始的10个寄存器,发送的报文是01 03 00 00 00 0A C5 CD。从站应以01 03 14 + 20字节数据 + CRC响应。CRC16的多项式是0x8005,按字节反相后就是0xA001,标准实现如下:
uint16_t modbus_crc16(const uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; while (len--) { crc ^= *data++; for (uint8_t i = 0; i < 8; i++) { crc = (crc & 0x0001) ? (crc >> 1) ^ 0xA001 : crc >> 1; } } return crc; /* 发送时低字节在前:先发 crc & 0xFF,再发 crc >> 8 */ }初学者最常犯的错误是把返回值的两个字节顺序发反。RTU规范规定CRC先发低字节,这跟很多上位机工具里的显示顺序相反,对接不上时先做字节交换再查别的。表驱动查表法能把这段计算提速数倍,但Cortex-M3上逐位计算一帧只要几十微秒,协议栈压力不大时保持简单就好。
3.2 用DMA加空闲中断收帧,别在中断里做CRC
Modbus对帧间隔有硬性约束:帧内字符间隔不能超过1.5字符时间,帧间至少3.5字符时间。按8N1格式每字符10bit计算,各波特率下的时间如下:
| 波特率 | 1.5字符时间(ms) | 3.5字符时间(ms) |
|---|---|---|
| 9600 | 1.56 | 3.65 |
| 19200 | 0.78 | 1.83 |
| 38400 | 0.39 | 0.91 |
| 115200 | 0.13 | 0.30 |
注意115200下3.5字符时间只有0.3ms,如果每收一个字节进一次中断,靠软件判断帧尾超时,高优先级任务一打断就漏判。工程上统一用DMA加串口空闲中断,收到完整一帧才交给协议栈处理。工程用STM32CubeMX生成初始化代码(Keil MDK编译),DMA通道和NVIC优先级在CubeMX里直接配好:
#define RX_LEN_MAX 256 static uint8_t rx_dma[RX_LEN_MAX]; void modbus_uart_start(void) { __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); HAL_UART_Receive_DMA(&huart1, rx_dma, RX_LEN_MAX); } void USART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(&huart1); uint16_t len = RX_LEN_MAX - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx); modbus_frame_ready(rx_dma, len); HAL_UART_AbortReceive(&huart1); /* 停掉上一轮DMA */ HAL_UART_Receive_DMA(&huart1, rx_dma, RX_LEN_MAX); } HAL_UART_IRQHandler(&huart1); }空闲中断在总线上没有新字符时触发,此时DMA计数器差值就是本次接收的字节数,正是一帧RTU数据的长度。拿到后拷贝到业务缓存,立刻重配DMA接收下一帧。这套结构在主站模式下同样适用——从站响应帧到达时间不定,DMA不占用CPU。
3.2.1 帧完整性的三段校验
帧到达协议栈后按顺序做三段校验:长度、地址、CRC。长度小于4字节或大于256字节直接丢弃;地址不在本网关关注的从站范围内也丢弃;最后过CRC。收帧状态机只有IDLE、RECV、DONE三态:
typedef enum { F_IDLE, F_RECV, F_DONE } FrameState; static FrameState f_state = F_IDLE; static uint8_t f_buf[256]; static uint16_t f_len; void modbus_frame_ready(const uint8_t *dma, uint16_t len) { if (len < 4) return; memcpy(f_buf, dma, len); f_len = len; f_state = F_DONE; /* 主循环轮询时消费 */ }状态机的价值在于收帧和解析解耦:DMA中断只负责把数据搬到内存,主循环里判断F_DONE后做CRC和业务处理。CRC错误时记录错误计数并回到IDLE,不要立即重发——先把错误计数暴露到状态寄存器,现场就能通过无线链路判断是线路干扰还是从站异常。
3.3 主站轮询表、失败重试与慢速从站
网关作为主站,必须自己决定什么时候问谁。用一张配置表描述所有轮询任务,固件按顺序执行:
typedef struct { uint8_t addr; uint8_t func; uint16_t reg; uint16_t cnt; uint16_t timeout_ms; /* 单次应答等待 */ uint8_t retry; /* 失败重试次数 */ } PollItem; static const PollItem poll_tbl[] = { {0x01, 0x03, 0x0000, 10, 100, 2}, /* 配电柜电能表 */ {0x02, 0x04, 0x0000, 20, 150, 2}, /* 温控仪 */ {0x03, 0x03, 0x2000, 2, 200, 1}, /* 变频器 */ };轮询表的意义在于把“问谁、问什么、等多久”从代码里抽出来,现场加一台从站只改配置。超时参数按设备类型给——普通仪表50到100ms能应答,老款变频器重负载时要到200ms,统一用100ms会让一部分设备频繁触发重试。整轮轮询间隔建议取单站响应时间的两倍再加帧间间隔,十个从站左右整轮控制在1到2秒比较合理。
注意:RS485总线上一个时刻只能有一个主站。网关做主站时,不要再接另一个上位机或手持调试器同时轮询,两个主站的问询帧会互相冲突,两边都会超时。
4. 无线链路封装:从RTU帧到Modbus TCP、MQTT与LoRa
4.1 把RTU载荷装进MBAP头,变成Modbus TCP帧
无线侧的数据最终要给谁,决定封装方式。监控后台用组态软件时,最省事的是让网关直接以Modbus TCP对外服务。Modbus TCP和RTU的差异集中在帧头:
| 字段 | Modbus RTU | Modbus TCP |
|---|---|---|
| 从站地址 | 1字节 | Unit ID,1字节 |
| 校验 | 帧尾CRC16 | 无,TCP负责 |
| 附加头 | 无 | MBAP 7字节 |
MBAP头包含事务ID(2字节,应答时原样带回)、协议ID(2字节,固定0x0000)、长度(2字节)。封装函数很直接:
static uint16_t modbus_tcp_wrap(uint8_t *tcp, uint16_t tid, const uint8_t *rtu, uint16_t rtu_len) { tcp[0] = tid >> 8; /* Transaction ID 高字节 */ tcp[1] = tid & 0xFF; tcp[2] = 0x00; /* Protocol ID = 0 */ tcp[3] = 0x00; uint16_t pdu_len = rtu_len - 3; /* 去掉RTU地址和CRC */ tcp[4] = (pdu_len + 1) >> 8; /* 长度=UnitID+PDU */ tcp[5] = (pdu_len + 1) & 0xFF; tcp[6] = rtu[0]; /* Unit ID */ memcpy(&tcp[7], &rtu[1], pdu_len); /* Function + Data */ return pdu_len + 7; }Length字段表示它后面还有多少字节,必须加1把Unit ID算进去。事务ID由网关维护,收到TCP应答时核对事务ID能区分多路请求。以读保持寄存器为例,RTU帧01 03 00 00 00 0A C5 CD去掉地址和CRC后封装,得到的TCP帧是00 01 00 00 00 06 01 03 00 00 00 0A,共12字节。
4.2 ESP8266的AT指令时序:TCP与MQTT两条路
模组固件成熟后,MCU侧就是发一串AT命令。TCP直连方式的命令序列:
AT+RST AT+CWMODE=1 AT+CWJAP="SSID","PASSWORD" AT+CIPSTART="TCP","192.168.1.100",1883 AT+CIPSEND=12 > 00 01 00 00 00 06 01 03 00 00 00 0ACIPSEND后面的数字必须和实际发送字节数严格一致,发多或发少都会让链路层错乱。所有AT命令都要配超时处理,ESP8266偶发无应答,挂起3秒没回就复位模组重新初始化,比死等要可靠得多。
直连MQTT云平台时,用AT固件拼MQTT报文容易出错,几个字段错一个连接就失败。更稳的做法是用带MQTT AT指令的固件,AT+MQTTCONN和AT+MQTTPUB把CONNECT、PUBLISH全部封装好,MCU端只维护topic和payload。采集数据JSON化示例:
{"gw":"A001","ts":1717234560,"slave":3,"reg":"2000","val":38.5}topic按gw/A001/rt上报,一个寄存器约6字节开销,10个从站每分钟上报一次,一年流量不到100MB。注意上报周期和本地轮询周期解耦:本地保持1秒轮询,上报可以30秒或1分钟一次,减少云端带宽和存储压力。
4.3 没有公网环境时,用LoRa做RS485的无线延长线
如果监控中心就在厂区内、只是中间隔了几栋厂房,LoRa比上云更合适。SX1278工作在433或470MHz,空旷环境能达到1到3公里,穿厂房也有几百米。此时结构变成两个端点:采集侧RS485接从站总线,STM32做透传,把RTU帧原封不动放进LoRa数据包;汇聚侧收到数据包后再灌回一条RS485总线,或直接转成Modbus TCP给上位机。
关键参数上,LoRa有效数据速率只有几十kbps以内,为了不丢帧,Modbus波特率要降到9600,从站响应超时从100ms加大到300到500ms。多个采集侧共用同一信道时,LoRa模组配不同地址字节做过滤,避免采集侧A发到采集侧B造成数据污染。
注意:LoRa是半双工信道,Modbus轮询本身是一问一答,天然适配半双工。不要在轮询过程中并发上传大块日志,碰撞重传会拖慢整个链路。
5. 网关稳定性的三个现场技巧:交叉验证、看门狗与断线补发
5.1 先分离问题:链路还是协议栈
网关写完代码不要直接上总线。先用USB转TTL接STM32的USART1,用串口助手发01 03 00 00 00 0A C5 CD,确认从站有应答;再接上RS485,用QModMaster这类开源工具或Modbus Poll官方试用版做上位机主站,把波特率、校验位、从站地址配好,轮询间隔设500ms。两次都通,说明物理链路和协议栈都没问题;只通一次,问题就定位到另一半。工具优先选择协议栈行为透明的开源方案,排查时能直接看到原始报文。
5.2 独立看门狗与AT重连状态机
IWDG超时设为1秒,主循环每个分支末尾喂狗。Modbus收到错误帧不影响喂狗,但不允许任何分支阻塞超过1秒。ESP8266的AT命令全部走3秒超时,超时后进入重连状态,30秒后再试CIPSTART。重连期间本地485轮询不能停,数据要一直采集——网关的价值就在于网络断时本地还能继续记录,恢复后补传。
5.3 断线窗口数据补发,别让时间轴出现空洞
断线期间把每轮寄存器快照连同本地时间戳写进RAM环形队列,链路恢复后按顺序补发,云端按时间戳合并,避免监控曲线出现空洞。补发用紧凑二进制格式而不是JSON:帧头1字节长度、4字节时间戳、后面跟寄存器原始值,相同带宽能塞进约3倍数据。帧头里固定放格式版本号,升级固件后新旧网关并存时按版本号选择解析方式。
现场验收时最后一步:把逻辑分析仪同时挂在485的A/B端子和无线模组的UART上,连续抓10分钟。CRC频繁错,先量120Ω终端电阻和公共地电位差;帧间隔忽大忽小,先数总线上从站数量是否超过负载上限——这两处查完再考虑改代码。
本文还有配套的精品资源,点击获取