ARTICLE DETAIL

资讯详情

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

基于STM32F103与CS1237的高精度应变数据采集实战指南

基于STM32F103与CS1237的高精度应变数据采集实战指南 做称重和应变采集的朋友对HX711肯定不陌生但如果你试过CS1237这颗国产24位ADC会发现它在不少场景下比HX711更顺手——SOP-8小封装、两线接口、内置可编程增益放大器和温度传感器配合STM32F103这类入门级MCU几十块成本就能搭出一套精度不错的数据采集系统。这篇内容不打算讲太多理论就从我实际做的一个基于STM32F103和CS1237的应变测量小模块说起把硬件连接、时序理解、代码实现到调试排错的整个流程完整走一遍适合正在做称重、张力、气压或者通用高精度模拟量采集的朋友参考。1. 为什么是CS1237而不是STM32内部ADC或HX7111.1 被内部12位ADC逼出来的需求STM32F103内部自带的ADC是12位分辨率满量程4096个码在3.3V参考电压下每个LSB大约对应0.8mV。检测电池电压、电位器分压、光敏电阻这类大幅度缓变信号它绰绰有余。但如果接的是应变式称重传感器情况就完全不同了传感器满量程输出通常只有1mV/V到2mV/V5V激励下满量程也不过5到10mV分配到12位ADC上整个量程只有几十到几百个码稍微有点噪声读数就跳得没法看。我当时做的是一个小量程拉力计要求能分辨到1克传感器满量程5kg也就是满量程内总共5000个目标分度用内部ADC无论如何都达不到这个精度。所以只能外挂高分辨率ADC。1.2 CS1237与HX711的差异对比挑选过程中绕不开的是HX711两者都是24位Σ-Δ ADC也都采用两线接口网上资料一抓一大把。我把关键参数整理成表格对比了一下项目CS1237HX711分辨率24位24位接口SCLK DOUT 两线SCLK DOUT 两线PGA增益1/2/4/8/16/32/64/128 可配置32/64/128 固定内部寄存器有可配置通道和增益无靠SCLK脉冲数选择内置温度传感器支持不支持输出速率10Hz / 40Hz / 640Hz / 1.28kHz视批次10Hz / 80Hz封装SOP-8SOP-16差分输入通道1路2路A/BHX711的优势是有两个差分输入通道适合同时接两个传感器。但它的增益只能选固定的32、64、128扩展性和灵活性不如CS1237。CS1237的增益从1倍到128倍连续可编程意味着它不只是给称重传感器用的还可以直连一些输出幅度稍大的传感器比如压力变送器、电流采样电阻甚至直接采集热电偶放大后的信号。另外CS1237内置温度传感器这个功能在做温漂补偿时特别有用省掉了一颗外置温度芯片。1.3 我最终选它的三个理由第一增益范围宽适配的传感器类型多第二SOP-8封装非常小在寸土寸金的PCB上能省不少面积第三内置温度传感器后期做温漂补偿有数据可依。还有一个现实原因同等采购量下CS1237单价通常比HX711便宜一两毛钱对于批量产品来说BOM成本就是这样一点一点抠出来的。如果你只需要做一个最简单的电子秤HX711完全够用但如果你想做的是一个精度要求更高、可能需要做温度补偿、未来还可能换不同量程传感器的通用采集模块CS1237明显更合适。2. 硬件连接五根线背后的电源与信号完整性问题2.1 供电、参考电压和滤波电容的处理CS1237的供电范围是2.7V到5.5VAVDD和DVDD在芯片内部是分开的引脚。我先说的是电源因为这个环节处理不好后面所有精度都是空谈。如果AVDD用5V供电传感器的激励电压也最好从5V取这样传感器的共模输出范围更宽如果用3.3V供电那么传感器满量程输出会成比例降低对ADC的噪声要求更高。为了保证精度我习惯在AVDD和GND之间放一颗10uF的钽电容和一颗100nF的陶瓷电容并联DVDD同样来一份两者在地端汇合尽量用星形接地。参考电压方面CS1237的REFP和REFN引脚可以直接接VCC和GND低成本场景这样最省事但代价是电源纹波会直接注入参考。如果项目对精度要求比较高建议在REFP前面加一颗如REF192之类的精密基准源实测下来稳定性提升非常明显。2.2 与STM32F103的引脚连接CS1237对外只需要两根信号线SCLK和DOUT。SCLK由MCU产生作为时钟输入DOUT是双向引脚读数据阶段输出转换结果写配置阶段接收配置字。我用引脚分配如下CS1237引脚功能STM32F103引脚VCC电源3.3V / 5VGND地GNDSCLK时钟输入PA5DOUT双向数据PA6PA5和PA6在F103最小系统板上都是普通GPIO没有复用冲突。需要注意尽量别用PA13、PA14、PA15、PB3、PB4这些引脚因为它们默认是SWD调试口或JTAG口如果用它们做SCLK和DOUT会导致下载器连不上芯片或者程序跑到一半被调试复位干扰。我之前在STM32F103C8T6最小系统板上就是先用PA15和PB3测试结果Keil能下载但程序一运行就卡死最后发现是JTAG引脚冲突换到PA5和PA6才解决。2.3 上电快速自检流程硬件焊好后不要急着写代码先做三个简单的检查能省下大量调试时间。先用万用表测量VCC和GND之间的电压值确认芯片供电正常且极性正确。接着断电用万用表二极管档检查一下VCC和GND之间有没有短路防止焊油或毛刺引起的问题。最后给系统上电不运行任何通信代码时用示波器或万用表观察SCLK引脚应该是稳定的低电平DOUT引脚应该是高电平。这里有一个非常重要的判断依据CS1237在转换完成后会把DOUT拉低一段时间但上电后尚未通信时芯片默认处于配置模式DOUT应该保持高。如果上电后DOUT一直是低大概率是芯片没有正确复位或者SCLK引脚有持续的干扰脉冲。这个自检只要三十秒却能提前排除六成以上的硬件问题。3. 读懂CS1237的时序图25个SCLK和一个8位配置字3.1 数据读取帧的完整过程CS1237的通信看起来像SPI但又不完全是标准SPI。它的数据读取帧由25个SCLK脉冲组成前24个脉冲用于移出24位ADC转换结果第25个脉冲移出的是一个状态位。每次转换完成后DOUT会从高电平变为低电平这个下降沿就是给MCU的数据准备好了信号。MCU检测到DOUT变低之后开始产生SCLK。在每个SCLK的上升沿之后DOUT上依次输出当前数据的最高位到最低位也就是MSB先行。第25个SCLK所移出的状态位含义是0表示数据已准备好且有效1表示数据尚未就绪。实际读取时我一般会把第25位也读出来如果读到1就丢弃当前数据重新进入等待状态这样可以在一定程度上避免读到新旧数据交界处的无效值。3.2 配置寄存器怎么写入DOUT读数据的过程理解之后更关键的是写配置。CS1237上电后芯片会要求MCU先写入一个8位配置字寄存器的字段包括增益选择、输出速率、工作通道等。具体来说写入的流程是在完成25个SCLK的读取帧之后SCLK继续产生时钟脉冲同时DOUT由芯片的输出模式切换为输入模式MCU在每一个SCLK周期内把配置字的最高位先放到DOUT上芯片在内部锁存依次移入8位数据。注意这里DOUT的方向发生了切换读数据阶段MCU是输入写配置阶段MCU是输出。如果不做方向切换配置字根本写不进去这是新手最容易卡住的地方。我用的CS1237默认配置是内部增益128倍10Hz输出速率单端输入通道1。对应的配置字在不同批次芯片上略有差异但常见的手册示例值是0x0C。如果你手头的配置字和我不一样务必以你买到的芯片手册为准。写配置时不要只写一次我推荐在上电后连续写入两次第一次作为唤醒第二次作为正式配置有效减少上电瞬间时序不稳定导致的首帧数据异常。3.3 用逻辑分析仪验证时序没有逻辑分析仪的调试都是盲人摸象我说的不是玩笑。CS1237的工作频率不高SCLK脉冲甚至可以放到1MHz左右常见的24MHz采样率的逻辑分析仪完全够用。接线很简单逻辑分析仪的CH1接STM32F103的PA5CH2接PA6GND接板子的GND。抓一段时序之后应该能在软件上看到这样几个特征DOUT先由高变低随后出现一组25个方波脉冲再接着是8个方波脉冲。如果在25个SCLK之前看到DOUT根本没有拉低说明芯片没有完成转换通常要查SCLK和DOUT是否接反或者供电是否正常。我曾经遇到过一种情况逻辑分析仪上看着波形没问题但代码就是读不到正确数据排查了很久才发现是GPIO初始化的速度等级配错了SCLK上升沿过缓导致芯片采样出错把GPIO速度从2MHz改到50MHz就恢复正常。4. 标准库驱动从零手写读写函数4.1 引脚初始化与微秒延时准备标准库版本的代码逻辑最直观适合完全理解底层的朋友。先做引脚初始化SCLK配成通用推挽输出DOUT先配成浮空输入等写配置时再切换方向。初始化代码如下#include stm32f10x.h #include delay.h // 需自行提供微秒级延时函数 #define CS1237_SCLK_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_5) #define CS1237_SCLK_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_5) #define CS1237_DOUT_READ() GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6) #define CS1237_DOUT_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_6) #define CS1237_DOUT_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_6) void CS1237_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 先初始化 SCLK 为推挽输出 GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin GPIO_Pin_5; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // DOUT 初始化为浮空输入等待芯片输出 GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_Init(GPIOA, GPIO_InitStructure); }延时函数在CS1237通信里非常重要不能省。SCLK的高电平和低电平至少要维持几百纳秒我一般直接延时1us左右这在72MHz主频下用简单的空循环就能实现。注意MDK的优化等级如果开得太高空循环可能被编译器优化掉建议延时函数用volatile变量实现或者直接用SysTick延时。4.2 读取24位原始数据与超时保护接下来是读取函数。核心逻辑是等待DOUT变低然后依次产生25个SCLK边沿读取数据。很多人写的代码里都有一个隐患while(CS1237_DOUT_READ());这个循环如果DOUT一直不拉低程序会永远卡死在这里。在实际调试中芯片虚焊、引脚配置错误、供电异常都会导致DOUT拉不低。我强烈建议加上超时机制例如uint32_t CS1237_ReadRaw(void) { uint32_t raw 0; uint32_t timeout 0xFFFFF; uint8_t i; // 等待DOUT拉低表示转换完成带超时保护 while (CS1237_DOUT_READ()) { if (--timeout 0) { return 0xFFFFFFFF; // 超时返回值 } } for (i 0; i 25; i) { CS1237_SCLK_HIGH(); delay_us(1); if (i 24) { raw 1; if (CS1237_DOUT_READ()) { raw | 1; } } CS1237_SCLK_LOW(); delay_us(1); } return raw; }读取顺序是从第0位到第23位每读一位前把上一轮的结果左移一位这样最终收集到的是MSB在前、LSB在后的24位二进制补码数据。每次读完第25个SCLK之后DOUT会恢复为高电平等待下一次转换完成。4.3 配置寄存器切换通道、增益和采样率配置字写入需要把DOUT切换为推挽输出模式然后逐位移入8位数据。这里的细节是位顺序芯片要求从最高位开始也就是MSB first。代码实现如下void CS1237_SetConfig(uint8_t config) { GPIO_InitTypeDef GPIO_InitStructure; uint8_t i; // DOUT 切换为推挽输出 GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 按位发送配置字MSB first for (i 0; i 8; i) { if (config 0x80) { CS1237_DOUT_HIGH(); } else { CS1237_DOUT_LOW(); } config 1; CS1237_SCLK_HIGH(); delay_us(1); CS1237_SCLK_LOW(); delay_us(1); } // DOUT 恢复为浮空输入准备读取数据 GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_Init(GPIOA, GPIO_InitStructure); }上电后的初始化顺序建议是先延时100ms以上等待芯片电源稳定然后写两次配置字再延时一段时间最后才开始循环读数据。这个顺序看似简单但跳过任何一步都会导致第一帧数据异常。配置字不同位段控制不同的功能比如增益和采样率具体每一位的含义同样要查芯片手册确认不同批次之间可能有位序差异。4.4 原始数据的补码转换读到的24位数据是二进制补码格式。正满量程时接近0x7FFFFF负满量程时接近0x800000。如果要换算成有符号数最简洁的做法是判断最高位如果data 0x800000为真说明是负数此时data减去0x1000000即可得到真正的有符号值。也可以直接用32位有符号变量接收24位数据然后左移8位再右移8位完成符号扩展效果一样。我在实际代码里用的是第一种方法逻辑简单别人接手也容易看懂int32_t CS1237_GetSignedValue(uint32_t raw) { if (raw 0xFFFFFFFF) { return 0; // 超时或者无效数据 } if (raw 0x800000) { return (int32_t)(raw - 0x1000000); } else { return (int32_t)raw; } }转换后的有符号值配合PGA增益和参考电压就能换算成实际电压电压 原始码 / 8388608 * Vref / 增益。这个公式留到后面的标定环节再展开。注意读取的原始值不是真实物理电压中间还隔着参考电压和增益这两个变量很多初学者直接把原始值当电压用结果在更换增益后莫名其妙发现读数差了好几倍。5. HAL库版本CubeMX工程化的配置路径5.1 CubeMX中的引脚和时钟配置工程化的项目现在基本都用CubeMX生成初始化代码HAL库和标准库的差异主要在于GPIO配置和读写函数。在CubeMX里配置PA5为GPIO_OutputPA6为GPIO_Input同时把PA6的上拉方式选为Pull-up。为什么要在CubeMX里给PA6配置上拉因为DOUT在部分模块上是开漏或者悬空状态如果MCU引脚没有上拉空闲时DOUT电平不确定可能直接导致等待DOUT拉低的循环提前退出或一直进不去。PA5的输出速度建议选High也就是50MHz那一档保证SCLK边沿足够陡峭。时钟树部分用默认的HSE 8MHz外部晶振PLL倍频到72MHz即可ADC和定时器都用不到所以不用额外处理。CubeMX生成的main函数里调用我们自己的CS1237_Init()之前确保SystemClock_Config已经执行。这里有个容易忽视的问题CubeMX生成代码时会默认把所有未使用的GPIO配置为模拟输入如果PA5和PA6在CubeMX里没有被显式配置生成的代码会把它们设成Analog模式通信时序自然就完全不正常了。碰到代码看着没问题硬件也正常但就是读不到数据的情况先检查一下CubeMX的Pinout页面。5.2 HAL库驱动代码的适配HAL库版本里的寄存器宏定义要换成HAL函数核心逻辑不变。我的习惯是把CS1237相关函数封装成一个独立模块cs1237.c和cs1237.h对外只暴露三个接口初始化、读原始值、写配置。这样上层应用不需要关心引脚细节日后换芯片平台时只需要改这个模块。#include cs1237.h void CS1237_SCLK_HIGH(void) { HAL_GPIO_WritePin(CS1237_SCLK_GPIO_Port, CS1237_SCLK_Pin, GPIO_PIN_SET); } void CS1237_SCLK_LOW(void) { HAL_GPIO_WritePin(CS1237_SCLK_GPIO_Port, CS1237_SCLK_Pin, GPIO_PIN_RESET); } uint8_t CS1237_DOUT_READ(void) { return HAL_GPIO_ReadPin(CS1237_DOUT_GPIO_Port, CS1237_DOUT_Pin) GPIO_PIN_SET ? 1 : 0; }在CubeMX生成的工程里把以上的宏定义换成HAL函数调用后读写函数主体可以直接复用标准库版本。如果对时序比较敏感也可以用寄存器方式加快IO速度比如直接操作GPIOA-ODR和GPIOA-IDR只要注意不要破坏CubeMX配置的其他引脚即可。我看过不少网友用HAL库版本说CS1237读取不稳定实测下来多数不是HAL函数本身的问题而是CubeMX引脚配置或延时精度的问题。5.3 硬件SPIDMA读取CS1237到底行不行还是那句话遇到CubeMX SPI DMA ADC这些热词组合时很多帖子把CS1237也拉了进来但我的实际结论是CS1237不适合用硬件SPIDMA读取。原因有三点首先CS1237的DOUT是双向引脚读数据阶段和写配置阶段方向不同SPI的MISO和MOSI是分开的用硬件SPI需要把MOSI外接DOUT配置时必须手动切换引脚模式其次CS1237的数据帧是25位加8位配置字不是8位或16位的整数倍帧长度对不上。DMA是按帧长度搬运数据的25位这种非标长度会让DMA配置变得极其别扭最后SCLK和DOUT之间的相位关系与标准SPI模式差一点即使强行配置成Mode 0或Mode 3都可能存在采样沿不匹配的问题。我在一次试验中确实用硬件SPI读出了数据但代码里横七竖八都是特殊处理稳定性远不如GPIO模拟。CS1237的时钟频率本身不高用GPIO模拟并不会消耗多少CPU资源所以我的建议很简单别在这个芯片上折腾SPIDMA。6. 调试图数据跳变、读取卡死、零点漂移6.1 数据跳变先怀疑电源和地线我做拉力计调试的第一天CS1237读出的数据在几百个码之间疯狂跳动看起来就像随机数。当时第一个怀疑对象是代码反复检查时序也没有问题。后来用示波器看CS1237的VCC引脚才发现纹波高达接近100mV电源来自电脑USB的5V经过AMS1117降压到3.3V在带动传感器和模块时已经力不从心。解决措施有三步把DC-DC电源换成了线性电源在CS1237的VCC引脚组并了一个100uF电解电容和一个100nF陶瓷电容同时把传感器和ADC之间的杜邦线全部换成了双绞屏蔽线一端接地。这三步做完跳动幅度从几百个码降到了十几个码。如果你也遇到类似情况先别急着优化代码把示波器探头戳到芯片电源脚上看一看往往比调试半天软件省力得多。6.2 读取卡死的两种常见原因程序卡死在等待DOUT变低的循环里是我收到提问最多的问题。第一个原因前面提过DOUT被初始化成了模拟输入或者引脚方向不对导致读不到电平变化。我见过有人在CubeMX里为了省电把所有GPIO初始化成Analog模式DOUT引脚也在其中后果就是读上来的电平永远不是期望值。第二个原因是芯片进入了一种半睡半醒的状态比如上电后没有正确配置就立刻读数据芯片不产生转换完成信号。这种情况的修复办法是上电后延时100ms然后完整写两次配置字每次写完都需要把DOUT方向切回输入。第三个比较少见但很坑DOUT引脚碰巧被其他外设占用例如I2C的SCL或SPI的NSS外设在后台产生了电平变化干扰CS1237的时序。排查卡死问题时我建议在等待循环里加一个串口打印或者一个翻转电平输出如果走到循环说明DOUT电平正常如果卡住就知道了然后用逻辑分析仪看DOUT波形比盲猜效率高得多。6.3 零点漂移与PGA组合的关系零点漂移表现为刚上电时读数缓慢变化或者温度变化后零点位置移动。CS1237在高增益档位下比如PGA128任何微小的共模电压变化都会被放大到很夸张的程度。我做了个简单实验同样一颗芯片增益从1倍切到128倍短时间漂移量从不到10个码变成了三四百个码。而且芯片上电后存在一个短时间的自热过程内部温度上升会让偏移发生可复现的缓慢变化。解决思路有两条一是对精度不是极高要求的场合上电后预热五分钟再开始采集或者用软件做零点扣除二是利用CS1237内置的温度传感器实时读取片内温度拟合一条零点温漂曲线来做软件补偿。后者的效果我实测下来能把满载漂移抑制掉七成以上代价是标定工作量大一些。7. 滤波和标定把原始值变成可靠的物理量7.1 三种滤波算法怎么选原始读取到的数据不可能直接用软件滤波是必要的最后一道工序。我试过三类滤波方式简单说说各自适用场景。算术平均滤波适合静态称重读取N次取平均N越大噪声抑制越强实时性越差一般取16到64次。滑动平均适合需要连续输出的动态场景新数据进来顶替最老的数据每读一次输出一个平均结果响应快且平滑。中值滤波适合采集信号里有偶发尖峰干扰的场合比如电机启停瞬间的电磁干扰取N次排序后取中间值能有效去掉离群点。如果要求更高可以在这三种基础之上再叠加一阶低通滤波公式是new new alpha * (sample - new)alpha取0.1到0.3之间相当于给数据流加惯性。7.2 两点标定法的完整过程滤波之后就要做标定把ADC码值转成物理量。两点标定是最常用也是最可靠的方法本质上就是解一条直线方程。先不施加任何负载记录稳定后的原始码值作为零点的y0然后放上一个已知重量的标准砝码记录这时的原始码值作为满量程点的y1。假设砝码重量是w1那么斜率k (y1 - y0) / w1截距b y0之后任意时刻的重量weight (raw - b) / k。这里有一个容易被忽略的点y0和y1不是直接读一次而是各取滤波后的稳定值而且要等示数稳定了再记录否则标定误差会被线性放大。不同增益档位下斜率不同所以更换PGA后必须重新标定。7.3 我实测下来的一组参考数据最后给一组我自己的实测数据供参考。我用的传感器是5kg量程的铝制悬臂梁式称重传感器灵敏度2mV/V5V激励CS1237配置为PGA128输出速率10Hz。不加负载时原始码稳定在约1200的位置零点峰峰值波动大约正负15个码。加上1kg标准砝码后原始码跳到约320000换算下来每克重量大约对应320个码。也就是说1个码对应约3mg的等效重量配合滑动平均滤波后最终显示的重量波动能控制在正负1g以内。如果你手头的原始码范围和我差很多先检查激励电压、PGA增益和传感器灵敏度这三个参数它们每一处都会成倍影响码值范围。做产品时这套标定流程我会要求在产线上自动执行一遍把每台设备的斜率存到EEPROM里因为传感器和ADC都存在个体差异换一台设备就预标定一次长期跑下来稳定性才有保障。
返回列表