STM32串口DMA发送10KB数据实战:解放CPU,提升系统实时性
2026/6/5 13:11:53 网站建设 项目流程

1. 项目概述:当串口遇上DMA,解放CPU的实战演练

搞嵌入式开发的朋友,尤其是玩STM32的,对串口通信肯定不陌生。无论是调试打印还是设备间数据交换,串口都是最常用、最直接的通信方式。但不知道你有没有遇到过这样的场景:需要发送一大段数据,比如一个10KB的配置文件或者一帧图像数据,而波特率又因为硬件限制不能设得太高。这时候,传统的发送方式就会让CPU陷入漫长的等待,整个系统仿佛“卡住”了一样,什么也干不了。我最近在做一个智能传感器的项目,就碰到了这个问题,传感器需要定时通过串口上报大量采集到的环境数据,如果CPU被串口发送拖住,就无法及时处理新的传感器中断,导致数据丢失。为了解决这个痛点,我重新拾起了DMA这个神器,并做了一次深入的实践。今天,我就把这个基于STM32F103的“USART+DMA”发送10KB数据的完整例子拆开揉碎了讲给你听,不仅仅是代码,更重要的是背后的设计思路、配置细节和那些容易踩的坑。

简单来说,这个项目的核心价值在于:利用DMA(直接存储器访问)控制器,实现串口数据的“无感”后台发送。CPU只需要像下达命令一样,告诉DMA“把这堆数据从内存A搬到外设B”,然后就可以转身去处理其他任务了。DMA会像一个勤恳的搬运工,在总线上默默工作,直到所有数据搬完才通知CPU。这样一来,发送10KB数据所需的近10秒时间里,CPU不再是“干等着”,而是可以并行执行点灯、按键扫描、算法计算等其他工作,极大地提升了系统的整体效率和实时性。下面,我们就从原理到代码,一步步看如何实现它。

2. 核心思路与方案选型:为什么是DMA?

在深入代码之前,我们必须搞清楚几个关键问题:不用DMA会怎样?DMA到底是怎么工作的?为什么这个场景下DMA是最优解?

2.1 传统串口发送方式的瓶颈分析

在嵌入式系统中,CPU与外设(如USART)的数据交互,通常有两种经典模式:

  1. 轮询模式:CPU不断读取外设的状态寄存器,检查“发送数据寄存器空”标志(TXE)。一旦发现寄存器空了,就立刻写入下一个要发送的字节。对于发送10KB(10240字节)数据,在9600波特率下(大约每秒960字节),整个过程需要10.6秒。这意味着CPU要在10秒内重复执行“读状态->判断->写数据”这个循环超过一万次,期间几乎不能做其他事情,CPU利用率极低。

  2. 中断模式:CPU发送第一个字节后,当发送数据寄存器空时,USART会产生一个中断。CPU进入中断服务程序,发送下一个字节,然后退出。这种方式比轮询稍好,因为发送间隙CPU可以执行主循环任务。但每发送一个字节(约1ms)就打断CPU一次,对于10240字节的数据,会产生一万多次中断。频繁的中断上下文保存与恢复,带来了巨大的开销,严重干扰了其他对时序敏感的中断(如电机PWM、ADC采样),系统整体响应性变差。

这两种方式的共同问题是:数据传输占据了CPU过多的注意力,使得CPU无法专注于更复杂的业务逻辑或实时任务,在需要处理大量数据或对系统实时性要求高的场合,这是不可接受的。

2.2 DMA工作机制与优势解读

DMA的出现,就是为了将CPU从这种简单、重复的数据搬运劳动中解放出来。你可以把DMA想象成CPU的一个“专职秘书”。当需要搬运大量数据时,CPU只需要给DMA秘书一张“工作单”,上面写明:

  • 源头地址:数据在哪(例如,内存中的数组SendBuff)。
  • 目标地址:数据要搬到哪(例如,USART的数据寄存器USART1->DR)。
  • 搬运量:要搬多少(例如,10240字节)。
  • 搬运规则:源头地址搬完一个要不要自动往后挪(内存地址递增),目标地址要不要动(外设地址固定)等。

DMA秘书拿到工作单后,就会独立地、悄无声息地通过系统总线进行数据搬运。它和CPU共享总线,但通过总线矩阵仲裁,两者可以交替使用总线,从而实现某种程度的并行。在这个例子中,当DMA在总线上搬运一个字节到串口时,CPU可以同时使用总线从Flash读取指令或访问其他外设。只有极少数情况(访问同一块内存)才会产生冲突。

因此,DMA方案的优势显而易见:

  • 极低的CPU占用率:配置和启动DMA传输只消耗极短的CPU时间,之后CPU完全自由。
  • 确定性的传输时间:DMA传输由硬件时序保证,不受CPU中断响应时间的影响,传输完成时间可精确预测。
  • 提升系统实时性:解放出来的CPU可以更好地响应其他紧急中断,处理复杂任务。

2.3 本项目的方案设计

基于以上分析,本项目选择了“USART发送DMA + CPU主循环并行任务 + 接收中断”的混合架构。

  • 发送端(主角):采用DMA的“单次传输”模式,将内存中预存好的10KB数据一次性、自动地搬运到USART的发送数据寄存器。CPU仅在开始时配置并启动DMA,结束时检查标志位。
  • CPU主循环:在DMA传输的10秒内,执行一个“点灯延时”任务(LED_1_REV; Delay();),模拟实际应用中的其他业务处理。
  • 接收端:为了保持系统的交互性,USART接收仍然使用中断模式。这样,即使在DMA发送大数据期间,系统也能随时响应从串口发来的指令,并将数据存入小缓冲区,供主循环处理。

这个设计巧妙地平衡了大数据发送的效率和小数据交互的实时性,是一个在实际项目中非常实用的模式。

3. 硬件与软件环境搭建

在分析代码细节前,我们先明确一下实验的舞台。虽然代码具有很高的可移植性,但了解其硬件基础对于排查问题和适配你自己的板子至关重要。

3.1 硬件平台解析:STM32F103VBT6与开发板

代码基于STM32F103VBT6这款经典的Cortex-M3内核MCU,以及万利EK-STM32F开发板。我们需要关注几个关键点:

  1. 芯片资源:STM32F103VBT6具有256KB Flash、48KB RAM,最高主频72MHz。它包含3个USART和2个DMA控制器(DMA1有7个通道,DMA2有5个通道)。本例子使用USART1,它默认映射到DMA1的Channel 4(发送)和Channel 5(接收),这是STM32F1系列芯片的固定映射,在参考手册中可以查到。
  2. 外设连接
    • USART1:TX引脚是PA9,RX引脚是PA10。代码中将它们配置为复用推挽输出和浮空输入,这是标准接法。
    • LED与按键:代码中使用了GPIOC的Pin 4-7控制LED(用于在DMA传输期间闪烁),以及GPIOD的Pin 3作为一个启动按键(低电平有效)。你需要根据自己板子的原理图,修改这些引脚定义。
    • 时钟:系统时钟通过外部8MHz晶振(HSE)倍频到72MHz。这是F103系列最常用的配置,确保了USART能产生精确的9600波特率。

注意:如果你使用的是其他型号的STM32(如F4、H7系列)或其他品牌的开发板,DMA通道映射、时钟配置函数、GPIO复用功能可能完全不同。务必查阅对应芯片的《参考手册》和《数据手册》,不能直接照搬。

3.2 软件工程与库函数选择

代码工程基于Keil MDK(ARM RealView)开发环境,使用了ST官方提供的标准外设库(Standard Peripheral Library, SPL)。虽然ST现在主推HAL库和LL库,但SPL库因其直接、高效,在F1系列和许多存量项目中依然广泛使用,理解它有助于深入理解寄存器操作。

工程的文件结构通常包含:

  • main.c:主程序文件,包含我们即将剖析的所有逻辑。
  • stm32f10x_lib.h:标准外设库的主头文件,包含了所有外设的声明。
  • system_stm32f10x.c:系统初始化文件,包含系统时钟配置(但本例在RCC_Configuration中自己配置了)。
  • 对应的启动文件(startup_stm32f10x_md.s等)。

在开始编程前,请确保你的MDK工程已经正确安装了STM32F1的Device Pack,并添加了标准外设库的源文件和头文件路径。

4. 代码深度剖析与实操要点

接下来,我们进入最核心的部分,逐模块解析代码,并补充那些数据手册里不会写的“潜规则”和实操技巧。

4.1 系统时钟配置:稳定运行的基石

RCC_Configuration函数负责搭建整个芯片的“心跳”。很多初学者容易忽视时钟配置,导致外设工作不正常,比如串口乱码。

void RCC_Configuration(void) { ErrorStatus HSEStartUpStatus; // ... 使能HSE,等待就绪 ... if (HSEStartUpStatus==SUCCESS) { // 设置各总线时钟 RCC_HCLKConfig(RCC_SYSCLK_Div1); // AHB时钟 = SYSCLK = 72MHz RCC_PCLK1Config(RCC_HCLK_Div2); // APB1时钟 = 36MHz (最大36MHz) RCC_PCLK2Config(RCC_HCLK_Div1); // APB2时钟 = 72MHz // 关键!设置Flash延迟,与CPU速度匹配 FLASH_SetLatency(FLASH_Latency_2); FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable); // 配置PLL:HSE(8MHz) /1 *9 = 72MHz RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); RCC_PLLCmd(ENABLE); while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET); RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); // 切换系统时钟源到PLL while(RCC_GetSYSCLKSource()!= 0x08); // 等待切换成功 } // 使能各模块时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | ... | RCC_APB2Periph_USART1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // DMA时钟在AHB总线上 }

要点与避坑指南:

  1. 总线时钟分频:STM32F1的APB1总线时钟最高为36MHz,APB2为72MHz。USART1挂在APB2上,所以它能跑72MHz。如果你错误地将USART2(挂在APB1上)的时钟源配置为超过36MHz,会导致其无法正常工作。
  2. Flash延迟等待:当CPU速度超过24MHz时,从Flash读取指令需要插入等待周期。FLASH_Latency_2表示插入2个等待周期,对应48-72MHz的主频。忘记设置或设置错误,会导致程序跑飞,出现HardFault等难以调试的问题。
  3. 时钟使能顺序:原则上,应先配置时钟系统,再使能具体外设时钟。但标准外设库的RCC_APBxPeriphClockCmd函数比较健壮,顺序影响不大。然而,必须确保在使用任何外设(包括配置其GPIO)之前,已经开启了对应的时钟,否则对寄存器的操作无效。

4.2 GPIO与USART初始化:通信管脚的设定

GPIO_ConfigurationUSART1_Configuration函数为通信准备好了硬件通路。

void GPIO_Configuration(void) { GPIO_InitTypeDef GPIO_InitStructure; // USART1_TX (PA9) 配置为复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 输出速度高,边沿更陡峭 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure); // USART1_RX (PA10) 配置为浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); } void USART1_Configuration(void) { USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate = 9600; 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_Tx | USART_Mode_Rx; // 使能收发 USART_Init(USART1, &USART_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 使能接收中断 USART_Cmd(USART1, ENABLE); // 最后才使能USART模块 }

要点与避坑指南:

  1. GPIO模式:USART的TX引脚必须设置为复用推挽输出(GPIO_Mode_AF_PP),RX引脚设置为浮空输入或上拉输入。设置为普通推挽输出是无法正常通信的。
  2. 波特率计算:9600波特率是如何来的?公式是:Tx/Rx波特率 = fCK / (16 * USARTDIV)。其中fCK是给USART的时钟(PCLK2,72MHz)。USARTDIV是一个存放在USART_BRR寄存器中的浮点数。库函数USART_Init内部会帮你计算并设置BRR寄存器值。你可以通过USART_GetDivisor函数反算验证。
  3. 使能顺序:务必在配置完所有参数后,最后调用USART_Cmd来使能USART模块。如果先使能再配置,可能导致发送一些乱码。

4.3 DMA配置详解:搬运工的工作单

这是整个项目的灵魂所在,DMA_Configuration函数填充了DMA“秘书”的工作单。

#define USART1_DR_Base 0x40013804 // USART1数据寄存器的物理地址 void DMA_Configuration(void) { DMA_InitTypeDef DMA_InitStructure; DMA_DeInit(DMA1_Channel4); // 复位通道4,恢复到默认状态 DMA_InitStructure.DMA_PeripheralBaseAddr = USART1_DR_Base; DMA_InitStructure.DMA_MemoryBaseAddr = (u32)SendBuff; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 方向:内存->外设 DMA_InitStructure.DMA_BufferSize = SENDBUFF_SIZE; // 搬运数量:10240 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_Medium; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 内存到外设模式,非内存到内存 DMA_Init(DMA1_Channel4, &DMA_InitStructure); }

参数深度解读与避坑指南:

  1. 地址设置

    • DMA_PeripheralBaseAddr:这是外设数据寄存器的地址。为什么是0x40013804?我们需要查《STM32F10xxx参考手册》的内存映射表。USART1的起始地址是0x40013800,其数据寄存器(DR)的偏移地址是0x04,所以绝对地址是0x40013804这是一个绝对物理地址,必须正确无误。
    • DMA_MemoryBaseAddr:这是内存中源数组的首地址。(u32)SendBuff将数组名转换为它的首地址。这里SendBuff被定义为vu8(易失性无符号8位)类型,确保编译器不会做优化。
  2. 数据传输方向与地址递增

    • DMA_DIR = DMA_DIR_PeripheralDST:表示外设是目的地(DST),内存是源(SRC),即从SendBuff搬数据到USART1->DR
    • DMA_PeripheralInc = Disable:外设地址不递增。因为我们是不断地往同一个USART数据寄存器写数据。
    • DMA_MemoryInc = Enable:内存地址递增。每搬运一个字节后,DMA会自动将内存地址加1,指向数组中的下一个元素。如果你要发送一个常量,或者重复发送缓冲区的第一个值,则需要将此设为Disable
  3. 数据大小与模式

    • DMA_PeripheralDataSizeDMA_MemoryDataSize都设置为Byte,表示以字节为单位搬运。也可以设置为HalfWord(16位)或Word(32位),但必须确保外设支持该数据宽度。USART数据寄存器是8/9位的,所以用Byte最安全。
    • DMA_Mode = DMA_Mode_Normal:单次模式。传输完BufferSize个数据后,DMA通道自动停止,传输完成标志置位。另一种常用模式是DMA_Mode_Circular(循环模式),传输完成后自动重装计数器,从头开始,适用于ADC连续采样等场景。
  4. M2M模式DMA_M2M_Disable表示这是外设触发模式。DMA传输由外设(此处是USART的TXE事件)触发,而不是由软件触发。这是内存到外设DMA的标准用法。

4.4 主程序逻辑:导演的指挥艺术

主函数main串联起了所有模块,体现了整个程序的流程控制。

int main(void) { // ... 初始化 ... RCC_Configuration(); GPIO_Configuration(); NVIC_Configuration(); DMA_Configuration(); // 配置DMA,但尚未启动 USART1_Configuration(); // 初始化发送缓冲区 for (u16 i=0; i<SENDBUFF_SIZE; i++) { SendBuff[i] = i & 0xff; // 填充0x00, 0x01, ... 0xff, 0x00... } printf("Waiting for transmission...\r\n"); // 等待按键按下(PD3为低电平) while(GPIO_ReadInputDataBit(GPIOD, GPIO_Pin_3)); printf("Start DMA transmission!\r\n"); // 关键步骤1:使能USART1的DMA发送请求 USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); // 关键步骤2:使能DMA通道,开始传输 DMA_Cmd(DMA1_Channel4, ENABLE); // 在DMA传输期间,CPU执行其他任务(点灯) while(DMA_GetFlagStatus(DMA1_FLAG_TC4) == RESET) { LED_1_REV; // 翻转LED状态 Delay(); // 延时,模拟任务处理时间 } // 传输完成,跳出循环 printf("\r\nDMA transmission successful!\r\n"); while(1) { /* 主循环 */ } }

流程解析与核心技巧:

  1. 初始化顺序:先初始化GPIO和USART,最后配置DMA。因为DMA的配置依赖于外设是否已就绪。但注意,此时不要开启USART的DMA请求USART_DMACmd),也不要使能DMA通道DMA_Cmd)。这就像给枪上了子弹但没打开保险。

  2. 缓冲区准备:在启动传输前,必须确保源数据缓冲区(SendBuff)已经填充了有效数据。本例中填充了一个循环序列。在实际项目中,这里可能是待发送的传感器数据包、图像数据等。

  3. 启动传输的双重使能:这是最容易出错的地方!

    • USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE):这条命令是告诉USART外设:“当你发送数据寄存器空(TXE)时,不要产生中断,而是去触发DMA请求”。只有开启了这项功能,USART才会在需要新数据时向DMA控制器“要活干”。
    • DMA_Cmd(DMA1_Channel4, ENABLE):这条命令是告诉DMA控制器:“通道4,开始工作吧,等待触发信号”。一旦使能,DMA通道就处于就绪状态,等待USART发出的触发信号。

    顺序很重要:理论上,先使能哪个都可以。但一个稳健的做法是:先使能USART的DMA请求,再使能DMA通道。这样可以避免在使能DMA通道的瞬间,如果USART的TXE标志恰好是空的,可能立即触发一次意外的DMA传输。

  4. 传输完成判断DMA_GetFlagStatus(DMA1_FLAG_TC4)用于查询DMA1通道4的“传输完成”标志。在Normal模式下,当设定的数据量(BufferSize)全部传输完毕,该标志由硬件自动置1。查询完成后,软件需要手动清除该标志(虽然本例没写,但最好加上DMA_ClearFlag(DMA1_FLAG_TC4)),为下一次传输做准备。

  5. DMA通道的停止:在Normal模式下,传输完成后DMA通道会自动禁用(ENABLE位被硬件清零)。所以代码中注释掉了DMA_Cmd(DMA1_Channel4, DISABLE)。如果需要再次启动传输,必须重新设置DMA_BufferSize,然后再次使能通道。

5. 关键问题排查与实战经验

理论很美好,但调试时总会遇到各种问题。下面是我在实际项目中总结的几个典型问题及其解决方法。

5.1 DMA传输不启动或数据发送不全

这是最常见的问题。请按照以下清单逐一排查:

现象可能原因排查方法
完全没数据发出1. USART或DMA时钟未开启。
2. USART的DMA发送请求未使能。
3. DMA通道未使能。
4. USART本身未使能(USART_Cmd)。
5. GPIO模式配置错误(TX不是AF_PP)。
1. 检查RCC_Configuration中相关ClockCmd是否调用。
2. 确认USART_DMACmd已调用且参数正确。
3. 单步调试,检查DMA_Cmd后DMA通道的CCR寄存器EN位是否为1。
4. 检查USART_CR1寄存器的UE位。
5. 用逻辑分析仪或示波器抓PA9引脚,看是否有波形。
只发送了第一个或前几个字节1. DMA内存地址递增未使能(DMA_MemoryInc)。
2. DMA缓冲区大小(BufferSize)设置过小。
3. 传输完成中断/标志过早被处理,错误地停止了DMA。
1. 检查DMA_InitStructure.DMA_MemoryInc是否为Enable
2. 确认SENDBUFF_SIZE宏定义和DMA_BufferSize赋值一致。
3. 确保主循环中判断的是正确的标志位(TCx),而不是半传输标志(HTx)或其他。
发送的数据是乱码或固定值1. 内存源数据缓冲区内容不对。
2. 波特率设置错误,与接收端不匹配。
3. DMA外设基地址错误,搬到了错误的外设寄存器。
1. 在初始化后、启动DMA前,查看SendBuff数组内存内容。
2. 双端确认波特率、数据位、停止位、校验位是否一致。
3.重点检查USART1_DR_Base地址是否正确。对于F1系列,USART1的DR地址是USART1_BASE + 0x04

一个高级调试技巧:利用Keil MDK的**“Peripherals” -> “DMA”** 视图。你可以实时查看DMA通道的寄存器状态,特别是CNDTR寄存器(当前待传输数据数量)。在传输过程中,这个值应该从SENDBUFF_SIZE开始递减。如果它不动,说明DMA没被触发;如果它递减但串口没数据,问题可能出在USART或GPIO。

5.2 CPU与DMA访问内存的冲突问题

DMA和CPU都可以访问内存(SRAM)。如果两者同时访问同一块内存区域,总线仲裁器会介入,但可能会引发意想不到的问题。

  • 问题场景:你在DMA传输SendBuff的过程中,CPU也在修改SendBuff的内容。
  • 后果:DMA读到的数据可能不是你想要的数据,导致发送数据错乱。这是一种非常隐蔽的Bug。
  • 解决方案
    1. 双缓冲区(Ping-Pong Buffer):准备两个缓冲区A和B。当DMA传输缓冲区A时,CPU填充缓冲区B。A传完后,迅速切换DMA到缓冲区B,同时CPU填充A。这需要配合DMA传输完成中断。
    2. 确保数据就绪再启动:像本例一样,在启动DMA传输前,确保所有待发送数据已完全准备好放入SendBuff。在传输过程中,CPU绝不修改这块内存。
    3. 使用__align(4)volatile:对于DMA缓冲区,建议定义时进行4字节对齐(如__align(4) u8 SendBuff[SENDBUFF_SIZE]),因为DMA和CPU对未对齐字的访问效率较低。volatile关键字(本例中的vu8)防止编译器对缓冲区访问做激进的优化。

5.3 如何与接收中断和谐共处

本例中,发送用了DMA,接收用了中断。这是一个很好的组合。但需要注意中断优先级。

NVIC_Configuration中,我们设置了USART1中断的抢占优先级为0(最高)。

NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;

为什么?因为串口接收的数据是“稍纵即逝”的实时事件,如果被其他中断(比如SysTick)长时间阻塞,可能导致数据丢失。而DMA传输完成事件我们用的是查询方式(while(DMA_GetFlagStatus...)),不占用中断。因此,将USART接收中断设为较高优先级是合理的。

最佳实践:对于时间关键的中断(如USART接收、某些传感器触发),赋予较高的抢占优先级。对于不那么紧急的(如DMA传输完成、定时器更新),可以设为较低优先级,或者像本例一样使用查询方式。

5.4 扩展应用:DMA接收与循环模式

本例只演示了发送,但DMA接收同样强大且常用。

  • DMA接收配置:需要使用DMA1的Channel5(对于USART1)。方向设为DMA_DIR_PeripheralSRC(外设为源),外设地址仍是USART1_DR_Base,内存地址指向一个接收缓冲区RecvBuff。使能USART的DMA接收请求(USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE))。
  • 循环模式(Circular Mode):将DMA_Mode设置为DMA_Mode_Circular。在此模式下,当数据传输计数达到BufferSize后,CNDTR寄存器会自动重载为初始值,DMA从缓冲区开头重新开始传输,周而复始。这非常适合用于持续不断的ADC采样数据流串口数据流监听。你只需要定期去缓冲区读取处理好的数据块即可,无需担心数据溢出。

6. 项目总结与优化思考

通过这个完整的例子,我们走通了STM32上USART DMA发送的整个流程。从时钟树配置到GPIO初始化,从DMA结构体填充到主流程控制,每一个环节都紧密相连。实测在STM32F103上,发送10KB数据期间,LED灯可以流畅闪烁,完美验证了CPU与DMA的并行工作能力。

回顾整个过程,有几点深刻的体会: 第一,理解硬件手册是基础。尤其是DMA通道与外设的映射关系、外设寄存器的地址、各种标志位的含义,这些必须查手册确认,不能想当然。 第二,配置顺序有讲究。就像组装一台精密仪器,先装哪个零件,后拧哪个螺丝,顺序错了可能就运转不起来。我的习惯是:时钟 -> GPIO -> 外设基本配置 -> DMA配置 -> 使能外设功能(如USART DMA请求)-> 最后使能DMA通道。 第三,调试需要耐心和工具。除了printf,更要善用仿真器的外设寄存器查看、内存观察和逻辑分析仪。看到CNDTR寄存器在递减,心里就踏实了一半。

最后,这个例子可以作为一个模板进行扩展。例如,结合定时器触发DMA,可以实现精确周期性的数据发送;将DMA模式改为循环,并搭配双缓冲区,可以构建一个高效、稳定的串口数据收发引擎,轻松应对物联网设备中频繁的数据上报与指令接收需求。希望这篇超详细的解析,能帮你彻底拿下STM32的DMA应用,在项目中游刃有余。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询