在单片机开发中,我们借助于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[]={'','','','','','','','',\
'','','a','b','c','d','e','f'}; static int outc(int c)
{
__out_putchar(c);
return ;
} static int outs (const char *s)
{
while (*s != '\0')
__out_putchar(*s++);
return ;
} static int out_num(long n, int base,char lead,int maxwidth)
{
unsigned long m=;
char buf[MAX_NUMBER_BYTES], *s = buf + sizeof(buf);
int count=,i=; *--s = '\0'; if (n < ){
m = -n;
}
else{
m = n;
} do{
*--s = hex_tab[m%base];
count++;
}while ((m /= base) != ); if( maxwidth && count < maxwidth){
for (i=maxwidth - count; i; i--)
*--s = lead;
} if (n < )
*--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=; for(; *fmt != '\0'; fmt++)
{
if (*fmt != '%') {
outc(*fmt);
continue;
}
lead=' ';
maxwidth=; //format : %08d, %8d,%d,%u,%x,%f,%c,%s
fmt++;
if(*fmt == ''){
lead = '';
fmt++;
} while(*fmt >= '' && *fmt <= ''){
maxwidth *=;
maxwidth += (*fmt - '');
fmt++;
} switch (*fmt) {
case 'd': out_num(va_arg(ap, int), ,lead,maxwidth); break;
case 'o': out_num(va_arg(ap, unsigned int), ,lead,maxwidth); break;
case 'u': out_num(va_arg(ap, unsigned int), ,lead,maxwidth); break;
case 'x': out_num(va_arg(ap, unsigned int), ,lead,maxwidth); break;
case 'c': outc(va_arg(ap, int )); break;
case 's': outs(va_arg(ap, char *)); break; default:
outc(*fmt);
break;
}
}
return ;
} //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 ;
} 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", ) ;
printf("test decimal number =%d\n\r", -) ;
printf("test hex number =0x%x\n\r", 0x55aa55aa) ;
printf("test string =%s\n\r", "www.100ask.org") ;
printf("num=%08d\n\r", );
printf("num=%8d\n\r", );
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 ;
}

最后说两句:

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函数的更多相关文章

  1. 一个简化的printf函数

    <C和指针>第7章第5道编程题: 实现一个简化的printf函数,它能够处理%d.%f.%s 和 %c 格式码,根据ANSI标准的原则,其他格式码的行为是未定义的.你可以假定已经存在函数 ...

  2. 可变参数列表与printf()函数的实现

    问题 当我们刚开始学习C语言的时候,就接触到printf()函数,可是当时"道行"不深或许不够细心留意,又或者我们理所当然地认为库函数规定这样就是这样,没有发现这个函数与普通的函数 ...

  3. printf函数

    printf函数的格式及含义 d                    以十进制带符号的形式输出整数(对正数不输出符号) o                    以八进制无符号的形式输出整数(不输出 ...

  4. Linux Linux下特殊的printf函数和fputs函数

    Linux下,printf函数必须以'\n'结尾才会立刻输出到屏幕,如果没有'\n'直到输出缓冲区满了以后才会打印到屏幕上(敲击换行也算),如果需要不换行的输出,一般可以使用write函数代替.'\n ...

  5. 关于printf函数的所思所想

    缘起大一下学期,C语言程序设计徐小青老师的随口一提,经娄嘉鹏老师提醒,我觉得应该自己整理清楚这一问题.涉及网上资料将会标明出处. 关于printf函数的所思所想 * printf的定义 printf( ...

  6. C语言printf()函数:格式化输出函数

    C语言printf()函数:格式化输出函数 头文件:#include <stdio.h> printf()函数是最常用的格式化输出函数,其原型为:     int printf( char ...

  7. 关于printf函数输出先后顺序的讲解!!

    对于printf函数printf("%d%d\n",a,b);函数的实际输出顺序是这样的先计算出b,然后在计算a,接着输出a,最后在输出b:例子如下:#include<ios ...

  8. printf()函数

    printf()函数是格式化输出函数, 一般用于向标准输出设备按规定格式输出信息. printf()函数的调用格式为: printf("<格式化字符串>", <参 ...

  9. C 中 关于printf 函数中度剖析

    题外话  这篇博文主要围绕printf函数分析的,主要讲解printf 使用C的可变参数机制, printf是否可重入(是否线程安全), printf函数的源码实现. 正文 1.C中可变参数机制 我们 ...

随机推荐

  1. 【C语言】练习2-9

     题目来源:<The C programming language>中的习题P38  练习2-9:  在求对二的补码时,表达式x &= (x-1)可以删除x中最右边值为1的一个二进 ...

  2. 31天重构学习笔记(java版本)

    准备下周分享会的内容,无意间看到.net版本的重构31天,花了两个小时看了下,可以看成是Martin Fowler<重构>的精简版 原文地址:http://www.lostechies.c ...

  3. 跟我学SharePoint 2013视频培训课程——理解SharePoint网站的体系结构(3)

    课程简介 第三天,理解SharePoint 2013 网站的体系结构 视频 SharePoint 2013 交流群 41032413

  4. MarkDown 使用指南

    https://frankbing.gitbooks.io/markdown/content/

  5. memcached(一):linux下memcached安装以及启动

    一. 安装文件 Linux系统安装memcached,首先要先安装libevent库. 下载memcached与libevent的安装文件 http://memcached.org/files/mem ...

  6. 设计模式-单例模式(Singleton Pattren)(饿汉模式和懒汉模式)

    单例模式(Singleton Pattren):确保一个类在整个应用中只有一个实例,并提供一个全局访问点. 实现要点: 1. 私有化构造方法 2. 类的实例在类初始化的时候创建 3. 提供一个类方法, ...

  7. 使用JS生成二维码QRCode

    这其实很简单,项目中使用插件即可生成,主要有两种方式: 一种是在项目中使用java生成,把图片生成到某个目录,然后在用tomcat或者nginx虚拟一下路径即可访问,这种方式我们不用,因为会在目录中生 ...

  8. 浏览器中beforeunload的使用

    打开一些慢的网站的时候只见浏览器在不停转圈,但是页面还停留在当前页面的,有些网站的效果是你点击链接要跳到另一个页面的时候,在当前页面弹出一个框提示“正在加载中....”, 用到了浏览器的beforeu ...

  9. Xilinx FPGA用户约束文件(转自xilinx ISE 开发指南

    FPGA设计中的约束文件有3类:用户设计文件(.UCF文件).网表约束文件(.NCF文件)以及物理约束文件(.PCF文件),可以完成时序约束.管 脚约束以及区域约束.3类约束文件的关系为:用户在设计输 ...

  10. 【Android】1.1 开发环境安装和配置

    分类:C#.Android.VS2015: 创建日期:2016-01-20 2016-08-03说明:此版本已过时,最新版本见本博客置顶的内容. 一.安装JDK.SDK.NDK 无论是用C#和VS20 ...