尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32H7 ADC多通道轮询采集实战:CubeMX配置与代码实现

STM32H7 ADC多通道轮询采集实战:CubeMX配置与代码实现 1. 项目缘起与整体设计思路STM32H7 这颗芯片在圈子里有个外号叫“性能怪兽”480MHz 的 Cortex-M7 内核加上双精度浮点单元跑起来确实带劲。但很多人拿到 H7 之后第一件想做的事往往不是跑什么复杂算法而是先把最基础的 ADC 多通道采集跑通。原因很简单ADC 是连接模拟世界和数字世界的桥梁电压、电流、温度、压力这些物理量最终都要变成数字才能被处理。而多通道轮询模式又是所有 ADC 采集方式里最直观、最容易理解的一种。我这次做的项目需求本身不复杂用 STM32H7 采集 4 路模拟信号分别是两路电压检测、一路 NTC 热敏电阻温度采样、一路外部传感器输出。采样率要求不高每路 1kHz 左右精度要求 12 位以上数据通过串口打印出来做实时监控。这种场景下用 DMA 或者中断当然可以但轮询模式反而更合适——代码逻辑简单调试方便出了问题一眼就能看出是哪一步卡住了。轮询模式的核心思想很朴素CPU 主动去问 ADC“转换完了没有”转换完了就读数据没完就继续等。这种方式在低采样率、少通道的场景下完全够用而且不需要配置 DMA 通道、不需要处理中断优先级省去了很多容易出错的环节。当然它的缺点也很明显CPU 在等待期间干不了别的事采样率越高CPU 被占用的时间就越长。但对于 1kHz 以下的采样需求这点开销完全可以接受。选择 CubeMX 来做初始化配置是因为 H7 的寄存器数量比 F1、F4 系列多了不少ADC 的时钟树也复杂得多。手动写初始化代码不是不行但很容易在时钟分频、采样时间这些细节上翻车。CubeMX 能把这些参数可视化生成的基础代码也足够规范省下来的时间可以花在业务逻辑上。不过 CubeMX 生成的代码只是起点真正要跑通多通道轮询还需要在生成的框架上做不少补充和调整。这个项目的目标读者是那些已经用过 STM32F1 或 F4 系列、想迁移到 H7 平台的开发者或者是对 ADC 多通道采集还不太熟悉、想找一个完整参考案例的初学者。我会从 CubeMX 的配置开始一步步讲到代码实现、数据验证和常见问题排查尽量把每个参数为什么这么设、每个步骤为什么这么做都讲清楚。2. CubeMX 配置全流程拆解2.1 时钟树配置与 ADC 时钟源选择打开 CubeMX 之后第一件事是选芯片型号。我用的具体型号是 STM32H743VIT6LQFP100 封装Flash 2MBRAM 1MB。选好之后先别急着配外设把时钟树理清楚再说。H7 的时钟树比 F4 复杂不少ADC 的时钟来源有好几个选项选错了后面采样时间怎么调都不对。H7 的 ADC 时钟可以来自per_ck、pll2_p、pll3_r或者hclk经过分频后的adc_ker_ck。我一般推荐用pll2_p因为 PLL2 可以单独配置不影响系统主频。具体配置是HSE 用 25MHz 晶振PLL2 的 N 分频设 8P 分频设 2这样pll2_p的输出就是 25MHz × 8 / 2 100MHz。然后 ADC 的 kernel clock 选择pll2_p再经过一个可编程分频器最终 ADC 的工作时钟不要超过 50MHz。我这里设的是 100MHz 再 2 分频得到 50MHz。为什么这么在意 ADC 时钟因为 H7 的 ADC 是逐次逼近型每次转换需要固定的时钟周期数。采样时间设得越长转换结果越稳定但总转换时间也越长。如果 ADC 时钟太快采样电容可能还没充到目标电压转换就开始了结果就会偏低。50MHz 是一个比较稳妥的值既能保证转换速度又不会因为太快而牺牲精度。注意H7 的 ADC 时钟源在 CubeMX 里叫ADC的Clock Mux不同版本的 CubeMX 界面可能略有差异但核心逻辑是一样的——先选源再分频最后确认不超过手册规定的最大值。2.2 ADC 多通道参数配置细节时钟配好之后在Analog菜单里找到ADC1把IN0到IN3这四个通道都勾上。这里有个细节H7 的 ADC 通道和引脚是固定映射的比如ADC1_IN0对应PA0_CADC1_IN1对应PA1_C。带_C后缀的引脚是 H7 特有的模拟引脚在 CubeMX 里会自动识别不需要手动改。接下来是参数设置。Mode选Independent因为只用了一个 ADC。Clock Prescaler选Synchronous clock mode divided by 4这样 50MHz 的 kernel clock 再 4 分频实际 ADC 工作时钟是 12.5MHz。Resolution选16 bitsH7 的 ADC 支持最高 16 位分辨率但实际有效位数受噪声影响12 到 14 位是比较现实的。Data Alignment选Right alignment这样读出来的数据直接就是转换结果不需要额外移位。Scan Conversion Mode必须使能因为要扫多个通道。Continuous Conversion Mode我建议先关掉用单次转换模式配合轮询逻辑更清晰。Discontinuous Conversion Mode也关掉不需要。Number of Conversion设成 4对应四个通道。External Trigger Conversion Source选Regular Conversion launched by software也就是软件触发轮询模式下每次转换都由代码主动启动。在Rank列表里把四个通道按顺序排好。每个通道的Sampling Time可以单独设置。我一般给电压检测通道设64.5 cycles给 NTC 通道设387.5 cycles因为 NTC 的阻抗比较高需要更长的采样时间让采样电容充分充电。这个参数不是随便设的采样时间太短会导致高阻抗信号源测量不准太长又会影响总转换时间。具体怎么算后面会详细说。2.3 GPIO 与串口配置要点四个 ADC 引脚在 CubeMX 里会自动配置成模拟模式不需要手动改。但要注意H7 的模拟引脚在未使用时最好保持默认状态不要随便配置成其他功能否则可能引入额外噪声。串口我用的USART3波特率 1152008 位数据位无校验1 位停止位。PA10和PA11分别接TX和RX。串口在轮询模式下只用来打印数据不需要开中断配置成默认的Asynchronous模式就行。NVIC里不需要勾选任何中断因为整个项目都是轮询逻辑。提示如果你用的板子上PA10和PA11被其他外设占用了可以换成USART1的PA9和PA10或者USART2的PA2和PA3。CubeMX 里改一下引脚映射就行代码层面只需要改huart的实例名。配置完成后生成代码时记得把Toolchain / IDE选成你实际用的环境我一般用STM32CubeIDE生成的工程可以直接编译下载。Code Generator里勾选Generate peripheral initialization as a pair of .c/.h files这样每个外设的初始化代码会单独放在adc.c、usart.c这些文件里结构更清晰。3. 轮询模式代码实现与核心逻辑3.1 ADC 校准与启动流程CubeMX 生成的MX_ADC1_Init()函数里已经包含了基本的参数配置但 H7 的 ADC 在使用前必须做一次校准否则转换结果会有偏差。校准的代码需要手动加到main()函数里在MX_ADC1_Init()之后、while(1)之前。HAL_ADCEx_Calibration_Start(hadc1, ADC_CALIB_OFFSET, ADC_SINGLE_ENDED);这个函数会启动 ADC 的内部校准流程校准完成后会自动停止。ADC_CALIB_OFFSET表示只校准偏移误差ADC_SINGLE_ENDED表示单端输入模式。如果你的信号是差分输入这里要改成ADC_DIFFERENTIAL_ENDED。校准过程大概需要几百个 ADC 时钟周期调用之后不需要额外等待函数返回时校准已经完成了。校准之后还需要配置 ADC 的通道。CubeMX 生成的代码里hadc1.Init.NbrOfConversion已经设成了 4但具体的通道顺序和采样时间是在MX_ADC1_Init()里通过sConfig结构体配置的。如果你在 CubeMX 里改了通道顺序这里会自动更新不需要手动改。启动 ADC 的函数是HAL_ADC_Start()它会启动 ADC 并等待第一次转换完成。但在轮询模式下我们通常用HAL_ADC_Start()启动之后再用HAL_ADC_PollForConversion()等待每次转换完成。具体流程是先调用HAL_ADC_Start()启动 ADC然后对每个通道调用HAL_ADC_PollForConversion()等待转换结束再用HAL_ADC_GetValue()读取数据。3.2 多通道轮询采集的代码结构轮询模式的核心代码其实就是一个循环但里面有几个容易踩坑的地方。下面是我实际用的代码结构#define ADC_CHANNELS 4 uint16_t adc_values[ADC_CHANNELS]; void ADC_ReadAllChannels(void) { HAL_ADC_Start(hadc1); for (int i 0; i ADC_CHANNELS; i) { if (HAL_ADC_PollForConversion(hadc1, 10) HAL_OK) { adc_values[i] HAL_ADC_GetValue(hadc1); } else { adc_values[i] 0xFFFF; // 标记错误 } } HAL_ADC_Stop(hadc1); }这段代码看起来简单但有几个关键点需要解释。第一HAL_ADC_Start()只需要调用一次它会启动整个扫描序列。第二HAL_ADC_PollForConversion()的超时参数我设的是 10 毫秒这个值不能太小否则在采样时间较长时可能还没转换完就超时了。第三每次HAL_ADC_GetValue()读取的是当前通道的转换结果读取之后 ADC 会自动切换到下一个通道所以循环的顺序必须和 CubeMX 里配置的 Rank 顺序一致。注意H7 的 ADC 在扫描模式下每次HAL_ADC_PollForConversion()返回HAL_OK时HAL_ADC_GetValue()读到的就是当前 Rank 对应的通道数据。如果你在循环里改变了通道顺序读出来的数据就会错位。还有一个细节HAL_ADC_Stop()在每次采集完成后调用这样 ADC 会停止转换下次调用HAL_ADC_Start()时重新开始。如果你不调用HAL_ADC_Stop()ADC 会一直处于启动状态下次再调用HAL_ADC_Start()可能会返回错误。我试过连续调用HAL_ADC_Start()而不 Stop结果第二次就卡住了后来加上 Stop 就正常了。3.3 采样时间计算与精度优化采样时间的设置直接影响到测量精度尤其是高阻抗信号源。H7 的 ADC 采样时间是以 ADC 时钟周期为单位的可选项从 1.5 到 810.5 个周期不等。计算公式是总转换时间 (采样时间 8.5) / ADC 时钟频率其中 8.5 是逐次逼近转换的固定开销。以我的配置为例ADC 时钟是 12.5MHz采样时间设 64.5 个周期那么总转换时间就是 (64.5 8.5) / 12.5MHz 5.84 微秒。四个通道轮流转换一次总时间大约是 23.4 微秒。这个速度对于 1kHz 的采样率来说绰绰有余。但采样时间不是越长越好。如果信号源阻抗很低比如运放输出的电压信号采样时间设 64.5 个周期就足够了。如果信号源阻抗很高比如 NTC 热敏电阻分压电路阻抗可能在几十千欧以上这时候采样时间就要设长一些比如 387.5 个周期让采样电容有足够的时间充电到目标电压。我实测过用 64.5 个周期的采样时间测 NTC 分压点读出来的值比实际值偏低大约 3% 到 5%改成 387.5 个周期之后偏差就降到了 0.5% 以内。这个差异在温度测量里可能意味着好几度的误差所以采样时间一定要根据信号源阻抗来调整。提示如果你不确定信号源阻抗可以先用较长的采样时间测一遍然后逐步缩短观察读数什么时候开始明显偏低那个临界点就是比较合适的采样时间。4. 数据验证与常见问题排查4.1 用串口打印验证采集结果代码写完之后最直接验证方式就是把采集到的数据通过串口打印出来。我在main()的while(1)里加了这样的逻辑while (1) { ADC_ReadAllChannels(); printf(CH0%d, CH1%d, CH2%d, CH3%d\r\n, adc_values[0], adc_values[1], adc_values[2], adc_values[3]); HAL_Delay(100); }printf需要重定向到串口具体做法是在usart.c里加上fputc函数的重定向代码。这个在 F1 和 F4 系列里很常见H7 上也是一样的。重定向之后用串口助手打开对应的 COM 口波特率 115200就能看到数据了。验证的时候我一般会先用一个已知电压源接上去比如 3.3V 的基准电压看看读出来的值是不是接近满量程。H7 的 ADC 是 16 位分辨率满量程是 65535如果参考电压是 3.3V那么 3.3V 对应的读数应该是 65535 左右。实测下来读数在 65400 到 65500 之间波动这个偏差是正常的因为 ADC 本身有噪声和偏移。然后我会把电压源换成 1.65V看看读数是不是在 32768 附近。如果偏差超过 1%就要检查采样时间是不是太短或者参考电压是不是稳定。H7 的VREF引脚需要接一个干净的 3.3V 参考源如果直接接数字电源噪声会比较大读数波动也会更明显。4.2 常见问题速查表在实际调试过程中我遇到过不少问题这里整理成一张表方便快速排查现象可能原因解决方法读数始终为 0ADC 未启动或校准失败检查HAL_ADC_Start()是否调用校准函数是否返回HAL_OK读数始终为 65535输入电压超过参考电压检查输入信号是否超过VREF必要时加分压电路读数波动很大采样时间太短或参考电压不稳增加采样时间给VREF加滤波电容通道数据错位Rank 顺序与代码循环顺序不一致核对 CubeMX 里的 Rank 配置和代码里的循环顺序第二次采集卡住未调用HAL_ADC_Stop()每次采集完成后调用HAL_ADC_Stop()串口无输出printf未重定向或波特率不对检查fputc重定向代码确认串口助手波特率这张表里的每一个问题我都在实际项目中遇到过。尤其是“第二次采集卡住”这个问题当时排查了很久后来发现是HAL_ADC_Start()在没有HAL_ADC_Stop()的情况下重复调用导致的。H7 的 HAL 库对 ADC 的状态管理比较严格启动和停止必须成对出现。4.3 轮询模式的局限性与优化方向轮询模式虽然简单但它的局限性也很明显。最大的问题是 CPU 占用率。以我的配置为例四个通道转换一次需要大约 23.4 微秒如果每 1 毫秒采集一次CPU 占用率大约是 2.3%。这个比例看起来不高但如果采样率提高到 10kHzCPU 占用率就会上升到 23%这时候再跑其他任务就会明显感觉到卡顿。另一个问题是轮询模式无法处理突发的高频信号。如果信号变化很快轮询的采样间隔可能跟不上信号的变化导致采集到的数据失真。这种情况下就需要考虑用 DMA 或者中断模式了。DMA 模式可以让 ADC 在后台自动转换和搬运数据CPU 只需要在数据准备好之后去处理就行效率高很多。不过对于大多数低速采集场景轮询模式已经足够了。它的代码量少调试方便不需要处理中断优先级和 DMA 通道冲突这些复杂问题。我个人的经验是如果采样率低于 10kHz通道数少于 8 个轮询模式是完全可行的。如果超过这个范围再考虑切换到 DMA 模式。提示如果你后续想从轮询模式迁移到 DMA 模式CubeMX 里的配置改动不大主要是把Continuous Conversion Mode使能然后在DMA Settings里添加 DMA 通道。代码层面把HAL_ADC_PollForConversion()换成HAL_ADC_Start_DMA()就行数据会自动搬运到指定的数组里。5. 从轮询到进阶采集的扩展思路5.1 定时器触发与采样率精确控制轮询模式下采样率是由HAL_Delay()或者循环执行时间决定的精度不高。如果对采样率有严格要求比如需要精确的 1kHz 采样可以用定时器来触发 ADC 转换。H7 的 ADC 支持外部触发可以把定时器的TRGO事件配置成 ADC 的触发源。具体做法是在 CubeMX 里配置一个定时器比如TIM2设置成 1kHz 的更新频率然后把TRGO输出选成Update Event。在 ADC 的配置里把External Trigger Conversion Source改成Timer 2 Trigger Out event。这样每次定时器溢出时ADC 就会自动启动一次转换。代码里只需要在定时器中断或者主循环里读取数据就行采样率的精度由定时器时钟决定非常稳定。这种方式的优点是采样率精确不受代码执行时间的影响。缺点是配置稍微复杂一些需要同时理解定时器和 ADC 的触发机制。不过一旦配好后续使用就很方便了。5.2 多通道数据的校准与换算ADC 读出来的原始值只是 0 到 65535 之间的数字要变成实际的电压值还需要做换算。公式很简单电压 原始值 × VREF / 65535其中VREF是参考电压通常是 3.3V。但实际使用中VREF可能有偏差比如实际是 3.28V 而不是 3.3V这时候就需要用高精度万用表测一下VREF引脚的实际电压代入公式计算。对于 NTC 热敏电阻换算就更复杂一些。NTC 的阻值随温度变化通常用 Steinhart-Hart 方程或者查表法来换算。我一般用查表法把常用的温度点对应的 ADC 值预先算好存在数组里运行时用二分查找或者线性插值来获取温度值。这样比实时计算 Steinhart-Hart 方程要快得多精度也足够。注意NTC 的换算精度受分压电阻精度影响很大。如果分压电阻用的是 5% 精度的碳膜电阻温度误差可能在 1 到 2 度。换成 1% 精度的金属膜电阻误差可以降到 0.5 度以内。这个细节在温度测量项目里很关键。5.3 从轮询到 DMA 的平滑迁移如果后续采样率要求提高或者通道数增加轮询模式可能就不够用了。这时候可以考虑迁移到 DMA 模式。迁移的过程其实不复杂CubeMX 里的改动主要是使能Continuous Conversion Mode在DMA Settings里添加ADC1的 DMA 请求模式选Circular数据宽度选Half Word。代码层面的改动也不大。把HAL_ADC_Start()换成HAL_ADC_Start_DMA()传入数据缓冲区的地址和长度。DMA 会在每次转换完成后自动把数据搬运到缓冲区CPU 只需要在需要的时候去读缓冲区就行。如果配置成循环模式DMA 会一直搬运缓冲区满了之后自动从头开始覆盖非常适合连续采集的场景。我实测过从轮询模式迁移到 DMA 模式代码改动量大概在 20 行左右但 CPU 占用率可以从 20% 以上降到 1% 以下。这个提升在高速采集场景下非常明显。不过 DMA 模式也有它的复杂性比如缓冲区溢出、数据对齐这些问题需要额外注意。5.4 实际项目中的经验总结这个项目做下来我最大的体会是轮询模式虽然简单但简单有简单的好处。在调试阶段轮询模式的代码逻辑一目了然出了问题很容易定位。我见过不少项目一上来就用 DMA 加中断结果数据对不上排查了好几天才发现是 DMA 配置里某个参数设错了。如果先用轮询模式把基本功能跑通再逐步切换到 DMA调试效率会高很多。另一个体会是ADC 的精度不仅仅取决于分辨率还和参考电压、采样时间、PCB 布局都有关系。H7 的 16 位 ADC 理论上能分辨到 50 微伏但实际电路中电源噪声、地线干扰、信号源阻抗都会影响最终精度。我在 PCB 上给VREF加了 10 微法和 100 纳法两个电容读数稳定性明显提升。这个细节在数据手册里不会特别强调但实际项目中非常有用。最后CubeMX 虽然方便但不能完全依赖它。生成的代码只是框架真正的逻辑还需要自己补充。比如 ADC 校准、通道读取顺序、错误处理这些CubeMX 不会自动生成必须手动加上。我一般会把 CubeMX 生成的代码当作起点然后在上面逐步添加自己的逻辑每加一部分就验证一次确保每一步都是可控的。
返回列表