零基础小白指南:如何在Keil中配置DMA外设
2026/4/18 13:07:36 网站建设 项目流程

零基础也能看懂的DMA实战课:在Keil里亲手“搭”一条硬件数据快车道

你有没有遇到过这样的场景?
ADC采样值一跳一跳像心电图,示波器上CLK信号规整得不行,但printf("%d", adc_val)出来的数字却总在抖;
SPI Flash写入速度卡在200KB/s,明明手册写着支持80MHz Quad SPI,可CPU一忙起来就丢包;
I²S录音时左声道正常、右声道断续,查寄存器全绿,中断也进去了——就是数据不对。

这些不是玄学,而是DMA没配对

别急着翻HAL库文档或复制粘贴例程。今天这堂课,我们不讲抽象概念,不堆术语,就用一支笔、一个Keil工程、一块STM32F407开发板,从寄存器怎么填、缓冲区放哪、中断为什么不进、数据为何错位开始,一步步把DMA这条“硬件数据快车道”真正搭通。


你真正该关心的三个问题,先说清楚

很多教程一上来就甩流程图、列寄存器表,结果新手照着抄完,编译过了,烧录上了,但DMA就是不干活。问题往往出在三个被忽略的“地基级细节”上:

  • 通道和外设之间不是随便连的:DMA1_Channel1 ≠ ADC1,它只认6个指定信号源(RM0090 Table 49),连错=静默失败,连对了才可能出错,连错了根本没反应;
  • 地址对齐不是“建议”,是铁律:当你要搬16位数据(比如ADC_DR),内存起始地址必须是2字节对齐;若搬32位(如SPI->RAM),必须4字节对齐——否则DMA直接报TE(Transfer Error)中断,而你可能连这个中断都没开;
  • 缓冲区不能随便定义在栈上或.data段里:编译器可能把它塞进Flash(只读)、或者紧挨着栈顶(一溢出就踩坏数据)。DMA要的是物理地址稳定、可写、无干扰的RAM块,不是编译器心情好分给你的任意一块内存。

这三个点,搞错任何一个,DMA就成“哑巴外设”。下面我们就用真实配置过程,一个一个把它焊牢。


第一步:让DMA1_Channel1真正认识ADC1

先明确目标:用DMA自动把ADC1规则通道的1024个16位采样值,搬进RAM缓冲区。不轮询、不等待、不占CPU。

▶ 寄存器配置不是填空,是做选择题

DMA1_Channel1->CCR这个32位控制寄存器,每一位都有含义。我们只动最关键的6位,其余清零再置位,避免残留配置干扰:

位域含义为什么这么选
EN1使能通道不开等于没装发动机
DIR0外设→内存(注意!不是存储器→外设)ADC_DR是只读寄存器,DMA只能从它“读”,往内存“写”
MINC1内存地址递增要存1024个数,当然不能全写进同一个地址
PINC0外设地址固定ADC_DR地址永远是0x4001204C,不用变
PSIZE10b(DMA_CCR_PSIZE_16)外设端宽度:16位ADC输出就是16位,错配会读半字或触发错误
MSIZE10b(DMA_CCR_MSIZE_16)内存端宽度:16位缓冲区是uint16_t[],必须匹配

✅ 正确写法(非拼凑):

DMA1_Channel1->CCR = DMA_CCR_EN | // 必开 DMA_CCR_MINC | // 必开(否则覆盖) DMA_CCR_PSIZE_16 | // 必配(与ADC_DR位宽一致) DMA_CCR_MSIZE_16 | // 必配(与buffer类型一致) DMA_CCR_PL_VERY_HIGH; // 优先级拉满,防抢占

❌ 典型错误:把DIR写成DMA_CCR_DIR(即1),以为“存储器→外设”是对的——实际ADC_DR不可写,DMA会卡死在传输准备阶段,DMA_FLAG_TC永远不置位。

▶ 地址对齐:不是加个__align(4)就万事大吉

你写:

uint16_t adc_buffer[1024] __attribute__((aligned(4)));

这没错,但还不够。因为aligned(4)保证的是起始地址%4==0,而16位传输只要求%2==0。但如果你后续升级为32位传输(比如用ADC+DMA+浮点FFT预处理),就必须4字节对齐。

更关键的是:链接器是否真把你放到了RAM里?
默认情况下,Keil把全局数组放进.data段,而.data段默认加载到Flash,运行时拷贝到RAM——但DMA访问的是运行时物理地址。如果adc_buffer被链接到Flash区域(比如0x08005000),DMA就会试图往只读Flash写数据,触发总线错误(BusFault)。

✅ 正解:用__attribute__((section(".dma_buffer")))强制归段 + scatter文件划出独立RAM区(后文详述)。


第二步:Keil里,让缓冲区“住进自己的房子”

Keil不是Arduino,没有malloc()自动管理内存。你定义的每个变量,最终落在哪片物理RAM,由链接脚本(scatter file)说了算。

▶ 默认scatter文件的陷阱

标准stm32f407vg.sct中,RAM段通常是这样写的:

RW_IRAM1 0x20000000 0x00030000 { *(+RW +ZI) }

意思是:所有可读写(.data)和清零初始化(.bss)段,都塞进0x20000000 ~ 0x2002FFFF这段192KB SRAM1里。

问题来了:
- 如果你的工程开了printf、用了大量全局结构体,.data/.bss可能吃掉前64KB;
- 你的adc_buffer[1024](2KB)被编译器安排在.data末尾,比如0x2001F800
- 某天你加了个新模块,.data膨胀到0x20020000,缓冲区就被挤到0x20020000之后——而这里可能紧挨着栈顶(0x2002FFFF),一次深函数调用就溢出覆盖。

✅ 破局之道:给DMA缓冲区单独划一块“自留地”,且位置可控、永不挪动。

▶ scatter文件实战修改(只需3行)

打开你的stm32f407vg.sct,在RW_IRAM1后面加一段:

DMA_BUFFER 0x2000C000 0x00001000 { ; 从0x2000C000起,分4KB专供DMA *(.dma_buffer) }

然后在代码里精准定位:

// 定义在RAM固定地址,不随其他段变化 uint16_t adc_buffer[1024] __attribute__((section(".dma_buffer"), aligned(4)));

现在,无论你工程多大,adc_buffer永远在0x2000C000开始的4KB内,远离栈、远离.data、远离一切干扰。你甚至可以在调试器Memory Window里直接输入0x2000C000,看着1024个0x0000慢慢变成ADC采样值——这就是确定性。


第三步:中断进了,但数据还是错?检查这三个“隐形开关”

DMA传输完成(TC)中断进来了,DMA1_Channel1_IRQHandler也执行了,但adc_buffer[0]还是0x0000?别怀疑硬件,先查这三项:

🔹 1. 启动文件里的中断向量,你真的重写了么?

打开startup_stm32f407xx.s,找到:

DCD DMA1_Stream1_IRQHandler ; DMA1 Stream 1

往下翻,你会发现:

WEAK DMA1_Stream1_IRQHandler WEAK DMA1_Channel1_IRQHandler ...

WEAK意味着:如果你没在C文件里定义同名函数,链接器就用一个空的弱实现(什么也不做)。所以即使NVIC使能了,中断也永远不会跳转到你的代码。

✅ 正解:在main.c最底下,明确定义:

void DMA1_Channel1_IRQHandler(void) { if (DMA_GetITStatus(DMA1_FLAG_TC1)) { // 注意:是TC1,不是TC DMA_ClearITPendingBit(DMA1_FLAG_TC1); // ✅ 此处你的数据已就绪 process_adc_data(adc_buffer, 1024); } }

🔹 2. 缓存(Cache)正在偷偷改你的数据

F4系列开启DCache后,CPU读内存可能从缓存拿,而DMA写的是物理RAM。结果:你process_adc_data()看到的可能是旧缓存值,不是DMA刚写的新数据。

✅ 解决方案(二选一):
-保守法(推荐新手):关DCache(SCB->CCR &= ~SCB_CCR_DC_Msk;),彻底规避问题;
-高效法:在DMA传输前SCB_CleanDCache_by_Addr((uint32_t*)adc_buffer, sizeof(adc_buffer)),传输后SCB_InvalidateDCache_by_Addr(...),确保缓存与RAM同步。

🔹 3. ADC还没真正启动,DMA就在等了

常见错误顺序:

DMA_Cmd(DMA1_Channel1, ENABLE); // ❌ 错!DMA先启,但ADC还没准备好 ADC_Cmd(ADC1, ENABLE);

正确顺序必须是:

ADC_Cmd(ADC1, ENABLE); // ✅ 先让ADC就绪 DMA_Cmd(DMA1_Channel1, ENABLE); // 再启DMA,它才能响应EOC信号

否则DMA处于“监听模式”,但ADC没发EOC,传输永远不开始。


实战延伸:双缓冲音频采集,如何做到“零间隙”?

上面是单缓冲(1024点一传完再处理)。但音频要求连续流,不能等1024点全满了才处理——那会有21ms延迟(48kHz下)。

▶ 双缓冲的本质:两块内存,一个填、一个算

  • Buffer A:0x2000C000,DMA正在往里写第1~1024个采样;
  • Buffer B:0x2000C800,CPU正在读取第1~1024个采样做FFT;
  • 当Buffer A填满一半(512点),DMA触发HT(Half Transfer)中断 → CPU切换到Buffer B开始处理;
  • 当Buffer A填满(1024点),DMA触发TC(Transfer Complete)中断 → CPU切回Buffer A,同时DMA自动把指针切回Buffer A开头,开始下一轮填充。

⚙️ 关键寄存器设置:

// 开启双缓冲模式 DMA1_Channel1->CCR |= DMA_CCR_DBM; // CMAR指向Buffer A首地址(0x2000C000) DMA1_Channel1->CMAR = (uint32_t)buffer_a; // CPAR仍是ADC1->DR // CNDTR = 1024(总长度) // HT中断使能 DMA1_Channel1->CCR |= DMA_CCR_HTIE; // TC中断使能 DMA1_Channel1->CCR |= DMA_CCR_TCIE;

此时,你不再需要process_adc_data(buffer_a, 1024),而是:

void DMA1_Channel1_IRQHandler(void) { if (DMA_GetITStatus(DMA1_FLAG_HT1)) { DMA_ClearITPendingBit(DMA1_FLAG_HT1); process_audio_block(buffer_b, 512); // 处理B块前半 } if (DMA_GetITStatus(DMA1_FLAG_TC1)) { DMA_ClearITPendingBit(DMA1_FLAG_TC1); process_audio_block(buffer_a, 512); // 处理A块后半 } }

✅ 效果:CPU处理时间只要<10.4ms(512点@48kHz),就能永远追上DMA,实现无缝音频流。


最后送你一句硬核经验

DMA不是魔法,它是可预测的硬件状态机。它的每一个行为,都对应着某一位寄存器的0或1、某一段内存的物理地址、某一个外设事件的精确时序。当你发现它不工作,不要猜,打开Keil的Register View,盯着CCRCNDTRCMARCPAR四个寄存器看——哪个值不对,就去源头改哪个。

真正的嵌入式掌控感,从来不是调通一个例程,而是在寄存器层面,看见数据流动的每一步

如果你在配置过程中遇到了其他挑战——比如SPI DMA收发不同步、TIM触发DMA相位偏移、或者想把DMA和FreeRTOS队列联动——欢迎在评论区分享,我们一起拆解。

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

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

立即咨询