
第一讲:如何实现现场采集网关(1)
第一讲:如何实现现场采集网关(1)
项目地址(对应分支)
仓库主页:
https://github.com/AIITVisionLab/shihu
本讲对应分支:
https://github.com/AIITVisionLab/shihu/tree/feat/stm32f429-modbus-gateway
一、什么是Modbus-RTU协议?
很多人第一次接触 Modbus,会把它理解成“串口协议”。 这句话不能说错,但太粗糙了。
更准确地说,Modbus-RTU 不是单独的一根串口线,而是一套建立在串行链路上的通信规则。
它至少要分成下面几层来看:
- 物理层:线怎么接,电平怎么传,方向怎么切
- 帧层:一帧从哪里开始,到哪里结束,CRC 怎么算
- 协议层:地址、功能码、寄存器、异常响应是什么意思
- 事务层:主站如何发请求、等响应、超时、重试、判定失败
- 应用层:这一帧数据在项目里到底代表什么
如果不分层,后面一切都会搅在一起。 而这套 F429 采集网关,恰恰就是按这个思路一点一点往上搭起来的。
二、物理层:为什么 Modbus-RTU 常常跑在 RS485 上
1. RS485 不是协议,它是电气接口
首先要明确一点:
RS485 不是 Modbus。
RS485 解决的是“电怎么传”的问题; Modbus-RTU 解决的是“这些字节是什么意思”的问题。
也就是说:
RS485负责差分传输、抗干扰、多点挂接;Modbus-RTU负责主从问答、帧格式、地址、功能码、CRC。
在工业现场,这两者经常绑定出现,所以很容易被混成一句话。 但在设计代码时,必须把这两件事分开。
2. 为什么工业现场爱用 RS485
RS485 最典型的特点有三个:
(1)差分传输
它不是靠单根线对地传电平,而是靠两根线之间的电压差来表示逻辑状态。 这意味着它对共模噪声更不敏感,抗干扰能力比普通 TTL 串口强得多。
(2)适合总线型多点挂接
多个设备可以挂在同一条总线上,形成“一个主站 + 多个从站”的拓扑。 这正好契合 Modbus-RTU 的主从模式。
(3)适合较远距离、较复杂现场
工业环境里电机、继电器、泵、风扇、电磁阀很多,噪声重、线长长, 这时候直接用普通 UART 电平做远距离多节点通信通常不现实,RS485 就很合适。
3. 这个项目里,RS485 具体承担什么角色
在当前工程的 F429 采集网关里,RS485 不是一个“附带接口”,而是采集链路的核心入口。
当前分支文档已经把这条链路讲得很明确:
- 主控:
STM32F429 - 角色:
Modbus-RTU主站 - 下挂:多个
STM32F103从站,以及一个PLC控制从站 - 串口:
USART2 - 引脚:
PD5/PD6+PB8(DE/RE) - 帧间定时:
TIM6 - 串口格式:
9600 8E1
F429 通过 USART2 收发字节,通过 PB8 控制 RS485 芯片的发送/接收方向,通过 TIM6 做 RTU 规定里的帧间隔计时。
换句话说:
USART2负责“字节流”PB8(DE/RE) 负责“半双工方向切换”TIM6负责“RTU 帧边界判定”
这三个部件合在一起,才构成真正可用的 Modbus-RTU 物理入口。
4. 半双工是当前层级里最重要的现实约束
在二线 RS485 里,一个非常关键的现实约束是:
同一时刻,总线上只能有一个方向在说话。
这就是半双工。
所以对主站来说,一次完整事务的最底层动作,通常都是这样的:
- 1.切到发送态;
- 2.把请求帧发出去;
- 3.等发送移位完成;
- 4.切回接收态;
- 5.等从站响应;
- 6.若超时或 CRC 错误,则按失败处理。
这意味着:
- 方向控制不能乱切;
- 发送结束的时机必须判断准;
- 接收窗口必须在正确时间打开;
- 帧边界必须靠定时规则确认,而不是靠“猜”。
所以你后面用 PB8(DE/RE) + TIM6 T3.5,本质上不是“为了写得复杂”,而是因为 RTU 在半双工总线上本来就必须这样严谨处理。
三、帧层:Modbus-RTU 的一帧到底长什么样
1. RTU 不是“见到几个字节就收下”
Modbus-RTU 和很多“长度固定的私有串口协议”不一样。
它的帧边界,并不是靠一个固定帧头比如 0x55AA 来识别,而是靠:
- 字节内容
- 功能码语义
- CRC
- 时间间隔
共同定义。
这也是为什么 RTU 实现里,定时器不是可有可无的点缀,而是协议的一部分。
2. RTU 帧的基本结构
从逻辑上讲,一帧 Modbus-RTU 可以看成这样:
+----------+-----------+------------+-----------+
| 地址(1B) | 功能码(1B) | 数据区(NB) | CRC16(2B) |
+----------+-----------+------------+-----------+
其中:
(1)从站地址
告诉总线上“这帧是发给谁的”。
(2)功能码
告诉对方“我要做什么”。
例如:
0x03:读保持寄存器0x06:写单个保持寄存器0x10:写多个保持寄存器
(3)数据区
不同功能码的数据区含义不同。 比如读寄存器请求里,通常会包含“起始地址 + 数量”; 响应里则会包含“字节数 + 实际寄存器内容”。
(4)CRC16
用于检查这一帧在传输过程中有没有被破坏。 如果 CRC 对不上,这帧就应当被当作无效帧处理。
3. 一个最典型的例子:读保持寄存器、
假设主站要从某个从站读取一段保持寄存器,那么请求的大致语义就是:
从站地址 = 目标设备
功能码 = 0x03
起始地址 = 从哪里开始读
寄存器数 = 读多少个
CRC = 对前面所有字节做校验
从站返回时,语义则会变成:
从站地址 = 自己是谁
功能码 = 0x03
字节数 = 后面数据有多少字节
数据区 = 实际寄存器值
CRC = 对前面所有字节做校验
这就是一个完整的“问—答”闭环。
4. RTU 帧边界为什么要靠时间来判定
RTU 有一个非常经典、也非常容易被忽略的规则:
帧与帧之间需要满足最小静默间隔。
通常会提到两个量:
T1.5:字符间间隔判定参考T3.5:帧间静默时间判定参考
它的意义是这样的:
如果你正在接收一帧,中间长时间没再收到新字节,就要怀疑这帧是否已经结束;
如果总线静默超过 T3.5,可以认为前一帧已经真正结束,新的一帧可以开始。
这正是你项目里用 TIM6 做 T3.5 的根本原因。
不是“刚好有个定时器可用”,而是 RTU 本来就需要用时间来认帧。
四、协议层:地址、寄存器、功能码,到底在说什么
1. Modbus 的核心不是“发字节”,而是“读写寄存器”
Modbus 最适合做的一类事,就是:
- 读某个设备内部的状态值;
- 写某个设备内部的控制值。
也就是说,它天然适合“现场设备数据采集”和“少量控制命令下发”。
这正是 F429 采集网关选择它的原因。
2. 当前项目里,寄存器已经被清楚映射出来了
当前分支文档里,寄存器映射已经明确给出:
- 从站1:
40001-> 光照 ADC
- 从站2:
40011-> 温度40012-> 湿度
- 从站3:
40021-> MQ2 ppm
- 从站4:
40031~40038-> 状态区40101~40108-> 控制命令写区
这说明你的 Modbus 链路并不是“先把协议做出来再说”,而是已经和具体业务对象绑定起来了:
- 环境量走读寄存器
- 状态量走读状态区
- 控制命令走写寄存器区
也就是说,协议层和应用层之间已经有了明确映射。
3. 功能码为什么重要
功能码的存在,决定了“同样是寄存器地址,主站此刻到底是要读还是要写”。
在当前场景里,最重要的不是功能码种类有多全,而是:
- 读功能要稳定;
- 写功能要可控;
- 异常响应要能被识别;
- 事务失败要能归因。
工业系统不怕协议简单,怕的是行为不清楚。
4. 异常响应也属于协议的一部分
很多初学者会只盯着“正常响应”,但真正能把系统做稳的,往往是你怎么处理异常。
Modbus 异常响应有一个很重要的规则:
如果从站无法正常处理请求,它会返回异常功能码。
常见形式是:
异常功能码 = 原功能码 | 0x80
后面再跟一个异常码。
这意味着主站不能只判断“有没有收到数据”,还要判断:
- 地址对不对;
- 功能码是否是正常响应;
- 是否是异常响应;
- CRC 是否正确;
- 数据长度是否匹配预期。
这就是为什么文档里专门区分了:
CRCPROTOCOLEXCEPTIONPORTTIMEOUT
因为这几类失败,根本不是一回事。
五、事务层:为什么主站不是“轮着发包”这么简单
1. 主站真正做的是事务管理
站在代码角度,主站并不是“把一串字节发出去”就完事了。 它真正做的是一整套事务控制:
- 组请求帧;
- 切发送方向;
- 发出请求;
- 等待发送完成;
- 切回接收;
- 等待响应;
- 判定帧结束;
- 做 CRC 校验;
- 解析功能码和数据区;
- 成功则交给上层,失败则超时/重试/记错。
所以,一个成熟的主站实现,天然就应该分层。 否则所有逻辑都塞在一个函数里,后面几乎必然不可维护。
2. 当前的设计,其实已经很成熟了
分支文档里,可以看到当前实现的几个关键原则:
(1)总线上固定只保留一个未完成请求
这是非常重要的设计。 因为现在做的是 RTU 主站轮询,不是高并发消息总线。 一次只允许一个未完成事务,逻辑最清晰,也最稳。
(2)协议状态机不放到中断里执行
中断里适合做的是“收了一个字节”“发完一个字节”“T3.5 到了”这种短动作, 不适合做完整状态流转、协议解析和复杂错误处理。
(3)单从站异常只影响该从站,不阻塞其他从站
这也是工业轮询系统非常重要的性质。 一台从站挂了,不应该让整条总线逻辑一起死掉。
这三条原则,已经足以说明: 这套实现不是“能跑就行”,而是已经是一个靠谱的主站。
六、把这些抽象概念对回 F429 上的设计
到这里,就可以把前面的协议知识重新映射回代码的工程分层了。
1. 物理层 / BSP
这一层对应:
mcu/bsp/rs485
它负责:
USART2PB8(DE/RE)TIM6- 收发中断
- T3.5 到期通知
这一层只回答一句话:
字节怎么在总线上被正确地发出去、收进来。
2. 帧层 / 链路层
这一层对应:
mcu/middleware/Modbus-RTU
其中至少包括这些职责:
- CRC16
- 帧缓存
- 帧结束判定
- 响应合法性初筛
这一层回答的问题是:
这一串字节到底能不能算一帧合法的 Modbus-RTU 报文。
3. 协议层
还是在:
mcu/middleware/Modbus-RTU
但这里关注的是:
- 功能码是什么
- 响应是不是异常响应
- 数据区长度对不对
- 读到的寄存器值怎么解释
这一层回答的问题是:
这帧是“什么意思”,而不是“有没有收到”。
4. 事务层
这一层同样在主站中间件里,但已经开始偏“主站控制逻辑”:
- 发请求
- 等响应
- 超时
- 重试
- 失败分类
- 返回上层结果
这一层回答的问题是:
这次主站访问到底成功了没有,失败又是为什么。
- 应用层
这一层对应:
mcu/app/task_modbus_master
mcu/app/app_data
mcu/app/app_control
这一层不再关心 CRC、字节计数、DE/RE,而是关心:
- 轮询谁
- 先轮询谁
- 读到的值放到哪里
- 写命令何时下发
- 哪些数据要打包成快照
- 哪些异常要标记为从站离线
这一层回答的问题是:
这些 Modbus 数据在这个项目里到底有什么业务意义。
七、我的当前轮询拓扑是什么
后来者接手时,最需要知道的是:
现在这套系统不是“任意轮询”,而是已经有固定拓扑和顺序的。
当前分支文档给出的每轮顺序是:
- 1.如果 uplink ACK 中有 pendingCommand,先写从站4的 40101~40108
- 2.读取从站4 40031~40038
- 3.读取从站1 40001
- 4.读取从站2 40011、40012
- 5.读取从站3 40021
- 6.生成采集快照并入队上报
这说明你现在的系统拓扑不是“纯采集”,而是已经具备了:
- 状态读
- 环境采集
- 控制写入
- 结果上报
四种角色。
换句话说,它已经是一个小型工业主站,而不只是一个读几个传感器数值的串口 demo。
八、这一版实现里,哪些地方明显是阶段性写死的
这一节我必须提前写给后来者。 因为“能跑”不等于“适合长期继承”。
从当前文档可以直接看出,至少有这些地方是明显写死或半写死的:
1. 从站拓扑是固定写死的
当前就是 4 个从站,地址固定:
- 1
- 2
- 3
- 4
如果以后从站数量变化、地址变化,或者设备类型变化, 现在这套轮询逻辑很可能要改代码,而不是改配置。
后续建议
把从站表抽成配置化结构,例如:
- 从站地址
- 读写功能
- 起始寄存器
- 数量
- 超时
- 重试次数
- 采样周期
- 数据落点
都放到统一描述表里。
2. 寄存器映射是按当前现场设备直接写死的
现在 40001、40011、40012、40021、40031~40038、40101~40108 都是按当前系统约定写的。
这对当前项目当然是合理的,但对长期继承来说,会带来两个问题:
- 新设备一接入就得改源代码;
- 老设备一换协议,整个上层解析都要跟着动。
后续建议
把“寄存器地址 -> 业务字段”的映射单独收口, 不要让轮询逻辑和业务解释逻辑强耦合在一起。
3. 串口与引脚绑定是固定的
当前文档里明确写了:
USART2PD5/PD6PB8(DE/RE)
这意味着当前实现目前是贴着这块板子的。
后续建议
至少要把以下内容集中到统一端口配置层:
UART实例TX/RX引脚DE/RE引脚- 波特率
- 校验位
- 停止位
这样后面如果迁移到别的 F4 或别的主控,代价会小很多。
4. 时序参数是固定值
当前参数也已经明确给出:
9600 8E1- 单事务超时
1000ms - 失败重试
2 - 请求间隔
10ms - 轮询周期
1000ms
这些值在当前现场是合理的,但未必适合未来所有节点。
后续建议
后面最好把它们从“写死常量”提升到“可调参数”。
因为不同从站的响应速度、总线长度、噪声水平都可能不同, 统一写死并不利于长期演进。
5. 轮询顺序是固定的
当前顺序是先控制写入,再读状态区,再读三个采集从站。 这很符合当前系统逻辑,但这其实已经隐含了“控制优先于采集”的策略。
后续建议
这类策略最好显式写成“调度策略”,而不是隐式藏在顺序代码里。
九、这一讲的结尾:先把 Modbus 看成一套分层系统
到这里,这一讲真正想留下的只有一句话:
Modbus-RTU 不是“串口发几个字节”,而是一套从 RS485 物理层一直往上叠到主站事务层的完整通信系统。
只有先把这件事看清楚,后面再去看:
- CRC 怎么算;
- T3.5 怎么定;
- 为什么 ISR 只做收发通知;
- 为什么主站必须一次只保留一个未完成事务;
- 为什么异常要分成 CRC / PROTOCOL / EXCEPTION / PORT / TIMEOUT;
这些实现选择,才都会变得顺理成章。