ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CS1237高精度ADC与卡尔曼滤波:DIY电子秤设计与实现指南

CS1237高精度ADC与卡尔曼滤波:DIY电子秤设计与实现指南 1. 项目概述与核心需求解析1.1 为什么选CS1237这颗ADC做电子秤很多教程默认用HX711。HX711便宜、例程多但它的最大问题是内部PGA增益和采样率固定噪声水平一般而且读出来的原始数据经过各种代码处理后稳定性完全看运气。CS1237是国产芯海科技的一颗24位ADC同样是两线接口价格和HX711差不多但它内置了可编程增益放大器和可配置采样率在低速率下的有效无噪声位数明显更好。我之前用同一套模具把HX711换成CS1237之后静态重复性从±1.5g左右直接压到±0.3g以内以5kg量程传感器为例这个差别在称粉末、配料的时候特别明显。CS1237另外一个优势是外围极简。它不需要外部晶振SCLK和DOUT两根线直接接MCU内部有振荡器和稳压电路甚至可以直接用MCU的3.3V供电然后通过软件配置PGA和采样率。对新手来说这意味着不需要折腾模拟前端布板难度低很多。说白了选CS1237就是花一样的钱买到更好的噪声底和更大的调节空间。对精度有要求的家用电子秤、咖啡秤、小包装分装秤甚至小量程工业配料秤这颗芯片都很合适。1.2 电子秤系统整体架构一套完整的电子秤系统并不复杂核心链路可以拆成四段电阻应变式称重传感器负责把重量变电信号CS1237负责把这个微弱的差分信号放大并转成24位数字量MCU读取数字量以后用滤波算法进行平滑最后根据标定系数换算成重量送到OLED、数码管或TFT显示。我在这个项目里用的主控是STM32F103C8T6你完全可以用ESP32、GD32或者任何带GPIO的MCU替代因为CS1237的通信协议就是普通的GPIO模拟时序不依赖任何硬件外设。显示部分选了一块0.96寸OLED用I2C接口方便调试时直接看重量和滤波前后的原始数据。整体方案确定以后接下来要解决三个核心问题传感器怎么接、CS1237怎么读、数据怎么滤。这三个问题解决掉整个项目就完成80%了。后面所有的工作包括去皮、标定、显示本质都是在一条稳定的数据流上做业务逻辑。2. 硬件底层原理与电路设计2.1 称重传感器与惠斯通电桥市售常见的单点式称重传感器内部其实是4片电阻应变片贴在弹性体的上下表面接成惠斯通电桥。当传感器受力变形时上下两对电阻的阻值一个增大一个减小电桥失去平衡输出一个与外力成比例的差分电压。这个电压有多小呢以5kg量程、灵敏度1mV/V的传感器为例5V激励下满量程也才只有5mV而1g对应的电压是1μV级别。这就意味着后端ADC必须具备两方面的能力一是能放大这么小的信号二是分辨能力要足够高。所以好传感器的素质很重要但ADC同样重要。普通的10位、12位ADC面对满量程5mV的信号根本没法定标必须用高分辨率加内置放大器的方案。CS1237内部PGA可设置增益1、2、64、128接电桥传感器时一般选64或128。增益128时满量程5mV放大为640mV配合24位ADC理论上每个码字对应的电压分辨率大概是参考电压除以增益再除以2的23次方这个量级。当然理论分辨率是一回事噪声会把有效位数拉低这一点后面会展开讲。传感器选型上也有讲究。家用秤推荐用单点式铝制传感器量程比实际需求大出30%到50%即可。比如你要称最大3kg的东西就选5kg传感器这样既保证线性度又不会让弹性体过早疲劳。体积很小的悬臂梁传感器适合做小量程大量程则要考虑材质和安装方式。记住一个原则传感器是机械结构安装不稳后面软件再怎么滤波都救不回来。2.2 CS1237引脚与电路连接CS1237只有两线接口SCLK和DOUT。引脚功能很直白VDD和GND接电源VREF需要接一个高质量的参考电压AVDD给模拟部分供电AIN和AIN-接传感器差分输出。很多人第一次画CS1237电路时会问VREF和AVDD是不是可以接一起可以但要在VREF引脚和AVDD引脚之间加足够的去耦电容。我用的是10μF钽电容加0.1μF陶瓷电容并联放在芯片旁边实测对数据稳定有很大帮助。硬件连接方面我给出自己验证过的接法表传感器线色功能接CS1237红激励AVDD黑激励-GND绿信号AIN白信号-AIN-这里有个常见误区很多模块把红黑接电源、绿白接AIN但不同厂家传感器的线色并不完全统一最好先用万用表确认一下。电桥输出阻抗一般几百欧到一千欧AIN端不需要额外保护但DOUT引脚建议加一个10kΩ上拉电阻防止上电时序不确定时读到随机电平。SCLK和DOUT的走线尽量短同时远离电机、继电器这类干扰源。我第一版飞线实验就因为这个吃了亏线从开关电源旁边绕了一圈结果数据跳得没法看。2.3 24位精度到底意味着什么“24位ADC”这个词很容易让人以为它能分辨出满量程的1/16777216但实际不是这么回事。ADC手册里的24位指的是转换结果长度真正有意义的是“无噪声位数”或者“有效位数”它和PGA增益、采样率、供电噪声都有关系。CS1237在PGA128、采样率10Hz时官方典型有效无噪声位数能做到20位左右采样率提到640Hz后有效位数会明显下降因为带宽变宽噪声进来了。打个比方24位ADC是一把刻了1677万个刻度的标尺但你的手会抖、空气会流动、桌面会振你实际能稳定读出的位置可能只有20位对应的刻度。所以DIY电子秤别盲目追求显示到0.01g要先看传感器量程和ADC的有效分辨率再决定显示小数位。5kg量程、20位无噪声分辨率对应约0.005g的代码级分辨率但经过机械结构和负载以后实际稳定显示到0.1g已经很了不起。搞懂这一点你就明白为什么数据出来一定要滤波不是滤掉毛刺而是要榨干这颗24位ADC在低噪声模式下的剩余价值。3. 软件驱动与卡尔曼滤波实现3.1 CS1237读取时序与驱动代码CS1237和MCU之间是类似SPI的两线时序但没有片选。DOUT有两个作用一是数据就绪信号二是数据输出。上电后如果芯片没有完成转换DOUT保持高电平转换完成后DOUT自动拉低通知MCU“数据已经准备好”。MCU检测到DOUT低电平即可开始读数据。读数据的过程是对SCLK连续打24个脉冲在每个脉冲的上升沿把DOUT上的数据位移进来高位在前结果是一个24位有符号数。读完之后如果你想修改寄存器配置可以继续打8个脉冲把配置字写进去。如果不想改配置打完24个脉冲后DOUT会自行恢复高电平下一次转换完成后又会拉低。这里有一个容易踩的坑读数据的时机必须严格等DOUT拉低不能自己用固定延时去凑。我第一次写驱动时图省事直接用延时去读结果数据时不时跳一个很大的值。改成检测DOUT下降沿或者循环等待拉低后就非常稳定了。下面是我在STM32上的一个极简驱动示例GPIO和延时函数请根据自己的平台替换// cs1237.h #ifndef __CS1237_H #define __CS1237_H #include stdint.h void CS1237_Init(void); int32_t CS1237_Read(void); uint8_t CS1237_IsDataReady(void); #endif// cs1237.c #include cs1237.h #include main.h #define CS1237_SCLK_PIN GPIO_PIN_0 #define CS1237_DOUT_PIN GPIO_PIN_1 #define CS1237_GPIO_PORT GPIOA static void SCLK_High(void) { HAL_GPIO_WritePin(CS1237_GPIO_PORT, CS1237_SCLK_PIN, GPIO_PIN_SET); } static void SCLK_Low(void) { HAL_GPIO_WritePin(CS1237_GPIO_PORT, CS1237_SCLK_PIN, GPIO_PIN_RESET); } static uint8_t DOUT_Read(void) { return HAL_GPIO_ReadPin(CS1237_GPIO_PORT, CS1237_DOUT_PIN); } void CS1237_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin CS1237_SCLK_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(CS1237_GPIO_PORT, GPIO_InitStruct); GPIO_InitStruct.Pin CS1237_DOUT_PIN; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(CS1237_GPIO_PORT, GPIO_InitStruct); SCLK_Low(); } uint8_t CS1237_IsDataReady(void) { return (DOUT_Read() 0) ? 1 : 0; } // 读取一次ADC转换结果配置寄存器暂用上电默认值 // 想修改PGA增益、采样率等参数时请参考数据手册按位写入 int32_t CS1237_Read(void) { uint32_t raw 0; uint32_t timeout 0xFFFFF; while (DOUT_Read() 1 timeout--); if (timeout 0) return 0; for (int i 0; i 24; i) { SCLK_High(); raw 1; if (DOUT_Read()) raw | 1; SCLK_Low(); } // 24位有符号数扩展为32位有符号数 if (raw 0x800000) raw | 0xFF000000; return (int32_t)raw; }要提醒的是CS1237写配置寄存器时DOUT需要切换为输出方向在SCLK配合下逐位送出配置字和读数据的方向正好相反。不同MCU实现时要特别注意DOUT引脚的方向切换。我上面的代码使用上电默认配置这样最保险。如果你要配成PGA64、40Hz采样建议写一个独立的CS1237_WriteConfig(uint8_t cfg)函数不要把它和读数据混在一起否则调起来会很痛苦。3.2 为什么称重数据必须做滤波从CS1237读回来的原始数据即使一切正常也会在静止状态下小幅跳动。原因有几个传感器本身的机械迟滞、ADC量化噪声、电源纹波、电磁干扰甚至桌面的轻微震动。跳动幅度通常有几十到几百个原始码值对应到重量可能是±0.1g到±1g这在很多场合不可接受。最简单的滤波是均值滤波连续采N次取平均效果也不错但缺点也很明显。均值滤波把所有采样点当成同等可信一旦出现一个大幅度的随机尖峰要把它完全平掉需要很多个点而点多了动态响应又变差。称重恰恰是“既要短时间稳定、又要在放上重物时快速跟随”的场景均值滤波很难兼顾。卡尔曼滤波就不一样。它把系统当成一个有数学模型的状态估计问题每一帧数据进来时先根据上一帧的预测值做“经验估计”再用当前观测值做“修正”并根据噪声水平给预测和观测分别分配一个随时间变化的权重。放到电子秤上效果就是静止时滤波输出非常平滑几乎看不出跳动放上砝码后几百毫秒内就能跟上真实重量不会像大窗口均值那样拖泥带水。3.3 卡尔曼滤波的五个核心公式卡尔曼滤波非常适合“线性系统高斯噪声”的场景。电子秤在静止或缓变时可以近似认为重量是一个常量只是被噪声污染。用数学语言描述就是x(k) x(k-1) w(k-1) z(k) x(k) v(k)其中x是真实重量z是ADC读数w是过程噪声v是测量噪声。整条滤波流程可以浓缩成五个公式预测部分 x_pred x_est P_pred P_est Q更新部分 K P_pred / (P_pred R) x_est x_pred K * (z - x_pred) P_est (1 - K) * P_pred这里的Q是过程噪声协方差代表你对状态模型本身的信任程度R是测量噪声协方差代表你对单次ADC读数的信任程度。Q越大滤波器越相信新数据R越大滤波器越平滑但反应越慢。放到程序里这个滤波器的全部状态就是两个变量x和P输入一个观测值z输出一个新的x。计算量非常小在STM32F103这种72MHz主频上跑几乎不占用CPU时间。所以不要被“卡尔曼”这个名字吓到它落到代码里就是一个结构体加两个函数和均值滤波的工程量差不了太多。我用的卡尔曼滤波代码是这样的// kalman.h #ifndef __KALMAN_H #define __KALMAN_H typedef struct { float x; // 状态估计值 float P; // 估计误差协方差 float Q; // 过程噪声协方差 float R; // 测量噪声协方差 float K; // 卡尔曼增益 } KalmanFilter_t; void Kalman_Init(KalmanFilter_t *kf, float init_x, float init_P, float Q, float R); float Kalman_Update(KalmanFilter_t *kf, float measurement); #endif// kalman.c #include kalman.h void Kalman_Init(KalmanFilter_t *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 Kalman_Update(KalmanFilter_t *kf, float z) { // 预测 // 常量模型下状态预测值就是上一刻的最优估计 // P_pred P Q // 更新 kf-K (kf-P kf-Q) / (kf-P kf-Q kf-R); kf-x kf-x kf-K * (z - kf-x); kf-P (1.0f - kf-K) * (kf-P kf-Q); return kf-x; }这段代码里我直接把P_pred的计算合并进了K和P的更新里少定义一个临时变量工程上完全没问题。你如果刚开始看卡尔曼建议先按五个公式原样写一遍确认理解以后再压缩代码。3.4 滤波参数怎么调Q和R的取值我见过很多人头疼其实有个实用起点。先把CS1237的原始码值换算成克然后看静止时数据的方差假设方差是0.01g²那R可以取0.01。Q可以先取一个很小的值比如0.001这时候滤波输出会很滑响应稍慢。如果你发现放上重物后数值需要一秒钟才到位就把Q调大一个数量级到0.01左右。相反如果静止时输出波动太大就把R调大。调参时注意一定要在真实使用场景下测不能只在桌面上拿传感器单独测。用手触碰传感器、旁边放风扇、底部垫软垫这些都会改变噪声特性。我自己的经验是5kg传感器、CS1237设置PGA64、采样率40Hz静止方差大约0.005g²最后取Q0.002、R0.02响应和稳定度都很理想。如果你把采样率降到10Hz噪声会更小Q还可以再低一点。4. 完整源码与工程实践4.1 工程结构与功能划分在写整个工程代码之前先想清楚功能划分。嵌入式程序不需要很复杂但模块清晰能让你以后改量程、换传感器、加按键都方便。我的工程分四个模块模块职责cs1237.cCS1237驱动初始化、读取数据、配置寄存器kalman.c卡尔曼滤波器初始化、更新、参数设置scale.c称重逻辑均值采样、标定系数、重量换算main.c主循环定时读取、滤波、显示、按键处理这里有个容易被新手忽略的地方标定系数不要直接写死在滤波后的数据里而应该把“零位”和“斜率”分成两个变量。这样你做两点标定的时候只修改这两个变量不需要重新编译。我习惯在Flash里专门存一组标定参数上电时读取按一下校准键就重新标定并写回。这样批量生产或者后续换传感器时不需要改程序。4.2 主循环与ADC采样流程主循环的逻辑其实很简单检测到DOUT拉低就读取一次原始码值把码值通过滤波器再用标定系数换算成重量刷新显示。不要在一个循环里强迫自己用等间隔采样CS1237在有数据时会拉低DOUT你可以配合定时器每隔2ms查一次DOUT也可以直接在循环里轮询。40Hz采样率下两次有效数据间隔25ms主循环里轮询完全够用。核心逻辑大概是这样的在主循环中先判断数据是否就绪如果就绪就读取原始码值然后做一次滤波将滤波结果当成“当前重量信号”再减去零点偏移乘上斜率得到重量值。如果按下去皮键就把当前滤波后的重量记录为零点偏移。整个过程没有任何阻塞等待OLED刷新放到一个独立的小节拍里避免影响采样。在实际工程中我会把ADC原始码值到克的换算放在滤波前还是滤波后取决于噪声分布。我的习惯是先做一次原始码值到克的线性映射再做滤波因为这样Q和R参数的单位直接就是克和克²调参时不至于对着几万几十万的码值发懵。4.3 两点标定法实操电子秤测量的基础是把码值映射成重量。这个映射关系在线性区间内就是一条直线所以理论上只要两个点就能确定斜率和截距。第一个点是空秤零点第二个点是放上已知重量的砝码。实际操作步骤如下空秤上电预热30秒以上等待数据稳定。记录零点平均值记为zero_raw。放上标准砝码例如1kg等待稳定记录均值记为span_raw。计算斜率scale 1000.0 / (span_raw - zero_raw)。实际重量 (raw - zero_raw) * scale。注意这里用的是滤波前还是滤波后都行但一定要保持口径一致。比如你滤波后得到的是一个“克”值那么重新标定时也要用同一个路径。如果用公斤或者市斤做单位斜率换算要同步。标定砝码不用追求特别多一个准确的质量块就够。你要是没有砝码可以用一瓶标注净含量500g的矿泉水做粗略标定。标定的关键不是“多重”而是“重量已知且准确”所以能用砝码就用砝码。我测下来用1kg标准砝码做两点标定以后0到3kg范围内的非线性误差基本在±0.5g以内对DIY项目来说已经很能打了。5. 常见问题与调试经验5.1 常见问题速查表我把DIY过程中最容易遇到的问题列成一张表基本能覆盖大多数情况现象可能原因解决办法读回数据一直是0xFFFFFF或满量程传感器接线反、激励没供电检查红黑绿白用万用表测桥臂输出数据跳变剧烈DOUT没上拉、供电纹波大、飞线过长加10kΩ上拉加强去耦缩短连线静止时数据缓慢漂移温漂、传感器蠕变、预热不充分上电预热1分钟放在避风处测试放上重物数值慢慢涨机械结构松、夹具变形检查称重平台的固定螺丝标定后量程偏大或偏小砝码不准、采样率过高导致有效位数下降换标准砝码降低采样率至10Hz或40Hz显示屏出现负数零位设置不对重新去皮或重新标定零点5.2 调试实录数据跳变与温漂我说两个自己真实踩过的坑。第一个是数据跳变。第一版飞线测试时我把CS1237模块直接插在面包板上用杜邦线连到STM32传感器放在桌子上。结果数据跳得非常明显滤波都滤不平。排查后发现问题出在DOUT引脚悬空时电平不稳。CS1237的DOUT在上电到第一次转换完成这段时间是不确定的MCU读到随机高电平时就会误判“数据就绪”导致读出乱码。加上10kΩ上拉电阻后问题立刻消失。另外我当时把SCLK线从开关电源旁边走过每次附近设备切换时数值都会抖一下把线挪到远离电源的地方就好了。第二个是温漂。北方冬天开窗测试我注意到空秤读数会在几分钟内缓慢变化大约0.2g。这不是ADC的问题而是应变片本身的零点温漂。解决办法不是换硬件而是在软件上做“自动零点跟踪”当滤波后的重量连续N秒都落在零点附近的一个小窗口内比如±0.3g就自动把该值更新为新的零位偏置。这个功能在很多商用电子秤里都有理解原理后自己写几十行代码就能加上。注意窗口不能设太大否则小重物放上去也会被当成零点漂移吃掉。6. 效果测试与优化扩展6.1 实测数据对比我拿机械砝码做了一组简单测试对比了不滤波、10点均值滤波和卡尔曼滤波三种方式的表现。测试条件是5kg单点传感器、CS1237 PGA64、采样率40Hz、STM32F103传感器静止放置每0.5秒记录一次重量。以下是1kg砝码连续读数时的极差和稳定时间滤波方式静止极差加载稳定时间±0.1g不滤波±1.2g不适用晃动太大10点均值±0.35g约1.2s卡尔曼滤波±0.1g约0.4s这个结果基本符合预期。卡尔曼滤波的优势主要在于它在降噪的同时没有像均值滤波那样积累大量延迟。静止极差从±1.2g降到±0.1g对一个5kg量程的传感器来说已经能胜任绝大多数家用和小型商用场景。如果你把采样率降到10HzPGA和滤波参数重新调一下精度还能更好但动态响应会变慢适合极低速平台。6.2 后续可以怎么扩展这套方案只需要改很少的代码就能做很多变体。比如把单路传感器换成四角支撑的多传感器结构硬件上需要把四个传感器的信号求和后接进CS1237软件流程基本不变。也可以加上蓝牙模块把滤波后的重量数据通过低功耗蓝牙发送到手机App做一个智能营养秤。这类扩展在工程上难度不大真正花时间的是数据格式和App端交互。关于显示OLED在小秤上很好用但如果你要做厨房秤或者商用标签秤建议换成LCD段码屏或者数码管可视角度和寿命会更好。按键方面目前只做了一个去皮键实际上还可以加单位切换键和标定键配合一个简单的菜单就能把DIY秤做成一个完整产品原型。我个人的看法是CS1237加卡尔曼滤波这套组合非常适合作为个人进入高精度数据采集领域的第一块敲门砖。它的硬件简单到一天就能搭完软件难度也集中在十几行滤波代码上但你能从中理解传感器、ADC、数字滤波、标定这一整套信号链路的逻辑。后面哪怕去做应变式压力计、拉力计、液位传感器这套方法论都是通用的。最后再分享一个小技巧如果你发现静止数据里偶尔有一个尖峰别急着改卡尔曼参数先在采样入口加一个最朴素的限幅滤波比如相邻两次读数差超过50g就丢弃本次数据。这个“前端限幅加卡尔曼平滑”的组合比单纯调Q和R省事得多而且不容易出现余差。这个坑我踩过写在这里帮你避开。
返回列表