目录
参考正点原子的视频教程,本文我们将编写一段以USART作为通信串口、接收到数据后立即引发中断、并执行中断处理函数将数据发送给MCU的程序。
源码请参考正点原子-实验4-串口实验。
一、什么是USART
1. USART简介
USART(Universal Synchronous/Asynchronous Receiver/Transmitter)的全称为通用同步/异步串行接收/发送器,它与普通串口UART的不同在于,USART有同步、异步两种工作模式;而UART是经过裁剪后的USART,只有异步工作模式。
2. STM32F4中的USART
2.1 USART的发送/接收引脚
STM32F4中有两个USART(USART1、USART2),其中,我们以USART1为例,它的发送、接收与PA9、PA10引脚相连,从如下的GPIO引脚复用图中可以看出,PA9可以复用为USART1的发送(TX)功能,而PA10可以复用为USART1的接收(RX)功能。
2.2 USART转为USB接口
单片机通常需要与电脑互相传输数据,但是电脑没有USART接口,这该怎么办呢?设计者通常通过一个芯片把USART接口转换为USB接口,这样就可以与电脑通信了。
能实现USART转USB的芯片有很多,STM32F4中使用的是CH340G。
STM32F4中USART1转USB的原理图如下所示:
其中,TXD/RXD 是相对于 CH340G 来说的,也就是 USB 串口的发送和接受脚。而 USART1_RX/USART1_TX 则是相对于 STM32F407ZGT6 来说的。这样,通过对接,就可以实现 USB 串口和 STM32F407ZGT6 的串口通信了。
注: 在本文的实验中,就是用USB串口把USB信号转换为串口信号,进而通过PA9和PA10的复用功能来与单片机进行通信。
二、常用的串口相关寄存器
常用的串口相关寄存器有三个:
- USART_SR 状态寄存器,用来记录一些状态,如是否接收到数据,是否要发送数据等
- USART_DR 数据寄存器,用来存储数据,包括要接收的数据和要发送的数据等
- USART_BRR 波特率寄存器,用来调整波特率的大小。
其中,USART_SR和USART_DR的各个位的含义在 《STM32F4xx中文参考手册》 的26.6节可以查到, 本文不再赘述。这里只简要介绍一下USART_BRR波特率寄存器的内部结构:
在上篇博文【STM32F4】四、串口通信1——硬件部分中我们在第三部分列出过,下面我们只把波特率发生器的硬件部分展示在下图中:
首先由USART_BRR寄存器产生初始的时钟信号,假设频率为** f ;输入到分频系数为USARTDIV的分频器后,输出信号频率变为 f / USARTDIV**;在经过采样除法器后,最终输出信号的波特率为** f / USARTDIV / [8 x (2 - OVER8)] **,其中,OVER8可人为设置。
其中,初始频率 f 通常是固定的,OVER8 通常设为0;那么公式就简化为波特率 = f / USARTDIV / 16
。而为了得到最终的波特率,我们要求的其实就是唯一的可变参数USARTDIV
,实际上它也是由USART_BRR寄存器决定的。
但在程序中,我们不需自己计算USART_BRR的配置。我们只要把想要的波特率(如115200)直接写入程序、传给相应函数即可,STM32F4提供的库函数会帮我们计算并对USART_BRR进行配置。
三、程序编写
1. 串口配置的一般步骤
根据正点原子的课程,列出串口配置(带中断响应)的一般过程如下:
① 必要的时钟使能
- 串口时钟使能:RCC_APBxPeriphClockCmd();
- GPIO时钟使能:RCC_AHB1 PeriphClockCmd();
注:要想使用一个外设,必须要对【外设】、以及【连接外设的GPIO引脚】的时钟进行使能。
② 引脚复用映射:GPIO_PinAFConfig():
③ GPIO端口模式设置:GPIO_Init(); //模式设置为GPIO_Mode_AF
④ 串口参数初始化:USART_Init(); //配置波特率等参数
⑤ 开启中断并且初始化NVIC(如果需要开启中断才需要这个步骤):
- NVIC_Init();
- USART_ITConfig();
⑥ 使能串口:USART_Cmd();
⑦ 编写中断处理函数:USARTx_IRQHandler();
⑧ 串口数据收发:
- void USART_SendData(); //从DR寄存器中将数据发送出去
- unit16_t USART_ReceiveData(); //从DR寄存器读取接收到的数据
⑨:串口传输状态获取:
- FlagStatus USART_GetFlagStatus();
- void USART_ClearITPendingBit();
下面我们也将按照上述步骤,一一编写程序。
2. 编写程序
下面我们把经过详细注释的代码放上来,全都是按照上述九个步骤来写的:
#include "stm32f4xx.h"
#include "usart.h"
#include "delay.h"
void My_USART1_Init(void) //配置和初始化的程序,除中断处理函数外,其他的配置都在这里面
{
GPIO_InitTypeDef GPIO_InitStructure; //用于GPIO配置的结构体
USART_InitTypeDef* USART_InitStruct; //用于USART配置的结构体
NVIC_InitTypeDef* NVIC_InitStruct; //用于NVIC配置的结构体
//===============================一、串口时钟使能===================================
//使能USART1,由于USART1挂载在APB2总线下,所以要去RCC相关的库函数中搜索APB2的时钟使能函数
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
//=================================GPIO时钟使能=====================================
//因为要通过PA9和PA10的复用功能来使用UART1,所以也要使能GPIOA的时钟,GPIOA挂载在AHB1总线下
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
//===============================二、引脚复用映射===================================
//通过下面这个函数,把PA9配置为复用功能——USART1_TX,PA10配置为复用功能——USART1_RX
GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);
//=============================三、端口模式设置==================================
//下面要配置PA9和PA10配置为复用模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //GPIO_Mode_OUT; //AF即复用模式
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//================================四、串口参数初始化=================================
//初始化USART1的配置
USART_InitStruct->USART_BaudRate = 115200; //波特率
USART_InitStruct->USART_HardwareFlowControl = USART_HardwareFlowControl_None;//不使用硬件流控制
USART_InitStruct->USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //把发送和接收功能都进行使能
USART_InitStruct->USART_Parity = USART_Parity_No;//不使用奇偶校验
USART_InitStruct->USART_StopBits = USART_StopBits_1;//使用1个停止位
USART_InitStruct->USART_WordLength = USART_WordLength_8b;//因为没有奇偶检验,所以可以使用8位字长
USART_Init(USART1, USART_InitStruct);
//====================================================================
//=============如果不使用中断,那么这个程序到这里就可以结束了=============
//====================================================================
//=============================五、开启中断并且初始化NVIC============================
//配置NVIC
//首先设置中断优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//再初始化NVIC
NVIC_InitStruct->NVIC_IRQChannel = USART1_IRQn;//不同的通道定义在顶层头文件stm32f4xx.h中 //设置NVIC通道为USART1的通道
NVIC_InitStruct->NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct->NVIC_IRQChannelPreemptionPriority = 1;//设置抢占优先级为1
NVIC_InitStruct->NVIC_IRQChannelSubPriority = 1;//设置响应优先级为1
NVIC_Init(NVIC_InitStruct);
//==================================六、使能串口====================================
//使能USART1
USART_Cmd(USART1, ENABLE);
//使能USART1的某种中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//把接收非空中断USART_IT_RXNE使能,即一旦接收到了信息,就引发中断,且执行相应的中断函数
//下一步就要在下面定义USART1的中断服务函数,函数名是固定的,在官方的系统系统文件中startup_stm32f40_41xxx.s已经给出:USART1_IRQHandler
}
//================================七、编写中断处理函数===============================
void USART1_IRQHandler(void)
{
u8 res; //用来记录接收到的数据,因为我们在上面设置的8位字长代表一个数据,所以这里可以用u8来记录?
//==============================九、串口传输状态获取=================================
if(USART_GetITStatus(USART1, USART_IT_RXNE)) //读取USART_IT_RXNE标志位的状态)
{
//================================八、串口数据收发==================================
res = USART_ReceiveData(USART1); //读取接收到的数据
USART_SendData(USART1, res); //把接收到的数据发出去
}
}
//主函数
int main(void)
{
My_USART1_Init();
while(1); //在这里无限循环即可,程序会自动执行 中断 和 中断处理函数
return 0;
}
注: 如果编译时程序出错,可能是因为在官方库文件usart.c里已经定义过一个中断处理函数USART1_IRQHandler()
,把它注释掉或者把这个文件删掉即可。
四、更复杂的程序(USART_RX_STA寄存器的应用)
我们在上面第三部分写的程序中,在中断处理函数里我们只是简单地把每个接收到的字符发送出去,没有做其他处理。但是,这往往不能满足工程师们的需求。
工程师们在使用串口时,通常要求在写完一段话后才进行发送(而不是每收到一个字符就发送出去),而且一定要有一个结束标志(如回车键Enter),当遇到结束标志时才进行发送,而不是像第三部分中机械地每收到一个字符就发送一个。
这就要求在以上程序的基础上,加入如下功能:
- 自己设计一个结束标志,以让单片机明白待发送的信息到哪里就结束了;
- 有一个buffer,用来存储我们输入的这一段话(这个buffer通常时我们自己创建的一个数组),知道遇到结束标志,才把这段话发送出去。
为了实现以上能存储一段话、且有结束标志的功能,STM32F4为我们提供了一个很方便的寄存器:USART_RX_STA。
1. USART_RX_STA 寄存器简介
USART_RX_STA寄存器共有16位,即0 ~ 15,每个位的作用如下图所示:
(下图源自正点原子《STM32F4开发指南——库函数版本》 5.3.3小节——USART1_IRQHandler 函数)
其中,0 ~ 13位用来表示接收到的有效数据个数,由于其有14个二进制位,因此最大的可保存数据为 2 ^14 -1,也就是说我们一段话最多可以包含 2 ^14 -1 个字符;
第 14 位用来表示是否接收到了某个标志符(图中的 0X0D 表示回车符,也就是说当接收到回车符时,要把第** 14** 位置 1),我们通常将回车符或者是回车符和某个字符的组合设为结束标志。
第 15 位用来表示是否接收完成,也就是要根据我们人为设计的结束标志(如回车+ c) 来判断,如果遇到了结束标志,则需要把这一位置1,对接收的信息处理完成后再把这一位置0。
2. 程序编写
2.1 程序思路
从上面的描述中可以看出,要使用USART_RX_STA寄存器,我们需要始终对 此寄存器的几个标志位 及 串口接收到的字符 进行监控和更新。
现在假设我们把标志为设为 ** 0X0D + 0X0A**,即回车键 + 换行键(其实只要在键盘上按下一次回车键就够了),那么我们要做的监控和更新操作如下:
- 每接收到一个字符,就把字符添加用以保存句子的buffer中,并把 0 ~ 13 位保存的数字加1,用以记录有效数据的个数;
- 监控接收到的字符,如果接收到了0X0D(回车符),则把第 14位置1;
- 监控接收到的字符,如果接收到了0X0A,则判断第 14 位是否为1,如果是1,表示上一个接收到的字符是回车符,当前接收到的字符是换行符,表示句子结束,这时候我们就把buffer中的信息拿来处理,并把寄存器的所有位都置0,重新开始工作;而如果当前接收到了0X0A,但第 14 位并不是1,那么表示出现了错误,则把buffer清空,且寄存器的所有位置0,重新开始工作。
2.2 编写程序
上述所有工作,都与我们的初始化函数My_USART1_Init
没有关系,初始化只是一个初始化功能,没有监督和更新的作用;
而上述所有监督和更新的工作,我们都在中断处理函数和主(main)函数中完成,如下,先展示出中断处理函数:
void USART1_IRQHandler(void) //串口1中断服务程序
{
u8 Res;
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
OSIntEnter();
#endif
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //根据USART_IT_RXNE标志位来确认是否接收到了新字符
{
Res =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的新字符
if((USART_RX_STA&0x8000)==0)//USART_RX_STA&0x8000,即读取USART_RX_STA的第15位(1000 0000 0000 0000,其实是第16位,但是用 0 ~ 15 来编号的话,我们把它叫做第15位)
{
if(USART_RX_STA&0x4000)//USART_RX_STA&0x4000,即读取USART_RX_STA的第14位,判断上一次接收到的字符是否是0x0d
{
if(Res!=0x0a)USART_RX_STA=0;//如果上一次是0x0d,但这次不是0x0a,则表示接收错误,USART_RX_STA全部清0,重新开始
else USART_RX_STA|=0x8000; //如果上一次是0x0d,且这一次是0x0a,表示接收到了结束标志位,当前的句子接收完成,把第15位置1
}
else //还没收到0X0D
{
if(Res==0x0d)USART_RX_STA|=0x4000;//如果这次接收到的是0x0d,则把USART_RX_STA的第14位置1
else
{
USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;//如果之前没有接收到0x0d,且这一个接收到的字符也不是0x0d,则表示当前这个字符是普通消息,则把它存在buffer(USART_RX_BUF)的第USART_RX_STA&0X3FFF中。其中,USART_RX_STA&0X3FFF是我们当前记录的字符个数,即USART_RX_STA的0 ~ 13位(0011 1111 1111 1111)
USART_RX_STA++; //当没有接收到0x0d时,USART_RX_STA的第14、15位都是0,只有前13位有数据,用以存储接收到的字符个数,因此这里直接把USART_RX_STA加1,用来记录字符个数,且不会影响到14、15位(因为它们都是0)
if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//如果接收到的字符个数比可接收的最多数量USART_REC_LEN多了,则表示溢出、接收错误,重新开始接收
}
}
}
}
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
OSIntExit();
#endif
}
#endif
从以上程序中可以看到,中断处理函数主要做了监督并更新第14、15位,以及保存字符、且监督并更新 0 ~ 13 位。
另外,关于程序中的#if SYSTEM_SUPPORT_OS OS
,我在这篇帖子中找到了相关的解释:
我感觉它的作用是保护传输数据不被打断。
如果有操作系统(对于STM32,一般是UCOS)的话,可能会涉及比串口中断优先级更高的中断,会打断串口传输,所以在进入串口中断后干脆就关掉总中断,等传完了再开中断,如果是这样的话中断结尾估计还得有个OSIntExit()。
这是一种保护临界段的手段。
那么,主(main)函数中需要做什么工作呢?
int main(void)
{
u8 t;
u8 len;
u16 times=0;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2
delay_init(168); //延时初始化
uart_init(115200); //串口初始化波特率为115200
LED_Init(); //初始化与LED连接的硬件接口
while(1)
{
if(USART_RX_STA&0x8000) //若检测到USART_RX_STA的第15位、即结束标志位为1,则表示当前信息传输结束
{
len=USART_RX_STA&0x3fff;//得到此次接收到的数据长度
printf("\r\n您发送的消息为:\r\n"); //把这句话通过串口打印到屏幕上
for(t=0;t<len;t++)
{
USART_SendData(USART1, USART_RX_BUF[t]); //向串口1发送数据
while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待发送结束
}
printf("\r\n\r\n");//插入换行
USART_RX_STA=0; //把当前信息处理完后,将USART_RX_STA清0,一切重新开始
}else //如果结束标志位为0.则表示当前信息传输还未结束
{
times++;
if(times%5000==0)
{
//打印信息以提示工程师继续输入信息
printf("\r\nALIENTEK 探索者STM32F407开发板 串口实验\r\n");
printf("正点原子@ALIENTEK\r\n\r\n\r\n");
}
if(times%200==0)printf("请输入数据,以回车键结束\r\n");
if(times%30==0)LED0=!LED0;//闪烁LED,提示系统正在运行.
delay_ms(10);
}
}
}
整个主函数中,最主要的就是``while(1)`循环,它负责监督USART_RX_STA寄存器的结束标志位,如果结束标志位被置1,则表示当前信息传输结束,主函数就把从串口USART1接收到的信息再通过USART1发送出去;
如果当前USART_RX_STA的结束标志位不是1,则表示传输还未结束,主函数则打印提示信息,以提示工程师继续输入信息。
源码请参考正点原子-实验4-串口实验。