libmodbus STM32 移植(板载 485 串口作后端)

目录

一、前言

此前我们已完成 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 通信后端的代码修改已成功实现。

六、总结

  1. 板载 485 串口适配的基础是配置 DMA 收发,保证数据传输效率;
  2. 核心改造逻辑是为 libmodbus 绑定串口设备指针,通过统一的 UART_Device 接口调用硬件操作;
  3. 实现串口设备统一管理,支持 USB / 串口 2 / 串口 4 的灵活切换;
  4. 完善 flush、connect 等配套函数,保证通信链路的完整性。

七、结尾

本次完成了 libmodbus 适配板载 485 串口后端的核心改造,实现了多串口的统一管理与灵活切换,至此 libmodbus 在 STM32 平台已支持 USB、板载 485 等多种通信后端。这套面向对象的串口适配思路,可直接复用至其他开源协议栈的移植工作中,大幅提升嵌入式通信开发的效率。感谢各位的阅读,持续关注本系列笔记,一起探索更多嵌入式串口通信与协议栈移植的实战技巧!

相关推荐
时代的凡人6 小时前
0208晨间笔记
笔记
今天只学一颗糖7 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
testpassportcn7 小时前
AWS DOP-C02 認證完整解析|AWS DevOps Engineer Professional 考試
网络·学习·改行学it
不做无法实现的梦~8 小时前
ros2实现路径规划---nav2部分
linux·stm32·嵌入式硬件·机器人·自动驾驶
游乐码10 小时前
c#变长关键字和参数默认值
学习·c#
饭碗、碗碗香11 小时前
【Python学习笔记】:Python的hashlib算法简明指南:选型、场景与示例
笔记·python·学习
Wils0nEdwards12 小时前
初中化学1
笔记
魔力军12 小时前
Rust学习Day4: 所有权、引用和切片介绍
开发语言·学习·rust
wubba lubba dub dub75012 小时前
第三十六周 学习周报
学习
学编程的闹钟12 小时前
PHP字符串表示方式全解析
学习