首页 / 资讯中心 / 文章详情

基于CS1237与卡尔曼滤波的高精度电子秤设计与实现

基于CS1237与卡尔曼滤波的高精度电子秤设计与实现 ★ FEATURED ARTICLE
从HX711换成CS1237之后我这个简易电子秤的实测数据才算真正能看空载稳定在±0.05g以内放上1g砝码能稳定识别满载500g时线性偏差不超过0.15g。这中间发挥关键作用的一方面是CS1237这颗24位ADC本身的底噪控制另一方面是我在代码里加的一维卡尔曼滤波。这篇文章就把硬件连接、驱动源码、滤波算法和标定流程一次性讲清楚适合想自己做高精度电子秤、或者正在做称重类嵌入式项目的朋友参考。先说明一下这个项目用的核心链路是电阻应变式称重传感器惠斯通电桥→ CS1237 24位Sigma-Delta ADC → 单片机读取原始码值 → 卡尔曼滤波平滑 → 两点标定换算成重量。整套方案成本很低传感器加ADC芯片加起来十几块钱但精度能做到实验室台秤的入门水平。下面从选型逻辑开始逐步拆解每个环节。1. 从HX711换到CS1237精度瓶颈到底卡在哪里1.1 称重信号的真实幅度小得超乎直觉很多朋友第一次做电子秤习惯性选HX711因为网上资料多、模块便宜。但如果你目标是“1g以下分辨率的稳定性”HX711的底噪会让你很痛苦。先算一笔账常见的悬臂梁称重传感器灵敏度是1mV/V也就是在5V激励电压下满量程输出只有5mV。如果你用500g量程的传感器那么1g对应的电压变化是5mV / 500 10µV。也就是说要分辨到0.1gADC至少要能分辨1µV级别的信号变化。HX711标称24位但实际有效位数ENOB在10Hz输出速率下大概只有20位左右折合噪声峰值在0.5µV到1µV量级刚好卡在0.1g分辨率的边缘。而CS1237同样是24位Sigma-Delta架构但内置了低噪声PGA可编程增益放大器在增益128倍、10Hz速率下有效分辨率能做到20位以上噪声典型值比HX711低一个档次实际称重时读数明显更稳。1.2 CS1237与HX711的关键参数对比参数CS1237HX711ADC位数24位24位实际有效约20位内置PGA增益1/2/64/12832/64/128数据速率10Hz / 40Hz / 640Hz10Hz / 80Hz内置系统校准支持可发命令触发无需软件校准基准电压AVDD比例测量AVDD比例测量通信接口仿SPIDOUT/SCLK仿SPIDOUT/PD_SCK价格约1.5-2元约1元从表格能看出来两者都是比例测量架构但CS1237的数据速率选项更细尤其是10Hz低速模式对称重很有价值——Sigma-Delta ADC的速率越低数字滤波越充分噪声越小。另外CS1237支持片上校准命令可以在硬件层面消除一部分增益误差和偏移误差这对批量生产很有用但DIY场景我们主要还是用软件两点标定。1.3 比例测量为什么电源波动不会被放大CS1237的基准电压直接取自AVDD而传感器激励电压也是AVDD。这意味着电源电压升高10%传感器输出信号会变大10%但ADC的满量程参考电压也变大10%两者抵消最终码值不受电源波动影响。这就是“比例测量”的核心价值。实际制作时只要保证AVDD纹波不要太大电源波动对称重结果的影响就很小。这也是为什么这类ADC不需要外接精密基准源的原因。2. 硬件电路和机械安装容易被低估的三个坑2.1 惠斯通电桥与四线制接线称重传感器内部是四个应变片组成的惠斯通全桥引出来四根线E激励正、E-激励负、S信号正、S-信号负。接线时E接AVDDE-接AGNDS接CS1237的VINPS-接VINN。需要注意的是信号线非常微弱尤其是小量程传感器空载时S和S-之间的差分电压接近0V此时共模干扰是主要威胁。我的做法是传感器线缆用双绞屏蔽线屏蔽层单端接AGND不要两端都接地避免形成地环路。我踩过一个很典型的坑把屏蔽层接在MCU的数字地DGND上结果读数出现周期性的波动频率和PWM调光LED一致。后来把屏蔽层改接到AGND让模拟地和数字地单点相连噪声立刻降下去。这个细节在洞洞板方案里尤其重要模拟走线和数字走线尽量分开AGND和DGND在电源输入端单点汇合。2.2 CS1237供电与去耦电路CS1237有独立的DVDD和AVDD引脚。很多DIY方案偷懒直接用一个3.3V同时供给也能工作但精度会受影响。推荐电路是主控3.3V先经过一个磁珠或者100Ω电阻分成两路一路直接给DVDD另一路再并一个10µF钽电容和0.1µF陶瓷电容后给AVDD。这样数字开关噪声不容易串进模拟电源。VINP和VINN输入端各加一个100pF到AGND的滤波电容滤除高频干扰但注意不要加太大否则会拖慢Sigma-Delta调制器的输入建立时间。如果传感器线比较长超过30cmVINP和VINN之间还可以并一个1000pF电容做差分滤波。这里有个取舍电容越大抗高频干扰越好但信号建立越慢。称重本身是准静态信号所以稍微大一点没关系实测1000pF也不会明显影响读数速度。2.3 机械安装的应力问题比电路更致命这一点我必须放在显著位置提醒电子秤精度做不上去很多时候不是ADC的问题而是机械结构把应力吃掉了。传感器两端各有固定孔一端锁在底座上一端锁在秤盘支撑架上。固定螺丝不能拧太死否则传感器本体发生形变输出零点会偏移而且偏移量随温度变化。我用的是M3螺丝扭矩控制在2kgf·cm左右对角顺序锁紧锁完再测零点反复调整到空载码值稳定。另外秤盘和支撑架之间要用球头或者软性连接保证重力垂直传递到传感器上。千万不要把秤盘和外壳用螺丝刚性锁死否则你按一下外壳读数就会跳。整个称重平台必须和外壳完全分离中间留缝隙这是机械秤的基本常识但很多DIY作品都会忽略。3. CS1237驱动源码时序、寄存器与实测注意3.1 通信时序DOUT/SCLK仿SPI的工作逻辑CS1237只有两根线DOUT数据输出和SCLK时钟输入典型的主从方式。每次转换完成后DOUT会从高电平拉低表示“数据已准备好”。主机检测到DOUT为低后开始发送24个SCLK脉冲每一个SCLK的上升沿读取一位数据高位在前读出24位有符号数。第25个脉冲用于读取状态位或者触发配置操作第26个脉冲结束本次通信。这个时序和HX711最大的区别在于HX711你随时可以拉时钟读数据而CS1237必须等DOUT拉低才表示新数据准备好。如果你不管DOUT直接发时钟读出来的可能是旧数据甚至是移位错乱的数据。所以驱动代码的核心逻辑就是阻塞等待DOUT变低然后快速读取避免下一次转换覆盖当前数据。3.2 驱动代码实现基于STM32标准库其他平台可移植#include cs1237.h #define CS1237_SCLK_H GPIO_SetBits(CS1237_GPIO_PORT, CS1237_SCLK_PIN) #define CS1237_SCLK_L GPIO_ResetBits(CS1237_GPIO_PORT, CS1237_SCLK_PIN) #define CS1237_DOUT_READ() GPIO_ReadInputDataBit(CS1237_GPIO_PORT, CS1237_DOUT_PIN) // 上电初始化给一串时钟脉冲让芯片复位 void CS1237_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStruct.GPIO_Pin CS1237_SCLK_PIN; GPIO_InitStruct.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(CS1237_GPIO_PORT, GPIO_InitStruct); GPIO_InitStruct.GPIO_Pin CS1237_DOUT_PIN; GPIO_InitStruct.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(CS1237_GPIO_PORT, GPIO_InitStruct); CS1237_SCLK_L; delay_ms(10); for (uint8_t i 0; i 25; i) { CS1237_SCLK_H; CS1237_SCLK_L; delay_us(2); } } // 读取一次转换结果返回24位有符号数 int32_t CS1237_Read(void) { uint32_t raw 0; uint8_t i; // 等待DOUT拉低表示数据准备好 uint16_t timeout 0; while (CS1237_DOUT_READ() timeout 10000) { timeout; delay_us(10); } if (timeout 10000) { return 0; // 超时说明通信异常 } // 读取24位数据高位在前 for (i 0; i 24; i) { CS1237_SCLK_H; delay_us(1); raw 1; if (CS1237_DOUT_READ()) { raw | 1; } CS1237_SCLK_L; delay_us(1); } // 第25个脉冲读取状态位可忽略 CS1237_SCLK_H; delay_us(1); CS1237_SCLK_L; delay_us(1); // 第26个脉冲结束本次通信 CS1237_SCLK_H; delay_us(1); CS1237_SCLK_L; // 数据是二进制补码24位有符号数转换 if (raw 0x800000) { raw | 0xFF000000; } return (int32_t)raw; } // 写配置寄存器增益、速率、缓冲器开关 void CS1237_WriteConfig(uint8_t config) { uint8_t i; // 先等DOUT拉低 while (CS1237_DOUT_READ()); // 读掉一次数据保证后续时序同步 for (i 0; i 24; i) { CS1237_SCLK_H; delay_us(1); CS1237_SCLK_L; delay_us(1); } // 第25个脉冲表示写寄存器 CS1237_SCLK_H; delay_us(1); CS1237_SCLK_L; delay_us(1); // 8位配置数据从高位开始发送 for (i 0; i 8; i) { CS1237_SCLK_L; delay_us(1); if (config 0x80) { CS1237_SCLK_H; } else { CS1237_SCLK_L; } delay_us(1); CS1237_SCLK_H; delay_us(1); CS1237_SCLK_L; delay_us(2); config 1; } }这段代码里我用了最保险的阻塞方式每个时钟脉冲之间加了1µs延时。在实际使用中CS1237对时序要求并不苛刻只要满足最小高低电平宽度即可所以即使不用延时也能跑。但我建议保留延时因为单片机主频不同、IO翻转速度不同加延时可以大大提高程序在不同平台间的可移植性。3.3 配置寄存器和实测注意事项CS1237的关键配置项有三个输出速率SPEED、PGA增益、输入缓冲器BUF。推荐配置是10Hz速率、PGA128、BUF关闭。这样设置的原因是称重信号本身变化慢10Hz足够PGA128把微伏级信号放大到ADC能充分利用的量程BUF关闭可以降低输入端的静态功耗和噪声但代价是输入阻抗相对低对于传感器这种低内阻信号源没有影响。需要特别提醒的是更改配置后要清空一次数据或者丢弃配置后的第一次读数。因为Sigma-Delta滤波器需要几个转换周期才能稳定到新配置下的真实值我实测大概需要3到5个周期也就是0.3到0.5秒。如果你改了配置立刻拿去标定读数会明显偏大。4. 卡尔曼滤波建模与调参让读数稳住而不是变迟钝4.1 原始数据到底有多“脏”CS1237在10Hz速率、PGA128下空载原始码值并不是恒定的而是在某个中心值附近上下波动波动范围大概在±20个LSB左右。按满量程500g、24位ADC计算每个LSB对应的重量约0.00003g其实不是这么算的因为码值变化对应的是传感器输出电压折合到重量还要除以PGA和传感器灵敏度。在5V激励、1mV/V灵敏度、PGA128的条件下满量程5mV信号经过128倍放大后是640mVADC的满量程是AVDD约5V所以ADC利用率约12.8%真实分辨率大约等于500g / (2^24 * 12.8%) ≈ 0.009g。也就是说理论上能分辨0.01g级别的变化但噪声会淹没这个分辨率。这时如果直接显示原始重量你会看到小数点后一位的数字不停跳动完全没法看。最简单的办法是滑动平均滤波但滑动窗口开大了读数响应变慢放上砝码后要好几秒才能稳定窗口开小了滤不干净。卡尔曼滤波的优势在于它能根据你设定的噪声模型自适应地决定“更相信当前估计值还是更相信新测量值”兼顾稳定性和响应速度。4.2 一维卡尔曼滤波的建模过程对于称重这种准静态过程可以把状态量定义为“当前重量x”并且假设重量在一段时间内基本不变。状态方程就是x_k x_(k-1) w这里w是过程噪声代表重量可能发生变化比如你放上砝码的那一瞬间。观测方程是z_k x_k v其中v是测量噪声代表ADC读数的随机波动。卡尔曼滤波的核心是根据过程噪声和测量噪声的协方差Q和R动态计算卡尔曼增益K然后在“预测”和“更新”之间做最优折中。具体公式就五条但在这个场景里可以大幅简化。因为状态转移矩阵是1所以预测步骤只剩下P_pred P Q更新步骤是K P_pred / (P_pred R) x x K * (z - x) P (1 - K) * P_pred这里x就是滤波后的重量z是ADC原始码值或者标定后的瞬时重量P是误差协方差Q是过程噪声方差R是测量噪声方差。整个滤波过程只需要四个浮点数变量在单片机上几乎没有负担。4.3 卡尔曼滤波源码实现typedef struct { float x; // 滤波后的估计值 float P; // 误差协方差 float Q; // 过程噪声 float R; // 测量噪声 float K; // 卡尔曼增益 } 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; kf-K 0.0f; } float Kalman1D_Update(Kalman1D *kf, float z) { // 预测 float P_pred kf-P kf-Q; // 更新 kf-K P_pred / (P_pred kf-R); kf-x kf-x kf-K * (z - kf-x); kf-P (1.0f - kf-K) * P_pred; return kf-x; }这段代码必须配合标定后的重量使用不能在原始ADC码值上直接滤波。原因很简单原始码值的噪声方差在称重范围内基本不变但经过标定换算后不同量程段的等效重量噪声可能不一样。不过如果你只在固定量程内使用直接在码值上滤波再换算也是可以的效果差别不大。4.4 调参经验Q和R怎么定先说R测量噪声方差的确定方法空载状态下连续采集100个原始重量值计算方差这个方差就是R的初始值。我实测下来PGA128、10Hz速率下500g量程传感器的测量方差大约在0.02g²左右也就是标准差约0.14g。再说Q过程噪声方差Q越大滤波器越“信任”新测量值响应越快但输出越毛糙Q越小输出越平滑但放上砝码后读数要更久才能稳定。我的经验是从R的千分之一开始调。如果R0.02那么Q可以先取0.00002。放上500g砝码测试如果稳定时间超过3秒就把Q调大一个量级如果稳定后峰峰值超过0.1g就把Q调小。我自己最终用的参数是Q0.00005R0.02。实测放上200g砝码后约1秒内读数稳定稳定后峰峰值在±0.05g以内。如果你对响应速度要求更高可以用一个“带通”思路正常称重时用较小Q保证稳定检测到突变新息大于某个阈值时临时把Q放大等读数稳定后再恢复。这个思路相当于一个自适应卡尔曼我后面在扩展思路里会讲。5. 标定流程与精度实测从ADC码值到“克”5.1 两点线性标定的完整步骤传感器和ADC都是线性器件但零点和增益都有偏差所以必须标定。最简单的两点标定法如下第一步上电后等待10秒让ADC内部滤波器和机械结构充分稳定。第二步空载状态下采集100次滤波后的码值取平均记为zero。第三步放上标准砝码我用的是一组100g标准砝码你也可以用已知重量的物体比如手机加充电宝配合精密秤测量后作为参考。等读数稳定后采集100次取平均记为span_raw。注意这个span_raw一定是在卡尔曼滤波之后的值标定和实际使用要用同一套滤波参数。第四步计算斜率和截距weight (filtered_raw - zero) / (span_raw - zero) * known_weight在代码里不需要每次计算除法直接预先算出两个系数float scale known_weight / (span_raw - zero); float offset zero; // 之后每次称重 float weight (filtered_raw - offset) * scale;如果还需要去皮功能就在此基础上增加一个tare变量float display_weight (filtered_raw - offset) * scale - tare;5.2 多档位实测数据标定完成后我用标准砝码做了几组测试每次等到滤波读数稳定后记录最大值和最小值连续观察20秒结果如下实际砝码重量(g)显示重量(g)偏差(g)20秒内峰峰值(g)00.010.010.0410.98-0.020.0555.020.020.061010.010.010.065050.000.000.0810099.98-0.020.08200200.040.040.10500500.110.110.15这个结果对于DIY电子秤来说已经相当不错了。可以看到0到100g区间内偏差都在±0.03g以内到500g满量程时偏差到0.11g这可能是传感器本身的非线性造成的不是ADC的问题。如果要做更高精度的秤可以考虑做多点标定比如每100g取一个校准点然后用分段线性插值或者二次多项式拟合。5.3 温漂和时间漂移的处理电子秤有个让人头疼的问题刚上电和开机半小时后的零点不一样。这是因为应变片受温度影响会产生零点漂移ADC的内部参考电压也会随温度缓慢变化。我实测过冷机开机时标定好的零点半小时后最多能漂移0.3g左右。应对办法有两个层面软件上可以在程序里做一个开机自动归零上电后等待3秒如果秤盘上有东西就提示拿去然后自动把当前读数设为tare。更稳妥的是每隔一段时间检测一次“长期未变化”状态如果读数在较长时间内保持稳定就自动微调零点。但要注意不要做“读数稳定就强制归零”的傻瓜逻辑否则你称完一个物体后放空秤盘它会把这个空秤状态当成零导致下次称重多出一个负值。硬件上尽量把传感器和电路板放在同一个温区避免局部加热。另外传感器底下垫一层薄橡胶垫既能减震也能减少底座传过来的温度冲击。6. 真实使用中的漂移噪声与避坑清单6.1 我实际踩过的几个坑第一个坑是数据突然跳到满量程。排查了半天发现是传感器信号线接触不良S那根线在螺丝接线端子处只压住了一小段铜丝稍微晃动一下就断开。建议所有信号线都直接用焊接加热缩管固定不要用端子排插接。第二个坑是卡尔曼滤波启动时如果秤盘上已经放了东西滤波初值x选错了会导致前面几十个读数都偏小然后慢慢爬升到真实值。解决办法是系统启动后先做一次空载读数作为初值x并且在上电提示“请清空秤盘”。如果是需要称重时再放物体的场景这个影响不大但如果你做的是连续称重设备这个细节会导致每次上电都有一段“假爬坡”。第三个坑是CS1237数据读出偶尔会错位。我用逻辑分析仪抓过波形发现是在读取数据的过程中被定时器中断打断了导致SCLK高电平时间远超过预期芯片状态机跑飞。后来我在读取CS1237的临界区加了关中断保护问题解决。如果不用关中断也可以在读取时把延时做精确确保SCLK周期的一致性。6.2 抗脉冲干扰的增强方案在实际使用中除了随机噪声还有一类脉冲干扰比如你拿东西不小心碰了一下秤盘或者桌子被人撞了一下卡尔曼滤波对这类突变虽然有一定抑制作用但响应会比较怪——它会猛地朝干扰方向偏一下然后慢慢回来。如果这个干扰恰好是在称重过程中发生的读数就会出现短暂错误。我的增强方案是加一个“新息限幅”步骤在卡尔曼更新前计算新息innovation z - x如果innovation的绝对值超过某个阈值比如1g就把新息压缩到这个阈值内再送入卡尔曼。这样放上砝码时真实重量变化大不会被限制因为我知道放砝码的动作会让新息连续多帧超过阈值而单个脉冲干扰往往只是一两帧限制后对估计值的影响就小得多。float Kalman1D_Update_Limit(Kalman1D *kf, float z, float limit) { float innovation z - kf-x; if (innovation limit) innovation limit; if (innovation -limit) innovation -limit; kf-x kf-K * innovation; return kf-x; }当然这个方案有个前提你要能区分“真实重量变化”和“脉冲干扰”。如果真实称重时是快速往秤盘上扔重物那这个限幅会把真实变化也切掉导致读数慢慢爬升。更进一步的做法是检测到连续三帧以上新息同方向且超限就判断为真实加载关闭限幅否则保持限幅。这就是一个非常轻量的异常检测逻辑。6.3 这个方案的扩展方向做完这个项目之后我最大的感受是CS1237这颗芯片的潜力不止于电子秤。同样的硬件链路换一个传感器和标定方式就能做拉力计、压力计、液位传感器模块。如果加上蓝牙就能做手机端实时记录体重的智能秤。卡尔曼滤波这套代码也可以直接用于其他慢变化信号的平滑比如温度采集、气压计高度计、电池电压监测。如果你想把精度再往上推一个台阶可以试试CS1237的片上校准功能发送校准命令后芯片会自行完成零点校准和增益校准校准结果存储后下次上电直接用。这个功能在批量生产阶段能省不少标定时间但DIY场景下软件两点标定的精度已经够了而且更直观。最后分享一个小技巧做电子秤调试时不要盯着串口终端一帧一帧看那样很难发现规律。用串口绘图工具或者简单地往串口发CSV格式数据丢进Excel里画折线图不管是调Q和R参数还是排查机械干扰都能一目了然看到问题所在。
阅读完成 · 觉得有帮助?
咨询建站