FreeModbus:嵌入式设备的 Modbus 从站协议栈
待复核FreeModbus 是一个给嵌入式设备用的 Modbus ASCII、RTU 和 TCP 协议栈,重点是实现从站设备。
日常类比:工厂里有很多仪表、传感器和 PLC。Modbus 像一套统一问答话术:“请告诉我 100 号寄存器的值”“请把这个线圈打开”。FreeModbus 就是帮小设备听懂这套话术的翻译员。
典型主循环长这样:
eMBInit(MB_RTU, 0x0A, 0, 38400, MB_PAR_EVEN);eMBEnable();for (;;) { eMBPoll();}应用代码不用自己解析每个字节,而是在回调里提供寄存器、线圈和离散输入的值。
它适合学习工业通信:代码小、接口清楚,能看到协议栈如何和串口、定时器、TCP 以及业务寄存器连接起来。
不理解 FreeModbus,嵌入式通信里几个常见问题会很难落地:
- Modbus 是工业现场常见协议,很多 PLC、仪表和采集器都支持。
- 嵌入式设备资源少,协议栈要小,不能随便 malloc 一堆状态。
- RTU 对串口时序敏感,协议正确不等于硬件配置正确。
- 应用寄存器和协议帧之间要有清晰边界,否则业务代码会和通信细节缠在一起。
-
从站协议栈:原项目说明里强调 BSD licensed MODBUS RTU/ASCII and TCP slave。它主要让设备响应主站请求。
-
三种传输模式:RTU 更紧凑,ASCII 更易调试,TCP 跑在以太网或 IP 网络上。
-
轮询驱动:应用先
eMBInit,再eMBEnable,主循环或 RTOS 任务里周期性调用eMBPoll。 -
寄存器由应用提供:协议栈不内部保存所有寄存器。读写发生时,它调用
eMBRegHoldingCB等回调。 -
移植层很关键:串口收发、定时器、事件队列和临界区通常要由具体芯片或 RTOS 适配。
案例 1:STM32 做一个 RTU 从站
Section titled “案例 1:STM32 做一个 RTU 从站”光调 eMBInit 不够,还要把 port 层接到芯片外设,否则收不到完整帧:
/* port/portserial.c:UART 收到字节 → 交给协议栈 */BOOL xMBPortSerialPutByte(CHAR c) { return HAL_UART_Transmit(&huart1, &c, 1, 10) == HAL_OK; }BOOL xMBPortSerialGetByte(CHAR *p) { return HAL_UART_Receive(&huart1, (uint8_t*)p, 1, 0) == HAL_OK; }
/* port/porttimer.c:RTU 帧间隔(约 3.5 字符时间)到点 → 通知「一帧结束」 */void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim == &htim2) (void)pxMBPortCBTimerExpired();}
void app_main(void) { eMBInit(MB_RTU, 10, 1, 9600, MB_PAR_EVEN); /* 地址 10,USART1,偶校验 */ eMBEnable(); while (1) { eMBPoll(); } /* 推进状态机:收字节 → 组帧 → 调寄存器回调 */}逐部分解释:
MB_RTU选串口二进制帧;10是从站地址,主站必须打到它或广播。9600/MB_PAR_EVEN必须和主站、UART 硬件一致,差一位就像没接线。- 定时器负责 RTU 的「沉默多久算一帧结束」;没接好就会永远组不齐帧。
案例 2:实现保持寄存器回调
Section titled “案例 2:实现保持寄存器回调”eMBErrorCode eMBRegHoldingCB(UCHAR *buf, USHORT addr, USHORT nregs, eMBRegisterMode mode) { if (addr < REG_START || addr + nregs > REG_END) return MB_ENOREG; if (mode == MB_REG_READ) copy_app_regs_to(buf, addr, nregs); else copy_buf_to_app_regs(addr, buf, nregs); return MB_ENOERR;}这段代码的重点是边界检查。回调返回 MB_ENOREG 时,协议栈会把它变成 Modbus 异常响应。
案例 3:TCP 模式接入上位机
Section titled “案例 3:TCP 模式接入上位机”eMBTCPInit(502);eMBEnable();for (;;) { eMBPoll();}TCP 模式省掉了串口波特率和校验位,但多了网络栈、端口、防火墙和连接管理。嵌入式里常见组合是 lwIP + FreeModbus。
-
地址从 0 还是从 1 开始混淆:Modbus 文档、上位机软件和代码数组常常差一位,先统一约定再调试。
-
串口参数不一致:波特率、校验位、停止位只要有一个不一致,RTU 就会像没接线一样。
-
忘记周期调用
eMBPoll:初始化成功不代表会处理帧,事件要靠轮询或 RTOS 任务推进。 -
回调里不做范围检查:主站请求越界时应该返回异常,而不是读坏内存。
-
忽略许可证边界:仓库 README 写到 core stack、port implementations、demo applications 许可证不同,商用前要逐文件检查。
适用 vs 不适用场景
Section titled “适用 vs 不适用场景”适用:
- MCU 或小型 Linux 设备做 Modbus 从站。
- 学习 RTU 帧、功能码、寄存器模型和异常响应。
- 需要把传感器、继电器、计量数据暴露给 PLC 或上位机。
- 已有串口、定时器、lwIP 等基础移植能力的项目。
不适用:
- 需要完整主站功能且不想改造源码的项目。
- 强安全隔离的公网工业网关,Modbus 本身没有现代认证加密。
- 对协议认证、诊断工具、长期维护 SLA 有商业要求的场景。
- 不熟悉底层串口时序,却希望直接复制代码一次成功。
历史小故事(可跳过)
Section titled “历史小故事(可跳过)”- Modbus 起源很早,官方规范把它定义成应用层 request/reply 协议,通过功能码描述读写动作。
- FreeModbus 约 2006–2007 由 Christian Walter 开源,定位小 MCU 上的 BSD 许可从站栈;代码风格是典型传统嵌入式 C。
- 官方稳定发布点长期停在 v1.6.0;后来 Doxygen 文档页脚常见 2018 字样,那是文档生成时间,不是项目诞生年。
- 仓库 README 写原作者活动较少,社区以更开放方式维护 master;2026 年 7 月 GitHub 约 1.1k stars。
- 工业协议栈的难点不是 API 多,而是时序、边界检查和移植层。
- FreeModbus 把协议解析和业务寄存器分开,用回调让应用提供真实数据。
- RTU、ASCII、TCP 是同一套 Modbus 应用语义落在不同传输上的样子。
- 学嵌入式通信时,先跑通最小从站,再逐步补寄存器表、异常处理和诊断日志。
- 官方仓库:cwalter-at/freemodbus
- API 文档:FreeModbus Modbus module
- 寄存器回调:FreeModbus Registers
- 官方规范:Modbus Specifications
- lwip —— TCP 模式常见网络栈搭档
- lwip —— 嵌入式 TCP/IP 栈,适合和 Modbus TCP 对照学习。
- mbedtls —— 如果要把工业通信放到更安全的链路上,会碰到 TLS 话题。
- mqtt-s-2008 —— 另一类物联网通信协议,和 Modbus 的主从模型不同。
- docker —— 可用容器跑上位机模拟器或测试环境。
- caddy —— 当 Modbus 数据被网关转成 Web API 时,可能需要 HTTPS 入口。
(暂无反向链接)