在工业物联网与传感器网络开发中,Modbus协议凭借其简单、可靠的特点成为事实上的通信标准。然而,在STM32F030这类低成本MCU上实现RS485通信,开发者往往需要手动控制方向引脚、处理中断与RTOS的协同。本文以实战为导向,深入讲解如何用面向对象思想封装串口设备,并结合FreeRTOS实现高效的收发闭环。无论你是刚接触嵌入式开发的新手,还是希望提升代码复用性的老手,这套方案都能助你快速落地。
一、RS485控制引脚的核心原理
本方案选用SIT3088ETK作为RS485转换芯片,这是一款低成本、低功耗的差分收发器。与标准的UART不同,RS485半双工通信要求MCU通过一个GPIO引脚手动切换收发方向:
- 接收数据时:控制引脚置为低电平,使能接收器。
- 发送数据时:控制引脚置为高电平,使能驱动器。
- 空闲状态:默认保持低电平,确保不干扰总线。
这种设计虽然增加了软件复杂度,但能有效降低硬件成本,尤其适合大批量生产的Modbus传感器。实践提示:在切换方向时,务必加入微秒级的延时(如10μs),避免总线冲突。
补充:此前 H5 芯片方案使用 MAX13487EESA 芯片,支持自动切换收发方向,无需额外控制引脚;本次为适配低成本 Modbus 传感器需求,选用需手动控制的 SIT3088ETK 芯片,需重点处理方向切换逻辑。
二、CubeMX工程配置详解
正确配置STM32CubeMX是后续开发的基础。以下是关键步骤:
1. 串口与GPIO配置
- 使能USART1,确认TX(PA9)和RX(PA10)引脚与硬件原理图一致。
- 将PA8配置为GPIO_Output模式,用于RS485方向控制。

2. 系统功能配置
- 使能FreeRTOS,为串口中断收发提供任务调度与同步机制(队列、信号量)。
- 在SYS模块中使能Debug Serial Wire,保留SWD调试功能,便于后期排查。


3. 工程文件整理
复制Module_driver文件夹到工程目录,仅保留uart_device.c与uart_device.h文件,精简工程结构,聚焦串口设备封装逻辑。⚠️注意:删减文件前请确认无依赖关系。
4. 编程工具配置
使用Source Insight进行代码开发:新建工程→添加uart_device.c/.h等核心文件→同步工程索引。该工具能极大提升代码浏览与函数跳转效率,尤其适合管理多文件项目。
注:代码原作者韦东山,仅作学习记录
三、面向对象的串口设备封装
为了在多串口场景下复用代码,我们采用类似TypeScript或Java的面向对象思想,将串口抽象为“设备对象”。
1. 串口设备结构体初始化
在uart_device.c中先定义串口1设备结构体,预留私有数据字段(后续绑定RS485控制引脚、串口句柄等):
#include <stdio.h>
#include <string.h>
#include "uart_device.h"
// 定义串口1设备结构体,xxx为私有数据占位符,后续绑定具体设备信息
struct UART_Device g_uart1_dev = {"uart1", stm32_uart_init, stm32_uart_send, stm32_uart_recv, stm32_uart_flush, xxx};
// 串口设备列表(示例,需根据实际工程调整)
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. 串口设备核心结构体定义
在uart_device.h中定义UART_Device结构体,封装串口的名称、操作函数与私有数据:
struct UART_Device {
char *name; // 设备名称(如"uart1")
int (*Init)( struct UART_Device *pDev, int baud, char parity, int data_bit, int stop_bit); // 初始化函数
int (*Send)( struct UART_Device *pDev, uint8_t *datas, uint32_t len, int timeout); // 发送函数
int (*RecvByte)( struct UART_Device *pDev, uint8_t *data, int timeout); // 字节接收函数
int (*Flush)(struct UART_Device *pDev); // 缓冲区刷新函数
void *priv_data; // 私有数据(存储串口句柄、RS485引脚等专属信息)
};
3. 串口私有数据结构体定义
定义UART_Data结构体,存储串口1的专属信息(句柄、RS485引脚、收发队列/信号量等),实现不同串口的差异化管理。这种模式与Python中的类封装异曲同工,但更适应C语言的静态特性。
// 串口私有数据结构体:存储串口专属的硬件信息与同步对象
struct UART_Data{
UART_HandleTypeDef *huart; // 串口句柄
GPIO_TypeDef* GPIOx_485; // RS485控制引脚GPIO组
uint16_t GPIO_Pin_485; // RS485控制引脚编号
QueueHandle_t xRxQueue; // 接收队列(FreeRTOS)
QueueHandle_t xTxSem; // 发送信号量(FreeRTOS)
uint8_t rxdata; // 接收缓存字节
};
// 初始化串口1私有数据:绑定串口1句柄、PA8为RS485控制引脚
static struct UART_Data g_uart1_data = {
&huart1,
GPIOA,
GPIO_PIN_8,
};
补充:私有数据字段的设计核心价值在于 —— 串口 1、2、4 可复用同一套收发函数逻辑,仅通过私有数据区分硬件参数,无需为每个串口编写独立代码。
四、核心函数实现:适配RS485与FreeRTOS
以下函数基于私有数据实现串口1的核心操作,重点增加RS485方向切换逻辑。
1. 初始化函数:创建同步对象 + 配置RS485初始状态
static int stm32_uart_init(struct UART_Device *pDev, int baud, char parity, int data_bit, int stop_bit)
{
// 获取串口1私有数据
struct UART_Data * uart_data = pDev->priv_data;
// 首次初始化时创建接收队列与发送信号量
if (!uart_data->xRxQueue)
{
uart_data->xRxQueue = xQueueCreate(200, 1); // 创建接收队列(200字节)
uart_data->xTxSem = xSemaphoreCreateBinary( ); // 创建发送完成信号量
// RS485初始状态:低电平,保持接收模式
HAL_GPIO_WritePin(uart_data->huart, uart_data->GPIO_Pin_485, GPIO_PIN_RESET);
// 启动串口中断接收
HAL_UART_Receive_IT(uart_data->huart, &uart_data->rxdata, 1);
}
return 0;
}
2. 发送函数:切换RS485为发送模式 + 中断发送
static int stm32_uart_send(struct UART_Device *pDev, uint8_t *datas, uint32_t len, int timeout)
{
// 获取串口1私有数据
struct UART_Data * uart_data = pDev->priv_data;
// RS485切换为发送模式:高电平
HAL_GPIO_WritePin(uart_data->huart, uart_data->GPIO_Pin_485, GPIO_PIN_SET);
// 启动串口中断发送
HAL_UART_Transmit_IT(uart_data->huart, datas, len);
// 等待发送完成信号量,超时则返回失败
if (pdTRUE == xSemaphoreTake(uart_data->xTxSem, timeout))
return 0;
else
return -1;
}
3. 接收函数:从FreeRTOS队列读取数据
static int stm32_uart_recv(struct UART_Device *pdev, uint8_t *pData, int timeout)
{
// 获取串口1私有数据
struct UART_Data * uart_data = pDev->priv_data;
// 从接收队列读取1字节数据,超时则返回失败
if (pdPASS == xQueueReceive(uart_data->xRxQueue, pData, timeout))
return 0;
else
return -1;
}
4. 缓冲区刷新函数:清空接收队列
static int stm32_uart_flush(struct UART_Device *pdev)
{
// 获取串口1私有数据
struct UART_Data * uart_data = pDev->priv_data;
int cnt = 0;
uint8_t data;
// 循环清空接收队列,返回清空的字节数
while (1)
{
if (pdPASS != xQueueReceive(uart_data->xRxQueue, &data, 0))
break;
cnt++;
}
return cnt;
}
5. 完善串口1设备结构体:绑定私有数据
// 最终版串口1设备结构体:绑定所有操作函数与私有数据
struct UART_Device g_uart1_dev = {"uart1", stm32_uart_init, stm32_uart_send, stm32_uart_recv, stm32_uart_flush, &g_uart1_data};
核心思想:通过函数指针和私有数据,将串口操作与具体硬件解耦。后续若需要适配Go或JavaScript风格的异步回调,只需修改私有数据中的队列/信号量实现即可。
[AFFILIATE_SLOT_1]补充:面向对象封装的核心优势 —— 串口 1、2、4 共用函数,仅通过区分硬件参数,大幅减少代码冗余。
五、中断回调函数:完成收发闭环
中断回调是串口通信的“最后一公里”,确保数据及时处理且不丢失。
1. 发送完成回调:切换RS485为接收模式 + 释放信号量
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
// 判断为串口1发送完成
if (huart == &huart1)
{
// 切换RS485为接收模式:低电平
HAL_GPIO_WritePin(uart_data->huart, uart_data->GPIO_Pin_485, GPIO_PIN_RESET);
// 获取串口1私有数据,释放发送完成信号量
struct UART_Data * uart_data = g_uart1_dev->priv_data;
xSemaphoreGiveFromISR(uart_data->xTxSem, NULL);
}
}
2. 接收完成回调:将数据入队 + 重启中断接收
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 判断为串口1接收完成(注:代码中示例为uart2,需根据实际工程调整)
if (huart == &huart2)
{
// 获取串口1私有数据
struct UART_Data * uart_data = g_uart1_dev->priv_data;
// 接收数据入队(中断上下文使用FromISR版本)
xQueueSendFromISR(uart_data->xRxQueue, (const void *)&g_uart2_rx_buf[i], NULL);
// 重启中断接收,等待下一字节
HAL_UART_Receive_IT(uart_data->huart, &uart_data->rxdata, 1);
}
}
✅设计要点:
- 发送完成后立即释放信号量,通知等待任务继续执行。
- 接收回调中采用中断方式逐字节入队,避免阻塞。
- RS485方向切换必须发生在中断上下文中,不能延迟。
六、总结
本文系统梳理了STM32F030串口编程的完整流程:从RS485引脚控制原理,到CubeMX配置,再到面向对象封装与FreeRTOS集成。关键收获包括:
- 手动控制方向:发送置高、接收/空闲置低,确保总线稳定。
- 面向对象封装:通过
uart_device_t+ 私有数据实现多串口代码复用,降低冗余。 - RTOS同步:利用FreeRTOS队列/信号量管理中断收发,提升实时性。
这套方案不仅适用于Modbus传感器,还可扩展至其他RS485应用场景。掌握该封装思路,你就能像使用Python或TypeScript一样高效地管理嵌入式通信层。
[AFFILIATE_SLOT_2]感谢你的阅读!欢迎持续关注本系列,一起完成低成本Modbus传感器的全流程开发。如果你在实践中有任何疑问,欢迎在评论区留言交流。
stm32_uart_init/send/recv/flushpriv_data
浙公网安备 33010602011771号