STM32-UART封装问题解析

基于STM32 HAL库 + FreeRTOS 实现的「串口设备抽象驱动」 ,核心是用C语言模拟面向对象思想,把串口硬件封装成统一的设备接口,实现上层应用和底层硬件解耦。
我会分3大部分讲解:

  1. 头文件(.h):规则定义、接口声明(做什么)
  2. 源文件(.c):硬件实现、中断处理、FreeRTOS同步(怎么做)
  3. 完整运行流程:从开机到收发数据的每一步动作
    全程会标注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;
};

逐成员详解 + 核心知识点

  1. char *name
    设备名称(如stm32_uart1),用于通过名字查找设备
  2. 三个函数指针 (*init)/(*send)/(*recv)
    核心知识点:C语言函数指针
  • 函数指针 = 「指向函数的指针」,把函数作为结构体成员。
  • 作用:统一接口,不同硬件实现不同逻辑(多态)。 比如:UART1和UART2的硬件操作不同,但上层调用的都是dev->send()。
  • 参数含义:
    • pDev:指向自身的结构体指针(面向对象的this指针)。
  • 后续参数:波特率、数据位、超时等串口参数。
  1. 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);
    }
}

逐行讲解 + 知识点

  1. huart == &huart1:过滤中断,只处理UART1。
  2. data = g_stm32_uart1.priv_data:
    关联:从抽象设备拿到私有数据(信号量、队列)。
  3. 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);
    }
}

逐行讲解 + 知识点

  1. xQueueSendFromISR:
    FreeRTOS队列:中断把数据写入队列,任务从队列读取(解耦中断和任务)。
  2. HAL_UART_Receive_IT:
    STM32中断接收默认只触发1次,必须重新启动才能持续接收。
  3. 关联:接收的数据存入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;
}

逐行讲解

  1. HAL_UART_Transmit_IT:启动中断模式发送,硬件自动发送数据。
  2. 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"),就能拿到设备。

相关推荐
星幻元宇VR1 小时前
VR航空航天科普设备助力航天知识普及
人工智能·科技·学习·安全·vr·虚拟现实
寒秋花开曾相惜2 小时前
(学习笔记)4.2 逻辑设计和硬件控制语言HCL(4.2.1 逻辑门&4.2.2 组合电路和HCL布尔表达式)
linux·网络·数据结构·笔记·学习·fpga开发
叶子野格2 小时前
《C语言学习:指针》12
c语言·开发语言·c++·学习·visual studio
光影少年2 小时前
前端线上屏幕出现卡顿如何排查?
开发语言·前端·javascript·学习·前端框架·node.js
Yeh2020582 小时前
request与response笔记
java·前端·笔记
国产化创客2 小时前
龙芯 2K0300-- 实现工业网关监控仪表盘项目
嵌入式硬件·物联网·数据可视化
项目題供诗3 小时前
STM32-OLED显示屏(六)
stm32·单片机·嵌入式硬件
Jiangxl~3 小时前
IP数据云如何为不同行业提供精准IP查询与风险防控解决方案?
网络·网络协议·tcp/ip·算法·ai·ip·安全架构
Fuyo_11193 小时前
C++ 内存管理
c++·笔记