跳转到内容
新建笔记

FreeModbus v1.6移植:STM32F103时序、回调与边界

FreeModbus 把协议状态机与串口、定时器、事件和业务寄存器分开。移植成功的标准不是“工程能编译”,而是帧边界、收发方向、请求校验和共享数据在实际硬件上都符合约定。

本文重整原 STM32F103 + HAL/CubeMX 笔记,参考原文对应的 FreeModbus v1.6,固定上游提交 5ceb9d1bb029ae9bf6b20618eddde143c4bfbf9b。这是历史版本审查,不能把“官方发布”当成无需检查的正确性保证;下文明确列出本次发现的旧版边界。

此处的 eMBInit()、eMBEnable()、eMBPoll() 是上游从站/服务器栈的入口。它响应外部主站请求,不等于提供 mb_master_03h() 一类私有主站API。要发起轮询或控制其他设备,应使用经过确认的主站实现,参见主站事务。

Modbus 是应用协议;RTU(Remote Terminal Unit)在串行线上使用紧凑二进制帧。RS485规定电气接口,UART提供串行收发,两者不能替代寄存器语义或报文校验。

原笔记保存的FreeModbus压缩包目录截图

上图保留原始下载记录,具体文件以固定版本实际源码为准。原稿将整个项目称为GPL v3不准确:本次检查的 modbus/ 核心文件具有BSD式三条款声明,demo/BARE/port示例具有LGPL 2.1或后续版本声明;其他移植和附带组件逐文件核对。不能只根据根目录出现gpl.txt、lgpl.txt或bsd.txt判断所有文件的许可证。

部分RTU从站中的作用
modbus/mb.c初始化、启停、事件轮询与功能码分派
modbus/rtu/mbrtu.c、mbcrc.cRTU收发状态机及CRC
modbus/functions/mbfunc*.c按需要启用的功能码处理
modbus/functions/mbutils.c位打包辅助与错误码转换;不是任意目录下的同名文件
modbus/include/与modbus/rtu/公共接口与RTU头文件
本项目port.h、portserial.c、porttimer.c、portevent.c平台类型、临界区、串口、定时与事件实现
业务回调保持/输入寄存器、线圈和离散输入与应用数据的映射

ascii/和tcp/分别服务ASCII/TCP模式;仅RTU工程应关闭相应开关并只编译需要的实现。TCP模式还需要网络适配,不是换一块W5500就自动得到完整接口。demo/保留各平台参考,doc/是对应版本文档;tools/的实际内容按发行包核对,不假定一定存在crcgen.py。__MACOSX若出现在原下载包中,是打包元数据,不是移植依赖。

在 mbconfig.h 中令 MB_RTU_ENABLED=1、MB_ASCII_ENABLED=0、MB_TCP_ENABLED=0,按实际需要开启读写功能。功能处理数量与MB_FUNC_HANDLERS_MAX匹配;要启用0x11从站标识或0x17读写组合,也要纳入处理器、缓冲区及测试。仅添加-D未必能覆盖头文件中已有的无条件定义,应检查预处理结果。

工程包含路径必须选中本项目的port.h,避免误包含BARE空临界区模板;保留完整类型定义、assert和所需标准头文件。CubeMX生成初始化代码后,不应再让两个模块分别重配同一个UART和定时器。

原稿同时出现9600/115200和固定1750微秒,不能混用。串行规范的常规RTU字符为11位:1起始、8数据、1校验、1停止;无校验则使用2停止位。F103 HAL使用8位有效数据加校验时,需注意其字长字段包含校验位置,应核对UART_WORDLENGTH_9B配合偶校验后的有效数据宽度;不能只看界面的“8”就断定线上格式。

在不高于19200 bit/s的情况下,11位字符的理论时间为:

tchar=11B,t1.5=16.5B,t3.5=38.5Bt_{\mathrm{char}}=\frac{11}{B},\quad t_{1.5}=\frac{16.5}{B},\quad t_{3.5}=\frac{38.5}{B}

9600时,t3.5≈4010.42 μst_{3.5}\approx4010.42\ \mu s,不是1750微秒。高于19200时,规范建议使用固定的750微秒和1750微秒。串行规范§2.5

若TIM2实际时钟72 MHz,PSC=71得到1 MHz计数;9600对应向上取整的4011个计数,ARR=4010。115200采用1750微秒时为ARR=1749。APB分频不为1时F1常见定时器时钟倍频规则应从时钟树确认;不能在不同芯片、总线配置下机械照抄72 MHz。

下面是独立的16位向上计数定时器周期计算,不负责配置HAL、清标志或分配中断:

#include <stdbool.h>
#include <stdint.h>
bool modbus_t35_arr(uint32_t baud, uint32_t counter_hz,
uint16_t *arr) {
if (baud == 0 || counter_hz == 0 || arr == 0) return false;
uint64_t numerator, denominator;
if (baud > 19200) {
numerator = UINT64_C(1750) * counter_hz;
denominator = UINT64_C(1000000);
} else {
numerator = UINT64_C(77) * counter_hz;
denominator = UINT64_C(2) * baud;
}
uint64_t ticks = (numerator + denominator - 1) / denominator;
if (ticks == 0 || ticks > UINT64_C(65536)) return false;
*arr = (uint16_t)(ticks - 1);
return true;
}

v1.6 的50微秒接口要单独审查

跳转到“v1.6 的50微秒接口要单独审查”

xMBPortTimersInit(USHORT usTimeOut50us)接收的是50微秒单位。所核对v1.6的低速计算使用整数向下取整,9600传入80,即4000微秒;直接乘50会早于理论t3.5t_{3.5}。应在选定实现中改为向上取整并验证计时基准,或在移植层明确增加足够裕量,记录由此产生的间隔;不能把原值当成准确4010微秒。

该版本高波特率传入35,其值是1750微秒,源码旁的“1800us”注释不一致。实际移植还应核对计数从0开始、ARR预装载、生效更新与UIF清除顺序,并在每个接收字节后重新计时;停止时禁止更新中断且清除遗留状态。

v1.6的这份RTU状态机主要以t3.5t_{3.5}判断帧完成,并未完整实现独立t1.5t_{1.5}间隔拒收。要满足对应串行时序要求,必须补充间隔检测和异常帧恢复,不能把“CRC通过并能回包”作为完整符合性证明。

4. RS485方向切换:必须等最后停止位

跳转到“4. RS485方向切换:必须等最后停止位”

以DE高有效、/RE低有效且两脚绑在同一GPIO的收发器为例:

控制脚驱动器接收器
低禁止发送允许接收
高允许发送禁止接收

原稿对RE高低的说明颠倒了。实际芯片可能采用分开的使能脚、反相控制或自动方向电路,须以原理图和数据手册为准;PA1仅是原项目示例引脚。

发送顺序是:先使能驱动器并满足建立时间→启动字节发送→等最后停止位完成→满足收发器保持要求→释放DE并启用接收。TXE表示数据寄存器可继续写,TC才表示该串口帧已发送完成。 ST RM0008 USART章节

若用TXE中断驱动pxMBFrameCBTransmitterEmpty(),栈在最后一个字节装载后请求vMBPortSerialEnable(TRUE,FALSE)时,移植层应关闭TXE并进入“等待TC”状态,保持DE;到TC才真正切回接收。启动发送前还需按芯片规定清理旧TC状态,避免把空闲时的TC当成本次完成。

在这种端口实现中,旧栈的EV_FRAME_SENT事件也可能早于物理线路发送完成,不能直接用它释放外部总线资源。中断内不要用无超时忙等TC拖住整个系统;明确安排TC中断及停机/错误恢复。

5. 中断、临界区和事件队列

跳转到“5. 中断、临界区和事件队列”

mbport.h中的pxMBFrameCBByteReceived、pxMBFrameCBTransmitterEmpty、pxMBPortCBTimerExpired是函数指针变量,不是可重新声明的普通函数。直接包含官方头文件,初始化完成后才允许中断调用。

中断/接口责任
RXNE使接收回调读取且只读取一次DR,处理错误标志与坏帧恢复
TXE让栈装载下一个字节;保证DE已经使能
TC完成移植层延后的方向切换
定时器更新确认中断来源,清标志,通知到期并避免重复调用
eMBPoll()处理排队事件、功能码和业务回调,不在这里长期阻塞

手写读取DR/清状态与HAL_UART_IRQHandler()若同时拥有同一状态,会出现重复消费或错误清除。选择一个清晰的中断所有者;同理,不要手动消费TIM更新后又让另一路对同一事件重复回调。CubeMX的USER CODE区能保护代码不被覆盖,但不能自动解决所有权问题。

临界区应保存进入前的PRIMASK,并在最外层退出时恢复,或使用明确支持嵌套的端口实现。原稿“进入__disable_irq()、退出永远__enable_irq()”会错误打开调用者本已屏蔽的中断。RTOS还要处理任务切换和FromISR规则,普通任务互斥锁不能代替ISR同步。

BARE示例的单个事件槽会覆盖尚未取走的事件;先置“有事件”再写事件值也会暴露竞争窗口。volatile不构成完整并发协议。需选择带临界保护的有界队列或经过证明的单槽调度契约,并定义满队列行为、故障计数与恢复,不能静默覆盖。

6. 寄存器回调:地址加1与完整范围

跳转到“6. 寄存器回调:地址加1与完整范围”

该v1.6功能处理器把PDU起始地址加1后传给应用回调。因而线上0对应回调地址1。直接把回调参数当零基地址,会整体错位;线上0xFFFF加1还会在16位回调参数中回绕到0,使用顶端地址必须另行修正接口或明确限制。

下面的业务回调仅实现10个保持寄存器,线上范围0~9。它验证整个范围后读写,按高字节在前打包;示例不代表完整板级工程。

#include <stdint.h>
#include "mb.h"
static uint16_t holding[10];
eMBErrorCode eMBRegHoldingCB(UCHAR *buffer, USHORT address,
USHORT count, eMBRegisterMode mode) {
if (buffer == 0 || count == 0) return MB_EINVAL;
if (address < 1 || address > 10) return MB_ENOREG;
unsigned offset = (unsigned)address - 1U;
if ((unsigned)count > 10U - offset) return MB_ENOREG;
if (mode != MB_REG_READ && mode != MB_REG_WRITE)
return MB_EINVAL;
for (unsigned i = 0; i < count; ++i) {
if (mode == MB_REG_READ) {
uint16_t value = holding[offset + i];
buffer[2U * i] = (UCHAR)(value >> 8);
buffer[2U * i + 1U] = (UCHAR)(value & 0xFFU);
} else {
holding[offset + i] = (uint16_t)(
((uint16_t)buffer[2U * i] << 8) |
(uint16_t)buffer[2U * i + 1U]);
}
}
return MB_ENOERR;
}

回调收到的缓冲区必须已经由协议层验证为包含完整有效载荷;回调签名没有携带缓冲区字节长度,无法自行修复上层截断报文问题。共享应用数据应再做快照/提交同步,值范围有约束时先验证全部值,避免部分提交。

eMBRegInputCB用于输入寄存器;eMBRegCoilsCB、eMBRegDiscreteCB处理位数据。v1.6的xMBUtilGetBits/SetBits会访问所涉及字节的下一个字节,应为缓冲区留出辅助空间并验证最后一个位组;一次不超过8位,传入值限制在指定有效位宽。返回MB_ENOREG表示请求地址不可用,而不是对所有情况都准确的“不支持功能码”;功能是否支持应由功能表决定。

7. 四个数据空间与报文字段

跳转到“7. 四个数据空间与报文字段”
数据空间单位/访问常用功能码
线圈1 bit,可读写01、05、0F
离散输入1 bit,只读02
输入寄存器16 bit,只读04
保持寄存器16 bit,可读写03、06、10

以上功能码为十六进制。手册中的0x/1x/3x/4x数据引用前缀不是PDU地址的高四位;字段也不强制对应物理I/O。其他功能包括07异常状态、08诊断、0B事件计数、11从站标识、14/15文件记录、16屏蔽写和17读写组合,实际支持以选定库和设备为准。

异常响应将功能码最高位置1,并携带异常码;常见01/02/03/04分别表示非法功能、非法地址、非法值与设备故障。错误CRC帧通常丢弃,不用异常响应冒充CRC确认。

16位0x1234的数据字节先12后34;CRC字段则低字节在前。一个UART数据字节的位序仍为低位先发,例如0x12的数据位序是0,1,0,0,1,0,0,0。这些不同层次不能用一个“大端/小端”概括。

原笔记中的32位参数示例:P6.000跨0600H与0601H

原图只证明该记录所示参数占两个16位寄存器,缺少完整设备型号与手册,不能推广其地址或字序。程序可用uint32_t组织32位整数,原稿的uint31_t是笔误;仍需按设备规定拆成两个寄存器,数量为2。低字/高字顺序、有符号数和浮点格式参见项目接口契约。

完成时钟、GPIO、UART、定时器和端口初始化→检查eMBInit(MB_RTU, 1, port, baud, parity)返回值→检查eMBEnable()→持续执行eMBPoll()。port只是传给移植层的编号,不能未经核对固定解释为某个USART。

先在无设备主机环境核对CRC、编码、回调边界和帧处理,再在板上验证UART格式、RTU间隔、RS485方向以及错误恢复。逻辑分析仪应观察最后停止位与DE,而不只看回显文字;业务验收包括非法地址、截断帧、最大请求、错误CRC、计时回绕、并发读写与长时间运行。

本次对固定v1.6源码编译运行的主机测试复现了三个问题:0x03数量解析覆盖高字节,使非法的0x0101被当成1;0x10未充分核对实际PDU长度与字节计数,会把逻辑帧以外的缓冲内容当成数据;0x10实现上限为120,拒绝121,而协议允许最多123。测试使用充分分配的本地缓冲区,不执行越界内存访问。以上是所测用例发现的问题,不构成穷尽审计。这些是旧版缺陷的复现,不能记成协议符合性通过;实际采用前应修正或更换经过验证的版本,并完成回归。

本次只进行主机侧源码、算法和接口验证,没有编译完整STM32工程、连接板卡或证明RS485时序。参考:官方版本入口、固定v1.6源码、应用协议、原笔记。