通过串⼝实现printf和scanf函数
在做裸板开发时,常常需要通过输出或者通过串⼝输⼊⼀些信息。
在有操作系统机器上,我们很少关⼼输⼊和输出的问题。因为有很多现成的库函数供我们调⽤。在做裸板开发时,可没有现成库函数供我们调⽤,⼀切都需要我们⾃⼰实现。
下⾯我们通过串⼝在裸板上实现⼀个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. 固定的参数函数调⽤过程
⼀般函数参数⼊栈是按照从右向左的顺序⼊栈。这样第⼀个参数arg1就放在了栈顶的位置。
2.变参函数调⽤过程
通过上⾯的参数⼊栈⽅式我们可以得到如下结论:
如果想将栈中的参数读出来,我们只需要知道,栈顶元素的地址即第⼀个参数的地址即可。通过前⾯变参函数的分析,通过变参函数第⼀个参数可以知道传递的参数个数。根据参数⼊栈的顺序,我们可以知道第⼀个参数是放在栈顶位置的。
总结⼀下,现在我们已经获得两个条件了:
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.
5. int my_printf(char *fmt,...)
6. {
7.    char *p;
8.    va_list ap;
9.
10.    //获得第⼀个实际参数的起始地址
11.    va_start(ap,fmt);
12.
13.    //分析fmt指向的字符串
14.    for(p = fmt; *p;p ++)
15.    {
16.        if(*p == '%')
17.        {
18.            p ++;
19.            switch(*p)
20.            {
21.            //整形⼗进制数
22.            case 'd':
23.                printf("%d",va_arg(ap,int));
24.                break;
25.
26.            //字符
27.            case 'c':
28.                //变参传递char类型变量时,编译器在
29.                //编译的时候将其提升为int类型
30.                printf("%c",va_arg(ap,int));
31.                break;
32.
33.            //字符串
34.            case 's':
35.                //地址占⽤4个字节
36.                printf("%s",(char *)va_arg(ap,int));
37.                break;
38.
39.            //浮点数
40.            case 'f':
41.                //变参传递float类型变量时,编译器在
字符串函数注册登录42.                //编译的时候将其提升为double类型
43.                printf("%f",va_arg(ap,double));
44.                break;
45.            // %
46.            case '%':
47.                putchar('%');
48.                break;
49.            }
50.
51.        }else{
52.            putchar(*p);
53.        }
54.    }
55.
56.    //将ap赋值为NULL
57.    va_end(ap);
58.
59.    return 0;
60. }
61.
62. int main(int argc, const char *argv[])
63. {
64.    int a = 123;
65.    char b = 'c';
66.    float c = 12.38;
67.    char buf[] = "hello my_printf";
68.
69.    my_printf("a = %d b = %c buf = %s c = %f.n",a,b,buf,c);
70.
71.    return 0;
72. }
实际上,格式化的转换有现成的函数可以调⽤,例如: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.
8.    va_start(args,fmt);
9.    n = vsprintf(buffer,fmt,args);
10.    va_end(args);
11.
12.    //初始化串⼝
13.    void uart_0_init();
14.
15.    for(i = 0;i < n;i ++)
16.    {
17.                //通过串⼝发送字符

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。