做工业项目时,STM32 需要和 PLC 通信。Modbus RTU 是最常见的工业协议,libmodbus 是最流行的开源实现。但 libmodbus 是为 Linux 设计的,移植到 STM32 需要替换底层硬件操作。

为什么选 libmodbus

  • 成熟稳定,社区活跃
  • 支持 Modbus RTU/TCP
  • API 设计清晰,易于使用
  • 有丰富的示例代码

坑在于:libmodbus 依赖 POSIX API(openreadwrite),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.cmodbus-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

踩坑总结

  1. libmodbus 依赖 POSIX API,STM32 需要替换底层操作
  2. 核心替换:串口收发、延时函数、互斥锁
  3. 保留上层协议处理(CRC、报文组装/解析)
  4. 波特率、校验位、数据位、停止位必须和 PLC 一致
  5. 从站地址要和 PLC 配置一致

从"libmodbus 不能用"到"STM32 和 PLC 通信成功",只需要替换底层操作。坑在于:很多人被 POSIX API 吓退了,其实替换量不大。

标签: 编程, AUTOSAR

添加新评论