STM32 UART FIFO发送接收 GCC编译器重定向printf
cubemx配置
这里只需要把串口中断打开就好
代码详解
UART_TOOLS
上次写的函数有个重定义问题 这里做出改进 现在不需要在主函数里添加引用或者设置编译器 会自动识别使用哪种prinf定向
/**
* @file uart_tools.c
* @brief UART 辅助工具函数实现
*/
#include "uart_tools.h"
#include <string.h>
#include <stdio.h>
#include <math.h>
#include "string.h"
extern UART_HandleTypeDef huart1;
/**
* @brief 通过 UART 发送浮点数
* @note 不使用 sprintf 以节省代码空间,纯整数运算实现
*/
void UART_SendFloat(UART_HandleTypeDef *huart, float num, uint8_t decimal_places)
{
char str[32]; // 临时存储 ASCII 字符串
int8_t idx = 0;
uint8_t is_negative = 0;
// 1. 处理 NaN 和 Inf (可选优化,这里简单处理负数)
if (num != num) { // Check for NaN
const char *nan_str = "NaN";
HAL_UART_Transmit(huart, (uint8_t*)nan_str, 3, HAL_MAX_DELAY);
return;
}
// 2. 处理负数
if (num < 0)
{
is_negative = 1;
num = -num;
}
// 3. 把浮点数放大为整数,避免逐位小数运算
// 注意:如果 decimal_places 过大可能导致溢出,建议限制在 6 以内
if (decimal_places > 6) decimal_places = 6;
float multiplier = 1.0f;
for (uint8_t i = 0; i < decimal_places; i++)
multiplier *= 10.0f;
// 四舍五入
uint32_t scaled = (uint32_t)(num * multiplier + 0.5f);
// 4. 分离整数和小数部分
uint32_t int_part = scaled / (uint32_t)multiplier;
uint32_t frac_part = scaled % (uint32_t)multiplier;
// 5. 构建字符串
// 添加负号
if (is_negative)
str[idx++] = '-';
// 转换整数部分(倒序转换再反转)
char temp[12];
uint8_t tlen = 0;
if (int_part == 0)
temp[tlen++] = '0';
while (int_part > 0)
{
temp[tlen++] = (int_part % 10) + '0';
int_part /= 10;
}
// 反向填入 str
for (int8_t i = tlen - 1; i >= 0; i--)
str[idx++] = temp[i];
// 添加小数点
str[idx++] = '.';
// 转换小数部分
char frac_str[10];
uint8_t flen = 0;
// 如果小数部分为0,直接补0
if (frac_part == 0) {
for(uint8_t k=0; k<decimal_places; k++) {
str[idx++] = '0';
}
} else {
// 正常转换小数位
while (frac_part > 0)
{
frac_str[flen++] = (frac_part % 10) + '0';
frac_part /= 10;
}
// 补够 decimal_places 位(从后往前填)
while (flen < decimal_places)
frac_str[flen++] = '0';
// 因为上面是逆序存的,需要反向输出到 str
for (int8_t i = flen - 1; i >= 0; i--)
str[idx++] = frac_str[i];
}
// 6. 发送数据
HAL_UART_Transmit(huart, (uint8_t*)str, idx, HAL_MAX_DELAY);
}
/**
* @brief 通过 UART 发送字符串
*/
void UART_SendString(UART_HandleTypeDef *huart, const char *str)
{
if (str == NULL) return;
uint16_t len = strlen(str);
if (len > 0) {
HAL_UART_Transmit(huart, (uint8_t*)str, len, HAL_MAX_DELAY);
}
}
// 针对 Keil MDK (ARM Compiler 5/6) 和 IAR
int fputc(int ch, FILE *f)
{
// 这里假设你主要使用 huart1,如果项目中有多个串口,可以考虑传参,
// 但 printf 标准接口无法传参,所以通常固定绑定一个调试串口。
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
// 针对 GCC (如 STM32CubeIDE, Makefile, CMake)
#ifdef __GNUC__
int _write(int file, char *ptr, int len)
{
HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, 0xFFFF);
return len;
}
#endif
/**
* @file uart_tools.h
* @brief UART 辅助工具函数头文件
* @note 包含浮点数发送、十六进制打印等常用功能
*/
#ifndef __UART_TOOLS_H
#define __UART_TOOLS_H
#ifdef __cplusplus
extern "C" {
#endif
#include "stm32f1xx_hal.h"
#include <stdint.h>
#include <stdio.h>
#include <math.h>
#include "string.h"
/**
* @brief 通过 UART 发送浮点数
* @param huart: UART 句柄指针 (如 &huart1)
* @param num: 要发送的浮点数
* @param decimal_places: 小数点后保留的位数
* @retval None
*/
void UART_SendFloat(UART_HandleTypeDef *huart, float num, uint8_t decimal_places);
/**
* @brief 通过 UART 发送字符串 (简化版 printf 替代)
* @param huart: UART 句柄指针
* @param str: 字符串指针
* @retval None
*/
void UART_SendString(UART_HandleTypeDef *huart, const char *str);
#ifdef __cplusplus
}
#endif
#endif /* __UART_TOOLS_H */
这里不过多赘述 主要讲一下FIFO
FIFO代码详解
/*
* File: fifo.c
* Brief: 通用环形缓冲区 (FIFO) 实现
*/
#include "fifo.h"
#include <string.h>
/**
* @brief 初始化 FIFO
* @note 第一个参数填结构体地址 第二个参数填缓冲区数组 第三个参数填缓冲区大小 大小为2的幂
*/
void FIFO_Init(FifoTypeDef *fifo, uint8_t *buffer, uint16_t size)
{
if (fifo == NULL || buffer == NULL || size == 0)
{
return;
}
fifo->buffer = buffer;
fifo->size = size;
fifo->head = 0;
fifo->tail = 0;
fifo->count = 0;
// 可选:清空缓冲区内存
// memset(fifo->buffer, 0, fifo->size);
}
/**
* @brief 向 FIFO 写入一个字节
* @note 此函数通常在中断中调用,确保逻辑简单高效,第一个参数填结构体地址,第二个参数填要写入的变量
*/
int8_t FIFO_Put(FifoTypeDef *fifo, uint8_t data)
{
if (fifo == NULL || fifo->buffer == NULL)
{
return -1;
}
// 检查是否已满
if (fifo->count >= fifo->size)
{
return -1; // FIFO Full
}
// 写入数据
fifo->buffer[fifo->head] = data;
// 移动写指针
fifo->head++;
if (fifo->head >= fifo->size)
{
fifo->head = 0; // 环形回绕
}
// 增加计数
fifo->count++;
return 0;
}
/**
* @brief 从 FIFO 读取一个字节
* @note 此函数通常在主循环中调用,第二参数填要存储数据的变量地址
*/
int8_t FIFO_Get(FifoTypeDef *fifo, uint8_t *data)
{
if (fifo == NULL || fifo->buffer == NULL || data == NULL)
{
return -1;
}
// 检查是否为空
if (fifo->count == 0)
{
return -1; // FIFO Empty
}
// 读取数据
*data = fifo->buffer[fifo->tail];
// 移动读指针
fifo->tail++;
if (fifo->tail >= fifo->size)
{
fifo->tail = 0; // 环形回绕
}
// 减少计数
fifo->count--;
return 0;
}
/**
* @brief 检查 FIFO 是否为空
*/
uint8_t FIFO_IsEmpty(FifoTypeDef *fifo)
{
if (fifo == NULL)
{
return 1;
}
return (fifo->count == 0) ? 1 : 0;
}
/**
* @brief 检查 FIFO 是否已满
*/
uint8_t FIFO_IsFull(FifoTypeDef *fifo)
{
if (fifo == NULL)
{
return 1;
}
return (fifo->count >= fifo->size) ? 1 : 0;
}
/**
* @brief 获取 FIFO 中当前数据长度
*/
uint16_t FIFO_GetCount(FifoTypeDef *fifo)
{
if (fifo == NULL)
{
return 0;
}
return fifo->count;
}
/**
* @brief 清空 FIFO
*/
void FIFO_Flush(FifoTypeDef *fifo)
{
if (fifo == NULL)
{
return;
}
fifo->head = 0;
fifo->tail = 0;
fifo->count = 0;
}
接下来是头文件
/*
* File: fifo.h
* Brief: 通用环形缓冲区 (FIFO) 定义
*/
#ifndef __FIFO_H
#define __FIFO_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stddef.h>
/**
* @brief FIFO 结构体定义
*/
typedef struct {
uint8_t *buffer; /**< 指向数据存储区的指针 */
uint16_t size; /**< 缓冲区总大小 */
uint16_t head; /**< 写指针 (入队位置) */
uint16_t tail; /**< 读指针 (出队位置) */
uint16_t count; /**< 当前缓冲区中的数据数量 */
} FifoTypeDef;
/**
* @brief 初始化 FIFO
* @param fifo: FIFO 结构体指针
* @param buffer: 外部分配的数据缓冲区指针
* @param size: 缓冲区大小
*/
void FIFO_Init(FifoTypeDef *fifo, uint8_t *buffer, uint16_t size);
/**
* @brief 向 FIFO 写入一个字节
* @param fifo: FIFO 结构体指针
* @param data: 要写入的数据
* @return 0: 成功, -1: FIFO 已满
*/
int8_t FIFO_Put(FifoTypeDef *fifo, uint8_t data);
/**
* @brief 从 FIFO 读取一个字节
* @param fifo: FIFO 结构体指针
* @param data: 用于存储读出数据的指针
* @return 0: 成功, -1: FIFO 为空
*/
int8_t FIFO_Get(FifoTypeDef *fifo, uint8_t *data);
/**
* @brief 检查 FIFO 是否为空
* @param fifo: FIFO 结构体指针
* @return 1: 为空, 0: 非空
*/
uint8_t FIFO_IsEmpty(FifoTypeDef *fifo);
/**
* @brief 检查 FIFO 是否已满
* @param fifo: FIFO 结构体指针
* @return 1: 已满, 0: 未满
*/
uint8_t FIFO_IsFull(FifoTypeDef *fifo);
/**
* @brief 获取 FIFO 中当前数据长度
* @param fifo: FIFO 结构体指针
* @return 当前数据字节数
*/
uint16_t FIFO_GetCount(FifoTypeDef *fifo);
/**
* @brief 清空 FIFO
* @param fifo: FIFO 结构体指针
*/
void FIFO_Flush(FifoTypeDef *fifo);
#ifdef __cplusplus
}
#endif
#endif /* __FIFO_H */
这里对FIFO做一个介绍
- 核心工作原理
- 先进先出:就像排队买票一样,先来的人先服务。数据从尾部(Tail/Read Pointer)进入,从头部(Head/Write Pointer)流出(注:具体头尾定义因实现而异,但逻辑一致)。
- 环形结构:为了解决普通数组队列在数据移出后空间无法复用的问题,FIFO 将存储空间逻辑上首尾相连。当指针到达缓冲区末尾时,会自动绕回到起始位置。
- 固定大小:通常预先分配一块固定大小的内存,避免动态内存分配带来的碎片化和不确定性。
- 关键组件
一个标准的软件 FIFO 通常包含以下要素:
-
存储介质:一段连续的内存数组。
-
写指针(Write Index):指向下一个可写入数据的位置。
-
读指针(Read Index):指向下一个可读取数据的位置。
-
状态标记
:用于判断缓冲区是“空”还是“满”。常见的实现方式包括:
- 使用一个计数器记录当前数据量。
- 浪费一个存储单元,通过指针位置关系判断(
(write+1)%size == read为满)。 - 使用额外的标志位。
3. 主要优势
-
解耦生产者与消费者:允许数据生成方(如传感器、网络接口)和数据消费方(如 CPU 处理程序)以不同的速度运行。
-
平滑突发流量:当数据突然大量到达时,FIFO 可以暂时存储这些数据,防止因处理不及时导致数据丢失。
-
高效性:入队和出队操作的时间复杂度均为 O(1),不需要移动内存中的数据,只需移动指针。
4. 典型应用场景 -
串口/通信接口:解决中断接收速度快而主程序处理速度慢的问题,防止数据覆盖。
-
音频/视频流处理:缓冲音视频帧,确保播放流畅,避免卡顿。
-
任务调度:操作系统中的就绪队列,等待 CPU 时间的任务按顺序排列。
-
打印队列:多个文档提交打印,按提交顺序依次输出。
5. 常见挑战与注意事项 -
并发安全:如果读写操作发生在不同的上下文(如中断和主循环,或多线程环境中),必须考虑原子性或加锁机制,防止指针竞争导致数据错乱。
-
溢出处理
:当 FIFO 满时,新数据如何处理?常见策略有:
- 丢弃新数据:保护旧数据完整性。
- 覆盖旧数据:保留最新数据(适用于实时性要求高、旧数据无价值的场景)。
- 阻塞等待:直到有空间可用(适用于实时操作系统中的任务同步)。
-
尺寸选择:缓冲区大小需要根据最大数据突发量和最长处理延迟来权衡,过大浪费内存,过小容易溢出。
总之,FIFO 是构建稳健、高效数据流系统的基石,尤其在处理异步数据和速率不匹配的场景中不可或缺。
这里用FIFO主要也是为了能够提升串口的数据读取能力 面对高速/大量数据时能够处理
主函数代码详解
/* USER CODE BEGIN PV */
uint8_t uart1_rx_buffer[256]; // HAL 接收缓冲区
FifoTypeDef uart1_rx_fifo; // 接收 FIFO
uint8_t fifo_storage[256]; // 接收 FIFO 存储区
// 【新增】发送相关变量
volatile uint8_t tx_busy = 0; // 发送忙标志
uint8_t tx_buffer[256]; // 发送临时缓冲区
volatile uint16_t tx_len = 0; // 待发送长度
volatile uint8_t rx_data_ready = 0;
volatile uint16_t rx_data_len = 0;
/* USER CODE END PV */
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
printf("UART FIFO Init\r\n");
FIFO_Init(&uart1_rx_fifo,fifo_storage, 256);
HAL_UARTEx_ReceiveToIdle_IT(&huart1,uart1_rx_buffer, 256);
while (1)
{
// 【核心逻辑】只有当 UART 发送空闲时,才去处理 FIFO 中的数据并启动发送
// 这样保证了 tx_buffer 在发送期间是稳定的,且避免了竞争条件
if (tx_busy == 0)
{
uint16_t len = 0;
// 尝试从 FIFO 中取出所有可用数据到发送缓冲区
// 如果 FIFO 为空,len 保持为 0
while (!FIFO_IsEmpty(&uart1_rx_fifo) && len < sizeof(tx_buffer))
{
FIFO_Get(&uart1_rx_fifo, &tx_buffer[len]);
len++;
}
// 如果取到了数据,则启动发送
if (len > 0)
{
tx_len = len;
tx_busy = 1; // 标记为忙碌,阻止下一次进入此分支直到发送完成
// 启动非阻塞发送
HAL_UART_Transmit_IT(&huart1, tx_buffer, tx_len);
}
}
// 注意:如果 tx_busy == 1,说明正在发送。
// 此时新来的数据会由中断存入 FIFO,等待本次发送结束后,
// 下一轮循环 tx_busy 变回 0 时再取出发送。
// 这实现了简单的队列效果,防止数据冲突。
}
/* USER CODE END WHILE */
}
/* USER CODE END 3 */
/* USER CODE BEGIN 4 */
/**
* @brief Tx Transfer completed callback
* @note 发送完成后,清除忙标志,允许主循环发送下一帧
*/
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
tx_busy = 0;
}
}
/**
* @brief Rx Event callback (Idle detected or Full Buffer)
* @note 将接收到的数据存入 FIFO,并置位标志
*/
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if(huart->Instance == USART1)
{
// 1. 快速将数据从 HAL 缓冲区搬运到 FIFO
// 这一步必须快,因为是在中断上下文中
for(uint16_t i = 0; i < Size; i++)
{
// 如果 FIFO 满了,FIFO_Put 会返回 -1,这里选择丢弃多余数据
// 也可以根据需求选择覆盖旧数据
FIFO_Put(&uart1_rx_fifo, uart1_rx_buffer[i]);
}
// 2. 更新状态
rx_data_len = Size;
rx_data_ready = 1;
// 3. 重新启动下一次空闲中断接收
// 检查 RX 状态是否为 READY,防止重入导致错误
if (huart->RxState == HAL_UART_STATE_READY)
{
HAL_UARTEx_ReceiveToIdle_IT(&huart1, uart1_rx_buffer, 256);
}
}
}
我一开始想的是用常规中断处理 不过这样做遇到了两个问题
-
第一个问题是常规中断一个字节就会触发一次中断服务函数 满足代码设置后会触发一次中断服务函数 传输单个字节的时候还好 如果传输多个字节会导致中断服务函数多次调用 在中断服务函数内读取数据会出现一些问题 由于我的能力无法解决
-
第二个问题就是如果常规中断接收数据设置为1字节 那发送一字节就会触发一次中断回调 这样看起来没什么问题 但是如果发送多字节会导致数据乱码 以及 多次中断回调触发会给MCU带来处理压力
所以最终选择空闲中断接收不定长数据 FIFO进行处理 效果还行
现在解释一下主函数
这里主函数只用了一个接收FIFO 发送的数据用了一个BUFFER进行处理 这里这么写是因为测试的时候发现多数据发送会出现一些问题 加了一个缓冲区会好一点 随后是函数初始化 FIFO的初始化存储数组 与 空闲中断的存储数组不同 大小相同都是256
随后是中断函数逻辑 第一个是接收中断 接收到数据后把buffer(空闲中断存储数组)存入FIFO内 更新一下接收数据标志位 与 这次的数据长度 随后是发送中断 这个是只要检测到不发送就把busy置0 说明可以发送
随后跳到主循环内 把刚才存好的FIFO数组读到要发送的txbuffer内 如果检测到 txbuffer内有数据 把busy置为1 说明现在在传数据 传完了触发空闲传输中断 再次把busy置为0 进入下一次循环
如果看不懂我说的 我贴一下AI说的
缓冲区设计(双缓冲策略)
- 接收端两个数组,职责不同但大小相同(均为 256 字节):
uart1_rx_buffer(HAL 底层缓冲区):供 DMA/硬件直接写入。它是“临时中转站”,每次空闲中断触发后,里面的数据会被立即搬走,然后重新开启接收。fifo_storage(软件 FIFO 存储区):供软件逻辑使用。它是“蓄水池”,用于累积多包数据,解决硬件接收速度快于软件处理速度的问题,防止数据覆盖丢失。
- 发送端一个数组:
tx_buffer(发送临时缓冲区):用于在主循环中组装待发送的数据包。在发送期间保持数据稳定,避免竞争。
接收流程(中断驱动 + FIFO 缓冲)
-
硬件接收:串口接收到数据,DMA 自动存入
uart1_rx_buffer。 -
空闲中断触发:当总线空闲(一帧数据结束)时,触发
HAL_UARTEx_RxEventCallback。 -
数据搬运(关键步骤)
:- 在中断回调中,通过循环将
uart1_rx_buffer中的数据逐个调用FIFO_Put存入fifo_storage。 - 目的:快速释放 HAL 缓冲区,以便硬件能立即开始接收下一帧数据。
- 在中断回调中,通过循环将
-
状态更新:更新
rx_data_ready和rx_data_len标志(可选,主要用于调试或外部查询)。 -
重启接收:调用
HAL_UARTEx_ReceiveToIdle_IT重新启动下一次空闲中断监听。
发送流程(主轮询 + 标志位同步) -
主循环检测
:检查
tx_busy标志。
- 若
tx_busy == 0(空闲):从 FIFO 中取出数据填入tx_buffer。 - 若
tx_busy == 1(忙碌):跳过,等待当前发送完成。
- 若
-
启动发送
:-
如果取到了数据 (
len > 0):
- 置
tx_busy = 1(加锁,防止主循环再次修改tx_buffer)。 - 调用
HAL_UART_Transmit_IT启动非阻塞发送。
- 置
-
-
硬件发送:UART 硬件自动逐位发送
tx_buffer中的所有数据(期间 CPU 可执行其他任务或空转)。 -
发送完成中断
- 当所有字节发送完毕后,硬件触发 发送完成中断 (Transmission Complete Interrupt)。
- 进入回调函数
HAL_UART_TxCpltCallback。
-
释放锁:在回调中将
tx_busy置为0,通知主循环可以发送下一包数据。
核心优势总结
- 解决频繁中断:接收使用空闲中断(每帧一次),发送使用完成中断(每包一次),极大降低了 CPU 负载。
- 解决数据乱码/冲突
- 接收侧:FIFO 作为中间层,避免了因主循环处理慢而导致的新数据覆盖旧数据。
- 发送侧:
tx_busy标志位确保了tx_buffer在发送过程中的原子性和稳定性,避免了半双工切换时的时序错误。
- 解耦:接收(中断上下文)与发送/处理(主循环上下文)完全分离,代码结构清晰,易于维护。
测试
发送数据边界测试








发送256字节 正常
发送257字节 会丢失一个 但是这里看不出来哪个丢了 = = 不过按照代码写的来说 出现数据丢失才是正常的
多条数据发送测试
可以看到这里发送周期200ms 100ms 50ms 10ms都没出问题
而单条长数据2ms周期短期内不会出现问题 但是长期就会乱码与数据丢失 这里可能就是极限了
更多推荐



所有评论(0)