ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32 SAR型ADC采样时间与转换计算详解:从硬件原理到数据采集

STM32 SAR型ADC采样时间与转换计算详解:从硬件原理到数据采集 1. 认识STM32的SAR型ADC从一颗采样电容开始1.1 逐次逼近的流程和为什么有最小采样时间很多做传感器项目的朋友一上来就在STM32CubeMX里把ADC通道打开跑起来读数好像也对但碰到信号源内阻稍大、引线稍长的情况数据就开始漂。这时候才知道ADC不只是一个“读寄存器的外设”它内部其实是 SAR逐次逼近寄存器架构整个转换过程分为采样、量化和输出三个阶段。先说采样阶段。芯片内部的模拟开关会把 ADC 通道对应的采样电容切换到输入引脚上让电容充电到和输入电压一致。这个过程需要时间因为引脚外部有信号源内阻内部有导通电阻电容本身也有容量。RC 充电的时间常数越大需要的采样窗口就越长。STM32 里专门有个参数叫“采样时间”你可以在初始化时选 1.5 个 ADC 时钟周期到 239.5 个 ADC 时钟周期。选得越长充电越充分测量越准选得越短转换越快但对外部源阻抗越敏感。很多人用高内阻传感器读数不准多半就是这个采样时间没有配合源阻抗去算。然后是量化阶段。采样电容充电完成开关断开芯片把这颗电容里的电压“锁住”然后内部 DAC 从最高位开始逐次产生比较电压。时钟每一个周期比较一位STM32 的 ADC 是 12 位这个比较过程就需要 12 个 ADC 时钟周期。所以你去看 F1 系列的数据手册会发现转换时间有一句固定公式总转换时间 采样时间 12.5 个 ADC 时钟周期那 0.5 个周期是内部的一些同步处理。明白了这个公式后面才能算出自己系统的最高采样率而不是光看别人帖子里写了一个最大 1MHz 就盲目套用。1.2 STM32F1/F4的ADC资源清单STM32F103 和 F407 这类主流芯片的 ADC 都是 12 位分辨率转换结果范围是 0 到 4095。F103 的 ADC1 有 16 个外部通道分别是 PA0 到 PA7、PB0 到 PB1、PC0 到 PC5另外还有内部温度传感器通道和内部参考电压通道。F407 的通道数量更多封装的引脚不同可用数量也不同。除了规则组STM32 ADC 还带一个注入组。规则组适合普通的连续采样注入组有更高的优先级可以“插队”打断规则组的转换适合电机控制、过压保护这类需要紧急采样的场景。普通应用大多数只用到规则组但你必须知道注入组的存在否则看手册看到 JDR 寄存器的时候会一脸懵。ADC 的参考电压同样要留意。很多封装把 VREF 内部直接连到了 VDDA也就是说你的 3.3V 电源精度直接影响 ADC 的满量程精度。高精度应用最好选择有独立 VREF 引脚的封装外部接一个低噪声的基准源。这个细节经常决定一个产品的测量精度上限。2. 采样时间、转换周期和时钟的“三本账”2.1 ADC时钟从哪儿来STM32F103 的系统主频最高 72MHz但 ADC 模块的工作时钟不是直接拿 72MHz 用。F1 系列的 ADC 输入时钟来自 APB2 总线时钟 PCLK2PCLK2 通过 RCC 的分频寄存器设置。最关键的一条限制是ADC 时钟最高不能超过 14MHz。配置的时候你需要在 Clock Configuration 里把 APB2 分频设置为 6。72MHz 除以 6得到 12MHz 的 ADC 时钟。这个 12MHz 就是计算采样时间的基准。如果不小心把 APB2 设成 2 分频ADC 时钟变成 36MHz超出规格轻则转换不准确重则长时间跑会损害 ADC 模块。这一点在 CubeMX 里很容易被忽略因为界面只显示 PCLK1 和 PCLK2不会主动提醒你 ADC 超频。F4 系列的 ADC 时钟逻辑不太一样它挂在 APB2 上但 ADC 模块内部还有一个独立的预分频器。无论哪一代芯片原则都是一样的先搞清楚 ADC 模块的输入时钟是多少 MHz再去选采样时间。2.2 算出生理周期的账假设 ADC 时钟为 12MHz我们选一个常用的采样时间比如 71.5 个周期。根据 F1 的转换周期公式总转换周期 71.5 12.5 84 个周期单个周期时长 1 / 12MHz ≈ 83.3ns单次转换时间 84 × 83.3ns ≈ 7μs也就是说这种配置下 ADC 一秒最多转换约 14 万次。如果你想追求更高的采样率比如靠近 1MSPS那就必须把采样时间压到 1.5 个周期。此时总周期数是 14 个单次转换时间约 1.17μs理论上算出 857kSPS。这个“理论值”并不是骗人而是要在低阻源、短引线的理想条件下才能保证精度。实际操作中我不会为了追求极限速率而把采样时间调到最短。一般传感器信号、电压采集场景选择 55.5 或 71.5 周期非常稳。音频、振动信号这种确实需要高速采样的场景才会用 7.5 或 13.5 周期并且必须在模拟输入前端加驱动运放把源阻抗压到几百欧以内。2.3 不同采样时间的直观对比我把常见采样时间的转换周期和估算时间整理了一下方便你快速查阅。采样时间配置总转换周期12MHz时钟下单次转换时间约合采样率1.5周期14周期约1.17μs约857kSPS7.5周期20周期约1.67μs约600kSPS13.5周期26周期约2.17μs约461kSPS28.5周期41周期约3.42μs约292kSPS55.5周期68周期约5.67μs约176kSPS71.5周期84周期约7μs约143kSPS239.5周期252周期约21μs约47kSPS这里还要考虑一个隐藏问题STM32F103 的 ADC 采样电容大约十几皮法如果信号源内阻是 10kΩRC 充电时间常数就有 100 多纳秒。选 1.5 周期采样时窗口只有 125ns电容根本充不满转换结果必然偏低。所以源阻抗高的场景要么加运放跟随器要么老老实实用 71.5 周期以上。这句话值得你记下来因为它解决了大量“ADC 读数偏低”的排查方向。3. 单通道轮询读取一条靠谱的“Hello ADC”3.1 CubeMX配置里最容易踩的三个开关先在 STM32CubeMX 里把对应引脚设置为 ADC 输入模式也就是模拟输入。这一步很多人会漏直接把引脚配成 GPIO 的 Input结果 ADC 读出来永远是 0 或者乱跳。原因很简单只有配置成模拟输入引脚内部的模拟开关才能正常接入 ADC 通道。初始化 ADC1 的界面里有几个参数我建议所有新手第一次写单通道程序时这样设Scan Conversion Mode 关闭Continuous Conversion Mode 关闭Discontinuous Conversion Mode 关闭。这三个开关关掉后ADC 就是一个纯粹的“按一下转一次”的软件触发状态机最容易理解和调试。External Trigger 选 Software trigger数据对齐方式默认右对齐即可。最关键的是 Number Of Conversion 填 1并且在下面通道配置里选中你用的通道号把采样时间设置为一个中间值比如 71.5 周期。这样配置出来不会有乱跳的问题也不会让新手一上来就被高级特性扰昏头。3.2 HAL代码启动、等待、取数CubeMX 生成初始化代码后核心读取流程其实是三段。以 F103 为例我写了下面这个函数你可以直接抄。#include adc.h uint16_t ADC_ReadRaw(ADC_HandleTypeDef *hadc) { HAL_ADC_Start(hadc); HAL_ADC_PollForConversion(hadc, 10); uint16_t raw (uint16_t)HAL_ADC_GetValue(hadc); HAL_ADC_Stop(hadc); return raw; }第一行 HAL_ADC_Start 把 ADC 从禁用状态切换到使能状态如果是软件触发它还负责触发一次转换。第二行 HAL_ADC_PollForConversion 等待转换完成第二个参数是超时时间单位毫秒。我写 10ms是因为正常情况下一次转换也就 1 到 20 微秒如果超过 10ms 还没转完说明配置有问题。第三行 HAL_ADC_GetValue 从数据寄存器读取结果。注意这种单次轮询方式在读取完成后最好调用 HAL_ADC_Stop。如果不停掉下一次 HAL_ADC_Start 时 ADC 还在运行行为会因芯片版本不同而变得不可预期。养成“一启一停”的习惯能让代码在 F1、F4、G0 这些不同内核芯片间顺利移植。3.3 原始码值换算电压的正确姿势ADC 读出来的 raw 值是 0 到 4095 的整数它不等于电压。换算公式是实际电压 raw × VREF / 4095比如用 3.3V 参考电压raw 为 2048实际电压约 1.65V。因为 STM32 的 ADC 是 12 位数字满量程 4095 对应参考电压本身所以这里不能除以 4096。虽然很多人习惯除以 4096 来折合 12 位二进制但严格按数据手册满量程码值是 4095对应的满量程电压是 VREF 减去一个 LSB。我写工程时通常会包一层转换函数把假设电压基准放进去float ADC_ReadVoltage(ADC_HandleTypeDef *hadc, float vref) { uint16_t raw ADC_ReadRaw(hadc); return (float)raw * vref / 4095.0f; }这种做法让主程序里不需要到处写魔法数字。以后换基准电压只改一处即可。4. 多通道扫描DMA让ADC自己一直干活4.1 扫描模式的通道序列设置单通道轮询适合读一下、算一下的简单逻辑。但实际工程里我遇到的需求一般是同时采集三相电压、三个温度点、两个电流信号。这时候如果要 CPU 不断去轮询四五个通道既占时间又容易漏数据。解法是开 Scan 扫描模式让 ADC 自动按配置的通道顺序转一圈。扫描模式拼接的是一串“规则组序列”。比如我用 ADC1 的三个通道分别是 IN0、IN1、IN2那么在 CubeMX 里打开 Scan Conversion ModeNumber Of Conversion 填 3然后把 Rank1 配 IN0、Rank2 配 IN1、Rank3 配 IN2。每次转换触发后ADC 会按 Rank 顺序依次转换三路最后统一产生一个中断或者 DMA 请求。扫描模式和连续模式可以同时开。连续模式让 ADC 在转换完一轮之后马上开始下一轮不需要再触发。这样配置成“连续扫描”后ADC 理论上会自己一直跑直到你主动停止。如果没有 DMA连续扫描模式每轮结束都会触发中断CPU 得不停进中断取数这对实时性要求高的系统很不友好。所以下一步我会直接接 DMA。4.2 DMA环形缓冲和双缓冲回调DMA 的好处是ADC 每次转换完成DMA 硬件自动把 ADC 数据寄存器里的值搬到内存数组里整个过程不占 CPU 时间。先定义一个数组注意 STM32 的 ADC 数据寄存器是 16 位宽所以数组类型用 uint16_t。#define ADC_BUF_SIZE 128 uint16_t adc_buf[ADC_BUF_SIZE];CubeMX 的 DMA Settings 里添加 ADC1 的 DMA 请求方向是 Peripheral To Memory模式选 Circular 循环模式。数据宽度设为 Half Word因为 12 位数据只需要 16 位。然后在 main 工程里这样启动HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, ADC_BUF_SIZE);这句执行之后ADC 采集的数据会持续不断地填充 adc_buf。当 DMA 传了一半和传完一轮时会触发回调void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { // bufs的前半段已经填完 } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { // bufs的后半段已经填完 }一个非常实用的小技巧是用这种“半满回调 整满回调”的双缓冲思路一边处理旧数据DMA 另一边继续填新数据永远不丢点。很多采集系统就是这么设计的。你不需要在回调里重新调用 HAL_ADC_Start_DMA因为循环模式下 DMA 会自动接力。这个细节能防止新手在中断里反复启停导致的数据错位。5. 定时器触发ADC按波形节奏采样5.1 为什么软件启动采样跟不上信号软件触发的问题在于时机不可控。主程序可能正在处理通信、显示随手调用一次 HAL_ADC_Start但那一刻波形正好在波峰采到的值自然偏高。下一轮主程序忙完再去采可能又是波谷。这样的数据如果直接用于计算有效值、频率或者相位结果就是一团糟。定时器触发 ADC 的原理是把采样时刻的决定权交给硬件定时器。你预先设定 PWM 脉冲输出的上升沿或下降沿ADC 收到触发边沿后自动开始转换与 CPU 正在执行什么代码无关。这就能让采样点均匀分布在信号周期的固定位置上。电机控制里计算相电流必须在 PWM 中点采样因为那里开关噪声最小这个中点时刻就是靠定时器专门触发出来的而不是靠 CPU 掐表。5.2 定时器触发配置实例以 STM32F103 为例我想让 TIM2 的更新事件周期触发 ADC。CubeMX 里先把 TIM2 的 Pre-scaler 和 Counter Period 配置成想要的采样周期。比如预分频 72计数周期 1000那么采样频率就是72MHz / 72 / 1000 1kHz然后在 ADC 配置页把 External Trigger Conversion Source 选为 Timer 2 Capture Compare 2 事件External Trigger Edge 选上升沿。HAL 初始化代码里你会看到hadc1.Init.ExternalTrigConv ADC_EXTERNALTRIGCONV_T2_CC2; hadc1.Init.ExternalTrigConvEdge ADC_EXTERNALTRIGCONVEDGE_RISING;主程序里只需要先启动定时器再启动 ADCHAL_TIM_Base_Start(htim2); HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, ADC_BUF_SIZE);之后每次 TIM2 到达比较点ADC 自动转换一次DMA 自动搬运CPU 不参与。需要改变采样率就改定时器的分频和周期不需要动 ADC 配置。这种模式在交流谐波分析、数字电源、振动检测里非常常见。定时器触发还有一个隐藏优势就是可以利用硬件级联动实现“多通道或多器件几乎同一时刻采样”。几个 ADC 共享同一个触发源采样起点的时间差只取决于触发信号传播抖动比软件轮询稳定得多。6. 采样之后不能直接用去直流、滤波和归一化套路6.1 滑动平均、中值和均值滤波的选择很多人把 raw 值直接画出来看波形发现一个正弦波绕着一个直流偏置往复跳这很正常。传感器输出一般都有直流偏置所以我们需要在软件层面做处理。最常见的滤波有三种。滑动平均滤波适合平滑高频噪声但对突发脉冲很敏感一个毛刺就能把平均值拉偏。中值滤波适合去除随机脉冲干扰比如电磁阀动作瞬间产生的尖峰它不会把尖峰直接算进平均值但计算量略大。限幅滤波也就是和前一次结果做比较、突变超过阈值就丢弃适合保护那些不能让输出跳变的控制系统。对于 ADC 数据我给自己定了一条经验规则如果只是测缓变的电压/温度用 8 到 16 次算术平均如果信号里偶发脉冲干扰先做 3 到 5 点中值滤波再做滑动平均如果是要还原一个动态波形要注意滤波带宽不能一味平均把波形削平。写一个简单的 8 次均值滤波函数uint16_t ADC_ReadAverage(ADC_HandleTypeDef *hadc, uint8_t times) { uint32_t sum 0; uint8_t i; for (i 0; i times; i) { HAL_ADC_Start(hadc); HAL_ADC_PollForConversion(hadc, 10); sum HAL_ADC_GetValue(hadc); HAL_ADC_Stop(hadc); } return (uint16_t)(sum / times); }times 通常取 2 的幂次比如 8 或 16这样除法可以用右移实现代码在编译后会更高效。当然工程语义优先这种优化属于锦上添花。6.2 去直流与幅值归一化代码交流信号的 ADC 采样还经常涉及两个操作去直流和归一化。比如把传感器输出的 1.5V 偏置、±100mV 波动的信号转换成围绕 0V、幅度在 -1 到 1 之间变化的数值。去直流的原理是先把一段数据的平均值算出来这个平均值就代表直流偏置然后每个采样点减去它。归一化的原理是先找出这段数据里的最大值和最小值计算出信号的近似峰值再把去直流后的值除以峰值幅度。#define ADC_NORM_BUF_LEN 256 void ADC_DcRemoveAndNormalize(const uint16_t *buf, uint16_t n, float *out) { uint32_t sum 0; uint16_t i; float dc, peak_amp; for (i 0; i n; i) { sum buf[i]; } dc (float)sum / n; uint16_t min_val buf[0]; uint16_t max_val buf[0]; for (i 1; i n; i) { if (buf[i] min_val) min_val buf[i]; if (buf[i] max_val) max_val buf[i]; } peak_amp (max_val - min_val) / 2.0f; if (peak_amp 1.0f) peak_amp 1.0f; for (i 0; i n; i) { out[i] ((float)buf[i] - dc) / peak_amp; } }这个函数输出 out 数组取值范围大概在 -1 到 1 之间。后面做波形比较、幅值计算、幅相分析时被处理过的数据就在一个稳定的尺度内。不过这里有个注意点如果波形不对称或者存在随机干扰用“最大值减最小值的一半”来估算峰值会被少数异常点带偏。更稳妥的做法是取一段信号里的 95% 分位点和 5% 分位点来估算幅度或者先做滤波再去峰峰值。至于具体选哪种取决于你采集的信号信噪比。7. 精度、校准和噪声从“能出数”到“测得准”7.1 ADC校准函数为什么必须调STM32 的 ADC 模块内部有校准逻辑尤其在 F1 系列里ADC 在出厂时会有一些偏移误差。如果不做校准零输入时可能读出 5 到 20 个 LSB 的偏移值在满量程 4095 里不算大但如果是做高精度电流电压测量这个偏移会被放大。HAL 库提供了校准函数一般放在 ADC 初始化之后调用一次HAL_ADCEx_Calibration_Start(hadc1);这个函数会启动一次内部校准流程校准系数保存在 ADC 的校准寄存器里。注意它应该在 ADC 尚未启用转换时调用。不少人喜欢在系统上电第一次测量前去校准这种做法没问题。更讲究一点的做法是在环境温度变化较大后或者每次从低功耗唤醒后重新校准一次因为温度漂移会影响内部偏移。工程上我遇到过运行一小时数据缓慢漂移的情况排查后根因就是校准没有在关键节点重新执行。7.2 电源、地、参考电压的实战建议ADC 模拟量的精度天花板往往不是 ADC 本身而是电源噪声和参考电压质量。具体项目里我见过太多人用同一个 LDO 给 MCU 和传感器供电传感器一启动ADC 读数同步跳。几个实用建议这套组合拳能给大多数测量工程带来直接改善VDDA 引脚单独接一个 1μF 和 100nF 电容紧靠芯片放置。VREF 如果由 VDDA 提供不要在它上面再接对地负载。模拟输入走线尽量短远离 IO 翻转频繁的数字信号线。传感器信号需要远距离传输时先经运放跟随/放大再进 ADC。如果信号本身带高频干扰可以在 ADC 输入前加一级低通 RC电阻不宜超过 10kΩ否则要和采样时间一起估算误差。还有一个经常被忽略的地回路。传感器和 MCU 必须在同一个参考地并且采样回路的地阻抗要尽量低。ADC 看起来是单端输入但它的参考地就是 GND如果 GND 上有噪声所有测量都会把这个噪声叠加进去。我在这上面栽过跟头最后是改了 PCB 走线把传感器地单独一条线引回主电容地读数立刻稳定了一个数量级。7.3 用信噪比和ENOB评估系统到底行不行ADC 规格书里的 12 位分辨率只是数字位数实际有效位数往往到不了 12。理论信噪比有个简化公式SNR 6.02 × N 1.76 dB其中 N 是位数。12 位 ADC 的理想 SNR 大约是 74dB。但实际测量时系统噪声会把这个值压低这时候要用“有效位数” ENOB 来表示。比如实际测出来 SNR 只有 60dB那么 ENOB 大概是ENOB (60 - 1.76) / 6.02 ≈ 9.7 位也就是说你的系统虽然拿回一个 4095 满量程的数值但真正可信的有效分辨率只有接近 10 位。很多高精度采样项目的指标就是奔着 ENOB 去设计的。如果你想快速评估自己板子的 ENOB可以给输入端短接到地连续采集 1024 个点计算标准差把它折算成 LSB 数。若标准差是 2 个 LSB那么噪声等效为约 12 - log2(2) 11 位有效分辨率。这个方法简单有效能在不依赖昂贵设备的情况下摸清硬件底细。我每做一块新板子都会先跑一遍这个自测心里有底再去调算法。最后分享一个我在实际项目里反复吃到的教训ADC 程序写得再漂亮都抵不过硬件上一颗劣质电容带来的噪声。无论你用的是 STM32F1 还是 F4先把供电引脚去耦做扎实再去碰寄存器。调试顺序也一定是先检查输入引脚电压是否真实、再检查 ADC 原始码、最后才怀疑滤波算法的参数。这样一条链路走下来你多半能发现问题不在代码而在你还没想明白的那颗采样电容上。
返回列表