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函数的主要内容,如果未能解决你的问题,请参考以下文章

s3c2440裸机-LCD编程(实现显示功能)

s3c2440裸机-内存控制器(SDRAM编程实现)

s3c2440裸机-代码重定位(2.编程实现代码重定位)

s3c2440裸机-异常中断(二. und未定义指令异常)

s3c2440 裸机开发 通用异步收发器UARN

s3c2440裸机-异常中断(四. irq之外部中断)