EMS 知识库
首页 / knowledge/代码解析/midware/debug

debug.c

源文件路径:midware/debug/debug.c 所属模块:midware/debug(设备中间件 - 板级调试/产线测试) 代码行数:69

1. 文件职责

本文件承担整个固件的标准输入输出重定向与十六进制 dump 打印两件事:通过 #pragma import(__use_no_semihosting) + 自定义 _sys_exit() + 重写 fputc(),把 Keil 半主机模式关掉,让代码里的 printf() 直接走 USART0 输出到调试串口;再提供一个 ShowData() 把任意内存块按十六进制逐字节打印出来。

它处于最底层的"日志输出通道"位置,被数据层大量使用——data/system_config/system_config.c、data/resume_data/flash/flash_data_store.c、data/net_event_log/net_event_log.c、midware/interface/modbus/modbus.c、net/server.c 等 30 余处在写 Flash、收发 Modbus 报文、处理事件日志时调用 ShowData() 做原始数据核对。它是被动调用型工具,没有任何任务或中断直接驱动它。

2. 关键数据结构与宏

名称类型说明
__use_no_semihosting编译指示(#pragma import,debug.c:22)关闭 ARM 半主机(semihosting)模式,必须由链接期符号 _sys_exit 配合,否则中断向量会回到调试器导致脱机运行停摆
struct __FILE结构体(debug.c:24-27)极简 FILE 替身,只含 int handle,供 ARM C 库替换半主机版本使用
FILE __stdout全局变量(debug.c:29)stdout 句柄的实体,ARM 库通过 __stdout 符号找到标准输出流
USART0GD32 SPL 宏fputc() 实际发送的目标外设
USART_FLAG_TBEGD32 SPL 宏发送缓冲区空标志,fputc() 用它做阻塞等待
RESETGD32 SPL 宏与 usart_flag_get() 返回的 FlagStatus 比较

3. 函数清单

函数名作用域说明
_sys_exit全局(debug.c:37)空实现,用于在关闭半主机模式后提供 ARM C 库期望的 _sys_exit 符号;形参 x 被自赋值以消除未使用告警
fputc全局(debug.c:43)重定向标准输出:把一个字符写进 USART0 数据寄存器,并忙等 USART_FLAG_TBE 直到发送缓冲区空;返回该字符
ShowData全局(debug.c:57)打印 len 字节内存的十六进制 dump,每字节以 %x 格式输出,前后各加一行说明;每字节循环内喂一次看门狗

4. 核心逻辑解析

本文件没有状态机、协议帧或调度,核心是两条"底层适配"逻辑。

4.1 关闭半主机并重定向 printf

Keil(ARMCC)默认把 printf 走半主机(semihosting):字符不是发给串口,而是通过 BKPT 指令交给调试器处理。脱机运行时没有调试器响应,printf 会直接卡死或触发硬件异常。本文件用三段配合解除这个依赖:

#pragma import(__use_no_semihosting)   → 告诉链接器不要链入半主机版本
void _sys_exit(int x)                  → 提供库要求的退出符号,空实现
int fputc(int ch, FILE *f)             → 提供字符级输出,改走 USART0

fputc()(debug.c:43-48)的执行时序:

usart_data_transmit(USART0, (uint8_t)ch);              // 写 USART0_DATA
while (RESET == usart_flag_get(USART0, USART_FLAG_TBE));
return ch;

注意先写数据、再等 TBE,这个顺序在语义上是"写完当前字节后等待发送寄存器的下一个空位",配合 GD32 的 TBE 语义(发送数据寄存器空)可以正常阻塞输出,但严格来说更稳妥的写法是先等 TBE 再写。因为是阻塞式忙等且没有超时,当 USART0 未初始化或波特率异常时,任何 printf 都会在此处永久卡死。

4.2 ShowData 十六进制 dump

void ShowData(u8 *buf, int len)
{
    printf("Show data is :\n");
    for (i = 0; i < len; i ++)
    {
        IWDG_Feed();                 // 每字节喂狗
        printf("%x ", buf[i]);
    }
    printf("\n");
}

要点:

5. 对外接口

本文件导出

名称类型供谁使用
ShowData(u8 *buf, int len)函数(声明在 debug.h:22)data/system_config/system_config.c(约 25 处)、data/resume_data/flash/flash_data_store.c(约 12 处)、data/net_event_log/net_event_log.c(2 处)、midware/interface/modbus/modbus.c(3 处)、net/server.c(2 处)
fputc(int ch, FILE *f)函数ARM C 库,由所有 printf() 调用间接进入
_sys_exit(int x)函数ARM C 库,配合 __use_no_semihosting 使用
FILE __stdout全局变量ARM C 库

本文件调用的外部函数/变量

来源模块使用内容
hardware/iwdg(经 iwdg.h)IWDG_Feed()
GD32 SPLusart_data_transmit()、usart_flag_get()、宏 USART0、USART_FLAG_TBE、RESET
标准库printf

注意:debug.c:16 包含了 usart1.h,但本文件使用的是 USART0(USART0 宏来自 chip.h 里的 GD32 器件头,与 usart1.h 无关)。这个 include 是多余/错配的。

6. 依赖关系

依赖

stdio.h、chip.h、debug.h、usart1.h、iwdg.h(debug.c:13-17)。

被依赖

所有需要 printf 输出或需要 STATIC_ASSERT/ASSERT_FLASH_SECTOR_SIZE 宏的编译单元。直接 #include "debug.h" 的文件包括:

data/system_config/system_config.c、data/system_config/system_config.h、logic/logic.h、midware/auxiliary/system_aux.h、midware/bms/bms.h、midware/debug/board_test.c、midware/debug/debug.c、midware/ethernt/ethernet.h、midware/evc/evc.h、midware/evc/evc_ii.h、midware/interface/dry_node/dry_node.h、midware/interface/modbus/modbus.h、midware/inverter/inverter.h、midware/meter/ct.h、midware/meter/meter.h 等。

由于它是 printf 的唯一落地点,几乎整个工程的日志能力都挂在本文件上。

7. 备注与风险点

  1. 文件头注释写的是 @file :bms.c(debug.c:2),是从 bms.c 复制粘贴后未改的遗留,容易误导后续维护者。
  2. #include "usart1.h" 是错配的:本文件操作 USART0,而 usart1.h 声明的是 usart1_*(对应 USART1)。删除该 include 不影响编译,保留则是噪音。
  3. fputc() 中的忙等没有超时(debug.c:46):若 USART0 未初始化、时钟未使能或 TX 引脚被复用为 GPIO(例如处于 ATE 透传模式、usart_deinit() 之后),printf 会在此永久阻塞。这在 task_system_aux 调用 board_test()/passthrough_detection() 的场景下尤其需要留意。
  4. ShowData() 的 %x 无宽度限定(debug.c:65):单字节值小于 0x10 时只打印 1 位,整行 dump 字段不等宽,人眼比对极易错位;建议改为 %02x 。这是本文件最容易被误用的地方。
  5. ShowData() 每字节调一次 printf 并喂一次狗:对大缓冲(4096 字节)会产生数千次 fputc 阻塞和数千次看门狗重载,严重拉长执行时间,并破坏独立看门狗对"任务是否卡住"的判定能力——喂狗频率被日志输出本身拉高了。
  6. ShowData() 的 len 形参为 int 且无负数/上限校验,越界读风险由调用方负责,接口本身无防护。
  7. _sys_exit() 的形参自赋值 x = x;(debug.c:39)是消除"未使用参数"告警的常见写法,但部分编译器会将该语句优化掉后仍然告警,改用 (void)x; 更规范。
  8. struct __FILE 只是最小替身:若将来开启文件操作(如 fopen)或使用 scanf,需要补充 _sys_open/_sys_read/_ttywrch 等更多符号,当前实现只覆盖 printf 场景。
  9. 本文件不涉及任何中断、信号量或邮箱,因此没有竞态问题;但 printf/ShowData 从多个任务(task_system_aux、task_net、task_interface 等)调用时没有任何互斥,多任务同时打印会出现字符交织。当前固件主要靠条件编译关闭大部分打印来规避。