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 符号找到标准输出流 |
USART0 | GD32 SPL 宏 | fputc() 实际发送的目标外设 |
USART_FLAG_TBE | GD32 SPL 宏 | 发送缓冲区空标志,fputc() 用它做阻塞等待 |
RESET | GD32 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");
}
要点:
- 格式串
%x没有宽度限定,因此一个字节0x05只会打印成5而不是05,0x0A打印成a。整行 dump 的字段宽度不固定(1~2 个字符 + 一个空格 + 每字节触发一次fputc阻塞等待),无法用等宽列对齐来肉眼比对,也容易看错位。 - 每字节一次
IWDG_Feed():这是为了在打印大块数据(例如FLASH_SECTOR_SIZE= 4096 字节,见data/resume_data/flash/flash_data_store.c:266)时不被独立看门狗复位。4096 字节 × 每字节一次printf,在 9600/115200 bps 下耗时极长,因此这些调用点在生产固件里通常被#if条件编译或注释屏蔽(例如net/ems_download.c:70就是注释掉的)。 len为int,负数会被当成巨长的正数导致越界读;调用点传的都是u16/常量,实际未触发。buf参数类型是u8 *,调用侧普遍需要强制转换(如ShowData((u8 *)&g_stEventLogStoreInfo.u16Head, l_u32Len),data/net_event_log/net_event_log.c:386)。
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 SPL | usart_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. 备注与风险点
- 文件头注释写的是
@file :bms.c(debug.c:2),是从bms.c复制粘贴后未改的遗留,容易误导后续维护者。 #include "usart1.h"是错配的:本文件操作USART0,而usart1.h声明的是usart1_*(对应 USART1)。删除该 include 不影响编译,保留则是噪音。fputc()中的忙等没有超时(debug.c:46):若 USART0 未初始化、时钟未使能或 TX 引脚被复用为 GPIO(例如处于 ATE 透传模式、usart_deinit()之后),printf会在此永久阻塞。这在task_system_aux调用board_test()/passthrough_detection()的场景下尤其需要留意。ShowData()的%x无宽度限定(debug.c:65):单字节值小于 0x10 时只打印 1 位,整行 dump 字段不等宽,人眼比对极易错位;建议改为%02x。这是本文件最容易被误用的地方。ShowData()每字节调一次printf并喂一次狗:对大缓冲(4096 字节)会产生数千次fputc阻塞和数千次看门狗重载,严重拉长执行时间,并破坏独立看门狗对"任务是否卡住"的判定能力——喂狗频率被日志输出本身拉高了。ShowData()的len形参为int且无负数/上限校验,越界读风险由调用方负责,接口本身无防护。_sys_exit()的形参自赋值x = x;(debug.c:39)是消除"未使用参数"告警的常见写法,但部分编译器会将该语句优化掉后仍然告警,改用(void)x;更规范。struct __FILE只是最小替身:若将来开启文件操作(如fopen)或使用scanf,需要补充_sys_open/_sys_read/_ttywrch等更多符号,当前实现只覆盖printf场景。- 本文件不涉及任何中断、信号量或邮箱,因此没有竞态问题;但
printf/ShowData从多个任务(task_system_aux、task_net、task_interface等)调用时没有任何互斥,多任务同时打印会出现字符交织。当前固件主要靠条件编译关闭大部分打印来规避。