历史上的今天
返回首页

历史上的今天

今天是:2024年10月09日(星期三)

正在发生

2019年10月09日 | s3c2440——实现裸机的简易printf函数

2019-10-09 来源:eefocus

在单片机开发中,我们借助于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 != '')    

        __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 = '';

    

    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 != ''; 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 testnr") ;    

    printf("test char           =%c,%cnr", 'A','a') ;    

    printf("test decimal number =%dnr",    123456) ;

    printf("test decimal number =%dnr",    -123456) ;    

    printf("test hex     number =0x%xnr",  0x55aa55aa) ;    

    printf("test string         =%snr",    "www.100ask.org") ;    

    printf("num=%08dnr",   12345);

    printf("num=%8dnr",    12345);

    printf("num=0x%08xnr", 0x12345);

推荐阅读

史海拾趣

东晨(DC)公司的发展小趣事

东晨(DC)公司深知市场需求的重要性,因此制定了精准的市场策略。公司通过深入研究消费者需求,不断推出符合市场趋势的新产品。同时,东晨(DC)公司还注重与渠道商的合作,通过建立稳定的销售渠道,将产品快速推向市场。此外,公司还积极参与各种展会和论坛,展示最新技术和产品,赢得了客户和业界的广泛赞誉。

德旭电子(DEXU)公司的发展小趣事

在德旭电子的发展历程中,供应链管理和成本控制一直是公司关注的重点。为了降低生产成本和提高产品质量,公司不断优化供应链管理流程和技术手段。

德旭电子与多家优质供应商建立了长期稳定的合作关系,并通过引入先进的供应链管理软件和系统实现了供应链的数字化和信息化管理。这些措施有效提高了供应链的响应速度和灵活性,降低了库存和运营成本。

在成本控制方面,德旭电子注重精细化管理和持续改进。公司通过优化生产流程、提高生产效率、降低能耗和原材料成本等措施不断降低生产成本。同时,公司还建立了完善的成本管理体系和考核机制确保成本控制工作的有效实施。

ETAL公司的发展小趣事

作为一家有社会责任感的企业,ETAL始终关注社会公益事业。公司积极参与各种慈善活动和社会捐赠项目,为贫困地区的教育事业、环保事业等提供了力所能及的帮助。此外,ETAL还定期组织员工参与志愿服务活动,通过实际行动传递正能量和爱心。这些公益事业不仅提升了公司的社会形象,也增强了员工的凝聚力和向心力。

EZchip Technologies Ltd公司的发展小趣事

作为一家领先的电子企业,EZchip深知自己在推动社会进步和可持续发展方面所肩负的责任。因此,公司一直致力于环境保护、社会公益和可持续发展等方面的工作。EZchip通过采用环保材料和节能技术降低生产过程中的能耗和排放;积极参与社会公益活动回馈社会;同时注重企业的可持续发展规划和管理模式的创新。这些举措不仅体现了公司的社会责任感和担当精神也为企业赢得了良好的社会声誉和品牌形象。

Chemi-Con公司的发展小趣事

在电子行业的发展过程中,Chemi-Con公司意识到单一产品线的局限性,并开始积极探索多元化发展的道路。公司不仅拓展了电容器产品的应用领域,还涉足了光电子机械等新兴产业。此外,Chemi-Con还积极寻求与其他企业的合作,通过技术共享、资源共享等方式实现共赢。这种开放、合作的姿态使得Chemi-Con在行业中树立了良好的形象,也为公司的长远发展奠定了坚实的基础。

Automatic Connector公司的发展小趣事

面对日益严重的环境问题,Automatic Connector公司积极响应绿色制造的号召。公司投入大量资金研发环保型电子连接器,采用环保材料和节能工艺,减少生产过程中的污染排放。同时,公司还倡导循环经济的理念,推动废弃电子产品的回收和再利用。这一举措不仅提升了公司的社会形象,也为公司的可持续发展奠定了坚实的基础。

问答坊 | AI 解惑

请教一下这个电路的作用。

这个电路中的每个元器件的作用是什么呢?另外这个电路的编码格式NEC表示什么意思?系统码是不是40BF?…

查看全部问答>

找DSP相位测量数字信号处理人才

找DSP相位测量数字信号处理人才,,有的给个电话. cui9711@163.com…

查看全部问答>

TI芯片选型指南

相信很多工程师都在用TI的产品 本人在此分享一下官网公布的选型指南 希望对面临选型困惑的工程师有帮助 http://focus.ti.com.cn/cn/analog/docs/selectionguides.tsp…

查看全部问答>

各位大虾,请教一个作业哈

有哪位大侠会用protel软件设计一个“arm”的最小系统,需要电路图的,谢谢哈,如果有会的话希望能留个QQ哈,谢谢了!…

查看全部问答>

SDRAM做缓存

请问,高速AD出来的数据直接存进SDRAM中的可行性如何?比如AD的采样率是100M,用S3C2440来控制。…

查看全部问答>

跪求USB无线网卡驱动源码

各位英雄:      小弟在PXA270+WINCE5.0平台下做USB无线网卡,目前选择的网卡芯片是VT6656。可是从网上下载的都是MSI格式的驱动,没有源码!不知道哪位英雄可以提供WINCE平台下的USB无线网卡驱动源码啊?      ...…

查看全部问答>

我怒了,OD只打开一次就不行了!?

注:系统是XP SP2 初学驱动编程,从网上下了个OllyDbg汉化版(1.10 ).安装后运行,程序启动并显示主界面,但提示说我使用客户帐号,OD部分功能将无法使用.(为了安全我一般不用管理帐户登录系统) 我于是切换到管理帐户,但OD却启动不了,双击或鼠标右键-打 ...…

查看全部问答>

eeworld给的分,慰劳慰劳弟兄们!(8)

以后咱好好干活... 我们得好好加油哈!!! 为了中国软件的明天还有,eeworld的未来!!! 呵呵 …

查看全部问答>

纽扣电池请教

关于纽扣电池,使用万用表测试其两端电阻,发现只有几欧姆的样子,这个是正常的吗?此外,想请教一下,如果我将纽扣电池串接在5V的电压源上放电,当串联电压降低后可以认为其电流都出自纽扣电池内吗?…

查看全部问答>

CC2530的双串口在协议栈中怎么配置?

CC2530的双串口在协议栈中怎么配置?在裸机上可以跑得通两个串口,可在协议栈中只有一个UART0跑得通…

查看全部问答>