尧图精选

STM32F407 ADC+DMA采样实战:从原理到避坑指南

🕒 发布时间:2026/9/1 8:57:50 📁 来源:尧图网络
简介这是一份基于STM32F407的ADC与DMA结合使用例程包面向嵌入式开发者和STM32学习人群旨在帮助理解并实现高效的多通道模拟信号采集适用于工业控制、环境监测、音频处理等需要实时数据采集的场景。压缩包共172个文件体积仅4.61MB以C/H源码文件为主并包含Keil工程文件uvprojx/uvoptx、编译生成的axf/hex/map以及外设驱动、LCD显示等相关模块工程结构完整可直接打开编译与调试也便于对照学习各模块之间的调用关系。目前已有639人学习内容涵盖ADC通道选择、采样时间与分辨率设置、DMA通道映射、传输触发配置以及中断服务程序等关键环节。通过实际代码展示了ADC与DMA从初始化、启动到转换结果自动传输至内存的完整流程并配有定时器、LCD等辅助文件便于观察采集结果与调试验证。对希望快速上手STM32F407数据采集开发、进阶学习DMA传输机制的工程师是一份实用的参考模板。 做嵌入式这些年被ADC采样折腾过的次数不少但要说最值得写成文章分享的还是STM32F407上这套ADCDMA的组合。很多人一开始会想ADC采集不就是调个库函数读取转换结果寄存器吗这么做确实简单但一旦采样率高起来、数据量大起来CPU就会被中断反复打断最后你会在调试中发现主循环里的任务全被拖慢了。我自己也走错过弯路后来把ADC和DMA配合起来用才发现这套方案才是真正能打的生产级用法。这篇文章我会把整个工程从配置原理到实际操作全部拆开从为什么DMA能解决问题、到CubeMX里面怎么配置、再到HAL库底层句柄是怎么关联的最后把我实测踩过的坑和进阶玩法一并写出来。不论你是在做电机电流采样、音频采集还是多通道传感器数据采集这套思路都适用。1. 为什么是ADCDMA一个被中断反复打断的CPU有多难受1.1 常规ADC读取方式的隐性代价在STM32F407上ADC外设本身其实非常勤快。只要你把它配置成连续转换模式它就会自动按照设定好的采样周期和分辨率一轮接一轮地完成模拟量到数字量的转换转换结果放在ADC_DR数据寄存器里。问题就出在谁去拿结果上。最粗暴的做法是CPU轮询主循环里死等ADC状态寄存器里的EOC标志位置位了就立刻去读ADC_DR。这种做法在单通道、低采样率的小demo里完全没问题频率只有几十赫兹的时候CPU根本感觉不到压力。可一旦采样率到了几十kHz甚至上百kHzCPU几乎什么都没干时间全花在等待转换完成、搬运数据上了。另一种做法是开ADC转换完成中断。每次转换完成硬件触发一次中断CPU暂停当前任务跳进中断服务函数把ADC_DR的值搬进内存数组。这个思路确实省了轮询的浪费但中断是有成本的——入栈、出栈、跳转、执行回调每一步都在消耗CPU周期。采样率100kHz时相当于每10微秒就要触发一次中断。如果中断服务函数再稍微写长一点整个系统的实时性会被严重侵蚀你能看到的现象就是主循环里明明没几行代码却跑得像蜗牛一样慢。1.2 DMA才是真正负责搬运的干活者DMADirect Memory Access直接存储器访问存在的意义就是把这件把数据从外设搬到内存的重复劳动从CPU手里接过去。它本身就是总线上的一个主设备可以配置好源地址、目标地址和传输长度之后完全靠硬件自动完成数据搬运全程不需要CPU干预。具体到ADC场景里DMA要做的事情是这样的ADC每完成一次转换产生一个DMA请求信号DMA收到请求后把ADC_DR里的16位数据搬运到内存里预先定义好的数组搬完一次源地址不变因为ADC_DR只有一个目标地址自动加1数组下标往后走一位计数值减1等整个数组都填满了DMA触发一次传输完成中断通知CPU数据已经全部就位你可以来处理了。也就是说原来要触发几百上千次中断的工作量现在被压缩成了一次中断数据在后台由DMA悄悄搬完。CPU只在采集完成之后收到一个通知。这是ADCDMA这套组合最核心的价值。1.3 什么情况下你才真正需要DMA我也见过不少朋友问我用ADC中断的方式做低速采集也没觉得卡有必要切换成DMA吗我的判断标准很简单有一条符合就建议用DMA采样率超过1kHz尤其是接近或超过ADC时钟可支持的上限多通道扫描采集每次转换需要保存多个通道的数据数据需要连续不间断采集比如音频流、振动分析、电流波形记录CPU同时还要跑屏幕刷新、通信协议栈、PID控制等任务。如果你的项目只是每隔几百毫秒读一次电位器电压那确实没必要上DMA中断足够用。但如果你是做中高速数据采集DMA这套方案早晚要掌握。2. CubeMX配置链路时钟树、DMA参数与Continuous Requests2.1 先从时钟树说起ADC时钟不是想多快就多快STM32F407的ADC是挂在APB2总线上的而APB2的最高时钟是84MHz当系统主频跑到168MHz时。但ADC内部需要的时钟频率并不是这个值它有一个独立的时钟分频器可以配置为PCLK2的2、4、6、8分频。有个硬性指标需要牢牢记住F407的ADC时钟最高不能超过36MHz。所以当你在CubeMX里把系统主频设为168MHz、APB2设为84MHz之后ADC的分频系数最小只能选4即84/421MHz这样才在36MHz的安全范围内。如果你选了2分频得到42MHz的ADC时钟已经超了上限转换结果精度和稳定性都会受影响。在野外跑实测的时候你未必能立刻发现问题但它就埋在那里长期运行后误差会逐渐显现。在CubeMX的Clock Configuration页面里找到APB2总线确认它显示为84MHz然后在ADC设置页面里把Prescaler选为4或更大。这里有个小技巧如果你对采样速度要求特别高可以先把APB2的超频脚位改掉但正规做法是保持ADC时钟不超过36MHz然后通过调整采样周期来平衡速度与精度。2.2 ADC参数详解扫描模式、连续转换和DMA Continuous Requests先说说Scan Conversion Mode扫描模式。如果你的需求是多通道采集比如同时采三路模拟信号那就要把扫描模式打开。扫描模式下ADC会按照你配置的通道顺序依次对每个通道完成采样和转换。如果你的需求是单通道那可以不开扫描模式但开了也没问题只是通道序列里只配一个通道即可。再来看Continuous Conversion Mode连续转换模式。这个模式意味着ADC完成一轮转换后立刻自动开始下一轮不需要外部触发也不需要软件再次启动。单通道采集时开这个模式会很舒服ADC一直自动跑DMA在后台一直搬。多通道扫描时连续转换模式同样适用但你要留意转换速度——扫描模式下ADC会轮流转换每个通道如果通道数很多每个通道的数据刷新率会被拉低。连续转换模式有个孪生开关叫DMA Continuous Requests这个选项非常关键很多人就是栽在它上面。它位于CubeMX的ADC配置面板里同时也会在DMA Settings中出现字面意思是DMA连续请求。勾选状态下ADC只要完成一次转换就会持续不断地向DMA发送请求DMA也会持续响应形成不间断的数据流搬运。不勾选状态下ADC完成一轮转换后就不再产生DMA请求你需要再次调用HAL_ADC_Start_DMA才能重新启动传输。我做连续波形采集时的配置思路是这样的ADC开启扫描模式、开启连续转换模式DMA设置为循环模式CircularDMA Continuous Requests务必勾选。这一套组合下来ADC就像一台永不停止的机器DMA在后台不停地搬运直到你主动调用停止函数才停下来。2.3 DMA参数的关键选择为什么数据宽度必须是Half WordDMA配置里常见的四个参数是Mode、Direction、Peripheral Increment和Memory Increment再往下还有Peripheral Data Width和Memory Data Width。大部分参数都好理解但有两个地方容易出错。Mode选择Circular循环模式这样DMA搬完一轮数据后会自动把内存地址重置回数组起始位置继续下一轮搬运。这样你拿到的是一个不断更新的环形缓冲区永远能读到最近一个周期的数据。如果选Normal搬完一次就停了连续采集就要靠反复重新启动DMA来实现比较麻烦。Direction选Peripheral To Memory这很好理解因为数据是从ADC外设流向内存数组的。Peripheral Increment要禁掉因为源地址ADC_DR是固定不变的始终就那一个寄存器。而Memory Increment要打开因为内存数组的地址需要逐次递增。这两个配置如果反了你读到的数据要么全是同一块地址的内容要么源地址乱跳采集出来的数据根本没法看。最关键的是数据宽度。ADC的转换分辨率是12位转换结果存在ADC_DR寄存器的低16位里。所以在DMA配置里Peripheral Data Width和Memory Data Width都要选Half Word16位。为什么不能选Word32位选Word会把ADC_DR寄存器连同旁边的无关数据一起搬进来高16位是未定义的你的数组里会混入莫名其妙的数值。为什么不能选Byte8位选Byte会把16位结果截断成低8位12位的分辨率直接掉到8位精度损失惨重。内存数组的类型也要配套C语言里定义成uint16_t数组这样才能保证数组元素大小和DMA搬运的数据宽度完全一致。如果定义了uint8_t数组但DMA宽度是Half WordDMA写入时会直接越界踩内存轻则数据错乱重则引发硬件错误中断。3. HAL库的句柄魔法ADC与DMA是怎么绑在一起的3.1 从hasDMAFlag到__HAL_LINK_DMA句柄的底层纽带用过HAL库的人对ADC_HandleTypeDef这个结构体应该不陌生它里面保存了ADC的所有状态和控制信息。但真正把ADC和DMA连接起来的其实是一组精心设计的回调指针和宏定义。在ADC的初始化代码里你会看到类似这样的片段这是CubeMX生成后的典型代码static void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig {0}; hadc1.Instance ADC1; hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode ENABLE; hadc1.Init.ContinuousConvMode ENABLE; hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConvEdge ADC_EXTERNALTRIGCONVEDGE_NONE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 1; hadc1.Init.DMAContinuousRequests ENABLE; hadc1.Init.EOCSelection ADC_EOC_SINGLE_CONV; HAL_ADC_Init(hadc1); }这里面的DMAContinuousRequests字段就是我们在CubeMX里勾选的那个选项在代码层面的实体。它直接决定了ADC完成转换后是否持续地向DMA发出请求信号。真正的绑定动作发生在HAL_ADC_MspInit函数里。CubeMX会在管理外设底层初始化的这个函数中生成DMA句柄并完成关联void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc) { static DMA_HandleTypeDef hdma_adc1; if(hadc-InstanceADC1) { __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_DMA2_CLK_ENABLE(); hdma_adc1.Instance DMA2_Stream0; hdma_adc1.Init.Channel DMA_CHANNEL_0; hdma_adc1.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc DMA_PINC_DISABLE; hdma_adc1.Init.MemInc DMA_MINC_ENABLE; hdma_adc1.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; hdma_adc1.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_adc1.Init.Mode DMA_CIRCULAR; hdma_adc1.Init.Priority DMA_PRIORITY_HIGH; hdma_adc1.Init.FIFOMode DMA_FIFOMODE_DISABLE; HAL_DMA_Init(hdma_adc1); __HAL_LINKDMA(hadc, DMA_Handle, hdma_adc1); } }__HAL_LINKDMA这个宏是HAL库里的一个胶水层它做的事情极简单也极关键把hdma_adc1结构体变量的地址赋值给hadc1的DMA_Handle字段。通过这一步ADC的句柄就拿到了与它绑定的DMA句柄的指针后面所有回调、状态同步都依赖这个指针。3.2 DMA中断是怎么被层层传递回ADC的ADC采集数据的流程并不复杂你调用HAL_ADC_Start_DMA。这个函数做的事情可以拆成三层启动ADC转换设置ADON位启动DMA传输调用HAL_DMA_Start_IT配置好传输参数并开启中断把ADC句柄的DMA状态标记为正在运行。此后硬件开始干活ADC转换一次DMA搬一次DMA搬一次地址递增一次直到整个缓冲区填满。当DMA把数据搬完后奇怪的事情发生了你在中断服务函数里看到的却是ADC相关的回调被触发而不是DMA的回调。这中间的传递关系并不神秘整个链路是这样的DMA2_Stream0的中断服务函数先跑进来调用HAL_DMA_IRQHandler这个函数检查传输完成标志后会通过DMA句柄里的XferCpltCallback回调指针执行一个函数。对于ADC和DMA这种通过__HAL_LINKDMA绑定的组合HAL_DMA_IRQHandler在结束前会把中断标志同步到ADC句柄上这个动作的底层实现其实就是通过那个绑定的指针反过来找到ADC句柄然后调用ADC_DMAConvCplt这是HAL库内部处理ADC DMA传输完成的函数。最终你看到的效果是在stm32f4xx_it.c里写DMA中断服务函数但实际响应用户逻辑的是HAL_ADC_ConvCpltCallback这个回调函数。很多初学者第一次看到这个现象都会懵但只要理解了__HAL_LINKDMA这个指针传递关系一切就顺理成章。3.3 中断服务函数的正确写法既然中断链路是这样传递的那么中断服务函数里写什么就很明确了。以下是标准写法void DMA2_Stream0_IRQHandler(void) { HAL_DMA_IRQHandler(hdma_adc1); }HAL_DMA_IRQHandler会处理所有DMA中断标志正常完成、半传输、传输错误并触发对应的回调。对于ADC采集场景你通常只需要关心传输完成回调void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if(hadc-Instance ADC1) { // 数据已全部搬运到adc_buffer // 在这里处理数据比如计算有效值、滤波、发送等 adc_conversion_complete 1; } }我在实际项目中会在主循环里扫描adc_conversion_complete这个标志位置位后再去处理缓冲区里的数据处理完清零。这样可以避免在中断里做太重的数据处理保持中断服务函数短小精悍。如果采集的数据量大需要一边搬运一边处理上一批那就要用到HAL_ADC_ConvHalfCpltCallback半传输完成回调。这个回调的触发时机是DMA已搬运数组前一半数据时此时你可以安全地处理前一半而后一半还在被DMA持续填充。这是无中断间隙连续采集的经典手法后面进阶部分还会细讲。4. 实测翻车记录数据错位、只采一次、读全零4.1 坑一不勾DMA Continuous Requests数据只更新一轮这是我在快速验证时遇到的最蹊跷的BUG。配置一切正常HAL_ADC_Start_DMA也调用了第一次启动时缓冲区里确实填满了有效数据但之后就再也没更新过看起来像ADC卡死了。排查方式比较直接在主循环里不断检查缓冲区内容的变化结果发现缓冲区里始终是第一次填入的那些值。再用调试器看ADC的状态寄存器发现ADC其实还在转换但DMA的传输计数已经归零了。问题出在DMAContinuousRequests字段没勾选上。不勾选时ADC完成一轮转换后不会再向DMA发送请求DMA传输自然就停止了。CubeMX里这个选项的位置在ADC的Parameter Settings页面底部或者DMA Settings页面的配置里。它默认是Disable的你得主动打开。这个选项要和DMA的Circular模式配合使用DMA要设为CircularADC的DMA Continuous Requests也要Enable两者缺一个都无法实现真正连续的数据采集。4.2 坑二多通道扫描时读到的数据是乱的在我做三通道采集时定义了一个这样的缓冲区#define ADC_CHANNEL_COUNT 3 #define ADC_SAMPLE_COUNT 100 uint16_t adc_buffer[ADC_SAMPLE_COUNT * ADC_CHANNEL_COUNT];理想情况下DMA搬运的顺序应该是通道0的转换结果、通道1的转换结果、通道2的转换结果、通道0的转换结果……但实测读出来的数据前几个值和预期完全对不上。进一步排查发现问题出在ADC通道配置的rank顺序上。CubeMX配置多通道扫描时在ADC的Channel Conversion Rank里你需要给每个通道指定一个Rank序号。这个Rank序号决定了扫描转换的先后顺序而不是你在列表里添加通道的顺序。比如你在CubeMX里先添加了Rank1为IN2Rank2为IN1那么扫描时会先采IN2再采IN1缓冲区的数据排列就是[IN2, IN1, IN2, IN1...]。如果你在程序里按照IN1、IN2的顺序去解析数据结果当然就是乱的。解决方式很直接要么在CubeMX里严格按照你期望的扫描顺序设置Rank要么在代码里按照Rank顺序来解析缓冲区。我在项目中习惯把通道序号和Rank序号设为一致比如IN0就用Rank1IN1就用Rank2这样从缓冲区解析时逻辑一目了然。4.3 坑三为什么Stm32CubeMX生成的工程反复进入HardFaultHardFault是STM32开发者最怕看到的画面ADCDMA场景下最常见的一个诱因是缓冲区越界。DMA搬数据是纯硬件行为它不知道C语言的数组边界在哪只知道目标地址从你给定的起始地址开始按递增方式搬完设定的数据量。如果DMA的目标地址不是合法可写区域或者搬移的数据量超过了数组的边界就可能踩坏其他变量甚至栈空间。我的建议很简单数组定义在全局作用域不要定义成函数内的局部变量。局部变量在栈上分配DMA在后台运行时函数可能已经返回了栈空间被释放给其他函数用这时候DMA还在傻乎乎地往里写数据写完就是一场灾难。还有一点如果在启动DMA前DMA的目标地址是16位对齐的那就一定要保证数组名按16位对齐。对STM32F407来说全局的uint16_t数组默认就是2字节对齐的这没问题。但如果你用了自定义的字节缓冲或者结构体包含缓冲就要小心对齐了。4.4 坑四单独看ADC转换数据正常但加了DMA之后ADC好像失灵了有一种坑现象是ADC中断方式采样完全正常但一旦配置成DMA方式就发现HAL_ADC_Start_DMA调用之后数据从来没有更新过。这种问题的根源多半出在DMA的中断优先级配置上。CubeMX生成工程时如果DMA中断的抢占优先级配得特别低而主循环或某个高频中断的任务一直在运行DMA中断就一直得不到响应。更重要的问题是DMA搬运完成后需要中断来通知CPU如果中断一直被别的更高优先级的任务抢占回调无法及时执行整个数据链路看起来就像停止了。解决方式在CubeMX的NVIC配置页面里把DMA2_Stream0_IRQn的抢占优先级设为较低的值比如5或6同时保证ADC的DMA中断是可以被嵌套打断的但不要设成最低。这样既不影响系统其他关键中断的实时性也能保证DMA传输完成事件及时得到处理。5. 进阶改造双缓冲与定时器触发把采样带宽榨干5.1 双缓冲模式让采集和处理完全重叠前文提到过HAL_ADC_ConvHalfCpltCallback这是实现双缓冲的关键。思路非常简单把缓冲区切成两半DMA前一半数据时触发半传输完成中断此时你处理前一半DMA继续往后一半搬运等你把前一半处理完DMA也正好把后一半填满并触发传输完成中断你再处理后一半。这实际上就是软件层面的乒乓Buffer只是STM32的HAL库已经帮你把触发点分好了。真正的硬件双缓冲DMA的双缓冲模式使用M0AR和M1AR两个内存地址寄存器交替传输在F407上也有支持但用起来麻烦一些核心逻辑就是需要在DMA传输完成中断里手动切换目标地址适合对数据连续性要求极高的场景。如果是做音频采集或振动分析这类连续采集需求用半传输回调的方式已经足够稳定CPU处理前半段数据时DMA正在写后半段两者时间上完全重叠采样过程没有空隙。5.2 定时器触发把采样时刻精确卡在PWM周期的特定相位上另一种进阶玩法是让定时器TRGO触发ADC采样。F407的定时器可以产生更新事件或比较匹配事件这些事件可以作为ADC的外部触发源触发规则组或注入组转换。这种方案典型的应用是电机控制你需要在一个PWM周期的中心点采集相电流此时电流信号最稳定。通过定时器输出PWM的同时把TRGO设置成比较匹配事件ADC就被精确地触发在PWM周期的指定时刻完成采样采样抖动可以控制在纳秒级别。CubeMX配置方式也不复杂在定时器配置中把Trigger Output (TRGO)选为Update Event或Enable在ADC配置中External Trigger Conversion Source选为Timer X Trigger Out eventExternal Trigger Conversion Edge选为Rising Edge或Falling Edge。配置完成后软件启动一次ADC调用HAL_ADC_Start_DMA之后每次定时器触发信号到来ADC都会自动开始转换转换结果由DMA搬运到缓冲区。全程CPU除了处理最终数据外不需要关心采样时刻的精确控制。用这种方式做PWM整流器或逆变器的电流环采样效果比单纯的连续采样好得多——你采到的不是一个随机的瞬时值而是严格对齐在开关周期特定位置的电流值这对闭环控制来说意义重大。5.3 多ADC同步采样的一个思路如果你需要同时采集多路模拟信号比如三相电流可以用F407的三个ADC分别采样A相、B相、C相电流然后通过ADC的注入组/规则组联动或者使用ADC1触发ADC2、ADC3的方式实现同步采样。在CubeMX中将ADC2和ADC3的External Trigger Source配置为Timer的同一个触发事件然后分别启动三个ADC的DMA传输就能得到同一时刻的三路采样值。注意三个DMA流要分配在不同的DMA控制器上或者使用不同优先级避免多个DMA流同时工作时的总线竞争过大导致数据传输延迟。写在后面从最开始用中断方式采个电压都嫌慢到后来用DMA连续搬运几十万点数据这套ADCDMA的组合几乎成了我所有采集类项目的基础设施。回想起踩过的那些坑——DMA Continuous Requests不勾、数据宽度选错、多通道Rank顺序搞混——每个坑其实都可以通过仔细读参考手册躲开但这些细节偏偏是文档最爱一笔带过的部分只有真正做起项目来才会碰上。如果你正准备开始写STM32F407的ADC采集程序我建议你直接把DMA这套方式作为首选方案来搭框架。前面多花十分钟理顺句柄关系和数据流后面能省下几天调试时间。遇到数据不对的时候先查DMA配置和缓冲区不要一上来就怀疑ADC坏了——绝大多数问题都藏在配置的细节里。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →