Files
STM32F4-Base/App/log/console_log.c
T

227 lines
8.6 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.
/*
* 模块名称:Console Single-Writer Logging — 控制台单写者日志(USART1)实现
* 模块功能:控制台单写者日志的环形缓冲 + 唯一写者发送实现(API 见 console_log.h);
* 兼载 MicroLIB printf 底层 fputc 重定向
* 适用平台:STM32F407ZGTx(仅 MCU 端)
* 作者:王建锋
* 创建日期:2026-09-24
* 修改记录:
* 2026-09-24 王建锋 创建(自 Src/usart.c USER CODE 区迁出,实现逻辑不变,
* 仅变更文件位置:HAL 库生成文件不得包含应用代码)
*/
#include "console_log.h"
#include <stdio.h>
#include "usart.h"
#include "FreeRTOS.h"
#include "task.h"
#include "cmsis_os.h"
#include "sys_events.h"
/* ===== 控制台单写者日志(2026-09-24 加固,HardFault 定位结论驱动)=====
* 背景:旧路径 DBG_* → printf → fputc **每字符一次** HAL_UART_Transmit。HAL 的锁只保证
* "单字节"原子,任务切换发生在字符之间 → 两行日志逐字符交错(2026-09-24 崩溃前
* 实测:"[2026-[2026-0909-24 15:2-24 15:27:37:39]…");更严重的是字符串指针一旦
* 被破坏,脏指针会直接进入 UART 缓冲遍历(同日 HardFault:BFAR=0x10010000 恰为
* CCM 末尾+1,PC 落在 HAL_UART_Transmit 的缓冲读取处)。
* 架构:**defaultTask 是唯一串口发送者**;其余任务/上下文只把文本写入环形缓冲。
* ① 行级原子:整行在一次临界区内拷入环形缓冲,写者按块一次 HAL_UART_Transmit
* 发出 → 行与行不可能再交错(UART 只有这一个调用点);
* ② 指针可控:进 UART 的缓冲恒为本文件的环形缓冲,调用方脏指针再也到不了 UART;
* ③ 写者未注册(内核启动早期/裸机)或中断上下文:回退直接阻塞发送(无并发或必须裸发)。
* 约束:不新增任务、不动态分配;环形缓冲为 BSS(SRAM1,非 CCM),无 DMA 可达性冲突。
*/
#define DBG_CON_RING_SIZE 4096U /* 环形缓冲字节数(必须为 2 的幂) */
#define DBG_CON_CHUNK_MAX 256U /* 写者单次 HAL_UART_Transmit 字节上限 */
#define DBG_CON_WAIT_MS 200U /* 缓冲满时生产者最长等待(ms),仍满则丢弃并计数 */
static uint8_t s_con_ring[DBG_CON_RING_SIZE];
static volatile uint16_t s_con_head; /* 生产者写入位置 */
static volatile uint16_t s_con_tail; /* 写者读取位置 */
static volatile uint32_t s_con_drop; /* 因缓冲满丢弃的行数 */
static volatile uint8_t s_con_writer_up; /* 1 = 写者已注册(此后走环形缓冲) */
static TaskHandle_t s_con_writer_task; /* 写者任务句柄(自等保护用) */
/*
* 函数功能:计算环形缓冲已用字节数(head-tail 差值回绕取模,无锁)
* 入口参数:无
* 返回值:已用字节数 uint16_t
*/
static uint16_t con_used(void) {
return (uint16_t)((uint16_t)(s_con_head - s_con_tail) & (DBG_CON_RING_SIZE - 1U));
}
/*
* 函数功能:控制台裸发送(异常/早期路径专用:有界、单次、不加锁)
* 入口参数:buf - 文本缓冲;len - 字节数(超上限截断)
* 限定条件:串口已初始化;仅用于写者未注册、中断/异常上下文、崩溃上报
*/
void dbg_console_write_raw(const char *buf, unsigned len) {
if ((buf == NULL) || (len == 0U)) {
return;
}
if (len > DBG_CON_CHUNK_MAX) {
len = DBG_CON_CHUNK_MAX;
}
(void)HAL_UART_Transmit(&huart1, (uint8_t *)buf, (uint16_t)len, 0xFFFFu);
}
/*
* 函数功能:注册控制台写者(defaultTask 在进入主循环前调用一次)
* 函数说明:注册后生产者改走环形缓冲;未注册时直接阻塞发送(内核启动早期)
*/
void dbg_console_writer_attach(void) {
s_con_writer_task = xTaskGetCurrentTaskHandle();
s_con_writer_up = 1U;
}
/*
* 函数功能:排空环形缓冲(仅写者任务调用)
* 函数说明:按块一次 HAL_UART_Transmit 发出;每块完成后再推进读指针(临界区保护)
*/
void dbg_console_drain(void) {
uint16_t used;
if (s_con_writer_up == 0U) {
return;
}
while ((used = con_used()) != 0U) {
uint16_t tail = s_con_tail;
uint16_t contig = (uint16_t)(DBG_CON_RING_SIZE - tail); /* 到环尾的连续段 */
uint16_t n = (used < contig) ? used : contig;
if (n > DBG_CON_CHUNK_MAX) {
n = DBG_CON_CHUNK_MAX;
}
(void)HAL_UART_Transmit(&huart1, &s_con_ring[tail], n, 0xFFFFu);
taskENTER_CRITICAL();
s_con_tail = (uint16_t)((uint16_t)(s_con_tail + n) & (DBG_CON_RING_SIZE - 1U));
taskEXIT_CRITICAL();
}
/* 丢弃可观测性:缓冲满丢行必须显式可见(否则会掩盖开机自检/测试标记的丢失) */
if (s_con_drop != 0U) {
char note[64];
uint32_t d = s_con_drop;
int n;
s_con_drop = 0U;
n = snprintf(note, sizeof(note), "\r\n[ERR] CONSOLE dropped %lu lines\r\n",
(unsigned long)d);
if (n > 0) {
if (n > (int)sizeof(note) - 1) {
n = (int)sizeof(note) - 1;
}
dbg_console_write_raw(note, (unsigned)n);
}
}
}
/*
* 函数功能:把一段文本放入控制台(写者已注册)/直接发出(未注册或中断上下文)
* 返回值:0 - 已入缓冲或已直发;-1 - 缓冲满丢弃(计入 dbg_console_dropped)
* 函数说明:生产者不持锁等待过久:最多 DBG_CON_WAIT_MS,仍满则丢弃该行;
* 写者自身记录日志时先排空缓冲,避免自等。
*/
static int con_append(const char *buf, uint16_t len) {
uint32_t waited = 0U;
if ((buf == NULL) || (len == 0U)) {
return -1;
}
if ((s_con_writer_up == 0U) || (__get_IPSR() != 0U)) {
dbg_console_write_raw(buf, len);
return 0;
}
if (len > (DBG_CON_RING_SIZE / 2U)) {
len = (uint16_t)(DBG_CON_RING_SIZE / 2U); /* 超长保护(正常路径不会触发) */
}
if (xTaskGetCurrentTaskHandle() == s_con_writer_task) {
dbg_console_drain(); /* 写者自己记录:先腾空,避免自等 */
}
for (;;) {
uint16_t used = con_used();
uint16_t free_space = (uint16_t)((DBG_CON_RING_SIZE - 1U) - used);
if (free_space >= len) {
break;
}
if (waited >= DBG_CON_WAIT_MS) {
s_con_drop++;
return -1;
}
vTaskDelay(1);
waited++;
}
taskENTER_CRITICAL();
{
uint16_t head = s_con_head;
uint16_t first = (uint16_t)(DBG_CON_RING_SIZE - head);
uint16_t i;
if (first > len) {
first = len;
}
for (i = 0U; i < first; i++) {
s_con_ring[head + i] = (uint8_t)buf[i];
}
for (i = first; i < len; i++) {
s_con_ring[(uint16_t)((uint16_t)(head + i) & (DBG_CON_RING_SIZE - 1U))] =
(uint8_t)buf[i];
}
s_con_head = (uint16_t)((uint16_t)(head + len) & (DBG_CON_RING_SIZE - 1U));
}
taskEXIT_CRITICAL();
if (sys_evtHandle != NULL) {
(void)osEventFlagsSet(sys_evtHandle, SYS_EVT_CONSOLE_TX); /* 唤醒写者 */
}
return 0;
}
/*
* 函数功能:把一行已格式化的文本交给单写者控制台(生产者入口,**非可变参数**)
* 入口参数:line - 整行文本(含结尾换行);len - 字节数
* 返回值:写入字节数;-1 - 参数非法或被丢弃
* 函数说明:整行一次入环形缓冲 → 行级原子(不会与他行交错);调用方缓冲随即可复用。
* 刻意不用自研可变参数函数(规范 S12 7.6);格式化由 DBG_PRINT 宏内
* 库 snprintf 在调用点完成。
*/
int dbg_console_write_line(const char *line, unsigned len) {
if ((line == NULL) || (len == 0U)) {
return -1;
}
if (len > DBG_CON_LINE_MAX) {
len = DBG_CON_LINE_MAX; /* 截断(丢尾,保开头的标记信息) */
}
if (con_append(line, (uint16_t)len) != 0) {
return -1;
}
return (int)len;
}
/*
* 函数功能:已丢弃的日志行数(诊断用)
*/
uint32_t dbg_console_dropped(void) {
return s_con_drop;
}
/*
* 函数功能:MicroLIB printf 底层重定向(库内部/遗留 printf 调用)
* 函数说明:同样只把字节交给单写者路径;未注册写者时直发(与旧行为一致)
*/
int fputc(int ch, FILE *f) {
uint8_t b = (uint8_t)ch;
(void)f;
(void)con_append((const char *)&b, 1U);
return ch;
}