目录
- 一、UART协议简介
-
- 1.UART通信的基本原理
- 2.UART通信过程
- 3.UART的常见特点
- 4.UART通信的应用场景
- 5.补充
- 二、UART硬件结构
-
- 1.内部框图
- 2.设置:波特率、数据位、校验位、停止位
- 3.发送数据
- 4.接收数据
- 5.中断方式
- 三、UART串口中断模式配置说明
-
- 1.HAL库外设初始化MSP回调机制-USART
- 2.HAL库中断回调机制-USART
- 3.USART/UART异步通信配置
- 四、UART编程
-
- 1.查询方式
- 2.中断方式
- 3.DMA方式
- 4.DMA+IDLE方式
- 5.printf函数实现(需包含 #include "stdio.h")
一、UART协议简介
UART(Universal Asynchronous Receiver-Transmitter) 协议是一种常用的串行通信协议,用于计算机和外设之间进行数据传输。它是一种异步通信协议,意味着发送和接收数据时不需要共享时钟信号。UART通常用于短距离、低速的数据交换,广泛应用于嵌入式系统、传感器通信、串口设备等领域。
1.UART通信的基本原理
- 串行传输:数据通过一条传输线(TX)发送,一条接收线(RX)接收,数据位逐位传送。
- 异步传输:不需要发送方和接收方有共同的时钟信号,数据的发送和接收通过约定的波特率(Baud Rate)同步。
- 起始位、数据位和停止位:
- 起始位:在数据传输开始时,发送一位低电平信号(通常是0),以表明数据传输开始。
- 数据位:数据以二进制形式逐位传输,常见的数据位长度是5、6、7或8位,其中8位居多。
- 停止位:在数据传输完成后,发送一或多个高电平(通常是1)信号,表示数据传输结束。常见的停止位长度是1位或2位。
- 波特率:波特率是指每秒钟传输的数据位数。例如,9600、115200是常见的波特率值。发送方和接收方必须设置相同的波特率,确保数据正确传输。
2.UART通信过程
- 发送端将数据分解为多个数据位,每个数据位通过TX线按约定波特率发送出去。
- 接收端通过RX线接收数据位,并根据设定的波特率、起始位、数据位和停止位重新组装数据。
3.UART的常见特点
- 简单易用:由于不需要时钟信号,UART通信简单、硬件要求低。
- 低速传输:通常适用于短距离、低速的数据交换,传输速率取决于波特率。
- 全双工/半双工:可以支持全双工(同时发送和接收)或半双工(只能单向传输)通信。
- 低成本:硬件实现简单,通常只需要两条线(TX和RX)即可完成通信。
4.UART通信的应用场景
- 嵌入式系统:在微控制器、传感器、GPS模块、蓝牙模块等嵌入式设备之间进行数据交换。
- 串口通信:计算机与外部设备(如调制解调器、打印机、串口设备)之间的通信。
- 调试接口:许多开发板和嵌入式系统提供UART接口用于调试、日志输出等功能。
5.补充
-
起始位探测:自己的RX引脚之前是高电平状态,一旦检测到有一个低电平的跳变之后会发起16次的检测,如果前面7次检测到最少两次的低电平就说明有可能是低电平从而继续检测,在8、9、10三次中至少检测到两个低电平就认为接收到了起始位,下面便开始接收数据。
-
数据位探测:在1bit的时间中连续检测16次,如果中间8、9、10三次检测到两次及以上低电平则认为是低电平,检测到两次及以上高电平则认为是高电平。
-
TTL/COMS逻辑电平下传输’A’的波形:电压较低容易受到像静电这样的干扰
A的ASCII码为65,二进制表示是 01000001
-
RS-232逻辑电平下传输’A’的波形:提高电压以便提高抗干扰能力
-
校验位
- 发送方计算数据位中1的个数,如果是偶数个,则校验位设置为1,使得数据位和校验位的1的总数为奇数;如果是奇数个,则校验位设置为0,确保1的个数是奇数。
- 假设我们传输的数据是10101100,如果采用偶校验:数据位是10101100,其中1的个数是4(偶数),所以校验位应该是0,以保持1的个数为偶数;如果采用奇校验,校验位应该是1,因为这样可以使1的总数变为奇数(5个1)。
-
波特率:1秒内传输信号的状态数(波形数)。比特率:1秒内传输数据的bit数。如果一个波形能表示N个bit,那么波特率*N=比特率。
二、UART硬件结构
1.内部框图
在实际工作中不需要像发送起始位、数据位、停止位这样一位一位发送数据,有专门的硬件串口帮忙进行发送,只需要将数据写入对应寄存器中即可。
2.设置:波特率、数据位、校验位、停止位
-
USART_BRR (波特率寄存器):波特率通过设置 USART_BRR 寄存器来配置。该寄存器存储了波特率的分频值。
- DIV_Mantissa:波特率的整数部分。
- DIV_Fraction:波特率的小数部分。
-
设置数据位(Data Bits):数据位的设置由 USART_CR1 寄存器中的 M 位 (Bit 12)来控制。
- M = 0 表示 8 位数据。
- M = 1 表示 9 位数据。
-
设置停止位(Stop Bits):停止位的设置由 USART_CR2 寄存器中的STOP 位( Bits 13:12)来控制。
- 00 = 1 位停止位。
- 01 = 0.5 位停止位。
- 10 = 2 位停止位。
- 11 = 1.5 位停止位。
-
设置校验位(Parity):校验位的设置由 USART_CR1 寄存器中的PS位 ( Bit 9)和 PCE位 (Bit 10)来控制。
- PCE (Parity Enable):启用或禁用校验位。设置为 1 启用,设置为 0 禁用。
- PS (Parity Selection):设置校验类型:
- PS = 0:偶校验。
- PS = 1:奇校验。
3.发送数据
- 数据val写入到TDR寄存器中,该寄存器中的数值会移动到Shift寄存器中,然后Shift寄存器会自动将数据一位一位的发送出去。
unsigned int *p = &TDR; *p = 0x78;
- TDR寄存器同一时间只能容纳一个数据,只有数据被全部移动到Shift寄存器中才可以写下一个数据。USART_SR 寄存器中的TXE位 ( Bit 7)表示TDR寄存器是否空,1表示数据已经被发送到Shift寄存器。USART_SR 寄存器中的TC位 ( Bit 6)表示Shift移位寄存器中的一字节数据是否被一位一位的发送出去了,该位由硬件进行设置,1表示数据已经发送完成。
4.接收数据
- 数据从RX引脚一位一位的接收保存到接收Shift寄存器中,接收完成后将数据发送到RDR寄存器中,此时便可以从该寄存器中读取数据。
unsigned int *p = &RDR; val = *p;
- 只有当接收Shift寄存器接收完成并把数据发送到RDR寄存器中才有数据可以读。USART_SR 寄存器中的RXNE位 ( Bit 5)表示RDR寄存器非空,1表示数据已经准备好被读取,此时就可以从该寄存器中读取数据。
- TDR寄存器和RDR寄存器的地址是一样的,发送数据时操作的是TDR寄存器,接收数据时操作的是RDR寄存器。
5.中断方式
上述的发送和接收数据都是通过查询的方式进行,需要使用while循环来读取状态位,该查询方式会自动阻塞程序,直到数据发送或接收完成才会释放。而使用中断的方式进行数据的发送和接收时,只有当完成操作时才会产生中断,节省CPU时间。
-
相关寄存器
-
补充
- 在STM32F103C8T6型号的单片机串口硬件中没有环形缓冲区,如果不及时的读取RDR寄存器中的数据很有可能会被后续接收的数据覆盖掉从而丢失数据。如果在接收移位寄存器和RDR寄存器中间加上RxFIFO接收环形缓冲区那么就可以暂存一些数据,避免因为没有及时处理数据造成数据的丢失。
- 在发送移位寄存器和TDR寄存器中间也加上TxFIFO发送环形缓冲区可以一次性地将数据写入到环形缓冲区中从而提高发送的效率。
三、UART串口中断模式配置说明
1.HAL库外设初始化MSP回调机制-USART
HAL_UART_Init();//串口初始化
HAL_UART_MspInit();//调用MSP回调函数
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
GPIO_InitTypeDef gpio_init_struct;
if(huart->Instance == USART1) //如果是串口1,进行MSP初始化
{
/*1.使能USART1和对应IO时钟 2.初始化IO 3.使能USART1中断,设置优先级*/
}
}
2.HAL库中断回调机制-USART
//在.s启动文件中
USARTx_IRQHandler();//同步
UARTx_IRQHandler();//异步
//用户调用HAL库中断共用处理函数
HAL_USART_IRQHandler();
HAL_UART_IRQHandler();
//HAL库自己调用中断回调函数
HAL_UART_RxCpltCallback();//接收完成
HAL_UART_TxCpltCallback();//发送完成
3.USART/UART异步通信配置
1.配置串口工作参数
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart)
typedef struct
{
uint32_t BaudRate;//波特率
uint32_t WordLength;//字长
uint32_t StopBits;//停止位
uint32_t Parity;//奇偶校验位
uint32_t Mode;//UART模式
uint32_t HwFlowCtl;//硬件流设置---一般不用
uint32_t OverSampling;//过采样设置---一般不用
}UART_InitTypeDef;
/*示例--串口2初始化*/
void usart2_init(uint32_t bound)
{
uart2_handler.Instance = USART2;
uart2_handler.Init.BaudRate = bound; /*波特率*/
uart2_handler.Init.WordLength = UART_WORDLENGTH_8B; /*字长为8位数据格式*/
uart2_handler.Init.StopBits = UART_STOPBITS_1; /*一个停止位*/
uart2_handler.Init.Parity = UART_PARITY_NONE; /*无奇偶校验位*/
uart2_handler.Init.Mode = UART_MODE_TX_RX; /*收发模式*/
uart2_handler.Init.HwFlowCtl = UART_HWCONTROL_NONE; /*无硬件流控*/
HAL_UART_Init(&uart2_handler); /*使能UART2*/
}
2.串口底层初始化
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
/*
作用:使能USART和对应IO时钟 初始化IO 使能USART中断和设置优先级
参数:UART_HandleTypeDef 结构体类型的指针变量
注意:该函数为弱函数,需要自己重新编写
*/
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
GPIO_InitTypeDef gpio_init_struct;
if(huart->Instance==USART2) //如果是串口2
{
__HAL_RCC_GPIOA_CLK_ENABLE();//使能GPIOA时钟
__HAL_RCC_USART2_CLK_ENABLE();//使能USART2时钟
gpio_init_struct.Pin = GPIO_PIN_2;
gpio_init_struct.Mode = GPIO_MODE_AF_PP;//推挽式复用输出
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;//高速
HAL_GPIO_Init(GPIOA, &gpio_init_struct);//初始化PA2引脚
gpio_init_struct.Pin = GPIO_PIN_3;
gpio_init_struct.Mode = GPIO_MODE_AF_INPUT;//推挽式复用输入
gpio_init_struct.Pull = GPIO_PULLUP;//上拉
HAL_GPIO_Init(GPIOA, &gpio_init_struct);//初始化PA3引脚
//在main函数中会调用HAL_Init()来进行HAL库的初始化
//HAL_Init()中会设置中断优先级分组
//HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
HAL_NVIC_EnableIRQ(USART2_IRQn);//使能USART2中断
HAL_NVIC_SetPriority(USART2_IRQn, 3, 3); //设置中断优先级
}
}
3.开启串口异步接收中断
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
/*
作用:以中断的方式接收指定字节的数据
参数1:UART_HandleTypeDef 结构体类型的指针变量
参数2:指向接收数据缓冲区
参数3:要接收的数据大小,以字节为单位
*/
/*示例--串口2初始化*/
HAL_UART_Receive_IT(&uart2_handler, (uint8_t *)aRxBuffer2, 1);
该函数也是属于初始化的一部分,一般放在HAL_UART_Init(&uart2_handler);的下面
4.设置优先级,使能中断
已经在串口底层初始化HAL_UART_MspInit中完成
5.编写中断服务函数
//中断服务函数在启动文件.s中进行查找
DCD USART1_IRQHandler ; USART1
DCD USART2_IRQHandler ; USART2
DCD USART3_IRQHandler ; USART3
DCD EXTI15_10_IRQHandler ; EXTI Line 15..10
DCD USBWakeUp_IRQHandler ; USB Wakeup from suspend
/*示例*/
void USART2_IRQHandler(void)
{
HAL_UART_IRQHandler(&uart2_handler);//调用HAL库中断处理公用函数
//该函数会清除相关中断标志位并调用HAL_UART_RxCpltCallback
//再次开启接收中断
HAL_UART_Receive_IT(&uart2_handler, (uint8_t *)aRxBuffer2, 1);
}
每接收或发送一个字节的数据USART2_IRQHandler就会调用,当接收到设置的字节数时才会调用HAL_UART_Receive_IT,因此正常应该在HAL_UART_RxCpltCallback回调函数中再次开启接收中断。
6.串口数据发送
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
/*
作用:以阻塞的方式发送指定字节的数据
参数1:UART_HandleTypeDef 结构体类型的指针变量
参数2:指向发送的数据地址
参数3:要发送的数据大小,以字节为单位
参数4:设置超时时间,单位ms
*/
/*示例*/
HAL_UART_Transmit(&uart3_handler,ALY,m,2000);
while(__HAL_UART_GET_FLAG(&uart3_handler, UART_FLAG_TC) != 1);
HAL_UART_Transmit 会在发送完所有数据后返回,发送完成时 UART_FLAG_TC 标志会被自动设置。因此 while (__HAL_UART_GET_FLAG(&uart3_handler, UART_FLAG_TC) != 1);其实没有必要,因为 HAL_UART_Transmit 已经处理了这个逻辑。
7.编写串口中断回调处理函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
//在该函数中可以直接处理HAL_UART_Receive_IT中设置的接收缓冲区中的数据
//也可以操作相关标志位,在main函数中判断标志位来进行处理
}
四、UART编程
串口接线方式
1.查询方式
-
要发送数据时,先将数据写入TDR寄存器,然后再判断TDR寄存器为空再返回。也可以先判断TDR为空再写入,这样效率更高。
-
要读取数据时,先判断RDR寄存器非空,再读取RDR得到数据。
-
HAL_UART_Transmit 和HAL_UART_Receive 函数内部使用寄存器实现了1、2点的逻辑判断
-
查询方式只需要配置UART串口和引脚配置即可
- 使用HAL_UART_Init进行串口初始化
- 使用HAL_UART_MspInit进行引脚初始化
- HAL_UART_Init() 在内部会自动调用 HAL_UART_MspInit()
- 发送:HAL_UART_Transmit
- 接收:HAL_UART_Receive
-
HAL_UART_Transmit 函数发送数据流程—以发送“abcdefg”为例
- 用户调用 HAL_UART_Transmit 并传入发送缓冲区的地址( “abcdefg”)和数据长度( 7 字节)
- 在开始发送之前,函数会检查 UART 的状态寄存器,确保 UART 发送缓冲区空闲。通过检查 UART_FLAG_TXE标志,确认数据寄存器是否为空。如果为空,表示可以发送数据。
- 将第一个字节写入 USARTx->DR(UART 数据寄存器)
- 进入循环,等待每次发送完成。每次发送完一个字节后,HAL_UART_Transmit 会查询 UART_FLAG_TXE 标志,检查发送缓冲区是否空闲。如果为空,说明当前字节已被传送
- 然后,将下一个字节写入 USARTx->DR,继续执行直到所有字节都被发送完
- 一旦所有数据都发送完成,HAL_UART_Transmit 会等待 UART_FLAG_TC标志变为设置状态,表示所有字节都已传输完毕
- 发送完毕后,函数返回,表示数据传输完成
-
HAL_UART_Receive 函数接收数据流程—以接收5个字节数据为例
- 用户调用 HAL_UART_Receive,并传入接收缓冲区的地址和接收字节数
- 在开始接收之前,函数会检查 UART 接收缓冲区是否已经接收到数据。通过查询 UART_FLAG_RXNE标志来判断 UART 数据寄存器中是否有新数据
- 如果 UART_FLAG_RXNE 为 1,表示接收到一个字节的数据,此时程序从 USARTx->DR(接收数据寄存器)读取该字节并将其存入用户传入的接收缓冲区
- 如果数据还没有接收到,程序会继续循环查询 UART_FLAG_RXNE,直到接收到一个字节
- 当接收到一个字节后,程序会继续检查 UART_FLAG_RXNE,并将字节存入缓冲区,直到接收完指定字节数
- 当接收了所有指定的字节后,HAL_UART_Receive 返回,表示数据接收完成
-
超时时间作用
- 发送数据的过程:HAL_UART_Transmit 会通过轮询方式检查 UART 状态寄存器,确保数据缓冲区(TXD)可写。如果在超时时间内没有成功发送一个字节(即没有检测到 UART_FLAG_TXE 标志或者出现其他错误),函数会超时并返回 HAL_TIMEOUT 错误,停止发送操作
- 接收数据的过程:HAL_UART_Receive 会通过轮询方式检查 UART_FLAG_RXNE(接收缓冲区非空)标志,来判断是否接收到一个字节。如果在超时时间内没有接收到所需的字节,函数会返回 HAL_TIMEOUT 错误
- 超时时间是针对发送或者接收的总过程而言,每个字节的发送时间由 波特率 决定。例如,在 9600 bps 的波特率下,发送一个字节大约需要 1 毫秒左右(假设一个字节包含 10 位:1 起始位 + 8 数据位 + 1 停止位,计算方式是 10 bits / 9600 bps ≈ 1.04 ms)
下面的代码演示了HAL_UART_Transmit 和HAL_UART_Receive 的使用方式。HAL_UART_Transmit 的使用很简单,发送的过程正常不会遇到问题,需要注意的是调用HAL_UART_Transmit 时程序会暂停直到所有数据发送完成或者达到超时时间返回错误信息;调用HAL_UART_Receive函数时如果在超时时间内没有接收到指定字节数的数据,函数会返回错误信息,代码中使用了while循环来判断是否接收成功,总接收字节数为5,只有在超时时间范围内接收到了5个数据才会停止循环将接收到的数据打印出来。HAL_UART_Receive 函数会按照提供的缓冲区地址,逐个字节地将接收到的数据填充到该缓冲区中,每次调用 HAL_UART_Receive 时,它都会从缓冲区的起始位置(即地址 0)开始填充数据,所以如果调用HAL_UART_Receive 函数之后如果没有在超时时间内接收到指定字节数据或者发送数据时没有及时调用HAL_UART_Receive 来接收都会丢失数据。如果只接收一个字节的数据,一次发送两个字节,则不会丢失,因为RDR寄存器会暂存一个字节,发送多个字节则会丢失数据。如果想解决接收数据丢失的问题则需要使用中断的方式来接收数据。
usart.c
#include "usart.h"
UART_HandleTypeDef huart1;
void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
}
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {
0};
if(uartHandle->Instance==USART1)
{
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/**USART1 GPIO Configuration
PA9 ------> USART1_TX
PA10 ------> USART1_RX
*/
GPIO_InitStruct