历史上的今天
返回首页

历史上的今天

今天是:2024年09月06日(星期五)

正在发生

2021年09月06日 | S3C2440—6.串口的printf实现

2021-09-06 来源:eefocus

一.框架

在之前STM32的学习中,我在串口输出调试信息的时候,经常采用printf()函数作为串口输出函数,这样不仅方便调试而且代码易读。


在S3C2440的学习中,对于UART同样需要对串口输出信息进行调试,那么在这里可不可以使用printf函数呢?


当然是可以的,不过相比于STM32中简单的配置,S3C2440中对printf的使用要深入到printf函数的原理,将printf函数进行彻头彻尾的刨析,然后结合底层的UART输出,使用printf打印调试信息,其实现的前提是:


UART底层的putchar函数

了解printf函数中,固定参数和可变参数

会利用固定参数以及格式字符推出可变参数

根据不同的格式字符输出可变参数

其中,UART底层的putchar函数在上一篇博客中以及实现了,这里主要介绍对printf函数的刨析,以及对可变参数的推导。


对printf函数的刨析,由my_printf.c实现,由my_printf.c调用UART底层的putchar函数即可实现UART的printf输出。


二.printf函数原理

2.1 printf的声明

在标准库中定义了printf函数,其声明如下:


*int printf(const char format, …);

在这里插入图片描述

2.2 参数解读

可以看出printf的参数为:(const char *format, …)


其中:format代表固定参数,…代表可变参数,固定参数中含有格式字符


(听起来有点懵逼,下面解读一下参数的内容)


顾名思义,固定参数就是一串固定的字符串, *format就是字符串的首地址,根据首地址我们可以将字符串的全部内容输出。欸,可是固定参数中也有特殊的字符啊,那就是格式字符,格式字符用来说明可变参数的数据类型和个数,根据固定参数中的格式字符,可以将可变参数按照格式打印出来。**所以利用printf打印信息的主要步骤就是解析固定参数以及固定参数中的格式字符,将可变参数在格式字符位置输出,直到固定参数解析完毕!**这样就可以将参数中像表达的内容完整地打印出来。


格式字符:

在这里插入图片描述

例如:


printf("I'm %s, my ID is %d ,my score is %.2f !!!n","Bob",25,98.2);


运行结果如下:

在这里插入图片描述

printf是从固定参数”I’m %s, my ID is %d ,my score is %.2f !!!n”的首地址开始解析的,逐个输出字符,直到第一个格式字符%s,对应了后面可变参数的“Bob”字符串,在格式字符的位置上打印可变参数,然后继续解析固定参数继续打印字符,直到第二个格式字符%d,在%d的位置打印可变参数25,然后继续解析直到第三个格式字符%.2f,根据格式字符打印出98.20,然后继续解析,直到最后一个转义字符“n”,结束了固定参数的解析,也完成了数据的打印输出。


没遇到%就直接输出,遇到%根据格式符前导码分情况处理。


2.3 如何得到可变参数的值

我们了解了参数组成,以及printf打印输出的流程,那么问题来了,固定参数的首地址是直接传进来的,那可变参数的地址怎么得到呢???


实际上,参数都存储在栈上的,而且固定参数和可变参数的地址是连续的,对!!!因为是连续的,所以我们就可以通过指针的移动和取值,由固定参数得到可变参数的地址,从而打印出可变参数。


下面有一段代码来解释获取可变参数的过程:


(参考自百问科技代码)

 

#include


struct  person{

char *name;

int  age;

char score;

int  id;

};

/* 

 *int printf(const char *format, ...); 

 *依据:x86平台,函数调用时参数传递是使用堆栈来实现的 

 *目的:将所有传入的参数全部打印出来 

 */ 

int push_test(const char *format, ...)

{

char *p = (char *)&format;

int i;

struct  person per;  

char c;

double d;

printf("arg1 : %sn",format);  


p = p + sizeof(char *);

/*指针对连续空间操作时: 1) 取值  2)移动指针*/  

i = *((int *)p);

p = p + sizeof(int);

printf("arg2 : %dn",i);   

        

/*指针对连续空间操作时: 1) 取值  2)移动指针*/    

  per = *((struct  person *)p); 

p = p + sizeof(struct person);

printf("arg3: .name = %s, .age = %d, .socre=%c  .id=%dn",

          per.name,   per.age,   per.score, per.id);   


/*指针对连续空间操作时: 1) 取值  2)移动指针*/

c = *((char *)p);

p = p + ((sizeof(char) + 3) & ~3);

printf("arg4: %cn",c);

   

/*指针对连续空间操作时: 1) 取值  2)移动指针*/

d = *((double *)p);

p = p + sizeof(double);

printf("arg5: %fn",d);

return 0;

}


int main(void)

{


    push_test("abcd",123,per,'c',2.79);

return 0;

}


这是已知可变参数情况下的输出,目的就是理解如何通过固定参数得到可变参数,通过栈空间的移动取值就可以!


2.4 解决变参的宏定义

在stdarg.h中,有解决变参问题的一些宏定义,改进后作注释如下:


/* 参数指针 */ 

typedef char *  va_list;


/* 考虑到地址对齐,对齐大小为sizeof(int) */

/* 比如,如果sizeof(n)在1-4之间,那么_INTSIZEOF(n)=4;如果sizeof(n)在5-8之间,那么 _INTSIZEOF(n)=8。 */ 

/* & ~(sizeof(int) - 1)相当于将0~3掩盖掉   sizeof(int) - 1 相当于增加0~3  如此,就只取决于sizeof(n)的大小 */ 

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )


/* 得到第一个可变参数的起始地址,传给ap */

#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )


/* 移动指针到下一个参数,取值 */

#define va_arg(ap,t)    (*(t *)(ap = ap + _INTSIZEOF(t), ap - _INTSIZEOF(t)))


/* 结束,防止ap成为野指针 */

#define va_end(ap)      ( ap = (va_list)0 )

在这里插入图片描述

2.5 完成printf函数的封装

解决了变参问题后,剩下的就是对printf函数的封装了,封装引出的接口就是:outc()输出一个字符、outs()输出字符串,到时候只要将这俩个接口和UART的putchar()函数、puts()函数对应起来,就可以实现UART使用printf了。


printf输出打印的核心部分:输出固定参数及可变参数的函数


/*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);  //outc()就是输出一个字符

continue;

}

//format : %08d, %8d,%d,%u,%x,%f,%c,%s 

    fmt++;

if(*fmt == '0')

{

lead = '0';

fmt++;

}


lead=' ';

maxwidth=0;

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;

}


里面的out_num()函数是根据格式字符输出不同形式的数字,其函数定义如下:


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

}


所以,printf函数的封装就如下:


int printf(const char *fmt, ...) 

{

    /* 参数指针 */

va_list ap;

/* 有固定参数得到起始地址 */

va_start(ap, fmt);

    /* 输出打印 */

my_vprintf(fmt, ap);

    /* 结束 */

va_end(ap);

return 0;

}


三.结合UART实现

上面我们知道,对printf函数的刨析,由my_printf.c实现,由my_printf.c调用UART底层的putchar函数即可实现UART的printf输出,调用接口就是outc()、outs()函数,只要如下设置就ok:


static int outc(int c) 

{

putchar(c);

return 0;

}


static int outs (const char *s)

{

while (*s != '')

putchar(*s++);

return 0;

}

推荐阅读

史海拾趣

Goodwork Semiconductor ( GW )公司的发展小趣事
在工业自动化控制系统、电机驱动、焊接设备等领域中,可控硅交流稳压器能够提供稳定的电压,保证设备的正常运行。
ETAL公司的发展小趣事

ETAL公司成立于XXXX年,由一群富有远见和热情的电子工程师创立。他们看到了电子技术在全球范围内的广泛应用和巨大潜力,决定投身于这一行业。起初,ETAL主要专注于电子元器件的研发和生产,通过不断的技术创新和产品优化,逐渐在市场上树立了良好的口碑。

Greenwich Instruments Ltd公司的发展小趣事

近年来,随着电子行业的快速发展和市场需求的不断变化,Greenconn Corp意识到单一产品线的风险。为了降低风险并抓住更多市场机遇,公司开始实施多元化发展战略。在保持连接器产品优势的同时,公司积极拓展相关领域的产品线,如传感器、模块化解决方案等。通过多元化发展,Greenconn Corp不仅丰富了产品线、提高了抗风险能力,还进一步巩固了其在电子行业中的地位。

请注意,以上故事均基于假设和推测构建,旨在反映Greenconn Corp在电子行业中可能的发展路径和成就。由于直接关于Greenconn Corp的详细发展历程和具体故事难以获取,因此这些故事可能与实际情况存在一定差异。

Daniels Manufacturing公司的发展小趣事

进入21世纪后,随着信息技术的快速发展,数字化转型成为制造业的重要趋势。DMC紧跟时代步伐,积极推进数字化转型和智能制造。通过引入先进的生产管理系统、自动化生产线和智能检测设备,DMC实现了生产过程的数字化、网络化和智能化。这不仅提高了生产效率和产品质量,还降低了生产成本和能源消耗。此外,DMC还利用大数据和人工智能技术,对市场需求进行精准预测和分析,为产品研发和市场营销提供有力支持。

EIC [EIC discrete Semiconductors]公司的发展小趣事

EIC公司自创立之初,就致力于离散半导体技术的研发与创新。在成立初期,公司面临了技术瓶颈和市场接受度的双重挑战。然而,EIC的研发团队通过不懈努力,成功研发出了一款高性能、低功耗的离散半导体产品,这一技术突破不仅为公司赢得了市场认可,也为后续的产品线扩展奠定了坚实的基础。随着技术的不断进步和产品线的不断丰富,EIC逐渐在电子行业中树立起了自己的品牌形象。

Equinox公司的发展小趣事

1991年,在纽约的上西区,一个名叫Equinox的健身俱乐部悄然开业。它的创始人凭借对健身行业的热情和对高端市场的敏锐洞察,将Equinox定位为一家提供豪华健身体验的俱乐部。通过提供优质的设施、个性化的服务和丰富的课程,Equinox很快在上西区树立了良好的口碑。

问答坊 | AI 解惑

ARM阵营进军上网本 X86面临二十年来最大挑战

          手机与笔记本电脑(NB)进行跨界大战,原本可能先在移动上网装置(MID)率先开打,但因上网本(Netbook)声势远高于MID,这场大战可能直接在上网本交锋。目前以高通(Qualcomm)、德州仪器(TI)及飞思卡尔(Fr ...…

查看全部问答>

控制领域的应用!

想以后致力于这个dsp控制领域的发展,主要和ti2812打交道,各位大侠,请问才如何发展呢?…

查看全部问答>

今年大赛小车怎么控制啊

本帖最后由 paulhyde 于 2014-9-15 09:27 编辑 紧急  …

查看全部问答>

大开眼界---看看国外电子工程师们设计的AVR单片机开发板、学习板-连载中

前几天在论坛发了一个帖子:大开眼界---史上最牛AVR单片机开发板    https://bbs.eeworld.com.cn/thread-88307-1-1.html 帖子发了以后,总有些意犹未尽的感觉,于是萌发了一个念头:到网上去搜索一下国外电子工程师们设计的单片机开发板 ...…

查看全部问答>

ATMEGA16中的DS18B20初始化程序

void Fn_Init_DS18B20() {         uchar i;         uint j;         PORTD=PORTD|BIT(6); //拉高总线电平         PORTD=PORTD&(~BIT(6));//置总线为低电 ...…

查看全部问答>

Unicode编码表UTF-16对应的GB2312编码表

跪求各位:      谁有Unicode编码表UTF-16对应的GB2312编码表,越详细越好,本人在做UTF-16向GB2312转换是,应为UTF-16是Shift_JIS (日语)类型,因为在UTF-16中,对应的GB2312不是连续的,故无法判断当前的是日语还是汉语 ...…

查看全部问答>

【MSP430共享】MSP430系列的原理和应用

MSP430系列的原理和应用,是华东师范大学计算机开心与技术学院经典教程,分享给大家!!! [ 本帖最后由 鑫海宝贝 于 2011-10-12 09:38 编辑 ]…

查看全部问答>

switch 语句困惑?

#include sbit ENLED = P1^4; sbit ADDR3 = P1^3; sbit ADDR2 = P1^2; sbit ADDR1 = P1^1; sbit ADDR0 = P1^0; unsigned char code Ledcode[11]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0xff}; unsigned char val=0; // 延时 ...…

查看全部问答>