STM32自学笔记3-串口
一、串口没有时钟线怎么同步波特率这是串口和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 ← RDRRXNE接收数据寄存器非空标志位读取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) → 控制寄存器3DMA、流控等 │└──────────────────────────────────────────────────────────┘SR位名称含义何时置1清除方式Bit 0PE奇偶校验错误接收到的数据奇偶校验不匹配软件写0清除Bit 1FE帧错误停止位检测错误如收到0电平软件写0清除Bit 2NF噪声错误引脚上检测到噪声软件写0清除Bit 3ORE溢出错误RDR数据没读走新数据又来了软件写0清除Bit 4IDLE空闲帧检测RX线空闲超过1个字符时间读SR→读DRBit 5RXNE接收数据寄存器非空数据从移位寄存器搬到RDR读DR自动清除Bit 6TC发送完成移位寄存器完全空含停止位软件写0清除Bit 7TXE发送数据寄存器空TDR数据搬到了移位寄存器写DR自动清除Bit 8LBDLIN断开检测LIN总线检测到断开软件写0清除Bit 9CTSCTS标志硬件流控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 空闲中断硬件配置开启串口DMA接收DMA自动把数据从USART_DR搬运到内存数组。开启空闲中断一帧收完触发IDLE中断。在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、双缓冲区双缓冲区是一种在数据传输过程中缓存数据的技术可以提高数据传输的效率和可靠性。其基本原理如下双缓冲区由两个缓冲区组成分别称为A缓冲区和B缓冲区。在数据传输过程中发送方先将数据存储在A缓冲区接收方从B缓冲区中读取数据。接收方完成B缓冲区中数据的读取后再通知发送方可以将下一批数据存储在A缓冲区中。由于发送方和接收方分别使用不同的缓冲区因此数据传输可以在不中断当前任务的情况下进行并且可以保证数据传输的连续性和可靠性。双缓冲区还可以通过设置缓冲区的大小和增加缓冲区的数量来满足不同的应用场景以提高数据传输的效率和可靠性。
