USART串口通讯
USART简介
USART(universal synchronous asynchronous receiver transmitter),通用同步异步接收发射机,是一种全双工异步通信串行通讯方式,是STM32内部集成的硬件外设,以帧格式传输数据。搭配DMA进行多缓冲配置,可进行高速数据通信。
异步通信接线图如下
这里给出码元传输速率 R B R_B RB和信息传输速率 R b R_b Rb的定义
- 码元传输速率
亦称传码率、波特率,单位时间内传输了多少个码元 - 信息传输速率
亦称传信率、比特率,单位时间内传递多少比特数
在M进制中,每个码元携带
log
2
M
\log_2M
log2M比特的信息量,二者满足:
R
b
=
R
B
log
2
M
R_b = R_B \log_2M
Rb=RBlog2M,二进制中两者相等。
USART协议
下面根据串口数据帧格式进行说明,可配置为8位或9位长,如下图
参数
- 起始位:低电平为一个数据帧的开始
- 数据位:可配置为8位或9位,低位先行
- 校验位:可选择进行奇偶校验,也可不进行校验,位于最后一个数据位之后,停止位之前
- 停止位:高电平为一个数据帧的结束,长度可设置为0.5、1(默认)、1.5、2位宽
发射端
发送器根据是否设置校验位(即USART_CR1寄存器的M位)发送8位或9位的数据字,USART_CR1寄存器的发送使能位TE置1时,发送移位寄存器的数据会在TX引脚输出,相应的时钟脉冲输出到CK引脚。在数据传输过程中为保证不丢失数据TE位不应被重置。
实现步骤:1、通过USART_CR1寄存器中的UE位使能USART;2、通过USART_CR1寄存器中的M位设置数据位长度;3、通过USART_CR2寄存器设置停止位长度;4、通过USART_CR3寄存器中的DMAT位选择使能DMA;5、通过USART_BRR寄存器设置波特率;6、通过USART_CR1中的TE位发送空闲帧作为第一次传输;7、将要发送的数据写入USART_DR寄存器(这回自动清除TXE位);8、在写完最后一个数据到USART_DR寄存器中,等待TC=1。
简述为:TXE=1,先发送一个空闲帧,数据就送入TDR同时TXE=0;等到开始位时TDR内数据送入移位寄存器,恢复TXE=1;TXE=1,下一个数据送入TDR同时TXE=0。依次类推。
接收端
接收器根据校验位(即USART_CR1寄存器的M位)接收8位或9位的数据字,前提是保证和发送器相同的波特率,并且要求每次采样的位置正好处于每一位的正中间,同时还要对噪声有一定的判断能力。起始位侦测如下
检测到正确的起始位后,就是进行数据接收。实现步骤:1、通过USART_CR1寄存器中的UE位使能USART;2、通过USART_CR1寄存器中的M位设置数据位长度;3、通过USART_CR2寄存器设置停止位长度;4、通过USART_CR3寄存器中的DMAT位选择使能DMA;5、通过USART_BRR寄存器设置波特率;6、通过USART_CR1寄存器中的RE位使能USART接收,RX引脚检测起始位。
当完整收到一个字节数据时,RXNE位置1、如果使能了RXNEIE将会产生中断、发生错误将产生错误标志位、在配置了DMA时RXNE由DMA清除、未配置DMA时RXNE是在RDR读完后自动清除。
选择合适的过采样法
为保证接收端接收到正确的数据帧,需配置合适的过采样技术,通过USART_CR1寄存器这OVER8位进行选择。
- 选择 8(OVER8=1)的过采样以实现更高的速度(最高 fPCLK/8),在这种情况下,接收机对时钟偏差的最大容差会降低
- 选择过采样 16 (OVER8=0) 以增加接收器对时钟偏差的容差。在这种情况下,最大速度限制为最大 fPCLK/16
小数波特率
波特率指数据信号对载波的调制速率,计算公式如下
波特率 = f C K 8 × ( 2 − O V E R 8 ) × U S A R T D I V 波特率 = \frac{f_{CK}}{8\times (2 - OVER8)\times USARTDIV} 波特率=8×(2−OVER8)×USARTDIVfCK
其中 f C K f_{CK} fCK是USART时钟即所在总线时钟频率,USARTDIV是一个存放在USART_BRR寄存器中的无符号定点数。其中DIV_Mantissa[11:0]位定义USARTDIV的整数部分, DIV_Fraction[3:0]位定义USARTDIV的小数部分,DIV_Fraction[3]位只有在OVER8位为0时有效,否则必须清零。
校验位控制
通过USART_CR1寄存器中的PCE位启用校验。分为奇校验和偶校验,如果使用了奇校验,则9位数据中会出现奇数个1;如果使用了偶校验,则9位数据中会出现偶数个1。
配置DMA
USART能够使用DMA进行连续通信。Rx 缓冲区和 Tx 缓冲区的 DMA 请求是独立生成的。
- 发送端配置DMA
通过USART_CR3寄存器中的DMAT位配置DMA进行发送。TXE位置位时,SRAM内数据都会通过DMA外设加载到TDR中。通过以下流程配置- 在DMA控制寄存器中写入TDR地址,将其作为传输目的地
- 在DMA控制寄存器中写入SRAM地址,将其作为传输起点
- 配置DMA其他配置
- 接收端配置DMA
通过USART_CR3寄存器中的DMAR位配置DMA进行接收。RXNE位置位时,RDR内数据都会通过DMA外设加载到SRAM中。通过以下流程配置- 在DMA控制寄存器中写入RDR地址,将其作为传输起始地
- 在DMA控制寄存器中写入SRAM地址,将其作为传输目的地
- 配置DMA其他配置
代码实现
- 单字节串口发送
- 开启USART时钟和GPIO引脚时钟
- 配置GPIO引脚
- 配置USART
- 使能USART
- 功能函数编写
代码如下
/*
**********************************************************************************
* @brief USART1串口初始化
* PA9是USART1_TX,PA10是USART1_RX
* GPIO是AHB1总线,USART1是APB2总线
* @param none
* @return none
* @use Serial_Init()
**********************************************************************************
*/
void Serial_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
//USART1对应引脚复用映射
GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);
//配置PA9、PA10引脚
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//配置USART1
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600; //波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,这里不选用
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //双工
USART_InitStructure.USART_Parity = USART_Parity_No; //校验位,这里不选用
USART_InitStructure.USART_StopBits = USART_StopBits_1; //1位长的停止位
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长8位
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
}
/*
**********************************************************************************
* @brief 发送单个字节数据
* @param 一个十六进制数据
* @return none
* @use Serial_SendOneByte(uint8_t Byte)
**********************************************************************************
*/
void Serial_SendOneByte(uint8_t Byte)
{
//数据先被写入TDR,然后发到发送移位寄存器才能通过GPIO口输出
USART_SendData(USART1, Byte);
//此处作等待标志位之用,即发送数据寄存器空标志位,标志位会自动置0
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) != SET);
}
/*
**********************************************************************************
* @brief 发送字符串
* @param 一个十六进制数组
* @return none
* @use Serial_SendArray(uint8_t *String)
**********************************************************************************
*/
void Serial_SendString(uint8_t *String)
{
for(uint8_t i = 0; String[i] != '\0'; i++)
{
Serial_SendOneByte(String[i]);
}
}
- 单字节串口接收
有两种接收方法,查询法和中断法
- 使用查询
不需要配置中断,需要在主函数中不断判断RXNE标志位。
if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == SET)
{
RxData = USART_ReceiveData(USART1);
Serial_SendOneByte(RxData);
}
- 使用中断
使用中断可使程序更加灵活
代码如下
#include "Serial.h"
/*
**********************************************************************************
* @brief USART1串口初始化
* PA9是USART1_TX,PA10是USART1_RX
* GPIO是AHB1总线,USART1是APB2总线
* @param none
* @return none
* @use Serial_Init()
**********************************************************************************
*/
void Serial_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
//USART1对应引脚复用映射
GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);
//配置PA9、PA10引脚
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//配置USART1
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600; //波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,这里不选用
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //双工
USART_InitStructure.USART_Parity = USART_Parity_No; //校验位,这里不选用
USART_InitStructure.USART_StopBits = USART_StopBits_1; //1位长的停止位
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长8位
USART_Init(USART1, &USART_InitStructure);
//配置USART中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //配置USART接收数据寄存器非空中断
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //选择第二组中断
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //配置USART1中断通道
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能NVIC
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //先占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //子优先级
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART1, ENABLE);
}
/*
**********************************************************************************
* @brief 发送单个字节数据
* @param 一个十六进制数据
* @return none
* @use Serial_SendOneByte(uint8_t Byte)
**********************************************************************************
*/
void Serial_SendOneByte(uint8_t Byte)
{
//数据先被写入TDR,然后发到发送移位寄存器才能通过GPIO口输出
USART_SendData(USART1, Byte);
//此处作等待标志位之用,即发送数据寄存器空标志位,标志位会自动置0
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) != SET);
}
/*
**********************************************************************************
* @brief 发送字符串
* @param 一个十六进制数组
* @return none
* @use Serial_SendArray(uint8_t *String)
**********************************************************************************
*/
void Serial_SendString(uint8_t *String)
{
for(uint8_t i = 0; String[i] != '\0'; i++)
{
Serial_SendOneByte(String[i]);
}
}
/*
**********************************************************************************
* @brief 发送数字
* @param 数字,以及数字长度
* @return none
* @use Serial_SendNumber(uint32_t Number, uint8_t Length)
**********************************************************************************
*/
//求x的y次方
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y --)
{
Result *= X;
}
return Result;
}
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
//需要把Number的个位、十位、百位..以十进制拆开,然后换成字符数字对应的数据依次发出去
for(uint8_t i = 0; i < Length; i ++)
{
Serial_SendOneByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
}
}
/*
**********************************************************************************
* @brief printf()函数重定向到串口,需要打开MicroLIB
* @param int ch, FILE *f
* @return none
* @use printf("Num = %d\n", 666)
**********************************************************************************
*/
//重定向fputc函数到串口,通过串口发送。
//但此方法printf函数只能用于一个串口
//fputc是printf的底层
int fputc(int ch, FILE *f)
{
Serial_SendOneByte(ch);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
return ch;
}
uint8_t Serial_RxFlag; //读取标志位
uint8_t Serial_RxData; //读取数据
/*
**********************************************************************************
* @brief 是否读取数据的标志位
* @param none
* @return 1表示正常读取,0表示读取失败
* @use Serial_GetRxFlag()
**********************************************************************************
*/
uint8_t Serial_GetRxFlag()
{
if(Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0;
}
/*
**********************************************************************************
* @brief 返回所读取的数据
* @param none
* @return 所读取的一个字节数据
* @use Serial_GetRxData()
**********************************************************************************
*/
uint8_t Serial_GetRxData()
{
return Serial_RxData;
}
/*
**********************************************************************************
* @brief 中断函数
* @param none
* @return none
* @use 中断事件触发会自动跳转到此函数
**********************************************************************************
*/
void USART1_IRQHandler()
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
Serial_RxData = USART_ReceiveData(USART1);
Serial_RxFlag = 1;
USART_ClearITPendingBit(USART1, USART_IT_RXNE); //如果正常读取RDR数据会自动清除标志位
}
}