大家好,我是老耿,高职青椒一枚,一直从事单片机、嵌入式、物联网等课程的教学。对于高职的学生层次,同行应该都懂的,老师在课堂上教学几乎是没什么成就感的。正因如此,才有了借助 **** 平台寻求认同感和成就感的想法。在这里,我准备陆续把自己花了很多心思的教学设计分享出来,主要面向广大师生朋友,单片机老鸟就略过吧。欢迎点赞+关注,各位的支持是本人持续输出的动力,多谢多谢!
通信,按照传统的理解就是信息的传输与交换。对于像STM32这样的单片机来说,通信则与传感器、存储芯片、外围控制芯片等技术紧密结合,成为整个单片机系统的“神经中枢”。没有通信,单片机所实现的功能仅仅局限于单片机本身,就无法通过其它设备获得有用信息,也无法将自己产生的信息告诉其它设备。如果单片机通信没处理好的话,它和外围器件的合作程度就会受到限制,最终整个系统也无法完成强大的功能,由此可见单片机通信技术的重要性。UART(Universal Asynchronous Receiver/Transmitter,即通用异步收发器)串行通信是单片机最常用的一种通信技术,通常用于单片机和电脑之间、单片机和单片机之间、单片机与外围器件的通信。
【学习目标】
- 知道通信基本概念的含义;
- 理解通信机制中物理层和协议层分离的理念;
- 学会配置STM32的串口功能;
- 了解printf()函数“打印”至串口的实现过程;
- 掌握使用串口调试软件对单片机的调试方法。
STM32串口通信涉及的知识较多,为了不让篇幅太长,本章打算分五个部分来讲解,本文是第五部分,详细解读串口收发的工程源码。
五、STM32的USART收发通信实验
5.6 代码剖析
和 这两个文件的源码来自正点原子STM32系列的开发板,版权归广州市星翼电子科技有限公司所有。根据笔者自己的开发和教学经验,在STM32串口驱动的完整性和可移植性方面,正点原子的确做的非常优秀,我们不妨就来学习一下人家的优秀之处。
的源码已在上一部分剖析过了,本节我们来剖析一下 的源码。为了便于阅读,我们将其大致分为三个代码段,如代码清单3所示。
代码段1
代码清单4有些抽象,如果初学者看不懂也不要紧。总之,你只需要明白,有了这段代码,printf() 函数“打印”的信息就到USART1上去了。
-
//------------------------------------------------------
-
// 代码清单4:的代码段1
-
//------------------------------------------------------
-
-
//加入以下代码,支持printf函数,而不需要选择use MicroLIB
-
#if 1
-
#pragma import(__use_no_semihosting)
-
//标准库需要的支持函数
-
struct __FILE
-
{
-
int handle;
-
};
-
-
FILE __stdout;
-
//定义_sys_exit()以避免使用半主机模式
-
void _sys_exit(int x)
-
{
-
x = x;
-
}
-
-
//重定义fputc函数
-
int fputc(int ch, FILE *f)
-
{
-
while((USART1->SR&0x40)==0); //循环发送,直到发送完毕
-
USART1->DR = (u8)ch;
-
return ch;
-
}
-
-
#endif
第7行声明提到的半主机是这么一种机制,它使得在ARM目标上跑的代码,如果电脑运行了调试器,那么该代码可以使用该主机电脑的输入输出设备。这点非常重要,因为开发初期,可能开发者根本不知道该ARM器件上有什么输出设备,而半主机机制使得你不必知道ARM器件的外设,利用电脑的外设就可以实现输出调试。所以要利用目标ARM器件的输出设备,首先要关掉半主机机制,然后再将输出重定向到ARM器件上,也就是我们期望的printf()函数输出重定向到USART1。
那么如何实现重定向呢?第22~27行重写的 fputc() 函数即可实现,因为如果你研究过 printf() 函数的源码,最终调用的是标准库 fputc(int ch,FILE *f) 这个函数。而我们现在自定义了一个与之同名的函数,那 printf() 函数会调用哪个呢?标准库里的?自定义的?还是编译报错呢?答案是使用自定义的。于是我们在 fputc() 函数内部将输出的字符指向USART1的数据寄存器即可。
第24行的等待发送完毕是通过读取 USART1->SR 状态寄存器里的TC位来实现的。我们把手册里对这个寄存器的描述摘录过来,如图20所示。TC位正好与0x40(01000000)里的那个1在相同位置,(USART1->SR&0x40)==0 意味着TC位为0,说明发送未完成,那就停在对应的while循环里。如果发送完成,TC位变成1,while循环退出。由此实现了“循环发送,直到发送完毕”。
第25行的 USART1->DR 是数据寄存器,如果是发送,把待发送的字符填入该寄存器,之后会借助移位寄存器将数据一位一位的发送出去。如果是接收,移位寄存器会将接收到的数据一位一位地填入该寄存器。因此,USART1->DR 这个数据寄存器是收发共用的,会根据程序的需要自动改变数据方向。关于该寄存器的详细描述,请参考STM32数据手册。
我们再回到与“半主机”有关的代码上,为确保没有从C库链接使用半主机的函数,那么 中有些使用半主机的函数要重写,于是就有了第9~19行的代码。至于为何这么写,有兴趣的朋友可以阅读ARM官方的两个文档《RealView编译工具开发指南》和《ARM Developer Suite Compilers and Libraries Guide》,内容比较多,理解起来也有难度。
最后,我们再补充一个在Keil编译中“使用微库”的问题,即第1行注释里提到的“use MicroLIB”,它其实是Keil编译器的一个配置选项,如图21所示。如果勾选此项,第5~19行的代码也就不需要了,只需要留下重定向 fputc() 函数的代码即可。因此,如果你看到有的STM32工程的串口“打印”没有出现类似的代码段1,不妨看一下是否勾选了此项。
微库可以看作是缺省标准C库的备选库,它用于必须在极少量内存环境下运行的嵌入式应用程序,它只保留了标准库里的一些常用功能,在一定程度上方便了程序的开发和移植,毕竟标准库是成熟和通用的。当然,也有弊端,使用微库编译出来的代码量会比较大,感兴趣的朋友可以在做完该实验之后,使用微库再编译一次,看看代码量究竟差了多少。
代码段2
这段代码主要就是一个串口初始化函数 uart_init(),完成 USART1_TX 和 USART1_RX 两个引脚的GPIO初始化、USART1串口中断的初始化以及串口自身参数的初始化,源码见代码清单5。
-
//----------------------------------------------------------------
-
// 代码清单5:代码段2
-
//----------------------------------------------------------------
-
-
#if EN_USART1_RX //如果使能了接收
-
-
u8 USART_RX_BUF[USART_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.
-
u16 USART_RX_STA = 0; //接收状态标记
-
-
/**
-
******************************************************************
-
* 函 数 名:uart_init
-
* 功 能:串口初始化
-
* 入口参数:bound --- 波特率
-
* 出口参数:无
-
* 说 明:PA9/PA10三方面的初始化:GPIO、USART和NVIC
-
******************************************************************
-
**/
-
void uart_init(u32 bound)
-
{
-
//定义必要的初始化结构体
-
GPIO_InitTypeDef GPIO_InitStructure;
-
USART_InitTypeDef USART_InitStructure;
-
NVIC_InitTypeDef NVIC_InitStructure;
-
-
//使能USART1和GPIOA外设时钟
-
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);
-
-
//USART1_TX(PA9),复用推挽模式
-
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
-
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
-
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
-
GPIO_Init(GPIOA, &GPIO_InitStructure);
-
-
//USART1_RX(PA10),浮空输入模式
-
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
-
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
-
GPIO_Init(GPIOA, &GPIO_InitStructure);
-
-
//USART1的中断配置
-
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
-
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;
-
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
-
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
-
NVIC_Init(&NVIC_InitStructure);
-
-
//USART1初始化设置:波特率、8位字长、1位停止位、无校验、无硬件流、收发一体
-
USART_InitStructure.USART_BaudRate = bound;
-
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
-
USART_InitStructure.USART_StopBits = USART_StopBits_1;
-
USART_InitStructure.USART_Parity = USART_Parity_No;
-
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
-
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
-
USART_Init(USART1, &USART_InitStructure);
-
-
//开启串口1及接收中断
-
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
-
USART_Cmd(USART1, ENABLE);
-
}
第5~8行,这几个宏和变量的作用已在前面剖析 源码时解释过了。
第29~38行是对PA9和PA10两个引脚的GPIO初始化。这里需要注意的是,复用功能下的GPIO模式怎么判定?查看手册得知,如表4所示,配置全双工的串口1,那么 TX(PA9)需要配置为推挽复用输出,RX(PA10)配置为浮空输入或者带上拉输入。
第41~45行,对于NVIC中断优先级管理,我们在前面章节已有讲解,这里不重复了。需要注意一点,因为使用到了串口的中断接收,必须在 里面设置 EN_USART1_RX 为 1(默认设置就是1)。该函数才会配置中断使能,以及开启USART1的NVIC中断。这里我们把USART1中断放在组2,优先级设置为组2里面的最低。
第48~54行,我们配置USART1通信参数为:波特率由参数bound传入,字长为8,1个停止位,无校验位,不使用硬件流控制,收发一体工作模式,然后调用 USART_Init() 库函数完成配置。
第57~58行,配置完NVIC之后调用 USART_ITConfig() 函数使能USART1接收中断,最后调用 USART_Cmd() 函数使能USART1。
代码段3
这段代码是USART1的中断服务函数,处理串口接收到的数据,源码见代码清单6。
-
//----------------------------------------------------------------
-
// 代码清单6:代码段3
-
//----------------------------------------------------------------
-
-
/**
-
******************************************************************
-
* 函 数 名:USART1_IRQHandler
-
* 功 能:串口中断处理函数
-
* 入口参数:无
-
* 出口参数:无
-
* 说 明:开发板若收到串口助手发来的消息,则进入此中断
-
******************************************************************
-
**/
-
void USART1_IRQHandler(void)
-
{
-
u8 Res;
-
-
//接收到消息(必须是0x0d 0x0a结尾)
-
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
-
{
-
Res = USART_ReceiveData(USART1); //读取接收到的数据
-
-
if((USART_RX_STA&0x8000)==0) //接收未完成
-
{
-
if(USART_RX_STA&0x4000) //接收到了0x0d
-
{
-
if(Res!=0x0a) USART_RX_STA = 0; //接收错误,重新开始
-
else USART_RX_STA |= 0x8000; //接收完成了
-
}
-
else //还没收到0X0D
-
{
-
if(Res==0x0d)
-
USART_RX_STA |= 0x4000;
-
else
-
{
-
USART_RX_BUF[USART_RX_STA&0X3FFF] = Res;
-
USART_RX_STA++;
-
-
//接收数据错误,重新开始接收
-
if(USART_RX_STA>(USART_REC_LEN-1))
-
USART_RX_STA = 0;
-
}
-
}
-
}
-
}
-
}
-
-
#endif
这个函数的实现思路是这样的,当接收到从电脑发过来的数据,把接收到的数据保存在 USART_RX_BUF 数组中,同时在接收状态寄存器 USART_RX_STA 中计数接收到的有效数据个数,当收到回车的第一个字节 0x0d 时,计数器将不再增加,等待 0x0a 的到来,而如果 0x0a 没有来到,则认为这次接收失败,重新开始下一次接收。如果顺利接收到 0x0a,则标记 USART_RX_STA 的第15位,这样完成一次接收,并等待该位被其他程序清除,从而开始下一次的接收,而如果迟迟没有收到 0x0d,那么在接收数据超过 USART_REC_LEN 的时候,则会丢弃前面的数据,重新接收。
第48行的 #endif 配对的是代码段2的第1行 #if EN_USART1_RX。当需要使用串口接收的时候,我们只要在 里面设置 EN_USART1_RX 为1就可以了。不使用的时候,设置 EN_USART1_RX 为0即可,这样可以省出部分SRAM和FLASH,我们默认设置 EN_USART1_RX 为1,也就是开启串口接收。
5.7 源码剖析
主程序的源码见代码清单7,请结合图22的实验效果来阅读,控制思路已在注释中交待。
-
/************************************************
-
* 代码清单7:
-
* 项 目:串口收发测试
-
* 任务描述:发送和接收
-
* 实验平台:OneNET STM32开发板
-
* 作 者:老耿
-
* 日 期:2024/4/23
-
***********************************************/
-
-
//-----------------------------------------------------
-
// 必要的头文件
-
//-----------------------------------------------------
-
#include ""
-
#include ""
-
#include ""
-
#include ""
-
-
//-----------------------------------------------------
-
// 主函数
-
//-----------------------------------------------------
-
int main()
-
{
-
u16 time = 0; //控制闪烁的时长
-
u16 len; //存放接收的长度
-
u8 i; //循环下标
-
-
//执行必要的初始化
-
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断优先级分组
-
delay_init(); //延时初始化
-
Led_Init(); //LED初始化
-
uart_init(9600); //初始化串口
-
-
//上电后等待1s打印开机信息
-
delay_ms(1000);
-
printf("STM32 USART Test\r\n");
-
printf("LaoGeng 014428\r\n");
-
-
//绿灯闪烁表示程序运行,
-
//串口等待用户发消息,若用户发来消息,则回显该消息
-
while(1)
-
{
-
//加到1s变一次绿灯
-
if(time == 100)
-
{
-
GREEN_TOG;
-
time = 0;
-
}
-
-
//处理接收到的消息
-
if(USART_RX_STA & 0x8000) //确认接收完毕
-
{
-
len = USART_RX_STA & 0x3fff; //取得数据长度
-
printf("\r\nMessages you send:\r\n"); //回显前的提示语
-
for(i=0; i<len; i++)
-
{
-
USART1->DR = USART_RX_BUF[i]; //收到的内容填入发送寄存器
-
while((USART1->SR & 0x40) == 0); //等待发送完成
-
}
-
printf("\r\n"); //换行分隔前后消息
-
USART_RX_STA = 0; //清空接收标志
-
}
-
-
//每10ms累加1次
-
delay_ms(10);
-
time++;
-
}
-
}
5.8 测试与验证
把程序下载到麒麟座开发板中,可以看到板子上的绿色LED开始闪烁,说明程序已经在跑了。串口调试助手软件很多,我们用的是正点原子的串口调试助手XCOM,从笔者的使用体验上,这款软件的界面和中文支持不错,接下来就可以按照图22里的步骤来测试了。至此我们就完成了串口“发送”和“接收”实验,后续其他实验也将继续沿用串口功能,尤其是“打印”必要的测量数据和调试信息。
(第五部分完,共五部分)