s3c2440——实现裸机的简易printf函数
Posted 悄然拔尖
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了s3c2440——实现裸机的简易printf函数相关的知识,希望对你有一定的参考价值。
在单片机开发中,我们借助于vsprintf函数,可以自己实现一个printf函数,但是,那是IDE帮我们做了一些事情。
刚开始在ARM9裸机上自己写printf的实现的时候,包含对应头文件也会提示vsprintf函数找不到,查询很多资料之后,发现使用arm-linux-ld就是找不到对应的库函数,换成arm-linux-gcc 使用,
arm-linux-gcc -v -static -Wl,-Tsdram.lds,-Map,system.map -nostartfiles -o sdram.elf $^
这样之后,倒是可以找到vsprintf的定义了,可是编译之后的文件有400多k,下载进入开发板还是没能正常工作。后面放弃了这种方法,先自己实现了一个简易版本的printf函数,用来作为调试已经足够了,没有人会拿ARM9以上的芯片只跑裸机,等之后上linux操作系统之后,我们可以有很多调试方式,只是传统IDE开发,帮我们做了很多我们并不知道的事情。
现在,我们开始实现自己的简易版本printf函数。
要实现printf函数,首先不得不说的就是可变参数了。
printf函数原型:
int printf(const char *format, ...);
一个参数是一个const的char指针,作为格式标志,
另一个参数是可变参数,用3个点表示。
实现依据:
X86和我们s3c2440的堆栈增长方向,默认是一样的,都是从高地址向低地址增长,函数调用,是依靠于堆栈实现的,在我们的默认模式下,先入栈的参数,保存在高地址。
用代码来说明这个问题:
这个是要说明什么问题呢?
参数传递的时候,在栈生长方向是高地址往地址这种方式下。先入栈的,存放在高地址,通过上面的打印可以看出,最右边的参数明显先入栈,所以才会先打印b,再打印a,如果C语言基础比较好的,应该是知道原因的。为什么C语言选择参数从右往左入栈?先说结论,要是C语言不支持可变参数,那么从左到右和从右到左的的顺序都是可以的,但是为了满足可变参数的语法,那么C语言的参数入栈顺序只能是从右向左。
解释原因:
假设参数入栈按照从左到右方式入栈,当遇到可变参数的时候,
func(p1,p2,...)
p1先入栈,p2再入栈,然后是可变参数入栈,由于可变参数的数目不可确定,那么就无法动态确定偏移,也就是不能求得可变参数,可变参数是根据确定参数然后地址偏移得到的,如果从右往左的方式,那么最后被入栈的就是最左边的那个确定参数,通过这个确定参数,然后偏移就能得到可变参数,而且,无论可变参数数目多少,都不会影响后面调用func函数,因为在最左边的最后一个参数入栈之后,下面一个地址就将进行函数调用,如果是按照从左往右的方式,最左边的确定参数一开始就背入栈了,那么无法动态确定可变参数的个数,如何通过偏移去调用func函数呢?这下你也应该明白,为什么C语言书上要告诉我们,可变参数前面,必须至少要有一个确定参数(当然,可变宏除外),而且,可变参数必须位于末尾,不能位于参数中间,位于参数中间,就会出现从左往右入栈的问题。
对于已经确定的参数,它在栈上的位置也必须是确定的。衡量参数在栈上的位置,就是离开确切的 函数调用点(call func)有多远。已经确定的参数,它在栈上的位置,不应该依 赖参数的具体数量,因为参数的数量是未知的!所以,选择只能是,已经确定的参 数,离函数调用点有确定的距离。满足这个条件,只有参数入栈遵从自右向左规则。
这道这个之后,我们可以开始编写我们的printf函数了,因为后面的实现,要使用这个特性。
对于具体的实现,我不想再赘述,只是说明一下里面的va_list等数据类型,以及他们的实现和原理。
按照ANSI(AmericanNationalStandardsInstitute)标准,不能对void指针进行算法操作,即下列操作都是不合法的:
void * pvoid;
pvoid ++;//ANSI:错误
pvoid += 1;//ANSI:错误
ANSI标准之所以这样认定,是因为它坚持:进行算法操作的指针必须是确定知道其指向数据类型大小的。例如:
int * pint;
pint ++;//ANSI:正确
pint++的结果是使其增大sizeof(int)。
但是大名鼎鼎的GNU(GNU’sNotUnix的缩写)则不这么认定,它指定void * 的算法操作与char * 一致。
因此下列语句在GNU编译器中皆正确:
pvoid ++;//GNU:正确
pvoid += 1;//GNU:正确
pvoid++的执行结果是其增大了1。
在实际的程序设计中,为迎合ANSI标准,并提高程序的可移植性,我们可以这样编写实现同样功能的代码:
void * pvoid;
(char*)pvoid ++;//ANSI:正确;GNU:正确
(char*)pvoid += 1;//ANSI:正确;GNU:正确
GNU和ANSI还有一些区别,总体而言,GNU较ANSI更“开放”,提供了对更多语法的支持。但是我们在真实设计时,还是应该尽可能地迎合ANSI标准。
在windows平台下,va_list是char *的别名,通过typedef声明而来,而在GNU上,
这个__ptr_t是va_list通过层层define之后最后的原型,可以看到,GNU中,va_list确实是定义为void *类型,但是上面的分析也可以看出,GNU中void *指针默认操作是char *.但是我们自己实现的printf函数中,还是采用char *这样的通用操作。
typedef char * va_list;
现在,应该说明va_start这个宏了,它的定义为:
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
这个_INTSIZEOF宏的作用就有讲究了,它要实现的功能是保证偏移是int类型大小的整数倍,比如你的类型所占字节是1或者2,或者4,通过这个宏之后,最后的结果都是4,如果你的类型为8,最后得出的也为8,读者可以自己计算验证.在我们的系统中,int为4个字节。为什么要这样?因为我们内存会有个对齐机制,关于这个机制我在以前的随笔中专门分析过。这个内存对齐机制,关系到指针的偏移情况,所以要确保偏移是编译器默认对齐字节的整数倍。一般情况,32位编译器,默认4字节对齐,64位编译器,有的为了兼容32位,采取4字节对齐,有的为了更高效,采取8字节对齐,这些默认对齐方式,是可以通过程序更改的。
为了保证内存的4字节对齐,GNU那帮大牛们实现了_INTSIZEOF宏,在我的arm-linux-gcc编译器上,默认是4字节对齐的。补充说明,直接写1.2345这样的小数,默认是double类型,而不是float类型,这个几乎在现代编译器上都是这样规定的。至于如何想到的这个偏移求解方式,就是基本功的累积和数学的累积了,我经常说自己怎么总是写if ,else这样的代码,别人也只是用了&和取反就实现了一个算法,既然已经有巨人存在了,我们就好好站在他们的肩膀上学习,争取以后自己慢慢也能成为这样的巨人。
回到可变参数,通过固定参数,然后指针偏移,然后取值。这样的步骤就可以得到可变参数了。
可以看出,va_arg这个宏,其实要执行两个操作,一个是取值,一个是移动指针,那么一个宏定义如何实现执行两步操作呢?答案是:逗号表达式。
va_end宏就比较简单了。
贴出编译器对这三个宏的定义。
vc6.0中的stdarg.h typedef char * va_list; //当sizeof(n)=1/2/4时,_INTSIZEOF(n)等于4 #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 )
主要说明va_arg,其实这个宏是通过逗号表达式化解而来的。我们知道要进行取值和移动指针操作,而且是先取值,再移动指针,那么逗号表达式就派上用场了。
#define va_arg(ap,t) (ap = ap + _INTSIZEOF(t), *(t *)(ap - _INTSIZEOF(t)))
这个逗号表达式,优先级低于赋值符 =,那么要执行取值,移动指针,首先ap保存了偏移ap + _INTSIZEOF(t),那么很显然,最后要实现取值,所以要减去偏移,然后解引用。把这个式子化解一下,就是下面的表达式。关于逗号表达式,我在之前的随笔中有讲过。(点击查看逗号表达式)
#define va_arg(ap,t) (*(t *)(ap = ap + _INTSIZEOF(t), ap - _INTSIZEOF(t)))
由于逗号表达式右边的才是最终结果,上式化解顺利成章,再化解,ap先偏移并保存,然后减去偏移不保存解引用,就成为了库函数头文件定义了:
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
源码(putchar是之前串口程序已经实现了的):
#include "my_printf.h" //================================================================================================== 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_arg(ap,t) ( *(t *)( ap=ap + _INTSIZEOF(t), ap- _INTSIZEOF(t)) ) #define va_end(ap) ( ap = (va_list)0 ) //================================================================================================== unsigned char hex_tab[]={\'0\',\'1\',\'2\',\'3\',\'4\',\'5\',\'6\',\'7\',\\ \'8\',\'9\',\'a\',\'b\',\'c\',\'d\',\'e\',\'f\'}; static int outc(int c) { __out_putchar(c); return 0; } static int outs (const char *s) { while (*s != \'\\0\') __out_putchar(*s++); return 0; } 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); } /*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); continue; } lead=\' \'; maxwidth=0; //format : %08d, %8d,%d,%u,%x,%f,%c,%s fmt++; if(*fmt == \'0\'){ lead = \'0\'; fmt++; } 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; } //reference : int printf(const char *format, ...); int printf(const char *fmt, ...) { va_list ap; va_start(ap, fmt); my_vprintf(fmt, ap); va_end(ap); return 0; } int my_printf_test(void) { printf("This is www.100ask.org my_printf test\\n\\r") ; printf("test char =%c,%c\\n\\r", \'A\',\'a\') ; printf("test decimal number =%d\\n\\r", 123456) ; printf("test decimal number =%d\\n\\r", -123456) ; printf("test hex number =0x%x\\n\\r", 0x55aa55aa) ; printf("test string =%s\\n\\r", "www.100ask.org") ; printf("num=%08d\\n\\r", 12345); printf("num=%8d\\n\\r", 12345); printf("num=0x%08x\\n\\r", 0x12345); printf("num=0x%8x\\n\\r", 0x12345); printf("num=0x%02x\\n\\r", 0x1); printf("num=0x%2x\\n\\r", 0x1); printf("num=%05d\\n\\r", 0x1); printf("num=%5d\\n\\r", 0x1); return 0; }
最后说两句:
Nor flash程序中,大部分是查看nor的芯片手册配置,不再单独列出做记录,SPI和IIC在单片机中已经使用过很多次,采用硬件方式,后面应该会出一个软件模拟IIC的,毕竟还没有用过软件IIC,应该尝试一下。还有LCD,在之后上文件系统,跑QT再单独拿出来解析。那么到目前为止,还有nand flash,其实这个也主要是了解这个flash特性,和nor一样,主要看芯片手册,了解其特性即可。那么,从下面开始,就应该正式进入uboot的学习了,也该往更高的方向前进了,裸机先告一段落。
在nor flash程序中,有一点需要注意:
1. 编译程序时加上: -march=armv4
否则
volatile unsigned short *p = xxx;
*p = val; // 会被拆分成2个strb操作
这样会让nor flash读取的数据和我们想要得到的不同。是在是佩服韦老师,我觉得这样的bug,要是在大型程序中真是很难调试的,汇编出来的代码不是我们理想的方式,但是,我们只要知道了有这个坑,之后加上这个编译选项,就好了。同时也提醒我们,分析反汇编对调试也是很有作用的,同时要学会用英文在必应和谷歌搜索答案。
以上是关于s3c2440——实现裸机的简易printf函数的主要内容,如果未能解决你的问题,请参考以下文章