STM32中为何必须使用microLIB实现printf重定向功能?
目录
明明是重定向printf,但是为什么重写的是fputc?
在单片机的开发中,关于重定向printf到串口,相信大家都不陌生。通过重定向到串口,就可以连接电脑调试程序中的bug,非常好用。那么为什么重写的是fputc呢?
原因是这样的:printf是高级格式化输出函数,其定义在标准库stdio.h中。它的输出逻辑分为两步,第一步,printf 调用 vfprintf,vfprintf解析格式字符串并生成字符序列;而第二步,vfprintf 通过 fwrite 或循环调用 fputc 将字符写入缓冲区或直接输出到流,若流是行缓冲(如 stdout),遇到换行符 \n 或缓冲区满时,数据才会通过 fputc 发送到设备。所以简单来说,fputc 是 printf 的底层输出实现,在 STM32 等资源受限设备中,重定向 printf 到串口的本质是重写 fputc。
当然fputc函数本身也是定义在标准库stdio.h中的。因为 printf 的所有输出最终都会转化为对 fputc 的调用,那么我们在重定向printf的时候,也就只需要重写fputc了。
fputc的函数原型为
int fputc(int c, FILE *stream);
它直接操作硬件或系统接口,是 I/O 的最基础单元。
那为什么在MDK开发STM32的时候,需要使用MicroLIB才能重定向呢?
半主机模式(Semihosting)
ARM标准库(如ARMCC的标准C库)默认使用半主机模式实现printf。半主机模式(Semihosting)是ARM架构中一种特殊的调试机制,允许在嵌入式目标设备(如STM32)上运行的代码借用主机(运行调试器的电脑)的输入/输出设备(如屏幕、键盘、文件系统)执行I/O操作。半主机需调试器作为中介,实际部署到嵌入式设备(无调试器连接)时,程序会因无法触发主机响应而卡死或崩溃。
也就是说该模式依赖调试器(如JTAG/SWD)与主机通信,需仿真器支持,如果没有仿真器的话,程序就会进入卡死和崩溃。而我们在使用芯片的时候,显然要断掉调试线的,所以我们如果通过标准库实现重定向printf的话,就需要关闭半主机模式。关闭半主机模式需要程序里写一些代码,这个问题我们后面再说。
MicroLIB
Microlib(也称MicroLIB)是ARM Keil MDK开发环境中提供的高度精简版C标准库,专为资源受限的嵌入式系统设计。它通过牺牲部分ISO C标准兼容性,换取极小的代码体积和内存占用。特别适合在小容量的芯片中使用,比如STM32F030中可减少30%-50% Flash占用。在这个库中,默认禁用半主机模式(Semihosting),无需额外代码处理调试依赖问题。那么这也就意味着,我们只要使用了这个MicroLIB的话,就再也不用程序设定关闭半主机模式了。一方面它默认关闭了半主机模式,另一方面代码内存占用大大减小,这也就是为什么在重定向printf的时候使用MicroLIB了。
当然了,这个库也有它的缺点。比如说,不支持文件I/O操作(fopen/fread等),没有线程安全机制和对C的标准兼容性差等。但是在不跑操作系统的STM32中,使用microlib仍然是最方便的操作方法。
使用MicroLIB重定向
第一步:首先勾选MDK中的use microlib
第二步:添加重定向代码
hal库版本
在hal库中的重定向,就是把fputc重定向到串口发送函数,fgetc重定向到串口接收函数。关于超时时间可以自行设置。
#include <stdio.h>// 包含标准输入输出头文件
int fputc(int ch,FILE *f)
{
//采用轮询方式发送1字节数据,超时时间设置为无限等待
HAL_UART_Transmit(&huart1,(uint8_t *)&ch,1,HAL_MAX_DELAY);
return ch;
}
int fgetc(FILE *f)
{
uint8_t ch;
// 采用轮询方式接收 1字节数据,超时时间设置为无限等待
HAL_UART_Receive( &huart1,(uint8_t*)&ch,1, HAL_MAX_DELAY );
return ch;
} //重定向scanf
标准库版本
/**
* 函 数:串口发送一个字节
* 参 数:Byte 要发送的一个字节
* 返 回 值:无
*/
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART3, Byte); //将字节数据写入数据寄存器,写入后USART自动生成时序波形
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待发送完成
/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}
int fgetc(FILE *f)
{
uint8_t ch;
ch=USART_ReceiveData(USART1);
return ch;
} //重定向scanf
int fputc(int ch, FILE* f)
{
UART_Send_Byte(USART1, (uint8_t)ch);
return ch;
}
寄存器版本
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR & 0X40) == 0); //循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
/*
** Rewrite fgetc function and make scanf function work
**/
int fgetc(FILE* file)
{
while((USART1->ISR & UART_IT_RXNE) == RESET);
return USART1->RDR;
}
不使用MicroLIB的方法
这种方法比较适用于需完整ISO C功能或非Keil环境(如GCC)。
#pragma import(__use_no_semihosting) // 阻止链接半主机函数
void _sys_exit(int x) { while(1); } // 避免未定义符号错误
// Change it if you use different USART port
#define USARTx USART1
struct __FILE { int handle; };
FILE __stdout;
FILE __stdin;
int fputc(int ch, FILE *f) {
while(USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET){}
USART_SendData(USARTx, ch);
return(ch);
}
int fgetc(FILE *f) {
char ch;
while(USART_GetFlagStatus(USARTx, USART_FLAG_RXNE) == RESET){}
ch = USART_ReceiveData(USARTx);
return((int)ch);
}
int ferror(FILE *f) {
return EOF;
}
void _ttywrch(int ch) {
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET){}
USART_SendData(USARTx, ch);
}
作者:Serein朔一