历史上的今天

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

正在发生

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

发布者:数字冲浪 来源: eefocus关键字: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 != '')    

        __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);

[1] [2]
关键字:s3c2440  裸机  printf函数 引用地址:s3c2440——实现裸机的简易printf函数

上一篇:ARM汇编:汇编中IA、IB、DA、DB和FD、ED、FA、EA什么意思?
下一篇:FL2440(3) 裸板程序

推荐阅读

美商安森美半导体(ON Semiconductor)与富士通半导体株式会社(Aizu Fujitsu Semiconductor Manufacturing Limited)于10月1日共同宣布,安森美半导体于当日完成对富士通半导体制造株式会社位于会津若松(Aizu-Wakamatsu)的富士通8英寸晶圆厂的递增20%股权之收购,使得安森美半导体持有该合资公司的60%大多数股权,并于10月1日股市收盘后进行品牌转...
国际货币基金(IMF)新任主席乔吉娃(Kristalina Georgieva)最新表示,全球多数经济体表现都有放缓迹象,预期美中贸易战将导致2020年全球经济承受7000亿美元的损失。该数字比6月时IMF预期的4500亿美元损失还要高出许多,显示IMF对于贸易战影响全球的看法越趋悲观。乔吉娃指出,IMF将会在本月中旬出炉的最新《世界经济展望》调降2019及2020年成长预测。此...
新兴公司NISI Limited目前正在开发一种微型手术机器人,该机器人可以通过人体的自然开口插入,并且只能在腹部内部展开。为了实现这一目标,工程师正在将组件推向极限并超越极限。在医疗技术世界中,最近有许多惊人的新发展。外科手术机器人的世界可能很快就会改变:2018年夏天,总部位于香港的初创公司NISI宣布已成功对活猪进行了一系列妇科手术。乍一看...
9 月 20 日,Synopsys 宣布扩展其 DesignWare® ARC® 处理器 IP 产品组合,推出针对低功耗嵌入式 SoC 的新型 128 位 ARC VPX2 和 256 位 ARC VPX3 DSP 处理器。 2019 年,该公司推出了用于高性能信号处理 SoC 的 512 位 ARC VPX5 DSP 处理器。Synopsys 处理器解决方案高级营销总监 Matt Gutierrez、ARC VPX DSP 处...

史海拾趣

问答坊 | AI 解惑

printf 和 RETAILMSG 打印出来的消息不一致?

wince6.0 下win32 api 写的代码。 char inFilename[128]; memset(inFilename, 0x00, sizeof(inFilename)); wcscpy(inFilename,_T("\\\\Temp\\\\test_420_1599_1198.jpg")); DNW串口打印消息: RETAILMSG(1,(TEXT("[RETAILMSG] inFilenam ...…

查看全部问答∨

vmware下linux连接开发板的问题

我用的是立宇泰的s3c2440开发板,我将开发板连入到局域网中,(板上只有一个uboot和一个不能启动的linux内核),用主机Ping开发板,发现无法Ping通。 ip设置如下: 主机:192.168.0.112 虚拟机里的linux:192.168.0.2 开发板:192.168.0.9 将开 ...…

查看全部问答∨

请教vxworks 实现rtc时钟设置

Int  clock_gettime(clockid_t clock_id,struct timespec *tp);/*取时钟时间*/ Int  clock_settime(clockid_t clock_id,const struct timespec *tp);/*设置时钟时间*/ 和时间有关的函数通过timespec数据结构作为入参 ...…

查看全部问答∨

485的通讯方式和组网方式之间的关系

   想请问一下,不同的组网方式对485的通讯方式有影响吗?希望各位高手指点一下,谢谢!…

查看全部问答∨

s3c2410 ENIT0中断后不能返回

s3c2410  按键EINT0 时IRQ 中断 打印以个X _start:         ldr        pc, vect_reset         ldr pc, vect_undef         ldr pc, vect_swi ...…

查看全部问答∨

请问磁珠的型号怎么读?

磁珠的型号是BLM21P221SN,买的磁珠上写着220R,请问是什么意思?…

查看全部问答∨

怎么样把写的程序下载到开发板上??

我用evc++4.0写的程序,想下载到YC2440的开发板上运行看一下效果,可是在YC2440SDK下一运行就出错,说很多头文件找不到,我已经装了YC2440的SDK包了啊,为什么还是总报错??是什么地方出问题了吗??…

查看全部问答∨

Avr超权威资料集锦

avr资料集锦,有些附件东西太大了,尴尬了哈哈哈,等我权限高的吧,再把那些给大家补充上,下载不留言可是很没礼貌偶哈哈哈   最近发现大家总喜欢花钱买一两个资料,我送这么多没人要,我真是心寒啊 [ 本帖最后由 鑫海宝贝 于 2011-4-20 ...…

查看全部问答∨

6517A型静电计/高阻表的光电效应

在比某种材料波长较短的波长范围里,光能足以激发电子从电极材料发射。这将导致电极之间气体的离子化,提供导电路径。如果在高真空中进行测量,这些电子将产生额外电流。为了避免这些效应,选择的照明波长应当大于光电效应阈值波长。对于镓来说, ...…

查看全部问答∨

求助

TA里的两个中断事件 1. 主计数值计满(或计至TACCR0)后复位,TAIFG标志被置1.中断发生在计数值从TACCR0跳至0的时刻。 2. 捕获通道0发生捕获事件,或主计数值TAR计至TACCR0(计数值从TACCR0-1跳至TACCR0的时刻),TACCTL0寄存器内的CCIFG标志被置1 ...…

查看全部问答∨
小广播
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

电子工程世界版权所有 京ICP证060456号 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved