芯片型号:STM32F103RC
软件开发包:标准外设库
一、USART 基本结构
如图,最左边是波特率发生器,用于产生约定的通信速率,时钟来源是 PCLK2/1。
经过波特率发生器分频后,产生的时钟通向发送控制器和接收控制器,发送控制器和接收控制器用于控制发送移位和接收移位。
对于发送,由发送数据寄存器和发送移位寄存器的配合,将数据一位一位地移出去,通过 GPIO 的复用输出,输出到 TX 引脚,产生串口协议规定的波形,当数据由数据寄存器转到移位寄存器时,会置一个 TXE 的标志位,检查这个标志位,就可以直到是不是可以写下一个数据了。
对于接收,也是一样的,RX 引脚的波形,通过 GPIO 输入,在接收控制器的控制下,一位一位地移入接收移位寄存器,移完一帧数据后,数据就会统一转运到接收数据寄存器,在转移的同时置一个 RXNE 标志位,检查这个标志位,就可以直到是不是收到数据了。同时这个标志位也可以申请中断,这样就可以在收到数据时直接进入中断函数,然后快速地读取和保存数据。
最后,右下角是一个开关控制。
二、USART 初始化结构体详解
typedef struct {
uint32_t USART_BaudRate; // 波特率
uint16_t USART_WordLength; // 字长
uint16_t USART_StopBits; // 停止位
uint16_t USART_Parity; // 校验位
uint16_t USART_Mode; // USART 模式
uint16_t USART_HardwareFlowControl; // 硬件流控制
} USART_InitTypeDef;
- USART_BaudRate:波特率设置。一般设置为 2400、9600、19200、115200。标准库函数会根据
设定值计算得到 USARTDIV 值,从而设置 USART_BRR 寄存器值。 - USART_WordLength:数据帧字长,可选 8 位或 9 位。它设定 USART_CR1 寄存器的 M 位的值。
如果没有使能奇偶校验控制,一般使用 8 数据位;如果使能了奇偶校验则一般设置为 9 数据位。 - USART_StopBits:停止位设置,可选 0.5 个、1 个、1.5 个和 2 个停止位,它设定 USART_CR2
寄存器的 STOP[1:0] 位的值,一般我们选择 1 个停止位。 - USART_Parity:奇偶校验控制选择,可选 USART_Parity_No(无校验)、USART_Parity_Even(偶
校验) 以及 USART_Parity_Odd(奇校验),它设定 USART_CR1 寄存器的 PCE 位和 PS 位的值。 - USART_Mode:USART 模式选择,有 USART_Mode_Rx 和 USART_Mode_Tx,允许使用逻辑或
运算选择两个,它设定 USART_CR1 寄存器的 RE 位和 TE 位。 - USART_HardwareFlowControl:硬件流控制选择,只有在硬件流控制模式才有效,可选有使
能 RTS、使能 CTS、同时使能 RTS 和 CTS、不使能硬件流。
三、编程要点
- 开启时钟,把需要用到的 GPIO 和 USART 时钟打开;
- 初始化 GPIO,把 TX 配置成复用输出,RX 配置成输入;
- 配置 USART 参数,即配置USART 初始化结构体;
- 如果只需要发送功能,就直接开启 USART,初始化就结束了;如果需要接收功能,可能还需要配置中断。
初始化完成之后,如果要发送数据,就调用函数;如果要接收数据,就调用接收函数;如果要获取发送和接收的状态,就调用获取标志位的函数。
四、举例
4.1 串口发送
这里使用 USART1,设定波特率为 9600,TX 对应 PA9。
- 配置 GPIO
void Serial_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能GPIOA时钟
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 设置GPIO模式为复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // 设置引脚为PA9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
- 配置 USART
void Serial_USART_Config(void)
{
USART_InitTypeDef USART_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // 使能USART1时钟
USART_InitStructure.USART_BaudRate = 9600; // 设置波特率为9600
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件流控制
USART_InitStructure.USART_Mode = 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); // 初始化USART1
USART_Cmd(USART1, ENABLE); // 使能USART1
}
配置完 USART1 之后,就可以使用串口实现一些功能了。
(1)发送一个字节
void Serial_SendByte(uint8_t byte)
{
USART_SendData(USART1, byte); // 发送数据
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // 等待发送完成
}
(2)发送一个数组
void Serial_SendArray(uint8_t *arr, uint16_t len)
{
uint16_t i;
for (i = 0; i < len; i++) // 遍历数组,逐个发送元素
{
Serial_SendByte(arr[i]); // 调用Serial_SendByte函数发送数组元素
}
}
(3)发送字符串
void Serial_SendString(char *str)
{
uint8_t i;
for (i = 0; str[i] != '\0'; i++) // 遍历字符串,直到遇到空字符'\0'
{
Serial_SendByte(str[i]); // 调用Serial_SendByte函数发送字符串中的字符
}
}
(4)发送数字字符
uint32_t Serial_Pow(uint32_t x, uint32_t y)
{
uint32_t result = 1;
while (y--) // 循环y次,计算x的y次方
{
result *= x; // 累乘结果
}
return result; // 返回结果
}
void Serial_SendNumber(uint32_t num, uint8_t len)
{
uint8_t i;
for (i = 0; i < len; i++) // 遍历数字的每一位
{
Serial_SendByte(num / Serial_Pow(10, len - i - 1) % 10 + '0'); // 将数字转换为字符并发送
}
}
(5)printf
函数重定向
- Keil 版
#include <stdio.h>
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//等待上一次串口数据发送完成
USART1->DR = (uint8_t)ch; //写DR,串口1将发送数据
return ch;
}
这里也可以直接使用 Serial_SendByte
函数:
int fputc(int ch, FILE *f)
{
Serial_SendByte((uint8_t)ch);
return ch;
}
- GCC 版
#include <stdio.h>
int _write (int fd, char *pBuffer, int size)
{
for (int i = 0; i < size; i++)
{
while((USART1->SR&0X40)==0); //等待上一次串口数据发送完成
USART1->DR = (uint8_t) pBuffer[i]; //写DR,串口1将发送数据
}
return size;
}
同样的:
int _write(int fd, char *pBuffer, int size)
{
for (int i = 0; i < size; i++) // 遍历缓冲区
{
Serial_SendByte((uint8_t)pBuffer[i]); // 调用Serial_SendByte函数发送缓冲区中的字节
}
return size; // 返回发送的字节数
}
4.2 串口发送和接收
这里使用 USART1,设定波特率为 9600,TX 对应 PA9,RX 对应 PA10。在串口发送的基础上,修改如下:
(1)不使用中断
在 Serial_GPIO_Config
函数中添加如下代码:
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 设置GPIO模式为上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // 设置引脚为PA10
GPIO_Init(GPIOA, &GPIO_InitStructure);
在 Serial_USART_Config
函数中将
USART_InitStructure.USART_Mode = USART_Mode_Tx; // 设置为发送模式
修改为
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; // 设置为发送和接收模式
为了方便读取数据,编写函数
uint8_t Serial_ReceiveByte(void)
{
// 等待接收缓冲区非空
while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
// 从接收缓冲区读取一个字节并返回
return USART_ReceiveData(USART1);
}
(2)使用中断
在不使用中断的基础上,在 Serial_USART_Config
函数中 USART_Cmd(USART1, ENABLE);
前添加代码
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 使能USART1的接收中断
并且还需要配置 NVIC:
void Serial_NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; // 设置中断通道为USART1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 设置抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 设置响应优先级为1
NVIC_Init(&NVIC_InitStructure);
}
既然使用了中断,那么就不需要使用 Serial_ReceiveByte
函数读取数据了,直接使用中断服务函数:
// 定义串口接收数据变量和标志位
uint8_t Serial_RxData;
uint8_t Serial_RxFlag;
// 获取接收标志位的函数
uint8_t Serial_GetRxFlag(void)
{
if (Serial_RxFlag == 1) // 如果接收标志位为1
{
Serial_RxFlag = 0; // 将接收标志位清零
return 1; // 返回1表示有新数据接收
}
return 0; // 返回0表示没有新数据接收
}
// 获取接收数据的函数
uint8_t Serial_GetRxData(void)
{
return Serial_RxData; // 返回接收到的数据
}
// USART1中断服务函数
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) // 如果接收寄存器非空中断标志位为1
{
Serial_RxData = USART_ReceiveData(USART1); // 读取接收到的数据
Serial_RxFlag = 1; // 设置接收标志位为1
USART_ClearITPendingBit(USART1, USART_IT_RXNE); // 清除接收寄存器非空中断标志位
}
}
参考视频源于B站up主: 野火科技、江协科技
参考文档:《STM32库开发实战指南——基于野火MINI开发板》