【STM32】串口

1. 什么是串口

1.1 补充点儿基础~

1.1.1 串行通信、并行通信

串行通信 是指计算机与I/O设备之间数据传输的各位是按顺序依次一位接一位进行传送 。通常数据在一根数据线或一对差分线上传输。
并行通信 是指计算机与I/O口设备间通过多条传输线交换数据,数据的各位同时进行传送

串行通信的传输速度慢,但使用的传输设备成本低,可利用现有的通信手段和通信设备,适合于计算机的远程通信;并行通信的速度快,但使用的传输设备成本高,适合于近距离的数据传输

1.1.2 单工、半双工、全双工

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

1.1.3 同步通信、异步通信

同步通信:发送和接收双方按照预定的时钟节拍进行数据的发送和接收,双方的操作严格同步。
异步通信:双方不需要严格的时钟同步,每个数据块之间通过特定的起始位和停止位进行分隔,接收方可以独立地识别每个数据块。

同步通信:相当于一秒钟发出一个数据,那边一秒钟接到个数据

同步通信有时钟信号,异步通信无时钟信号

1.1.4 通信速率(比特率、波特率)

通信速率是指在通信系统中单位时间内传输的信息量,是评估通信系统性能的重要指标之一。

a 比特率:
每秒能传输的二进制 位数 。它用单位时间内传输的二进制代码的有效位( bit )数来表示,其单位为比特 / 秒( bit/s bps )。
比特率越高,表示单位时间内传送的数据量越大,信息传输的速率越快。它经常被用作连接速
度、传输速度、信息传输速率和数字带宽容量的同义词。

注意:位数,不是字节数,一个字节是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.有效数据位

数据位是由一系列二进制值组成,用于传输或接收实际的数据。数据位的数量决定了可以传输的不同二进制值的数量,常见的有5 位、 6 位、 7 位、 8 位, LSB 在前, MSB 在后。数据位紧随起始位之后,包含了要传输的实际信息。

|-------|----------|----------|
| | 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 ,可以根据需要配置不同的波特率。
  1. 硬件流控制 : USART 支持硬件流控制,通过特定的信号线(如 RTS/CTS )实现数据的可靠传输。当接收端没有准备好接收数据时,可以通过RTS 信号通知发送端暂停发送;当接收端准备好接收数据时,再通过CTS 信号通知发送端恢复发送。
    接下来单独分出一章重点讲USART吧!

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)

第一步:初始化串口
cpp 复制代码
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()中自动调用

cpp 复制代码
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

cpp 复制代码
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

cpp 复制代码
#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里

cpp 复制代码
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是否在动

cpp 复制代码
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;
}

测试函数

cpp 复制代码
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 继续写

打开空闲中断

cpp 复制代码
		__HAL_UART_ENABLE_IT(huart,UART_IT_IDLE);

在这里void USART1_IRQHandler(void)判断有没有接收到空闲中断,如果接收到就说明数据接收完整了

cpp 复制代码
	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);
}
}

相关推荐
ThreeYear_s2 小时前
【FPGA+DSP系列】——PWM电平光耦转换电路实验分析----电路原理分析,器件选型
单片机·嵌入式硬件·fpga开发
天天爱吃肉82183 小时前
深入理解电流传感器相位补偿:原理、方法与典型应用
人工智能·嵌入式硬件·汽车
夜月yeyue4 小时前
嵌入式开发中的 Git CI/CD
c++·git·单片机·嵌入式硬件·ci/cd·硬件架构
锻炼²4 小时前
(已解决)vscode打开stm32cubemx生成的工程报红色波浪线警告
ide·vscode·stm32·stm32cubemx·vscode打开keil工程
axuan126515 小时前
16.【NXP 号令者RT1052】开发——实战-FlexPWM 输出
单片机·嵌入式硬件·mcu
墨辰JC5 小时前
基于STM32标准库的FreeRTOS移植与任务创建
数据库·stm32·嵌入式硬件·freertos
时光の尘6 小时前
【STM32】DMA超详细解析·入门级教程
stm32·单片机·嵌入式硬件·mcu·串口·dma·usart
chao1898446 小时前
基于TMS320F28069 DSP开发板实现RS485通信
单片机·嵌入式硬件
朱嘉鼎7 小时前
消费级MCU如何管理内存
单片机·嵌入式硬件