天天看点

嵌入式单片机实现printf函数的原理

为什么需要printf函数

标准库的 printf函数 能方便的输出格式化的字符串

而单片机只有串口,只能打印int型数。如果能借用标准库 stdio.h 文件里的 printf函数,那么单片机输出格式化字符串就方便了。

printf函数原理

printf函数 的底层会调用字符输出函数,我们只需要把这个字符输出函数给重新实现为我们的单片机串口输出函数,就可以实现 printf函数 功能了。

stdio.h有两种:

keil用的是armcc编译器(只支持stm32)

​MDK\ARM\ARMCC\include​

​​这个目录下的​

​stdio.h​

armcc这个stdio.h里的 printf函数 的底层调用的 字符输出函数 是fputc(int ch, FILE *f)函数,所以我们重新实现该函数即可。(见附录中的方案2的解释)

//加入以下代码,支持printf函数,而不需要选择use MicroLIB    
#if 1
#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((USART1->SR&0X40)==0);//循环发送,直到发送完毕   
  USART1->DR = (u8) ch;      
  return ch;
}
#endif      

关于半主机,参考附录的解决方案2

其它IDE用的是gcc编译器

​GNU-Tools-ARM-Embedded\arm-none-eabi\include​

​​里面的​

​stdio.h​

gcc编译器这个stdio.h里的 printf函数 的底层调用的 字符输出函数 是print_char(char c)函数,所以我们重新实现该函数即可

#include <stdio.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdarg.h>

#undef errno
extern int errno;
extern int _end;
//extern void print_char(char);

//这样重新定义一下即可
void print_char(char c)
{
    HAL_UART_Transmit(&handle, (uint8_t *) (&c), 1, 1); //我们单片机的串口输出函数
}


caddr_t _sbrk(int incr)
{
    static unsigned char *heap = NULL;
    unsigned char *prev_heap;

    if (heap == NULL)
    {
        heap = (unsigned char *) &_end;
    }
    prev_heap = heap;

    heap += incr;

    return (caddr_t) prev_heap;
}

int link(char *old, char *new)
{
    return -1;
}

int _close(int file)
{
    return -1;
}

int _fstat(int file, struct stat *st)
{
    st->st_mode = S_IFCHR;
    return 0;
}

int _isatty(int file)
{
    return 1;
}

int _lseek(int file, int ptr, int dir)
{
    return 0;
}

int _read(int file, char *ptr, int len)
{
    return 0;
}

void abort(void)
{
    /* Abort called */
    while (1)
        ;
}


int _write(int fd, char *pBuffer, int size)
{
    for (int i = 0; i < size; i++)
    {
        if (pBuffer[i] == '\n')
        {
            print_char('\r');
        }
        print_char(pBuffer[i]);
    }
    return size;
}      

附录

关于微库,半主机的解释:

半主机:专门用arm的,比如我们的开发板没有 键盘和屏幕 ,但是,使用半主机模式后,我们就可以利用仿真器或其他连接到电脑(主机),使用电脑(即主机)的屏幕和键盘通过printf() 和 scanf() 来与开发板交互。

单片机使用 printf() 和 scanf() 函数时 ,只是希望通过自身硬件带有的串口,打印或接收数据。所以此时的单片机并不是工作在半主机模式的。

而我们通常使用的C库 中 printf() 和 scanf() 函数 是需要工作在半主机模式。如何解决这个矛盾?

方案1:在KEIL中勾选 Use MicroLIB . 即使用微库. (因为微库是一个压缩库,而微库中的printf() 和 scanf() 函数 就不是工作在半主机模式下的),其实keil就是在编译参数里传入了一个

-D__MICROLIB 这个宏

方案2:继续使用标准的C库,在代码中声明不使用半主机模式。所以就有了上面keil中stm32程序那段代码了。而gcc的stdio.h中的printf是不需要指明不使用半主机的。

参考博客:

​​配置CLion用于STM32开发【优雅の嵌入式开发】 - 知乎​​