libmodbus 移植到 STM32H5

目录

  • 一、前言
  • 二、移植总思路
  • [三、Step 1:生成专属文件 modbus-st-rtu.c](#三、Step 1:生成专属文件 modbus-st-rtu.c)
  • [四、Step 2:裁剪------删掉所有 POSIX 代码](#四、Step 2:裁剪——删掉所有 POSIX 代码)
  • [五、Step 3:替换六个核心硬件函数](#五、Step 3:替换六个核心硬件函数)
  • [六、Step 4:创建 STM32 专用的上下文构造函数](#六、Step 4:创建 STM32 专用的上下文构造函数)
  • [七、Step 5:替换 malloc → pvPortMalloc](#七、Step 5:替换 malloc → pvPortMalloc)
  • [八、Step 6:工程集成 + 从机任务](#八、Step 6:工程集成 + 从机任务)
  • 九、常见坑
  • 十、结尾

一、前言

大家好,这里是 Hello_Embed

前几篇我们分析了 Modbus 协议和 libmodbus 源码架构。本篇动手移植------把 libmodbus 从 Windows/Linux 平台搬到 STM32H5 + FreeRTOS + UART_Device 上运行。

移植的核心是 "上层不动、底层替换"modbus_read_registersmodbus_reply 等 API 一行不改,只把底层 write()/read()/open()/select() 这些 POSIX 系统调用换成 STM32 的 HAL + OOP 串口操作。


二、移植总思路

libmodbus 的 _modbus_rtu_backend 结构体里绑定了所有硬件操作函数:

复制代码
原版 libmodbus(POSIX)              移植版(STM32H5)
─────────────────────────          ──────────────────────────
write(fd, req, len)        →       pdev->send(pdev, req, len, 1000)
read(fd, rsp, len)         →       pdev->RecvByte(pdev, rsp, timeout)
open("/dev/ttyS0", flags)  →       pdev->Init(pdev, baud, ...)
tcflush(fd, ...)           →       pdev->Flush(pdev)
select(fd+1, ...)          →       FreeRTOS xQueueReceive 自带超时
close(fd)                  →       (空函数,FreeRTOS 任务删除时清理)
malloc / free              →       pvPortMalloc / vPortFree

六步走完全程:①生成专属文件 → ②裁剪POSIX代码 → ③替换六个函数 → ④定制上下文构造函数 → ⑤替换malloc → ⑥工程集成。


三、Step 1:生成专属文件 modbus-st-rtu.c

从官方 libmodbus-3.1.10 的 src/modbus-rtu.c 出发:

bash 复制代码
cp modbus-rtu.c modbus-st-rtu.c   # st = STM32

之后的裁剪和替换只在这个文件上做,不动原始的 modbus-rtu.c最终工程里只编译 modbus-st-rtu.c,删除 modbus-rtu.c


四、Step 2:裁剪------删掉所有 POSIX 代码

modbus-st-rtu.c 里充斥着 POSIX/Win32 的条件编译,逐一清理:

4.1 删除头文件引用

c 复制代码
// 删除以下 include:
#include <termios.h>    // Linux 串口配置
#include <unistd.h>     // POSIX read/write/close
#include <fcntl.h>      // POSIX open
#include <sys/select.h> // POSIX select
#include <sys/ioctl.h>  // POSIX ioctl
#if defined(_WIN32) ... #endif  // 整个 Win32 分支

4.2 删除跨平台条件编译

c 复制代码
// 删除所有 #if defined(_WIN32) / #else / #endif 分支
// 删除 #if HAVE_DECL_TIOCSRS485 / #if HAVE_DECL_TIOCM_RTS 分支
// 删除 #if defined(__linux__) 分支

4.3 删除无用的串口模式函数

这些函数在 STM32 上没有对应概念,直接删除整个函数体:

复制代码
_get_termios_speed
modbus_rtu_set_serial_mode
modbus_rtu_get_serial_mode
modbus_rtu_get_rts / modbus_rtu_set_rts / modbus_rtu_set_custom_rts
modbus_rtu_get_rts_delay / modbus_rtu_set_rts_delay

4.4 简化"连接状态"判断

c 复制代码
// STM32 串口初始化完成即视为已连接
static unsigned int _modbus_rtu_is_connected(modbus_t *ctx)
{
    return 1;
}

五、Step 3:替换六个核心硬件函数

这是移植的核心环节------逐一替换底层 I/O 函数。

5.1 send --- 替换 write

c 复制代码
// 原版 (POSIX):
static ssize_t _modbus_rtu_send(modbus_t *ctx, const uint8_t *req, int req_length)
{
    return write(ctx->s, req, req_length);
}

// 移植版 (STM32):
static ssize_t _modbus_rtu_send(modbus_t *ctx, const uint8_t *req, int req_length)
{
    modbus_rtu_t *ctx_rtu = ctx->backend_data;
    struct UART_Device *pdev = ctx_rtu->dev;
    if (0 == pdev->send(pdev, (uint8_t *)req, req_length, 1000))
        return req_length;
    else {
        errno = EIO;
        return -1;
    }
}

5.2 recv --- 替换 read

c 复制代码
// 原版 (POSIX):
static ssize_t _modbus_rtu_recv(modbus_t *ctx, uint8_t *rsp, int rsp_length)
{
    return read(ctx->s, rsp, rsp_length);
}

// 移植版 (STM32):
static ssize_t _modbus_rtu_recv(modbus_t *ctx, uint8_t *rsp, int rsp_length, int timeout)
{
    modbus_rtu_t *ctx_rtu = ctx->backend_data;
    struct UART_Device *pdev = ctx_rtu->dev;
    if (0 == pdev->RecvByte(pdev, rsp, timeout))
        return 1;  // 一次收一个字节
    else {
        errno = EIO;
        return -1;
    }
}

5.3 connect --- 替换 open

c 复制代码
// 原版 (POSIX):
static int _modbus_rtu_connect(modbus_t *ctx)
{
    ctx->s = open(ctx_rtu->device, flags);
    return 0;
}

// 移植版 (STM32):
static int _modbus_rtu_connect(modbus_t *ctx)
{
    modbus_rtu_t *ctx_rtu = ctx->backend_data;
    struct UART_Device *pdev = ctx_rtu->dev;
    pdev->Init(pdev, ctx_rtu->baud, ctx_rtu->parity,
               ctx_rtu->data_bit, ctx_rtu->stop_bit);
    ctx->s = 1;  // 标记已连接
    return 0;
}

5.4 flush --- 替换 tcflush

c 复制代码
// 原版 (POSIX):
static int _modbus_rtu_flush(modbus_t *ctx)
{
    return tcflush(ctx->s, TCIOFLUSH);
}

// 移植版 (STM32):
static int _modbus_rtu_flush(modbus_t *ctx)
{
    modbus_rtu_t *ctx_rtu = ctx->backend_data;
    struct UART_Device *pdev = ctx_rtu->dev;
    return pdev->Flush(pdev);  // 清空 RX Queue
}

5.5 select --- 替换为 FreeRTOS 超时

原版 _modbus_rtu_select 用 POSIX select() 实现接收超时。STM32 移植版利用 FreeRTOS 队列自带超时 ------xQueueReceivexSemaphoreTake 本身就有 timeout 参数。因此 _modbus_rtu_select 直接简化为:

c 复制代码
static int _modbus_rtu_select(modbus_t *ctx, fd_set *rset,
                               struct timeval *tv, int length_to_read)
{
    return 0;  // 超时由 RecvByte 内部的 xQueueReceive(timeout) 处理
}

5.6 close + free

c 复制代码
// 原版 (POSIX):
static void _modbus_rtu_close(modbus_t *ctx)
{
    close(ctx->s);
}

// 移植版 (STM32):
static void _modbus_rtu_close(modbus_t *ctx) { /* 空, 资源清理在 free 里 */ }

static void _modbus_rtu_free(modbus_t *ctx)
{
    if (ctx->backend_data) {
        vPortFree(((modbus_rtu_t *)ctx->backend_data)->device);
        vPortFree(ctx->backend_data);
    }
    vPortFree(ctx);
}

六、Step 4:创建 STM32 专用的上下文构造函数

原版 modbus_new_rtu() 只需设备路径(如 /dev/ttyS0)。STM32 版需要额外的串口参数:

c 复制代码
modbus_t *
modbus_new_st_rtu(const char *device, int baud, char parity,
                  int data_bit, int stop_bit)
{
    modbus_t *ctx;
    modbus_rtu_t *ctx_rtu;
    struct UART_Device *pdev;

    // ① 分配 modbus_t, 初始化通用字段
    ctx = (modbus_t *)pvPortMalloc(sizeof(modbus_t));
    _modbus_init_common(ctx);

    // ② 绑定 STM32 专用的 backend vtable
    ctx->backend = &_modbus_rtu_backend_uart;

    // ③ 通过 OOP 层查找设备
    pdev = GetUARTDevice((char *)device);
    if (!pdev) {
        modbus_free(ctx);
        errno = ENOENT;
        return NULL;
    }

    // ④ 分配 RTU 私有数据
    ctx->backend_data = (modbus_rtu_t *)pvPortMalloc(sizeof(modbus_rtu_t));
    ctx_rtu = (modbus_rtu_t *)ctx->backend_data;
    ctx_rtu->dev = pdev;       // ★ 核心桥接:UART_Device 指针存入上下文

    // ⑤ 保存设备名
    ctx_rtu->device = (char *)pvPortMalloc(strlen(device) + 1);
    strcpy(ctx_rtu->device, device);

    // ⑥ 保存串口配置参数
    ctx_rtu->baud = baud;
    ctx_rtu->parity = (parity == 'N' || parity == 'E' || parity == 'O') ? parity : 'N';
    ctx_rtu->data_bit = data_bit;
    ctx_rtu->stop_bit = stop_bit;
    ctx_rtu->confirmation_to_ignore = FALSE;

    return ctx;
}

注意:malloc/free 已全部替换为 pvPortMalloc/vPortFree(FreeRTOS 线程安全的内存分配)。


七、Step 5:替换 malloc → pvPortMalloc

整个 libmodbus 源码(modbus.cmodbus-data.cmodbus-st-rtu.c)中的 malloc/free 必须统一替换为 FreeRTOS 接口:

c 复制代码
// 原版              替换为
malloc(size)    →    pvPortMalloc(size)
free(ptr)       →    vPortFree(ptr)

工具提示 :用 VS Code 或 Keil 的全局搜索替换,关键字 malloc(pvPortMalloc(free(vPortFree(


八、Step 6:工程集成 + 从机任务

8.1 文件清单

操作 文件
✓ 保留 modbus.h, modbus.c, modbus-private.h
✓ 保留 modbus-rtu.h, modbus-rtu-private.h
✓ 保留 modbus-st-rtu.c, modbus-data.c, modbus-version.h
✓ 保留 errno.h, errno.c, errno-base.h
✗ 删除 modbus-rtu.c (POSIX 原版, 含 <termios.h>)
✗ 删除 modbus-tcp.c, modbus-tcp.h, modbus-tcp-private.h

8.2 Keil 工程配置

  • Include Paths 添加 ..\Middlewares\Third_Party\libmodbus
  • 新建分组 Middlewares/libmodbus,添加保留的 .c 文件
  • 确认 modbus-rtu.c 不在编译列表中

8.3 CubeMX DMA 补充

UART2 需要双向 DMA(当前只有 TX):

复制代码
USART2 → DMA Settings → 添加 RX 通道
UART4 → DMA Settings → 添加 TX 通道(后续需要)

8.4 从机任务

c 复制代码
static void ModbusServerTask(void *pvParameters)
{
    modbus_t *ctx;
    modbus_mapping_t *mb_mapping;
    uint8_t *query;
    int rc;

    ctx = modbus_new_st_rtu("uart2", 115200, 'N', 8, 1);
    modbus_set_slave(ctx, 1);
    query = pvPortMalloc(MODBUS_RTU_MAX_ADU_LENGTH);

    mb_mapping = modbus_mapping_new_start_address(
        0, 10, 0, 10, 0, 10, 0, 10);
    memset(mb_mapping->tab_bits, 0, mb_mapping->nb_bits);
    memset(mb_mapping->tab_registers, 0x55, mb_mapping->nb_registers * 2);

    rc = modbus_connect(ctx);
    if (rc == -1) {
        modbus_free(ctx);
        vTaskDelete(NULL);
    }

    for (;;) {
        do {
            rc = modbus_receive(ctx, query);
        } while (rc == 0);

        if (rc < 0) continue;
        modbus_reply(ctx, query, rc, mb_mapping);

        if (mb_mapping->tab_bits[0])
            HAL_GPIO_WritePin(GPIOC, GPIO_PIN_12, GPIO_PIN_RESET);
        else
            HAL_GPIO_WritePin(GPIOC, GPIO_PIN_12, GPIO_PIN_SET);
    }
}

九、常见坑

9.1 编译报 <termios.h> 找不到

说明 modbus-rtu.c(POSIX 原版)被编译了。从工程中删除它,只保留 modbus-st-rtu.c

9.2 链接错误:_modbus_rtu_send 重复定义

modbus-rtu.cmodbus-st-rtu.c 同时存在。删除 modbus-rtu.c

9.3 modbus_new_st_rtu 返回 NULL

检查 g_uart_devices[] 数组中是否有对应设备名的 entry。GetUARTDevice 返回 NULL 时构造函数会清理内存并返回 NULL。

9.4 errno-base.h 找不到

errno.h 依赖 errno-base.h,后者定义了 EPERMEIO 等宏。确保三个 errno 文件都在工程中。

9.5 收不到 ModbusPoll 请求

  • UART2 RX DMA 是否已在 CubeMX 中配置?
  • ST-Link 虚拟串口对应的 COM 口号是否正确?

十、结尾

本篇完成了 libmodbus 从 POSIX 到 STM32H5 的完整移植:

  1. modbus-rtu.cmodbus-st-rtu.c,裁剪所有 POSIX 代码
  2. 六个核心函数(send/recv/connect/flush/select/close)替换为 UART_Device OOP 调用
  3. modbus_new_st_rtu 新增串口参数,通过 GetUARTDevice 桥接
  4. mallocpvPortMalloc 适配 FreeRTOS
相关推荐
爱吃的小肥羊1 小时前
刚刚,Codex 上线手机端,免费用户也能用!
aigc·openai·ai编程
Biocloudy1 小时前
循环肿瘤细胞的分离和分型技术
人工智能·经验分享·笔记·其他
REDcker1 小时前
嵌入式MCU内存布局详解 Flash SRAM Keil MAP与启动分散加载实践
单片机·嵌入式硬件
Undergoer_TW2 小时前
【SLAM性能评估笔记】公开的Vo性能评估工具调研与局限性分析
笔记·evo·kitti·vo·性能评估·tum
空太Jun2 小时前
Git 使用学习笔记
笔记·git·学习
qdprobot2 小时前
【无标题】
人工智能·单片机·嵌入式硬件·51单片机·硬件工程·iot·mixly
智者知已应修善业2 小时前
【51单片机独立按键控制数码管自增自减】2023-10-5
c++·经验分享·笔记·算法·51单片机
SHARK_pssm2 小时前
【数据结构——复杂度】
c语言·数据结构·经验分享·笔记