通过串口实现printf和scanf函数

时间:2021-06-10 09:59:22

转自  草根老师博客(程姚根)

在做裸板开发时,常常需要通过输出或者通过串口输入一些信息。

在有操作系统机器上,我们很少关心输入和输出的问题。因为有很多现成的库函数供我们调用。在做裸板开发时,可没有现成库函数供我们调用,一切都需要我们自己实现。


下面我们通过串口在裸板上实现一个printf和scanf函数。


printf主要用来进行格式化输出,scanf函数主要用来进行格式化输入的。这里个函数都是不定参数函数,这里简单介绍一下不定参函数实现方法。


一、不定参数的造型


function(type  arg,...);

一般第一个参数arg指定参数的个数,


int function(3,arg1,arg2,arg3);

这是一种显示的告诉编译器有几个参数,第一个参数3,即代表后面有三个参数。


但也不是唯一的,比如printf的第一个参数是一个字符串,它是通过这个参数来确定有几个参数的.

int printf(const char *format, ...);

例如我们调用的时候printf("%d,%s,%d",i,str,j);

第一个参数是"%d,%s,%d",通过分析它我们可以知道有几个。


二、函数的参数压栈


1. 固定的参数函数调用过程

通过串口实现printf和scanf函数


一般函数参数入栈是按照从右向左的顺序入栈。这样第一个参数arg1就放在了栈顶的位置。

2.变参函数调用过程


通过串口实现printf和scanf函数

通过上面的参数入栈方式我们可以得到如下结论:


如果想将栈中的参数读出来,我们只需要知道,栈顶元素的地址即第一个参数的地址即可。通过前面变参函数的分析,通过变参函数第一个参数可以知道传递的参数个数。根据参数入栈的顺序,我们可以知道第一个参数是放在栈顶位置的。


总结一下,现在我们已经获得两个条件了:


1.栈中参数的个数

2.栈顶元素的地址


有了这两个条件,我们就可以从栈中读取我们想要的参数了。

当然,每个参数都有自己的类型,还有的就是字节对齐了。在读取参数的时候,这些问题都必须考虑到。

幸运的是这些问题在已经被大牛们解决了,已经封装成相应的宏,我们在操作的时候只需要知道这些宏的含义即可。


三、变参函数常用宏


typedef   char  * va_list;

#define   _INTSIZEOF(n)   ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))

#define   va_start(ap,v)   (ap = (va_list)&v + _INTSIZEOF(v))

#define   va_arg(ap,t)     (*(t*)((ap += _INTSIZEOF(t)) -  _INTSIZEOF(t)))

#define   va_end(ap)      (ap = (va_list)0)


这些宏在不同的操作系统,有不同的实现,想使用的话,只需要包含头文件stdarg.h就可以了。


(1)va_start宏的作用 : 


v是第一个参数,通过前面我们知道,第一个参数就是用来表明有几个参数,它不是我们实际需要的参数。我们通过它来计算出,第一个实际参数的地址,主意哦是实际参数,可不是第一个表明参数个数的参数地址,让ap指针变量保存。


(1)va_arg宏的作用:


通过va_start,我们的ap的指针已经指向了第一个实际参数。

可以看到的是ap指针先更新了,然后又减了一个值,最终把这个值返回。这里面的t代表即将获得参数的类型。

可以看出,通过va_arg宏我们获得每个实际参数的值。


(2)va_end宏的作用


将ap指针赋值为NULL,即0

下面我们自己写一个测试程序来看一下,这些宏怎么使用。

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <stdarg.h>

  4. int my_printf(char *fmt,...)
  5. {
  6.     char *p;
  7.     va_list ap;
  8.     
  9.     //获得第一个实际参数的起始地址
  10.     va_start(ap,fmt);
  11.     
  12.     //分析fmt指向的字符串
  13.     for(= fmt; *p;++)
  14.     {
  15.         if(*== '%')
  16.         {
  17.             p ++;
  18.             switch(*p)
  19.             {
  20.             //整形十进制数
  21.             case 'd':
  22.                 printf("%d",va_arg(ap,int));
  23.                 break;

  24.             //字符
  25.             case 'c':
  26.                 //变参传递char类型变量时,编译器在
  27.                 //编译的时候将其提升为int类型
  28.                 printf("%c",va_arg(ap,int));
  29.                 break;

  30.             //字符串
  31.             case 's':
  32.                 //地址占用4个字节
  33.                 printf("%s",(char *)va_arg(ap,int));
  34.                 break;

  35.             //浮点数
  36.             case 'f':
  37.                 //变参传递float类型变量时,编译器在
  38.                 //编译的时候将其提升为double类型
  39.                 printf("%f",va_arg(ap,double));
  40.                 break;
  41.             // %
  42.             case '%':
  43.                 putchar('%');
  44.                 break;
  45.             }
  46.         
  47.         }else{
  48.             putchar(*p);
  49.         }
  50.     }

  51.     //将ap赋值为NULL
  52.     va_end(ap);

  53.     return 0;
  54. }

  55. int main(int argc, const char *argv[])
  56. {
  57.     int a = 123;
  58.     char b = 'c';
  59.     float c = 12.38;
  60.     char buf[] = "hello my_printf";

  61.     my_printf("a = %d b = %c buf = %s c = %f.n",a,b,buf,c);

  62.     return 0;
  63. }

实际上,格式化的转换有现成的函数可以调用,例如:vsprintf()和vsscanf()这些函数的源代码可以从bootloader和内核源码上获得。


四、常用格式转换函数


 int vsprintf(char *str, const char *format, va_list ap);

这个函数的功能,就是把输入的格式字符串进行解释,把解释好的字符串放在str。这个函数的源码可以直接在内核中获得。


 int vsscanf(const char *str, const char *format, va_list ap);

str中是我们从键盘上输入的一些字符串,format是我们调用scanf的时候输入的格式串。通过这些信息,vsscanf函数解释出每个变量应该赋为什么值。这个函数的源码可以直接在内核中获得。

有了这两个函数后,我们就可以通过串口封装自己的printf和scanf了。


(1)通过串口实现printf函数


  1. int printf(const char *fmt,...)
  2. {
  3.     int i = 0;
  4.     va_list args;
  5.     unsigned int n;
  6.     char buffer[1024];

  7.     va_start(args,fmt);
  8.     n = vsprintf(buffer,fmt,args);
  9.     va_end(args);

  10.     //初始化串口
  11.     void uart_0_init();

  12.     for(= 0;< n;++)
  13.     {
  14.                 //通过串口发送字符
  15.         send_char(buffer[i]);
  16.     }

  17.     return n;
  18. }


(2)通过串口实现scanf函数


  1. int scanf(const char *fmt,...)
  2. {
  3.     int i = 0;
  4.     unsigned char c;
  5.     va_list args;
  6.     char buffer[1024];
  7.         
  8.     //初始化串口
  9.     void uart_0_init();

  10.     while(1)
  11.     {
  12.                 //从串口接收字符
  13.         c = recv_char();
  14.         send_char(c);
  15.         if((== 0x0d) || (== 0x0a))
  16.         {
  17.             buffer[i] = '';
  18.             break;
  19.         }else{
  20.             buffer[i++] = c;
  21.         }
  22.     }

  23.     va_start(args,fmt);
  24.     i = vsscanf(buffer,fmt,args);
  25.     va_end(args);

  26.     send_char('r');
  27.     send_char('n');

  28.     return i;
  29. }