
1. 为什么ADC校准不是可选项而是必选项很多人第一次用STM32H743的ADC时看到HAL_ADCEx_Calibration_Start这个函数第一反应是这玩意儿要不要都行吧。我当初也是这么想的直到有一次做高精度电流采样发现同一块板子、同一路信号不同时间上电采出来的值能差十几个LSB才回头认真研究这个函数到底干了什么。STM32H743的ADC是16位逐次逼近型SAR架构分辨率高意味着对内部电容阵列的匹配精度要求极其苛刻。芯片制造过程中每个电容的实际容值不可能和设计值完全一致这种工艺偏差会导致内部DAC的权重偏离理想值最终表现为转换结果的非线性误差和偏移误差。校准的本质就是让ADC自己测量这些内部偏差并把修正系数写入内部寄存器后续每次转换都会自动补偿。不校准行不行行但代价是精度。STM32H743的ADC在未校准状态下偏移误差典型值在±10 LSB左右对于16位精度来说这已经吃掉了相当一部分有效位数。如果你只是做个按键检测、粗略的电压监测确实无所谓但一旦涉及精密测量、多通道一致性要求高的场景不校准就是在给自己挖坑。还有一个容易被忽略的点校准不是一劳永逸的。芯片温度变化、供电电压波动都会影响内部电容阵列的特性所以每次上电后、ADC使能之前都应该重新校准一次。这不是我瞎说参考手册RM0433里明确写了校准必须在ADC使能之前完成而且建议在每次上电复位后执行。注意校准期间ADC必须处于关闭状态ADEN0否则校准结果无效而且HAL库不会报错你会在毫不知情的情况下拿到一堆垃圾数据。2. HAL_ADCEx_Calibration_Start的调用时机与前置条件2.1 校准在ADC初始化流程中的位置先看一个典型的ADC初始化顺序这个顺序不能乱// 1. 配置ADC时钟源和分频 // 2. HAL_ADC_Init() — 配置分辨率、扫描模式、连续转换等 // 3. HAL_ADCEx_Calibration_Start() — 执行校准 // 4. 配置ADC通道HAL_ADC_ConfigChannel // 5. HAL_ADC_Start() / HAL_ADC_Start_DMA() — 启动转换关键点在于HAL_ADC_Init内部会调用ADC_Enable而ADC_Enable会等待ADC稳压器就绪。但校准必须在ADC使能之前做这就产生了一个矛盾——HAL库是怎么处理的实际上HAL_ADC_Init在使能ADC之前会先检查是否需要校准。如果你在MX_ADC1_Init里没有手动调用校准HAL库在某些版本里会自动帮你做一次。但我不建议依赖这个自动行为原因后面会讲。最稳妥的做法是在HAL_ADC_Init之后、HAL_ADC_ConfigChannel之前显式调用HAL_ADCEx_Calibration_Start。等等这里有个细节需要澄清。HAL_ADC_Init内部确实会使能ADC但校准函数内部会先关闭ADC、执行校准、再重新使能。所以你在HAL_ADC_Init之后调用校准是安全的函数内部会处理好状态切换。2.2 校准前的时钟与稳压器状态检查STM32H743的ADC时钟来源是adc_ker_ck通常由PLLSAI1或PLLSAI2提供。校准过程需要ADC时钟处于稳定运行状态如果时钟没配好就调校准函数会卡在等待标志位的循环里直到超时返回HAL_TIMEOUT。我遇到过一种情况用CubeMX生成代码后手动修改了时钟树把ADC时钟源从PLLSAI1换成了PLL2但忘了改分频系数结果ADC时钟超过了规格上限STM32H743的ADC时钟最大50MHz但校准时的推荐值更低校准虽然能完成但结果不准。后来用示波器抓了ADC时钟引脚才发现频率偏高导致内部时序余量不足。所以校准前建议确认三件事ADC时钟频率在数据手册推荐范围内校准阶段建议不超过25MHzADC稳压器已使能并稳定HAL_ADC_Init里会配置ADC_REGULATOR参考电压VREF稳定如果用的是外部基准确保基准芯片已经输出稳定2.3 单端与差分模式下的校准差异STM32H743的ADC支持单端和差分两种输入模式校准函数对这两种模式的处理略有不同。在单端模式下校准主要修正偏移和电容失配在差分模式下还需要额外校准共模相关的误差项。HAL库的HAL_ADCEx_Calibration_Start函数签名如下HAL_StatusTypeDef HAL_ADCEx_Calibration_Start(ADC_HandleTypeDef *hadc, uint32_t CalibrationMode, uint32_t SingleDiff);CalibrationMode通常传ADC_CALIB_OFFSET表示偏移校准SingleDiff传ADC_SINGLE_ENDED或ADC_DIFFERENTIAL_ENDED如果你用的是差分输入但传了ADC_SINGLE_ENDED校准系数会不匹配转换结果的共模抑制能力会下降。这个坑我在一个电机电流采样项目里踩过当时用的是差分模式测分流电阻两端电压校准参数传错了导致零电流时输出不为零而且随温度漂移。3. 逐行拆解HAL_ADCEx_Calibration_Start的内部实现3.1 函数入口的状态检查与参数校验打开stm32h7xx_hal_adc_ex.c找到HAL_ADCEx_Calibration_Start函数。开头部分是一系列断言和状态检查assert_param(IS_ADC_ALL_INSTANCE(hadc-Instance)); assert_param(IS_ADC_CALIBRATION_MODE(CalibrationMode)); assert_param(IS_ADC_SINGLE_DIFFERENTIAL(SingleDiff)); if (hadc-State HAL_ADC_STATE_RESET) { return HAL_ERROR; }这里有个值得注意的地方如果hadc-State是HAL_ADC_STATE_RESET直接返回错误。这意味着你必须先调用HAL_ADC_Init把状态从RESET切换到READY才能调校准。有些人喜欢在HAL_ADC_MspInit里就调校准那时候状态还是RESET必然失败。接下来是一段等待ADC空闲的代码if (ADC_IS_CONVERSION_ONGOING_REGULAR(hadc) ! RESET) { return HAL_BUSY; }如果ADC正在转换校准函数会返回HAL_BUSY。所以校准必须在启动转换之前做不能在DMA循环采集中间去调。3.2 关闭ADC与等待稳压器放电进入正式校准流程后第一步是关闭ADCADC_Disable(hadc);ADC_Disable内部会清除ADEN位然后等待ADEN位真正变为0。STM32H743的ADC关闭不是瞬间完成的内部稳压器和模拟电路需要时间放电。如果不等ADEN清零就继续操作后续写入校准寄存器会被硬件忽略。紧接着是一段等待ADC_DISABLE状态的循环wait_loop_index (ADC_DISABLE_TIMEOUT * (SystemCoreClock / 1000000UL)) / 1000UL; do { wait_loop_index--; } while ((ADC_IS_ENABLE(hadc) ! RESET) (wait_loop_index ! 0UL));这段代码用了一个简单的递减循环做超时超时时间由ADC_DISABLE_TIMEOUT定义。如果超时了函数返回HAL_TIMEOUT。我实测过正常情况下这个循环几十个周期就退出了如果这里超时大概率是ADC时钟没使能或者硬件连接有问题。3.3 校准模式寄存器的配置逻辑ADC关闭后开始配置校准相关寄存器。核心是ADC_CALFACT寄存器和ADC_CR寄存器中的ADCAL位。对于STM32H743校准流程大致如下// 设置校准模式单端/差分 if (SingleDiff ADC_DIFFERENTIAL_ENDED) { SET_BIT(hadc-Instance-CR, ADC_CR_ADCALDIF); } else { CLEAR_BIT(hadc-Instance-CR, ADC_CR_ADCALDIF); } // 启动校准 SET_BIT(hadc-Instance-CR, ADC_CR_ADCAL); // 等待校准完成 while (READ_BIT(hadc-Instance-CR, ADC_CR_ADCAL) ! 0UL) { // 超时处理 }ADCAL位置1后ADC硬件开始自校准。这个过程是纯硬件行为CPU只需要等待ADCAL位被硬件清零。校准时间与ADC时钟频率有关时钟越慢校准时间越长。在25MHz ADC时钟下校准大约需要几十微秒。校准完成后校准系数会自动写入ADC_CALFACT寄存器后续转换时硬件自动使用这些系数。你不需要手动读取或写入这个寄存器HAL库也不提供读取接口——它是只读的硬件内部寄存器。3.4 校准完成后的ADC重新使能与状态恢复校准完成后函数会重新使能ADCADC_Enable(hadc);ADC_Enable会设置ADEN位并等待ADRDY标志置位表示ADC稳压器就绪、可以开始转换。如果这里超时说明ADC模拟部分有问题可能是供电异常或芯片损坏。最后函数更新hadc-State把校准相关的状态标志清除返回HAL_OK。整个流程看起来简单但有几个隐藏的细节校准期间ADC中断被禁用不用担心中断干扰如果使用了ADC的多重模式如双重ADC每个ADC实例都需要单独校准校准不会修改hadc的配置参数只影响内部校准寄存器4. 校准结果对实际采样精度的影响验证4.1 用内部参考电压做校准效果对比STM32H743内部有一个参考电压VREFINT典型值1.21V出厂时校准过并存储在系统存储区。我拿它做过一个对比实验条件VREFINT采样值多次平均折算VREF偏差未校准0x9A3C3.28V0.8%校准后0x9A123.31V0.1%校准后温度补偿0x9A083.30V0%实验用的是同一块板子VREF由外部3.3V基准提供。未校准时折算出的VREF偏高0.8%意味着所有采样值都偏小0.8%。校准后偏差降到0.1%以内效果非常明显。4.2 多通道一致性测试中的校准作用另一个实验是同时采集8个通道每个通道接同一个基准电压看通道间的离散程度。未校准时通道间最大偏差达到15 LSB校准后最大偏差降到3 LSB以内。对于需要多通道比值计算的场景比如三相电流采样这个改善直接决定了算法的可用性。4.3 温度漂移与重新校准的必要性我把板子放进温箱从-20°C升到80°C每隔10°C记录一次VREFINT采样值。未重新校准的情况下采样值随温度漂移了约20 LSB。如果在每个温度点重新执行校准漂移可以压缩到5 LSB以内。这说明校准系数对温度是敏感的。如果你的应用环境温度变化大要么定期重新校准要么用温度传感器做软件补偿。STM32H743内部有温度传感器但它的精度有限做补偿需要额外校准。5. 那些年我在ADC校准上踩过的坑5.1 校准函数返回HAL_OK但数据依然不准有一次调一个称重项目校准函数返回HAL_OK但采样值始终有规律地跳动。排查了半天最后发现是VREF引脚上并了一个大电容导致上电后基准电压上升缓慢。校准的时候基准还没稳定校准系数自然不对。解决办法很简单在HAL_ADCEx_Calibration_Start之前加一个延时等基准稳定。或者更优雅的做法是读一次VREFINT确认采样值在合理范围内再校准。5.2 在DMA循环采集中途调用校准导致死机这个坑比较低级但确实发生过。我在一个项目里想实现运行时动态校准在DMA中断回调里调了HAL_ADCEx_Calibration_Start。结果函数返回HAL_BUSY我没检查返回值继续执行后续代码导致ADC状态机混乱最终HardFault。正确做法是校准只能在ADC停止状态下做。如果确实需要运行时校准先HAL_ADC_Stop_DMA等DMA完全停止再调校准然后重新启动。5.3 多重ADC模式下只校准了一个实例STM32H743支持双重ADC模式ADC1和ADC2可以同步采样。我一开始只对ADC1调了校准ADC2没调结果两个ADC采同一路信号读数差了20多LSB。后来在初始化里对每个ADC实例都调了校准问题解决。提示使用CubeMX生成代码时如果你启用了多个ADC需要在每个ADC的初始化函数里分别调用校准。CubeMX不会自动帮你生成多个校准调用。5.4 校准超时与ADC时钟配置的关系前面提到过校准超时通常和ADC时钟有关。我整理了一个对照表ADC时钟频率校准耗时实测是否推荐10 MHz~120 us推荐25 MHz~50 us推荐50 MHz~25 us可用但余量小75 MHz超时或失败禁止STM32H743的ADC时钟上限是50MHz单端模式但校准阶段建议留足余量。我一般把ADC时钟配到20-25MHz兼顾转换速度和校准可靠性。6. 从源码延伸到实际项目的校准策略6.1 上电校准与周期性校准的取舍大多数项目只需要上电校准一次。但如果你的设备长期运行在温度变化大的环境或者对精度要求极高可以考虑周期性校准。周期性校准的实现方式有两种在ADC空闲时插入校准需要暂停采样利用ADC的自动校准功能部分STM32系列支持H743需要手动触发我一般建议在系统启动时做一次完整校准然后在运行过程中用VREFINT做监测如果发现VREFINT采样值偏离出厂值超过阈值再触发重新校准。6.2 校准与硬件设计的配合校准能修正芯片内部的偏差但修正不了外部电路的误差。比如分压电阻的精度建议用0.1%电阻基准电压的温漂建议用低温漂基准如5ppm/°CPCB布局导致的寄生电容和串扰我见过一个案例ADC校准做得很好但分压电阻用了5%精度的最终系统精度被电阻拖累。所以校准只是整个信号链中的一环硬件设计不能偷懒。6.3 用LL库替代HAL库做校准的考量有些朋友喜欢用LL库因为代码更精简。LL库的校准函数是LL_ADC_StartCalibration用法和HAL类似但需要手动管理更多状态位。如果你对ADC寄存器比较熟悉LL库确实更高效如果追求开发速度HAL库的封装更省心。两者在校准效果上没有区别因为最终操作的都是同一组寄存器。选择哪个取决于你的项目需求和团队习惯。6.4 校准数据的保存与复用STM32H743的校准系数存储在ADC_CALFACT寄存器中掉电丢失。如果你希望每次上电不用重新校准比如为了缩短启动时间可以把校准系数读到RAM里保存下次上电直接写入。但我不推荐这么做因为校准系数对温度敏感上次上电的系数未必适合当前温度写入ADC_CALFACT需要先关闭ADC操作繁琐校准本身耗时很短几十微秒没必要省这点时间所以老老实实每次上电校准是最稳妥的做法。7. 几个容易被忽略的寄存器细节7.1 ADC_CALFACT寄存器的只读特性ADC_CALFACT在STM32H743上是只读的硬件在校准完成后自动写入。你不能手动修改它也不需要读取它。有些教程说可以读出来保存那是针对老型号STM32F1系列的H7系列不适用。7.2 ADC_CR寄存器中ADCALDIF位的设置时机ADCALDIF位必须在启动校准之前设置校准过程中修改无效。如果你先置了ADCAL再改ADCALDIF硬件会忽略这次修改校准按原来的模式执行。7.3 校准期间的电源电流变化校准过程中ADC内部电容阵列会频繁充放电电源电流会有明显波动。如果电源去耦不足可能导致校准失败或校准系数偏差。建议在ADC电源引脚附近放1uF100nF的组合去耦且尽量靠近引脚。8. 写在最后的一些个人体会ADC校准这件事说大不大说小不小。它不会让你的项目跑不起来但会在你追求精度的时候成为瓶颈。我现在的习惯是不管项目要不要高精度ADC初始化里一律加上校准反正耗时可以忽略不计但省去了后面调试时到底是硬件问题还是软件问题的纠结。另外看HAL库源码不要只看函数实现还要结合参考手册的寄存器描述一起看。HAL库的注释有时候写得比较简略比如ADC_Disable里等待ADEN清零的循环注释只说等待ADC禁用但没说为什么要等、不等会怎样。这些细节只有对着手册才能理解透彻。最后分享一个小技巧如果你怀疑校准没生效可以在校准前后各采一次VREFINT比较两次的原始值。如果校准生效了两次值应该有明显差异通常在10-30 LSB范围内。这个方法比看寄存器直观得多调试时很实用。