简介:面向嵌入式开发者的 STM32F103 DMA 串口收发实例工程,基于标准库实现 USART3 的接收与发送。资源核心解决串口通信中 CPU 频繁搬移数据导致负载过高的问题,围绕 DMA1 通道选择(发送 Channel5、接收 Channel6)、传输方向、源/目标地址与传输量设置,以及 USART3 时钟、波特率、数据格式配置和中断处理展开,适合正在学习 STM32 标准库或需要优化串口通信的开发者参考。压缩包共 171 个文件,主体为 30 个 c、31 个 h 源码文件,配合 33 个 s 启动文件、uvproj/uvopt 工程配置、uvgui 窗口布局和 hex/axf 固件输出;同时包含 map 映射、lst 反汇编、crf 编译列表等辅助文件,便于对照分析编译与链接结果,整体约 3.41MB。这些文件与源码配合,可完整复现从编译、链接到烧录验证的开发流程,便于快速二次开发。已有 3311 人学习下载。通过该工程可快速复用 USART3+DMA 收发框架,参考 DMA 单次/循环模式切换与半满/全满中断处理思路,减少调参与排错时间,也可烧录 hex 直接验证效果。
1. 用DMA把USART3从“中断泥潭”里救出来:一个串口数据搬运的颠覆方案
如果你写STM32F103的串口通信还在用“每收一个字节进一次中断”,那当数据量稍微上来一点,比如GPS模块每秒输出一串9600波特率的定位帧,或者和蓝牙模块做批量OTA升级,CPU就会被中断风暴打到喘不过气。这个项目做的就是一件很“反直觉”的事:收发数据全交给DMA去搬,CPU只在一帧数据完整到达时才被通知一次。整套方案基于标准库实现,不依赖HAL库,代码结构清晰,适合想搞懂DMA原理、或者正在为串口中断频繁而头疼的开发者直接拿来改。你不需要重新设计硬件,只需要在现有STM32F103最小系统板上改几行配置,就能把USART3的收发模式从“满负荷中断”换成“DMA安静搬运”,彻底解放CPU去跑主循环逻辑。
2. 项目结构与硬件映射:USART3在DMA1上的关键路径
2.1 硬件连接与引脚分配:PB10和PB11上的USART3
STM32F103共有5个串口,USART3是其中一个,默认复用引脚是PB10(发送)和PB11(接收)。如果你的板子把USART3重映射到了PD8/PD9,那配置会不同,但本文以最常见的PB10/PB11方案为准。注意,USART3挂载在APB1总线上,时钟频率默认36MHz(如果你把系统时钟配成72MHz),而DMA1是独立外设,挂在AHB总线上。这个区别决定了你初始化时钟时,要分别打开RCC_APB1Periph_USART3和RCC_AHBPeriph_DMA1。
DMA1有7个通道,USART3_TX对应DMA1_Channel2,USART3_RX对应DMA1_Channel3。这一点极其容易搞混,因为USART1_TX对应DMA1_Channel4,USART2_TX对应DMA1_Channel7,每次查参考手册都要翻半天。我的经验是直接把映射表抄在工程头文件注释里,免得下次重新查。DMA的请求映射是硬件固定的,你没法在代码里把Channel2改成Channel3给发送用,配错了就是收不到或发不出。
引脚配置也要注意复用功能。GPIOB_10和GPIOB_11需要配置为复用推挽输出和浮空输入,分别对应AF_PP和AF_IN(标准库用GPIO_Mode_AF_PP和GPIO_Mode_IN_FLOATING)。少了这一步,USART3根本不会输出波形,但初始化时也不会报错,典型“灵异现象”。我一般会在配置GPIO后加一行断言,检查GPIOB的配置寄存器是否正确写入了复用模式,调试时能少走半小时弯路。
2.2 标准库工程结构:外设驱动、头文件与中断服务
这个项目的源码建议按标准库的常见工程结构划分:stm32f10x_conf.h统一管理外设头文件,stm32f10x_it.c放中断服务函数,main.c做顶层初始化,usart3_dma.c/h封装专用的初始化和收发接口。不要把DMA配置散落在main函数里,因为DMA通道参数多,改起来容易漏,封装成USART3_DMA_Init()和USART3_DMA_SendBuffer()这种接口,后续移植到别的项目会很顺手。
工程文件清单大致如下:
| 文件 | 作用 |
|---|---|
main.c | 系统时钟配置、调用初始化函数、主循环业务逻辑 |
usart3_dma.c | USART3和DMA通道的初始化,接收环形缓冲管理,发送接口 |
stm32f10x_it.c | DMA接收半传输/完成中断、发送完成中断、USART空闲中断的处理 |
stm32f10x_conf.h | 外设头文件包含,启用需要的库模块 |
stm32f10x_dma.c | 标准库DMA驱动,不需要修改,直接调用 |
中断服务函数是核心。DMA接收有半传输和全传输两个中断事件,空闲中断是USART3本身的中断源,用来判断“一帧数据结束了”。这三个事件配合,可以实现不丢字节的接收逻辑。发送则可以只开DMA完成中断,也可以在轮询模式下轮询DMA_GetFlagStatus(),但项目里建议用中断,因为主循环不必阻塞等待发送结束。
2.3 为什么用DMA而不是直接寄存器轮询:从CPU占用率看收益
有人会说,USART3波特率只有115200,每秒约11.5KB,一个字节一个中断也才每秒一万多次,STM32F103主频72MHz完全能扛。但实战场景里,串口数据往往不是均匀到达的,可能是突发性的几百字节瞬间涌入,比如GPS模块冷启动时一次输出好几条NMEA句子。如果每个字节都进中断,中断栈抖动和现场保存的开销会叠加起来,导致主循环的实时任务(比如PWM控制、ADC采样)出现明显抖动。用DMA后,CPU只在一帧数据完整到达时被唤醒来解析缓冲区,中断次数从“每个字节一次”降到“每帧一次”,实测CPU占用率能下降接近一个数量级。这就是实用价值。
3. 核心代码拆解:DMA接收与发送的全链路实现
3.1 USART3初始化:配置波特率、使能DMA收发请求
先做USART3本身的初始化。这个步骤和普通串口初始化基本一样,但末尾多了两行关键使能语句,告诉USART3把接收和发送的DMA请求打开。
void USART3_DMA_Init(uint32_t baudrate) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; // 打开时钟:GPIOB、USART3、DMA1 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // PB10=TX, PB11=RX GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, &GPIO_InitStructure); // USART3配置:这里用115200-8-N-1,可改成其他波特率 USART_InitStructure.USART_BaudRate = baudrate; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART3, &USART_InitStructure); // 使能USART3的空闲中断,用于判断一帧数据接收完成 USART_ITConfig(USART3, USART_IT_IDLE, ENABLE); // 使能USART3的接收DMA请求和发送DMA请求,关键! USART_DMACmd(USART3, USART_DMAReq_Rx | USART_DMAReq_Tx, ENABLE); // 用空闲中断在NVIC里挂上USART3的IRQ NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); USART_Cmd(USART3, ENABLE); }逻辑说明:先配GPIO复用,再配串口参数,然后使能USART3的DMA请求,最后开中断。USART_DMACmd这一行是灵魂,只有把接收和发送的DMA请求都打开,DMA通道才会被触发。USART_ITConfig配的是空闲中断,这个中断的作用是:当接收线上一个停止位都收不到时,说明数据流停下来了,我们就在这个时刻去把DMA接收缓冲区里的数据“截获”下来,当作完整的一帧。注意,波特率参数通过USART_InitStructure.USART_BaudRate传入,这比把速率写死在函数里灵活得多。
参数说明:
baudrate:支持标准库内所有除零值,比如9600、19200、115200。修改后无需改DMA配置,DMA不管波特率。USART_Mode_Rx | USART_Mode_Tx:同时开启收发,确保DMA两个方向都能工作。USART_IT_IDLE:这是USART3自己的中断,不是DMA的中断。它依赖RX线路进入空闲态来触发,和DMA接收完成事件是两回事。
3.2 DMA通道配置:从外设到内存的数据搬运工
接下来配DMA1的通道2和通道3。通道2是发送(内存→USART3数据寄存器),通道3是接收(USART3数据寄存器→内存)。接收通常设置成循环模式,因为串口数据是源源不断进缓冲区的,循环模式会让DMA自动回卷,我们只需要关注“数据写到哪了”。发送则用正常模式,一次发送完就停,等下次调用再重新填充。
void DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; // 先关闭,再配置,最后开启,必须是这个顺序 DMA_Cmd(DMA1_Channel2, DISABLE); DMA_DeInit(DMA1_Channel2); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART3->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)usart3_tx_buffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 外设是目标,内存是源 DMA_InitStructure.DMA_BufferSize = USART3_TX_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_Normal; // 发送用正常模式,送完即止 DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel2, &DMA_InitStructure); // 接收通道,循环模式是核心 DMA_Cmd(DMA1_Channel3, DISABLE); DMA_DeInit(DMA1_Channel3); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART3->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)usart3_rx_buffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设是源 DMA_InitStructure.DMA_BufferSize = USART3_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_VeryHigh; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel3, &DMA_InitStructure); // 使能接收DMA的中断:传输一半和传输完成出触发 DMA_ITConfig(DMA1_Channel3, DMA_IT_HT | DMA_IT_TC, ENABLE); DMA_ITConfig(DMA1_Channel2, DMA_IT_TC, ENABLE); // 开启两个通道 DMA_Cmd(DMA1_Channel3, ENABLE); DMA_Cmd(DMA1_Channel2, ENABLE); }逻辑说明:DMA的配置核心是“指出谁把数据搬到谁那里”。这里用DMA_DeInit先复位通道,然后依次填外设地址、内存地址、方向、缓冲区大小、数据宽度、模式、优先级。接收通道的DMA_Mode_Circular是关键,一旦DMA的缓冲区写完,它会自动把写指针回卷到缓冲区头部,这样你永远有一个环形窗口在接收新数据,不需要手动重装。发送通道用DMA_Mode_Normal,因为每次发送的内容不同,最好重新设定缓冲区内容后再启动。
参数说明:
DMA_BufferSize:单位是“次传输”,这里一个字节一次,所以就是缓冲区字节数。接收缓冲区建议设成2的幂,方便环形索引做位运算。DMA_IT_HT:半传输中断,当DMA写满了缓冲区一半时触发,可以用来处理“分批数据传输”的场景,比如双缓冲。DMA_IT_TC:全传输完成中断,接收缓冲区写满或者说发送缓冲区全部送出后触发。DMA_PeripheralBaseAddr必须取&USART3->DR,不要写错成&USART3->SR,否则数据永远飞不走。
3.3 接收与发送完成中断:环形缓冲区与标志位
现在交互的关键是在中断里把DMA缓冲区读出来,并重新整理成可用的数据结构。接收侧我用一个简单的环形缓冲区,结构体如下:
#define RX_BUF_SIZE 256 #define TX_BUF_SIZE 128 volatile uint8_t usart3_rx_buffer[RX_BUF_SIZE]; volatile uint8_t usart3_tx_buffer[TX_BUF_SIZE]; volatile uint16_t rx_head = 0, rx_tail = 0; // head: 当前DMA写位置,tail: 应用读位置 volatile uint16_t rx_last_pos = 0; // 记录上次读取时DMA的位置用DMA的循环模式,配合DMA_GetCurrDataCounter(DMA1_Channel3)拿到当前未传输的字节数,从而推算出DMA已经写到了缓冲区哪个位置。接收中断里也处理空闲事件,当一帧数据完整到达时立即处理帧尾和DMA位置差值,把新数据拷贝到主循环可见的地方。
void USART3_IRQHandler(void) { if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) { // 读USART->SR和USART->DR,才能清除空闲中断标志 USART_ReceiveData(USART3); USART_ClearITPendingBit(USART3, USART_IT_IDLE); // 获取DMA当前未传输的数量,计算已经收到的数据位置 uint16_t current_pos = RX_BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Channel3); uint16_t received_len = (current_pos >= rx_last_pos) ? (current_pos - rx_last_pos) : (RX_BUF_SIZE - rx_last_pos + current_pos); if (received_len > 0) { // 保存数据到应用缓冲,并更新读位置 for (uint16_t i = 0; i < received_len; i++) { rx_buff[rx_tail] = usart3_rx_buffer[(rx_last_pos + i) % RX_BUF_SIZE]; rx_tail = (rx_tail + 1) % RX_BUF_SIZE; } rx_head = rx_tail; rx_last_pos = current_pos; data_ready_flag = 1; } } }逻辑说明:空闲中断触发代表当前没有新数据到来,此时检查DMA内部计数器,DMA_GetCurrDataCounter返回的是“还没搬运的字节数”,所以用缓冲区总长度减去它,就是DMA已经把数据写到的偏移。然后和上次读取的位置比较,算出这一段时间新收获了多长数据。接着把这些数据拷贝到自己的rx_buff,再从丢失的位置开始记录,等待主循环的处理。这个方式规避了“DMA溢出后旧数据丢了”的问题,因为循环模式让DMA总是在缓冲区里转圈,只要我们的应用读取足够快,就永远不丢数据。
参数说明:
rx_last_pos:用来做环形缓冲区的差分位置,初始为0,每次中断后更新。data_ready_flag:置1后主循环会去解析rx_buff,解析完了再清0,避免重复处理。
发送侧更简单,调用USART3_DMA_SendBuffer时填充目标数据,更新DMA源地址到发送缓冲区,再重启DMA通道。这里有个暗坑:DMA一旦开启,你改DMA_MemoryBaseAddr和DMA_BufferSize前必须先DMA_Cmd(DISABLE),否则配置不会生效。发送完成中断里把状态标志置0,方便上层知道一包已经发完了。
void USART3_DMA_SendBuffer(uint8_t *data, uint16_t len) { while (tx_busy_flag) ; // 等待上一次发送完成,防止覆盖 DMA_Cmd(DMA1_Channel2, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel2, len); DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)data; DMA_Init(DMA1_Channel2, &DMA_InitStructure); DMA_Cmd(DMA1_Channel2, ENABLE); tx_busy_flag = 1; }逻辑说明:发送过程中,主循环可能同时收到新的发送请求,这时如果直接改源地址和长度,会破坏第一次发送的数据,所以必须用tx_busy_flag做互斥。等待条件也可以是DMA_GetFlagStatus(DMA1_FLAG_TC2),但读标志直接轮询更省事。发送完成中断里把标志清零,这样下一个待发送数据才能进入。
4. 避坑指南:DMA与USART3联调的五个典型陷阱
4.1 现象:DMA接收不到数据,但普通中断方式正常
现象描述:程序里USART3的RX脚用查询中断方式能正常收到数据,但换成DMA后完全收不到,调试器里看接收缓冲区一直是空的。
原因分析:最常见的是DMA请求没有使能。USART外设默认不通知DMA搬运数据,你必须主动调用USART_DMACmd(USART3, USART_DMAReq_Rx, ENABLE)。第二个常见原因是DMA通道配错了,USART3接收固定对应DMA1_Channel3,不是Channel2,也不是Channel7。第三个原因是GPIO模式不对,PB11如果没配成浮空输入,接收引脚阻抗异常,信号到不了外设。
解决办法:先查USART_DMACmd有没有执行;再对着参考手册看DMA请求映射表,确认通道号。最后用逻辑分析仪或示波器看PB11有没有波形出来。如果波形正常但DMA不搬,用调试器读DMA_GetCurrDataCounter(DMA1_Channel3),如果返回值一直等于缓冲区大小,说明DMA从未触发,问题在外设请求到DMA之间的链路,而不是内存地址。
4.2 现象:发送数据错位,第一个字节丢失
现象描述:调用USART3_DMA_SendBuffer发送一串数据,上位机收到的内容整体右移一位,比如发送AB CD EF,收到的是CD EF 00或者?? AB CD。
原因分析:DMA启动后,第一次传输发生在外设请求到来时,而你在调用DMA_SetCurrDataCounter后马上ENABLE,此时USART3的发送数据寄存器(DR)可能尚未准备好,导致第一个字节被吞掉。另一个原因是在DMA禁止状态下改源地址时,你改了DMA_MemoryBaseAddr,却没把新的值写进去,就执行了DMA_Init,但用的是旧结构体,导致发送数据从错误位置开始。
解决办法:发送前先等一下,确保TXE标志为1,或者不直接改DMA地址,而是把数据复制到固定的usart3_tx_buffer,再让DMA从该缓冲区发送。这样DMA的源地址固定,你只需要更新内容,避免了改地址带来的时序风险。发送完全以后,再等TC标志,不要立刻关闭DMA,否则最后一个字节还没移出就会被截断。
4.3 现象:DMA传输完成标志不置位,永远卡在等待循环
现象描述:发送函数里用while (tx_busy_flag);等待发送完成,但发送完一次以后,tx_busy_flag再也没被清0,主循环卡死。
原因分析:发送完成中断没有正确清除标志。DMA的DMA_IT_TC中断,必须在中断处理函数里清除DMA_ClearITPendingBit(DMA1_IT_TC2),否则中断标志一直挂着,中断服务函数重复进入,但你的状态变量或标志没有清。另外一个原因是没开启DMA的发送完成中断,只开了DMA_IT_HT,结果发送完成后无中断通知,tx_busy_flag也永远不被清。
解决办法:要么在DMA1_Channel2_IRQHandler里检查DMA_GetITStatus(DMA1_IT_TC2),确认置位后清中断并清标志;要么就不依赖中断,发送函数里直接轮询DMA_GetFlagStatus(DMA1_FLAG_TC2),循环等这个标志。轮询适合短帧,中断适合长帧或高并发场景。我一般把标志的处理放在中断里,这样主循环不用阻塞。
4.4 现象:接收数据中偶尔出现一个0x00或丢一帧
现象描述:DMA循环接收模式下,主循环读取缓冲区时,偶发出现一两个0x00,或者一帧数据整体丢掉一部分。
原因分析:这是典型的“应用读速度跟不上DMA写速度”导致缓冲区覆盖。DMA在循环模式里持续往缓冲区写,如果主循环没有及时处理,新数据会覆盖旧数据,切中间留出的空洞读出来就是0x00。也可能是空闲中断发生时,DMA缓冲区内的数据尚未完全稳定,你立刻去读,读到了半截状态。
解决办法:加大RX_BUF_SIZE,从128改成512或1024,给应用更多喘息时间。同时,在空闲中断中不要立即复制大量数据,而是先记录位置,把复制动作放到主循环里做,中断里只做标志置位。或者,把DMA接收改成固定长度帧模式,配合半传输和全传输中断做双缓冲处理,这样即使DMA写满了一次缓冲区,你还有另一个缓冲区可以解析。
4.5 现象:初始化时STM32直接HardFault,或调试器卡在DMA配置
现象描述:在调用DMA_Init后程序跳转HardFault_Handler,或者在RCC_AHBPeriphClockCmd这里卡死。
原因分析:AHB外设时钟没开好,或者RCC_AHBPeriphClockCmd参数写错了。STM32F103的DMA1时钟是RCC_AHBPeriph_DMA1,不是RCC_APB2Periph_DMA1,很多人习惯照抄APB2的写法,导致DMA寄存器访问不到。另一个原因是数组越界,比如DMA缓冲区定义成8字节,但你在初始化时写入了16字节长度,DMA写越界到了其他变量。
解决办法:检查三条时钟语句,APB1管USART3,APB2管GPIOB,AHB管DMA1,一 一对应。同时检查所有数组定义和DMA_BufferSize是否匹配,发送长度永远不要超过缓冲区大小,这是我在多个项目里踩过最深的坑。建议初始化后调用if (DMA_GetFlagStatus(DMA1_FLAG_TC2) == SET)做一次快速自检,确认DMA能正常工作再进主循环。
5. 二次开发思路:让USART3 DMA驱动适配你的项目
如果你想把这个工程直接捡起来用到自己的板子上,第一件事就是改波特率和数据格式。把USART3_DMA_Init(115200)改成USART3_DMA_Init(9600)很简单,但要注意如果你的传感器是8位数据、偶校验、2停止位,那USART_WordLength_8b要改成USART_WordLength_9b并打开USART_Parity_Even。注意,9位数据模式下的DMA搬运依然是以字节为单位,DMA外设数据宽度必须也改成DMA_MemoryDataSize_HalfWord,否则高低字节会错位,这是一个很容易被忽视的坑。
更大的改造方向是给接收增加“双缓冲”机制。目前的环形缓冲区适合非实时解析,但如果你的应用需要把USART3收上来的数据直接转发给另一个外设,比如一路CAN总线或Flash写入,你可以开两个接收缓冲区,DMA半传输中断填充缓冲A,全传输中断填充缓冲B,主循环在处理A的时候DMA已经在写B,两边互不阻塞。你需要做的改动就是把DMA_Mode_Circular改成DMA_Mode_Normal,并分别在DMA_IT_HT和DMA_IT_TC里重启DMA并切换缓冲区指针。每个缓冲区的长度固定,重启一次DMA后,又要重新设置DMA_SetCurrDataCounter,这一行千万别漏。
再往上走一层,你想让它更省心,可以把发送接口改造成队列模式。现在USART3_DMA_SendBuffer是“一次发完,发完再回”,如果你的业务逻辑时不时要连发两三包数据,一包还没发完,另一包请求就来了,锁机制就会把第二次请求挡掉。做法是把发送缓冲区改成一个FIFO队列,SendBuffer只是把数据入队,实际发送在DMA完成中断里一包一包地排队发送。这样主循环永远不会因为发送而阻塞,哪怕数据再密集,也只是队列消耗快慢的问题。核心实现思路是不在调用者线程里启动DMA,而是在中断里检查队列是否为空,非空则取下一包数据并启动DMA。这套代码用熟了以后,对任何串口收发场景都能一招通吃。
我自己的习惯是每次拿到新板卡,第一件事就是验证DMA通道映射表对不对。从那以后,我每次初始化的最后一步强制跑一遍DMA_GetCurrDataCounter和USART_GetFlagStatus的组合自检,确认数据和预期一致再往下写业务。这套“先测底层链路,再写上层逻辑”的流程,帮我避开了无数个“莫名其妙没数据”的深夜。希望这篇解析能让你少踩几个我踩过的坑,顺利把USART3的DMA收发跑起来。
本文还有配套的精品资源,点击获取