资讯动态

STM32F407 ADC+DMA寄存器版单通道采集详解

发布时间:2026/10/14 1:06:10 来源:尧图企业网站定制
1. 项目概述1.1 为什么选择“寄存器版”ADC驱动很多朋友拿到STM32F407开发板第一步都是点亮LED第二步就是用标准库或者HAL库点一下ADC读个电位器电压值出来。但真到了做数据采集类的项目——比如电流采样、电池电压监测、传感器模拟量输入——再回头扒ADC寄存器时往往就抓瞎了。寄存器版本的好处在于你不需要把整个HAL库的初始化链路背下来而是直接对照参考手册把ADC的每一个关键配置位敲进去心里清清楚楚知道“我这个采样率是怎么来的”“为什么DMA要开循环模式”。F407的ADC和F103的ADC在寄存器层面差别不小。F407有三路ADC每个ADC最高12位分辨率多达16个外部通道还支持双重和三重ADC模式。本文讲的“独立ADCDMA单通道”其实是最常用也最适合入门的一种组合ADC1独立工作采集一个通道比如PA1对应的通道1DMA自动把转换结果搬到内存数组里CPU从头到尾不用管数据搬运这件事。这种结构在电机控制、电源管理、仪表类产品里非常常见。1.2 这套方案要解决什么问题如果只配置ADC而不开DMA你需要在主循环里不断读取ADC数据寄存器ADC_DR软件触发一次转换等待EOC标志再读数据。这在低速场景下没什么问题但一旦采集频率上去了或者CPU还有别的任务需要处理比如跑屏幕刷新、通信协议、控制算法反复中断和轮询就会浪费大量主频。而且如果每次读取数据之间有时间间隙采集出来的数据就不是严格的等间隔采样后续做滤波、FFT之类的处理时会产生误差。开启DMA之后ADC每次转换完成会自动触发DMA请求DMA把结果搬到指定的内存缓冲区缓冲区存满还能触发传输完成中断。整个过程不占用CPU采样间隔完全由ADC的时钟分频和采样时间决定等间隔、自动化、高一致。这正是我想说的核心思路不是去调一个简单能用的ADC而是把一个适合做真实产品的高速采集前端搭出来。个人建议是即使你最终要用HAL库也值得跑一遍寄存器版本。因为寄存器版本让你把时钟树、ADC时钟分频、采样周期、DMA传输方向这些都摸透了回头用库的时候才知道每个函数背后帮你干了什么、默认参数是什么含义排查问题的时候思路会快得多。2. 硬件准备与资源规划2.1 引脚和ADC通道映射F407的ADC1通道映射关系如下ADC1_IN0PA0ADC1_IN1PA1ADC1_IN2PA2ADC1_IN3PA3ADC1_IN4PA4ADC1_IN5PA5ADC1_IN6PA6ADC1_IN7PA7ADC1_IN8PB0ADC1_IN9PB1ADC1_IN10PC0ADC1_IN11PC1ADC1_IN12PC2ADC1_IN13PC3ADC1_IN14PC4ADC1_IN15PC5我用的是PA1对应ADC1的通道1。PA1在多数开发板上没有复用冲突不容易和USART、I2C的引脚打架调试的时候最省心。如果你手头要采集的信号恰好分布在别的引脚上查一下映射表就行代码里只需要改通道选择位和引脚配置这两个地方。需要注意一个细节F407的ADC输入引脚全部是模拟功能需要把对应的GPIO模式设为模拟输入MODER寄存器配置成11。这里最容易踩的坑是忘记把引脚设置成模拟模式——如果不设置引脚默认是浮空输入模拟电压进去后采样结果会偏差很大甚至读数固定在一个异常值。寄存器版里对这个很敏感少配一位就会出问题。2.2 DMA控制器选型F407有DMA1和DMA2两个控制器DMA1有8个数据流Stream0到Stream7每个数据流有8个通道Channel0到Channel7。ADC1的DMA请求映射情况是DMA2_Stream0 Channel0ADC1DMA2_Stream4 Channel0ADC1这里有个容易搞混的点F103的DMA只有通道的概念没有“流”的概念F407把这两层结合起来。实际配置时要同时选对Stream和Channel缺一个DMA都不会工作。常用的是DMA2_Stream0_Channel0因为它的优先级设置相对独立且不太容易和别的外设冲突。当然DMA2_Stream4_Channel0也可以用看你的工程里DMA2的其他流被谁占用了。如果项目里SPI、UART也用了DMA2就要统一规划。2.3 时钟树对采样频率的影响ADC的时钟来自APB2但中间有个分频器。F407的ADC时钟是挂在APB2下的最大允许频率是36MHz。如果APB2是84MHz那么ADC时钟分频最大只能到4分频也就是21MHz4分频再往下到6分频是14MHz8分频是10.5MHz。分频系数由ADC_CCR寄存器里的ADCPRE位控制。大家最关心的采样频率计算公式是Fadc Fapb2 / ADCPRE分频系数Fin Fadc / (采样周期 12)这里的采样周期是可配置的有3、15、28、56、84、112、144、480这几个档位。比如用4分频的21MHz ADC时钟采样周期选3个周期那么一次转换耗时就是31215个ADC时钟周期也就是15/21MHz约714ns换算成采集频率约1.4MHz。这已经相当高了实际项目中极少有这个需求。我自己的实践是ADC时钟分频取4采样周期取84计算下来一组样品大概是4.57微秒约219kHz采样率。这个参数对多数传感器、电压采集、电流采样都够用抗干扰能力也比较好。如果后面还需要更高的采样率只需要把采样周期往下降一档即可。3. 寄存器级ADC配置实操3.1 开启时钟和GPIO模拟模式第一步是打开ADC1和DMA2的时钟。F407的ADC1挂载在APB2总线上DMA2挂载在AHB1总线上。对应的时钟使能位在RCC的寄存器里RCC-APB2ENR | RCC_APB2ENR_ADC1EN; // 使能ADC1时钟 RCC-AHB1ENR | RCC_AHB1ENR_DMA2EN; // 使能DMA2时钟 RCC-AHB1ENR | RCC_AHB1ENR_GPIOAEN; // 使能GPIOA时钟这一步看似基础但遗漏的概率不低。好多时候排查半天最终发现是某个时钟没打开。注意F407的ADC1时钟使能位置在APB2ENR的第8位和F103的ADC1在APB2ENR位置相同但DMA使能的寄存器完全不一样。GPIOA引脚模式设置为模拟输入。PA1对应的MODER寄存器位是GPIOA-MODER的位[3:2]两个位配成11即是模拟模式。实际写的时候可以先把对应位清零再置位避免破坏其他引脚配置GPIOA-MODER ~(3U (1 * 2)); // 清除PA1模式位对应位2-3 GPIOA-MODER | (3U (1 * 2)); // 设置PA1为模拟模式 GPIOA-PUPDR ~(3U (1 * 2)); // 不使能上下拉保持浮空模拟输入模式下上下拉必须关掉。上下拉电阻会影响信号源的驱动能力和采样精度尤其是采集高内阻信号源时下拉电阻会直接把输入电压拉低导致读数偏小。3.2 ADC时钟分频与ADCCR寄存器在配置ADC模块本身之前需要先把ADC工作时钟确定下来。ADC时钟分频位在ADC_CCR寄存器的最低位ADCPRE这个寄存器是三个ADC共用的不是ADC1独有。取值关系是00PCLK2分频201PCLK2分频410PCLK2分频611PCLK2分频8我的系统里APB2是84MHz选择01即4分频得到21MHz ADC时钟低于F407要求的36MHz上限。这个余量比较舒服频率足够高采样速度快又不会让ADC工作在极限边缘避免引入噪声。ADC-CCR | (0x01 16); // ADCPRE 01即4分频注意ADC-CCR是ADC公共控制寄存器地址在ADC1、ADC2、ADC3的公共部分操作的是外设基地址偏移量。这三个ADC共同分享这组寄存器配置一次作用于所有ADC模块。3.3 ADC1的CR1、CR2、SQR1和SQR3接下来是ADC1的个性化配置。我用到的寄存器主要有CR1、CR2、SQR1和SQR3。CR1寄存器需要开启扫描模式吗不需要。只采集单通道时扫描模式开不开影响不大但最好是关闭保证ADC只处理一个通道。同时要配置分辨率为12位。下面这段是我实测可用的初始化ADC1-CR1 | (0x00 24); // 分辨率12位RES[1:0] 00 ADC1-CR1 ~(1 8); // 关闭扫描模式 SCAN 0 ADC1-CR2 | (1 0); // ADON 1开启ADCCR2寄存器是本项目的重点因为DMA使能位和连续转换模式都在这里。我最终的配置如下ADC1-CR2 | (1 1); // CONT 1连续转换模式 ADC1-CR2 | (1 8); // DMA 1使能DMA请求 ADC1-CR2 | (1 9); // DDS 1DMA连续请求 ADC1-CR2 | (1 10); // EOCS 0EOC在每次转换完成后置位 ADC1-CR2 | (1 30); // SWSTART 1软件启动转换逐位解读一下CONT 1ADC在完成一次转换后立即开始下一次转换不需要再次触发。这是实现“自动连续采样”的核心。DMA 1当ADC转换完成、数据寄存器更新后可以发出DMA请求把数据搬到内存。DDS 1只要DMA是使能状态每次转换结束都会发出DMA请求如果置0则只发出一次请求。在做连续转换时必须置1否则只能采到第一个数据。EOCS 0每次单个通道转换完成时EOC标志都会置位。SWSTART 1触发一次软件启动因为CONT1启动后就一直跑下去了。SQR1寄存器里要设置通道序列长度。虽然有扫描模式但扫描模式关闭后规则通道序列长度为1就够了。设置方法是ADC1-SQR1 ~(0x0F 20); // L[3:0] 0规则序列长度为1SQR3寄存器用来设置第一个规则通道是哪个通道。我们要采集ADC1_IN1所以设置ADC1-SQR3 ~(0x1F 0); // SQ1[4:0] 0 ADC1-SQR3 | (0x01 0); // SQ1 通道1SMPR2寄存器用来设置采样时间。ADC1_IN1属于SMPR2寄存器管理的通道0到9。采样周期选择是关键直接决定采样速度和抗干扰能力。我用84个周期的配置ADC1-SMPR2 | (0x05 (1 * 3)); // SMP1[2:0] 101即84周期4. DMA配置与数据搬运细节4.1 DMA2_Stream0_Channel0的寄存器配置ADC1的DMA请求对应DMA2的Stream0和Channel0。F407的DMA配置比F103复杂需要同时设置数据流方向、外设地址、内存地址、传输数量、优先级、FIFO模式、突发传输等。我的完整初始化代码RCC-AHB1ENR | RCC_AHB1ENR_DMA2EN; DMA2_Stream0-CR ~(1 0); // 关闭DMA流EN 0 while (DMA2_Stream0-CR (1 0)); // 等待确认关闭 DMA2_Stream0-CR | (0x01 6); // CHSEL[2:0] 001选择通道1 DMA2_Stream0-CR ~(1 4); // DIR[1:0] 00外设到内存 DMA2_Stream0-CR ~(1 5); DMA2_Stream0-CR ~(0x01 13); // PSIZE[1:0] 00外设数据宽度16位 DMA2_Stream0-CR ~(0x01 14); DMA2_Stream0-CR ~(0x01 11); // MSIZE[1:0] 00内存数据宽度16位 DMA2_Stream0-CR ~(0x01 12); DMA2_Stream0-CR ~(0x03 9); // PL[1:0] 00优先级低 DMA2_Stream0-CR ~(0x01 16); // PINC 0外设地址不递增 DMA2_Stream0-CR | (0x01 17); // MINC 1内存地址递增 DMA2_Stream0-CR ~(0x01 20); // CIRC 0后面单独配置 DMA2_Stream0-CR | (0x01 20); // CIRC 1循环模式 DMA2_Stream0-CR ~(0x01 15); // PFCTRL 0由DMA控制器决定FIFO使用 DMA2_Stream0-NDTR 32; // 传输数量32个采样点 DMA2_Stream0-PAR (uint32_t)(ADC1-DR); // 外设地址ADC1数据寄存器 DMA2_Stream0-M0AR (uint32_t)adc_buf; // 内存地址采样缓冲区 DMA2_Stream0-FCR | (0x01 2); // DMDIS 1不使用直接模式 DMA2_Stream0-FCR ~(0x03 0); // FTH[1:0] 00FIFO阈值1/4 DMA2_Stream0-CR | (0x01 0); // 使能DMA流有几点我特别想提醒第一在配置DMA寄存器之前必须先关闭DMA流EN0配置完成后再打开。DMA流不能在运行状态下修改关键参数否则会导致未定义行为。这是F407和F103的显著差异F103修改DMA配置前也需要先关但F407如果不等待确认关闭残留状态会影响后续配置。第二PAR寄存器和M0AR寄存器赋值时一定要加取地址符和类型转换。好多新手犯的错是把外设寄存器值赋给PAR比如写成ADC1-DR而不是(ADC1-DR)DMA就会去访问一个错误的地址采样结果永远是0。第三NDTR寄存器存放的是“传输次数”而不是“字节数”。当数据宽度是16位时NDTR32意味着传输32个半字也就是64字节。4.2 内存缓冲区设计缓冲区数组定义在全局作用域不要定义成函数内部的局部变量——在函数内定义会自动分配到栈上DMA写入时栈指针可能已经移动导致数据写到错误地址而且栈空间有限大数组会把栈挤爆。正确做法是定义成全局数组#define ADC_BUF_LEN 32 volatile uint16_t adc_buf[ADC_BUF_LEN];加不加volatile必须加。因为DMA是直接写内存CPU从数组里读数据时编译器如果不知道这个数组会被“外部”修改可能会优化掉一些读取操作或者在循环里缓存旧值。volatile告诉编译器每次访问都要真实读内存这样你读到的一定是最新一次DMA搬运的结果。为什么选择32个点这其实是一个折中。NDTR越大内存占用越大DMA中断触发的间隔越长NDTR太小比如8个点DMA中断会很频繁CPU负担大。32个点16位宽度共64字节在F407上占用极小同时能很好地平衡中断频率。如果你采样率很高比如500kHz以上建议把缓冲区放大到128甚至256减少中断次数。4.3 循环模式与半传输中断DMA循环模式CIRC1的好处是缓冲区填满后DMA自动把NDTR重新复制为初始值外设地址继续从ADC1-DR读取内存地址回到缓冲区起始位置整个过程不需要CPU介入。这样ADC采集是真正无缝的——永远在采永远在搬永远在覆盖。如果开了DMA中断通常有两种做法传输完成中断缓冲区整体填满后进入中断处理完所有数据后继续。半传输中断缓冲区的上半部分填满时进入一次中断下半部分填满时再进一次。这样CPU可以在DMA搬运后半部分数据时处理前半部分达到流水线效果。本项目只用了传输完成中断做数据更新标志不做半传输原因是单通道低速采样对这个优化不敏感。但在高速采样或FOC类控制应用中半传输中断是必须考虑的。中断配置如下DMA2_Stream0-CR | (0x01 21); // TCIE 1传输完成中断使能 NVIC_EnableIRQ(DMA2_Stream0_IRQn); NVIC_SetPriority(DMA2_Stream0_IRQn, 2);中断服务函数里只做一件事置一个标志位。数据处理放到主循环里做不要在中断里做复杂运算这是中断编程的基本原则。volatile uint8_t adc_data_ready 0; void DMA2_Stream0_IRQHandler(void) { if (DMA2_Stream0-ISR (1 5)) { // TCIF0 DMA2_Stream0-IFCR | (1 5); // 清除传输完成标志 adc_data_ready 1; } }IFCR寄存器的对应位要写1清除中断标志这是F4系列DMA中断服务里必做的一步。漏掉的话中断会反复触发整个程序会卡死在中断里出不来。我在这里卡过一次调试器看代码逻辑怎么都正确最后发现是没清标志服务函数每次中断退出后马上又进来。5. 主循环数据读取与电压换算5.1 数据读取流程主循环的框架代码int main(void) { ADC1_Init(); DMA2_Init(); while (1) { if (adc_data_ready 1) { adc_data_ready 0; uint32_t sum 0; for (int i 0; i ADC_BUF_LEN; i) { sum adc_buf[i]; } uint32_t average sum / ADC_BUF_LEN; float voltage (float)average * 3.3f / 4095.0f; printf(average_raw: %lu, voltage: %.2f\r\n, average, voltage); } } }这里我特意对32个原始值做了一次均值。为什么虽然ADC本身是12位分辨率但单次采样的最低位跳动比较大噪声环境下可能波动十几个LSB。对多个点求平均是一种最简单的数字滤波能有效压低随机噪声。32个点平均后有效分辨率大约能提升2-3位同时读数会稳定很多。这对应付“电位器旋钮电压读数乱跳”的场景特别有效。很多初学者发现ADC值不稳定第一反应是怀疑硬件接线其实更多时候是缺少滤波。采样周期、均值次数、前端RC滤波器三者配合才能得到干净的数据。5.2 电压换算的细节F407的ADC是12位满量程对应数字4095参考电压一般是3.3VVREF。换算公式电压 原始值 × VREF / 4095上面的代码里用的是L点到浮点的浮点运算精度够用。但如果做嵌入式定点系统或者mcu没有FPU最好用整数运算先乘后除来避免截断误差。F407有FPU这里无所谓。需要注意如果参考电压不是标准3.3V比如某些板子的VREF引脚接了外部基准源2.5V那么公式里的基准电压也必须改否则所有读数都会有固定比例的偏差。我遇到过一种情况开发板的VDDA和VREF分别供电VREF实际是3.29V和3.3V差了0.01V虽然看起来不多但在高精度测量场景下每个采样点都差约5个LSB累计起来就不是小数了。最好的做法是用万用表实测VREF把实际值写进去。5.3 打印和调试验证串口打印初始化我用的是USART1重定向fputc即可。调试验证的关键是给PA1接一个可调的直流电压比如用滑阻分压、信号发生器或者直接碰VDD和GND观察打印出来的电压值是否和万用表测量值一致。每次我完成这类工程都会按这个顺序验证接GND到PA1读数应该是0附近如果偏大检查地线是否共地。接VDD到PA1读数应该接近3.3V如果偏小检查VREF供电和ADC内部阻抗。接一个2V左右的中值电压核对线性度。三组测试都通过说明ADCDMA链路完全正常。如果中间某一个点偏差明显多半是采样时间不够导致源阻抗过大或者电源噪声而不是DMA配置问题。6. 常见问题速查与避坑心得6.1 现象对应原因分析表现象最可能原因排查优先级采样值一直是0DMA未使能、ADC通道配置错误1. 检查CR2的DMA位和DDS位2. 检查SQR3通道号3. 检查DMA PAR地址采样值随机跳变未做均值滤波、采样时间太短1. 软件均值2. 增大SMPR采样时间3. 检查VREF去耦电容第一次采样正常后面全为0DDS位没置1置CR2的DDS位采样值大约正确但恒低一点VREF实际值低于3.3V1. 实测VREF2. 检查VCAP等参考电压DMA中断一直触发程序卡死中断服务函数未清标志IFCR对应位置1采样值全是65535数据宽度设为32位或地址不对检查PSIZE/MSIZE这张表是我自己调试经验的浓缩。其中DDS位问题最隐蔽也最容易漏连续转换模式下DDS0只能产生一次DMA请求采到第一个值之后DMA不再搬运程序表现为“第一次读到数据后面再也不更新”。如果只在主循环里打印adc_buf[0]根本发现不了问题因为那个值可能是合法的。换成一次性打印整个数组就露馅了后面的值全是旧数据。6.2 关于引脚复用冲突PA1在多数开发板上是纯GPIO引脚上的其他复用功能非常少。但如果是用其他通道比如PA0需要确认板上有没有接按键、LED、或者别的外设因为PA0往往和WKUP、TIM2_CH1等复用。一旦复用了定时器PWM或者编码器模式引脚电平会被外设抢占ADC采样会失败。遇到这种问题不要先查ADC配置先查这个引脚还有谁在用。单片机开发里有个老经验硬件复用的问题比软件问题丑十倍。软件错了能立即定位硬件复用冲突经常是玄学级别的慢慢排查。所以选通道时一定要避开和定时器PWM输出、I2C、SPI这些常用功能冲突的引脚。6.3 电源和参考电压处理ADC本身就是模拟器件对电源噪声极其敏感。F407虽然有内部参考电压但VREF引脚外部必须放去耦电容一般用1uF和100nF并联。VDDA也需要类似的滤波。实际硬件中如果发现采样值在静止状态下还有较大波动我建议按以下顺序检查用示波器看VREF上的纹波。用示波器看PA1上的信号源是否有毛刺。确认ADC采样保持电容充电时间是否足够也就是采样周期是否太短。信号源内阻太大也会导致问题。假设信号源内阻10KADC采样保持电容大约几pF在一个很短采样窗口内充不满电采样结果就会偏离真实值。这个时候增大采样周期是最简单的补救方案如果还不行就要在信号源和ADC引脚之间加一个运放跟随器。6.4 初始化顺序的影响寄存器的配置顺序是有讲究的不是随便写完就能工作。我推荐的标准顺序是打开GPIOA、ADC1、DMA2时钟。配置GPIOA_PA1为模拟输入。配置ADC分频、ADC电源开启ADON1。等待稳定时间官方建议至少几个us。配置采样时间、序列长度、通道号。配置DMA的所有参数并打开DMA。在ADC_CR2置位SWSTART启动转换。等待DMA中断或标志位。这里最容易被忽视的是ADON1之后到SWSTART之前的稳定时间。如果上电后马上就启动转换ADC内部可能还在建立偏置第一批数据的准确性会打折扣。实用做法是在初始化最后加个几十us的延时或者干脆在启动转换前用for循环做个空转。6.5 常踩的寄存器位坑最后集中总结几个我实际操作中确认过的位配置CR2的EXTSEL位和EXTEN位软件触发的时候不需要配置外部触发选择但EXTEN要清除。有些人把默认启动序列里的EXTEN遗漏清洗导致ADC被上一个工程的触发电平意外触发整个转换节奏错乱。SQR1的L[3:0]位是“序列长度减1”不是序列长度本身。配置长度为1时写0配置长度为4时写3。写错会导致ADC认为规则序列里有多个通道转换流程混乱。DMA_CR的MBURST和PBURST位如果设成了突然传输模式FIFO和地址对齐都变了ADC用不上这个特性保持默认不变就行。改动的后果往往是NDTR传完一次就停了DMA中断也不再触发。内存数组的数据宽度必须和外设数据宽度一致。ADC_DR是16位内存数组也必须是uint16_t。如果定义成uint32_tDMA传输会正常搬但字节对齐和地址填充方式会导致每个有效数据被错位放置。7. 扩展思路从单通道到多通道和多重ADC这套“ADC1DMA2_Stream0单通道”的方案稳定运行之后扩展能力其实很强。下面三个方向都是我个人实测过有投入价值的多通道规则采集把SQR1的L改为N-1SQR3到SQR1寄存器按顺序填入多个通道号开启扫描模式SCAN1DMA缓冲区长度设为通道数×每通道均值数。要注意的是多通道下DMA传输顺序和通道顺序完全一致数据处理时要按索引分组不能一团浆糊地平均。注入通道配合规则通道F407支持规则组和注入组交替采集。规则组负责连续后台采样注入组在控制时刻插入采样。比如电机控制里规则组采集三相电流注入组采集母线电压配合起来非常方便但配置复杂程度上一个台阶。双重ADC模式ADC1和ADC2同步采样能同时采两路信号DMA可以直接从高16位、低16位组合读取结果。这在电网同步采样、双通道振动检测里很有用。我自己目前的项目里已经把这套代码顺手改造成了四通道扫描采集DMA缓冲区128点中断频率约1kHz。跑下来CPU占用率比原来的轮询方式低了差不多三成波形也干净很多。整套逻辑仍然是基于这套单通道基础扩展的可见把地基打好的重要性。8. 最后的实操提点写了这么多最想对你说的其实是拿到代码之后别急着下载先把整个初始化流程在脑子里过一遍——为什么先开时钟为什么引脚要配成模拟模式为什么DMA要先关再配为什么启动转换要放最后。把这几个问题答上来这套代码才真正是你的。真机调试的时候建议先用万用表量好输入电压打印值一旦和预期不符马上定位到“配置问题”还是“硬件问题”。如果打印出来是0先查DMA使能位再查通道号如果跳变先加均值再拉长采样周期。这些分支可以画一张脑图会极大缩短你的调试时间。这套方案我用来做过电池电压监测、压力传感器采集和音频采样基本换一个引脚和通道号就能跑通。F407的ADC资源足够日常绝大多数使用场景发挥性能希望这篇流程能帮你绕过我踩过的坑一次调通。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑