/** ****************************************************************************** * @file uart2_print.c * @brief UART2调试打印模块实现 * @author Application Layer * @version 1.1 ****************************************************************************** * @attention * 本模块实现基于环形缓冲区的非阻塞调试信息输出 * 关键特性: * 1. 环形缓冲区避免数据丢失 * 2. 中断安全,支持ISR中调用 * 3. 非阻塞发送,不影响实时性 * * 修订历史: * v1.1 - 修复审查报告高危-1/2/3,中危-4 ****************************************************************************** */ #include "uart2_print.h" #include "usart.h" #include #include /*============================================================================== * 调试宏定义 *============================================================================*/ /* DEBUG_PRINT_ENABLED: 调试日志开关,置1时启用模块自测日志输出 */ #define DEBUG_PRINT_ENABLED 1 #if DEBUG_PRINT_ENABLED /* 调试日志宏,带模块前缀"[UART2]"方便过滤日志 */ #define DEBUG_LOG(fmt, ...) UART2_Print_Printf("[UART2] " fmt "\r\n", ##__VA_ARGS__) #else /* 禁用时为空宏,避免无用代码生成 */ #define DEBUG_LOG(fmt, ...) #endif /*============================================================================== * 数据结构定义 *============================================================================*/ /** * @brief 环形发送缓冲区数据结构 * @note 采用Ring Buffer(环形缓冲区)设计,实现FIFO队列管理 * * 设计目的: * 解决UART发送与CPU执行速度不匹配的问题。当UART正在发送数据时, * 后续数据先存入缓冲区,待发送完成后再取出发送,实现异步非阻塞打印。 * 环形缓冲区相比线性缓冲区的优势是无需数据搬移,head和tail指针 * 循环递增,到达末尾后自动回绕到开头。 * * 字段说明: * - buffer: 存放数据的字节数组,大小由UART2_TX_BUFFER_SIZE定义 * - head: 写入位置指针,指向下一个待写入位置(生产者指针) * - tail: 读取位置指针,指向下一个待读取位置(消费者指针) * - count: 当前缓冲区中有效数据字节数 * - is_sending: 发送忙标志,表示UART硬件是否正在发送数据 * - overflow_count: 溢出错误计数,缓冲区满时丢弃数据的次数 * * 索引计算规则: * head和tail均采用模运算实现回绕: new_index = (old_index + 1) % buffer_size * 这确保了指针在达到缓冲区末尾时自动回到开头,形成"环形"效果 * * 线程安全说明: * 所有修改共享资源的代码段均使用__disable_irq/__enable_irq暂时禁用中断, * 防止在临界区内被ISR打断导致数据竞争(race condition) */ typedef struct { uint8_t buffer[UART2_TX_BUFFER_SIZE]; /**< 发送数据缓冲区,静态分配避免动态内存 */ volatile uint16_t head; /**< 写指针,下一个数据写入位置 */ volatile uint16_t tail; /**< 读指针,下一个数据读取位置 */ volatile uint16_t count; /**< 缓冲区中有效数据字节计数 */ volatile bool is_sending; /**< UART发送忙标志,防止重复启动发送 */ volatile uint16_t overflow_count; /**< 溢出错误计数,统计因缓冲区满丢弃的数据 */ } ring_buffer_t; /*============================================================================== * 全局变量定义 *============================================================================*/ /** * @brief 环形发送缓冲区实例 * @note static修饰确保仅本文件内可访问,初始化为全零 */ static ring_buffer_t tx_ring = {0}; /*============================================================================== * 公共函数实现 *============================================================================*/ /** * @brief UART2打印模块初始化 * @note 在系统启动或UART外设初始化后调用,重置环形缓冲区状态 * * @param 无 * @return 无 * * 功能说明: * 1. 重置所有指针(head/tail/count)为初始状态 * 2. 清除发送忙标志 * 3. 清零溢出错误计数器 * * 初始化安全: * 此函数应在UART硬件初始化完成之后调用,确保huart2已正确配置 */ void UART2_Print_Init(void) { /* 重置环形缓冲区各状态变量 */ tx_ring.head = 0; tx_ring.tail = 0; tx_ring.count = 0; tx_ring.is_sending = false; tx_ring.overflow_count = 0; /* 输出模块初始化完成日志 */ DEBUG_LOG("Init OK, buffer size: %d", UART2_TX_BUFFER_SIZE); } /** * @brief 发送数据到UART2(核心写入函数) * @note 将数据写入环形缓冲区,若UART空闲则自动启动首次发送 * 此函数是线程安全的,可从ISR或主线程调用 * * @param data: 待发送数据缓冲区的指针(输入) * @param len: 待发送数据字节数(输入) * @return 无返回值 * * 算法说明 - 生产者逻辑: * 1. 遍历待发送数据的每个字节 * 2. 检查缓冲区是否有空间(count < buffer_size) * 3. 如有空间,将数据写入buffer[head],head递增并回绕 * 4. 如缓冲区满,放弃剩余数据并增加overflow_count * * 发送触发机制: * - 如果缓冲区有数据且UART当前空闲(is_sending=false), * 设置is_sending=true并立即触发首次发送(Kickoff) * - 后续发送由TxCpltCallback中断回调驱动,形成连续发送直到缓冲区清空 * * 中断安全: * - 使用__disable_irq/__enable_irq保护临界区 * - 防止在检查count和写入buffer之间被ISR打断 */ void UART2_Print_Send(const uint8_t *data, uint16_t len) { /* 参数合法性检查,防止空指针或零长度 */ if (len == 0 || data == NULL) { return; } uint16_t written = 0; bool needs_kickoff = false; /*---------------------------------------------------------- * 临界区:禁用中断,保护共享缓冲区的写入操作 *----------------------------------------------------------*/ __disable_irq(); for (uint16_t i = 0; i < len; i++) { /* 检查缓冲区是否有空间 */ if (tx_ring.count >= UART2_TX_BUFFER_SIZE) { /* 缓冲区已满,丢弃数据并计数 */ tx_ring.overflow_count++; break; } /* 将数据写入环形缓冲区,更新写指针(模缓冲区大小回绕) */ tx_ring.buffer[tx_ring.head] = data[i]; tx_ring.head = (tx_ring.head + 1) % UART2_TX_BUFFER_SIZE; tx_ring.count++; written++; } /*---------------------------------------------------------- * 判断是否需要触发首次发送 * 条件:成功写入数据 AND UART当前处于空闲状态 *----------------------------------------------------------*/ if (written > 0 && !tx_ring.is_sending) { tx_ring.is_sending = true; needs_kickoff = true; } __enable_irq(); /* 退出临界区 */ /*---------------------------------------------------------- * 启动首次发送(Kickoff) * 从缓冲区取出第一个字节通过DMA/中断方式发送 *----------------------------------------------------------*/ if (needs_kickoff) { uint8_t byte; /* 再次禁用中断以读取尾指针(消费者操作) */ __disable_irq(); byte = tx_ring.buffer[tx_ring.tail]; __enable_irq(); /* 启动UART中断发送,发送完成后会触发TxCpltCallback */ HAL_UART_Transmit_IT(&huart2, &byte, 1); } } /** * @brief 发送字符串到UART2 * @note 封装UART2_Print_Send,自动计算字符串长度 * * @param str: 待发送的以'\0'结尾的字符串指针(输入) * @return 无返回值 * * 使用说明: * - str必须为有效指针且以'\0'结尾 * - strlen计算长度时不包括终止符,所以实际发送的也不包括 */ void UART2_Print_String(const char *str) { /* 空指针保护 */ if (str == NULL) { return; } /* 使用strlen获取字符串长度并发送 */ UART2_Print_Send((const uint8_t *)str, strlen(str)); } /** * @brief 格式化打印到UART2 * @note 仿printf风格,支持可变参数格式化输出 * * @param fmt: 格式化字符串,与printf语法兼容(输入) * @param ...: 可变参数列表,对应格式化字符串中的占位符(输入) * @return 无返回值 * * 实现说明: * 1. 使用va_list/va_start/va_end处理可变参数 * 2. vsnprintf将格式化参数写入临时缓冲区(128字节) * 3. 将格式化后的字符串通过UART2_Print_Send发送 * * 缓冲区限制: * - 最大格式化输出为127字节(128-1留作字符串终止符) * - 超出部分会被截断,不报错 * * 线程安全: * 此函数本身是中断安全的,但内部调用UART2_Print_Send, * 多线程并发调用时输出可能交叉 */ void UART2_Print_Printf(const char *fmt, ...) { /* 格式化字符串合法性检查 */ if (fmt == NULL) { return; } char buffer[128]; /* 临时格式化缓冲区,大小固定 */ va_list args; /* 可变参数列表变量 */ /*---------------------------------------------------------- * 可变参数处理:初始化va_list并执行格式化 *----------------------------------------------------------*/ va_start(args, fmt); int len = vsnprintf(buffer, sizeof(buffer), fmt, args); va_end(args); /*---------------------------------------------------------- * 发送格式化后的字符串 * vsnprintf返回值len: * - >=0: 成功格式化,需要发送的字符数(不含终止符) * - <0: 格式化失败,不发送任何数据 *----------------------------------------------------------*/ if (len >= 0) { /* 边界检查:若len超过缓冲区实际容量则截断 */ if (len >= (int)sizeof(buffer)) { len = sizeof(buffer) - 1; } UART2_Print_Send((const uint8_t *)buffer, len); } } /** * @brief UART2打印任务(轮询模式驱动) * @note 在主循环或定时器中调用,驱动环形缓冲区数据发送 * 与TxCpltCallback配合形成轮询+中断混合发送模式 * * @param 无 * @return 无 * * 工作原理: * 正常发送流程依赖TxCpltCallback中断回调驱动。但若某些平台 * 或情况下中断被屏蔽,此函数作为备用发送机制,从缓冲区取出 * 数据启动新的发送。 * * 调用时机: * 建议在主循环中周期性调用(如每次循环或10ms定时), * 配合UART空闲中断或DMA完成中断实现高效发送 * * 中断安全: * - 使用__disable_irq/__enable_irq保护临界区 */ void UART2_Print_Task(void) { uint8_t byte; uint16_t current_tail; /*---------------------------------------------------------- * 临界区:检查是否可以发送 *----------------------------------------------------------*/ __disable_irq(); /* 条件:不正在发送 且 缓冲区有数据 */ if (tx_ring.is_sending || tx_ring.count == 0) { __enable_irq(); return; /* 条件不满足,不执行发送 */ } /*---------------------------------------------------------- * 从缓冲区取出数据,更新读指针 *----------------------------------------------------------*/ current_tail = tx_ring.tail; tx_ring.tail = (tx_ring.tail + 1) % UART2_TX_BUFFER_SIZE; /* 环形回绕 */ tx_ring.count--; tx_ring.is_sending = true; /* 标记为发送中,防止重复启动 */ byte = tx_ring.buffer[current_tail]; __enable_irq(); /* 退出临界区 */ /*---------------------------------------------------------- * 启动UART中断发送 *----------------------------------------------------------*/ HAL_UART_Transmit_IT(&huart2, &byte, 1); } /** * @brief UART发送完成回调函数 * @note 应在UART TX完成中断中调用,负责驱动环形缓冲区连续发送 * 此函数是发送流程的核心驱动引擎 * * @param 无 * @return 无 * * 工作原理 - 消费者逻辑: * 1. 清除is_sending标志,表示UART硬件已空闲 * 2. 检查环形缓冲区是否还有待发送数据 * 3. 如有,取出下一字节并启动新的发送 * 4. 重复上述过程直到缓冲区清空 * * 中断安全: * - 使用__disable_irq/__enable_irq保护共享数据访问 * - 在中断上下文调用,必须确保操作原子性 * * 调用约定: * 此函数需要与HAL库中断处理正确关联,通常在 * HAL_UART_TxCpltCallback中断回调中调用 */ void UART2_Print_TxCpltCallback(void) { uint8_t byte; uint16_t current_tail; bool has_more = false; /*---------------------------------------------------------- * 第一步:标记UART为空闲状态 *----------------------------------------------------------*/ __disable_irq(); tx_ring.is_sending = false; /*---------------------------------------------------------- * 第二步:检查缓冲区是否有更多数据待发送 *----------------------------------------------------------*/ if (tx_ring.count > 0) { /* 取出下一字节,更新读指针(环形回绕) */ current_tail = tx_ring.tail; tx_ring.tail = (tx_ring.tail + 1) % UART2_TX_BUFFER_SIZE; tx_ring.count--; tx_ring.is_sending = true; /* 立即标记为发送中 */ byte = tx_ring.buffer[current_tail]; has_more = true; /* 标记有待发送数据 */ } __enable_irq(); /*---------------------------------------------------------- * 第三步:如有待发送数据,立即启动下一次发送 *----------------------------------------------------------*/ if (has_more) { HAL_UART_Transmit_IT(&huart2, &byte, 1); } } /** * @brief 查询发送模块忙状态 * @note 用于判断是否有数据正在发送或缓冲区中是否有待发数据 * * @param 无 * @return bool: true=忙(正在发送或缓冲区有数据),false=空闲 * * 使用场景: * - 在进入低功耗模式前检查是否有数据待发送 * - 在系统关机前确认所有调试日志已发送完毕 * * 中断安全: * - 使用临界区保护,确保检查到返回之间数据不被修改 */ bool UART2_Print_IsBusy(void) { bool busy; __disable_irq(); busy = tx_ring.is_sending || (tx_ring.count > 0); __enable_irq(); return busy; } /** * @brief 查询发送缓冲区可用空间 * @note 返回环形缓冲区当前剩余可写入的空间大小 * * @param 无 * @return uint16_t: 可用字节数 * * 计算公式:available = buffer_size - count * * 使用场景: * - 在发送大数据块前检查缓冲区是否足够容纳 * - 实现流量控制逻辑 * * 中断安全: * - 使用临界区保护 */ uint16_t UART2_Print_Available(void) { uint16_t available; __disable_irq(); available = UART2_TX_BUFFER_SIZE - tx_ring.count; __enable_irq(); return available; } /** * @brief 获取溢出错误计数 * @note 统计因缓冲区满导致数据丢弃的次数,用于诊断 * * @param 无 * @return uint16_t: 溢出错误累计次数 * * 使用说明: * - 此计数器仅在缓冲区满时递增 * - 若计数持续增长,说明UART发送速度跟不上数据产生速度 * - 可通过增大UART2_TX_BUFFER_SIZE或降低打印频率解决 */ uint16_t UART2_Print_GetOverflowCount(void) { return tx_ring.overflow_count; } /*============================================================================== * 标准库printf重定向实现 *============================================================================*/ /** * @brief printf重定向函数 (Keil MDK编译器) * @note 将标准库printf输出重定向到UART2 * * @param ch: 待发送字符(输入) * @param f: 文件指针(未使用,Keil MDK参数要求) * @return int: 发送的字符 * * 编译器条件编译: * 此函数仅在__CC_ARM(或__ARMCC_VERSION)定义时编译, * 即Keil MDK-ARM/ARMCC编译器环境下生效 * * 实现说明: * 标准库printf最终会调用fputc输出每个字符, * 此处重定向到UART2_Print_Send实现串口打印 */ #if defined(__CC_ARM) || defined(__ARMCC_VERSION) int fputc(int ch, FILE *f) { (void)f; /* 未使用参数,避免编译器警告 */ UART2_Print_Send((uint8_t *)&ch, 1); return ch; } #endif /** * @brief printf重定向函数 (GCC编译器 - 单字符版本) * @note 将标准库printf输出重定向到UART2 * * @param ch: 待发送字符(输入) * @return int: 发送的字符 * * 编译器条件编译: * 此函数仅在__GNUC__定义时编译,即GCC/Clang编译器环境下生效 * * 实现说明: * 新一代ARM GCC使用__io_putchar作为printf输出目标, * 此处将其重定向到UART2_Print_Send */ #if defined(__GNUC__) int __io_putchar(int ch) { UART2_Print_Send((uint8_t *)&ch, 1); return ch; } /** * @brief write系统调用重定向 (GCC编译器) * @note 将文件系统write调用重定向到UART2 * * @param file: 文件描述符(未使用,仅为兼容标准接口) * @param ptr: 数据缓冲区指针(输入) * @param len: 数据长度(输入) * @return int: 已发送的字节数 * * 实现说明: * 有些GCC配置下printf会调用_write而非__io_putchar, * 此函数提供完整的write接口兼容 */ int _write(int file, char *ptr, int len) { (void)file; /* 忽略文件描述符,只处理标准输出 */ UART2_Print_Send((uint8_t *)ptr, len); return len; } #endif