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做一个介绍

  1. 核心工作原理
  • 先进先出:就像排队买票一样,先来的人先服务。数据从尾部(Tail/Read Pointer)进入,从头部(Head/Write Pointer)流出(注:具体头尾定义因实现而异,但逻辑一致)。
  • 环形结构:为了解决普通数组队列在数据移出后空间无法复用的问题,FIFO 将存储空间逻辑上首尾相连。当指针到达缓冲区末尾时,会自动绕回到起始位置。
  • 固定大小:通常预先分配一块固定大小的内存,避免动态内存分配带来的碎片化和不确定性。
  1. 关键组件

一个标准的软件 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 缓冲)
  1. 硬件接收:串口接收到数据,DMA 自动存入 uart1_rx_buffer

  2. 空闲中断触发:当总线空闲(一帧数据结束)时,触发 HAL_UARTEx_RxEventCallback

  3. 数据搬运(关键步骤)

    • 在中断回调中,通过循环将 uart1_rx_buffer 中的数据逐个调用 FIFO_Put 存入 fifo_storage
    • 目的:快速释放 HAL 缓冲区,以便硬件能立即开始接收下一帧数据。
  4. 状态更新:更新 rx_data_readyrx_data_len 标志(可选,主要用于调试或外部查询)。

  5. 重启接收:调用 HAL_UARTEx_ReceiveToIdle_IT 重新启动下一次空闲中断监听。
    发送流程(主轮询 + 标志位同步)

  6. 主循环检测

    :检查

    tx_busy
    

    标志。

    • tx_busy == 0(空闲):从 FIFO 中取出数据填入 tx_buffer
    • tx_busy == 1(忙碌):跳过,等待当前发送完成。
  7. 启动发送

    • 如果取到了数据 (

      len > 0
      

      ):

      • tx_busy = 1(加锁,防止主循环再次修改 tx_buffer)。
      • 调用 HAL_UART_Transmit_IT 启动非阻塞发送。
  8. 硬件发送:UART 硬件自动逐位发送 tx_buffer 中的所有数据(期间 CPU 可执行其他任务或空转)。

  9. 发送完成中断

    • 当所有字节发送完毕后,硬件触发 发送完成中断 (Transmission Complete Interrupt)。
    • 进入回调函数 HAL_UART_TxCpltCallback
  10. 释放锁:在回调中将 tx_busy 置为 0,通知主循环可以发送下一包数据。
    核心优势总结

  • 解决频繁中断:接收使用空闲中断(每帧一次),发送使用完成中断(每包一次),极大降低了 CPU 负载。
  • 解决数据乱码/冲突
    • 接收侧:FIFO 作为中间层,避免了因主循环处理慢而导致的新数据覆盖旧数据。
    • 发送侧tx_busy 标志位确保了 tx_buffer 在发送过程中的原子性稳定性,避免了半双工切换时的时序错误。
  • 解耦:接收(中断上下文)与发送/处理(主循环上下文)完全分离,代码结构清晰,易于维护。

测试

发送数据边界测试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
发送256字节 正常

发送257字节 会丢失一个 但是这里看不出来哪个丢了 = = 不过按照代码写的来说 出现数据丢失才是正常的

多条数据发送测试

可以看到这里发送周期200ms 100ms 50ms 10ms都没出问题

而单条长数据2ms周期短期内不会出现问题 但是长期就会乱码与数据丢失 这里可能就是极限了

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐