【STM32】串口
1. 什么是串口
1.1 补充点儿基础~
1.1.1 串行通信、并行通信
串行通信是指计算机与I/O设备之间数据传输的各位是按顺序依次一位接一位进行传送。通常数据在一根数据线或一对差分线上传输。
并行通信是指计算机与I/O口设备间通过多条传输线交换数据,数据的各位同时进行传送。
串行通信的传输速度慢,但使用的传输设备成本低,可利用现有的通信手段和通信设备,适合于计算机的远程通信;并行通信的速度快,但使用的传输设备成本高,适合于近距离的数据传输



1.1.2 单工、半双工、全双工

单工通信:数据只能沿一个方向传输半双工通信:数据可以沿两个方向传输,但需要分时进行全双工通信:数据可以同时进行双向传输
1.1.3 同步通信、异步通信

同步通信:发送和接收双方按照预定的时钟节拍进行数据的发送和接收,双方的操作严格同步。异步通信:双方不需要严格的时钟同步,每个数据块之间通过特定的起始位和停止位进行分隔,接收方可以独立地识别每个数据块。
同步通信:相当于一秒钟发出一个数据,那边一秒钟接到个数据
同步通信有时钟信号,异步通信无时钟信号
1.1.4 通信速率(比特率、波特率)
通信速率是指在通信系统中单位时间内传输的信息量,是评估通信系统性能的重要指标之一。
a 比特率:
注意:位数,不是字节数,一个字节是8位
b 波特率:
什么是码元?
STM32只需两个档位,高电平3.3V,低电平0V,一个码元——1为高电平,0为低电平;
若想表示四个档位呢(3.6V,2.4V,1.2V,0V),2个码元——11代表3.6V,10代表2.4V,01代表1.2V,00代表0V
二进制系统中,波特率数值上等于比特率
1.2 串口通信
1.2.1 串口
串行通信接口,实现数据一位一位顺序传送
串口通信的接口类型包括TTL、CMOS、RS-232和RS-485等,它们分别代表了不同的电平标准。

我们用的CH340(USB转TTL)是什么角色呢?

注意区分USB转TTL和STlink:它们都是连接在板子上,USB转TTL是串口,STlink是烧录器
1.2.2 通信协议
四部分构成
a.启动位
一开始拉低,告诉接收方数据传输即将开始,准备接收。
b.有效数据位
| LSB在前 | LSB在后 | |
| 0x05: | 00000101 | 01010000 |
c.校验位
其实不怎么准确,一般不使用
d.停止位
最后拉高,接收端知道数据传输已经完成,并且可以开始处理接收到的数据。

1.2.3 STM32的USART
USART:同步异步收发器
UART:异步收发器
STM32有3个USART

1. 全双工通信 : USART 支持全双工通信,即数据可以在两个方向上同时传输( A → B 且 B → A )。这使得USART能够满足许多需要双向通信的应用场景。2. 同步与异步传输 :尽管 USART 的 “S” 代表同步,但在实际应用中, USART 更常用于异步通信。然而,它也支持同步通信模式,只是这种模式通常用于兼容其他协议或特殊模式,并且两个USART 设备不能通过同步模式进行直接通信。3. 波特率发生器 : USART 自带波特率发生器,最高可达 4.5Mbits/s ,可以根据需要配置不同的波特率。4. 硬件流控制 : USART 支持硬件流控制,通过特定的信号线(如 RTS/CTS )实现数据的可靠传输。当接收端没有准备好接收数据时,可以通过RTS 信号通知发送端暂停发送;当接收端准备好接收数据时,再通过CTS 信号通知发送端恢复发送。
2. USART
2.1 框图
TX发,RX收,先看简单的框图

我想发数据怎么发呢?
往IDR写入内容,它会把内容转运到发送移位寄存器,发送移位寄存器会将它一位一位移出去,通过GPIO口(复用TX)发送给其他设备
怎么接收呢?
如果外界有数据进来,通过复用RX的GPIO口移到到接收移位寄存器,接收移位寄存器再转运到RDR,外面就能把数据读走了
移位寄存器由控制器控制,控制器由波特率发生器控制,波特率发生器来源于PCLK时钟
再看STM32手册的官方框图
无非是多了TE、PCE使能控制分别控制发送器和接收器,USART中断控制

2.2 USART寄存器
2.2.1 状态寄存器
如位5:读数据寄存器非空
通过读取这个位的值,判断是否收到了完整的数据
串口已经接收到了数据,并且已经写入到了USART_DR寄存器
2.2.2 数据寄存器
0~8位共9位
数据寄存器USART_DR,只使用了位0-8,其他位保留

读寄存器:读取该寄存器获取接收到的数据值
写寄存器:向该寄存器写入发送的数据对数据进行发送
2.2.3 波特比率寄存器
波特率寄存器USART_BRR,只用到了低16位,高16位保留

0-3位[3:0] : USART分频器的小数部分DIV_Fraction
4-15位[15:4] : USART分频器的整数部分DIV_Mantissa
波特率计算方法:

假如我们设置串口1波特率为115200MHz:
串口1的时钟来自PCLK2=72MHz
由公式得到:
USARTDIV=72000000/(115200*16)=39.0625小数部分DIV_Fraction=16*0,0625=1=0x01
整数部分DIV_Mantissa=39=0x27
所以设置USART->BRR=0x0271,就可以实现设置串口1的波特率为115200MHz
DIV_Mantissa: 0000 0010 0111 DIV_Fraction: 0001 组合后: 0000 0010 0111 0001
2.2.3 控制寄存器
USART_CR1
USART_BRR波特率寄存器,设置串口寄存器使能位
如:接收使能,发送使能


USART_CR2

最常用的就是1个停止位
USART_CR3


去掉TX或RX就退化为半双工,但是一般不这么干
2.3 USART常用库函数

init函数
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);
发送transmit函数
HAL_UART_Transmit(&uart1_handle,&recieve_data,1,1000);
接收receive函数
HAL_UART_Receive(&uart1_handle,&recieve_data,1,1000);
DMA发送
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size);
DMA接收
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
接收完成回调函数

如果数据全部接收完成后,就会调用它
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
3. 实验
3.1 实验一:串口实现一个字符收发
3.1.1 硬件准备
CH340,ST-LINL,STM32
使用串口1完成一个字符收发,根据原理图可知要使用的引脚是PA9和PA10

3.1.2 写代码(uart1.c)
第一步:初始化串口
UART_HandleTypeDef uart1_handle = {0};
void uart1_init(uint32_t baudrate)
{uart1_handle.Instance = USART1;uart1_handle.Init.BaudRate = baudrate;uart1_handle.Init.WordLength = UART_WORDLENGTH_8B;uart1_handle.Init.StopBits = UART_STOPBITS_1;uart1_handle.Init.Parity = UART_PARITY_NONE;uart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE;uart1_handle.Init.Mode = UART_MODE_TX_RX;HAL_UART_Init(&uart1_handle);
}
第二步:初始化MSP
查用户手册
可以发现,复用GPIO时,串口引脚配置需要设置,时钟仍设置按最快的频率运行
HAL_UART_MspInit是HAL库的回调函数,会在HAL_UART_Init()中自动调用
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{if(huart->Instance == USART1){__HAL_RCC_USART1_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef gpio_initstruct;//调用GPIO初始化函数gpio_initstruct.Pin = GPIO_PIN_9; // TX1对应的引脚gpio_initstruct.Mode = GPIO_MODE_AF_PP; // 推挽输出gpio_initstruct.Pull = GPIO_PULLUP; // 上拉gpio_initstruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速HAL_GPIO_Init(GPIOA, &gpio_initstruct);gpio_initstruct.Pin = GPIO_PIN_10; // RX1对应的引脚gpio_initstruct.Mode = GPIO_MODE_AF_INPUT; // 推挽输入HAL_GPIO_Init(GPIOA, &gpio_initstruct);HAL_NVIC_EnableIRQ(USART1_IRQn);HAL_NVIC_SetPriority(USART1_IRQn, 2, 2);__HAL_UART_ENABLE_IT(huart, UART_IT_RXNE);}
}
第三步:写中断服务函数
中断服务函数在.s里,IRQ是中断服务函数,在这里可以进行数据的收发了


怎么知道串口收到数据了呢?——RXNE不为空时,就会进行中断,然后进行后续操作
画圈的含义:最长等待多少ms
void USART1_IRQHandler(void)
{uint8_t receive_data = 0;if(__HAL_UART_GET_FLAG(&uart1_handle, UART_FLAG_RXNE) != RESET){HAL_UART_Receive(&uart1_handle, &receive_data, 1, 1000);HAL_UART_Transmit(&uart1_handle, &receive_data, 1, 1000);}
}
3.1.3 实现效果
main.c
#include "sys.h"
#include "delay.h"
#include "led.h"
#include "uart1.h"int main(void)
{HAL_Init(); /* 初始化HAL库 */stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */led_init(); /* 初始化LED灯 */uart1_init(115200);while(1){ led1_on();led2_off();delay_ms(500);led1_off();led2_on();delay_ms(500);}
}
实现LED1和LED2交替闪烁,串口调试助手能收发数据
串口实现一个字符收发
3.2 如何确保收到一帧完整的数据?
“小白,帮我找下小花”————“小白,帮我找下小”???(没接收完整)
a. 固定格式
"AABB小白,帮我找下小花BBAA"
接收一个字符就得判断一下是不是A是不是B,比较浪费芯片资源
b. 接收中断+超时判断
接收一个数据,触发一下中断
如果一帧与一帧数据之间的间隔比字符和字符之间的间隔长,假设字符和字符之间的间隔为1ms,那么我们就可以把一帧与一帧数据之间的间隔按其1.5倍(1.5ms)来判断是否接收到数据,如果1.5ms内没接收到数据,那我们就认为以前收到的已经是完整的数据包了
c. 空闲中断
一帧数据接收完后,触发空闲中断
和b原理一样,只不过一般高端的MCU才会有空闲中断(吃硬件)
3.3 实验二:接收不定长数据(接收中断+超时判断)
仍然用3.1的代码
把接收到的数据放到uart1_rx_buf里
void USART1_IRQHandler(void)
{uint8_t recieve_data=0;if(__HAL_UART_GET_FLAG(&uart1_handle,UART_FLAG_RXNE!=RESET)){if(uart1_cnt>=sizeof(uart1_rx_buf))uart1_cnt=0;HAL_UART_Receive(&uart1_handle,&recieve_data,1,1000);uart1_rx_buf[uart1_cnt]=recieve_data;uart1_cnt++;HAL_UART_Transmit(&uart1_handle,&recieve_data,1,1000);}
}
怎么知道数据接收完了呢?————看uart1_cnt还动不动了,不动了就是接收完了
所以要再写个函数,判断uart1_cnt是否在动
uint8_t uart1_wait_recieve(void)
{if(uart1_cnt==0)return UART_ERROR;if(uart1_cnt==uart1_cntPre){uart1_cnt=0;return UART_EOK;}uart1_cntPre=uart1_cnt;return UART_ERROR;
}
测试函数
int fputc(int ch, FILE *f)
{while((USART1->SR & 0X40) == 0);USART1->DR = (uint8_t)ch;return ch;
}uint8_t uart1_wait_receive(void)
{if(uart1_cnt == 0)return UART_ERROR;if(uart1_cnt == uart1_cntPre){uart1_cnt = 0;return UART_EOK;}uart1_cntPre = uart1_cnt;return UART_ERROR;
}void uart1_rx_clear(void)
{memset(uart1_rx_buf, 0, sizeof(uart1_rx_buf));uart1_rx_len = 0;
}void uart1_receiv_test(void)
{if(uart1_wait_receive() == UART_EOK){printf("recv: %s\r\n", uart1_rx_buf);uart1_rx_clear();}
}
3.4 实验三:接收不定长数据(空闲中断)
3.4.1 继续写
打开空闲中断
__HAL_UART_ENABLE_IT(huart,UART_IT_IDLE);
在这里void USART1_IRQHandler(void)判断有没有接收到空闲中断,如果接收到就说明数据接收完整了
if(__HAL_UART_GET_FLAG(&uart1_handle,UART_IT_IDLE!=RESET)){printf("recv:%s\r\n",uart1_rx_buf);uart1_rx_clear(); __HAL_UART_CLEAR_FEFLAG(&uart1_handle);}
效果

3.4.2 理解中断
我们顺便在这里复习一下中断
完整的uart1.c代码
#include "uart1.h"
#include "stdio.h"
#include "string.h"uint8_t uart1_rx_buf[UART1_RX_BUF_SIZE];
uint16_t uart1_rx_len = 0;UART_HandleTypeDef uart1_handle = {0};
void uart1_init(uint32_t baudrate)
{uart1_handle.Instance = USART1;uart1_handle.Init.BaudRate = baudrate;uart1_handle.Init.WordLength = UART_WORDLENGTH_8B;uart1_handle.Init.StopBits = UART_STOPBITS_1;uart1_handle.Init.Parity = UART_PARITY_NONE;uart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE;uart1_handle.Init.Mode = UART_MODE_TX_RX;HAL_UART_Init(&uart1_handle);
}void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{if(huart->Instance == USART1){__HAL_RCC_USART1_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef gpio_initstruct;//调用GPIO初始化函数gpio_initstruct.Pin = GPIO_PIN_9; // 两个LED对应的引脚gpio_initstruct.Mode = GPIO_MODE_AF_PP; // 推挽输出gpio_initstruct.Pull = GPIO_PULLUP; // 上拉gpio_initstruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速HAL_GPIO_Init(GPIOA, &gpio_initstruct);gpio_initstruct.Pin = GPIO_PIN_10; // 两个LED对应的引脚gpio_initstruct.Mode = GPIO_MODE_AF_INPUT; // 推挽输出HAL_GPIO_Init(GPIOA, &gpio_initstruct);HAL_NVIC_EnableIRQ(USART1_IRQn);HAL_NVIC_SetPriority(USART1_IRQn, 2, 2);__HAL_UART_ENABLE_IT(huart, UART_IT_RXNE);__HAL_UART_ENABLE_IT(huart, UART_IT_IDLE);}
}void uart1_rx_clear(void)
{memset(uart1_rx_buf, 0, sizeof(uart1_rx_buf));uart1_rx_len = 0;
}void USART1_IRQHandler(void)
{uint8_t receive_data = 0;if(__HAL_UART_GET_FLAG(&uart1_handle, UART_FLAG_RXNE) != RESET){if(uart1_rx_len >= sizeof(uart1_rx_buf))uart1_rx_len = 0;HAL_UART_Receive(&uart1_handle, &receive_data, 1, 1000);uart1_rx_buf[uart1_rx_len++] = receive_data;//uart1_cnt++;//HAL_UART_Transmit(&uart1_handle, &receive_data, 1, 1000);}if(__HAL_UART_GET_FLAG(&uart1_handle, UART_FLAG_IDLE) != RESET){printf("recv: %s\r\n", uart1_rx_buf);uart1_rx_clear();__HAL_UART_CLEAR_IDLEFLAG(&uart1_handle);}
}int fputc(int ch, FILE *f)
{while((USART1->SR & 0X40) == 0);USART1->DR = (uint8_t)ch;return ch;
}
问:以上如何实现中断的?
1. 中断使能配置
在 HAL_UART_MspInit 函数中完成了中断的使能:
// 4. 配置NVIC(中断控制器)
HAL_NVIC_EnableIRQ(USART1_IRQn); // 使能USART1中断
HAL_NVIC_SetPriority(USART1_IRQn, 2, 2); // 设置优先级// 5. 使能串口中断
__HAL_UART_ENABLE_IT(huart, UART_IT_RXNE); // 接收数据中断
__HAL_UART_ENABLE_IT(huart, UART_IT_IDLE); // 总线空闲中断
2. 中断触发条件
条件1:接收到数据(RXNE中断)
__HAL_UART_ENABLE_IT(huart, UART_IT_RXNE);
触发时机:当串口接收到一个字节数据,并转移到接收数据寄存器(DR)时
硬件行为:USART_SR 寄存器中的 RXNE 标志位自动置1
中断产生:由于RXNE中断已使能,产生中断请求
条件2:总线空闲(IDLE中断)
__HAL_UART_ENABLE_IT(huart, UART_IT_IDLE);
触发时机:当串口RX线上检测到1个字节时间内没有新数据时
硬件行为:USART_SR 寄存器中的 IDLE 标志位自动置1
中断产生:由于IDLE中断已使能,产生中断请求
3. 中断处理流程流程
硬件事件发生
↓
RXNE或IDLE标志位置1
↓
中断信号发送到NVIC
↓
NVIC根据优先级调度
↓
CPU跳转到 USART1_IRQHandler
↓
在中断函数中检查具体中断源
↓
执行相应的处理代码
↓
清除中断标志
↓
返回主程序
void USART1_IRQHandler(void)
{
uint8_t receive_data = 0;
// 检查是否是"接收到数据"中断
if(__HAL_UART_GET_FLAG(&uart1_handle, UART_FLAG_RXNE) != RESET)
{
// 防止缓冲区溢出
if(uart1_rx_len >= sizeof(uart1_rx_buf))
uart1_rx_len = 0;
// 关键:读取数据寄存器,这个操作会自动清除RXNE标志!
HAL_UART_Receive(&uart1_handle, &receive_data, 1, 1000);
// 存储数据到缓冲区
uart1_rx_buf[uart1_rx_len++] = receive_data;
}
// 检查是否是"总线空闲"中断
if(__HAL_UART_GET_FLAG(&uart1_handle, UART_FLAG_IDLE) != RESET)
{
// 打印接收到的完整数据帧
printf("recv: %s\r\n", uart1_rx_buf);
// 清空缓冲区准备接收下一帧
uart1_rx_clear();
// 关键:必须手动清除IDLE标志!
__HAL_UART_CLEAR_IDLEFLAG(&uart1_handle);
}
}
