STM32 UART FIFO发送接收 GCC编译器重定向printf
cubemx配置这里只需要把串口中断打开就好代码详解UART_TOOLS上次写的函数有个重定义问题 这里做出改进 现在不需要在主函数里添加引用或者设置编译器 会自动识别使用哪种prinf定向/** * file uart_tools.c * brief UART 辅助工具函数实现 */#includeuart_tools.h#includestring.h#includestdio.h#includemath.h#includestring.hexternUART_HandleTypeDef huart1;/** * brief 通过 UART 发送浮点数 * note 不使用 sprintf 以节省代码空间纯整数运算实现 */voidUART_SendFloat(UART_HandleTypeDef*huart,floatnum,uint8_tdecimal_places){charstr[32];// 临时存储 ASCII 字符串int8_tidx0;uint8_tis_negative0;// 1. 处理 NaN 和 Inf (可选优化这里简单处理负数)if(num!num){// Check for NaNconstchar*nan_strNaN;HAL_UART_Transmit(huart,(uint8_t*)nan_str,3,HAL_MAX_DELAY);return;}// 2. 处理负数if(num0){is_negative1;num-num;}// 3. 把浮点数放大为整数避免逐位小数运算// 注意如果 decimal_places 过大可能导致溢出建议限制在 6 以内if(decimal_places6)decimal_places6;floatmultiplier1.0f;for(uint8_ti0;idecimal_places;i)multiplier*10.0f;// 四舍五入uint32_tscaled(uint32_t)(num*multiplier0.5f);// 4. 分离整数和小数部分uint32_tint_partscaled/(uint32_t)multiplier;uint32_tfrac_partscaled%(uint32_t)multiplier;// 5. 构建字符串// 添加负号if(is_negative)str[idx]-;// 转换整数部分倒序转换再反转chartemp[12];uint8_ttlen0;if(int_part0)temp[tlen]0;while(int_part0){temp[tlen](int_part%10)0;int_part/10;}// 反向填入 strfor(int8_titlen-1;i0;i--)str[idx]temp[i];// 添加小数点str[idx].;// 转换小数部分charfrac_str[10];uint8_tflen0;// 如果小数部分为0直接补0if(frac_part0){for(uint8_tk0;kdecimal_places;k){str[idx]0;}}else{// 正常转换小数位while(frac_part0){frac_str[flen](frac_part%10)0;frac_part/10;}// 补够 decimal_places 位从后往前填while(flendecimal_places)frac_str[flen]0;// 因为上面是逆序存的需要反向输出到 strfor(int8_tiflen-1;i0;i--)str[idx]frac_str[i];}// 6. 发送数据HAL_UART_Transmit(huart,(uint8_t*)str,idx,HAL_MAX_DELAY);}/** * brief 通过 UART 发送字符串 */voidUART_SendString(UART_HandleTypeDef*huart,constchar*str){if(strNULL)return;uint16_tlenstrlen(str);if(len0){HAL_UART_Transmit(huart,(uint8_t*)str,len,HAL_MAX_DELAY);}}// 针对 Keil MDK (ARM Compiler 5/6) 和 IARintfputc(intch,FILE*f){// 这里假设你主要使用 huart1如果项目中有多个串口可以考虑传参// 但 printf 标准接口无法传参所以通常固定绑定一个调试串口。HAL_UART_Transmit(huart1,(uint8_t*)ch,1,0xFFFF);returnch;}// 针对 GCC (如 STM32CubeIDE, Makefile, CMake)#ifdef__GNUC__int_write(intfile,char*ptr,intlen){HAL_UART_Transmit(huart1,(uint8_t*)ptr,len,0xFFFF);returnlen;}#endif/** * file uart_tools.h * brief UART 辅助工具函数头文件 * note 包含浮点数发送、十六进制打印等常用功能 */#ifndef__UART_TOOLS_H#define__UART_TOOLS_H#ifdef__cplusplusexternC{#endif#includestm32f1xx_hal.h#includestdint.h#includestdio.h#includemath.h#includestring.h/** * brief 通过 UART 发送浮点数 * param huart: UART 句柄指针 (如 huart1) * param num: 要发送的浮点数 * param decimal_places: 小数点后保留的位数 * retval None */voidUART_SendFloat(UART_HandleTypeDef*huart,floatnum,uint8_tdecimal_places);/** * brief 通过 UART 发送字符串 (简化版 printf 替代) * param huart: UART 句柄指针 * param str: 字符串指针 * retval None */voidUART_SendString(UART_HandleTypeDef*huart,constchar*str);#ifdef__cplusplus}#endif#endif/* __UART_TOOLS_H */这里不过多赘述 主要讲一下FIFOFIFO代码详解/* * File: fifo.c * Brief: 通用环形缓冲区 (FIFO) 实现 */#includefifo.h#includestring.h/** * brief 初始化 FIFO * note 第一个参数填结构体地址 第二个参数填缓冲区数组 第三个参数填缓冲区大小 大小为2的幂 */voidFIFO_Init(FifoTypeDef*fifo,uint8_t*buffer,uint16_tsize){if(fifoNULL||bufferNULL||size0){return;}fifo-bufferbuffer;fifo-sizesize;fifo-head0;fifo-tail0;fifo-count0;// 可选清空缓冲区内存// memset(fifo-buffer, 0, fifo-size);}/** * brief 向 FIFO 写入一个字节 * note 此函数通常在中断中调用确保逻辑简单高效第一个参数填结构体地址第二个参数填要写入的变量 */int8_tFIFO_Put(FifoTypeDef*fifo,uint8_tdata){if(fifoNULL||fifo-bufferNULL){return-1;}// 检查是否已满if(fifo-countfifo-size){return-1;// FIFO Full}// 写入数据fifo-buffer[fifo-head]data;// 移动写指针fifo-head;if(fifo-headfifo-size){fifo-head0;// 环形回绕}// 增加计数fifo-count;return0;}/** * brief 从 FIFO 读取一个字节 * note 此函数通常在主循环中调用第二参数填要存储数据的变量地址 */int8_tFIFO_Get(FifoTypeDef*fifo,uint8_t*data){if(fifoNULL||fifo-bufferNULL||dataNULL){return-1;}// 检查是否为空if(fifo-count0){return-1;// FIFO Empty}// 读取数据*datafifo-buffer[fifo-tail];// 移动读指针fifo-tail;if(fifo-tailfifo-size){fifo-tail0;// 环形回绕}// 减少计数fifo-count--;return0;}/** * brief 检查 FIFO 是否为空 */uint8_tFIFO_IsEmpty(FifoTypeDef*fifo){if(fifoNULL){return1;}return(fifo-count0)?1:0;}/** * brief 检查 FIFO 是否已满 */uint8_tFIFO_IsFull(FifoTypeDef*fifo){if(fifoNULL){return1;}return(fifo-countfifo-size)?1:0;}/** * brief 获取 FIFO 中当前数据长度 */uint16_tFIFO_GetCount(FifoTypeDef*fifo){if(fifoNULL){return0;}returnfifo-count;}/** * brief 清空 FIFO */voidFIFO_Flush(FifoTypeDef*fifo){if(fifoNULL){return;}fifo-head0;fifo-tail0;fifo-count0;}接下来是头文件/* * File: fifo.h * Brief: 通用环形缓冲区 (FIFO) 定义 */#ifndef__FIFO_H#define__FIFO_H#ifdef__cplusplusexternC{#endif#includestdint.h#includestddef.h/** * brief FIFO 结构体定义 */typedefstruct{uint8_t*buffer;/** 指向数据存储区的指针 */uint16_tsize;/** 缓冲区总大小 */uint16_thead;/** 写指针 (入队位置) */uint16_ttail;/** 读指针 (出队位置) */uint16_tcount;/** 当前缓冲区中的数据数量 */}FifoTypeDef;/** * brief 初始化 FIFO * param fifo: FIFO 结构体指针 * param buffer: 外部分配的数据缓冲区指针 * param size: 缓冲区大小 */voidFIFO_Init(FifoTypeDef*fifo,uint8_t*buffer,uint16_tsize);/** * brief 向 FIFO 写入一个字节 * param fifo: FIFO 结构体指针 * param data: 要写入的数据 * return 0: 成功, -1: FIFO 已满 */int8_tFIFO_Put(FifoTypeDef*fifo,uint8_tdata);/** * brief 从 FIFO 读取一个字节 * param fifo: FIFO 结构体指针 * param data: 用于存储读出数据的指针 * return 0: 成功, -1: FIFO 为空 */int8_tFIFO_Get(FifoTypeDef*fifo,uint8_t*data);/** * brief 检查 FIFO 是否为空 * param fifo: FIFO 结构体指针 * return 1: 为空, 0: 非空 */uint8_tFIFO_IsEmpty(FifoTypeDef*fifo);/** * brief 检查 FIFO 是否已满 * param fifo: FIFO 结构体指针 * return 1: 已满, 0: 未满 */uint8_tFIFO_IsFull(FifoTypeDef*fifo);/** * brief 获取 FIFO 中当前数据长度 * param fifo: FIFO 结构体指针 * return 当前数据字节数 */uint16_tFIFO_GetCount(FifoTypeDef*fifo);/** * brief 清空 FIFO * param fifo: FIFO 结构体指针 */voidFIFO_Flush(FifoTypeDef*fifo);#ifdef__cplusplus}#endif#endif/* __FIFO_H */这里对FIFO做一个介绍核心工作原理先进先出就像排队买票一样先来的人先服务。数据从尾部Tail/Read Pointer进入从头部Head/Write Pointer流出注具体头尾定义因实现而异但逻辑一致。环形结构为了解决普通数组队列在数据移出后空间无法复用的问题FIFO 将存储空间逻辑上首尾相连。当指针到达缓冲区末尾时会自动绕回到起始位置。固定大小通常预先分配一块固定大小的内存避免动态内存分配带来的碎片化和不确定性。关键组件一个标准的软件 FIFO 通常包含以下要素存储介质一段连续的内存数组。写指针Write Index指向下一个可写入数据的位置。读指针Read Index指向下一个可读取数据的位置。状态标记用于判断缓冲区是“空”还是“满”。常见的实现方式包括使用一个计数器记录当前数据量。浪费一个存储单元通过指针位置关系判断(write1)%size read为满。使用额外的标志位。 3. 主要优势解耦生产者与消费者允许数据生成方如传感器、网络接口和数据消费方如 CPU 处理程序以不同的速度运行。平滑突发流量当数据突然大量到达时FIFO 可以暂时存储这些数据防止因处理不及时导致数据丢失。高效性入队和出队操作的时间复杂度均为O(1)不需要移动内存中的数据只需移动指针。4. 典型应用场景串口/通信接口解决中断接收速度快而主程序处理速度慢的问题防止数据覆盖。音频/视频流处理缓冲音视频帧确保播放流畅避免卡顿。任务调度操作系统中的就绪队列等待 CPU 时间的任务按顺序排列。打印队列多个文档提交打印按提交顺序依次输出。5. 常见挑战与注意事项并发安全如果读写操作发生在不同的上下文如中断和主循环或多线程环境中必须考虑原子性或加锁机制防止指针竞争导致数据错乱。溢出处理当 FIFO 满时新数据如何处理常见策略有丢弃新数据保护旧数据完整性。覆盖旧数据保留最新数据适用于实时性要求高、旧数据无价值的场景。阻塞等待直到有空间可用适用于实时操作系统中的任务同步。尺寸选择缓冲区大小需要根据最大数据突发量和最长处理延迟来权衡过大浪费内存过小容易溢出。总之FIFO 是构建稳健、高效数据流系统的基石尤其在处理异步数据和速率不匹配的场景中不可或缺。这里用FIFO主要也是为了能够提升串口的数据读取能力 面对高速/大量数据时能够处理主函数代码详解/* USER CODE BEGIN PV */uint8_tuart1_rx_buffer[256];// HAL 接收缓冲区FifoTypeDef uart1_rx_fifo;// 接收 FIFOuint8_tfifo_storage[256];// 接收 FIFO 存储区// 【新增】发送相关变量volatileuint8_ttx_busy0;// 发送忙标志uint8_ttx_buffer[256];// 发送临时缓冲区volatileuint16_ttx_len0;// 待发送长度volatileuint8_trx_data_ready0;volatileuint16_trx_data_len0;/* USER CODE END PV */intmain(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_busy0){uint16_tlen0;// 尝试从 FIFO 中取出所有可用数据到发送缓冲区// 如果 FIFO 为空len 保持为 0while(!FIFO_IsEmpty(uart1_rx_fifo)lensizeof(tx_buffer)){FIFO_Get(uart1_rx_fifo,tx_buffer[len]);len;}// 如果取到了数据则启动发送if(len0){tx_lenlen;tx_busy1;// 标记为忙碌阻止下一次进入此分支直到发送完成// 启动非阻塞发送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 发送完成后清除忙标志允许主循环发送下一帧 */voidHAL_UART_TxCpltCallback(UART_HandleTypeDef*huart){if(huart-InstanceUSART1){tx_busy0;}}/** * brief Rx Event callback (Idle detected or Full Buffer) * note 将接收到的数据存入 FIFO并置位标志 */voidHAL_UARTEx_RxEventCallback(UART_HandleTypeDef*huart,uint16_tSize){if(huart-InstanceUSART1){// 1. 快速将数据从 HAL 缓冲区搬运到 FIFO// 这一步必须快因为是在中断上下文中for(uint16_ti0;iSize;i){// 如果 FIFO 满了FIFO_Put 会返回 -1这里选择丢弃多余数据// 也可以根据需求选择覆盖旧数据FIFO_Put(uart1_rx_fifo,uart1_rx_buffer[i]);}// 2. 更新状态rx_data_lenSize;rx_data_ready1;// 3. 重新启动下一次空闲中断接收// 检查 RX 状态是否为 READY防止重入导致错误if(huart-RxStateHAL_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周期短期内不会出现问题 但是长期就会乱码与数据丢失 这里可能就是极限了
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2584651.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!