目录
- 一、前言
- 二、移植总思路
- [三、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_registers、modbus_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 队列自带超时 ------xQueueReceive 和 xSemaphoreTake 本身就有 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.c、modbus-data.c、modbus-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.c 和 modbus-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,后者定义了 EPERM、EIO 等宏。确保三个 errno 文件都在工程中。
9.5 收不到 ModbusPoll 请求
- UART2 RX DMA 是否已在 CubeMX 中配置?
- ST-Link 虚拟串口对应的 COM 口号是否正确?
十、结尾
本篇完成了 libmodbus 从 POSIX 到 STM32H5 的完整移植:
modbus-rtu.c→modbus-st-rtu.c,裁剪所有 POSIX 代码- 六个核心函数(send/recv/connect/flush/select/close)替换为
UART_DeviceOOP 调用 modbus_new_st_rtu新增串口参数,通过GetUARTDevice桥接malloc→pvPortMalloc适配 FreeRTOS