STM32中为何必须使用microLIB实现printf重定向功能?

目录

  • 明明是重定向printf,但是为什么重写的是fputc?
  • 那为什么在MDK开发STM32的时候,需要使用MicroLIB才能重定向呢?
  • 半主机模式(Semihosting)
  • MicroLIB
  • 使用MicroLIB重定向
  • 第一步:首先勾选MDK中的use microlib
  • 第二步:添加重定向代码
  • hal库版本
  • 标准库版本
  • 寄存器版本
  • 不使用MicroLIB的方法
  • 明明是重定向printf,但是为什么重写的是fputc?

    在单片机的开发中,关于重定向printf到串口,相信大家都不陌生。通过重定向到串口,就可以连接电脑调试程序中的bug,非常好用。那么为什么重写的是fputc呢?

    原因是这样的:printf是​​高级格式化输出函数​​,其定义在标准库stdio.h中。它的输出逻辑分为两步,第一步,printf 调用 vfprintfvfprintf解析格式字符串并生成字符序列;而第二步,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朔一

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32中为何必须使用microLIB实现printf重定向功能?

    发表回复