本文共 5210 字,大约阅读时间需要 17 分钟。
在之前STM32的学习中,我在串口输出调试信息的时候,经常采用printf()函数作为串口输出函数,这样不仅方便调试而且代码易读。
在S3C2440的学习中,对于UART同样需要对串口输出信息进行调试,那么在这里可不可以使用printf函数呢?
当然是可以的,不过相比于STM32中简单的配置,S3C2440中对printf的使用要深入到printf函数的原理,将printf函数进行彻头彻尾的刨析,然后结合底层的UART输出,使用printf打印调试信息,其实现的前提是:
其中,UART底层的putchar函数在上一篇博客中以及实现了,这里主要介绍对printf函数的刨析,以及对可变参数的推导。
对printf函数的刨析,由my_printf.c实现,由my_printf.c调用UART底层的putchar函数即可实现UART的printf输出。
在标准库<stdio.h>中定义了printf函数,其声明如下:
*int printf(const char format, …);
可以看出printf的参数为:(const char *format, …)
其中:format代表固定参数,…代表可变参数,固定参数中含有格式字符
(听起来有点懵逼,下面解读一下参数的内容)
顾名思义,固定参数就是一串固定的字符串, *format就是字符串的首地址,根据首地址我们可以将字符串的全部内容输出。欸,可是固定参数中也有特殊的字符啊,那就是格式字符,格式字符用来说明可变参数的数据类型和个数,根据固定参数中的格式字符,可以将可变参数按照格式打印出来。**所以利用printf打印信息的主要步骤就是解析固定参数以及固定参数中的格式字符,将可变参数在格式字符位置输出,直到固定参数解析完毕!**这样就可以将参数中像表达的内容完整地打印出来。
格式字符:
例如:
printf("I'm %s, my ID is %d ,my score is %.2f !!!\n","Bob",25,98.2);
运行结果如下:
printf是从固定参数”I’m %s, my ID is %d ,my score is %.2f !!!\n”的首地址开始解析的,逐个输出字符,直到第一个格式字符%s,对应了后面可变参数的“Bob”字符串,在格式字符的位置上打印可变参数,然后继续解析固定参数继续打印字符,直到第二个格式字符%d,在%d的位置打印可变参数25,然后继续解析直到第三个格式字符%.2f,根据格式字符打印出98.20,然后继续解析,直到最后一个转义字符“\n”,结束了固定参数的解析,也完成了数据的打印输出。
没遇到%就直接输出,遇到%根据格式符前导码分情况处理。
我们了解了参数组成,以及printf打印输出的流程,那么问题来了,固定参数的首地址是直接传进来的,那可变参数的地址怎么得到呢???
实际上,参数都存储在栈上的,而且固定参数和可变参数的地址是连续的,对!!!因为是连续的,所以我们就可以通过指针的移动和取值,由固定参数得到可变参数的地址,从而打印出可变参数。
下面有一段代码来解释获取可变参数的过程:
(参考自百问科技代码)
#includestruct person{ char *name; int age; char score; int id;};/* *int printf(const char *format, ...); *依据:x86平台,函数调用时参数传递是使用堆栈来实现的 *目的:将所有传入的参数全部打印出来 */ int push_test(const char *format, ...){ char *p = (char *)&format; int i; struct person per; char c; double d; printf("arg1 : %s\n",format); p = p + sizeof(char *); /*指针对连续空间操作时: 1) 取值 2)移动指针*/ i = *((int *)p); p = p + sizeof(int); printf("arg2 : %d\n",i); /*指针对连续空间操作时: 1) 取值 2)移动指针*/ per = *((struct person *)p); p = p + sizeof(struct person); printf("arg3: .name = %s, .age = %d, .socre=%c .id=%d\n",\ per.name, per.age, per.score, per.id); /*指针对连续空间操作时: 1) 取值 2)移动指针*/ c = *((char *)p); p = p + ((sizeof(char) + 3) & ~3); printf("arg4: %c\n",c); /*指针对连续空间操作时: 1) 取值 2)移动指针*/ d = *((double *)p); p = p + sizeof(double); printf("arg5: %f\n",d); return 0;}int main(void){ push_test("abcd",123,per,'c',2.79); return 0;}
这是已知可变参数情况下的输出,目的就是理解如何通过固定参数得到可变参数,通过栈空间的移动取值就可以!
在stdarg.h中,有解决变参问题的一些宏定义,改进后作注释如下:
/* 参数指针 */ typedef char * va_list;/* 考虑到地址对齐,对齐大小为sizeof(int) *//* 比如,如果sizeof(n)在1-4之间,那么_INTSIZEOF(n)=4;如果sizeof(n)在5-8之间,那么 _INTSIZEOF(n)=8。 */ /* & ~(sizeof(int) - 1)相当于将0~3掩盖掉 sizeof(int) - 1 相当于增加0~3 如此,就只取决于sizeof(n)的大小 */ #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )/* 得到第一个可变参数的起始地址,传给ap */#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )/* 移动指针到下一个参数,取值 */#define va_arg(ap,t) (*(t *)(ap = ap + _INTSIZEOF(t), ap - _INTSIZEOF(t)))/* 结束,防止ap成为野指针 */#define va_end(ap) ( ap = (va_list)0 )
解决了变参问题后,剩下的就是对printf函数的封装了,封装引出的接口就是:outc()输出一个字符、outs()输出字符串,到时候只要将这俩个接口和UART的putchar()函数、puts()函数对应起来,就可以实现UART使用printf了。
printf输出打印的核心部分:输出固定参数及可变参数的函数
/*reference : int vprintf(const char *format, va_list ap); *//* 输出固定参数及可变参数的函数 */static int my_vprintf(const char *fmt, va_list ap) { char lead=' '; int maxwidth=0; for(; *fmt != '\0'; fmt++) { if (*fmt != '%') { outc(*fmt); //outc()就是输出一个字符 continue; } //format : %08d, %8d,%d,%u,%x,%f,%c,%s fmt++; if(*fmt == '0') { lead = '0'; fmt++; } lead=' '; maxwidth=0; while(*fmt >= '0' && *fmt <= '9') { maxwidth *=10; maxwidth += (*fmt - '0'); fmt++; } switch (*fmt) { case 'd': out_num(va_arg(ap, int), 10,lead,maxwidth); break; case 'o': out_num(va_arg(ap, unsigned int), 8,lead,maxwidth); break; case 'u': out_num(va_arg(ap, unsigned int), 10,lead,maxwidth); break; case 'x': out_num(va_arg(ap, unsigned int), 16,lead,maxwidth); break; case 'c': outc(va_arg(ap, int )); break; case 's': outs(va_arg(ap, char *)); break; default: outc(*fmt); break; } } return 0;}
里面的out_num()函数是根据格式字符输出不同形式的数字,其函数定义如下:
static int out_num(long n, int base,char lead,int maxwidth) { unsigned long m=0; char buf[MAX_NUMBER_BYTES], *s = buf + sizeof(buf); int count=0,i=0; *--s = '\0'; if (n < 0) m = -n; else m = n; do { *--s = hex_tab[m%base]; count++; } while ((m /= base) != 0); if( maxwidth && count < maxwidth) { for (i=maxwidth - count; i; i--) *--s = lead; } if (n < 0) *--s = '-'; return outs(s);}
所以,printf函数的封装就如下:
int printf(const char *fmt, ...) { /* 参数指针 */ va_list ap; /* 有固定参数得到起始地址 */ va_start(ap, fmt); /* 输出打印 */ my_vprintf(fmt, ap); /* 结束 */ va_end(ap); return 0;}
上面我们知道,对printf函数的刨析,由my_printf.c实现,由my_printf.c调用UART底层的putchar函数即可实现UART的printf输出,调用接口就是outc()、outs()函数,只要如下设置就ok:
static int outc(int c) { putchar(c); return 0;}static int outs (const char *s){ while (*s != '\0') putchar(*s++); return 0;}
对UART的配置已经在之前的博客中有详细介绍了。
有问题可以q我,一起学习:2723808286
转载地址:http://qpwzi.baihongyu.com/