Files
433_STM32/Core/Src/uart2_print.c

510 lines
18 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
******************************************************************************
* @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 <string.h>
#include <stdio.h>
/*==============================================================================
* 调试宏定义
*============================================================================*/
/* 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