libmodbus 移植 STM32:从零开始的 Modbus 通信
做工业项目时,STM32 需要和 PLC 通信。Modbus RTU 是最常见的工业协议,libmodbus 是最流行的开源实现。但 libmodbus 是为 Linux 设计的,移植到 STM32 需要替换底层硬件操作。
为什么选 libmodbus
- 成熟稳定,社区活跃
- 支持 Modbus RTU/TCP
- API 设计清晰,易于使用
- 有丰富的示例代码
坑在于:libmodbus 依赖 POSIX API(open、read、write),STM32 裸机环境下没有这些。
移植思路
libmodbus 的架构分两层:
- 上层:Modbus 协议处理(CRC 计算、报文组装/解析)
- 底层:串口收发、定时器、延时
移植的关键是保留上层,替换底层。
第一步:简化源码
# 下载 libmodbus
git clone https://github.com/stephane/libmodbus.git
# 只保留核心文件
src/
├── modbus.c # Modbus 协议核心
├── modbus.h # 公共头文件
├── modbus-private.h # 私有头文件
├── modbus-rtu.c # RTU 模式实现
├── modbus-rtu.h # RTU 模式头文件
└── config.h # 配置文件
删除 TCP 相关文件(modbus-tcp.c、modbus-tcp.h),只保留 RTU 模式。
第二步:配置 config.h
// config.h
#define HAVE_STRLCPY 1
#define HAVE_STRERROR 1
#define HAVE_ARPA_INET_H 0 // STM32 没有
#define HAVE_NETINET_IN_H 0
#define HAVE_SYS_SOCKET_H 0
#define HAVE_WINSOCK2_H 0
坑在于:很多宏默认开启,STM32 环境下要手动关闭。
第三步:替换串口收发
libmodbus 的底层操作在 modbus-rtu.c 的 _step 函数里:
// 原始代码(Linux)
static ssize_t _step(modbus_t *ctx, uint8_t *msg, int msg_length)
{
return write(ctx->s, msg, msg_length);
}
// 替换为 STM32 HAL
static ssize_t _step(modbus_t *ctx, uint8_t *msg, int msg_length)
{
modbus_rtu_t *ctx_rtu = ctx->backend_data;
HAL_StatusTypeDef status;
// 发送
status = HAL_UART_Transmit(&huart1, msg, msg_length, 1000);
if (status != HAL_OK) {
return -1;
}
// 接收
status = HAL_UART_Receive(&huart1, msg, msg_length, 1000);
if (status != HAL_OK) {
return -1;
}
return msg_length;
}
第四步:替换延时函数
// 原始代码(Linux)
static void usleep_ms(int ms)
{
usleep(ms * 1000);
}
// 替换为 STM32 HAL
static void usleep_ms(int ms)
{
HAL_Delay(ms);
}
第五步:替换互斥锁(可选)
如果是单线程环境(裸机或 FreeRTOS 单任务),可以简化互斥锁:
// 简化为无操作
#define LOCK(ctx)
#define UNLOCK(ctx)
第六步:初始化和使用
#include "modbus.h"
int main(void)
{
// 初始化硬件
HAL_Init();
SystemClock_Config();
MX_USART1_UART_Init();
// 创建 Modbus RTU 上下文
modbus_t *ctx = modbus_new_rtu("/dev/ttyS0", 9600, 'N', 8, 1);
if (ctx == NULL) {
printf("无法创建 Modbus 上下文\n");
return -1;
}
// 设置从站地址
modbus_set_slave(ctx, 1);
// 连接(RTU 模式不需要,但要调用)
modbus_connect(ctx);
// 读取保持寄存器(功能码 0x03)
uint16_t tab_reg[10];
int rc = modbus_read_registers(ctx, 0, 10, tab_reg);
if (rc == 10) {
printf("读取成功:");
for (int i = 0; i < 10; i++) {
printf("%d ", tab_reg[i]);
}
printf("\n");
} else {
printf("读取失败,错误码:%d\n", rc);
}
// 写入单个寄存器(功能码 0x06)
rc = modbus_write_register(ctx, 0, 12345);
if (rc == 1) {
printf("写入成功\n");
}
// 关闭连接
modbus_close(ctx);
modbus_free(ctx);
return 0;
}
常见问题
1. CRC 校验失败
// 检查波特率配置
modbus_t *ctx = modbus_new_rtu("/dev/ttyS0", 9600, 'N', 8, 1);
// 波特率 校验 数据位 停止位
坑在于:PLC 和 STM32 的波特率、校验位、数据位、停止位必须一致。
2. 超时问题
// 设置超时时间
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 100000; // 100ms
modbus_set_response_timeout(ctx, &timeout);
3. 从站地址不对
// 设置从站地址(要和 PLC 配置一致)
modbus_set_slave(ctx, 1); // 从站地址 1
踩坑总结
- libmodbus 依赖 POSIX API,STM32 需要替换底层操作
- 核心替换:串口收发、延时函数、互斥锁
- 保留上层协议处理(CRC、报文组装/解析)
- 波特率、校验位、数据位、停止位必须和 PLC 一致
- 从站地址要和 PLC 配置一致
从"libmodbus 不能用"到"STM32 和 PLC 通信成功",只需要替换底层操作。坑在于:很多人被 POSIX API 吓退了,其实替换量不大。