历史上的今天
今天是:2024年09月10日(星期二)
2019年09月10日 | 基于STM32从零写操作系统系列---将printf指向串口输出
2019-09-10 来源:eefocus
为什么需要printf?
首先,这个printf不是标准C中的printf,这个printf是自己参考标准库实现的。只是简单地完成了打印输出int,long long int, unsigned int, unsigned long long int, float, double和十六进制数等功能。主要用于在以后的学习中,输出变量、寄存器等的数据,便于调试程序。
1.函数调用中的参数传递
根据《Procedure Call Standard for the ARM ® Architecture》(文章结尾有下载分享)这个文档可知,标准规定在寄存器(r0-r3)和堆栈中传递参数。对于采用少量参数的子程序,仅使用寄存器,大大减少了调用的开销。还有就是,char,short,int这些类型的数据入栈时,会占用4字节的空间;long long int,double等的8字节数据入栈时,只会放置到8字节对齐的地址上。下面通过反汇编查看参数传递的过程:
C语言调用过程:

反汇编:

先了解test_func1函数的参数(long long int a1, ...)中的“...”省略号,它表示这是一个可变参数列表。用于表示将来调用该函数时,可能会传递除参数a1以外的一个、两个或多个的参数给test_func1函数。那么如何获取可变参数列表中的参数呢?经过上面的标准文档说明和反汇编代码的分析,然后参照网上的一些分享,用以下的方法获取可变参数列表:

自定义va_list类型,typedef char *va_list。其实就是一个指向char类型的指针,void类型的指针void *应该更合理(没试过)。va_list指针用于指向可变参数列表中的不同类型的参数。
定义宏va_start(ap,v),ap就是va_list类型的变量,v就是靠近可变参数列表左边的第一个参数(这里是a1参数);这个宏的目的就是用a1变量在栈中的地址初始化va_list类型的ap指针,让它指向可变参数列表中的第一个参数(这里是a2)。
定义宏va_arg(ap,t),ap就是va_list类型的变量,已通过va_start(ap,v)初始化;t就是要获取的参数的类型,如在这里要获取a2参数,就是va_arg(ap,int);这个宏的作用是首先用sizeof(t)判断要获取的参数的类型t的大小,如果是小于等于4字节,就按4字节大小在栈中取值,如果大于4字节(在这里就默认为8字节),就需要判断ap指针是否在8字节对齐的地址上,如果是就直接在当前位置取8字节数据,ap=ap+8指向下一个数据,如果不是,ap就需要ap=ap+4加4到达8字节对齐的地址上取8字节数据,ap=ap+8再加8指向下一个数据。
定义宏va_end(ap),ap就是va_list类型的变量,这个宏用于销毁ap指针,就是出于安全让指针指向0地址处(相当于NULL指针)。
定义宏_INTSIZEOF(n),n是数据类型,源于计算4字节对齐。
通过定义了上面的宏,我们就可以在test_func1函数中使用这些宏去获取可变参数列表中的参数了。用法如下:

2.printf实现
printf的实现就是需要用到可变参数列表,定义好上面的宏后,就需要开始写如何格式化输出信息了。所谓的格式化,可以简单理解为在一串字符串中使用占位符表示将要输出的数据,如“a = %drn”,%d就相当于占位符,表示这个位置将用一个十进制有符号整型数据(int)来代替。
怎么实现呢?其实就是通过读取格式化字符串中的每个字符,当读取到%百分号时,再读取%百分号的下一个字符,判断是什么字符,如‘d’这个字符表示将数据转换为十进制后输出。本次实验的printf只实现了一下几种格式输出:

1.十进制整型输出(包括d,u,ld,lu)
首先就是就是计算这个十进制数有几位,如123,很明显有3位,代码实现如下:

“/”斜杠表示求除法中的商,如123除以10的商为12,余数为3
例如,s32_tmp = 123;第一次计算,123除以10的商为12,即s32_tmp = 12,count = 1;第二次计算,12除以10的商为1,即s32_tmp = 1,count = 2;第三次计算,1除以10的商为0,即s32_tmp = 0,count = 3。此时s32_tmp=0,退出while循环。求得count=3,即表示123这个数有3位。
然后,从高位到低位输出十进制数,代码如下:

“%”百分号表示除法中求余数,如123除以10的商为12,余数为3
pow_10()这个函数用于求10的n次方,如pow_10(2)返回10的2次方100的值。这里需要注意pow_10()的返回值定义为long long int类型,否则在格式化长整型(ld,lu)时会出错。
myputc(),用于串口输出一个字符。
例如,s32_tmp = 123;输出第一个字符,123除以10的2次方,商为1,余数23,即c = 1,s32_tmp = 23,c + ‘0’表示1加0的ascii码0x30,就是1的ascii码0x31,然后串口输出0x31,这会在串口调试助手中显示字符1。以后的输出也是相似的,直到count为0,退出while循环。
注意,如果s32_tmp = -123,求得的c的值也是负的,在myputc()中就需要用‘0’-c,才能输出正确的字符。
2.十六进制输出
输出十六进制与输出十进制差不多,只是一个除以16,一个除以10。代码如下:

3.浮点输出
将浮点数分成整数部分和小数部分,整数部分的处理如上面说明的;小数部分通过将小数乘以10,再强制类型转换为long long int类型(这里需要小心强制类型转换后,数据的变化;由于float(例外,转换后为64位)和double都是64位,刚开始是转换为char类型的,后来成就出错了,应该是符号位在转换时改变了,导致出错),如,-0.1234乘以10得-1.234,转换后为-1。负浮点数输出代码实现如下:

4.回车,换行

3.串口字符输出函数
如何初始化串口,请看基于STM32从零写操作系统系列---基于寄存器写串口驱动,这里有详细的步骤。或参考文章结尾分享的源代码,会有所不同,但原理一样。
字符输出函数代码实现如下:

4.效果


5.代码编译
这里解释一些自己定义的编译指令:
printf功能,我是通过编译成库来提供的,所以首先要编译库命令,在项目根目录printf_proj输入make mylib编译库
清除库的.o文件,make clean_libobj
make编译项目
make all_clean,用于清除所有编译后得到的文件,包括库
make clean,清除所有编译后得到的文件,除lib文件夹下的文件
6.小结
printf的功能基本实现了,代码比较粗糙,还可以进行修改;实现的方法,我就想到这种,如有其它好方法请介绍给我!!下面有源代码分享和arm文档分享,以及串口调试工具。
源代码包文件名:printf_proj.zip
百度云分享:
链接:https://pan.baidu.com/s/1DlzYMo8oZsnF9ammJuuZoQ
提取码:dc5h
史海拾趣
|
摘 要:随着科技的不断向前发展,汽车电子化程度也越来越高,半导体技术也随之崛起。本文详尽的描述了硅技术的进步,微控制器在汽车应用上的发展以及硅产品在汽车网络所发挥的巨大潜力。最后作者希望汽车制造商和半导体生产商能够密切合作为 ...… 查看全部问答> |
|
湘潭钢铁集团公司(以下简称湘钢)煤气调度系统在改造前使用的都是 型淘汰仪表,截至改造前安装的 /0 块仪表因!电缆等原因已全部瘫痪。“六五”以来湘钢经过几次大的改造煤气用户大量增加,煤气测量点由原来的 12 多点已增至近/22 点,显然现有的煤 ...… 查看全部问答> |
|
最近买了个usb接口键盘,老是要重插才能用,按照网上所说把设备管理-》usb room hub-》电源管理-》允许计算机关闭设别以节约电源停掉了。好像也不是qq冲突问题,在qq目录中找不到网上所说的的那两个文件。如果我把液晶显示其关掉,让机器开着, ...… 查看全部问答> |
|
sysAuxClkRateSet(int rate)函数中,rate只能设成(2,4,8,16,32,64,128,..,1024等等),我想精确定时到1ms或5ms、10ms该怎么办,或者有其它方法吗,请大家帮忙!… 查看全部问答> |
|
大家好。本人对于UCOS还是新手。想找UCOS的系统移植到C51上,遇到一些问题。希望大家来帮忙解决一下。 (在网上下载了一个移植实例有些看不太明白) 问题一:实例代码如下 ;定义重定位段 ...… 查看全部问答> |
|
近日小弟准备用两组MC3486/MC3487实现数据通讯,但不知道MC3486/3487该怎么使用,接口电路怎么画?是否需要进行阻抗匹配?是否需要光藕隔离? 还望各位大哥小弟们赐教.谢谢~~~~~… 查看全部问答> |




