一、串口没有时钟线,怎么同步?(波特率)

这是串口和I2C/SPI最大的不同。异步串口没有时钟线,靠的是“提前约定速度”

  • 你配置的“波特率”(比如115200):就是告诉硬件“每秒钟采样多少次”

  • 硬件实现:STM32内部有一个“波特率发生器”(其实就是分频器)。你配置后,硬件会生成一个精确的采样时钟(通常是波特率的16倍或8倍)。

  • 类比:就像两个人约好“每秒眨一次眼”作为节拍。发报机按这个节拍发送高低电平,接收机也按这个节拍去读取引脚。只要误差在2%以内,硬件就能准确采样,不需要时钟线。

二、“配置参数”到底在配置什么?

你填写的结构体参数,是在告诉硬件“数据帧长什么样”,这样硬件才知道哪里是开头、哪里是结尾。

  • 起始位(硬件自动生成)空闲时TX引脚是高电平。当你要发数据,硬件自动把引脚拉低一个周期。接收方检测到这个“高→低”跳变,就知道“要开始干活了”。

  • 数据位(你配置 8/9位):硬件在起始位之后,按波特率节拍,逐位把电平锁存进移位寄存器

  • 停止位(你配置 1/1.5/2位):数据发完后,硬件自动把引脚拉高一段时间,表示“这一帧结束”。

你的角色:你只是把“帧格式”(几位数据、几位停止、有无校验)告诉了硬件。之后,硬件里的状态机就会自动去解析引脚上的电平序列,你完全不用管。

三、数据怎么从“引脚电平”变成“字节”?

不需要记忆,理解即可,这些都是硬件自动完成

  • 发送时:你把字节(比如 0x55写入 DR(数据寄存器)硬件瞬间把它复制到TDR(发送数据寄存器),TDR将数据移至接收移位寄存器并置位“接收完成标志”(TXE。然后,硬件的波特率时钟开始推动这个寄存器,每次把最低位推到TX引脚上。移位寄存器在波特率时钟驱动下,逐位把数据推到TX引脚(从起始位→数据位→停止位),移位寄存器完全移空,停止位已发出,TC标志位自动置1(传输完成)。

 硬件内部发生的事情(分4步)

步骤 硬件动作 标志位变化
① 写入DR 你写入的 0x55 被存入 TDR(发送数据寄存器) TXE标志位自动清零(因为寄存器不为空了)
② 搬运到移位寄存器 硬件自动把TDR的数据拷贝到 发送移位寄存器(此时TDR变空) TXE标志位自动置1(可以写下一个数据了)
③ 逐位发送 移位寄存器在波特率时钟驱动下,逐位把数据推到TX引脚(从起始位→数据位→停止位) TC标志位为0(发送未完成)
④ 发送完成 移位寄存器完全移空,停止位已发出 TC标志位自动置1(传输完成)
// 方式1:判断TXE(发送数据寄存器空)
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);  // 等待TDR变空
USART_SendData(USART1, next_byte);  // 发送下一个字节

// 方式2:判断TC(传输完成)
while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);  // 等待移位寄存器完全空
// 到这里,最后一个字节的停止位已经发出去了,可以关闭串口或进入低功耗
  • 接收时:RX引脚的电平变化,会触发硬件采样,写入 DR(数据寄存器),采样到的位被逐位移入接收移位寄存器接收移位寄存器将数据移至RDR(接收数据寄存器)置位“接收完成标志”(RXNE)。你只需要在中断或主循环里读这个寄存器就行。

硬件自动完成(你不需要主动触发)

步骤 硬件动作 标志位变化
① 检测起始位 RX引脚检测到高→低跳变,硬件开始按波特率采样
② 逐位移入 每收到1位,就移入接收移位寄存器(从LSB到MSB)
③ 完整字节 收到停止位后,硬件自动把接收移位寄存器的数据拷贝到 RDR(接收数据寄存器) RXNE标志位自动置1(寄存器有数据)
④ 通知CPU 如果开启了接收中断,此时触发 USART_IT_RXNE 中断 CPU在中断里读取 USART_DR

// 方式1:轮询读取
if (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) != RESET) {
    uint8_t data = USART_ReceiveData(USART1);  // 读取USART_DR,同时硬件自动清除RXNE
}

// 方式2:中断读取(推荐)
void USART1_IRQHandler(void) {
    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
        uint8_t data = USART_ReceiveData(USART1);  // 立即读走,防止溢出
    }
}

在STM32中,USART_DR 是一个读/写不同物理寄存器的同一地址

  • 写操作:写入 USART_DR → 实际写入 TDR(发送数据寄存器)

  • 读操作:读取 USART_DR → 实际读取 RDR(接收数据寄存器)

硬件自动根据你是读还是写,选择对应的寄存器。

// 写入:自动选择TDR
USART_SendData(USART1, 0x55);  // → 内部写USART_DR → TDR

// 读取:自动选择RDR
uint8_t data = USART_ReceiveData(USART1);  // → 内部读USART_DR ← RDR

RXNE(接收数据寄存器非空)标志位:读取DR寄存器时硬件自动清除,不需要手动操作!
但其他标志位(如ORE、IDLE、TC等):必须手动清除!

┌──────────────────────────────────────────────────────────┐
│                     USART1 寄存器组                                                                                                  │
├──────────────────────────────────────────────────────────┤
│  SR  (Status Register)     →  状态寄存器(只读,查询状态)                                                   │
│  DR  (Data Register)       →  数据寄存器(读/写,传输数据)                                                   │
│  BRR (Baud Rate Register)  →  波特率寄存器(配置波特率)                                                 │
│  CR1 (Control Register 1)  →  控制寄存器1(使能、中断配置)                                              │
│  CR2 (Control Register 2)  →  控制寄存器2(停止位等配置)                                                  │
│  CR3 (Control Register 3)  →  控制寄存器3(DMA、流控等)                                                 │
└──────────────────────────────────────────────────────────┘

SR位 名称 含义 何时置1 清除方式
Bit 0 PE 奇偶校验错误 接收到的数据奇偶校验不匹配 软件写0清除
Bit 1 FE 帧错误 停止位检测错误(如收到0电平) 软件写0清除
Bit 2 NF 噪声错误 引脚上检测到噪声 软件写0清除
Bit 3 ORE 溢出错误 RDR数据没读走,新数据又来了 软件写0清除
Bit 4 IDLE 空闲帧检测 RX线空闲超过1个字符时间 读SR→读DR
Bit 5 RXNE 接收数据寄存器非空 数据从移位寄存器搬到RDR 读DR自动清除
Bit 6 TC 发送完成 移位寄存器完全空(含停止位) 软件写0清除
Bit 7 TXE 发送数据寄存器空 TDR数据搬到了移位寄存器 写DR自动清除
Bit 8 LBD LIN断开检测 LIN总线检测到断开 软件写0清除
Bit 9 CTS CTS标志 硬件流控CTS引脚变化 软件写0清除

四、 中断和DMA的作用

你配好参数只是基础,能通讯不代表好用。如果你在主循环里死等 RXNE 标志位,CPU就被“锁死”了。

  • 中断:当接收移位寄存器装满了,硬件主动触发中断,CPU收到通知后去读数据。这样CPU可以在数据没来时去处理其他任务。

  • DMA(直接存储器访问):这是终极大招。配置好后,连中断都不用进。硬件收到数据后,直接通过DMA通道把 USART_DR 里的值搬运到你的数组里。等收完一帧,DMA才触发一次中断通知你“数据已到位”。


总结一下“为什么配参数就能用”:

你做的配置 硬件内部做了什么
配置波特率 设定内部采样时钟的分频系数
配置数据/停止/校验位 设定硬件状态机的“帧解码规则”
配置引脚复用 把引脚控制权交给串口硬件电路
使能串口 启动硬件状态机,开始监听引脚跳变

你配置的不是“软件算法”,而是“硬件电路的工作参数”。一旦参数设定,电路里的触发器、逻辑门、移位寄存器就开始自动运转,本质上和你配置定时器产生PWM是同一个逻辑——都是硬件自动完成,无需CPU逐位干预。

五、项目运用

1、串口中断 + 帧头帧尾解析

硬件配置

  • 开启串口接收中断(USART_IT_RXNE)。

  • 每收到1个字节就进中断,在中断里把数据存入缓冲区。

    //软件解析逻辑(状态机)
    // 协议定义
    #define FRAME_HEAD  0xAA
    #define FRAME_TAIL  0x55
    #define MAX_FRAME_LEN  64
    
    // 接收缓冲区
    uint8_t rx_buf[MAX_FRAME_LEN];
    uint8_t rx_index = 0;
    uint8_t frame_started = 0;
    
    // 在串口中断服务函数中调用
    void UART_RX_Handler(uint8_t data)
    {
        if (frame_started == 0) {
            // 等待帧头
            if (data == FRAME_HEAD) {
                frame_started = 1;
                rx_index = 0;
                rx_buf[rx_index++] = data;
            }
        } else {
            // 正在接收数据
            rx_buf[rx_index++] = data;
            
            // 检查是否收到帧尾
            if (data == FRAME_TAIL) {
                // 判断长度是否合法
                if (rx_index >= 5 && rx_index <= MAX_FRAME_LEN) {
                    // 解析完整指令
                    ParseFrame(rx_buf, rx_index);
                }
                frame_started = 0;  // 复位状态机
            }
            
            // 防溢出保护
            if (rx_index >= MAX_FRAME_LEN) {
                frame_started = 0;
            }
        }
    }
    如果数据里恰好包含 0xAA 或 0x55,状态机会误判!

    解决方案(二选一)

  • 转义字符:数据中的 0xAA 替换为 0xAA, 0x010x55 替换为 0xAA, 0x02

  • 改用长度帧:帧头 + 数据长度 + 数据 + 校验和(不用帧尾)。

// 在状态机解析到帧尾后调用
uint8_t VerifyFrame(uint8_t *buf, uint8_t len)
{
    uint8_t sum = 0;
    for (int i = 0; i < len - 1; i++) {
        sum += buf[i];
    }
    return (sum == buf[len - 1]);  // 最后一个字节是校验和
}
// 在主循环中增加超时检测
void CheckFrameTimeout(void)
{
    static uint32_t last_time = 0;
    if (frame_started && (millis() - last_time > 100)) {
        frame_started = 0;  // 超时复位
    }
}

2、串口 + 空闲中断(IDLE)

核心原理

  • 串口硬件检测到RX线空闲超过1个字节时间,自动置位 IDLE 标志位。

  • 你开启 USART_IT_IDLE 中断后,一包数据收完立刻触发,不用在中断里逐字节判断帧尾。

void UART_Init(void)
{
    // ... 串口基本配置(波特率、8N1等)
    
    // 开启接收中断和空闲中断
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
    USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
    
    // 使能串口
    USART_Cmd(USART1, ENABLE);
}

// 中断服务函数
void USART1_IRQHandler(void)
{
    uint8_t data;
    
    // 收到1个字节(逐字节处理)
    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
        data = USART_ReceiveData(USART1);
        // 可以存入环形缓冲区,也可以直接处理
        RingBuffer_Push(data);
    }
    
    // 检测到空闲(一帧结束)
    if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) {
        // ★★★ 关键步骤:必须先读SR再读DR,才能清除IDLE标志 ★★★
        USART_GetITStatus(USART1, USART_IT_IDLE);  // 读SR
        USART_ReceiveData(USART1);                 // 读DR
        
        // 触发帧处理标志(在主循环中处理)
        frame_ready_flag = 1;
    }
}
int main(void)
{
    while (1) {
        if (frame_ready_flag) {
            frame_ready_flag = 0;
            
            // 从环形缓冲区取出完整一帧数据
            uint16_t len = RingBuffer_GetLength();
            if (len > 0) {
                // 解析帧头帧尾(此时已经是完整一包了)
                ParseFrame(rx_buffer, len);
            }
        }
    }
}

优势

  • 中断里只做数据搬运和置标志,解析在主循环做,不阻塞中断。

  • IDLE中断自动识别帧结束,不需要你手动判断超时。

  • 可以配合DMA实现零CPU干预接收(见下一节)。

3、串口 + DMA + 空闲中断

硬件配置

  1. 开启串口DMA接收:DMA自动把数据从 USART_DR 搬运到内存数组。

  2. 开启空闲中断:一帧收完,触发IDLE中断。

  3. 在IDLE中断里:停止DMA,计算本次收到的长度,然后重新启动DMA。

#define RX_BUF_SIZE  256
uint8_t rx_dma_buf[RX_BUF_SIZE];
uint16_t rx_len = 0;

void UART_DMA_Init(void)
{
    // 1. 串口配置(同前,开启IDLE中断)
    USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
    
    // 2. DMA配置(循环模式,从外设到内存)
    DMA_InitTypeDef DMA_InitStructure;
    DMA_DeInit(DMA1_Channel5);  // USART1_RX 对应 DMA1 Channel5
    
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)rx_dma_buf;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize = RX_BUF_SIZE;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;  // 循环模式
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel5, &DMA_InitStructure);
    
    // 3. 使能DMA
    DMA_Cmd(DMA1_Channel5, ENABLE);
    
    // 4. 串口DMA接收使能
    USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
}

// 空闲中断处理(在USART1_IRQHandler中)
void USART1_IRQHandler(void)
{
    if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) {
        // 清除IDLE标志
        USART_ReceiveData(USART1);  // 读DR自动清除
        
        // ★★★ 计算接收到的数据长度 ★★★
        // DMA_CNDTRx 是剩余未传输的数据个数
        uint16_t remain = DMA_GetCurrDataCounter(DMA1_Channel5);
        rx_len = RX_BUF_SIZE - remain;
        
        // 如果长度>0,说明收到了一帧数据
        if (rx_len > 0) {
            // 方式1:直接处理(注意数据在rx_dma_buf中)
            ParseFrame(rx_dma_buf, rx_len);
            
            // 方式2:交给主循环处理(推荐)
            // memcpy(app_rx_buf, rx_dma_buf, rx_len);
            // frame_ready_flag = 1;
        }
        
        // ★★★ 重置DMA计数器,准备下一帧 ★★★
        DMA_Cmd(DMA1_Channel5, DISABLE);
        DMA_SetCurrDataCounter(DMA1_Channel5, RX_BUF_SIZE);
        DMA_Cmd(DMA1_Channel5, ENABLE);
    }
}

4、循环缓冲区

(20 封私信 / 65 条消息) 环形缓冲区(ring buffer)原理与实现详解 - 知乎

5、双缓冲区

双缓冲区是一种在数据传输过程中缓存数据的技术,可以提高数据传输的效率和可靠性。其基本原理如下:

  1. 双缓冲区由两个缓冲区组成,分别称为A缓冲区和B缓冲区。在数据传输过程中,发送方先将数据存储在A缓冲区,接收方从B缓冲区中读取数据。
  2. 接收方完成B缓冲区中数据的读取后,再通知发送方可以将下一批数据存储在A缓冲区中。
  3. 由于发送方和接收方分别使用不同的缓冲区,因此数据传输可以在不中断当前任务的情况下进行,并且可以保证数据传输的连续性和可靠性。
  4. 双缓冲区还可以通过设置缓冲区的大小和增加缓冲区的数量来满足不同的应用场景,以提高数据传输的效率和可靠性。
Logo

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

更多推荐