
1. 为什么多通道ADC轮询在H7上值得单独聊一聊STM32H7这颗芯片的ADC模块跟F1、F4时代完全不是一个量级的东西。很多人从F103转过来第一反应是ADC不还是那个ADC吗配置一下通道、开个转换、读DR寄存器就完事了。结果一上手H7就发现CubeMX里ADC的配置页面多了一堆没见过的选项什么Conversion Mode、Oversampling、Offset、Regular Sequence的Rank排列还有那个让人头大的Clock Prescaler。更坑的是H7的ADC时钟源默认走的是per_ck或者pll2_p跟系统时钟不是一条线配错了要么转换结果全是0要么采样率跟预期差一大截。这篇内容要解决的就是一个非常具体的场景用STM32H7的ADC1在轮询模式下依次采集多个通道的模拟量不依赖DMA不依赖中断纯靠查询EOC标志位来读取数据。你可能会问都2025年了为什么还要用轮询答案很实际——当你只需要采集三四个通道、采样率要求不高比如几十到几百Hz、而且系统里已经有其他中断在跑的时候轮询反而是最省心、最不容易出幺蛾子的方案。DMA配置错了会静默丢数据中断优先级配不好会互相打架轮询虽然笨但它透明、可控、调试起来一目了然。适合读这篇的人手上有H7的开发板不管是Nucleo-H723、H743还是自己画的板子需要在项目里采集几路电压、电流或者传感器信号对实时性要求不算苛刻但希望代码干净、逻辑清晰、不引入额外复杂度。如果你正在做工业数据采集、设备状态监测、或者简单的多路模拟量巡检这套方案可以直接拿去用。我下面会从CubeMX的时钟树配置开始一步步拆到代码层面的轮询逻辑中间会重点讲几个H7特有的坑比如ADC时钟分频的计算、采样时间的选取、以及多通道扫描模式下EOC和EOS的区别。这些细节在官方手册里都有但散落在不同章节实际配的时候很容易漏。2. CubeMX里ADC时钟树的配置逻辑与常见误判2.1 H7的ADC时钟来源跟F4完全不是一回事在F4时代ADC时钟直接挂在APB2上分频系数就是简单的2/4/6/8。到了H7ADC有自己的独立时钟域时钟源可以选per_ck、pll2_p、pll3_r或者hse/2之类。CubeMX的Clock Configuration页面里ADC的时钟输入是一个单独的mux选择器你得先确定用哪个源再看分频。我一般推荐用pll2_p作为ADC时钟源原因是pll2通常用来给其他外设提供时钟频率比较灵活而且可以独立于系统时钟调整。假设你的H7主频跑在480MHzpll2_p设成120MHz那么ADC的输入时钟就是120MHz。接下来关键的一步是设置ADC的PrescalerH7的ADC时钟分频寄存器叫ADC_CLK_PRESC分频系数可以是1/2/4/6/8/10/12/16/32/64/128/256。注意这个分频是两级的先经过一个固定的/2或者/4取决于ADC_CLK_PRESC的配置再经过一个可编程的分频。实际计算的时候H7的ADC最大时钟频率是50MHz不同型号可能略有差异查你手上的数据手册确认。如果你pll2_p给了120MHz那分频系数至少要选4得到30MHz留有余量。选2的话就是60MHz超频了转换结果会不稳定。我见过有人图省事直接选1结果采出来的数据跳得跟心电图一样查了半天以为是硬件滤波没做好其实是ADC时钟超了。提示CubeMX在Clock Configuration页面会实时显示ADC的最终时钟频率如果超过芯片手册标称的最大值它会标红。但有时候标红了也能生成代码别忽略这个警告。2.2 采样时间不是随便选的跟信号源阻抗直接相关H7的ADC每个通道可以独立配置采样时间范围从1.5个ADC时钟周期到810.5个周期。采样时间越长采样电容充电越充分结果越准但单次转换耗时也越长。很多人直接选个默认的然后发现高阻抗信号源采出来偏差很大。这里有个经验公式采样时间秒≥ (信号源阻抗 内部采样开关阻抗) × 采样电容 × ln(2^N)其中N是ADC分辨率。H7的采样电容大概是4pF左右内部开关阻抗跟采样时间设置有关。如果你采的是一个经过运放缓冲的信号源阻抗很低几十欧姆那1.5个周期就够了。但如果你直接采一个分压电阻网络源阻抗可能到几十kΩ那就得把采样时间拉到最大。我一般会在CubeMX里把采样时间设成64.5 cycles或者387.5 cycles作为起步前者适合大多数低阻抗场景后者适合高阻抗或者需要高精度的场合。具体选哪个后面在代码里可以动态改但CubeMX里先给个保守值。2.3 扫描模式与连续转换模式的组合逻辑H7的ADC支持扫描模式Scan Mode开启后会自动按Rank顺序依次转换多个通道。轮询模式下你需要关注两个标志位EOCEnd of Conversion和EOSEnd of Sequence。EOC在每个通道转换完成后置位EOS在整个序列完成后置位。如果你开了扫描模式但只查EOC那每次读DR寄存器的时候数据可能是当前通道的也可能是上一个通道的取决于你读的时机。正确的做法是在扫描模式下每个通道转换完成后EOC置位你读DR的同时EOC自动清除然后ADC自动开始下一个通道的转换。当所有通道转换完EOS置位。所以轮询的逻辑应该是等EOC读数据存到对应通道的数组里循环直到EOS置位。CubeMX里还有一个选项叫Continuous Conversion Mode开了之后ADC会在序列结束后自动重新开始不需要手动触发。轮询模式下我建议不开这个因为你需要精确控制每次采集的时机开了之后ADC一直在跑你读数据的速度跟不上就会覆盖。3. 从CubeMX生成代码到第一个可运行的轮询采集3.1 引脚与通道的映射关系确认在CubeMX的Pinout视图里找到你要用的ADC通道对应的引脚。比如ADC1_IN3对应PA3ADC1_IN4对应PA4等等。注意H7的ADC通道跟引脚的对应关系跟F4不一样别照搬旧项目的配置。你可以在CubeMX里直接点引脚选择ADCx_INy它会自动把对应的通道加到Regular Sequence里。这里有个细节H7的ADC1和ADC2可以工作在双重模式Dual Mode但轮询模式下我建议只用单个ADC别开双重否则EOC标志位的逻辑会变复杂。如果你确实需要更高的采样率那应该考虑DMA或者双重交替模式那是另一个话题了。3.2 Regular Sequence的Rank排列在ADC配置页面的Regular Conversion Mode里你会看到Number of Conversions和Rank列表。假设你要采3个通道IN3、IN4、IN5。Number of Conversions设成3然后Rank1选IN3Rank2选IN4Rank3选IN5。采样时间可以每个通道单独设也可以在Common Settings里统一设。注意Rank的顺序决定了转换的顺序不是按通道号自动排的。你可以把采样时间长的通道放在后面这样前面的通道转换完之后ADC有更多时间准备。3.3 生成代码后的初始化检查CubeMX生成代码后先别急着写业务逻辑打开adc.c文件检查几个关键点hadc1.Init.ClockPrescaler是否跟你计算的一致hadc1.Init.ScanConvMode是否使能hadc1.Init.ContinuousConvMode是否禁用hadc1.Init.DiscontinuousConvMode是否禁用轮询模式下不需要hadc1.Init.NbrOfConversion是否等于你的通道数hadc1.Init.EOCSelection是设成EOC还是EOS最后一项特别关键。如果你设成EOC那每个通道转换完都会置位EOC你需要自己计数。如果设成EOS那只有整个序列完成才置位。轮询模式下我建议设成EOC然后手动判断是否所有通道都读完了。4. 轮询采集的核心代码结构与实测时序分析4.1 启动转换与等待EOC的标准流程H7的ADC启动转换有三种方式软件触发、硬件触发、或者连续模式自动触发。轮询模式下我们用软件触发调用HAL_ADC_Start()然后循环等待EOC标志。HAL_ADC_Start(hadc1); for (int i 0; i ADC_CHANNEL_COUNT; i) { // 等待当前通道转换完成 while (!__HAL_ADC_GET_FLAG(hadc1, ADC_FLAG_EOC)) { // 可以加超时计数防止死循环 } // 读取转换结果 adc_values[i] HAL_ADC_GetValue(hadc1); } // 等待整个序列完成 while (!__HAL_ADC_GET_FLAG(hadc1, ADC_FLAG_EOS)) { } HAL_ADC_Stop(hadc1);这段代码看起来简单但有几个坑第一HAL_ADC_GetValue()内部会读DR寄存器读完之后EOC自动清除。但如果你用的是LL库或者直接操作寄存器要确认清除机制。第二EOC标志在扫描模式下每个通道都会置位所以循环次数必须等于通道数。如果你只循环了2次但配了3个通道那第三个通道的数据就丢了而且EOS可能一直不置位。第三超时保护一定要加。如果ADC时钟没配好或者通道配置有误EOC可能永远不置位没有超时的话程序就卡死了。4.2 实测时序一次完整采集到底花多长时间我用手上的H743 Nucleo板实测过ADC时钟设成30MHz采样时间64.5 cycles3个通道。单次转换的时间计算如下采样阶段64.5 cycles / 30MHz 2.15μs转换阶段H7的ADC是逐次逼近型12位分辨率需要大概12.5个ADC时钟周期约0.42μs单通道总时间约2.57μs3个通道约7.71μs加上软件开销循环判断、函数调用实际一次完整采集大概在10-12μs左右。也就是说轮询模式下你最高能做到80kHz左右的采样率3通道合计。当然这是理论值实际跑的时候如果系统里有其他中断时间会被拉长。如果你需要更高的采样率那就得考虑DMA或者双重模式了。但80kHz对于大多数工业数据采集场景已经绰绰有余。4.3 数据对齐与均值滤波的处理H7的ADC支持12位、10位、8位、6位分辨率默认是12位。读出来的数据是右对齐的范围0-4095。如果你开了过采样Oversampling数据位数会变右对齐的偏移量也不同。我一般会在读完之后做一个简单的滑动平均滤波窗口大小取8或者16。注意不要在轮询循环里做浮点运算H7虽然有FPU但浮点运算会拉长循环时间影响采样时序。用整数移位来实现除法// 假设adc_values是uint16_t数组filtered是滤波后的值 filtered[i] (filtered[i] * 7 adc_values[i]) / 8; // 等价于右移3位或者用累加和的方式每8次采集求一次平均这样计算量更小。5. 那些CubeMX不会告诉你的H7 ADC轮询坑5.1 ADC上电后的稳定时间被严重低估H7的ADC在从掉电模式唤醒或者刚上电的时候需要一段稳定时间。官方手册里写的是至少1μs但实际测试下来如果你在HAL_ADC_Init()之后立刻开始转换前几次的结果会明显偏低。我一般会在初始化之后加一个HAL_Delay(1)或者先做几次丢弃采集。// 初始化后先丢弃前3次采集 for (int i 0; i 3; i) { HAL_ADC_Start(hadc1); while (!__HAL_ADC_GET_FLAG(hadc1, ADC_FLAG_EOC)); HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); }这个坑在F4上不明显因为F4的ADC稳定时间短。H7的模拟前端更复杂稳定时间更长忽略这一点会导致低值信号采不准。5.2 通道切换时的串扰问题H7的ADC内部有多路复用器切换通道的时候如果前一个通道的电压跟当前通道差异很大采样电容上残留的电荷会影响当前通道的读数。表现就是采完一个3.3V的通道立刻切到一个0.1V的通道读出来的值会偏高。解决办法有两个一是增加采样时间让采样电容有足够时间充放电二是在通道之间插入一个空转换或者把采样时间短的通道放在电压变化大的通道后面。我在实际项目里一般会把采样时间统一设成387.5 cycles虽然慢一点但串扰基本消失。5.3 温度对ADC精度的影响不可忽略H7的ADC内部有一个温度传感器通道ADC_CHANNEL_TEMPSENSOR但那个是测芯片温度的不是环境温度。如果你用ADC采外部传感器要注意H7的ADC参考电压VREF会随温度漂移。H7的VREF可以外部提供也可以用内部的VREFBUF。内部VREFBUF的精度大概是±0.1%温度漂移在几十ppm/°C量级。如果你的应用对精度要求高建议外部加一个高精度基准源比如REF3033之类的。5.4 轮询模式下的中断干扰如果你的系统里有其他中断比如串口接收、定时器它们会在轮询循环执行期间打断ADC的等待。这本身不是问题但如果中断服务程序执行时间过长会导致EOC标志被延迟读取极端情况下下一个通道的转换已经完成EOC被覆盖你就丢了一个通道的数据。解决办法要么把ADC轮询放在高优先级的中断里要么在轮询期间关闭全局中断不推荐会影响系统实时性。我一般会评估中断的执行时间如果都在几微秒以内那影响可以忽略。6. 从轮询到DMA的平滑过渡思路轮询方案跑通之后如果你发现采样率不够、或者CPU占用太高下一步自然是上DMA。H7的DMA跟F4不一样它有一个DMAMUXDMA请求复用器可以把ADC的请求映射到任意DMA流上。CubeMX里配置DMA的时候注意选择DMA1或DMA2然后设置Circular模式或者Normal模式。从轮询切到DMA代码改动其实不大把HAL_ADC_Start()换成HAL_ADC_Start_DMA()传入缓冲区指针和长度然后在DMA完成回调里处理数据。但要注意DMA模式下EOC标志位的处理方式不同HAL库会自动处理你不需要手动查标志。我一般会保留轮询模式的代码作为一个调试模式在系统启动时先跑一遍轮询确认ADC硬件没问题再切到DMA。这样排查问题的时候可以快速定位是硬件问题还是DMA配置问题。7. 几个实际项目中的参数配置参考下面这张表是我在几个不同项目里用过的ADC配置可以直接参考项目类型ADC时钟采样时间通道数分辨率过采样实测采样率低速传感器巡检20MHz387.5 cycles412位关闭约15kHz电机电流采样40MHz64.5 cycles212位4x约50kHz电池电压监测10MHz810.5 cycles612位关闭约3kHz音频信号采集50MHz1.5 cycles112位关闭约200kHz注意最后一行的音频采集单通道、最短采样时间、最高ADC时钟轮询模式下能做到200kHz左右。但这时候CPU基本被占满了实际项目里还是建议上DMA。提示过采样Oversampling开启后ADC会自动做多次转换并求平均结果位数会增加。比如12位4x过采样结果变成14位。这时候读出来的数据范围是0-16383不是0-4095代码里的换算系数要相应调整。8. 调试阶段快速验证ADC是否正常工作的技巧刚配好ADC的时候怎么快速判断它是不是在正常工作我的做法是先把一个通道接到GND读出来应该是0附近再接到VREF读出来应该是4095附近。如果GND读出来是几百那说明有偏移或者串扰如果VREF读出来不到4095那可能是参考电压不对或者采样时间不够。还有一个更直观的方法用DAC输出一个已知电压接到ADC输入上看读出来的值跟理论值差多少。H7的DAC和ADC可以同时用DAC输出1.65VVREF/2ADC读出来应该在2048左右。偏差在±10以内算正常超过±50就要查配置了。如果手头没有DAC也可以用PWM加RC滤波产生一个可调的模拟电压虽然精度不高但用来验证ADC的通道切换和基本功能足够了。9. 个人在实际项目中的几点体会轮询模式最大的优势是确定性。你知道每一次采集什么时候开始、什么时候结束、数据存在哪里。DMA虽然效率高但一旦配置有误数据丢了你可能半天都发现不了。我在一个工业巡检项目里最开始用DMA采8个通道跑了几天才发现第7通道的数据一直是0查了半天是DMA缓冲区长度算错了。后来改成轮询虽然CPU占用高了点但数据从来没丢过。另一个体会是H7的ADC对电源质量很敏感。如果你用的开发板是USB供电ADC的噪声会明显比外部稳压电源大。我在采一个毫伏级信号的时候USB供电下读出来的数据跳变有十几个LSB换成电池供电后就降到两三个LSB。所以如果你的应用对精度要求高电源部分一定要处理好VDDA和VREF的滤波电容不能省。最后CubeMX生成的ADC初始化代码里HAL_ADCEx_Calibration_Start()这个函数一定要调用。H7的ADC出厂校准值存在OTP区域不调用校准的话精度会差很多。这个函数在HAL_ADC_Init()之后、HAL_ADC_Start()之前调用只需要调一次。我见过有人忘了调结果采出来的数据整体偏了5%左右查了好久才发现是校准没做。