主站调用的核心是管理一笔完整事务:准备请求、获得总线、发送、等待匹配响应、处理异常或超时,再把结果交给业务。函数返回、收到任意帧、寄存器写回成功和电机实际完成动作,是不同的完成条件。
本文合并两份 mb_master 工程记录。原库源码与版本尚未提供,以下函数名、缓冲区及 ack_* 标志均是历史私有接口记录,不能当成 FreeModbus 或所有 Modbus 库的统一 API。
1. 保留的工程线索
跳转到“1. 保留的工程线索”| 项目 | 原记录 | 需要核对的契约 |
|---|---|---|
| 串口/方向 | UART1、RS485使能脚 | 引脚、收发器、DE极性、最后停止位后的切换时刻 |
| 波特率 | mb_port.h 中38400 | 实际数据位、校验、停止位以及双方配置 |
| 模块 | mb_master.c/.h、mb_port.c/.h、mb_reg.c/.h | 文件名暗示职责,仍须阅读具体源码 |
| 初始化 | HAL_Init、时钟、GPIO、DMA、USART1 | DMA仅在该工程确实采用时启用;还须完成库自身初始化 |
| 推进 | 主循环持续调用 mb_master_poll() | 它是否负责解帧、超时、发送和回调 |
| 完成标志 | ack_03h、ack_04h、ack_10h 等 | 是有效响应、接收事件还是提交成功;何时清除 |
另一份控制接口笔记使用 USART2/115200,是不同项目配置,不应拼接成同一工程的确定设置。
2. 八个功能接口与标准含义
跳转到“2. 八个功能接口与标准含义”| 历史调用名称 | 标准功能码 | 操作 |
|---|---|---|
mb_master_01h(id, start, n) | 0x01 | 读线圈 |
mb_master_02h(id, start, n) | 0x02 | 读离散输入 |
mb_master_03h(id, start, n) | 0x03 | 读保持寄存器 |
mb_master_04h(id, start, n) | 0x04 | 读输入寄存器 |
mb_master_05h(id, addr, value) | 0x05 | 写单线圈 |
mb_master_06h(id, addr, value) | 0x06 | 写单个保持寄存器 |
mb_master_0fh(id, start, n, data) | 0x0F,十进制15 | 写多个线圈 |
mb_master_10h(id, start, n, data) | 0x10,十进制16 | 写多个保持寄存器 |
保持寄存器并不天然等于模拟输出,输入寄存器也不必来自物理模拟输入;内容由应用定义。API是否接受布尔值还是0xFF00/0x0000、多线圈缓冲区如何按位打包、数量单位、返回值和最大长度,都应从实际头文件与实现确认。应用协议
3. 三个原示例的正确阅读方式
跳转到“3. 三个原示例的正确阅读方式”以下为历史调用片段,缺少实际库,不能独立编译运行。
/* 从站1:从保持寄存器地址0读10个字。 */mb_master_03h(1, 0x0000, 10);
/* 从站1:从0x0010起写入五个16位值。 */static uint16_t write_data[5] = {100, 200, 300, 400, 500};mb_master_10h(1, 0x0010, 5, write_data);
/* 从站1:从输入寄存器0x0100起读两个字。 */mb_master_04h(1, 0x0100, 2);原稿分别在轮询后观察 ack_03h、ack_10h 和 ack_04h,处理一次后清零。保留这个历史用法,但必须先确认标志只会对当前事务的合法响应置位;清除旧标志不能代替匹配请求。
原读取缓冲区记录如下:
| 功能 | 历史缓冲区名 | 不能直接假设的事项 |
|---|---|---|
| 读线圈 | mb_coil_buf[] | 每元素是一位还是一字节;是否位打包 |
| 读离散输入 | mb_discrete_buf[] | 位序、有效数量与更新范围 |
| 读保持寄存器 | mb_hold_buf[] | 绝对地址下标还是本次请求从0开始 |
| 读输入寄存器 | mb_input_buf[] | 站号隔离、稀疏映射、字节序及陈旧状态 |
原温湿度示例把 0x0100 当成温度、0x0101 当成湿度,并将温度除以10。这是示例设备假设,不是 Modbus 定义。真实温度可能有负值,需确认补码、有符号位宽和比例系数;使用紧凑映射后不能继续直接用 buffer[0x0100]。
4. 一秒轮询必须有忙碌状态
跳转到“4. 一秒轮询必须有忙碌状态”下面是与具体库无关的组织逻辑。需要把“提交、完成、异常、超时”映射到真实库接口后才能实现。
初始化硬件、主站与业务状态每次主循环: 推进主站状态机,处理收到的字节与计时事件 若当前事务完成:验证/复制结果,释放事务所有权 若异常或超时:记录原因,执行结束/恢复流程 若总线可用、无未完成事务且距离上次提交达到1秒: 准备从站1、功能03、起始0、数量10的请求 若库接受请求:记录提交时间和预期响应,进入等待状态 执行其他非阻塞业务用 uint32_t 毫秒计数时,可用无符号差值处理一次计数回绕,但任务必须足够频繁地运行,不能跨过一个完整计数周期仍指望差值保留真实时间。更重要的是:计时到期不等于上一事务完成,不能每秒无条件覆盖正在使用的请求结构。
响应处理应核对站号、功能码/异常功能码、长度及读响应的字节计数;写响应核对地址和数量/值。帧校验失败、设备异常和无响应超时分别记录。多个调用者应经过同一个串行调度器,RTU总线上同一时刻只保持一笔未完成事务。
RTU帧没有应用可任意填写的事务序号。内部递增编号可以保护本地回调,却不能使迟到的相同站号/功能码响应自动带上该编号;超时后的收发清理、静默间隔和下一请求时机仍需要驱动与协议状态机配合。
5. 写请求寿命与重复触发
跳转到“5. 写请求寿命与重复触发”旧短稿每轮先 mb_master_poll(),再调用 mb_master_10h(2, 0xA864, 2, ...),然后阻塞5秒。需要分别处理三个问题:
- 5秒阻塞可能停止接收状态机与超时处理,应改为非阻塞计时。
- 无条件重复写入可能反复触发动作。原注释称地址为“启动”,但没有设备手册,不能把
0xA864作为通用启动命令发布。 - 异步库若只借用数组指针,缓冲区必须保持有效且内容不变,直到发送/事务真正不再访问它。局部数组、C复合字面量的作用域及C++支持差异都需核对。
超时也不必然意味着底层DMA已经停止访问发送缓冲区。释放或重用前,应确认驱动取消/完成契约。static只解决存储寿命,并不阻止下一请求提前改写同一数组。
对读取请求和某些“写入同一目标值”的请求,可以按设备语义设置有限重试;相对运动、计数递增、一次性启动等命令不能默认无害重试。应让应用保存命令状态,并区分“写响应成功”“设备接受命令”“动作完成”。
6. 落地时保存哪些证据
跳转到“6. 落地时保存哪些证据”保存实际库版本/源码、串口设置、设备寄存器手册、请求与响应十六进制、超时门限和一次异常记录。先验证只读访问,再验证参数写入及读回,最后在受控条件下联调动作。未读到返回值定义前,不把私有函数的返回当成响应成功。
本次重整没有连接设备或编译未知库;独立的CRC与映射算法在各自笔记中验证。相关:CRC计算、地址映射、FreeModbus从站移植。
原始记录:主站使用示例、写多个寄存器调用记录。