目录
- 一、前言
- [二、串口硬件配置:DMA 收发配置](#二、串口硬件配置:DMA 收发配置)
- [三、libmodbus 串口设备适配:核心函数改造](#三、libmodbus 串口设备适配:核心函数改造)
- 四、串口设备封装与管理:统一接口实现
- [五、收尾适配:flush 与 connect 函数完善](#五、收尾适配:flush 与 connect 函数完善)
- 六、总结
- 七、结尾
一、前言
此前我们已完成 USB 串口作为 libmodbus 通信后端的适配与验证,本次将通信后端切换为开发板板载 485 串口,核心工作是将封装好的面向对象 UART 源码与主机实验源码进行合并,改造 libmodbus 底层硬件操作函数,使其适配板载串口的收发逻辑,实现多串口(串口 2、串口 4)的灵活切换与统一管理。
二、串口硬件配置:DMA 收发配置
为保证板载串口数据收发的高效性,需先完成串口 DMA 配置:将串口 2 与串口 4 均配置为 DMA 收发模式,其中 CH0~CH4 的配置截图如下(按顺序展示):




配置完成后,将原有适配 USB 串口的文件复制至新文件中,替换掉与 USB 相关的底层收发逻辑,为板载串口适配做准备。
三、libmodbus 串口设备适配:核心函数改造
libmodbus 中数据传输通道需绑定具体串口设备,因此需先改造核心收发函数,使其能识别并调用对应串口的操作接口。
1. 发送函数初始改造
先标记_modbus_rtu_send函数的改造方向,明确需调用串口 2/4 的 UART_Device 发送接口:
c
static ssize_t _modbus_rtu_send(modbus_t *ctx, const uint8_t *req, int req_length)
{
/*使用UART2或UART4的UART_Device来发送数据*/
return 0; // write(ctx->s, req, req_length);
}
2. 串口设备识别与绑定
在modbus_new_st_rtu函数内添加设备指针定义,实现 USB 串口与板载串口的分支判断,识别并获取指定板载串口设备:
c
modbus_new_st_rtu(const char *device, int baud, char parity, int data_bit, int stop_bit)
{
//其他部分未展示
struct UART_Device *pdev;
//其他部分未展示
// 分支判断:USB串口使用专属后端,板载串口绑定UART后端并获取设备指针
if (!strcmp(device, "usb"))
ctx->backend = &_modbus_rtu_backend_usbserial;
else
{
ctx->backend = &_modbus_rtu_backend_uart;
// 根据设备名称(如"uart2"/"uart4")获取对应的串口设备结构体
pdev = GetUARTDevice((char *)device);
if(!pdev)
{
modbus_free(ctx);
errno = ENOENT;
return NULL;
}
}
}
3. 扩展 modbus_rtu 结构体记录设备
修改_modbus_rtu结构体定义,新增串口设备指针字段,用于存储识别到的板载串口设备:
c
typedef struct _modbus_rtu {
struct UART_Device *dev; // 新增:存储绑定的串口设备指针
} modbus_rtu_t;
4. 设备指针赋值
回到modbus_new_st_rtu函数,将获取到的串口设备指针赋值给 modbus_rtu 结构体,完成设备绑定:
c
ctx_rtu = (modbus_rtu_t *) ctx->backend_data;
ctx_rtu->dev = pdev; // 将识别到的串口设备指针存入上下文
5. 完善发送函数
基于绑定的串口设备指针,完善_modbus_rtu_send函数,使其自动调用对应串口的发送接口:
c
static ssize_t _modbus_rtu_send(modbus_t *ctx, const uint8_t *req, int req_length)
{
/*使用UART2或UART4的UART_Device来发送数据*/
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, TIMEROUT_SEND_MSG))
return req_length;
else
{
errno = EIO;
return -1;
}
}
6. 接收函数适配
接收函数与发送函数逻辑对应,调用串口设备的接收接口实现数据读取:
c
static ssize_t _modbus_rtu_recv(modbus_t *ctx, uint8_t *rsp, int rsp_length, int timeout)
{
/*使用UART2或UART4的UART_Device来接收数据*/
modbus_rtu_t *ctx_rtu = ctx->backend_data;
struct UART_Device *pdev = ctx_rtu->dev; // 获取绑定的串口设备
// 调用串口设备的字节接收接口,成功返回1(接收1字节),失败设置错误码
if(0 == pdev->RecvByte(pdev, rsp, timeout))
return 1;
else
{
errno = EIO;
return -1;
}
}
补充:发送 / 接收函数的核心改造思路是 "解耦 libmodbus 协议层与硬件层",通过设备指针调用统一的 UART_Device 接口,无需修改协议层逻辑即可适配不同串口。
四、串口设备封装与管理:统一接口实现
仿照板载串口的封装方式,为 USB 串口完善配套函数(在 ux_devcie_cdc_acm.c 中实现),保证接口一致性:
c
// USB串口初始化接口
static int USBSerial_Init(struct UART_Device *pDev, int baud, char parity, int data_bit, int stop_bit)
{
return 0;
}
// USB串口发送接口
static int USBSerial_Send(struct UART_Device *pDev, uint8_t *datas, uint32_t len, int timeout)
{
return ux_device_cdc_acm_send(datas, len, timeout);
}
// USB串口数据读取接口
int USBSerial_GetData(struct UART_Device *pdev, uint8_t *pData, int timeout)
{
return ux_device_cdc_acm_getchar(pData, timeout);
}
// USB串口缓冲区刷新接口
int USBSerial_Flush(struct UART_Device *pdev)
{
return ux_device_cdc_acm_flush();
}
// 定义USB串口设备结构体
struct UART_Device g_usbserial_dev = {"usb", USBSerial_Init, USBSerial_Send, USBSerial_GetData, USBSerial_Flush};
在管理串口设备的文件中,统一声明所有串口设备并实现设备查找函数,支持通过设备名称快速匹配:
c
// 声明各串口设备结构体
extern struct UART_Device g_uart2_dev;
extern struct UART_Device g_uart4_dev;
extern struct UART_Device g_usbserial_dev;
// 串口设备列表,统一管理
static struct UART_Device *g_uart_devices[] = {&g_uart2_dev, &g_uart4_dev, &g_usbserial_dev};
// 根据设备名称查找对应的串口设备结构体
struct UART_Device *GetUARTDevice(char *name)
{
int i = 0;
for (i = 0; i < sizeof(g_uart_devices)/sizeof(g_uart_devices[0]); i++)
{
if (!strcmp(name, g_uart_devices[i]->name))
return g_uart_devices[i];
}
return NULL;
}
板载串口 2/4 的 Flush 函数参考 USB 串口实现,清空接收队列完成缓冲区刷新:
c
// 串口2缓冲区刷新:清空接收队列
int UART2_Flush(struct UART_Device *pdev)
{
int cnt = 0;
uint8_t data;
while (1)
{
if (pdPASS != xQueueReceive(g_Uart2_Rx_Queue, &data, 0))
break;
cnt++;
}
return cnt;
}
// 串口4缓冲区刷新:清空接收队列
int UART4_Flush(struct UART_Device *pdev)
{
int cnt = 0;
uint8_t data;
while (1)
{
if (pdPASS != xQueueReceive(g_Uart4_Rx_Queue, &data, 0))
break;
cnt++;
}
return cnt;
}
// 定义串口2/4设备结构体,绑定对应操作接口
struct UART_Device g_uart2_dev = {"uart2", UART2_Rx_Start, UART2_Send, UART2_GetData, UART2_Flush};
struct UART_Device g_uart4_dev = {"uart4", UART4_Rx_Start, UART4_Send, UART4_GetData, UART4_Flush};
补充:若无需使用 USB 串口,可删除代码中所有带有 usbserial 的函数与设备声明,仅保留串口 2/4 的管理逻辑,精简工程代码。
五、收尾适配:flush 与 connect 函数完善
1. 完善缓冲区刷新函数
将_modbus_rtu_flush函数适配为调用串口设备的 Flush 接口,统一缓冲区刷新逻辑:
c
static int _modbus_rtu_flush(modbus_t *ctx)
{
modbus_rtu_t *ctx_rtu = ctx->backend_data;
struct UART_Device *pdev = ctx_rtu->dev; // 获取绑定的串口设备
// 调用串口设备的Flush接口完成缓冲区清空
return pdev->Flush(pdev);
}
2. 完善连接函数
改造_modbus_rtu_connect函数,调用串口设备的初始化接口完成参数配置:
c
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;
}
完成所有函数改造后,工程编译无误,说明板载串口作为 libmodbus 通信后端的代码修改已成功实现。
六、总结
- 板载 485 串口适配的基础是配置 DMA 收发,保证数据传输效率;
- 核心改造逻辑是为 libmodbus 绑定串口设备指针,通过统一的 UART_Device 接口调用硬件操作;
- 实现串口设备统一管理,支持 USB / 串口 2 / 串口 4 的灵活切换;
- 完善 flush、connect 等配套函数,保证通信链路的完整性。
七、结尾
本次完成了 libmodbus 适配板载 485 串口后端的核心改造,实现了多串口的统一管理与灵活切换,至此 libmodbus 在 STM32 平台已支持 USB、板载 485 等多种通信后端。这套面向对象的串口适配思路,可直接复用至其他开源协议栈的移植工作中,大幅提升嵌入式通信开发的效率。感谢各位的阅读,持续关注本系列笔记,一起探索更多嵌入式串口通信与协议栈移植的实战技巧!