UART
UART
1. 理论知识
1.1 通讯常见概念
-
串行通信和并行通信分别指什么?
串行通信是按位传输,一次传输一位。并行同学是多位同时传输。串行通信 并行通信 通信距离 远 近 抗干扰能力 强 弱 传输速率 弱 高 成本 低 高 -
单工、半双工和全双工的区别。
三者主要是描述通信方向的不同名称 通信方向 单工 数据只能单向传输 半双工 数据可以从A到B,也可以从B到A,但同一时间只能一个方向 全双工 同一时间,数据可以A到B,也可以B到A -
同步通信和异步通信的区别。
同步通信在数据传输过程中要与时钟信号一一对应,而异步通信两边各自根据约定好的频率发送和接收数据。同步通信会比异步通信多一根信号线。
1.2 串口通信协议参数
串口通信的定义等见博客:https://www.cnblogs.com/yangyang13/p/18664776
此篇主要介绍STM32的串口控制与应用。
USART工作流程如下图:

-
功能引脚说明
TX:发送数据的输出引脚;
RX:接收数据的输入引脚;
SW_RX:数据接收引脚,只用于单线和智能卡模式,属于内部引脚,无外部引脚;
nRTS:请求发送(Request to Send),n表示低电平有效。使能RTS流控制,如果USART做好接收数据准备,nRTS就会变成低电平;接收寄存器装满时,就把nTRS置为高电平。仅适用硬件流控制。
nCTS:清除发送(Clear to Send),n低电平有效。使能RTS流控制,发送器在发送下一帧数据前检测nCTS。如果为低电平,则可以发送数据;高电平则在发送完当前数据帧后停止发送。 -
波特率产生
发送器和接收器波特率相同,通过设置BRR寄存器来实现

APB2一般是72MHz,APB1是36MHz

需要的波特率是115200,则对应的分频值应该是:39.0625,把这个值写入到BRR寄存器中。39.0625的小数部分:0.0625 * 16 = 1, 整数部分是:39(0x27)。

所以写入到BRR寄存器的值是:0x0271。
3.串口相关寄存器的配置。主要寄存器如下图:
串口控制寄存器



数据寄存器


状态寄存器



实现中断控制串口相关寄存器


2. 实践验证
2.1 寄存器方式
验证的硬件环境:stm32最小系统板+串口板
验证一: 轮询的方式收发
实现串口接收数据后原样返回,效果如下图:

实现逻辑(关键):
- 串口的初始化。
开启相关时钟-》配置IO工作模式-》配置串口参数(BRR波特率寄存器,CR控制寄存器) - 串口的接收和发送。
发送等待 USART_SR_TXE 寄存器为空,接收等待 USART_SR_RXNE 寄存器非空。接收字符串时还要通过 USART_SR_IDLE 判断总线是否空闲。
USART.h
#ifndef USART_H
#define __USART_H__
#include "stm32f10x.h"
void Driver_USART1_Init(void);
void Driver_USART1_SendChar(uint8_t byte);
void Driver_USART1_SendString(uint8_t *str, uint16_t len);
uint8_t Driver_USART1_ReceiveChar(void);
void Driver_USART1_ReceiveString(uint8_t buff[], uint8_t *len);
#endif
USART.c
#include "USART.h"
/**
* @description: 初始化串口1
*/
void Driver_USART1_Init(void)
{
/* 1. 开启时钟 */
/* 1.1 串口1外设的时钟 */
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
/* 1.2 GPIO时钟 */
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
/* 2. 配置GPIO引脚的工作模式 PA9=Tx(复用推挽 CNF=10 MODE=11) PA10=Rx(浮空输入 CNF=01 MODE=00)*/
GPIOA->CRH |= GPIO_CRH_CNF9_1;
GPIOA->CRH &= ~GPIO_CRH_CNF9_0;
GPIOA->CRH |= GPIO_CRH_MODE9;
GPIOA->CRH &= ~GPIO_CRH_CNF10_1;
GPIOA->CRH |= GPIO_CRH_CNF10_0;
GPIOA->CRH &= ~GPIO_CRH_MODE10;
/* 3. 串口的参数配置 */
/* 3.1 配置波特率 115200 */
USART1->BRR = 0x271;
/* 3.2 串口使能,并使能接收和发送 */
USART1->CR1 |= USART_CR1_UE;
USART1->CR1 |= USART_CR1_TE;
USART1->CR1 |= USART_CR1_RE;
/* 3.3 配置一个字的长度 8位 */
USART1->CR1 &= ~USART_CR1_M;
/* 3.4 配置不需要校验位 */
USART1->CR1 &= ~USART_CR1_PCE;
/* 3.5 配置停止位的长度为1个停止位 */
USART1->CR2 &= ~USART_CR2_STOP;
}
/**
* @description: 发送一个字节
* @param {uint8_t} byte 要发送的字节
*/
void Driver_USART1_SendChar(uint8_t byte)
{
/* 1. 等待发送寄存器为空 */
while ((USART1->SR & USART_SR_TXE) == 0)
;
/* 2. 数据写出到数据寄存器 */
USART1->DR = byte;
}
/**
* @description: 发送一个字符串
* @param {uint8_t} *str 要发送的字符串
* @param {uint16_t} len 字符串中字节的长度
* @return {*}
*/
void Driver_USART1_SendString(uint8_t *str, uint16_t len)
{
for (uint16_t i = 0; i < len; i++)
{
Driver_USART1_SendChar(str[i]);
}
}
/**
* @description: 接收一个字节的数据
* @return {*} 接收到的字节
*/
uint8_t Driver_USART1_ReceiveChar(void)
{
/* 等待数据寄存器非空 */
while ((USART1->SR & USART_SR_RXNE) == 0)
;
return USART1->DR;
}
/**
* @description: 接收变长数据.接收到的数据存入到buff中
* @param {uint8_t} buff 存放接收到的数据
* @param {uint8_t} *len 存放收到的数据的字节的长度
*/
void Driver_USART1_ReceiveString(uint8_t buff[], uint8_t *len)
{
uint8_t i = 0;
while (1)
{
// 等待接收非空
while ((USART1->SR & USART_SR_RXNE) == 0)
{
//在等待期间, 判断是否收到空闲帧
if (USART1->SR & USART_SR_IDLE)
{
*len = i;
return;
}
}
buff[i] = USART1->DR;
i++;
}
}
main.c
#include "USART.h"
#include "Delay.h"
#include "string.h"
uint8_t buff[100] = {0};
uint8_t len = 0;
int main()
{
Driver_USART1_Init();
// Driver_USART1_SendChar('a');
while (1)
{
// uint8_t *str = "Hello yangyang!\r\n";
// Driver_USART1_SendString(str, strlen((char *)str));
/* uint8_t *str = "yangyang\r\n";
Driver_USART1_SendString(str, strlen((char *)str));
Delay_s(1); */
// uint8_t c = Driver_USART1_ReceiveChar();
// Driver_USART1_SendChar(c);
Driver_USART1_ReceiveString(buff, &len);
Driver_USART1_SendString(buff, len);
}
}
验证二:中断方式收发
实现串口接收数据后原样返回,效果如下图:

实现逻辑(关键):
- 在USART初始化时使能中断,设置中断模式和优先级
- 在中断响应函数函数中判断中断触发类型,完成对应的反应,利用标志位避免在中断中进行耗时类操作。
UASRT.h
#ifndef USART_H
#define __USART_H__
#include "stm32f10x.h"
void Driver_USART1_Init(void);
void Driver_USART1_SendChar(uint8_t byte);
void Driver_USART1_SendString(uint8_t *str, uint16_t len);
uint8_t Driver_USART1_ReceiveChar(void);
void Driver_USART1_ReceiveString(uint8_t buff[], uint8_t *len);
#endif
USART.c
#include "USART.h"
/**
* @description: 初始化串口1
*/
void Driver_USART1_Init(void)
{
/* 1. 开启时钟 */
/* 1.1 串口1外设的时钟 */
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
/* 1.2 GPIO时钟 */
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
/* 2. 配置GPIO引脚的工作模式 PA9=Tx(复用推挽 CNF=10 MODE=11) PA10=Rx(浮空输入 CNF=01 MODE=00)*/
GPIOA->CRH |= GPIO_CRH_CNF9_1;
GPIOA->CRH &= ~GPIO_CRH_CNF9_0;
GPIOA->CRH |= GPIO_CRH_MODE9;
GPIOA->CRH &= ~GPIO_CRH_CNF10_1;
GPIOA->CRH |= GPIO_CRH_CNF10_0;
GPIOA->CRH &= ~GPIO_CRH_MODE10;
/* 3. 串口的参数配置 */
/* 3.1 配置波特率 115200 */
USART1->BRR = 0x271;
/* 3.2 串口使能,并使能接收和发送 */
//USART1->CR1 |= USART_CR1_UE;
USART1->CR1 |= USART_CR1_TE;
USART1->CR1 |= USART_CR1_RE;
/* 3.3 配置一个字的长度 8位 */
USART1->CR1 &= ~USART_CR1_M;
/* 3.4 配置不需要校验位 */
USART1->CR1 &= ~USART_CR1_PCE;
/* 3.5 配置停止位的长度为1个停止位 */
USART1->CR2 &= ~USART_CR2_STOP;
/* 3.6 使能串口的各种中断 */
USART1->CR1 |= USART_CR1_RXNEIE; /* 接收非空中断 */
USART1->CR1 |= USART_CR1_IDLEIE; /* 空闲中断 */
/* 4. 配置NVIC */
/* 4.1 配置优先级组 */
NVIC_SetPriorityGrouping(3);
/* 4.2 设置优先级 */
NVIC_SetPriority(USART1_IRQn, 2);
/* 4.3 使能串口1的中断 */
NVIC_EnableIRQ(USART1_IRQn);
/* 4. 使能串口 */
USART1->CR1 |= USART_CR1_UE;
}
/**
* @description: 发送一个字节
* @param {uint8_t} byte 要发送的字节
*/
void Driver_USART1_SendChar(uint8_t byte)
{
/* 1. 等待发送寄存器为空 */
while ((USART1->SR & USART_SR_TXE) == 0)
;
/* 2. 数据写出到数据寄存器 */
USART1->DR = byte;
}
/**
* @description: 发送一个字符串
* @param {uint8_t} *str 要发送的字符串
* @param {uint16_t} len 字符串中字节的长度
* @return {*}
*/
void Driver_USART1_SendString(uint8_t *str, uint16_t len)
{
for (uint16_t i = 0; i < len; i++)
{
Driver_USART1_SendChar(str[i]);
}
}
/**
* @description: 接收一个字节的数据
* @return {*} 接收到的字节
*/
uint8_t Driver_USART1_ReceiveChar(void)
{
/* 等待数据寄存器非空 */
while ((USART1->SR & USART_SR_RXNE) == 0)
;
return USART1->DR;
}
/**
* @description: 接收变长数据.接收到的数据存入到buff中
* @param {uint8_t} buff 存放接收到的数据
* @param {uint8_t} *len 存放收到的数据的字节的长度
*/
void Driver_USART1_ReceiveString(uint8_t buff[], uint8_t *len)
{
uint8_t i = 0;
while (1)
{
// 等待接收非空
while ((USART1->SR & USART_SR_RXNE) == 0)
{
//在等待期间, 判断是否收到空闲帧
if (USART1->SR & USART_SR_IDLE)
{
*len = i;
return;
}
}
buff[i] = USART1->DR;
i++;
}
}
/* 缓冲接收到的数据 */
uint8_t buff[100] = {0};
/* 存储接收到的字节的长度 */
uint8_t len = 0;
uint8_t isToSend = 0;
void USART1_IRQHandler(void)
{
/* 数据接收寄存器非空 */
if (USART1->SR & USART_SR_RXNE)
{
// 对USART_DR的读操作可以将接收非空的中断位清零。 所以不用单独清除了.
//USART1->SR &= ~USART_SR_RXNE;
buff[len] = USART1->DR;
len++;
}
else if (USART1->SR & USART_SR_IDLE)
{
/* 清除空闲中断标志位: 先读sr,再读dr.就可以实现清除了 */
USART1->SR;
USART1->DR;
/* 变长数据接收完毕 */
//Driver_USART1_SendString(buff, len);
isToSend = 1;
/* 把接收字节的长度清0 */
// len = 0;
}
}
main.c
#include "USART.h"
/* 缓冲接收到的数据 */
extern uint8_t buff[100];
/* 存储接收到的字节的长度 */
extern uint8_t len;
extern uint8_t isToSend;
int main()
{
Driver_USART1_Init();
while (1)
{
if(isToSend){
Driver_USART1_SendString(buff, len);
isToSend = 0;
len = 0;
}
}
}
验证三: 重定向printf

实现逻辑(关键):
printf实现的底层时调用fputc函数,所以要重写fputc函数。
在usart.h中引入stdio.h, usart.c添加fputc定义,同时在keil设置包含c语言库

USART.h
#ifndef USART_H
#define USART_H
#include "stm32f10x.h"
#include "stdio.h"
void Driver_USART1_Init(void);
void Driver_USART1_SendChar(uint8_t byte);
void Driver_USART1_SendString(uint8_t *str, uint16_t len);
uint8_t Driver_USART1_ReceiveChar(void);
void Driver_USART1_ReceiveString(uint8_t buff[], uint8_t *len);
#endif
USART.c
#include "USART.h"
/**
* @description: 初始化串口1
*/
void Driver_USART1_Init(void)
{
/* 1. 开启时钟 */
/* 1.1 串口1外设的时钟 */
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
/* 1.2 GPIO时钟 */
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
/* 2. 配置GPIO引脚的工作模式 PA9=Tx(复用推挽 CNF=10 MODE=11) PA10=Rx(浮空输入 CNF=01 MODE=00)*/
GPIOA->CRH |= GPIO_CRH_CNF9_1;
GPIOA->CRH &= ~GPIO_CRH_CNF9_0;
GPIOA->CRH |= GPIO_CRH_MODE9;
GPIOA->CRH &= ~GPIO_CRH_CNF10_1;
GPIOA->CRH |= GPIO_CRH_CNF10_0;
GPIOA->CRH &= ~GPIO_CRH_MODE10;
/* 3. 串口的参数配置 */
/* 3.1 配置波特率 115200 */
USART1->BRR = 0x271;
/* 3.2 串口使能,并使能接收和发送 */
//USART1->CR1 |= USART_CR1_UE;
USART1->CR1 |= USART_CR1_TE;
USART1->CR1 |= USART_CR1_RE;
/* 3.3 配置一个字的长度 8位 */
USART1->CR1 &= ~USART_CR1_M;
/* 3.4 配置不需要校验位 */
USART1->CR1 &= ~USART_CR1_PCE;
/* 3.5 配置停止位的长度为1个停止位 */
USART1->CR2 &= ~USART_CR2_STOP;
/* 3.6 使能串口的各种中断 */
USART1->CR1 |= USART_CR1_RXNEIE; /* 接收非空中断 */
USART1->CR1 |= USART_CR1_IDLEIE; /* 空闲中断 */
/* 4. 配置NVIC */
/* 4.1 配置优先级组 */
NVIC_SetPriorityGrouping(3);
/* 4.2 设置优先级 */
NVIC_SetPriority(USART1_IRQn, 2);
/* 4.3 使能串口1的中断 */
NVIC_EnableIRQ(USART1_IRQn);
/* 4. 使能串口 */
USART1->CR1 |= USART_CR1_UE;
}
/**
* @description: 发送一个字节
* @param {uint8_t} byte 要发送的字节
*/
void Driver_USART1_SendChar(uint8_t byte)
{
/* 1. 等待发送寄存器为空 */
while ((USART1->SR & USART_SR_TXE) == 0)
;
/* 2. 数据写出到数据寄存器 */
USART1->DR = byte;
}
/**
* @description: 发送一个字符串
* @param {uint8_t} *str 要发送的字符串
* @param {uint16_t} len 字符串中字节的长度
* @return {*}
*/
void Driver_USART1_SendString(uint8_t *str, uint16_t len)
{
for (uint16_t i = 0; i < len; i++)
{
Driver_USART1_SendChar(str[i]);
}
}
/**
* @description: 接收一个字节的数据
* @return {*} 接收到的字节
*/
uint8_t Driver_USART1_ReceiveChar(void)
{
/* 等待数据寄存器非空 */
while ((USART1->SR & USART_SR_RXNE) == 0)
;
return USART1->DR;
}
/**
* @description: 接收变长数据.接收到的数据存入到buff中
* @param {uint8_t} buff 存放接收到的数据
* @param {uint8_t} *len 存放收到的数据的字节的长度
*/
void Driver_USART1_ReceiveString(uint8_t buff[], uint8_t *len)
{
uint8_t i = 0;
while (1)
{
// 等待接收非空
while ((USART1->SR & USART_SR_RXNE) == 0)
{
//在等待期间, 判断是否收到空闲帧
if (USART1->SR & USART_SR_IDLE)
{
*len = i;
return;
}
}
buff[i] = USART1->DR;
i++;
}
}
/* 缓冲接收到的数据 */
uint8_t buff[100] = {0};
/* 存储接收到的字节的长度 */
uint8_t len = 0;
uint8_t isToSend = 0;
void USART1_IRQHandler(void)
{
/* 数据接收寄存器非空 */
if (USART1->SR & USART_SR_RXNE)
{
// 对USART_DR的读操作可以将接收非空的中断位清零。 所以不用单独清除了.
//USART1->SR &= ~USART_SR_RXNE;
buff[len] = USART1->DR;
len++;
}
else if (USART1->SR & USART_SR_IDLE)
{
/* 清除空闲中断标志位: 先读sr,再读dr.就可以实现清除了 */
USART1->SR;
USART1->DR;
/* 变长数据接收完毕 */
//Driver_USART1_SendString(buff, len);
isToSend = 1;
/* 把接收字节的长度清0 */
// len = 0;
}
}
int fputc(int c, FILE *file)
{
Driver_USART1_SendChar(c);
return c;
}
main.c
#include "USART.h"
#include "Delay.h"
int main()
{
Driver_USART1_Init();
while (1)
{
printf("hello Yangyang\r\n");
Delay_ms(500);
}
}
2.2 库函数方式
验证一: 轮询的方式收发
实现串口接收数据后原样返回,效果如下图:

实现逻辑(关键):
除了配置debug和时钟等基础功能外,还要在CubeMX配置USART相关参数,然后再在生成的工程中实现想要的功能逻辑。
配置过程:




添加功能代码
uint8_t buff[10];
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
while (1)
{
/* 从串口读取数据:
参数1 指定的串口
参数2:存储读取到的数据
参数3:一共接收多少条数据
参数4:超时时间
*/
if (HAL_UART_Receive(&huart1, buff, 10, HAL_MAX_DELAY) == HAL_OK)
{
// 把收到的数据原封不动的发出去
HAL_UART_Transmit(&huart1, buff, 10, HAL_MAX_DELAY);
}
}
}
验证二:中断方式收发定长数据和变长数据
实现逻辑(关键):
启用串口中断,两种逻辑思路。
1.在中断中实现串口收发。(中断中耗时和系统资源增加)
2.在中断中改变标志位,在主函数中循环判断标志位,完成收发。(主函数需要放置判断标志位到while循环)
实现串口接收数据后原样返回,效果如下图:
定长数据:

变长数据:

添加功能代码
方法一:在中断中设置标志位:
main.c
/* USER CODE BEGIN 2 */
// HAL_UART_Receive_IT(&huart1, buff, 10);
HAL_UARTEx_ReceiveToIdle_IT(&huart1, rxBuff, 1000);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
if (isOver)
{
// HAL_UART_Transmit(&huart1, buff, 10, HAL_MAX_DELAY);
HAL_UART_Transmit(&huart1, rxBuff, size, HAL_MAX_DELAY);
isOver = 0;
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
it.c
/* USER CODE BEGIN 1 */
extern uint8_t isOver;
extern uint16_t size;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
isOver = 1;
}
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
isOver = 1;
size = Size;
}
/* USER CODE END 1 */
方法二:在中断中发送和接收:
定长数据
uint8_t buff[1]; // 接收缓冲, 一次接受一个字节的数据
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart1.Instance == USART1)
{
HAL_UART_Transmit(&huart1, buff, 1, HAL_MAX_DELAY);
HAL_UART_Receive_IT(&huart1, buff, 1); // 继续接收
}
}
int main(void)
{
HAL_Init();
MX_GPIO_Init();
MX_USART1_UART_Init();
/* 用中断的方式接收一个字节的数据 */
HAL_UART_Receive_IT(&huart1, buff, 1);
while (1)
{
}
}
变长数据
/* USER CODE BEGIN 0 */
uint8_t rxBuff[1000]; // 接收缓冲区
// Size 是实际接收的数据的长度
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart1.Instance == USART1)
{
HAL_UART_Transmit(&huart1, rxBuff, Size, HAL_MAX_DELAY);
HAL_UARTEx_ReceiveToIdle_IT(&huart1, rxBuff, 1000);
}
}
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
/* 当接收到1000个字符或者碰到空闲帧, 则接收结束 */
HAL_UARTEx_ReceiveToIdle_IT(&huart1, rxBuff, 1000);
while (1)
{
}
}
验证三: 重定向printf
实现效果:

实现逻辑(关键):
cubmx创建完工程后,在keil界面添加c语言简洁库,然后在usart.h头文件中添加stdio.h,最后在usart.c重写fputc感受,在函数内调用串口输出函数。
关键代码:
usart.c
/* USER CODE BEGIN 1 */
int fputc(int ch, FILE *f)
{
/* 发送一个字节数据到串口DEBUG_USART */
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
return (ch);
}
/* USER CODE END 1 */
usart.h
/* Includes ------------------------------------------------------------------*/
#include "main.h"
/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */
main.c
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
printf("Hello World!\r\n");
int n = 373737;
printf("n = %d\r\n", n);
补充
HAL库函数中,调用USART函数的说明,来之 stm32f1xx_hal_uart.c


接收与发送字符
/* 从串口读取数据:
参数1 指定的串口
参数2:存储读取到的数据
参数3:一共接收多少条数据
参数4:超时时间
*/
HAL_UART_Receive(&huart1, buff, 10, HAL_MAX_DELAY)
HAL_UART_Transmit(&huart1, buff, 10, HAL_MAX_DELAY)
接收到定长字符触发中断,buff实际数据的地址,“1”传输的数据大小
HAL_UART_Receive_IT(&huart1, buff, 1)
接收定长数据中断的响应
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
接收不定长中断的触发
HAL_UARTEx_ReceiveToIdle_IT(&huart1, rxBuff, 1000);
接收不定长中断的响应
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
printf重定向,关键在重写fputc函数,需要引入stdio.h,要在keil的Target界面选上Use MicroLIB

浙公网安备 33010602011771号