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

415 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.
/*
* 模块名称: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 库生成文件不得包含应用代码)
* 2026-10-02 王建锋 僵尸通道喂狗节奏保护(台架 gen-288 实证):round-3 的
* "单次 drain 预算 250ms" 只限单次调用——ORE 锁存在外设软复位后
* 毫秒级重锁(PA10 悬空 + RX 使能),背靠背 drain 每轮再耗 100ms
* 块超时 → 主循环周期拉到 450~500ms → 100ms 节流的喂狗抽样间隙
* 超过 IWDG 最坏容限 1.7s → 停喂 → 误复位(且心跳位全员健康、
* 无 stall 日志,纯节奏性饥饿)。修复:发送故障后立即结束本轮
* drain 并进入 300ms 发送冷却(冷却期内 drain 直接让位主循环),
* 保证喂狗节奏 ≤ ~300ms。根治(USART1 TX-only 灭 ORE 机制)
* 另行推进(.ioc 结构性修改 + CubeMX 重生成,见待处理内容①)。
* 2026-10-02 王建锋 round 8b 加固:发送超时判据改用 DWT CYCCNT(时间源无关)。
* 台架 round 8a 取证(实机测试规划.md §0.5.4.2)定位到:defaultTask
* 冻结在 console 突发行内、无 STALL、复位为 IWDGRSTF。唯一自洽机理 =
* HAL_UART_Transmit 的超时判据 `HAL_GetTick() - Tickstart` 依赖
* TIM7 的 uwTick —— TIM7 一旦停摆,超时永不成立 → 写者死等 TXE →
* 唯一喂狗者停摆 → IWDG 复位。本改动给"裸发送"与"缓冲满等待"两处
* 都换成 DWT 周期计数硬上界(DWT 使能失败时退回旧 HAL 行为)。
* 另加 USART1 未初始化(huart1.Instance == NULL)短路:防止在
* HAL_Init 早期/断言-故障上报路径上解引用 NULL 读写地址 0。
*/
#include "console_log.h"
#include <stdio.h>
#include "usart.h"
#include "FreeRTOS.h"
#include "task.h"
#include "cmsis_os.h"
#include "sys_events.h"
#include "sys_log.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),仍满则丢弃并计数 */
#define DBG_CON_TX_TIMEOUT_MS 100U /* 单次发送超时:256B 块 @115200 标称 22ms;僵尸通道限时 + 自愈 */
#define DBG_CON_DRAIN_BUDGET_MS 250U /* 单次 drain 连续发送上限:让位喂狗轮询(缺失容忍 800ms) */
#define DBG_CON_FAULT_COOL_MS 300U /* 故障后发送冷却:ORE 锁存毫秒级重锁,背靠连发无益
且拖长主循环周期 → 喂狗节奏饥饿(gen-288 实证);
冷却期内 drain 只让位、不发送,下轮重试 */
/* ==== round 8b:与时间源无关的硬上界(DWT CYCCNT)====
* HAL_UART_Transmit 的超时判据是 `HAL_GetTick() - Tickstart > Timeout`,而 HAL 时基
* = TIM7 的 uwTick。TIM7 停摆 → 超时永不成立 → 写者死等 TXE → 唯一喂狗者停摆 →
* IWDG 误复位(round 8a 台架现场)。改用 DWT CYCCNT(内核硬件周期计数,不依赖任何
* 中断/uwTick)做判据;DWT 使能失败时退回 HAL_UART_Transmit(旧行为,功能不退化)。 */
#define CON_TX_TIMEOUT_CYC (SystemCoreClock / 10U) /* 发送硬上界 = 100ms 对应周期数 */
#define CON_APPEND_WAIT_CYC (SystemCoreClock / 5U) /* 缓冲满等待硬上界 = 200ms 对应周期数 */
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 uint32_t s_con_tx_fault; /* TX 失败(僵尸通道)次数,均经外设软复位自愈 */
static volatile uint32_t s_con_cool_until; /* 故障冷却截止 tick(0 = 无冷却) */
static volatile uint8_t s_con_draining; /* 1 = 处于 drain 中(重入保护) */
static volatile uint8_t s_con_writer_up; /* 1 = 写者已注册(此后走环形缓冲) */
static volatile uint8_t s_con_dwt_up; /* 1 = DWT CYCCNT 已使能(硬上界基准可用) */
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));
}
/*
* 函数功能:惰性使能 DWT 周期计数器(时间源无关的硬上界基准)
* 入口参数:无
* 返回值:无
* 限定条件:任意上下文可调(幂等;纯寄存器写,不创建任何内核对象)
* 函数说明:CoreDebug->DEMCR.TRCENA=1 → DWT->CYCCNT=0 → DWT->CTRL.CYCCNTENA=1;
* 使能成功置 s_con_dwt_up=1(供超时判据选择),失败保持 0(退回 HAL 超时)。
* 在 Cortex-M4 上 DWT 恒存在;失败仅可能出现在调试单元被硬件关闭时。
*/
static void con_dwt_ensure(void) {
if (s_con_dwt_up != 0U) {
return;
}
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0U;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
if ((DWT->CTRL & DWT_CTRL_CYCCNTENA_Msk) != 0U) {
s_con_dwt_up = 1U;
}
}
/*
* 函数功能:有界裸发送(不依赖 uwTick / HAL 超时)
* 入口参数:p_buf - 待发数据;len - 字节数(调用方已限 ≤ DBG_CON_CHUNK_MAX)
* 返回值:HAL_OK - 已全部写入发送寄存器;HAL_TIMEOUT - 有界上界内未完成
* 限定条件:USART1 已初始化
* 函数说明:寄存器级轮询 TXE/TC,超时判据用 DWT CYCCNT(CON_TX_TIMEOUT_CYC);
* DWT 未使能时退回 HAL_UART_Transmit(旧行为)。本函数只做 TX,
* 不触碰 RX/错误标志——僵尸通道自愈由调用方按返回值处理。
* ⚠ USART1 未初始化(huart1.Instance == NULL,如 HAL_Init 早期或
* 断言/故障上报路径)时立即返回 HAL_ERROR:否则会解引用 NULL 去读写
* 地址 0(flash 别名区)→ 在故障处理里再触发故障(2026-10-02 台架实测)。
*/
static HAL_StatusTypeDef con_tx_bounded(const uint8_t *p_buf, uint16_t len) {
USART_TypeDef *u;
uint32_t t0;
uint16_t i;
con_dwt_ensure();
if (huart1.Instance == NULL) {
return HAL_ERROR; /* USART1 未初始化:无可用通道,安全让位 */
}
if (s_con_dwt_up == 0U) {
return HAL_UART_Transmit(&huart1, (uint8_t *)p_buf, len, DBG_CON_TX_TIMEOUT_MS);
}
u = huart1.Instance;
t0 = DWT->CYCCNT;
for (i = 0U; i < len; i++) {
while ((u->SR & USART_SR_TXE) == 0U) {
if ((uint32_t)(DWT->CYCCNT - t0) >= CON_TX_TIMEOUT_CYC) {
return HAL_TIMEOUT; /* 有界:本块放弃(调用方自愈) */
}
}
u->DR = (uint16_t)p_buf[i];
}
while ((u->SR & USART_SR_TC) == 0U) {
if ((uint32_t)(DWT->CYCCNT - t0) >= CON_TX_TIMEOUT_CYC) {
return HAL_TIMEOUT;
}
}
return HAL_OK;
}
/*
* 函数功能:控制台裸发送(异常/早期路径专用:有界、单次、不加锁)
* 入口参数:buf - 文本缓冲;len - 字节数(超上限截断)
* 限定条件:串口已初始化;仅用于写者未注册、中断/异常上下文、崩溃上报
* 函数说明:走 con_tx_bounded(DWT 硬上界),tick 停摆时仍能在有界时间内返回。
*/
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)con_tx_bounded((const uint8_t *)buf, (uint16_t)len);
}
/*
* 函数功能:注册控制台写者(defaultTask 在进入主循环前调用一次)
* 函数说明:注册后生产者改走环形缓冲;未注册时直接阻塞发送(内核启动早期)
*/
void dbg_console_writer_attach(void) {
s_con_writer_task = xTaskGetCurrentTaskHandle();
s_con_writer_up = 1U;
}
/*
* 函数功能:USART1 僵尸通道自愈(清错误锁存 + 整外设重初始化)
* 函数说明:RX 悬空噪声(PA10 未接线)锁存的 ORE/NE/FE/PE 使 HAL 轮询 TX 恒快速失败
* (控制台静默、系统不复位);发送器卡死状态则使 TXE 等待耗满超时(冻结唯一
* 写者 → 看门狗误复位,2026-10-02 台架实证)。外设软复位清锁存与卡死状态后
* 重新初始化,故障时间被单块超时限定
*/
static void con_uart_reinit(void) {
__NVIC_ClearPendingIRQ(USART1_IRQn);
__HAL_RCC_USART1_CLK_DISABLE();
__HAL_RCC_USART1_CLK_ENABLE();
huart1.gState = HAL_UART_STATE_RESET;
huart1.RxState = HAL_UART_STATE_RESET;
huart1.ErrorCode = HAL_UART_ERROR_NONE;
(void)MX_USART1_UART_Init();
}
/*
* 函数功能:排空环形缓冲(仅写者任务调用)
* 函数说明:按块一次 con_tx_bounded 发出(DWT 硬上界,不依赖 uwTick);每块完成后
* 再推进读指针(临界区保护);发送失败 → 外设软复位自愈(丢当前块);
* 单次调用最多连续发 DBG_CON_DRAIN_BUDGET_MS 后让位主循环,保证看门狗轮询
* 不被僵尸通道饿死
*/
void dbg_console_drain(void) {
uint16_t used;
uint32_t t0;
if (s_con_writer_up == 0U) {
return;
}
if (s_con_draining != 0U) {
return; /* 重入保护:写者自记录不触发嵌套 drain */
}
s_con_draining = 1U;
t0 = (uint32_t)HAL_GetTick();
/* 故障冷却:僵尸通道(ORE 重锁)秒内不自愈,冷却期内不发送——
* 本轮 drain 直接让位主循环,保住喂狗节奏(gen-288 停喂误复位的
* 直接诱因 = 背靠连发的块超时链)。无符号差值跨 49.7 天回绕安全 */
if (s_con_cool_until != 0U) {
if ((uint32_t)(t0 - s_con_cool_until) < DBG_CON_FAULT_COOL_MS) {
s_con_draining = 0U;
return;
}
s_con_cool_until = 0U;
}
while ((used = con_used()) != 0U) {
uint16_t tail;
uint16_t contig;
uint16_t n;
HAL_StatusTypeDef st;
tail = s_con_tail;
contig = (uint16_t)(DBG_CON_RING_SIZE - tail); /* 到环尾的连续段 */
n = (used < contig) ? used : contig;
if (n > DBG_CON_CHUNK_MAX) {
n = DBG_CON_CHUNK_MAX;
}
st = con_tx_bounded(&s_con_ring[tail], n);
taskENTER_CRITICAL();
s_con_tail = (uint16_t)((uint16_t)(s_con_tail + n) & (DBG_CON_RING_SIZE - 1U));
taskEXIT_CRITICAL();
if (st != HAL_OK) {
/* 僵尸通道:软复位自愈 + 进入发送冷却(ORE 毫秒级重锁,
* 继续背靠连发只会把主循环耗在块超时链上);本块丢弃
* (行已出环,不可重放) */
con_uart_reinit();
s_con_tx_fault++;
s_con_cool_until = (uint32_t)HAL_GetTick();
if (s_con_tx_fault == 1U) {
static const char note[] = "\r\n[WARN] CONSOLE tx fault: UART reinit (self-healed)\r\n";
syslog_post(SYSLOG_RUN, "console tx fault, uart reinit"); /* 仅 run.log,不入自检判定 */
(void)dbg_console_write_raw(note, (unsigned)sizeof(note) - 1U);
}
break; /* 故障即止:本轮 drain 到此为止,让位主循环 */
}
if ((uint32_t)(HAL_GetTick() - t0) >= DBG_CON_DRAIN_BUDGET_MS) {
break; /* 预算用尽:让位主循环(看门狗轮询),下轮续排 */
}
}
s_con_draining = 0U;
/* 丢弃可观测性:缓冲满丢行必须显式可见(否则会掩盖开机自检/测试标记的丢失) */
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,仍满则丢弃该行;
* 写者自身记录日志时先排空缓冲,避免自等。
* 等待上界优先用 DWT 真实时间(CON_APPEND_WAIT_CYC,时间源无关)——
* tick 停摆时 vTaskDelay 本身也会停,故不能用它作唯一判据(round 8b)。
*/
static int con_append(const char *buf, uint16_t len) {
uint32_t waited = 0U;
uint32_t d0 = 0U;
uint8_t have_dwt = 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) && (s_con_draining == 0U)) {
dbg_console_drain(); /* 写者自己记录:先腾空,避免自等(drain 进行中则跳过,防嵌套) */
}
con_dwt_ensure();
if (s_con_dwt_up != 0U) {
d0 = DWT->CYCCNT;
have_dwt = 1U;
}
for (;;) {
uint16_t used = con_used();
uint16_t free_space = (uint16_t)((DBG_CON_RING_SIZE - 1U) - used);
if (free_space >= len) {
break;
}
if (have_dwt != 0U) {
if ((uint32_t)(DWT->CYCCNT - d0) >= CON_APPEND_WAIT_CYC) {
s_con_drop++;
return -1;
}
} else 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;
}
/*
* 函数功能:已发生的 TX 故障(僵尸通道自愈)次数(诊断用)
*/
uint32_t dbg_console_tx_fault(void) {
return s_con_tx_fault;
}
/*
* 函数功能: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;
}