文章目录
O、说在前面的话
原先一直用的原子和野火的USART模块代码学习,原理也只是浅尝辄止,后来随着工程越来越复杂,遇到了一些printf函数导致的程序卡死问题,才着重学习了下USART模块函数,发现小小一个USART模块涉及的内容也大有学问。
平台:keil-MDK、Windows电脑、单片机开发板
一、名词解释(简单版)
1.半主机
半主机是用于ARM目标的一种机制;可将来自STM32单片机应用程序的输入输出请求传送至运行仿真器的PC主机。使用此机制可以启用C库中的函数,如printf()和scanf(),来使用PC主机的屏幕和键盘。
这样就可以看到单片机的输入输出,方便进行调试。但是这种机制的运行需要仿真器,否则无法运行。
简单的来说,半主机模式就是通过仿真器实现开发板在电脑上的 输入 和 输出 。
在嵌入式的编程中你是避免不了使用printf、fopen、fclose等函数的但是因为嵌入式的程序中并没有对这些函数的底层实现,使得设备运行时会进入软件中断BAEB处,这时就需要__use_no_semihosting_swi这个声明,使程序遇到这些文件操作函数时不停在此中断处。
所以,我们需要禁止半主机模式。当目标板脱离仿真器(jlink/ulink)单独运行时,不能使用半主机模式。
否则进入软件中断BAEB处,无法再执行下去。
有仿真器帮忙运行到PC主机,可以用半主机模式,调用C库中的函数来使用主机的屏幕和键盘。
没有仿真器;就要禁用半主机,防止程序进入BAEB中断,导致无法运行。
禁用半主机:
#pragma import(__use_no_semihosting) //关闭半主机模式,只需要在任意一个C文件中加入即可。
摘自以下知乎链接
链接: Semihosting半主机模式&重定向(百问网学习笔记)
2.重定向
什么是重定向?printf函数原本是打印控制台上的,可单片机又没有控制台,若想要让他传输到串口,就需要对printf重定,将fputc里面的输出指向目标设备。因printf函数调用了fputc,而fputc输出有默认指向的目标,且不同库中的fputc输出指向不同,所以需要重写fputc。
二、关于串口助手
USART外设本质上是传输数据,软件写数据,例如要传输1byte数据0x41,硬件负责产生0x41对应的波形,串口助手大相径庭其中一个功能是接收原始数据并且显示。
如图一所示,野火的串口助手可以选择HEX或ASCII模式显示
十六进制 | 二进制 | HEX模式
: 原始数据格式显示
文本模式
: 以原始数据译码后的数据格式显示,这里的译码方式有很多种,不过原子和野火的串口助手文本模式只支持ASCII模式。可能和野火、原子的代码都是ASCII格式,写另外的译码代码耗时很多但收效不高有关?
注意:如果字符,数字,字符串都能正常打印显示,但是中文就会出现乱码,那大抵是keil的编码格式和串口助手的译码格式不匹配,也就是图2的“编码“”译码“不匹配,比如你keil用utf-8编码,但是串口助手用ASCII解码显示数据,如果是英文数字那基本上没事,如果是中文,那基本会乱码或者出现一些奇怪的文字。
参考图三是ASCII编码字符集。
编码有关资料参考链接: 一文帮你彻底弄懂编码/字节/字符
参考上面链接,编码范围的不同,可能会导致你转格式的时候已经写好的程序乱码。
12.22更新:下面给出一个简单例子。
ASCII和ANSI编码格式区别参考链接: ASCII码和ANSI码的区别
如图四,写一个简单的打印(蓝框),点击扳手看到当前是在ANSI编码格式下(红框)。
如图五,在扳手设置中下拉编码格式栏找到选择UTF8格式编码(红框),发现此时之前写的ASCII格式
的中文乱码了,此时在UTF-8编码格式下写一行打印UTF8格式
的函数(蓝框)。
如图六,切换回ANSI编码,之前在UTF-8编码格式下写的中文乱码了。但是ANSI编码格式下写的代码的中文部分恢复了。利用数字英文切换编码格式不容易乱码,printf函数里使用英语单词是不错的解决方案,注释同理,但是注释本就是方便理解代码,英文注释阅读起来更困难了。
此外还有一个技巧就是在copy移植其他工程的代码,粘贴过来是乱码的话可以先修改工程的编码格式为更大编码范围的编码格式例如GB2312(keil工程中的代码复制到另外一个keil工程,两边都需要修改编码格式为GB2312),然后copy过来大概率就可以无乱码,还有一种方法是把.c、.h等代码原文件利用转码工具转码(简单的txt文档编辑也有一定的转码功能,自行搜索)。
此时在主函数里调用此子函数如图七,编译烧录运行程序(此例子用到usart模块,delay模块,以及全篇所述的printf函数调用条件)修改串口波特率和串口助手一致。
如图八,以野火的调试助手为例,修改COM口、波特率、开启串口(黄框),接收区显示通过ASCII编码(红框)翻译的译文,此外还有HEX(蓝框:原数据),勾选日志模式可显示时间轴和编码翻译格式(绿框)。
一般涉及较复杂工程、收发控制命令,协议解析等数据看数据原文是很有必要的。
通过这个简单例子,有望帮助理解图二内容。
USART外设模块本身就能发送一个BYTE、任意数字,数组和字符串,但是需要调用不同函数,设置实参,才能发送不同类型的数据。USART的传输字符串的函数原理目前看来和printf函数没什么两样,实现的功能也一样,学到后来发现,为了实现printf这一行代码,花那么多时间精力,但是结果好像只是稍微方便了一点?
参考链接: STM32串口通信中使用printf和USART_SendData比较
另外附上一份两分钟做的粗糙的要实现打印串口信息的的流程图如图十,方便理解,接着就是对每个部分的详细解释和展示代码部分
三、使用printf函数
首先,printf函数学过c语言都知道,用于打印信息到控制台上,但是若想在单片机上实现,单片机又没有控制台,那么它要打印到哪里去?要想实现printf打印信息,就只能打印到lcd屏幕或者通过串口打印显示在电脑上。同时,以串口为例,光有printf和串口也不行,printf也不知道要打印到串口还是哪里,打印到哪个串口,此时就需要对printf重定向,通过修改fputc()函数来实现,fputc()是printf()的底层函数。
方式 | 比较 |
---|---|
调用printf | 格式化字符,字符串比较有优势,适合用于调试、打印显示大文本及信息。 |
USART标准库代码 | 适合在涉及自定义帧头+校验的通信时,用USART_SendData实现单字符及控制命令。例如stm32基于USART通信发送CMD指令控制ESP8266工作。 |
使用printf函数还需要注意在何种库的环境下,不同库,要求也不同,此处只列举出keil-MAK平台下,在微库和标准C库的环境下使用printf函数的方法。
参考链接: MicroLib微库和ARM标准C库有什么区别?
方式1.使用ARM标准C库(不勾选Use MicroLIB)
使用标准库同时使用printf函数就需要强调不使用半主机(no semihosting)模式
(1)包含头文件:#include "#include “stm32f10x.h” #include <stdio.h>
(2)加入支持函数
(3)避免半主机模式
(4)printf重定向,修改fputc()函数的内容
注意:USART外设初始化资料很多就不贴出,另外文末也有源码,不过是只有简单实现串口打印功能,只初始化了串口接收,一般调试场景也够用,要更多功能移植也很简单
//标准库下重定向c库函数printf到串口,并且关闭半主机模式
*注意此代码我是封装在USART.c模块中*
#if 1
#pragma import(__use_no_semihosting)
//定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x) //这里有的版本没有void会导致错误
{
x = x;
}
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//重定向fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕//操作寄存器方式
USART1->DR = (u8) ch;
return ch;
}
#endif
方式2.使用MicroLIB微库(勾选Use MicroLIB)
使用微库的话,不会使用半主机模式,所以就省略了自己设置避免半主机模式。
(1)点击“魔术棒” Target标签下Use MicroLIB—勾选。
(2)包含头文件:#include "#include “stm32f10x.h” #include <stdio.h>
(3)printf重定向,修改fputc()函数的内容
//重定向c库函数printf到串口,重定向后可使用printf函数
*注意此代码是封装在USART.c模块中*
*微库和标准库的代码可以同时加到USART.c中,但要做点小修改*
#if 1
int fputc(int ch, FILE *f)
{
/* 发送一个字节数据到串口 */
usart_SendByte(ch);//根据自己封装的发送字节函数来修改//操作函数方式
/* 等待发送完毕 */
while (USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
return (ch);
}
/重定向c库函数scanf到串口,重写向后可使用scanf、getchar等函数 取消注释以开启
//int fgetc(FILE *f)
//{
// /* 等待串口输入数据 */
// while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
// return (int)USART_ReceiveData(DEBUG_USARTx);
//}
#endif
四、不使用printf函数
1.使用sprintf函数
优点
:节省代码空间(应该是printf的头文件支持库比这个函数需要的支持库大,用这个注释printf函数代码量减少挺多),不用重定向,灵活,不需要关闭半主机模式。
推荐使用这个函数
简单几行代码实现打印,用法:usart_printf("你好,世界!");
#include <stdarg.h>//支持sprintf,vsprintf函数
void usart_printf(char *format,...)
{
char String[100]; //定义输出字符串
va_list arg; //定义一个参数列表变量va_list是一个类型名,arg是变量名
va_start(arg,format); //从format位置开始接收参数表放在arg里面
//sprintf打印位置是String,格式化字符串是format,参数表是arg,对于封装格式sprintf要改成vsprintf
vsprintf(String,format,arg);
va_end(arg); //释放参数表
usart_SendString(String);//发送String
}
CSDN也找到一个大佬用vsprintf函数写了这个,内容更多,遇到问题也可以参考。
参考链接: 关于Stm32串口输出模仿printf编写的函数方法(野火stm32F103指南者板适用)
参考链接: C语言printf()、sprintf()、vsprintf()的区别与联系
五、常见问题
1.串口打印数字英文正常,但中文乱码
大概率是串口助手和keil的编码译码格式不匹配导致的中文乱码问题,在关于串口助手部分也详细说明了。
解决办法:更换keil中有printf函数语句的模块的编码格式就好了,当然也小概率有其他原因导致中文乱码,但是我没遇到过,不展开介绍了,自行搜索。
另外值得注意的一点是,UTF-8的编码范围大于ASCII,在keil里configuration(扳手设置)把UTF-8变ASCII基本不会乱码,但是ASCII改UTF-8基本全是乱码。而且ASCII又有烦人的中文删减字变乱码的问题,我想是不是建一个基础开发框架用UTF-8编码开始堆代码,到了调试需要printf了,再改成ASCII编码。减少ASCII删减中文注释带来的浪费时间问题呢?
具体操作方法可参考链接: STM32:串口通信——printf打印中文乱码问题解决
2.单片机正常运行,但是printf函数无法打印信息
1.在微库环境下,单片机正常运行到while但是无法打印printf,那就是printf的重定向部分出了问题。微库环境下不重定向printf,单片机也不会卡死,因为微库没使用半主机模式。
2.仔细检查连线,单片机的RX—USB转TTL的TX。单片机的TX—USB转TTL的RX。
另外,如果只是串口接收数据可以单用一根TX线(只发生数据呢?从来没试过)。
3.未知原因
3.程序卡死,Debug没加入主函数
1.单片机供电电压不够,最好都是外部独立供电,例如我这块ze板,上面有DC6-24V供电接口,那就最好通过DC6-24V供电,不然很容易就因为电平达不到标准卡在几个奇怪的地方进不了主函数。此外注意仔细检查下载器和单片机连线,杜邦线接触不良等问题,注意听USB接口插入电脑的提示音,以及例如ST-link上的LED灯状态。还有就是debug设置下的Settings中SWDIO有无数据。
2.在标准C库下没有关闭半主机模式就调用printf函数,那就是没仔细看这篇文章了
12.22更新:项目中移植代码又又又遇到这个问题,检查代码和供电,又花了好久到头还是调用printf()函数导致的问题,如图十一编译无错误,但是单片机无反应。原因是main函数调用子函数,子函数里使用的printf函数但是在usart模块代码中只有重定向的代码且没有勾选微库。
图十一
如图十二debug操作检查发现程序卡死在 LDR R0, =SystemInit,继续运行始终卡在此行。且代码并没有从main函数(如图十三)开始执行(黄色光标是程序运行到哪,蓝色是你滚轮选择的Disassembly栏代码行或者鼠标左键点击选中的代码行的位置)。
![]()
图十二
![]()
图十三
查看usart串口模块代码(usart.c),如图十四只有printf重定向的函数,参考图十,此时使用微库斯可行的,但是我们并没有勾选微库,此时可以通过勾选微库直接解决。或者如图十所示加入避免半主机、printf函数的支持函数的步骤来在标准库下支持printf函数打印。这里选择后者方式。
![]()
图十四
如图十五,在usart.c代码中加入红框内的代码。同时还需要加stdio.h头文件(标准输入输出)以支持printf函数的底层函数修改。前文有介绍。
![]()
图十五
如图十六,在usart.c模块函数中先包含usart.h头文件。
![]()
图十六
然后如图十七,在usart.h文件中包含32单片机寄存器和外设、标准输入输出头文件(红框),如要加入对sprintf、vsprintf函数的支持,可以加入绿框内的代码。较为详细的程序已在文末提供下载链接以便参考。
![]()
图十七
如图十八,重新debug,程序从main函数第一行开始执行了(黄色箭头),问题解决。
![]()
图十八
3.未知原因
多调试,多查阅,做笔记记录原因
六、拓展:在非串口的地方使用printf打印信息
找资料找着找着,越找发现越多原先未曾涉猎的东西,没怎么去深入学习,但是贴出原博客地址链接,有兴趣自行了解,真是让人摸不着头脑。
1.利用ITM机制调试
主要是使用printf进行调试,可以打印局部变量(当然Call Stack-Locals窗口和Watch窗口也可以,不过需要打断点)、打印程序运行记录等,跟串口重定向的printf功能一样,相比较优势是:利用Keil自带调试窗口,无需串口助手;利用仿真器下载线,不占用串口资源。
2.在其他位置使用printf函数打印信息
链接: 嵌入式 printf的几种办法 (ITM、SWO、semihosting、Keil Debug Viewer、RTT、串口重定向printf)
链接: printf系列教程00_概述printf各种打印输出方法和相关内容
最后放上源码便于大家参考、调试、学习:
源码分享: 百度网盘
提取码:MCYC
----来自一位不愿意透露姓名的百度用户的分享
有什么问题可以留言或者私信找我,我看到就会回,没回就是没看到。
这么详细应该没问题了吧O.0
哇,第一次写博客原来这么费时间费精力,刚开始只是想要记录一次学习经历,为了理解透彻讲明白,然后发现越学越多,越深入,两三天就这么没了。懒得检查稿子了,错就错吧
应该没人看到这段话
其他参考资料/博客/文章:
江协科技: STM32入门教程-2023持续更新中
不得不说江协神中神,stm32启蒙老师,视频又新,内容涉及又广
参考链接: 关于Use MicroLIB是否勾选问题
参考链接: STM32的UART读写及printf打印
这边大佬也讲的很清楚: 彻底搞清printf在STM32上的使用
文章有涉及数组的一些讲解: 自写Usart_Printf()串口发送函数实现方法详解
printf函数重定向问题以及fputc函数底层解释: 困惑多年,为什么printf可以重定向?
更新信息:
发布于:2023.8.23
第一次更新:12.22
位置:
1.【二、关于串口助手】
2.【五、常见问题 3.程序卡死,Debug没加入主函数】
内容:
1.增加一个简单例子说明
2.更新错误解决图示
第二次更新:1.15
修改了一句话的文字表述