
做电子秤方案最常见的一顿操作是STM32 HX711 5kg称重传感器。但真正把产品做到稳定显示±1g的人都清楚这里面水有多深——HX711的片内稳压在电池供电时表现尚可一接入USB或开关电源读数就开始跳舞程序里再怎么滤波都救不回来。我后来在一款小尺寸计重秤里换成了CS1237配合一维卡尔曼滤波把静止状态下的码值波动从±2000多压到±100以内折算成重量大约是0.2g级别的稳定度。这篇就把这套组合的完整思路写出来从CS1237这颗24位ADC的选型逻辑到硬件布局再到卡尔曼滤波的参数整定和实际踩坑。如果你正在做电子秤、张力计、压力仪表这类低速高精度采集设备这篇应该能省下你至少一周的调试时间。1. 先搞清楚CS1237凭什么能上称重场景1.1 从HX711到CS1237一颗被低估的24位ADCHX711在国内电子秤圈子里几乎是默认选项但用久了你会发现它的别扭之处PGA增益只有32/64/128三档参考电压基本绑定在片内稳压或者AVDD上想外挂基准都不太方便通道只有一个差分对想同时挂两路传感器就得再加一颗芯片没有内置温度传感器温漂补偿全靠外部NTC再走一遍数学建模。CS1237在纸面上是冲着这些问题去的24位Σ-Δ架构PGA从1到128八档可调两个差分输入通道内置1.024V/2.048V参考电压也留有外部参考引脚内部集成了RC振荡器SOT23-6封装外围元件少到令人发指。关键的是和HX711一样也是两线制接口只是时序不同换芯片的成本并不高。下面这张表是我当时选型时整理的对比列出来给大家作参考对比项CS1237HX711ADS1232分辨率24位24位24位PGA增益1/2/4/8/16/32/64/12832/64/1281/2/64/128差分输入通道212内置温度传感器有无无内置基准1.024V/2.048V可外接片内稳压外部基准通信接口两线SPI-like两线SPI封装SOT23-6SOP-16TSSOP-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的满量程差分输入范围大约是±39mV24位码值满量程约±8388608。传感器满量程输出是5V×2mV/V10mV于是5kg对应的码值约为10mV / 39mV × 8388608 ≈ 215万个码折算下来每克约430个码。如果静止时原始码值峰峰值波动能控在±430码以内显示稳定度就是±1g要做到±0.1g级别波动必须小于43码。而CS1237在10SPS、PGA128的条件下典型输入噪声在几十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。如果Vref5VPGA128ADC满量程输入范围±39mV10mV只是用了约26%的量程码值利用率不太够这时可以换PGA16范围变成±312mV量程利用率反而只有3.2%更糟。所以PGA128在这种比例式场景下反而是合适的因为它把10mV抬到量程的1/4附近。反过来如果你用内部2.048V基准PGA128时满量程输入范围是±16mV10mV占到约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偏小时看动态响应放上砝码后输出爬升太慢就加大QQ偏大时看静态波动稳定后显示跳得厉害就减小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就是克平方不要混用公斤和克否则调参会调到怀疑人生。另外浮点数在单片机上要注意开启FPUCortex-M4F以上或者用编译器的软浮点优化否则性能悬殊。4.4 动态称重时的响应问题不止卡尔曼滤波一个变量卡尔曼滤波解决了静止时显示稳定但电子秤实际使用中还有两个动态问题一是放上重物后显示值要快速爬升到最终值二是拿起重物后要快速归零。这些和传感器本身的动态特性也有关系不能全推给滤波器。我的经验是把整个测量回路拆成三个环节传感器机械响应、ADC采样延迟、数字滤波延迟。传感器机械响应通常几十毫秒级ADC在10SPS下采样周期是100ms卡尔曼滤波如果Q调得小还会有几百毫秒的建立时间。三个环节叠加放上砝码到读数稳定的时间可能来到1秒钟这在工业场景里很难接受。应对办法有两个方向。一个是在突变检测的基础上加入快速稳定窗口检测到突变后先以较大的Q跑50~100次滤波更新再切回小Q相当于给滤波器一个冲刺后刹车的动作另一个是提高ADC采样率比如从10SPS提到80SPS让滤波器的输入更新得更密虽然单次噪声变大但配合卡尔曼滤波后最终稳定度反而更好。第二个方向在CS1237上实现很轻松改一下命令字里的速率位就行推荐优先试它。5. 实测数据与调参记录波动从±5g到±0.3g的演进5.1 静置状态下滤波前后码值对比为了让大家对噪声到底有多少有个直观概念我把一组实测数据放出来。测试条件是CS12375V比例式PGA128采样率10SPS5kg/2mV/V传感器空秤静置。从这个表格可以看出原始数据在重量域表现为约±5g晃动码值折算下来的确就是之前估算的±2000码级别。而经过卡尔曼滤波后稳态波动基本被压到±0.3g以内。采样序号原始重量g卡尔曼滤波输出g10.250.032-0.80-0.0131.210.164-2.05-0.1450.640.026-1.53-0.2072.880.328-3.47-0.0691.050.0310-0.380.01单纯看滤波后的值波动范围已经不是肉眼能看出来的了但你们会发现滤波后的输出和真实零点之间还有零点几个克的偏差——这是标定残留的零点误差不是滤波器的锅需要用开机去皮或者更精细的标定去处理。5.2 不同Q/R组合对稳定性和响应速度的影响参数整定过程中我记录了三组Q/R参数的效果。为了方便对比R统一取实测静态方差约4.5单位g²Q分别取0.0001、0.01、1.0单位g²用同一个放砝码动作去测响应时间。Q值静置稳定度±g100g台阶响应时间s特征0.0001±0.1以内约3~4稳但响应极慢0.01±0.3以内约1平衡点1.0±1.5左右约0.3毛糙但跟手这个表的结论很简单Q越小越稳但别小到让响应时间失控。我的最终选择是Q0.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值大概率要重新统计一遍不能吃老本。