从零开始学嵌入式之STM32——11.STM32---USART串行通讯

一、通讯相关基础知识

1.并行通信和串行通信

1.1.区别

按数据传送的方式,通讯可分为串行通讯和并行通讯,两者的区别:
传输方式:
串行:一次传输一位(一般从低位开始发送)
并行:一般是多位同时传输,比如8位 16位 32位等
通讯线路:
串行:通常只需一对数据线(收发各一)或单根数据线(半双工时),外加公共地线。
并行:多根线

1.2.对比

  • **通信速度:**在相同时钟频率下,并行通信比串行通信快,但受距离限制更大。
  • **复杂性:**并行通信需要更多的数据线,布线更复杂;串行通信简化了布线和接口。
  • **距离:**串行通信更适合远距离传输,并行通信适合短距离传输。
  • **频率:**串行通讯频率可以更高
  • **成本:**串行接口成本较低
  • **抗干扰:**串行通讯抗干扰能力通常优于并行通讯
  • **未来发展:**串行通讯越来越受欢迎。原因在于其较低的复杂性和成本,以及适应长距传输的能力。
    目前在数据传输领域,处于高速串行通信的时代。

1.3.小知识

显卡的数据传输发展历程:
早期使用并行通信:PCI AGP
目前使用串行通信:PCIe
硬盘的数据传输发展历程:
早期使用并行通信:PATA (Parallel ATA)
早期使用并行通信:SATA
那按照这样的趋势,并行通讯就没有用武之地了吗?不是的,在某些应用中(如内部总线)中,并行通信仍然是必要的,尤其是短距离高速数据传输的场合。

2.单工、半双工、全双工

**单工:**信息只能单向传输。
**半双工:**信息可以双向传输,但是需要分时进行。
**全双工:**信息可以双向且同时传输。

3.同步通信、异步通信

同步通信: 通信双方在同一个时钟信号的控制下进行数据收发。该时钟信号可能由发送方、接收方或第三方提供,并通过专用时钟线 传输,或嵌入在数据流 中(如曼彻斯特编码)。同步通信要求严格的时序对齐,适用于高速、连续的数据块传输。
异步通信: 通信双方使用各自独立的时钟,无需共享物理时钟线。但双方必须预先严格约定 数据传输速率(波特率)、数据帧格式(起始位、数据位、校验位、停止位)。数据传输以 为单位,每帧均以起始位重新同步。其可靠性依赖于本地时钟的精度和帧内时序的短期稳定性。

4.串口通讯

串口通讯(Serial Communication),是一种设备间常用的串行通讯方式,由于其简单便捷的特性,大部分电子设备都支持该通讯方式。它是一种串行通信方式 。UART/USART是实现异步串行通信最常用、最经典的硬件协议/模块。
在电脑上,经过对UART进行了一系列电气化的升级、调整后,形成了一系列的电气化标准,例如我们常见的RS232、RS422、RS485。
我们通常不称RS232、RS485、RS422为通讯协议,而称其为电气化标准。

5.串口通讯协议

一帧数据主要有起始位、数据位、校验位、停止位组成,其中起始位、数据位、停止位是必选项,校验位是可选项。数据帧可以参考下图:

5.1.通信过程简介

通讯开始前,发送方先发送一个低电平作为开始信号。起始位作为通讯的开始信号。
发送方发出起始信号后开始发送数据。发送时,发送一个字节先从低位开始发送。
完成一帧数据发送后,再单独发送一个高电平,作为停止信号,在平时不通讯时,使用高电平作为空闲状态。在帧与帧之间以及不通讯时,线路保持高电平,称为空闲状态。

5.2.起始位

发送方正式发送数据前,先发送一个低电平作为起始信号。

5.3.有效数据位

在数据包的起始位之后紧接着的就是要传输的主体数据内容,也称为有效数据。
有效数据的长度常被约定为 5、6、7 或 8 位长。构成一个字符(一般都是 8 位)。
数据发送时,先发送最低位,最后发送最高位。
使用低电平表示"0'高电平表示1'完成数据位的传输。

5.4.奇偶校验

在有效数据位后可以设置一位校验位用于校验数据在传输过程中有无出错。(可选配置)
**奇校验模式:**数据中如果有奇数个1,奇偶校验位写0,如果数据中有偶数个1,奇偶校验位写1
**偶校验模式:**数据中如果有奇数个1,奇偶校验位写1,如果数据中有偶数个1,奇偶校验位写0
**总结:**奇偶校验位是用来将一帧的数据中的1的个数调整到奇偶校验要求的状态。等对方收到数据后对数据进行校验,如果校验失败,则可以按照机制重传。

5.5.停止位

在一个数据帧的末尾,会使用一个高电平作为停止位。高电平持续1比特时间(最常用)还可能是持续1.5或2个比特时间。
在STM32中,持续一帧时间长度的高电平(空闲位) 被称为空闲帧。接收器可将其用作一个特殊标志,例如用于检测一包可变长度数据的结束。

5.6.波特率

波特率表示每秒钟传输了多少个码元 ,在标准UART(使用2电平,1码元1比特) 的场景中,可以认为波特率数值上等于比特率。但是两者在意义上是有差别的:
**波特率:**一秒钟传递的码元,码元指的是信息传递时的基本单位,根据不同的通信方式,可能是不同的比特。
**比特率:**一秒钟传递的比特数,指的就是比特率。
根据下图可以直观地看出区别:

STM32提供了串口异步通讯,异步通讯由于没有时钟,通讯的双方需要提前约定好通讯的速率:波特率。

二、STM32中的USART外设

在51单片机中,使用的是UART(universal asynchronus receiver and transmitter)通用异步收发器
在STM32中,使用的是USART(universal asynchronous receiver and transmitter)通用同步异步收发器,USART相较于UART增加了同步收发的功能。本文以STM32F103ZET6芯片为例进行介绍。
STM32F103ZE系列中有5个串口,其支持的功能统计如下:

USART1的速度最快,理论上可以达到4.5MB/S。其余串口理论上只能达到2.25MB/S。
造成速度差异的原因是接到了不同的外设总线上:
APB2是高速外设总线,时钟频率为72MHz。
APB1是低速外设总线,时钟频率为36MHz。

1.STM32中USART逻辑框图

2.波特率设置寄存器 (USART_BRR)

假设时钟频率为72MHz,波特率的计算公式如下:
波特率 = 72MHz / (16 * (DIV_Mantissa + (DIC_Fraction / 16)))
由此推导出:
72MHz / (16 * 波特率) = DIV_Mantissa + (DIC_Fraction / 16)

cpp 复制代码
//以常用的72MHz频率、波特率115200为例:
72MHz = 72000000Hz
72000000 / (16 * 115200) = DIV_Mantissa + (DIC_Fraction / 16)
39.0625 =  DIV_Mantissa + (DIC_Fraction / 16)
//按照推导公式:DIV_Mantissa = 39 转为16进制:0x27
//             DIC_Fraction = 0.0625 * 0.0635 = 1 转为16进制:0x1
//将这两个结果组合到一起赋值给波特比率寄存器(USART_BRR) 

具体的寄存器配置参考下图:

如果直接操作寄存器进行波特率配置,需要操作的寄存器信息如下:

采用寄存器方式配置波特率的代码:

cpp 复制代码
//72MHz频率、波特率115200
USART1->BRR = 0X271;

常用的波特率

3.串口控制寄存器1(USART_CR1)

在USART_CR1中,常用的控制位有:

UE、TXEIE、TCIE、RXNEIE、IDLEIE、TE、RE

使用方法及功能见下图:

4.串口控制寄存器2(USART_CR2)

在USART_CR2中,常用的控制位有:STOP

使用方法及功能见下图:

5.串口控制寄存器3(USART_CR3)

在普通应用场景中,这个寄存器用到的较少,具体内容如下:

6.串口控制状态寄存器(USART_SR)

USART_SR寄存器为程序检测串口数据的工作状态提供信息,通过寄存器不同的位的状态来表达不同的状态,寄存器信息如下:

注意1:状态寄存器SR的初始值

RXNE:初始值0 代表接收寄存器内为空

TXE:初始值1 代表发送缓冲区内没有可发送的数据

TC:初始值1 代表发送完成,为什么初始值是1呢?因为TC代表的是移位寄存器内的数据为空,初始值中,移位寄存器内是没有数据的,因此初始值是1

IDLE: 初始值0 为何初始值不是1呢?原因是:IDLE是用来判断一个字符串接收完毕的,必须要等前边已经有字符接收了,并且再次收到了空闲帧后才会置1。简单来说:只有先收到数据,接着又收到了空闲帧IDLE才会置1,因此初始值为0

7.数据寄存器(USART_DR)

DR寄存器用于存放接收到的数据和需要发送的数据,在物理底层其实是有两个寄存器,但是在编写程序时,操作的名称是一致的。

三、应用

1.使用轮询法实现串口通讯

1.1.需求:

使用芯片的USART1模块实现与电脑通讯,电脑向芯片发送不定长的字符,芯片将信息再发回电脑。

1.2.硬件分析:

STM32系列的USART1的数据引脚是:

GPIOA9:TX

GPIOA10:RX

与电脑端通过USB转串口相连接。

1.3.寄存器方式USART1初始化流程

第一步:开启对应模块的时钟:GPIOA、USART1

第二步:设置GPIOA9/10引脚的工作模式:

A9为TX,端口模式应设置为复用推挽输出模式,CNF:10,MODE:11

A10为RX,端口模式应设置为浮空输入模式,CNF:01,MODE:00

第三步:设置USART1波特率,72MHz频率,115200波特率

第四步:设置USART1使能寄存器:CR1寄存器的UE、TE、RE位

第五步:选配(默认配置),设置USART1的相关参数:数据位、校验位、停止位

(1)时钟配置
cpp 复制代码
// 首先是时钟,开启GPIOA模块、USART1的时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
(2)引脚配置
cpp 复制代码
// GPIOA9/10引脚模式配置
// A9  TX 复用功能推挽输出模式;CNF-10; MODE-11;
// A10 RX 浮空输入模式;CNF-01; MODE-00;
GPIOA->CRH &= ~GPIO_CRH_CNF9_0;
GPIOA->CRH |= GPIO_CRH_CNF9_1;
GPIOA->CRH &= ~GPIO_CRH_MODE9;

GPIOA->CRH |= GPIO_CRH_CNF10_0;
GPIOA->CRH &= ~GPIO_CRH_CNF10_1;
GPIOA->CRH &= ~GPIO_CRH_MODE10;
(3)波特率设置寄存器
cpp 复制代码
USART1->BRR = 0X271; //波特率115200
(4)串口模块使能、收发使能

主要涉及的寄存器是CR1寄存器

关键配置:UE:模块使能、TE:发送使能、RE:接收失能

次要配置:M:用于选择字长、PS:校验方式的选择、PCE:校验方式使能。

配置代码:

cpp 复制代码
// USART1使能
USART1->CR1 |= (USART_CR1_TE | USART_CR1_RE | USART_CR1_UE);
// USART1选配(默认配置)
USART1->CR1 &= ~USART_CR1_M;// 8位数据位
USART1->CR1 &= ~USART_CR1_PCE;// 不开启校验
USART1->CR2 &= ~USART_CR2_STOP;// 停止位1

1.4.发送和接受数据

DR寄存器用于存放接收到的数据和需要发送的数据,在物理底层其实是有两个寄存器,但是在编写程序时,操作的名称是一致的。

1.5.轮询方式实现收发数据

接收时,等待接收缓冲区RXNE变为1,代表接收到一帧数据。此时接收DR寄存器内的数据

发送时,等待发送缓冲区TXE变为1,代表一帧数据已经发送完成。此时可以向DR内写入下一帧要发送的数据

根据上图:当缓冲区为空时,严格意义上并不代表发送完成,因为数据还要在移位寄存器中移位发送。如果需要更加严谨的在发送完成一帧后再次进行下一帧数据的发送,就不能以TXE标志位置1为判断条件,而是要以TC(发送完成标志位)位为判断条件。

(1)数据接收逻辑

检测RXNE位:

收到数据时,该位会置1,收到数据后将数据存入缓冲区,再次检测是否还有数据传入。

没有收到数据,该位为0,同时检测空闲帧(IDLE位),如果收到空闲帧,直接结束接收函数,执行发送数据的逻辑。

在接收数据的同时,记录接收字符的数量,作为字符串的长度信息。

cpp 复制代码
开始 USART_Receive_String
├── 初始化 i = 0
│
└── 进入无限循环
│
├── 内层循环:等待接收数据
│   ├── 检查 USART_SR_RXNE 位 (接收寄存器非空)
│   │   ├── 如果 RXNE = 0: 代表没有收到数据,执行内部逻辑检测空闲帧
│   │   └── 如果 RXNE = 1: 跳出内层循环
│   │
│   └── 在等待期间检查 USART_SR_IDLE 位 (空闲帧检测)
│       ├── 如果 IDLE = 1:
│       │   ├── 将字符计数 i 存入 *size
│       │   └── 返回函数,结束循环
│       └── 如果 IDLE = 0: 继续检查 RXNE
│
└── 接收到字符后的处理 (当跳出内层循环时)
├── 读取 USART1->DR 到 *str
├── str 指针递增 (移动到下一个位置)
├── 字符计数 i 递增
└── 回到循环开始继续接收下一个字符

不定长字符串接收模块的代码实现

cpp 复制代码
// 接收一个字符串
void USART_Receive_String(uint8_t* str, uint8_t* size)
{
    uint8_t i = 0;
    while(1)
    {
        while((USART1->SR & USART_SR_RXNE) == 0)// 接收一个字符,等待接收完成
        {
            if(USART1->SR & USART_SR_IDLE)// 当收到空闲帧的时候将字符计数结果传到size变量中,并退出函数,注意:没有使用软件将IDLE清零,根据手册:先查询SR位再读取DR的操作会将IDLE清零。
            {
                *size = i;// 将字符串的长度信息带回主函数
                return;
            }
        }

        *str = USART1->DR;// 将接收到的数据存入缓冲区
        str++;
        i++;
    }
}
(2)数据发送逻辑

当数据接收完成后,接收函数会将接收到的字符串存到缓冲区,同时带回了字符串的长度信息,这时实现发送数据的逻辑就很简单了,采用一个for循环,依次将数据放入DR中,检测发送状态即可。

实现代码:

cpp 复制代码
// 发送一个字节
void USART_Send_Char(uint8_t ch)
{
    while((USART1->SR & USART_SR_TXE) == 0);// 等待上一帧数据发送完毕,注意:这里的TXE我们没有通过软件清零,根据手册:对USART_DR的写入操作可以将该位清零。
    USART1->DR = ch;
}
// 发送一个已知长度的字符串
void USART_Send_String(uint8_t* str, uint8_t size)
{
    if((str == NULL) || (size == 0))//参数有效性检查
    {
        return;
    }
    for(uint8_t i = 0; i < size; i++)
    {
        USART_Send_Char(*str);
        str++;
    }
}

1.6.轮询方式实现代码

USART.h文件

cpp 复制代码
#ifndef __USART_H
#define __USART_H
#include "stm32f10x.h"
#include "stdlib.h"
// USART1初始化
void USART1_Init(void);
// 发送一个字节
void USART_Send_Char(uint8_t ch);
// 接收一个字节
uint8_t USART_Receive_Char(void);
// 发送一个已知长度的字符串
void USART_Send_String(uint8_t* str, uint8_t size);
// 接收一个字符串
void USART_Receive_String(uint8_t* str, uint8_t* size);
#endif

USART.c文件

cpp 复制代码
#include "USART.h"

// USART1初始化
void USART1_Init(void)
{
    // 1.首先是时钟,开启GPIOA模块、USART1的时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

    // 2.GPIOA9/10引脚模式配置
    // 2.1 A9  TX 复用功能推挽输出模式;CNF-10; MODE-11;
    // 2.2 A10 RX        浮空输入模式;CNF-01; MODE-00;
    GPIOA->CRH &= ~GPIO_CRH_CNF9_0;
    GPIOA->CRH |= GPIO_CRH_CNF9_1;
    GPIOA->CRH &= ~GPIO_CRH_MODE9;

    GPIOA->CRH |= GPIO_CRH_CNF10_0;
    GPIOA->CRH &= ~GPIO_CRH_CNF10_1;
    GPIOA->CRH &= ~GPIO_CRH_MODE10;

    // 3.设置波特率,72MHz晶振,波特率:115200
    USART1->BRR = 0x271;

    // 4.打开USART1使能
    USART1->CR1 |= (USART_CR1_UE | USART_CR1_RE | USART_CR1_TE);

    // 5.可选配置(默认配置)
    USART1->CR1 &= ~USART_CR1_M;//8位数据位
    USART1->CR1 &= ~USART_CR1_PCE;//无校验位
    USART1->CR2 &= ~USART_CR2_STOP;//停止位1位
}

// 发送一个字节
void USART_Send_Char(uint8_t ch)
{
    while((USART1->SR & USART_SR_TXE) == 0);// 等待上一帧数据发送完毕,注意:这里的TXE我们没有通过软件清零,根据手册:对USART_DR的写入操作可以将该位清零。
    USART1->DR = ch;
}

// 接收一个字节
uint8_t USART_Receive_Char(void)
{
    while((USART1->SR & USART_SR_RXNE) == 0);// 等待一帧数据接收完成,注意:这里的RXNE位,我们并没有通过软件去清零,根据手册:对USART_DR的读操作可以将该位清零。当然,也可以通过写入0来清除
    return USART1->DR;
}

// 发送一个已知长度的字符串
void USART_Send_String(uint8_t* str, uint8_t size)
{
    if((str == NULL) || (size == 0))//参数有效性检查
    {
        return;
    }
    for(uint8_t i = 0; i < size; i++)
    {
        USART_Send_Char(*str);
        str++;
    }
}

// 接收一个字符串
void USART_Receive_String(uint8_t* str, uint8_t* size)
{
    uint8_t i = 0;
    while(1)
    {
        while((USART1->SR & USART_SR_RXNE) == 0)// 接收一个字符,等待接收完成
        {
            if(USART1->SR & USART_SR_IDLE)// 当收到空闲帧的时候将字符计数结果传到size变量中,并退出函数,注意:没有使用软件将IDLE清零,根据手册:先查询SR位再读取DR的操作会将IDLE清零。
            {
                *size = i;
                return;
            }
        }

        *str = USART1->DR;
        str++;
        i++;
    }
}

main.c文件

cpp 复制代码
#include "USART.h"
#include <string.h>

int main(void)
{
    uint8_t str[100] = {0};
    uint8_t size = 0;
    USART1_Init();
    while(1)
    {
        USART_Receive_String(str, &size);
        USART_Send_String(str, size);
    }
}

2.使用中断方式实现串口通讯

轮询方式实现串口通讯的方法非常占用CPU资源,使得CPU无法抽身做真正该做的事情,因此在实际应用中,大多数情况下采用中断方式实现串口通讯。

一般的场景:芯片不清楚具体什么时候回收到信息,但是发送信息的动作是芯片本身实现的,是可预期的,因此我们只需要在接收到数据的时候产生中断即可。在使用中断方式接收数据的时候需要将CR寄存器中的中断使能位打开:

USART1->CR1 |= USART1_CR1_RXNEIE;

当接收到数据时,接收缓冲区不为空,就会产生中断,在中断服务函数中将接收状态标志位置位,这样就可以在适当的时候实现需要的操作了。

2.1.寄存器方式USART1初始化流程--和轮询方法类似

第一步:开启对应模块的时钟:GPIOA、USART1

第二步:设置GPIOA9/10引脚的工作模式:

A9为TX,端口模式应设置为复用推挽输出模式,CNF:10,MODE:11

A10为RX,端口模式应设置为浮空输入模式,CNF:01,MODE:00

第三步:设置USART1波特率,72MHz频率,115200波特率

第四步:设置USART1使能寄存器:CR1寄存器的UE、TE、RE位,另外需要使能中断位:RXNEIE和IDLEIE

第五步:选配(默认配置),设置USART1的相关参数:数据位、校验位、停止位

第六步:配置中断优先级

(1)时钟配置--与轮询法一致
cpp 复制代码
// 首先是时钟,开启GPIOA模块、USART1的时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
(2)引脚配置--与轮询法一致
cpp 复制代码
// GPIOA9/10引脚模式配置
// A9  TX 复用功能推挽输出模式;CNF-10; MODE-11;
// A10 RX 浮空输入模式;CNF-01; MODE-00;
GPIOA->CRH &= ~GPIO_CRH_CNF9_0;
GPIOA->CRH |= GPIO_CRH_CNF9_1;
GPIOA->CRH &= ~GPIO_CRH_MODE9;

GPIOA->CRH |= GPIO_CRH_CNF10_0;
GPIOA->CRH &= ~GPIO_CRH_CNF10_1;
GPIOA->CRH &= ~GPIO_CRH_MODE10;
(3)波特率设置寄存器--与轮询法一致
cpp 复制代码
USART1->BRR = 0X271; //波特率115200
(4)串口模块使能、收发使能--比轮询法多出中断控制的设置
cpp 复制代码
 // USART1使能
    USART1->CR1 |= (USART_CR1_TE | USART_CR1_RE | USART_CR1_UE);
    // USART1选配(默认配置)
    USART1->CR1 &= ~USART_CR1_M;// 8位数据位
    USART1->CR1 &= ~USART_CR1_PCE;// 不开启校验
    USART1->CR2 &= ~USART_CR2_STOP;// 停止位1
    // USART1中断配置
    USART1->CR1 |= USART_CR1_RXNEIE;// 接收完成一帧数据后发出中断
    USART1->CR1 |= USART_CR1_IDLEIE;// 收到空闲帧后发出中断
(5)中断优先级配置
cpp 复制代码
// NVIC中断配置
    NVIC_SetPriorityGrouping(3);// 全部为抢占优先级
    NVIC_SetPriority(USART1_IRQn, 3);// 中断优先级为3
    NVIC_EnableIRQ(USART1_IRQn);
(6)中断服务函数
cpp 复制代码
// 中断服务函数
void USART1_IRQHandler(void)
{
    if(USART1->SR & USART_SR_RXNE) // 如果收到了一帧数据
    {
        string_buff[size] = USART1->DR;// 将收到的数据读取到缓冲区内
        size++;// 将索引移到下一个空间
    }
    else if(USART1->SR & USART_SR_IDLE)// 如果收到了一个空闲帧
    {
        // 清除IDLE
        (void)USART1->SR;
        (void)USART1->DR;
        receive_over = 1;// 置位接收完成标志位
    }
}

2.2.中断方式实现代码

USART1.h文件

cpp 复制代码
#ifndef __USART_H
#define __USART_H

#include "stm32f10x.h"
#include "stdlib.h"

// USART1初始化
void USART1_Init(void);

// 发送一个字节
void USART_Send_Char(volatile uint8_t ch);

// 发送一个已知长度的字符串
void USART_Send_String(volatile uint8_t* str, uint8_t str_size);

volatile extern uint8_t string_buff[100];
extern volatile uint8_t size;
extern volatile uint8_t receive_over;

#endif

USART1.c文件

cpp 复制代码
#include "USART.h"



// USART1初始化
void USART1_Init(void)
{
    // 1.首先是时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

    // 2.GPIOA9/10引脚工作模式初始化
    // 2.1.GPIOA9 -TX; 复用功能推挽输出模式;CNF-10; MODE-11
    // 2.2.GPIOA10-RX; 浮空输入模式         CNF-01; MODE-00
    GPIOA->CRH &= ~GPIO_CRH_CNF9_0;
    GPIOA->CRH |= GPIO_CRH_CNF9_1;
    GPIOA->CRH |= GPIO_CRH_MODE9;

    GPIOA->CRH |= GPIO_CRH_CNF10_0;
    GPIOA->CRH &= ~GPIO_CRH_CNF10_1;
    GPIOA->CRH &= ~GPIO_CRH_MODE10;

    // 3.USART1设置
    // 3.1.波特率设置,72MHz,115200波特率
    USART1->BRR = 0X271;
    // 3.2.USART1使能
    USART1->CR1 |= (USART_CR1_TE | USART_CR1_RE | USART_CR1_UE);
    // 3.3.USART1选配(默认配置)
    USART1->CR1 &= ~USART_CR1_M;// 8位数据位
    USART1->CR1 &= ~USART_CR1_PCE;// 不开启校验
    USART1->CR2 &= ~USART_CR2_STOP;// 停止位1
    // 3.4.USART1中断配置
    USART1->CR1 |= USART_CR1_RXNEIE;// 接收完成一帧数据后发出中断
    USART1->CR1 |= USART_CR1_IDLEIE;// 收到空闲帧后发出中断

    // 4.NVIC中断配置
    NVIC_SetPriorityGrouping(3);// 全部为抢占优先级
    NVIC_SetPriority(USART1_IRQn, 3);// 中断优先级为3
    NVIC_EnableIRQ(USART1_IRQn);
}

// 发送一个字节
void USART_Send_Char(volatile uint8_t ch)
{
    while((USART1->SR & USART_SR_TXE) == 0);// 等待发送完成
    USART1->DR = ch;
}

// 发送一个已知长度的字符串
void USART_Send_String(volatile uint8_t* str, uint8_t str_size)
{
    for(uint8_t i = 0; i < str_size; i++)
    {
        USART_Send_Char(str[i]);
    }
}

// 中断服务函数
void USART1_IRQHandler(void)
{
   
    if(USART1->SR & USART_SR_RXNE) // 如果收到了一帧数据
    {
        string_buff[size] = USART1->DR;
        size++;
    }
    else if(USART1->SR & USART_SR_IDLE)// 如果收到了一个空闲帧
    {
        // 清除IDLE
        (void)USART1->SR;
        (void)USART1->DR;
        receive_over = 1;
    }
    
}

main.c文件

cpp 复制代码
#include "USART.h"
#include <string.h>

volatile uint8_t string_buff[100] = {0};
volatile uint8_t size = 0;
volatile uint8_t receive_over = 0;

int main(void)
{
    USART1_Init();
    USART_Send_String("USART1_Init ...... ok", strlen("USART1_Init ...... ok"));
    while(1)
    {
        if(receive_over)
        {
            //发送数据到PC
            USART_Send_String(string_buff, size);
            // 清除标志位
            size = 0;
            receive_over = 0;
        }
    }
}
相关推荐
宵时待雨3 小时前
STM32笔记归纳9:定时器
笔记·stm32·单片机·嵌入式硬件
逐步前行3 小时前
STM32_新建工程(寄存器版)
stm32·单片机·嵌入式硬件
bai5459364 小时前
STM32 CubeIDE 通过PWM占空比控制舵机角度
stm32·单片机·嵌入式硬件
松涛和鸣6 小时前
72、IMX6ULL驱动实战:设备树(DTS/DTB)+ GPIO子系统+Platform总线
linux·服务器·arm开发·数据库·单片机
简单中的复杂6 小时前
【避坑指南】RK3576 Linux SDK 编译:解决 Buildroot 卡死在 host-gcc-final 的终极方案
linux·嵌入式硬件
上海合宙LuatOS6 小时前
LuatOS核心库API——【audio 】
java·网络·单片机·嵌入式硬件·物联网·音视频·硬件工程
Hhh __灏7 小时前
stm32的SRAM内存不足如何分析和优化?堆栈空间如何优化?
单片机
LS_learner7 小时前
Snapd和Apt—Linux 上两种完全不同的软件包管理系统
嵌入式硬件
点灯小铭7 小时前
基于51单片机的双档交流电压表设计与实现
单片机·嵌入式硬件·毕业设计·51单片机·课程设计·期末大作业