【STM32F407学习笔记】串口外设USART
串口通讯是一种设备间非常常用的串行通讯方式,因为它便捷,大部分电子设备都支持该通讯方式。并且它也是一个很好的调试工具,我们可以利用它输出调试信息到电脑屏幕。
本节任务:利用USART外设实现STM32与电脑通信,重定向Printf() 函数
涉及外设:GPIO(复用输入输出) ,RCC (复位和时钟控制),USART(串口)
1. STM32 USART简介
1.1 串口通讯协议简介
- 通信的方式分类:并行通信,串行通信
1)并行通信:指数据的各位同时在多根数据线上发送或接收,如图中所示:
并行通讯特点:控制简单,传输速度快;由于传输线多,适用于距离近传输
2)串行通信:指数据的各位在同一根数据线上逐位发送或接收,如图中所示:
串行通讯特点:控制复杂,传输速度慢;只需要一个线,适用于远距离传输。 - 串行通信方式分类
在串行通信中,根据对数据流的分界、定时及同步方法不同,可分为同步串行通信方式和异步串行通信方式。
1)同步串行通信方式:把多个字符组成一个信息组(信息帧),每帧的开始用同步字符来指示、并且发送和接收的双方必须采用同一时钟,这样接收方就可以用时钟信号来确定每个信息位。同步通信如图所示:
同步通信:发送端和接收端必须使用统一时钟,它是一种连续传送数据的通信方式,一次通信传送多个字符数据,称为一帧信息。帧格式如图中所示:
同步串行通信帧:是将许多字符组成一个信息帧,这样,字符可以一个接一个的传输,但是在每帧信息的开始要加上同步字符,在没有信息要传输时,要填上空字符,因为同步传输不允许有间隙。
同步串行通信的特点:必须有同步时钟,传输信息量大,传输速度高,但是传输设备复杂,技术要求高。
2)异步串行通信方式:指通信双方以一个字符(包括特定附加位)作为数据传输单位,且放松放传送字符的间隔时间不一定,具有不规则数据段传送特性的串行数据传输。异步串行通信如图中所示:
异步通信:指发送和接收端使用各自的时钟,并且它是一种不连续传输的通信方式,一次通信只传一个字符数据,成为字符帧。字符帧之间的间隙可以是任意间隙。帧格式如图中所示:
异步串行通信帧:是将一个字节数据加上起始位、校验位、停止位,构成的字符帧。由于异步通信没有同步时钟,所以接收端要时刻处于接收状态。
- 起始位:在没有数据传送时即空闲状态,此时通信线上为逻辑“1”状态(高电平)。当发送端要发送1个字符数据时,首先发送一个逻辑“0”信号(低电平),这个低电平就是帧格式的起始位。
- 数据位:在起始位之后,发送端发出的就是数据位,数据位的位数没有严格限制5 ~8 位都可以。低位在前,高位在后,由低位到高位逐位传送。
- 校验位:数据发送完成后,可发送一位用来检验数据在传送过程中是否出错的校验位。(一般使用奇偶校验位)
- 停止位:字符帧格式的最后部分时停止位,逻辑“1”电平有效,它可以是1/2位,1位或2位。
异步串行通信特点:不需要时钟同步,通信实现简单,设备开销小。但是传输速率不高。
- 串行通信数据传送方向
根据串行数据的传输方向,我们可以将通信分为单工,半双工,全双工
1)单工:指数据传输仅能沿一个方向,不能实现反向传输。
2)半双工:指数据传输可以沿两个方向,但需要分时进行传输。
3)全双工:指数据可以同时进行双向传输。
- 串行通信传输速率
比特率:每秒钟传送的二进制位数。bps
波特率:每秒钟调制信号变化的次数。Baud
串行通信常用波特率表示数据传输速率。
比特率=波特率 × \times ×单个调制状态对应的二进制位数
串行通信双方识别位的时间间隔要相同,因此通信双方的波特率必须一致。
1.2 STM32 USART简介
STM32具有多个USART外设用于串口通信,USART(通用同步异步收发器)能够灵活的与外部设备进行全双工数据通信。
此图可以分为四部分:引脚、波特率发生器、USART控制单元、数据交换相关寄存器。
1. USART功能引脚
Tx:发送数据输出引脚
Rx:接收数据输入引脚
SW_Rx:数据接收引脚,只用于单线和智能卡模式,属于内部引脚,没有具体外部引脚。
nRTS:请求以发送(Request To Send),n表示低电平有效。该引脚仅适用于硬件流控制。
nCTS:清除以发送(Clear To Send),n表示低电平有效。该引脚仅适用于硬件流控制。
SCLK:发送时钟输出引脚.这个引脚仅适用于同步模式
2. 波特率发生器
USART的发送器速率控制和接收器速率控制共用一个波特率寄存器(USART_BRR),波特率寄存器里存放的是时钟分频值,它一共是16位,分为整数部分DIV_Mantissa[16:5]和小数部分DIV_Fraction[4:0]两部分。USART通信所需波特率是对相应总线时钟分频,然后一系列计算所得到的。USART波特率计算公式如下:
Tx/Rx波特率
=
f
c
k
8
×
(
2
−
OVER8
)
×
USARTDIV
\text{Tx/Rx波特率}=\frac{f_{ck}}{8\times(2-\text{OVER8})\times\text{USARTDIV}}
Tx/Rx波特率=8×(2−OVER8)×USARTDIVfck
f
c
k
f_{ck}
fck:系统总线时钟。USART1和USART6在APB2总线下,USART2在APB1总线下。
OVER8:是由USART_CR1的第15位设置。
USARTDIV:波特率分频系数,USART_BRR配置得到。USARTDIV的计算公式:
USARTDIV=DIV_Mantissa+(DIV_Fraction/8*(2-OVER8))
3. USART控制单元
整个USART控制单元包括:发送控制器、唤醒单元、接受控制器。我们通过配置寄存器相应位来设置这些控制器的工作模式。
发送控制器:工作在发送模式时,它将按照程序设置的波特率,帧格式将CPU的数据或者DMA总线上的数据一位一位送到Tx引脚。
接收控制器:工作在接收模式时,它将按照程序设置的波特率,帧格式将数据从Rx引脚一位一位的接收外部发来的数据并上传给CPU或DMA
4. 数据寄存器和移位寄存器
图中部分4一共有四个寄存器,发送模式用的“发送数据寄存器(TDR)”和“发送移位寄存器”,接收模式用的“接收数据寄存器(RDR)”和“接收移位寄存器”。其实TDR和RDR都属于数据寄存器(USART_DR)
USART发送过程:
- 使能发送,即USART_CR1的TE位置1
- 内部总线的数据的一个字节写入“发送数据寄存器(TDR)”(该操作将清零TXE位(发送数据寄存器非空),其他数据不可以写入)
- “TDR”中的数据一次性复制进入“发送移位寄存器”(将TXE置位(发送数据寄存器为空),后续数据可以接着写入)
- “发送移位寄存器”将刚才“TDR”复制的数据一位一位的送到Tx引脚
- 循环执行上述操作,直到总线将最后一个数据写入“发送数据寄存器*(TDR)”后,等待TC=1。表明最后一帧的传送已完成。
USART接收过程:
Rx引脚有数据输入时
- 首先使能接收USART_CR1的RE位置1
- Rx引脚移入数据的最低有效位,到“接收移位寄存器”
- 当接收移位寄存器8位满时,将数据一次性写入“接收数据寄存器(RDR)”(该操作将RXNE置1,即接收数据寄存器非空,总线可读取)
- 总线发现RXNE=1时,立即读取数据并将RXNE置0(接收期间每接收一个字节,RXNE都置1)
- 循环执行上面操作,直到Rx引脚将最后一字节数据传送入”接收数据寄存器(RDR)“后,等待总线读取完成。
2. 硬件连接
Rx
⇒
\Rightarrow
⇒Tx
Tx
⇒
\Rightarrow
⇒Rx
3. 软件设计
我们需要将PA9和PA10当作USART1的Tx和Rx引脚来使用,即IO不再是通用功能了,而是用到了IO引脚的复用功能。STM32将这种应用叫做“IO引脚复用”,也就是说PA9(Tx)将被配置为复用推挽输出模式,PA10(Rx)将被配置为复用输入模式。
3.1 IO引脚复用功能初始化
- IO引脚的复用功能
STM32F4有很多片内外设,这些外设的外部引脚都是由GPIO复用得来的。片内外设的功能引脚不是随意复用的,也就是说片内外设的功能引脚是特定在某个或者多个GPIO引脚上的。例如USART1的Tx引脚就固定在PA9/PB6上,Rx引脚固定在PA10/PB7上。其他外设的复用也类似。
3.2 串口配置步骤
-
使能串口时钟:使用外设USART1和USART6,因为它们挂载在APB2总线上,则用函数RCC_APB2PeriphClockCmd() 使能;使用外设USART2~USART5,则用函数RCC_APB1PeriphClockCmd() 使能。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);// 使能 USART1 时钟
-
配置串口使用的GPIO
- 使能GPIO时钟,USART1,使用PA9和PA10,则使能GPIOA的时钟。
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); // 使能 GPIOA 时钟
- 引脚复用功能映射,用函数GPIO_PinAFConfig() 设置引脚复用。
GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1); // PA9 GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1); // PA10
- 配置引脚工作模式
- PA9 ⇒ \Rightarrow ⇒Tx(复用推挽输出)
- PA10 ⇒ \Rightarrow ⇒Rx(复用上拉输入)
//Tx Rx GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; //PA9与PA10 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; //速度100MHz GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // 推挽复用输出 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉 GPIO_Init(GPIOA,&GPIO_InitStructure); //初始化PA9,PA10
-
配置串口
通过USART_init() 函数,配置串口1的工作模式等具体参数。USART_InitStructure.USART_BaudRate = 115200;// 波特率设置为115200 USART_InitStructure.USART_WordLength = USART_WordLength_8b;// 字长为8位数据格式 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_Cmd() 函数,使能配置好的串口USART_Cmd(USART1,ENABLE);
-
重定向printf()函数
首先我们需要知道printf()是C语言的标准输入输出stdio.h 中的函数,因此需要在工程中包含该头文件,并且在Keil中勾选上Use Micro Lib的选项
然后对printf()函数重定向,而这个函数是基于fputc()来写的,因此只需要对这个函数进行修改int fputc(int ch,FILE *f) { // 发送数据 USART_SendData(USART1,(uint_8t)ch); // 等待发送完毕 while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);// 查询发送数据寄存器为空标志位,SET了即为发送完成一帧 return ch; }
-
串口通信
然后我们就可以愉快的在工程中使用printf()进行串口输出了。
3.3 完整USART.c实现
下面给出一个完整的usart.c的实现,在这个程序中串口1用到的引脚是PB6和PB7,串口2用到的引脚是PA2和PA3。
#include "usart.h"
/// @brief 初始化串口1(PB6->Tx PB7->Rx)
/// @param baud 波特率
void USART_init(uint32_t baud)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 使能USART1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
// 使能GPIO时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
// 引脚复用功能
GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_USART1);
GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_USART1);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = baud;
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);
USART_Cmd(USART1, ENABLE);
}
void USART2_init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 使能USART2时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
// 使能GPIO时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
// 引脚复用功能
GPIO_PinAFConfig(GPIOA, GPIO_PinSource3, GPIO_AF_USART2);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_USART2);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = 9600;
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(USART2, &USART_InitStructure);
USART_Cmd(USART2, ENABLE);
}
// 重定向printf
int fputc(int ch, FILE *f)
{
USART_SendData(USART1,(uint8_t)ch);// 串口1发送数据
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);// 等待发送完成
return ch;
}