一,Keil中Printf导致程序无法运行到Main函数
在Keil中调试STM32程序,编译烧录后,发现程序不能运行,Main函数中点亮LED灯的语句没起作用,说明没有进入Main函数。用Keil调试的时候,虽然设置了Run to main(),
但发现确实进入不了Main函数。也就是程序烧录后其实无法进入Main函数运行。(这个和Semihosting的机制有关,后面再解释)
想到自己是因为调试的需要,添加了Printf语句,所以怀疑是这个问题,然后倒腾了下,勾选了Use MicroLIB:
烧录后,发现程序就可以运行了,Printf也有输出。(程序里已经有重写fputc重定向到串口的函数)
然后注释掉Printf语句,并且取消勾选Use MicroLIB,程序烧录到开发板上也能正常运行。
小结:
1. 要使用Printf函数,在Keil中,请勾选使用MicroLIB,重定向fputc到串口。
/*!
* @brief Redirect C Library function printf to serial port.
* After Redirection, you can use printf function.
*
* @param ch: The characters that need to be send.
*
* @param *f: pointer to a FILE that can recording all information
* needed to control a stream
*
* @retval The characters that need to be send.
*
* @note
*/
int fputc(int ch, FILE* f)
{
/* send a byte of data to the serial port */
USART_TxData(DEBUG_USART, (uint8_t)ch);
/* wait for the data to be send */
while (USART_ReadStatusFlag(DEBUG_USART, USART_FLAG_TXBE) == RESET);
return (ch);
}
2. 不勾选MicroLIB,即使重写了fputc到串口,使用Printf会使程序无法运行到Main函数。
再提前加一点解释:
3. 不勾选MicroLIB而使用Printf,无法运行到Main函数,是因为Printf/scanf之类的函数受了Semihosting机制的影响
二,Keil中是必须要使用MicroLIB实现串口打印吗
因为要和同事的代码合并,他是在VSCODE中写的,所以我要迁移代码到VSCODE,但是搜了一圈发现VSCODE没有MicroLIB, 这个微库是Keil独有内置的(也许也有别的类似的,但我没有研究)。既然VSCODE没有Micro LIB,那么一定也有别的方式,那么Keil中是必须要使用MicroLIB来实现串口打印吗?答案是NO
然后突然想起之前了解到的所谓的半主机模式(英文原文Semihosting,这个“半主机”翻译感觉怪怪的,其实意思就是嵌入式设备因为硬件功能受限,而需要借用主机的一部分功能,这主要在调试的时候提供了方便,感觉 “半托管” 更直观贴切)。
Semihosting是一种机制,它允许嵌入式系统与主机计算机的操作系统进行通信,进行输入/输出操作,例如文件I/O,标准I/O(stdio)和其他与系统相关的功能。它使开发人员在开发和调试过程中与嵌入式系统进行交互,而无需额外的硬件或专用通信渠道。
使用半主机时,通常由嵌入式系统硬件处理的某些操作,例如从文件读取或写入文件,被转移(offload)到由主机的操作系统来操作。这使开发人员可以在调试目的时使用主机操作系统提供的熟悉的文件系统和I/O功能。
半主机通常涉及开发工具链提供的一小部分软件函数,这些函数充当嵌入式应用程序和主机操作系统之间的中介,为嵌入式系统与主机计算机之间的通信提供了便利。这些函数允许开发人员执行诸如打印调试消息、从主机读取输入或访问主机文件系统中的文件等任务。
半主机的一个常见用例是调试嵌入式软件应用程序,开发人员可以在其代码中使用标准的printf语句将调试信息输出到主机计算机的控制台,而不是依赖于专用的调试硬件。此外,半主机可用于任务,如将文件加载到嵌入式系统中,执行软件更新或访问主机操作系统提供的系统资源。
总的来说,半主机为开发人员提供了一种方便的方式,在开发和调试过程中与嵌入式系统进行交互,有助于简化开发流程并提高生产力。
所以就搜了一下,在Keil工程添加了以下代码禁用Semihosting :
//加入以下代码,支持printf函数,而不需要选择use MicroLIB
#pragma import(__use_no_semihosting) // 确保没有从 C 库链接使用半主机的函数
void _sys_exit(int x) //定义 _sys_exit() 以避免使用半主机模式
{
x = x;
}
struct __FILE // 标准库需要的支持函数
{
int handle;
};
/* FILE is typedef ’ d in stdio.h. */
FILE __stdout;
然后同时取消勾选MicroLIB,发现程序也能正常运行,能使用串口打印输出。
小结:
Keil中也可以不使用MicroLIB,通过禁用Semihosting,正常使用C库,可以实现串口打印。
三、VSCODE中该怎么通过串口使用Printf
既然KEIL中可以不使用MicroLIB,那么VSCODE中没有MicroLIB也就不是问题了,同样禁用Semihosting,添加重定向fputc。但是却仍然出现问题,没有输出。
搜了一圈,发现在VSCODE中若使用GCC编译器,要重定向的函数不是fputc,而是_write和__io_putchar:
/*!
* @brief Redirect C Library function printf to serial port.
* After Redirection, you can use printf function.
*
* @param ch: The characters that need to be send.
*
* @param *f: pointer to a FILE that can recording all information
* needed to control a stream
*
* @retval The characters that need to be send.
*
* @note
*/
int __io_putchar(int ch)
{
/* send a byte of data to the serial port */
USART_TxData(DEBUG_USART, ch);
/* wait for the data to be send */
while (USART_ReadStatusFlag(DEBUG_USART, USART_FLAG_TXBE) == RESET);
return ch;
}
int _write(int fd, char* ptr, int len)
{
int DataIdx;
for (DataIdx = 0; DataIdx < len; DataIdx++)
{
__io_putchar(*ptr++);
}
return len;
}
注:上述USART相关函数实际上是极海MCU的,而不是STM32,因为我实际上是在极海APM32上做开发,是参照STM32而已。但原理是通的,具体函数名字不同,根据自己是使用标准库还是HAL库,做一下改变就行了。
小结:
不同编译环境下的重定向函数有所不同,
Keil、IAR等 IDE上面,都是用以下方式重定向的:
-
int fputc(int ch, FILE *f) int fgetc(FILE *f)
在 GCC 环境下,使用的是如下方式:
int _write(int file, char *ptr, int len)
int _read(int file, char *ptr, int len)
四、完整重定向代码(通用)
最后贴上极海SDK里面的重定向完整代码,供参考:
#if defined (__CC_ARM) || defined (__ICCARM__) || (defined(__ARMCC_VERSION) && (__ARMCC_VERSION >= 6010050))
/*!
* @brief Redirect C Library function printf to serial port.
* After Redirection, you can use printf function.
*
* @param ch: The characters that need to be send.
*
* @param *f: pointer to a FILE that can recording all information
* needed to control a stream
*
* @retval The characters that need to be send.
*
* @note
*/
int fputc(int ch, FILE* f)
{
/* send a byte of data to the serial port */
USART_TxData(DEBUG_USART, (uint8_t)ch);
/* wait for the data to be send */
while (USART_ReadStatusFlag(DEBUG_USART, USART_FLAG_TXBE) == RESET);
return (ch);
}
#elif defined (__GNUC__)
/*!
* @brief Redirect C Library function printf to serial port.
* After Redirection, you can use printf function.
*
* @param ch: The characters that need to be send.
*
* @retval The characters that need to be send.
*
* @note
*/
int __io_putchar(int ch)
{
/* send a byte of data to the serial port */
USART_TxData(DEBUG_USART, ch);
/* wait for the data to be send */
while (USART_ReadStatusFlag(DEBUG_USART, USART_FLAG_TXBE) == RESET);
return ch;
}
/*!
* @brief Redirect C Library function printf to serial port.
* After Redirection, you can use printf function.
*
* @param file: Meaningless in this function.
*
* @param *ptr: Buffer pointer for data to be sent.
*
* @param len: Length of data to be sent.
*
* @retval The characters that need to be send.
*
* @note
*/
int _write(int file, char* ptr, int len)
{
int i;
for (i = 0; i < len; i++)
{
__io_putchar(*ptr++);
}
return len;
}
#else
#warning Not supported compiler type
#endif
根据自己的MCU,修改USART相关的函数即可。