目录
- 一、前言
- [二、RS485 控制引脚核心原理](#二、RS485 控制引脚核心原理)
- [三、CubeMX 工程配置](#三、CubeMX 工程配置)
- 四、串口设备封装:面向对象编程实现
- [五、串口核心函数实现:适配 RS485 与 FreeRTOS](#五、串口核心函数实现:适配 RS485 与 FreeRTOS)
- 六、中断回调函数:完成收发闭环
- 七、总结
- 八、结尾
一、前言
在低成本 Modbus 传感器开发场景中,STM32F030CCT6 搭配 RS485 通信是核心方案之一。本次笔记聚焦 STM32F030 的串口编程,重点讲解 RS485 控制引脚的配置逻辑、基于面向对象思想的串口设备封装,以及适配 FreeRTOS 的串口收发函数实现,解决低成本芯片下 RS485 方向切换、多串口代码复用的核心问题。
二、RS485 控制引脚核心原理
本开发板选用低成本的 SIT3088ETK RS485 转换芯片,需通过 GPIO 引脚手动控制数据收发方向:
- 接收数据时:控制引脚设置为低电平;
- 发送数据时:控制引脚设置为高电平;
- 空闲状态:默认设置为低电平,保持接收状态。
补充:此前 H5 芯片方案使用 MAX13487EESA 芯片,支持自动切换收发方向,无需额外控制引脚;本次为适配低成本 Modbus 传感器需求,选用需手动控制的 SIT3088ETK 芯片,需重点处理方向切换逻辑。
三、CubeMX 工程配置
1. 串口与 GPIO 配置
- 使能串口 1,确认默认引脚与硬件原理图匹配;
- 配置 PA8 为GPIO_Output模式,作为 RS485 控制引脚;
配置截图如下:

2. 系统功能配置
- 使能 FreeRTOS,为串口中断收发提供任务调度与同步机制;

- SYS 模块中使能Debug Serial Wire,保留调试功能;

3. 工程文件整理
复制 Module_driver 文件夹到工程目录,仅保留uart_device.c与uart_device.h文件,精简工程结构,聚焦串口设备封装逻辑。
4. 编程工具配置
使用 Source Insight 进行代码开发:新建工程→添加uart_device.c/.h等核心文件→同步工程索引,便于代码编辑与函数跳转。
注:代码原作者韦东山,仅作学习记录
四、串口设备封装:面向对象编程实现
1. 串口设备结构体初始化
uart_device.c中先定义串口 1 设备结构体,预留私有数据字段(后续绑定 RS485 控制引脚、串口句柄等信息):
c
#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结构体,封装串口的名称、操作函数与私有数据,体现面向对象编程思想:
c
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 引脚、收发队列 / 信号量等),实现不同串口的差异化管理:
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
仿照串口 2/4 的函数逻辑,基于私有数据实现串口 1 的核心操作函数,重点增加 RS485 控制引脚的方向切换:
1. 初始化函数:创建同步对象 + 配置 RS485 初始状态
c
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 为发送模式 + 中断发送
c
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 队列读取数据
c
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. 缓冲区刷新函数:清空接收队列
c
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 设备结构体:绑定私有数据
c
// 最终版串口1设备结构体:绑定所有操作函数与私有数据
struct UART_Device g_uart1_dev = {"uart1", stm32_uart_init, stm32_uart_send, stm32_uart_recv, stm32_uart_flush, &g_uart1_data};
补充:面向对象封装的核心优势 ------ 串口 1、2、4 共用
stm32_uart_init/send/recv/flush函数,仅通过priv_data区分硬件参数,大幅减少代码冗余。
六、中断回调函数:完成收发闭环
1. 发送完成回调:切换 RS485 为接收模式 + 释放信号量
c
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. 接收完成回调:将数据入队 + 重启中断接收
c
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);
}
}
七、总结
- STM32F030 串口编程需手动控制 SIT3088ETK 的 RS485 引脚,发送时置高、接收 / 空闲时置低;
- 采用面向对象封装思想,通过
UART_Device+ 私有数据实现多串口代码复用,降低冗余; - 结合 FreeRTOS 队列 / 信号量实现串口中断收发的同步管理,适配嵌入式实时场景。
八、结尾
本次完成了 STM32F030 串口的面向对象封装与 RS485 适配,这套方案既适配了低成本 Modbus 传感器的硬件需求,又通过代码复用提升了开发效率。掌握该封装思路,可快速适配不同型号串口与 RS485 芯片,大幅降低工业传感器通信层的开发成本。感谢各位的阅读,持续关注本系列笔记,一起完成低成本 Modbus 传感器的全流程开发!