基于STM32 HAL库 + FreeRTOS 实现的「串口设备抽象驱动」 ,核心是用C语言模拟面向对象思想,把串口硬件封装成统一的设备接口,实现上层应用和底层硬件解耦。
我会分3大部分讲解:
- 头文件(.h):规则定义、接口声明(做什么)
- 源文件(.c):硬件实现、中断处理、FreeRTOS同步(怎么做)
- 完整运行流程:从开机到收发数据的每一步动作
全程会标注C语言知识点 、STM32 HAL库知识点 、FreeRTOS知识点,并说明每一步和上一步的关联。
一、头文件 FY_uart_device.h 详解
作用 :定义串口设备的通用规则、对外暴露接口,不涉及具体硬件实现(相当于「设备说明书」)。
1. 头文件保护宏
cs
#ifndef __FY_UART_DEVICE_H
#define __FY_UART_DEVICE_H
知识点:C语言头文件重复包含保护
- 防止同一个头文件被多次#include,导致结构体/函数重复定义报错。
- 原理:第一次包含时__FY_UART_DEVICE_H未定义,执行宏定义;后续包含时直接跳过。
2. 标准整型头文件
#include <stdint.h>
知识点:固定宽度整型
- uint8_t = 无符号8位整型(0~255),是嵌入式开发的标准类型,比unsigned char更严谨。
3. 核心:串口设备抽象结构体 struct UART_Device
cs
struct UART_Device{
/* 1. 设备名字:属性 */
char *name;
/* 2. 初始化函数指针:方法 */
int (*init)(struct UART_Device *pDev, int bau, int datas, char parity, int stop);
/* 3. 发送函数指针:方法 */
int (*send)(struct UART_Device *pDev, uint8_t *datas,int len, int timeout_ms);
/* 4. 接收函数指针:方法 */
int (*recv)(struct UART_Device *pDev, uint8_t *data, int timeout_ms);
/* 5. 私有数据指针:属性 */
void* priv_data;
};
逐成员详解 + 核心知识点
- char *name
设备名称(如stm32_uart1),用于通过名字查找设备。 - 三个函数指针 (*init)/(*send)/(*recv)
✅ 核心知识点:C语言函数指针
- 函数指针 = 「指向函数的指针」,把函数作为结构体成员。
- 作用:统一接口,不同硬件实现不同逻辑(多态)。 比如:UART1和UART2的硬件操作不同,但上层调用的都是dev->send()。
- 参数含义:
- pDev:指向自身的结构体指针(面向对象的this指针)。
- 后续参数:波特率、数据位、超时等串口参数。
- void* priv_data
✅ 核心知识点:万能指针 void*
- void*可以指向任意类型的数据,用于存放「硬件相关的私有数据」。
- 作用:分离抽象层和硬件层,上层不用关心底层硬件细节。
4. 对外接口函数声明
cs
struct UART_Device *GetUARTDevice(char *name);
- 作用:通过设备名字,获取对应的串口设备指针(工厂模式)。
- 上层应用只调用这个函数,就能拿到设备,不用关心底层实现。
二、源文件 FY_uart_device.c 详解
作用:实现头文件定义的接口,完成STM32串口硬件操作、FreeRTOS同步、中断处理(相当于「设备的具体实现」)。
1. 头文件包含
cs
#include "FY_uart_device.h"
#include "stm32f1xx_hal.h"
#include "stm32f1xx_hal_uart.h"
#include "FreeRTOS.h"
#include "semphr.h"
#include "queue.h"
#include <string.h>
- 自己的头文件:引入设备结构体定义。
- HAL库:STM32硬件操作API。
- FreeRTOS:信号量(同步)、队列(数据缓存)。
- string.h:用于字符串比较strcmp。
2. 宏定义 + 外部变量声明
cs
#define UART_RX_QUEUE_LEN 100 // 接收队列长度(缓存100字节)
extern UART_HandleTypeDef huart1; // 外部声明CubeMX生成的UART1句柄
✅ 知识点: extern 外部变量
- huart1是CubeMX自动生成的全局串口句柄(包含GPIO、时钟、中断配置),这里声明「外部已有这个变量」,直接使用。
3. 全局设备提前声明
cs
struct UART_Device g_stm32_uart1;
- 提前声明:因为后面中断回调函数需要用到这个设备,所以先声明,后定义。
4. 私有数据结构体 struct UART_Data
cs
struct UART_Data{
UART_HandleTypeDef *handle; // STM32串口硬件句柄
SemaphoreHandle_t xTxsem; // FreeRTOS二值信号量(发送同步)
QueueHandle_t xRxQueue; // FreeRTOS队列(接收缓存)
uint8_t rxdata; // 单字节接收缓存
};
✅ 作用 :存放硬件相关的私有数据,和上层抽象结构体分离。
✅ 关联:这个结构体的指针,最终会赋值给struct UART_Device的priv_data成员。
5. 核心:STM32 HAL库串口中断回调函数
STM32串口收发完成后,硬件自动触发中断,HAL库会调用这两个回调函数(用户重写实现自定义逻辑)。
5.1 发送完成回调 HAL_UART_TxCpltCallback
cs
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
struct UART_Data *data;
// 判断:是否是UART1的中断
if(huart == &huart1)
{
// 从抽象设备中,取出私有数据
data = g_stm32_uart1.priv_data;
// 中断中释放二值信号量
xSemaphoreGiveFromISR(data->xTxsem, NULL);
}
}
逐行讲解 + 知识点
- huart == &huart1:过滤中断,只处理UART1。
- data = g_stm32_uart1.priv_data:
✅ 关联:从抽象设备拿到私有数据(信号量、队列)。 - xSemaphoreGiveFromISR:
✅ FreeRTOS知识点:
- 中断中必须用FromISR结尾的API。
- 二值信号量:用于任务和中断的同步(中断通知任务:发送完成)。
5.2 接收完成回调 HAL_UART_RxCpltCallback
cs
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
struct UART_Data *data;
if(huart == &huart1)
{
data = g_stm32_uart1.priv_data;
// 中断中把接收到的1字节写入队列
xQueueSendFromISR(data->xRxQueue, &data->rxdata, NULL);
// 再次启动接收中断(循环接收,否则只能收1次)
HAL_UART_Receive_IT(data->handle, &data->rxdata, 1);
}
}
逐行讲解 + 知识点
- xQueueSendFromISR:
✅ FreeRTOS队列:中断把数据写入队列,任务从队列读取(解耦中断和任务)。 - HAL_UART_Receive_IT:
STM32中断接收默认只触发1次,必须重新启动才能持续接收。 - 关联:接收的数据存入rxdata,再写入队列,供上层recv函数读取。
6. 底层硬件实现函数(对应头文件的函数指针)
这三个函数是真正操作STM32硬件的逻辑,会赋值给抽象结构体的init/send/recv函数指针。
6.1 串口初始化函数 stm32_uart_init
cs
static int stm32_uart_init(struct UART_Device *pDev, int bau, int datas,char parity, int stop)
{
// 1. 取出私有数据
struct UART_Data* data = pDev->priv_data;
// 2. 创建二值信号量(发送同步)
data->xTxsem = xSemaphoreCreateBinary();
// 3. 创建接收队列(长度100,每元素1字节)
data->xRxQueue = xQueueCreate(UART_RX_QUEUE_LEN, 1);
// 4. 启动第一次接收中断
HAL_UART_Receive_IT(data->handle, &data->rxdata, 1);
return 0;
}
✅ 核心作用:初始化FreeRTOS同步对象,启动串口接收。
✅ 关联:pDev->priv_data指向struct UART_Data,直接操作硬件和同步对象。
6.2 串口发送函数 stm32_uart_send
cs
static int stm32_uart_send(struct UART_Device *pDev, uint8_t *datas,int len, int timeout_ms)
{
struct UART_Data* data = pDev->priv_data;
// 1. 启动中断发送(仅触发硬件,不阻塞)
HAL_UART_Transmit_IT(data->handle, datas, len);
// 2. 阻塞等待:直到发送完成中断释放信号量
if(pdTRUE == xSemaphoreTake(data->xTxsem, timeout_ms))
return 0;/* 成功 */
else
return -1;
}
逐行讲解
- HAL_UART_Transmit_IT:启动中断模式发送,硬件自动发送数据。
- xSemaphoreTake:
任务阻塞等待信号量,直到中断发送完成,才继续执行。
✅ 同步逻辑:任务发起发送 → 硬件中断发送 → 中断释放信号量 → 任务继续。
6.3 串口接收函数 stm32_uart_recv
cs
static int stm32_uart_recv(struct UART_Device *pDev, uint8_t *data, int timeout_ms)
{
struct UART_Data *uart_data = pDev->priv_data;
// 阻塞读取队列:直到中断写入数据
if(pdPASS == xQueueReceive(uart_data->xRxQueue, data, timeout_ms))
return 0;
else
return -1;
}
✅ 接收逻辑:任务读取队列 → 阻塞等待 → 中断收到数据写入队列 → 任务拿到数据。
7. 实例化串口设备(绑定抽象层和硬件层)
这一步是把所有零散的模块组装成一个完整的设备。
7.1 初始化私有数据
cs
static struct UART_Data g_stm32_uart1_data = {
.handle = &huart1, // 绑定UART1硬件句柄
};
7.2 初始化抽象设备
cs
static struct UART_Device g_stm32_uart1 = {
.name = "stm32_uart1", // 设备名
.init = stm32_uart_init, // 绑定初始化函数
.send = stm32_uart_send, // 绑定发送函数
.recv = stm32_uart_recv, // 绑定接收函数
.priv_data = &g_stm32_uart1_data, // 绑定私有数据
};
✅ 关键关联:
- 函数指针 → 底层硬件实现函数
- priv_data → 私有数据结构体(硬件+FreeRTOS对象)
- 这就是C语言模拟面向对象的核心:属性+方法封装。
7.3 对外设备数组
cs
struct UART_Device *g_uart_devs[] = {&g_stm32_uart1};
- 把所有串口设备放入数组,方便遍历查找。
7.4 设备查找函数
cs
struct UART_Device *GetUARTDevice(char *name)
{
// 遍历设备数组
for(int i=0; i<sizeof(g_uart_devs)/sizeof(g_uart_devs[0]); i++)
{
// 字符串比较:匹配名字则返回设备指针
if(0 == strcmp(name, g_uart_devs[i]->name))
return g_uart_devs[i];
}
return NULL; // 未找到
}
✅ 上层入口:应用层只需要调用GetUARTDevice("stm32_uart1"),就能拿到设备。