STM32F407寄存器实现ADC-DMA单通道采集实战

发布时间:2026/10/14 19:49:36
STM32F407寄存器实现ADC-DMA单通道采集实战 做嵌入式这段时间我反而越来越喜欢用寄存器方式写STM32F407的外设驱动。标题里这个“ADC-DMA-单通道”组合看起来是个入门到不能再入门的例程但实际动手把寄存器从头配置一遍你会发现HAL库帮你打包了很多细节而这些细节恰恰是最容易踩坑的地方。这篇文章就把我实际调试这个项目时用到的寄存器配置、时钟参数计算、DMA配合逻辑以及最后排错的心得完整写出来希望对想脱离库函数做底层开发的朋友能有点参考价值。1. 项目背景与寄存器方案的实际意义1.1 为什么选寄存器版本先说个最直接的原因代码小、可控性强。用HAL库生成一个ADC采集工程编译出来通常是几十KB起步而寄存器版本加上启动文件往往不到10KB在一些对Flash容量压得很紧的产品上这个差距很实在。更重要的原因是排查问题的效率。HAL库把外设的状态机封装在结构体和函数内部真出了问题你不光要看寄存器手册还要去翻库函数源码看它到底做了哪几步、有没有在某个条件分支里把位给清掉。寄存器版本则没有这层遮蔽配置了什么、没配置什么一清二楚。调试时打开仿真器看外设寄存器直接就能定位问题是出在时钟、采样时间、DMA方向还是触发方式上。另外一个点可能很多人没意识到寄存器版本对“理解芯片工作方式”的帮助非常大。F407的ADC有规则组、注入组、扫描模式、连续模式、外部触发、DMA请求等一堆概念HAL库里一个Start_DMA函数就把它们全包装了。搞清楚这些概念之间的关联后换到任何一款带ADC的单片机上都能快速上手因为底层原理是通的。1.2 我的硬件准备与接线我用的是一块很常见的STM32F407VGT6开发板ADC部分选择了ADC1的通道1也就是PA1引脚。这个引脚本身没有特殊复用冲突用作模拟输入非常合适电位器接法如下电位器两端分别接3.3V和GND滑动端接PA1模块上再并一个100nF电容到地用于滤除采集点的高频噪声实际项目里如果采集的是传感器输出尽量先看清传感器输出阻抗。如果源阻抗太高采样瞬间内部采样电容充电不够充分得到的数值会有明显跳动。后面我会专门讲这个问题。2. 时钟树与ADC采样率之间的关系2.1 ADC时钟从哪里来STM32F407的ADC外设挂在APB2总线上但它的工作时钟并不是直接使用APB2时钟而是先经过一个专门的分频器再由这个分频器输出给ADC内核。这个分频器位于RCC-CFGR寄存器的高位称为ADCPRE位段可选的分频系数为2、4、6、8。很多开发板默认系统时钟是168MHzAPB2总线频率为84MHz。如果直接把这个84MHz送给ADC内核就超出了数据手册规定的36MHz上限转换结果是不可靠的。所以我这里将ADCPRE配置为4分频得到21MHz的ADC时钟。RCC-CFGR ~RCC_CFGR_ADCPRE; RCC-CFGR | RCC_CFGR_ADCPRE_DIV4;这里有一个容易忽略的地方分频后的ADC时钟才是后续所有时间计算的基础而一定要记得ADC时钟不允许超过36MHz。开发板上如果有其他外设重配了RCC-CFGR务必检查自己的配置是否被后续代码覆盖。2.2 采样时间决定了转换周期和速率ADC一次规则转换的总时间由两部分组成采样阶段时长和逐次逼近阶段时长。F407的逐次逼近阶段固定为12个ADC时钟周期采样阶段则由SMP位决定。我在这份工程里把通道1的采样周期配置为28个ADC时钟ADC1-SMPR2 ~(7UL 3); ADC1-SMPR2 | (2UL 3);在21MHz ADC时钟下单个时钟周期约47.6ns。一次转换所需时间就是采样周期总周期数(SMP12)单次转换时间对应采样率3150.71us1.40Msps15271.29us778ksps28401.90us525ksps56683.24us309ksps84964.57us219ksps1121245.90us169ksps1441567.43us135ksps48049223.43us42.7ksps选择采样时间时要考虑两件事。第一外部信号源阻抗高不高如果传感器输出阻抗大应当选长一点采样时间。第二需要的采样率够不够如果做音频采样或高速信号抓取就得在精度和速度之间找平衡。日常电位器采集、温度传感器这类慢速信号用28或56周期完全够用还附带滤波效果。3. ADC与DMA的寄存器配置细节3.1 GPIO时钟与模拟输入模式使用PA1前先打开GPIOA的AHB1时钟再把引脚配置为模拟模式。模拟模式下引脚的数字输入和输出缓冲器被禁用外部信号可以直达ADC采样开关内部这是所有ADC引脚必须做的第一步。RCC-AHB1ENR | RCC_AHB1ENR_GPIOAEN; GPIOA-MODER ~(3UL 2); GPIOA-MODER | (2UL 2); // PA1 模拟模式 GPIOA-PUPDR ~(3UL 2); // 禁止上拉下拉很多朋友直接用HAL库久了以为只要调用HAL_ADC_Init就行会忽视GPIO模拟模式的重要性。之前我见过一个现象引脚没设置成模拟模式ADC读到的值一直固定在某个值附近不变化因为GPIO内部的施密特触发器还在工作信号被数字电路绑定住了。3.2 ADC外设时钟与校准顺序ADC1挂载在APB2总线上所以需要单独打开APB2外设时钟RCC-APB2ENR | RCC_APB2ENR_ADC1EN;接下来是校准操作。很多人会跳过这一步但这很容易让ADC转换结果带上固定偏差。F407的校准流程要求ADC处于关闭状态也就是ADON位为0时把CR2寄存器的CAL位置1。硬件会在校准完成后自动清除CAL位此时校准系数已经写入ADC内部。ADC1-CR2 0; // 确保ADC处于关闭状态 ADC1-CR2 | ADC_CR2_CAL; while (ADC1-CR2 ADC_CR2_CAL); // 等待校准完成校准代码放在ADC使能之前执行这个顺序是个硬性约束。如果ADC已经上电直接置CAL会得到错误结果。我的惯例做法初始化函数开头先把CR2清零再执行校准这样即使重复调用初始化函数也不会出问题。3.3 规则组序列寄存器设置F407的ADC转换序列放在SQR寄存器组里。规则组最多可以排16个转换通道SQR1的L位保存的是“序列长度减1”。单通道场景下序列长度就是1所以L填写0。转换序列中的第一个转换放在SQR3寄存器的SQ1位置。这里我要做的是通道1就直接把SQR3的低5位写成1ADC1-SQR1 ~(15UL 20); // L 0表示1次转换 ADC1-SQR3 1; // SQ1 ADC1_IN1如果不写SQR3复位后它默认是0规则序列就会去转换通道0到时候读到的数值和接线对不上容易白忙一场。这个位置也顺带解释了“单通道”和“扫描模式”的关系。当前序列只有1个转换即使把CR1的SCAN位置1实际行为和关闭扫描没有区别。真正用到多通道时扫描模式必须打开DMA会把每个通道结果按序列顺序连续写到内存里。3.4 DMA流与通道选择STM32F407的每个外设DMA请求都对应特定的流和通道组合。ADC1的DMA请求可以连接到DMA2的流0或流4通道均为通道0。工程里我使用DMA2流0通道0。打开DMA2时钟后先配置外设地址、内存地址和传输数量再写控制寄存器RCC-AHB1ENR | RCC_AHB1ENR_DMA2EN; DMA2_Stream0-CR 0; while (DMA2_Stream0-CR DMA_SxCR_EN); // 确保流处于禁用状态 DMA2_Stream0-PAR (uint32_t)ADC1-DR; // 外设地址ADC1数据寄存器 DMA2_Stream0-M0AR (uint32_t)adc_samples; // 内存地址数组 DMA2_Stream0-NDTR 8; // 传输8次后循环 DMA2_Stream0-CR | DMA_SxCR_CL...控制寄存器里的关键位说明DIR方向位设为00表示外设到内存也就是DMA外设读取ADC数据寄存器写入内存数组PSIZE和MSIZE设为16位因为ADC1_DR的有效数据是右对齐的16位MINC置1内存地址每传输一次递增一次这样才能连续填充数组PINC不能置1ADC数据寄存器是单地址外设地址不需要递增CIRC置1用循环模式配合ADC的连续转换PL位设置为高优先级避免在密集DMA环境下队列延迟影响采样通道选择位CHSEL要填0对应DMA2的通道0DMA2_Stream0-CR | (0UL 25); // CHSEL 000不同型号芯片CHSEL位宽度可能不同最稳妥的办法是直接用以下方式写数值然后对照参考手册逐位核对。我在调试时经常打开外设寄存器窗口把DMA_SxCR实际值和期望值比对。最后使能DMA流DMA2_Stream0-CR | DMA_SxCR_EN;3.5 ADC的DMA请求、连续转换与软件触发ADC侧还有三个控制位必须理解清楚它们直接影响DMA的运行状态。第一个就是CR2的DMA位。它只是许可ADC产生DMA请求不打开它ADC即便转换完成也不会把结果推向DMA。第二个是DDS位。这个位的含义很多人在初次配置时都会忽略它决定DMA请求是一锤子买卖还是持续有效。当DMA位为1且DDS为1时规则通道转换结束后会持续产生DMA请求配合DMA循环模式可以实现无需CPU介入的连续采样。如果DDS保持0DMA完成设定数量的传输后请求就被切断即使MCU和DMA流都还在循环模式数据流也会中止。第三个是CONT位也就是连续转换模式。置1后一次规则通道序列转换完成后立即启动下一轮不需要再次软件触发。连续转换和DMA循环搭配是最常用的组合。ADC1-CR2 | ADC_CR2_DMA | ADC_CR2_DDS | ADC_CR2_CONT; ADC1-CR2 | ADC_CR2_ADON;最后是启动转换。当EXTEN保持0、外部触发功能关闭时把CR2的SWSTART位置1即可。连续模式下只需要启动这一次后面ADC就会自动循环运行ADC1-CR2 | ADC_CR2_SWSTART;这里有经验的点就是SWSTART必须在ADON置位后设置。如果写入顺序反了或者把ADON和SWSTART放在同一条语句里有些芯片配置序列会产生不可预测的启动失败。养成先ADON再SWSTART的习惯能少踩很多坑。4. 可以直接上板的寄存器工程代码4.1 完整配置流程把上面所有配置整合成一个初始化函数我在工程里大致是这样写的#include stm32f4xx.h static volatile uint16_t adc_samples[8]; static void ADC1_DMA_Init(void) { /* 1. 开启外设时钟 */ RCC-AHB1ENR | RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_DMA2EN; RCC-APB2ENR | RCC_APB2ENR_ADC1EN; /* 2. PA1 配置为模拟输入 */ GPIOA-MODER ~(3UL 2); GPIOA-MODER | (2UL 2); GPIOA-PUPDR ~(3UL 2); /* 3. ADC时钟分频PCLK2 / 4 21MHz */ RCC-CFGR ~RCC_CFGR_ADCPRE; RCC-CFGR | RCC_CFGR_ADCPRE_DIV4; /* 4. ADC关闭状态校准 */ ADC1-CR2 0; ADC1-CR1 0; ADC1-CR2 | ADC_CR2_CAL; while (ADC1-CR2 ADC_CR2_CAL); /* 5. 规则序列单通道通道1 */ ADC1-SQR1 ~(15UL 20); ADC1-SQR3 1; /* 6. 通道1采样时间28周期 */ ADC1-SMPR2 ~(7UL 3); ADC1-SMPR2 | (2UL 3); /* 7. DMA2流0通道0 */ DMA2_Stream0-CR 0; while (DMA2_Stream0-CR DMA_SxCR_EN); DMA2_Stream0-PAR (uint32_t)ADC1-DR; DMA2_Stream0-M0AR (uint32_t)adc_samples; DMA2_Stream0-NDTR 8; DMA2_Stream0-CR | DMA_SxCR_MSIZE_0; // 内存16位 DMA2_Stream0-CR | DMA_SxCR_PSIZE_0; // 外设16位 DMA2_Stream0-CR | DMA_SxCR_MINC; // 内存地址递增 DMA2_Stream0-CR | DMA_SxCR_CIRC; // 循环模式 DMA2_Stream0-CR | (2UL 16); // 优先级高 DMA2_Stream0-CR | DMA_SxCR_EN; /* 8. 配置ADC连续模式与DMA请求 */ ADC1-CR2 | ADC_CR2_DMA | ADC_CR2_DDS | ADC_CR2_CONT; /* 9. 上电并通过软件触发启动 */ ADC1-CR2 | ADC_CR2_ADON; ADC1-CR2 | ADC_CR2_SWSTART; }需要提一下DMA_SxCR的CHSEL字段默认是0所以我没有显式写通道选择位。如果工程里曾复用DMA2的其他流记得要重置CR后再配置。4.2 只要最新值不要环形缓冲上面的例子使用了8个16位元素的数组DMA每转换一次就往数组的下一个位置写写满8个后回到起点。这在需要观察一段波形或做简易滑动平均时很方便但如果你想在应用里直接拿“最新一次转换结果”环形缓冲会让主循环分不清哪个下标才是最新的。解决思路很简单把传输次数改成1同时关闭内存地址递增。这样DMA从ADC数据寄存器搬运结果后永远写入同一个内存地址也就是一个全局变量读取时永远不会拿到历史数据。static volatile uint16_t adc_latest; DMA2_Stream0-NDTR 1; DMA2_Stream0-M0AR (uint32_t)adc_latest; DMA2_Stream0-CR | DMA_SxCR_MSIZE_0 | DMA_SxCR_PSIZE_0; /* 不置MINC位 */实际使用中连续模式加循环DMA加NDTR1的配置比我最初想的要稳定得多。每次ADC转换完成后新数值直接覆盖旧数值主循环什么时候读取都是最新结果。前提是CONFIRC因为仅在非环形模式下样本指针会随时间漂移。4.3 主循环中如何读数据如果使用的是adc_samples[8]这种环形方式简单做法是把数组的任意一个位置作为输出电压近似值。严谨一点可以在DMA传输完成中断里处理数据但单通道连续采样没必要把简单问题复杂化。如果使用的是adc_latest读取更直接uint16_t val adc_latest; float voltage (float)val * 3.3f / 4095.0f;主循环里读这个变量不会因为DMA写入而崩溃因为ADC数据寄存器是16位写入操作是原子完成的。不过不建议在主循环里频繁对这个变量做大量计算连续模式下样本速率可以达到每秒几十万个计算跟不上反而造成逻辑混乱。5. 常见故障与现场排查心得5.1 读到的数据一直为0这种情况我遇到十次有八次是GPIO模式没配置成模拟或者配置成了复用模式。先查GPIOA-MODER确认PA1两位确实是二进制10。其次查ADC1的SQR3寄存器确认SQ1段里写的是1而不是0。如果SQR3还是复位值配置的通道就是ADC1的通道0而实际接线接在通道1上读自然为0。另外一个看起来隐蔽、实际很常见的原因是PA1引脚没连接好。电位器滑动端悬空时ADC输入相当于浮空读到的往往接近0或随机跳动。用万用表量一下PA1的实际电压最直接。5.2 数值波动很大或者明显偏低如果读数不是稳定的0而是大幅度跳变先往电源方向查。开发板USB供电的3.3V噪声通常比较大ADC的参考电压VREF可能和实际3.3V不完全一致。解决方法是采集点加一个100nF电容到地必要时并联一个10k电阻做下拉。如果信号源阻抗很高采样时间也尽量拉长。比如传感器输出经过一个几十千欧的分压电阻网络28周期采样时间可能不够把SMPR2改成480周期再看波形通常能明显改善。5.3 DMA传输几次就停住DMA停止不动第一嫌疑就是DDS位没置1。只配置了DMA位没有配置DDS位ADC虽然在连续转换但DMA请求并不会持续给出。这个问题在HAL库里被封装得很隐蔽寄存器版本却直接暴露在CR2寄存器的第9位上。第二怀疑对象是DMA流被异常终止。打开DMA2流0的中断标志寄存器看看TEIF或者FEIF有没有被置位。如果传输出现错误流会停在第0号再次使能前要先清标志。5.4 溢出标志OVR如何处理连续转换模式下如果DMA请求没有被及时响应ADC1的数据寄存器就会在下一轮转换完成前被覆盖OVR标志被硬件置起。多数情况下OVR事件本身不会让ADC停摆但处理不当会让主循环拿到的样本时间乱序。我的处理策略是利用ADC1的SR寄存器OVR位做一个全局错误计数一旦出现就说明DMA带宽可能不够要么降低采样时间要么提高DMA优先级。若需要完全清掉OVR请参照参考手册建议的流程先置DMA位为0再清OVR最后重新使能DMA位。5.5 从单通道扩展到多个通道这个工程本身是单通道但寄存器思路可以平滑扩展修改SQR1的L位为通道数减1再在SQR3、SQR2甚至SQR1里分别填写所需通道编号同时把NDTR改成通道数量DMA内存数组大小也要对应增加。扩展后要注意三个点第一所有通道尽量用相同采样时间否则连续模式下不同通道的转换周期不同步第二扫描模式必须使能否则只转第一个通道第三DMA输出的数据顺序就是SQR里排列的顺序读取数组时要按这个顺序对应实际信号这是一个非常容易搞错的地方。6. 调试过程中的经验与建议寄存器版本调试ADC-DMA的过程本质上是一场x读外设寄存器状态的分析。我最常做的是在仿真器里同时打开ADC_SR、ADC_CR2、DMA_SxCR和内存窗口观察每次软件触发后各个寄存器之间的变化时序。一个很实用的小技巧先把DMA和连续模式全部关掉每次手动把SWSTART位置1然后在仿真器里查看ADC1-DR的数值。这样可以把ADC本身和DMA传输解耦快速定位问题出在模拟链路还是数据搬运环节。我每次接手新的ADC项目都会先用这个方法验一遍硬件通路。另一个经验是关于代码组织的。寄存器工程不写注释的话几天后再看很容易忘掉某个位为什么这么配。我都会在每个配置块前写上对应的参考手册章节后续维护时一句“见手册第X章”能省下大量翻手册时间。最后建议手头常备数据手册和参考手册的PDF重点看ADC和DMA两章。遇到位名对不上、宏定义含义模糊的情况不要猜直接看寄存器位定义表。F407这款芯片的寄存器文档写得非常直白尤其DMA的流映射表看一眼就知道ADC1该接哪个流哪个通道比盲目调库函数靠谱得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询