1.串口的简介和特性(部分摘自STM32F10xxx参考手册USART通用同步异步收发器部分)

1.1.USART介绍

通用同步异步收发器(USART)提供了一种灵活的方法与使用工业标准NRZ异步串行数据格式的 外部设备之间进行全双工数据交换。USART利用分数波特率发生器提供宽范围的波特率选择。 它支持同步单向通信和半双工单线通信,也支持LIN(局部互连网),智能卡协议和IrDA(红外数据 组织)SIR ENDEC规范,以及调制解调器(CTS/RTS)操作。它还允许多处理器通信。 使用多缓冲器配置的DMA方式,可以实现高速数据通信。

1.2.USART主要特性

全双工的,异步通信

● NRZ标准格式

● 分数波特率发生器系统

发送和接收共用的可编程波特率,最高达4.5Mbits/s

可编程数据字长度(8位或9位)

可配置的停止位-支持1或2个停止位

● LIN主发送同步断开符的能力以及LIN从检测断开符的能力

─ 当USART硬件配置成LIN时,生成13位断开符;检测10/11位断开符

发送方为同步传输提供时钟

● IRDA SIR 编码器解码器 ─ 在正常模式下支持3/16位的持续时间

● 智能卡模拟功能

─ 智能卡接口支持ISO7816-3标准里定义的异步智能卡协议

智能卡用到的0.5和1.5个停止位

单线半双工通信

● 可配置的使用DMA的多缓冲器通信

─ 在SRAM里利用集中式DMA缓冲接收/发送字节

● 单独的发送器和接收器使能位

● 检测标志 ─ 接收缓冲器满

─ 发送缓冲器空

─ 传输结束标志

校验控制

─ 发送校验位

─ 对接收数据进行校验

● 四个错误检测标志通用同步异步收发器(USART)

─ 溢出错误

─ 噪音错误

─ 帧错误

─ 校验错误

● 10个带标志的中断源

─ CTS改变

─ LIN断开符检测

─ 发送数据寄存器空

─ 发送完成

─ 接收数据寄存器满

─ 检测到总线为空闲

─ 溢出错误

─ 帧错误

─ 噪音错误

─ 校验错误

● 多处理器通信 -- 如果地址不匹配,则进入静默模式

● 从静默模式中唤醒(通过空闲总线检测或地址标志检测)

● 两种唤醒接收器的方式:地址位(MSB,第9位),总线空闲

1.3.USART的工作模式

USART(Universal Synchronous/Asynchronous Receiver/Transmitter)是一种通用串行通信接口,支持同步和异步两种传输模式。其核心功能是通过 TX(发送) 和 RX(接收) 引脚实现数据的串行传输,并可扩展硬件流控和同步时钟线以增强通信可靠性。

1.3.1.TX 与 RX 引脚
  • TX(Transmit):数据发送引脚,将并行数据转换为串行数据输出。
  • RX(Receive):数据接收引脚,将接收到的串行数据转换为并行数据。
  • 异步模式:无需时钟线,依赖预定义的波特率(Baud Rate)实现同步。数据帧通常包括起始位、数据位、校验位和停止位。
1.3.2.硬件流控

硬件流控通过 RTS(Request to Send) 和 CTS(Clear to Send) 引脚协调收发双方的数据流,避免缓冲区溢出:

  • RTS:由发送方控制,表示准备好发送数据。
  • CTS:由接收方控制,表示准备好接收数据。
  • 工作流程:发送方检测 CTS 信号为低电平时才发送数据,接收方通过 RTS 信号暂停或恢复数据流。
1.3.3.同步模式与时钟线

在同步模式下,USART 通过 SCLK(Serial Clock) 引脚提供时钟信号,实现严格时序同步:

  • 主从设备:主设备(如 MCU)提供 SCLK,从设备(如传感器)同步时钟收发数据。
  • 数据对齐:时钟上升沿或下降沿触发数据采样,确保稳定性。
  • 优势:相比异步模式,同步模式支持更高传输速率,但需额外时钟线连接。
1.3.4典型应用场景
  • 异步模式:UART 通信(如 MCU 与 PC 串口通信)。
  • 同步模式:SPI/I2C 替代方案或高速设备通信。
  • 硬件流控:高波特率或长距离通信(如工业 RS-485)。

USART 特性字长设置

发送器:配置停止位

接收器:起始位侦测

1.4.串口收发顺序就好比“数据结构”里面的队列

【我一开始在学的时候恍惚的认为是栈了(下面是对三者的详细阐述)】(个人理解兼AI回答)

核心区别:栈 (Stack) vs. 队列 (Queue) vs. 串口缓冲区

特性 数据结构 -  数据结构 - 队列 STM32 串口收发
数据存取原则 后进先出 先进先出 先进先出
操作比喻 压栈, 弹栈。像叠盘子,你只能从顶部取。 入队, 出队。像单行道排队,先来的人先接受服务。 发送:数据按写入顺序依次发出。
接收:数据按到达顺序依次存入缓冲区。
典型应用 函数调用、中断处理、表达式求值 消息传递、打印任务调度、网络数据包处理 USART:异步串行通信

结论:串口的数据流更接近于一个“队列”(Queue),而不是“栈”(Stack)。


1.4.1.深入理解STM32串口的“缓冲区”

STM32的串口(USART/UART)硬件本身包含两个关键寄存器,它们本质上是非常小的硬件队列

  1. 发送数据寄存器:当你把要发送的数据写入这个寄存器,串口硬件会自动将数据一位一位地通过TX引脚发送出去。

  2. 接收数据寄存器:当串口从RX引脚一位一位地接收到一个完整的数据(如8位)后,会将数据存入这个寄存器,等待CPU或DMA来读取。

问题来了: 这两个寄存器通常只能存放一个字节(8位)的数据。如果数据来得太快,CPU来不及处理怎么办?

这就是STM32更强大的地方,它通过以下方式扩展了这个“队列”:

1.4.2.使用查询或中断方式
  • CPU主动查询:程序不断检查接收寄存器是否有新数据。这效率低下,且容易丢失数据(如果在新数据到来时,旧数据还没被读走)。

  • 中断:这是更常用的方式。

    • 发送中断:当发送数据寄存器了(数据已发出),会产生中断,告诉CPU:“我可以发送下一个数据了”。

    • 接收中断:当接收数据寄存器收到新数据,会产生中断,告诉CPU:“快来取数据,不然下一个数据来了可能会覆盖它”。

    • 此时,程序员需要在中断服务函数中手动管理一个软件缓冲区(数组)。这个软件缓冲区就是一个在内存中实现的队列。接收中断函数将数据从硬件寄存器“出队”,然后放入软件缓冲区的“队尾”。

1.4.3.使用DMA——最像“自动队列”的方式

DMA是解决这个问题的终极武器,它使得串口通信非常接近你想象中的“自动压栈/弹栈”。

  • 发送:你只需要把要发送的一串数据(比如一个数组)的首地址长度告诉DMA和串口。DMA会自动地、无需CPU干预地将数据从内存这个“大队列”中搬运到串口的发送数据寄存器中。CPU此时可以去做其他事情。

  • 接收:你提前开辟好一段内存作为接收缓冲区(一个数组),并将其首地址最大长度告诉DMA和串口。此后,每收到一个字节,DMA都会自动地将其从接收数据寄存器搬运到你指定的内存缓冲区中,并自动更新地址。整个过程CPU完全不用操心。

在这种情况下:

  • 你的内存数组就是一个大的“队列”缓冲区。

  • DMA的职责就是自动执行“入队”(接收)和“出队”(发送)操作。

  • CPU只需要在DMA完成传输(或传输一半)时产生中断,然后去处理缓冲区里已经排好队的数据即可。

2.串口的对应操作

2.1.使用串口前必须开启时钟,有了这个时钟初始化才能进行接下来操作
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
//对USART1进行时钟初始化,USART1默认GPIO口为PA9和PA10,如果要启用其他IO口进行
//USART1操作,就必须对USART1进行重映像
2.2.在启用时钟后,需开启这部分的“总开关”
void USART_Cmd(USARTTypeDef *USARTx,FunctionalState NewState);
//作用:控制USART模块的使能和禁止(总开关)
//*USARTx串口名称
//FunctionalState NewState:ENABLE-使能;DISABLE-禁止
2.3.然后对串口进行初始化

波特率、数据位长度、停止位长度、校验方式、数据收发方向 

void USART_Init(USARTTypeDef *USARTx,//串口名称
USART_InitTypeDef *USART_InistStruct);//初始化的参数
作用:初始化串口

struct USART_InitTypeDef{

uint32_t USART_BaudRate;
// 波特率

uint16_t USART_WordLength;
//数据位长度 
- USART_WordLength_8b
- USART_WordLength_9b

uint16_t USART_StopBits;
// 停止位长度
-USART_StopBits_0_5
-USART_StopBits_1
-USART_StopBits_1_5
-USART_StopBits_2

uint16_t USART_Parity;
// 校验方式
- USART_Parity_No
- USART_Parity_Even
- USART_Parity_0dd

uint16_t USART_Mode;
// 数据收发方向 
- USART_Mode_Tx
- USART_Mode_Rx
- USART_Mode_Tx | USART_Mode_Rx
}

###初始化USART1  设置波特率115200,数据位8位,停止位1位,无校验###

RCC_APB2PerihClockCmd(RCC_APB2Periph_USART1,ENABLE);//开启时钟

USART_InitTypeDef USART_InitStruct;

USART_InitStruct.USART_BaudRate =115200;//波特率115200
USART_InitStruct. USART_Mode = USART_Mode_Tx | USART_Mode_Rx; // X
USART_InitStruct.USART_WordLength =USART_WordLength_8b;//8位数据位
USART_InitStruct.USART_Parity=USART_Parity_No;// 无校验
USART_InitStruct.USART_StopBits=USART_StopBits_1;// 1位停止位

USART_Init(USART1, &USART_InitStruct);
2.4.对GPIO口进行初始化
GPIO_InitTypeDef GPIO_InitStruct;
// Tx PA9 复用输出推挽 GPIoA
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStruct. GPIO_Pin= GPIO_Pin_9; //PA9
GPIO_InitStruct. GPIO_Mode = GPIO_Mode_AF_PP; // 复用输出推挽模式
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_10MHz;// 最大速度
GPIO_Init(GPIOA, &GPIO_InitStruct);

// Rx PA10 输入浮空 输入上拉
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;//输入上拉模式
//由于这里是输入模式,所以不用对Speed进行设置(因为这是从外部输入,而不是从单片机自生引出)
GPIO_Init(GPIOA, &GPIO_InitStruct);
2.5.如果要对串口(如USART1)进行重映射
配置项 USART1_REMAP 值 USART1_TX 引脚 USART1_RX 引脚 说明
默认复用功能 0 (或 不重映射) PA9 PA10 芯片复位后的默认状态,无需开启AFIO时钟的重映射控制。
重映像功能 1 (或 部分重映射) PB6 PB7 需要开启AFIO时钟,并配置AFIO_MAPR寄存器的USART1_REMAP位为1。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//使能AFIO模块的时钟
GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE); // USART1_REMAP=1

GPIO_InitTypeDef GPIO_InitStruct;
// Tx PB6 输出推挽
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStruct. GPIO_Pin = GPIO_Pin_6;
GPIO_InitStruct.GPIO_Mode =GPIO_Mode_AF_PP;// 复用输出推挽模式
GPIO_InitStruct.GPIO_Speed =GPIO_Speed_10MHz;// 最大速度
GPIO_Init(GPIOB, &GPIO_InitStruct);

// Rx PB7 输入浮空 输入上拉
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStruct. GPIO_Pin = GPIO_Pin_7;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;//模式
GPIO_Init(GPIOB, &GPIO_InitStruct);

其中“使能AFIO模块的时钟”是使用AFIO任何功能(如引脚重映射、外部中断配置)之前必须进行的一个基础且必要的步骤。

使能AFIO时钟 给AFIO模块供电,让它开始工作。
配置AFIO功能 设置重映射、外部中断线等具体功能。
2.6.状态标志位
2.6.1TXE(Transmit Data Register Empty)
  • 定义:TXE标志位表示发送数据寄存器(TDR)是否为空。当TDR中的数据被传输到移位寄存器(准备发送)时,TXE会被硬件置位。
  • 触发条件:TDR中的数据被转移到移位寄存器后自动置位。
  • 用途:通常用于检查是否可以写入新数据到TDR。若TXE=1,表示可以发送新数据;若TXE=0,表示当前数据未完全转移,需等待。
  • 清除方式:向TDR写入数据后,硬件自动清除TXE标志。
2.6.2.TC(Transmission Complete)
  • 定义:TC标志位表示整个发送过程(包括移位寄存器的数据发送)是否完成。当移位寄存器中的数据全部发送完毕且TDR为空时,TC被置位。
  • 触发条件:移位寄存器发送完最后一位数据且TDR为空(无新数据待发送)。
  • 用途:用于判断一帧数据是否完全发送完毕。例如,在需要严格时序控制的场景(如RS-485切换方向)时,需等待TC=1。
  • 清除方式:读取状态寄存器(SR)后写入数据寄存器(DR)或直接通过软件清除。
    FlagStatus USART_GetFLagStatus
    (USARTTypeDef *USARTx,//串口名称
    uint16_t USART_FLAG);//要查询的标志位
    作用:查询USART标志位的值。返回值:RESET-0; SET-1
    
    标志位:
    USART_FLAG_TXE // TXE 
    USART_FALG_RXNE // RXNE
    USART FLAG_TC // TC 
    USART_FLAG_PE // PE
    
    
    if(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == SET){//判断发送数据寄存器是否为空
    
    }
    if(USART_GetFlagStatus(USART1,USART_FLAG_TC) == SET){//判断数据发送是否完成
    
    }
    2.7.发数据(1.4里面有对串口收发数据的理解)
    void USART_SendData(
    USARTTypeDef *USARTx,//串口名称
    uint16_t Data);//要发送的数据
    
    作用:把要发送的数据写入到发送数据寄存器里
    
    // 发送字节0x01
    USART_SendData(USART1, 0x01);
    //@作用:使用串口一次性发送多个字节
    //@参数:pData - 要发送的数据 Size- 字节的数量
    void My_USART_SendBytes(USART_TypeDef *USARTx, uint8_t *pData, uint16_t Size)
    {
    
    for(uint32_t i = 0; i < Size; i++){
    //#1. 等待发送数据寄存器空
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    //#2. 将要发送的数据写入到发送数据寄存器
    USART_SendData(USARTx, pData[i]);
    }
    //#3. 等待数据发送完成
    while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
    }
    2.7.1.fputc函数(要引人C语言的stdio.h头文件)实现当我们printf一段字符生成化字符串然后通过串口发出去
    const char *strName = "Tan";
    
    printf("Hi, %s! Nice to meet you! \r\n", strName);
    
    //生成格式化字符串
    Hi, Tom! Nice to meet you! \r\n
    
    //通过fputc发送到控制台
    _weak int fputc(int ch, File f){
    }
    
    //因为单片机用不了控制台,是通过串口形式发送的,重新写fputc
    int fputc(int ch, File f){
    }
    
    

    简单执行操作:

    int fputc(int ch, FILE *f){
    // #1.等待发送数据寄存器为空
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    //#2. 写入发数据寄存器当中
    USART_SendData(USART1, (uint8_t)ch);
    return ch;
    }

    ###fputc函数的基本功能###

    fputc是C标准库中的函数,用于将一个字符写入指定的文件流。其原型为:

    int fputc(int char, FILE *stream);
    
  • char:要写入的字符(以int类型传递,实际写入低8位)。
  • stream:目标文件流指针(如stdout、文件流或自定义流)。
  • 返回值
    • 成功时返回写入的字符(unsigned char 类型)。
    • 失败时返回 EOF(通常为 -1)。
      #include <stdio.h>
      
      int main() {
          // 向屏幕写入字符
          fputc('A', stdout);  // 输出字符 'A'
      
          // 向文件写入字符
          FILE *file = fopen("example.txt", "w");
          if (file != NULL) {
              fputc('B', file);  // 写入字符 'B' 到文件
              fclose(file);
          }
          return 0;
      }
      

      相关函数

    • putc:功能与 fputc 相同,但可能通过宏实现(通常性能更高)。
    • fgetc:从文件流中读取单个字符。
    • fputs:写入字符串到文件流。
2.8.收数据

读取数据接口

uint16 t USART_ReteiveData(USARTTypeDef *USARTx);//串口名称
作用:从接收数据寄存器读取数据

同样要等待接收数据寄存器非空 

//#1. 等待接收数据寄存器非空_
while(USART_GetFlagStatus(USARTx, USART_FLAG_RXNE) == RESET;
//#2. 接收数据
uint8_t btyeRcvd = USART_ReceiveData(USARTx);
//#3. 处理数据

###对单片机发送数据实现STM32板载LED的亮灭###

板载LED接的是PC13引脚,为开漏输出GPIO_Mode_Out_OD

while(1){
//#1. 等待接收数据寄存器RDR非空
while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
//#2. 读取数据
uint8_t byteRcvd = USART_ReceiveData(USART1);
//#3. 对数据进行处理
if(byteRcvd == '0')
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);//亮灯
else if(byteRcvd == '1')
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET); //灭灯
}

3.帧错误、噪声错误、溢出错误(FE、NE、ORE)

这三个错误都发生在数据接收的过程中,与数据的帧格式 和采样 有关。当这些错误发生时,通常意味着物理线路有干扰、波特率不匹配、或者对方设备发送有问题。


3.1. FE - 帧错误

  • 全称: Framing Error

  • 触发条件: 当接收器没有在预期的时刻检测到停止位(Stop Bit)时,该标志位会被置起。

  • 详细解释

    • 串口协议规定,每个数据帧的结尾必须有一个或多个停止位(通常为1位),其电平为高电平(逻辑1)。

    • 接收器在采样完数据位后,会在帧的中间位置采样停止位。如果此时采样到的不是高电平,而是低电平,它就认为这个“帧”的结构不完整或被破坏了,于是产生帧错误。

  • 常见原因

    1. 波特率不匹配:这是最常见的原因。例如,发送方以115200的波特率发送,而接收方设置为9600。接收方会错误地解读比特流,导致它找不到正确的停止位位置。

    2. 线路干扰:噪声或干扰可能在停止位期间将高电平拉低。

    3. 发送方驱动能力不足线路断开/接触不良

3.2. NE - 噪声错误

  • 全称: Noise Error

  • 触发条件: 在接收数据位的过程中,检测到了电平跳变(噪声)

  • 详细解释

    • 为了抗干扰,STM32的串口接收器对每个数据位会进行多次采样(通常是3次采样,取多数值)。

    • 如果在采样期间,检测到了电平的意外跳变(例如,三次采样结果不一致,有的是1,有的是0),硬件就认为这个比特受到了噪声干扰,并置起NE标志。同时,接收到的数据仍然会被存入接收数据寄存器。

  • 与FE的区别

    • FE 发生在停止位

    • NE 发生在数据位(也可能包括校验位)。FE意味着整个帧无效,而NE意味着这个帧的某个数据位可能不可靠,但硬件仍然尽力接收了它。

  • 常见原因

    1. 强烈的电磁干扰

    2. 线路接触不良,时通时断。

    3. 接地问题。

3.3. ORE - 溢出错误

  • 全称: Overrun Error

  • 触发条件: 新的数据已经接收完成,但旧的数据还未从接收数据寄存器中被读取

  • 详细解释

    • 这是最需要重视的错误,因为它直接导致数据丢失

    • 串口的接收数据寄存器只能存放一个字节的数据。当收到一个字节后,该数据会从移位寄存器转移到接收数据寄存器中,并置起标志位(如RXNE)。

    • 如果CPU或DMA没有及时读取这个数据,而下一个字节已经接收完毕,那么新数据将无处安放,此时就会发生溢出错误。新数据会被丢弃,ORE标志被置起。

  • 常见原因

    1. 中断响应不及时:主程序忙于处理高优先级任务,导致串口接收中断迟迟得不到响应。

    2. 没有使用DMA或DMA配置不当:在高速、大数据量通信时,使用查询或普通中断方式很容易导致溢出。使用DMA是避免溢出的最佳方法,因为DMA可以在无CPU干预的情况下将数据直接搬运到指定的大缓冲区中。

    3. 程序逻辑错误:例如,在中断服务函数中忘了读取数据寄存器。

3.4. 奇偶校验错误

3.4.1. 奇偶校验的目的

奇偶校验是一种非常简单的错误检测机制,用于在数据传输过程中检查单个比特是否发生了错误。它在每个数据帧中添加一个额外的位(校验位)。

  • 偶校验: 确保数据位 + 校验位中 ‘1’ 的总个数为偶数

  • 奇校验: 确保数据位 + 校验位中 ‘1’ 的总个数为奇数

举例说明(偶校验)
假设要发送的数据字节是 1100 1010(其中 ‘1’ 的个数是4个,已经是偶数)。

  • 发送方(STM32或其他设备)会计算‘1’的个数。因为是偶校验,且‘1’的个数已是偶数,所以校验位设置为 ‘0’

  • 发送方发送完整的帧:数据位 1100 1010 + 校验位 0

  • 接收方收到后,计算数据位中‘1’的个数(4个),并查看校验位(0)。

  • 接收方验证:4(数据位‘1’的个数) + 0(校验位值) = 4,是偶数。校验通过,不产生PE错误

3.4.2. 奇偶校验错误(PE)的发生

现在,假设在传输过程中,有一个比特因干扰发生了翻转,比如最高位从‘1’变成了‘0’。接收方收到的数据位变成了 0100 1010

  • 接收方计算收到的数据位中‘1’的个数:0100 1010 中有3个‘1’(是奇数)。

  • 接收方看到的校验位仍然是 ‘0’。

  • 接收方验证:3(数据位‘1’的个数) + 0(校验位值) = 3,是奇数。

  • 但是,我们配置的是偶校验,期望结果是偶数。实际结果与期望不符

  • 此时,硬件就会判定这次传输不可靠,并置起 PE (Parity Error) 标志位

3.4.3.PE错误的常见原因
  1. 电气噪声和干扰:这是最主要的原因,导致比特位翻转。

  2. 波特率不匹配:虽然波特率不匹配更容易引起FE(帧错误),但在某些情况下也可能导致PE。

  3. 双方配置不一致:一方设置为奇校验,另一方设置为偶校验或无校验。

###总结###

错误标志 全称 触发原因 后果 关键区别
FE 帧错误 停止位采样不为1 当前帧结构错误,数据不可信 发生在帧的结尾
NE 噪声错误 数据位采样时检测到电平跳变 数据位可能出错,但数据仍被接收 发生在数据位/校验位
ORE 溢出错误 新数据到来,旧数据未被读取 数据丢失,是最严重的错误 处理速度有关,与数据内容无关
PE 奇偶校验错误 使能校验后,接收到的数据其校验位与计算值不匹配 只能检测奇数个比特错误(如1位、3位错误),无法检测偶数个比特错误。开销大(每帧多传一位),在现代高速通信中较少使用,常用于一些简单的工业控制器或老式设备。 一种单比特错误检测机制

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐