使用AI解析了STM32例程中对printf重定向的代码。源码如下:
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x)
{
x = x;
}
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART2->SR&0X40)==0);//循环发送,直到发送完毕
USART2->DR = (u8) ch;
return ch;
}
首先是:
#pragma import(__use_no_semihosting)
- 这是一个编译器指令,告诉链接器 不要使用半主机功能。
- 半主机(Semihosting)是 ARM 调试时的一种机制,允许你在裸机程序中调用标准库函数(如
printf
、fopen
、exit
等),它们会被调试器“劫持”并转发到主机(PC)上执行。 - 在没有调试器连接时,这些函数会导致程序卡死,因此需要禁用。
什么是 Semihosting(半主机)?
Semihosting(半主机) 是 ARM 提供的一种调试辅助机制,它允许嵌入式应用程序在没有操作系统或文件系统支持的情况下,通过调试器与运行该调试器的 PC 主机进行交互。简单来说,就是让目标板上的程序可以借助调试器,借用 PC 的 I/O 能力。
它是如何工作的?(底层原理)
ARM Cortex-M 处理器提供了一个特殊的指令:BKPT
(Breakpoint Instruction)。当你的程序中调用了某些标准库函数(如 printf()
)并且启用了半主机功能时,编译器会将这些函数替换为对 __semihosting
的调用,并插入一个 BKPT
指令。
典型流程如下:
- 程序调用
printf("Hello")
。 - 编译器链接的是半主机版本的
printf
,最终会执行一个BKPT 0xAB
指令。 - 处理器进入调试异常状态,暂停运行。
- 调试器(如 Keil、J-Link、OpenOCD、GDB)检测到这个特定的
BKPT
指令。 - 调试器解析这是一个半主机请求,比如打印字符串。
- 调试器把数据发送给 PC 主机,在控制台输出。
- 调试器返回结果给目标设备,继续执行。
✅ 半主机本质上是“程序在目标设备运行,但 I/O 操作由主机代理完成”。
哪些函数属于半主机范畴?
常见的被重定向为半主机操作的标准 C 库函数包括:
函数 | 功能 |
---|---|
printf() / puts() / putchar() | 输出信息到主机终端 |
scanf() / getchar() | 从主机终端输入 |
fopen() / fread() / fwrite() | 文件操作(读写主机上的文件) |
exit() / _sys_exit() | 程序退出处理 |
time() | 获取当前时间 |
这些函数在启用半主机模式后,不会在 MCU 上真正执行 I/O 操作,而是通过调试器转发给主机。
其次是这部分:
struct __FILE
{
int handle;
};
FILE __stdout;
- 这是一个结构体(
struct
)定义,名为__FILE
。 - 它只包含一个成员变量:
int handle;
handle
是一个整数,用来代表某种资源的标识符(即“句柄”)。- 在这里,
handle
很可能表示的是一个文件描述符(file descriptor),在类 Unix 系统中,0 表示标准输入(stdin),1 表示标准输出(stdout),2 表示标准错误(stderr)。 - 注意:这里的结构体名是
__FILE
,使用了双下划线开头,这是 C/C++ 中常用于保留给编译器或系统库的命名方式,表明这是内部使用的类型。
- 最后一行声明了一个
FILE
类型的全局变量__stdout,
根据变量名可以推测,它是标准输出流的一个表示。后续可以通过对这个结构体中的handle
字段赋值为1
(标准输出的文件描述符),来实现将数据输出到标准输出设备(如控制台)。 - 因此这部分代码:
- 这是对标准 I/O 库中的
FILE
结构体的一个最小化实现。 __stdout
是标准输出流的句柄,在调用printf
时会用到它。- 这些定义是为了让标准库能正常工作,即使你没有完整的文件系统或操作系统支持。
之后是:
void _sys_exit(int x)
{
x = x;
}
_sys_exit()
是一个系统调用函数,当程序退出(如调用exit()
)时被调用。- 因为我们禁用了半主机模式,所以必须提供这个函数的空实现,否则链接器会报错。
x = x;
是为了防止编译器警告“unused parameter”。
最后是:
int fputc(int ch, FILE *f)
{
while((USART2->SR & 0X40) == 0); // 等待发送寄存器为空
USART2->DR = (u8) ch; // 发送字符
return ch;
}
fputc()
是标准库中的一个函数,用于向FILE
流写入一个字符。- 当你使用
printf()
时,最终会调用fputc()
,默认情况下它是空的或指向半主机输出。 - 你在这里重写了它,让它把字符通过 USART2 发送到串口助手(如串口调试工具)。