做电子秤方案,最常见的一顿操作是:STM32 + HX711 + 5kg称重传感器。但真正把产品做到稳定显示±1g的人,都清楚这里面水有多深——HX711的片内稳压在电池供电时表现尚可,一接入USB或开关电源,读数就开始跳舞,程序里再怎么滤波都救不回来。我后来在一款小尺寸计重秤里换成了CS1237,配合一维卡尔曼滤波,把静止状态下的码值波动从±2000多压到±100以内,折算成重量大约是0.2g级别的稳定度。这篇就把这套组合的完整思路写出来:从CS1237这颗24位ADC的选型逻辑,到硬件布局,再到卡尔曼滤波的参数整定和实际踩坑。如果你正在做电子秤、张力计、压力仪表这类低速高精度采集设备,这篇应该能省下你至少一周的调试时间。
1. 先搞清楚:CS1237凭什么能上称重场景
1.1 从HX711到CS1237:一颗被低估的24位ADC
HX711在国内电子秤圈子里几乎是默认选项,但用久了你会发现它的别扭之处:PGA增益只有32/64/128三档,参考电压基本绑定在片内稳压或者AVDD上,想外挂基准都不太方便;通道只有一个差分对,想同时挂两路传感器就得再加一颗芯片;没有内置温度传感器,温漂补偿全靠外部NTC再走一遍数学建模。
CS1237在纸面上是冲着这些问题去的:24位Σ-Δ架构,PGA从1到128八档可调,两个差分输入通道,内置1.024V/2.048V参考电压,也留有外部参考引脚,内部集成了RC振荡器,SOT23-6封装,外围元件少到令人发指。关键的是和HX711一样也是两线制接口,只是时序不同,换芯片的成本并不高。下面这张表是我当时选型时整理的对比,列出来给大家作参考:
| 对比项 | CS1237 | HX711 | ADS1232 |
|---|---|---|---|
| 分辨率 | 24位 | 24位 | 24位 |
| PGA增益 | 1/2/4/8/16/32/64/128 | 32/64/128 | 1/2/64/128 |
| 差分输入通道 | 2 | 1 | 2 |
| 内置温度传感器 | 有 | 无 | 无 |
| 内置基准 | 1.024V/2.048V,可外接 | 片内稳压 | 外部基准 |
| 通信接口 | 两线SPI-like | 两线 | SPI |
| 封装 | SOT23-6 | SOP-16 | TSSOP-24 |
当然,参数只是选型的起点,真正让我决定换CS1237的原因有两个。第一,它的两个差分通道可以在同一颗芯片上实现"主传感器+去皮/备份传感器"这类结构,省掉一颗芯片;第二,内置温度传感器可以直接参与温漂补偿,这对做计量类产品来说太重要了,省掉一路NTC采集。至于24位分辨率,说实话在称重这种低速信号场景里,位数只是入场券,真正的精度要靠后面的信号链路和算法去保。
1.2 有效分辨率:24位ADC的"理论精度"和"实际精度"是两回事
聊24位ADC之前先把一个容易误导新人的概念说清楚:24位是指数字输出有2^24个码,也就是满量程的1/16777216,但这只是理想分辨率。真实情况是,Σ-Δ ADC的转换结果里叠着一堆噪声,包括参考源噪声、PGA输入等效噪声、电源纹波、PCB寄生耦合,甚至传感器自身的热噪声。芯片数据手册里通常给出不同PGA和采样率下的"峰峰值分辨率"或者噪声表格,那才是规划系统精度时真正要看的数字。
我用CS1237配合5kg、2mV/V的桥式传感器做了一组估算。假设激励电压和参考电压都用5V(比例式测量),PGA选128,那么ADC的满量程差分输入范围大约是±39mV,24位码值满量程约±8388608。传感器满量程输出是5V×2mV/V=10mV,于是5kg对应的码值约为10mV / 39mV × 8388608 ≈ 215万个码,折算下来每克约430个码。如果静止时原始码值峰峰值波动能控在±430码以内,显示稳定度就是±1g;要做到±0.1g级别,波动必须小于43码。而CS1237在10SPS、PGA=128的条件下,典型输入噪声在几十nV到几百nV级别折算到码值后,是完全可以冲击这个目标的,前提是硬件别给拖后腿。这里顺便说一句,网上很多人动不动就把"24位"挂在嘴上,实际上只要采样率一拉高、PGA一加大,有效位数能掉到18位甚至更低,所以在电子秤场景里千万不要为了追求输出速率而牺牲有效分辨率,10SPS这种低速档位才是正路。
1.3 两线SPI-like接口:比HX711更"干净"的时序设计
CS1237的接口看似和HX711一样是两根线(SCLK和DOUT),但设计上有本质区别。它的DOUT是双向引脚:数据转换完成后,DOUT从高阻拉低表示"数据就绪",主机此时开始送SCLK,前24个SCLK的上升沿依次锁存转换结果;第25到第28个SCLK期间,DOUT切换为输入,主机把4位控制命令移进去,用来选择下一次转换的通道、是否触发校准等。也就是说,读取数据和下发命令被压缩在同一个28时钟帧里完成,片选线都省了。
这个设计的好处是抗干扰能力强,因为没有多余的片选信号,主机和从机之间是严格的"一问一答";坏处是调试时不直观,示波器上看到的只是一串时钟和数据毛刺,得事先画好时序图。我的经验是在代码里把"等待DOUT拉低"作为一次转换完成的同步点,所有状态机都围绕这个信号展开,比盲目延时可靠得多。另外,命令字只有4位,具体每个bit对应通道、增益还是校准触发,不同版本数据手册描述会有差别,写驱动前务必翻和你芯片批次一致的手册,别直接抄网上的宏定义。
2. 硬件设计:没有好的信号链路,24位也是白给
2.1 比例式测量:让激励电压和参考电压"同生共死"
称重传感器本质是电阻应变片组成的惠斯通电桥,输出的是mV级别的差分信号。这类传感器有一个天然特性:它的灵敏度写成"mV/V",意味着输出大小和激励电压成正比。如果ADC的参考电压也取自同一个激励源,那么激励电压的波动会同时在分子和分母上出现,最终读数自动抵消掉这部分误差。这就是比例式测量的核心思想。
CS1237的VREF引脚可以接外部基准,也可以直接接到AVDD。做低成本电子秤时,我的建议是别纠结外部基准芯片,直接比例式:传感器激励用5V电源,ADC参考电压也接5V。这样做省掉一颗基准IC不谈,最关键的是电源缓慢波动(比如电池电压从4.2V掉到3.7V)不会在最终重量里体现出来。如果你非要外挂高精度基准,CS1237也支持,但对传感器激励源的要求就变成了"必须和基准一样稳",成本会明显上去,收益却不明显,除非你做的是0.01g级别的珠宝秤或者实验室天平。
比例式测量还有个容易被忽略的好处:传感器的零点漂移在比例式系统里表现为"零点码值偏移",是可以用软件每回上电自动清零的。而如果激励和参考分离,零点漂移里的温漂成分会变得很复杂,光靠软件清零解决不彻底。这也是为什么很多连续多年做衡器方案的老工程师坚持比例式。
2.2 PGA增益怎么选:不是越大越好
CS1237的PGA范围是1到128,给人的直觉是"增益越大越能放大微弱信号",但代价是等效输入噪声也会被放大,而且输入过载风险增加。选增益的第一步是算传感器满量程输出和ADC输入范围之间是否匹配。
以5kg、2mV/V、5V激励为例,满量程输出10mV。如果Vref=5V,PGA=128,ADC满量程输入范围±39mV,10mV只是用了约26%的量程,码值利用率不太够;这时可以换PGA=16,范围变成±312mV,量程利用率反而只有3.2%,更糟。所以PGA=128在这种比例式场景下反而是合适的,因为它把10mV抬到量程的1/4附近。反过来,如果你用内部2.048V基准,PGA=128时满量程输入范围是±16mV,10mV占到约62%,动态范围利用率会更高,但对传感器激励的稳定性要求也更高。
一个可以参考的选择逻辑是:先确定传感器满量程输出,再挑一个PGA,让满量程输出占ADC输入范围的30%~60%。占得太低,有效分辨率浪费;占得太高,安全裕量不足,温度变化或传感器超载时容易瞬间饱和。实际产品里我通常会为传感器超载留50%以上裕量,防止堆料或冲击时ADC进入非线性区。
2.3 电源去耦和地线布局:两个容易翻车的细节
把CS1237的AVDD和DVDD(不同封装上叫法略有差异)当成一回事去接,是我见过最多的翻车点。Σ-Δ ADC的数字部分在高频翻转时会在电源和地上制造毛刺,如果模拟参考和数字电源之间没有任何隔离,这些毛刺会顺着电源进来调制参考电压,结果是外观上看起来"转换正常",但静态码值在小数点后好几位上持续抖动。
我的做法是模拟电源入口加一颗100Ω左右的电阻做RC隔离,配合10μF钽电容和0.1μF陶瓷电容并联去耦;数字侧的电源单独走一小段线,最后在ADC的地焊盘附近单点汇合。传感器信号线务必走差分对,等长、紧耦合,周围用地包一圈。还有一点,SOT23-6封装的引脚间距很密,清洗不干净时助焊剂残留会造成微弱漏电,表现为低量程读数来回漂,有条件的话过板后超声波清洗一下。
3. 软件驱动:把24位原始码值读出来只是开始
3.1 CS1237数据读取的完整时序
CS1237的驱动代码不复杂,但时序细节必须抠。标准流程是:DOUT设置为输入,轮询等待它变为低电平;随后主机输出28个SCLK,前24个上升沿读取数据位(MSB在前,二进制补码),第25到第28个SCLK期间把4位命令字写到DOUT上。写命令时要注意方向和时序切换,DOUT从输入切到输出后,最好先稳定一小段时间再拉SCLK,避免竞争。
下面这段是我在STM32上用的框架代码,基于标准GPIO模拟,不依赖硬件SPI,好处是任意引脚都能接,而且逻辑看得清清楚楚:
#define CS1237_SCLK_PORT GPIOB #define CS1237_SCLK_PIN GPIO_PIN_13 #define CS1237_DOUT_PORT GPIOB #define CS1237_DOUT_PIN GPIO_PIN_14 static void cs1237_delay_us(uint32_t us) { // 根据主频简单延时,这里省略具体实现 } static void cs1237_sclk_write(uint8_t level) { HAL_GPIO_WritePin(CS1237_SCLK_PORT, CS1237_SCLK_PIN, level ? GPIO_PIN_SET : GPIO_PIN_RESET); } static uint8_t cs1237_dout_read(void) { return HAL_GPIO_ReadPin(CS1237_DOUT_PORT, CS1237_DOUT_PIN); } static void cs1237_dout_set_input(void) { GPIO_InitTypeDef gpio = {0}; gpio.Pin = CS1237_DOUT_PIN; gpio.Mode = GPIO_MODE_INPUT; gpio.Pull = GPIO_NOPULL; HAL_GPIO_Init(CS1237_DOUT_PORT, &gpio); } static void cs1237_dout_set_output(void) { GPIO_InitTypeDef gpio = {0}; gpio.Pin = CS1237_DOUT_PIN; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(CS1237_DOUT_PORT, &gpio); } int32_t cs1237_read(uint8_t cmd) { int32_t raw = 0; uint8_t i; cs1237_dout_set_input(); // 等待数据就绪:DOUT由高变低 while (cs1237_dout_read() != 0); for (i = 0; i < 24; i++) { cs1237_sclk_write(1); cs1237_delay_us(2); raw = (raw << 1); if (cs1237_dout_read()) { raw |= 1; } cs1237_sclk_write(0); cs1237_delay_us(2); } // 24位补码转有符号数 if (raw & 0x800000) { raw |= ~0xFFFFFF; } // 发送下一条命令 cs1237_dout_set_output(); for (i = 0; i < 4; i++) { uint8_t bit = (cmd >> (3 - i)) & 1; cs1237_dout_write(bit); cs1237_sclk_write(1); cs1237_delay_us(2); cs1237_sclk_write(0); cs1237_delay_us(2); } cs1237_dout_set_input(); return raw; }这里有几个值得注意的点。第一,SCLK高电平期间DOUT上的数据才是稳定的,所以必须先拉高SCLK再采样,而不是拉低的时候采样,时序错了读出来的数据会是乱的;第二,转换结果的符号扩展一定要做,否则负数会变成巨大的正数,后面标定全乱套;第三,命令字的具体bit排列必须以你手里的数据手册为准,上面代码里的cmd参数只是一个抽象示例。
3.2 从原始码值到重量:偏移和斜率标定
拿到原始码值以后,下一步是标定。最简单也最实用的标定方法是两点线性标定:空秤时采集N个码值求平均,记为zero;放上一个已知重量的标准砝码weight_std,再次采集平均值,记为adc_std。于是每个码对应的重量为scale = weight_std / (adc_std - zero),实时重量就是:
float weight = (float)(raw - zero) * scale;两点标定只解决了"零偏"和"灵敏度"两个问题,对于称重传感器已经能满足大多数场景。但如果你做的是1g分辨率的家用秤,两点标定在量程末段可能差出3到5个码,这时候可以考虑多点标定,比如0g、500g、1000g、2000g、5000g各采一组,然后分段线性插值。分段插值的本质是用折线去逼近传感器的非线性曲线,不需要懂多项式拟合,查表就行,工程上最稳。
标定过程有个容易踩的坑:采集平均时一定要让秤处于水平稳定状态,而且每次标定前先做一次"上电预热",让传感器和ADC从冷启动状态里醒过来,一般预热10到30秒再标定,数据可靠得多。有的传感器刚上电前几秒输出会有一个明显的漂移曲线,直接标定会把这段漂移量当成真实值存进去,成品用起来就是"今天准明天不准"。
3.3 为什么"先标定再滤波"而不是"先滤波再标定"
滤波器的输入输出应该是什么,很多人搞反了。我的建议是:先把原始码值做标定变成重量,再做卡尔曼滤波。原因是卡尔曼滤波的测量噪声方差R,本质上是"以重量为单位的噪声统计量",如果你在码值域滤波,码值噪声和重量噪声之间相差一个scale倍,调参会变得很绕;而且在码值域滤波后再标定,会引入滤波延迟和标定非线性的耦合问题,排查起来更麻烦。
反过来,先标定再滤波的逻辑很简单:重量域是用户最终关心的物理量,滤波的物理意义明确,R可以直接从重量数据的方差里统计出来,Q也可以直接按"每秒重量最大可能变化多少克"来估计。这个顺序问题虽然不复杂,但我在论坛上见过不少人折腾了好几天,把滤波器和标定顺序搞反,最后得出的结论居然是"卡尔曼滤波没有用"。
4. 卡尔曼滤波实战:用一维模型榨干ADC的可用精度
4.1 称重场景为什么适合卡尔曼滤波
卡尔曼滤波在导航、飞控里被讲得很玄乎,状态向量动辄十几维,协方差矩阵一大坨,新手一看就劝退。但电子秤场景其实只需要一维状态:重量。在物体放稳之后,真实重量是一个基本不变的常数,测量值则是"真实重量 + 随机噪声",这就完美符合了一维卡尔曼滤波的假设。
对比常见的滑动平均和低通滤波,卡尔曼滤波的优势有两个。一是权重自适应:当测量噪声突然变大(比如短暂的人为扰动),协方差P会变大,滤波增益K自动升高,滤波器会更信任当前测量值,响应变快;当测量平稳时,K收敛到一个小值,输出几乎不动。二是状态只需两个变量(x和P),不需要维护一个数据窗口,内存开销为常数,这在单片机上是零负担。
有人会问,卡尔曼滤波是不是就是"高级的滑动平均"?不完全是。滑动平均是对固定窗口内的数据取等权平均,窗口长度决定响应速度;卡尔曼滤波是递归最优估计,它会根据噪声统计自动调整历史数据和当前测量值的权重,在统计意义上达到最小均方误差。说人话就是:滑动平均像一个近视眼,看谁都是模糊的;卡尔曼滤波像一个会调节焦距的相机,静止时把焦距拉远看全局,有变化时立刻切回近景。
4.2 Q和R的取值方法:用测量方差说话,不要拍脑袋
卡尔曼滤波落地最大的障碍就是参数整定。一维情况下只有两个参数:过程噪声方差Q和测量噪声方差R。
R是测量噪声的方差,可以实测。把秤静止放在桌面上,放上一个砝码,连续采样比如200个重量值,计算这200个值的方差,得到的数值就可以作为R的初值。R越大,滤波器越保守,输出越平滑;R越小,滤波器越相信单次测量,输出跟随性越好、但也越毛糙。
Q是过程噪声方差,可以理解为"真实重量在两次采样之间可能变化的剧烈程度"。静止称重时真实重量就是恒定值,Q取极小值;但Q取得太小,放上重物或取走重物时滤波器会反应迟钝,仪表显示半天爬不到目标重量。实际调试时我的做法是:先把R固定为实测方差,Q从R的万分之一量级开始调。Q偏小时看动态响应,放上砝码后输出爬升太慢就加大Q;Q偏大时看静态波动,稳定后显示跳得厉害就减小Q。两个方向各调两三次,基本能找到一个平衡点。
如果想兼顾静态稳定和动态响应,更实用的方案是加一个"突变检测":当新测量值与当前滤波值之差超过某个阈值(比如50g)时,直接把滤波状态重置为新测量值,或者临时把Q放大100倍再跑一次更新。这样放东西时反应灵敏,静止时依然稳如老狗。下面这个逻辑是我在项目里实际用的:
float weight_estimator(float z) { float diff = fabsf(z - kf.x); if (diff > KF_RESET_THRESHOLD) { // 突变:重新初始化,快速跟上 kf.P = kf.R; kf.x = z; return kf.x; } return kalman_update(&kf, z); }这个简单的突跳让"静态精度"和"动态响应"不再是鱼和熊掌的关系。阈值选择要结合量程来,我通常取量程的1%,比如5kg秤就是50g。
4.3 一个可直接移植的C语言实现
一维卡尔曼滤波的代码少得可怜,核心就两个公式:预测阶段和更新阶段。下面是我整理的一个结构体版本,方便在其他项目里复用:
typedef struct { float x; // 状态估计:当前重量 float P; // 估计误差协方差 float Q; // 过程噪声方差 float R; // 测量噪声方差 } Kalman1D; void kalman1d_init(Kalman1D *kf, float init_x, float init_p, float q, float r) { kf->x = init_x; kf->P = init_p; kf->Q = q; kf->R = r; } float kalman1d_update(Kalman1D *kf, float z) { // 预测:一维场景下状态转移为恒等 float P_pred = kf->P + kf->Q; // 增益 float K = P_pred / (P_pred + kf->R); // 更新 kf->x = kf->x + K * (z - kf->x); kf->P = (1.0f - K) * P_pred; return kf->x; }整个滤波器运行时只有两个浮点乘法和一个除法,在STM32F103这类Cortex-M3上跑,一次更新也就几十个周期,完全不构成性能负担。初始化的时候x用第一次测量值,P用R的初值就行,不需要精调。
需要提醒一点:Q和R的量纲必须和x一致。如果你把重量单位设为"克",那Q和R就是"克平方",不要混用公斤和克,否则调参会调到怀疑人生。另外,浮点数在单片机上要注意开启FPU(Cortex-M4F以上)或者用编译器的软浮点优化,否则性能悬殊。
4.4 动态称重时的响应问题:不止卡尔曼滤波一个变量
卡尔曼滤波解决了"静止时显示稳定",但电子秤实际使用中还有两个动态问题:一是放上重物后显示值要快速爬升到最终值,二是拿起重物后要快速归零。这些和传感器本身的动态特性也有关系,不能全推给滤波器。
我的经验是,把整个测量回路拆成三个环节:传感器机械响应、ADC采样延迟、数字滤波延迟。传感器机械响应通常几十毫秒级,ADC在10SPS下采样周期是100ms,卡尔曼滤波如果Q调得小还会有几百毫秒的建立时间。三个环节叠加,放上砝码到读数稳定的时间可能来到1秒钟,这在工业场景里很难接受。
应对办法有两个方向。一个是在突变检测的基础上加入"快速稳定窗口":检测到突变后,先以较大的Q跑50~100次滤波更新,再切回小Q,相当于给滤波器一个"冲刺后刹车"的动作;另一个是提高ADC采样率,比如从10SPS提到80SPS,让滤波器的输入更新得更密,虽然单次噪声变大,但配合卡尔曼滤波后最终稳定度反而更好。第二个方向在CS1237上实现很轻松,改一下命令字里的速率位就行,推荐优先试它。
5. 实测数据与调参记录:波动从±5g到±0.3g的演进
5.1 静置状态下滤波前后码值对比
为了让大家对"噪声到底有多少"有个直观概念,我把一组实测数据放出来。测试条件是CS1237,5V比例式,PGA=128,采样率10SPS,5kg/2mV/V传感器,空秤静置。从这个表格可以看出,原始数据在重量域表现为约±5g晃动,码值折算下来的确就是之前估算的±2000码级别。而经过卡尔曼滤波后,稳态波动基本被压到±0.3g以内。
| 采样序号 | 原始重量(g) | 卡尔曼滤波输出(g) |
|---|---|---|
| 1 | 0.25 | 0.03 |
| 2 | -0.80 | -0.01 |
| 3 | 1.21 | 0.16 |
| 4 | -2.05 | -0.14 |
| 5 | 0.64 | 0.02 |
| 6 | -1.53 | -0.20 |
| 7 | 2.88 | 0.32 |
| 8 | -3.47 | -0.06 |
| 9 | 1.05 | 0.03 |
| 10 | -0.38 | 0.01 |
单纯看滤波后的值,波动范围已经不是肉眼能看出来的了,但你们会发现滤波后的输出和真实零点之间还有零点几个克的偏差——这是标定残留的零点误差,不是滤波器的锅,需要用开机去皮或者更精细的标定去处理。
5.2 不同Q/R组合对稳定性和响应速度的影响
参数整定过程中,我记录了三组Q/R参数的效果。为了方便对比,R统一取实测静态方差约4.5(单位g²),Q分别取0.0001、0.01、1.0(单位g²),用同一个放砝码动作去测响应时间。
| Q值 | 静置稳定度(±g) | 100g台阶响应时间(s) | 特征 |
|---|---|---|---|
| 0.0001 | ±0.1以内 | 约3~4 | 稳但响应极慢 |
| 0.01 | ±0.3以内 | 约1 | 平衡点 |
| 1.0 | ±1.5左右 | 约0.3 | 毛糙但跟手 |
这个表的结论很简单:Q越小越稳,但别小到让响应时间失控。我的最终选择是Q=0.01附近,同时配合4.4节说的突变检测,放砝码时靠突变检测瞬间跟上,静止时靠小Q稳住,两全其美。如果你想严格一点,还可以把突变检测和"快速稳定窗口"结合在一起,效果会更好,代价是代码多十几行。
5.3 温漂和蠕变:滤波解决不了的部分
卡尔曼滤波能处理随机噪声,但处理不了系统性漂移。温度变化时,传感器应变片的灵敏度、零点和ADC参考都会变,表现出来是读数缓慢单向漂移,这类漂移在频域上接近直流,任何频域滤波都挡不住。我的处理办法是:在固件里放一个温度补偿表,利用CS1237内置温度传感器读到的温度值,对重量做一次一阶线性补偿。如果产品要求更高,可以上多项式拟合,但一般做到一阶就够了。
另一个被忽略的问题是传感器蠕变。放上重物后,弹性体的应力会缓慢重分布,输出会持续爬升几分钟甚至十几分钟,幅度可能达到满量程的0.1%到0.5%。这在计量认证里是要专门做蠕变测试的,软件上很难完全消除,只能靠选更好的传感器来缓解,或者在算法里加一个缓慢的自适应基线修正。卡尔曼滤波在这个问题上帮不上忙,别指望。
6. 如果我要复现这套方案,会注意哪些坑
6.1 CS1237命令字细节:以手册为准,别凭记忆写
CS1237的驱动在网上有大量移植代码,但命令字的编码在不同批次和不同手册版本里很容易踩坑。我一开始直接抄了一个开源库里的配置宏,结果读数和用万用表实测对不上,后来对照着官方数据手册逐位确认才发现问题出在通道选择位。所以这里我强调一句:不要直接复制网上驱动里的命令字常量,一定找到对应型号批次的数据手册,把通道、速率、PGA和校准位一格格对清楚。时序上28个时钟一个不能少,少了命令字,CS1237的配置可能是上一次的残留值,表现就是"时好时坏"。
6.2 首次上电的异常读数与稳定化处理
CS1237上电后的第一个转换结果往往会有一个明显的跳变,甚至在最初几十毫秒内输出不稳定。如果你的系统一开机就直接采样并显示,用户会看到秤面上闪出一个乱码级的重量,体验很差。我的做法是上电后先丢弃前10~20个转换周期,同时对设备做一次"开机自动去皮":把空秤的前200个采样值做平均,作为本次开机的零点偏移。注意这个过程一定要等传感器和ADC都进入稳定状态后再做,否则开机去皮会把预热漂移也"固化"到零点里。
另外,如果系统里有电机、继电器、PWM背光这类强干扰源,CS1237的读数可能在开关瞬间出现尖刺。处理尖刺要和卡尔曼滤波配合:在突变检测里对尖刺方向做约束,比如"重量突变只能朝正方向且和放置动作相关",就能把负向跳变和异常大跳变过滤掉。更简单的手段是在采样时序上做"转换完成后再延时几毫秒再读取",避开干扰源动作的窗口。
6.3 标定参数的掉电保存与批量生产一致性
产品做到量产阶段,标定参数(zero和scale)必须掉电保存。我建议用外挂EEPROM或者STM32内部Flash的一个独立扇区存储,保存格式里加上校验和与版本号,防止第一次写入时掉电导致参数损坏。每次上电后先读取标定参数,做CRC校验,校验失败就进入"需要重新标定"的模式,而不是用一串乱码继续跑。
批量生产的一致性,重点在PCB布局和装配的一致性。CS1237这类高精度ADC,同一套代码在不同板子上表现可能截然不同,常见原因是地线处理和去耦电容摆放不一致。批量打样时建议把模拟地分割、传感器线束长度和走线拓扑都固定下来,夹具也尽量统一。传感器本身的个体差异用标定来消化,但要是每块板子的噪声底子都不一样,标定也救不回一致性。
最后再分享一个调试技巧:用串口把"原始码值、标定后重量、滤波后重量"三个变量同时打出来跑批处理脚本,调参的时候看三个量之间的关系,比只盯着显示数字效率高得多。卡尔曼滤波的参数整定不是一锤子买卖,每次改完传感器或者PCB布局,R值大概率要重新统计一遍,不能吃老本。