1. 项目概述:为什么DMA是STM32性能提升的关键
如果你正在用STM32F103C8T6做项目,无论是驱动屏幕、处理串口数据还是做高速ADC采集,大概率都遇到过CPU被数据搬运“拖累”的情况。主频72MHz的Cortex-M3核心,如果频繁陷入“接收一个字节-存起来-再接收下一个字节”这样的循环里,宝贵的计算资源就被白白浪费了。这正是DMA(Direct Memory Access,直接存储器访问)技术要解决的核心痛点。
简单来说,DMA就像一个独立的、高效的“数据搬运工”。它可以在不打扰CPU的情况下,在外设(如UART、ADC、SPI)和内存(如SRAM)之间,或者内存与内存之间,自动完成大批量数据的传输。CPU只需要在传输开始前告诉DMA“从哪里搬、搬到哪里、搬多少”,然后就可以去执行其他更重要的任务,比如算法处理、逻辑判断。等DMA搬完了,再通过中断通知一下CPU即可。这种“解放CPU”的特性,对于实时性要求高、数据吞吐量大的应用(如音频处理、图像采集、多路通信)至关重要。
STM32F103C8T6作为经典的“蓝桥杯”入门神器和众多工控项目的核心,其内部集成了两个DMA控制器(DMA1和DMA2),共12个独立的通道(DMA1有7个,DMA2有5个)。每个通道可以配置为服务于一个特定的外设请求。理解并掌握DMA,是从单片机“点灯”阶段迈向高效嵌入式系统开发的必经之路。接下来,我将结合自己踩过的坑和实战经验,带你彻底搞懂STM32F103C8T6的DMA原理与使用。
2. DMA核心机制与STM32F103架构解析
2.1 DMA工作原理:三总线架构下的数据高速公路
要理解DMA,必须先明白STM32的“三总线”架构:I-Bus(指令总线)、D-Bus(数据总线)和S-Bus(系统总线)。CPU主要通过I-Bus和D-Bus访问Flash和SRAM来取指和读写数据。而DMA控制器,以及大部分外设(如GPIO、UART、TIM),都挂载在S-Bus上。
DMA工作的本质,是在S-Bus上发起一次“总线主设备”操作。通常,CPU是总线的唯一主设备。但当DMA传输启动后,DMA控制器会向总线仲裁器申请总线控制权。一旦获得批准,DMA就暂时成为总线的主设备,可以直接在挂载于S-Bus上的外设数据寄存器(源地址)和SRAM(目标地址)之间进行数据读写,整个过程完全绕开CPU。
这里有一个关键点:DMA传输的最小单位是“节拍”(Beat),对应一次总线操作。对于8位数据,就是一个字节的传输;对于16位数据,就是半字;32位则是字。STM32的DMA传输宽度(数据宽度)可以配置为字节、半字或字。但源地址和目标地址的数据宽度可以不同,DMA控制器会自动进行打包或拆包操作,不过这通常需要额外的总线周期,会影响效率。
注意:DMA传输期间,CPU并非完全停止。如果CPU要访问的资源(比如Flash中的代码)不占用S-Bus(通过I-Bus取指),那么CPU和DMA是可以并行工作的。但如果CPU也需要通过S-Bus访问SRAM或外设,总线仲裁器会根据优先级进行调度,这可能会导致CPU访问稍有延迟。因此,合理规划内存布局(如将CPU频繁访问的数据放在CCM RAM,如果支持的话)和设置DMA优先级,是高级优化的方向。
2.2 STM32F103C8T6的DMA资源全景图
STM32F103C8T6的DMA资源是其能力边界,用之前必须了然于胸。
DMA1控制器:拥有7个通道(Channel 1-7)。这是最常用的DMA控制器,服务于以下外设请求:
- 通道1:ADC1、TIM2_CH3、TIM4_CH1
- 通道2:SPI1_RX、ADC1、TIM2_UP、TIM3_CH3、TIM4_CH2
- 通道3:SPI1_TX、ADC1、TIM3_CH1、TIM3_TRIG
- 通道4:SPI/I2S2_RX、I2C2_TX、USART3_TX、TIM1_CH1、TIM2_UP、TIM3_CH2、TIM4_CH3
- 通道5:SPI/I2S2_TX、I2C2_RX、USART3_RX、TIM1_CH2、TIM2_CH1、TIM3_CH3、TIM4_UP
- 通道6:USART1_TX、I2C1_TX、TIM1_CH4、TIM1_TRIG、TIM1_COM、TIM3_CH4、TIM4_CH1
- 通道7:USART1_RX、I2C1_RX、TIM1_UP、TIM2_CH2、TIM3_CH1、TIM4_CH2
DMA2控制器:拥有5个通道(Channel 1-5)。注意,DMA2的时钟默认是关闭的(位于APB1总线),使用前必须先通过RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2, ENABLE)使能其时钟。它主要服务于高级外设:
- 通道1-5:服务于ADC3、SPI/I2S3、UART4/5、TIM5、TIM8以及内存到内存的传输。
一个至关重要的规则是:每个DMA通道在同一时间只能分配给一个外设。比如,如果你已经将DMA1的通道4配置给了USART3_TX,那么该通道就不能同时用于SPI2_RX。规划通道资源是项目硬件设计阶段就要考虑的事情。
2.3 数据流与传输模式:单次、循环与双缓冲
DMA的传输行为由几个关键模式决定:
普通模式(单次模式):DMA传输完指定的数据量(
NDTR寄存器值减到0)后,便自动停止,通道使能位被硬件清除。这是最常用的模式,适用于已知长度的单次传输,比如通过SPI发送一段固定长度的数据包。循环模式(Circular Mode):当
NDTR计数器减到0后,硬件会自动将其重载为初始值,传输重新开始,周而复始。这个模式极其有用,常用于:- ADC连续采集:配合ADC的连续扫描模式,可以实现“永不停歇”的模拟量采样,数据自动存入指定的内存数组,形成环形缓冲区。
- 音频流播放:双缓冲区(乒乓缓冲区)策略的基础。一个缓冲区用于DMA传输(播放),另一个缓冲区由CPU填充新的音频数据,通过循环模式无缝切换。
内存到内存模式:这是DMA2的专属功能(在F1系列)。允许数据在两个内存区域之间高速搬运,比如复制一个大数组,或者对图像数据进行预处理(如灰度转换)。此时,需要软件触发传输(通过
DMA_Cmd使能通道),传输速度取决于内存和总线带宽。外设到内存/内存到外设模式:最常见的模式。由外设的硬件事件(如UART收到数据、ADC转换完成)触发DMA请求,DMA控制器响应请求并搬运一次数据。
传输完成与半传输中断:DMA支持传输完成中断(NDTR=0)和半传输中断(NDTR减到一半时)。半传输中断是实现“双缓冲”或“乒乓缓冲”的关键。例如,设置一个大小为N的缓冲区,当DMA传输到N/2时产生半传输中断,此时CPU可以安全处理缓冲区的前半部分数据;当全部传输完成时产生完成中断,CPU处理后半部分数据。这样,数据生产和消费可以并行,几乎没有等待时间。
3. 核心配置详解:从寄存器到HAL库
理解了原理,我们来看如何配置。无论是直接操作寄存器、使用标准外设库(SPL)还是HAL库,核心思想都是一致的。
3.1 寄存器级配置要点(以USART1_TX DMA为例)
对于追求极致效率和理解的开发者,直接看寄存器很有帮助。主要涉及以下几个寄存器:
DMA_CPARx:通道x的外设地址寄存器。填入USART1->DR的地址((uint32_t)&(USART1->DR))。DMA_CMARx:通道x的内存地址寄存器。填入你要发送的数据数组的首地址。DMA_CNDTRx:通道x的数据数量寄存器。填入要传输的数据字节数(或半字/字数)。DMA_CCRx:通道x的配置寄存器。这是核心,需要配置:MEM2MEM:内存到内存模式使能(本例为0)。PL[1:0]:通道优先级(00低,01中,10高,11最高)。当多个通道同时请求时,优先级高的先服务。MSIZE/PSIZE:内存/外设数据宽度(00=8位,01=16位,10=32位)。必须注意:MSIZE和PSIZE通常应设置一致,除非你明确知道自己在做数据打包。对于USART(8位数据),这里应设为00(字节)。MINC/PINC:内存/外设地址增量模式。对于内存中的数组,MINC应使能(=1),这样每传一个数据,内存地址自动递增。对于外设固定寄存器(如USART->DR),PINC应禁止(=0)。CIRC:循环模式使能位。DIR:传输方向(0=从外设读,1=向外设写)。对于TX,是内存到外设,DIR=1。TEIE/HTIE/TCIE:传输错误/半传输/传输完成中断使能。EN:通道使能。最后才置1。
配置流程:初始化时钟 -> 配置DMA_CCRx-> 配置DMA_CPARx、DMA_CMARx、DMA_CNDTRx-> 使能外设的DMA请求(如USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE)) -> 最后置位DMA_CCRx的EN位启动传输。
3.2 HAL库配置流程与避坑指南
HAL库封装了底层细节,但理解其流程同样重要。以UART DMA发送为例:
// 1. 声明DMA和UART句柄 DMA_HandleTypeDef hdma_usart1_tx; UART_HandleTypeDef huart1; // 2. 初始化DMA(通常在MX_DMA_Init中自动生成) hdma_usart1_tx.Instance = DMA1_Channel4; // USART1_TX对应通道4 hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE; hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_usart1_tx.Init.Mode = DMA_NORMAL; // 或 DMA_CIRCULAR hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW; if (HAL_DMA_Init(&hdma_usart1_tx) != HAL_OK) { Error_Handler(); } // 3. 将DMA句柄与UART句柄关联 __HAL_LINKDMA(&huart1, hdmatx, hdma_usart1_tx); // 4. 启动DMA传输 uint8_t tx_data[] = "Hello DMA!"; if (HAL_UART_Transmit_DMA(&huart1, tx_data, sizeof(tx_data)-1) != HAL_OK) { // 错误处理 } // 5. 在传输完成回调函数中处理后续事宜 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { // 发送完成,可以点亮一个LED或准备下一包数据 } }HAL库使用DMA的三大坑点:
- 缓冲区生命周期问题:
HAL_UART_Transmit_DMA函数是非阻塞的,函数调用后立即返回。你必须确保tx_data数组在DMA传输完成前一直有效。如果tx_data是函数内的局部变量,函数退出后栈内存可能被覆盖,导致DMA发送乱码。解决方案:使用全局数组、静态(static)数组或在堆上动态分配。 - 重复调用与状态检查:在DMA传输进行中(
HAL_DMA_STATE_BUSY),再次调用HAL_UART_Transmit_DMA会直接返回HAL_BUSY错误。在发送新数据前,要么等待回调函数执行完毕,要么使用HAL_UART_GetState()检查状态。 - 中断冲突与优先级:DMA传输完成中断和UART中断共用同一个中断向量(对于USART1是
USART1_IRQn)。在HAL库中,UART中断服务程序会调用HAL_UART_IRQHandler,它内部会判断并调用相应的DMA回调函数。你需要确保在stm32f1xx_it.c中正确实现了该中断服务程序,并且合理配置了NVIC优先级。
3.3 CubeMX图形化配置实战
对于新手或快速原型开发,STM32CubeMX是神器。配置DMA的步骤如下:
- 在
Pinout & Configuration标签页,找到你要用的外设(如USART1)。 - 将模式设为“Asynchronous”(异步),并开启“DMA Settings”选项卡。
- 点击“Add”添加DMA请求。对于USART1 TX,选择“USART1_TX”,系统会自动分配通道(Channel 4)。
- 在下方配置DMA参数:
Direction:Memory To PeripheralIncrement Address:Memory勾选Yes(地址递增),Peripheral勾选No。Data Width:Byte(USART通常8位)。Mode:Normal(单次)或Circular(循环)。Priority:根据需求选择。
- 生成代码后,CubeMX会自动完成DMA和UART的初始化代码,并在
main.c中声明好句柄。你只需要调用HAL_UART_Transmit_DMA即可。
实操心得:CubeMX生成的DMA初始化代码通常放在
xxx.c文件的MX_DMA_Init()函数里。但有时它不会自动关联外设和DMA句柄(即__HAL_LINKDMA)。如果遇到DMA不工作,第一件事就是检查生成的huart1.Init部分或HAL_UART_MspInit函数里,是否有__HAL_LINKDMA这行代码。没有的话,需要手动添加。
4. 典型应用场景与代码实现
4.1 应用一:UART DMA不定长数据接收(空闲中断法)
这是最经典也最实用的DMA应用之一。传统串口接收需要每个字节都进中断,效率低。结合DMA和UART空闲中断(IDLE Interrupt),可以实现高效的不定长数据包接收。
原理:使能UART的DMA接收请求和空闲中断。DMA配置为循环模式,指向一个足够大的缓冲区(如rx_buffer[256])。串口一旦开始接收数据,DMA就自动将数据搬运到缓冲区,且地址循环覆盖。当一帧数据发送完毕,串口总线会进入短暂的空闲状态,触发空闲中断。在中断里,我们通过计算DMA当前剩余数据量(__HAL_DMA_GET_COUNTER)来推算出已经接收到的数据长度,然后进行帧处理。
// 变量声明 #define RX_BUF_SIZE 256 uint8_t rx_buffer[RX_BUF_SIZE]; volatile uint16_t rx_len = 0; // 接收到的数据长度 volatile uint8_t rx_flag = 0; // 接收完成标志 // 在UART初始化后,启动DMA循环接收 HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUF_SIZE); __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 使能空闲中断 // 在stm32f1xx_it.c的USART1_IRQHandler中,调用HAL_UART_IRQHandler(&huart1); // 重写空闲中断回调函数(在main.c或uart.c中) void HAL_UART_IdleCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART1) { __HAL_UART_CLEAR_IDLEFLAG(huart); // 清除空闲中断标志 HAL_UART_DMAStop(huart); // 暂停DMA,防止数据被覆盖 // 计算已接收数据长度 rx_len = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx); if(rx_len > 0) { rx_flag = 1; // 设置标志,主循环中处理数据 // 此时,rx_buffer[0] 到 rx_buffer[rx_len-1] 就是本次接收的数据 } // 重新设置DMA传输数据量,并启动DMA,准备接收下一帧 __HAL_DMA_SET_COUNTER(huart->hdmarx, RX_BUF_SIZE); HAL_UART_Receive_DMA(huart, rx_buffer, RX_BUF_SIZE); } } // 主循环中处理数据 if(rx_flag) { rx_flag = 0; process_uart_data(rx_buffer, rx_len); // 你的数据处理函数 }注意事项:
- 缓冲区大小要足够容纳最大可能的一帧数据,否则会覆盖未处理的数据。
- 在
HAL_UART_IdleCallback中暂停DMA和重新配置的间隙,如果来了新数据会丢失。这个窗口极短,但对于极高波特率(如2Mbps)且连续发送的场景,有极低概率丢包。工业上有时会结合定时器做超时判断来增强鲁棒性。 - 清除空闲中断标志位
__HAL_UART_CLEAR_IDLEFLAG是必要的,否则会连续进入中断。
4.2 应用二:ADC多通道扫描+DMA循环采集
这是模拟信号采集的黄金搭档。利用ADC的扫描模式依次转换多个通道,并让DMA在每次转换完成后自动搬运数据,形成连续的数据流。
配置要点:
- ADC配置:工作模式为“扫描模式(Scan)”和“连续转换模式(Continuous)”。在“规则通道”中,按顺序添加要转换的通道(如
IN0,IN1,IN2),并设置每个通道的采样时间。 - DMA配置:为ADC1(或ADC3)添加DMA请求。模式设为“循环模式(Circular)”。方向为“外设到内存”。内存地址递增,外设地址不递增。数据宽度为半字(ADC结果是12位,存为16位)。
- 内存缓冲区:定义一个
uint16_t adc_buf[N]数组,其中N是通道数×每个通道想缓存的样本数。例如,3个通道,想每个通道缓存100次转换结果,则N=300。DMA会按CH1->CH2->CH3->CH1->CH2...的顺序填充这个数组。
#define ADC_CHANNEL_NUM 3 #define ADC_SAMPLE_PER_CH 100 uint16_t adc_value_buf[ADC_CHANNEL_NUM * ADC_SAMPLE_PER_CH]; // CubeMX生成初始化代码后,启动ADC HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_value_buf, ADC_CHANNEL_NUM * ADC_SAMPLE_PER_CH); // DMA传输完成一半和全部完成的中断回调函数 void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) { // 当DMA搬运了上半部分数据时触发 // 可以安全地处理 adc_value_buf[0] 到 adc_value_buf[N/2-1] 的数据 // 此时DMA正在向下半部分写入,互不干扰(双缓冲) } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { // 当DMA搬运了全部数据时触发 // 可以安全地处理 adc_value_buf[N/2] 到 adc_value_buf[N-1] 的数据 }数据处理:缓冲区adc_value_buf中的数据是交错的。第i个通道的第j次采样值,位于数组索引为i + j * ADC_CHANNEL_NUM的位置。例如,adc_value_buf[0]是通道1第一次采样,adc_value_buf[1]是通道2第一次采样,adc_value_buf[2]是通道3第一次采样,adc_value_buf[3]是通道1第二次采样,以此类推。
4.3 应用三:SPI驱动LCD屏(内存到外设)
驱动像ST7789、ILI9341这类LCD屏,需要频繁发送命令和数据。使用SPI的DMA发送可以极大解放CPU,特别是在刷屏(填充矩形、显示图片)时,效果显著。
// 发送命令函数(无DMA,阻塞式) void LCD_Write_Cmd(uint8_t cmd) { LCD_CS_LOW(); LCD_DC_CMD(); // 设置DC线为命令模式 HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); LCD_CS_HIGH(); } // 发送数据函数(使用DMA,非阻塞) void LCD_Write_Data_DMA(uint8_t *data, uint16_t size) { LCD_CS_LOW(); LCD_DC_DATA(); // 设置DC线为数据模式 HAL_SPI_Transmit_DMA(&hspi1, data, size); // 函数立即返回,传输在后台进行 } // 在SPI传输完成回调函数中拉高CS(如果需要) void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if(hspi->Instance == SPI1) { LCD_CS_HIGH(); // 注意:需根据你的LCD芯片时序决定是否在此拉高CS } }关键技巧:对于刷屏操作,可以提前在内存中构建好一行的像素数据缓冲区(例如,对于16位色深,一个像素是2字节)。然后调用LCD_Write_Data_DMA发送这个缓冲区。在DMA传输期间,CPU可以准备下一行的数据,实现并行。更高级的做法是使用双缓冲和循环DMA,实现类似“动画”的流畅效果。
踩坑记录:SPI的DMA发送有一个常见问题——最后几个字节发不出去。这通常是因为在DMA传输完成前,就提前关闭了SPI或片选(CS)。确保在
HAL_SPI_TxCpltCallback回调函数中再进行后续操作。另外,检查SPI时钟极性(CPOL)和相位(CPHA)是否与LCD驱动芯片要求严格一致,一个不对都会导致显示乱码。
5. 高级话题与性能优化
5.1 内存对齐与性能瓶颈
DMA传输效率受总线带宽和内存对齐影响。ARM Cortex-M3内核是32位架构,访问32位对齐的地址效率最高。对于DMA,如果源地址和目标地址都是32位对齐的,并且数据宽度设置为字(32位),那么一次总线事务就能传输4个字节,效率最高。
建议:定义用于DMA传输的缓冲区时,使用编译器指令进行对齐。例如,在GCC或ARM Compiler中:
uint32_t buffer[100] __attribute__((aligned(4))); // 4字节对齐或者使用HAL库提供的宏:
uint32_t buffer[100] __ALIGNED(4);对于8位或16位数据,虽然不强制对齐,但保持地址为偶数(2字节对齐)也能带来一定性能提升。
5.2 DMA与CPU的缓存一致性(Cache Coherency)问题
STM32F103没有缓存(Cache),所以不存在这个问题。但对于更高端的Cortex-M7内核(如STM32H7系列)或带有数据缓存(D-Cache)的芯片,这个问题至关重要。如果CPU缓存了某块内存数据,而DMA直接修改了这块物理内存,CPU读到的将是缓存中的旧数据,导致数据不一致。
解决方案(针对有Cache的芯片):
- 将DMA缓冲区放在非缓存区域(如SRAM4)。
- 或者在DMA传输前后,使用
SCB_CleanDCache_by_Addr()(DMA写入内存前,CPU清理缓存)和SCB_InvalidateDCache_by_Addr()(DMA写入内存后,CPU使缓存失效)函数来手动维护缓存一致性。
对于F103用户,了解这个概念有助于未来升级到更高级的芯片。
5.3 调试DMA传输的技巧
DMA传输不成功时,因为没有CPU参与,调试起来比较麻烦。以下是一些实用的调试方法:
- 使用断点和变量观察:在DMA传输启动前和传输完成回调函数里设置断点。观察源缓冲区、目标地址寄存器的值是否正确。检查
DMA_CNDTRx寄存器的值是否在递减。 - 利用传输完成中断:确保NVIC中DMA通道的中断已使能,并在中断服务程序或回调函数里设置一个标志变量(如
dma_done_flag),通过点灯或串口打印来确认中断是否发生。 - 检查外设的DMA请求使能:这是最容易被忽略的一步!以UART为例,除了配置DMA,还必须使能UART本身的DMA发送/接收请求:
__HAL_UART_ENABLE_DMA_REQ(&huart1, UART_DMA_REQ_TX)。HAL库的HAL_UART_Transmit_DMA函数内部通常会做这个,但自己写寄存器时千万别忘了。 - 逻辑分析仪或示波器:这是最直接的手段。用逻辑分析仪抓取SPI的SCK、MOSI信号,或者用示波器测量UART的TX引脚,看是否有预期的波形和数据发出。如果没有,说明DMA或外设根本没工作。
6. 常见问题排查速查表
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| DMA根本不启动 | 1. DMA时钟未使能。 2. DMA通道配置错误(源/目标地址、数据宽度、传输方向)。 3. 外设的DMA请求未使能。 4. DMA通道优先级冲突,且一直有更高优先级通道在传输。 | 1. 检查RCC_AHBPeriphClockCmd是否调用。2. 单步调试,核对 DMA_CPARx,DMA_CMARx,DMA_CNDTRx,DMA_CCRx寄存器值。3. 检查外设相关DMA使能位(如 USART_CR3的DMAT位)。4. 简化程序,只保留一个DMA通道测试。 |
| DMA能启动,但传输数据错误 | 1. 内存/外设地址增量模式(MINC/PINC)设置错误。2. 数据宽度( MSIZE/PSIZE)不匹配。3. 缓冲区在传输完成前被释放或覆盖。 4. 外设数据寄存器访问有特殊要求(如需要先读状态位)。 | 1. 确认内存数组传输是否需递增,外设寄存器是否固定地址。 2. 确保源和目标数据宽度与外设一致(如UART是8位)。 3. 将缓冲区改为全局或静态变量。 4. 查阅外设数据手册,确认数据寄存器的访问方式。 |
| DMA传输完成中断不进 | 1. DMA通道中断在NVIC中未使能。 2. 中断服务函数(IRQHandler)未正确实现或未链接。 3. 中断优先级被其他更高优先级中断屏蔽。 4. 在中断回调函数中未清除中断标志。 | 1. 检查NVIC_Init或CubeMX中NVIC配置。2. 确认 stm32f1xx_it.c中对应中断向量函数存在并调用了HAL_DMA_IRQHandler。3. 适当提高DMA中断优先级。 4. 检查是否需要手动清除 DMA_ISR中的标志位(HAL库通常自动处理)。 |
| UART DMA空闲中断不触发 | 1. UART空闲中断未使能。 2. 空闲中断标志未清除,导致只进一次。 3. 波特率偏差太大,导致停止位被误识别为数据。 | 1. 确认调用__HAL_UART_ENABLE_IT(&huart, UART_IT_IDLE)。2. 在空闲中断回调函数开头调用 __HAL_UART_CLEAR_IDLEFLAG(&huart)。3. 校准系统时钟和UART波特率发生器。 |
| ADC DMA采集的数据全是0或固定值 | 1. ADC的DMA请求未使能。 2. ADC未正确校准或启动。 3. ADC通道对应的GPIO未配置为模拟输入。 4. DMA目标缓冲区地址或数据宽度错误。 | 1. 检查ADC_CR2的DMA位。2. 确保调用了 HAL_ADC_Start_DMA。3. 检查CubeMX中GPIO配置模式是否为“Analog”。 4. 用调试器查看ADC的 DR寄存器在转换期间是否有变化。 |
掌握DMA,是让STM32F103C8T6这类MCU发挥其真正性能的关键一步。它不仅仅是“数据搬运”,更是构建高效、实时、低功耗嵌入式系统的基石。从理解总线架构开始,到熟练配置各个寄存器,再到巧妙运用循环模式和双缓冲策略解决实际问题,每一步都需要动手实践和思考。希望这篇笔记能帮你少走弯路,当你第一次看到CPU利用率大幅下降而外设数据流却稳定不断时,你会感受到这种设计的精妙之处。