4. ADC采样与校准:STM32/ESP32的ADC模块配置、采样率设置、过采样技术、软件校准算法(最小二乘法拟合)
各位同学,咱们今天聊点硬核的——ADC采样与校准。
说实话,做电池监测,ADC就是咱们的“眼睛”。眼睛要是近视了,后面再牛逼的算法也是白搭。我在项目里见过太多人,花大价钱买了高精度传感器,结果ADC没配好,数据一塌糊涂。嗯,今天咱们就把这块彻底讲透。
4.1 STM32的ADC模块配置
STM32的ADC,我个人习惯用规则组+注入组的模式。规则组负责常规采样,注入组用来处理紧急任务。比如电池电压是常规监测,但突然检测到过流,就得用注入组立刻打断当前采样。
配置时,有几个关键参数你得盯紧了:
- 分辨率:STM32F4系列支持6/8/10/12位。做电池监测,我建议直接上12位。别省那点转换时间,精度才是王道。
- 转换模式:连续转换还是单次转换?我个人习惯用连续转换,配合DMA,CPU几乎不用管。
- 对齐方式:右对齐。左对齐是给某些特殊应用用的,咱们用不上。
核心配置代码(STM32 HAL库):
// 初始化ADC句柄
ADC_HandleTypeDef hadc1;
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = DISABLE; // 单通道
hadc1.Init.ContinuousConvMode = ENABLE; // 连续转换
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
HAL_ADC_Init(&hadc1);
这里有个坑——时钟预分频。STM32的ADC时钟不能超过36MHz(F4系列)。我曾经有个同事,直接把PCLK2不分频就给了ADC,结果采样值跳得像心电图。你想想看,时钟太快,采样电容还没充饱就被读走了,数据能准吗?
4.2 ESP32的ADC模块配置
ESP32的ADC跟STM32不太一样。它有两个SAR ADC模块,每个最多支持10个通道。但注意了——ESP32的ADC线性度一般,尤其是靠近电源轨和地轨的时候。
配置要点:
- 衰减器设置:ESP32的ADC默认输入范围是0-1.1V。如果你要测3.7V的锂电池,必须开启11dB衰减,把量程扩展到0-3.6V左右。
- 位宽选择:支持9/10/11/12位。我建议用12位,但要做好心理准备——实际有效位数可能只有9-10位。
- 采样率:ESP32的ADC采样率最高能到200ksps,但别跑满。我一般控制在50ksps以内,数据稳定很多。
ESP32 Arduino环境配置示例:
// 配置ADC参数
analogReadResolution(12); // 12位分辨率
analogSetAttenuation(ADC_11db); // 11dB衰减,量程0-3.6V
analogSetWidth(12); // 设置位宽
// 读取电压值
uint32_t raw = analogRead(36); // GPIO36为ADC1_CH0
float voltage = raw * 3.6f / 4095.0f;
⚠️ 注意:ESP32的ADC在WiFi开启时会有明显噪声干扰。我建议在WiFi休眠时段采集数据,或者用硬件平均功能来降噪。
4.3 采样率设置与过采样技术
采样率怎么定?说白了就是奈奎斯特定理——采样频率至少是信号最高频率的两倍。电池电压变化很慢,1kHz的采样率都嫌多。但电流呢?尤其是电机启动瞬间的浪涌电流,变化可能很快。
我个人习惯:
- 电压:10Hz采样率,足够了。电池电压不会突变。
- 电流:100Hz-1kHz,看负载类型。如果是纯阻性负载,100Hz够用;如果是电机或开关电源,建议1kHz以上。
- 温度:1Hz,温度变化最慢。
接下来说说过采样。这是个好东西——用速度换精度。
原理很简单:你采集N次,累加后右移log2(N)位。比如你要提高4倍分辨率(增加2位),就采集16次,累加后右移4位。
过采样实现代码:
// 过采样函数:将12位ADC提升到14位
uint16_t oversample_14bit(uint8_t channel) {
uint32_t sum = 0;
for (int i = 0; i < 16; i++) { // 16次采样
sum += analogRead(channel);
}
return (uint16_t)(sum >> 4); // 右移4位,得到14位数据
}
但注意了,过采样不是万能的。它只能降低白噪声,对周期性干扰效果有限。我曾在项目中用64倍过采样,结果发现50Hz工频干扰反而被放大了。后来加了硬件滤波才搞定。
4.4 软件校准算法:最小二乘法拟合
硬件调完了,数据还是不准?别急,还有最后一招——软件校准。
ADC的误差主要来自:
- 偏移误差:输入为0时,输出不为0。
- 增益误差:实际斜率与理想斜率不一致。
- 非线性误差:这个比较麻烦,但电池监测场景下通常可以忽略。
校准的思路很简单:用已知电压源(比如高精度参考电压)测几个点,然后拟合出一条直线。以后每次采样,都用这条直线来修正。
最小二乘法就是干这个的。它的核心思想:找到一条直线,让所有采样点到这条直线的垂直距离平方和最小。
最小二乘法拟合公式:
斜率 k = (n*Σ(xi*yi) - Σxi*Σyi) / (n*Σ(xi²) - (Σxi)²)
截距 b = (Σyi - k*Σxi) / n
其中:
xi = 标准电压值(已知)
yi = ADC原始读数
n = 采样点数
实际项目中,我一般取3个校准点:0V、中间值、满量程附近。比如测3.7V锂电池,我会用1.0V、2.5V、4.0V三个点。
校准代码实现:
// 校准数据结构
typedef struct {
float slope; // 斜率
float intercept; // 截距
} Calibration_t;
// 最小二乘法校准
Calibration_t calibrate_adc(float *std_voltage, float *adc_raw, int n) {
float sum_x = 0, sum_y = 0, sum_xy = 0, sum_xx = 0;
for (int i = 0; i < n; i++) {
sum_x += std_voltage[i];
sum_y += adc_raw[i];
sum_xy += std_voltage[i] * adc_raw[i];
sum_xx += std_voltage[i] * std_voltage[i];
}
Calibration_t cal;
cal.slope = (n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x * sum_x);
cal.intercept = (sum_y - cal.slope * sum_x) / n;
return cal;
}
// 使用校准参数修正ADC值
float get_calibrated_voltage(uint16_t raw_value, Calibration_t *cal) {
return (raw_value - cal->intercept) / cal->slope;
}
💡 小技巧:校准参数可以存储在Flash或EEPROM中。每次上电时读取,不用每次都重新校准。但建议每隔3-6个月重新校准一次,因为器件会老化。
4.5 知识体系总览
说了这么多,咱们用一张图把整个流程串起来:
这张图把整个流程串起来了。从硬件配置开始,到采样率设定,再到过采样和软件校准,每一步都不可或缺。
最后说一句心里话:ADC校准这件事,看起来简单,做起来全是细节。我见过有人用万用表当标准源,结果万用表本身误差就0.5%,校准完反而更不准了。所以,标准源一定要比被测设备高一个精度等级。这是底线。
好了,这一章的内容就到这儿。代码都给了,原理也讲了,剩下的就是动手去试。记住,纸上得来终觉浅,绝知此事要躬行。