ARTICLE DETAIL

资讯详情

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

STM32F103与CS1237高精度称重采集实战:从硬件连接到代码调试

STM32F103与CS1237高精度称重采集实战:从硬件连接到代码调试 做高精度小信号采集尤其是电子秤、拉力计这类应变式传感器方案绕不开24位ADC。前阵子我用手头的STM32F103最小系统板配CS1237做了一套称重采集模块整体流程走下来从硬件连接、寄存器配置到代码调试都踩了不少坑。写这篇文章就是想把这套方案从头到尾复盘一遍给准备在STM32F103上接CS1237的朋友一个能直接参考的实战样本。如果你只是想跑一个现成demo可以直接跳到第4节看代码但如果想知道为什么这么接、为什么这么写建议按顺序看。CS1237和HX711虽然都是桥式传感器常用的ADC但前者提供了可配置增益和输出速率精度上限更高和STM32F103这种M3内核搭配相当顺滑。这次项目最终实现了3kg量程、0.1g分辨率的稳定称重下面我把完整的硬件连接、软件驱动和调试经验一次性讲清楚。1. 项目概述与选型分析1.1 为什么选择CS1237而不是HX711很多人一说到电子秤就想到HX711这没问题HX711确实便宜、稳定、生态成熟。但我在这个项目里换用CS1237根本原因是HX711的增益和速率太固定了增益只有64和128两档输出速率基本就是10Hz和80Hz想做稍微灵活点的采集就很受限。CS1237也是国产芯片价格并不高但它内置了可编程增益放大器增益档位有1、2、64、128倍输出速率从10Hz到1280Hz分四档可调而且采用Sigma-Delta架构有效分辨率在低速率下可以达到20位以上。这意味着同一块板子既能接高灵敏度的小量程传感器也可以通过降低增益去采几十毫伏级别的信号适用范围宽很多。另一个关键点是CS1237的接口实在太省引脚了只有SCLK和SDO两根线。STM32F103虽然引脚多但很多项目要同时驱动显示屏、按键、无线模块GPIO资源经常紧张。两根线的类SPI接口在布线和IO分配上非常友好而且它没有片选脚省掉了一个GPIO。1.2 CS1237在同类ADC中的定位做选型的时候我认真对比了几颗常见芯片这里直接给个表格方便大家做方案时快速判断。型号分辨率PGA增益输出速率接口参考电压典型应用HX71124位64/12810/80Hz两线外部VAVDD分压电子秤CS123724位1/2/64/12810/40/640/1280Hz两线内部基准约2.5V称重、压力、小信号ADS123224位1/2/64/12810/80SPS三线/四线外部参考工业称重ADS122024位1/2/4/8/16/32/64/128最高2kSPSSPI/I2C内部2.048V通用高精度采集从表格能看出来CS1237在低价位芯片里提供了一个比较均衡的组合PGA可调、速率跨度大、内部参考做非工业级别的称重和压力采集完全够用。如果你的应用要求更高采样率或者需要真正的硬件SPI接口那可能得考虑ADS1220但成本和代码复杂度都会上去。1.3 这套方案能用到哪些场景我用这套STM32F103CS1237做的是一台电子秤但它的适用范围远不止称重。桥式应变传感器、膜片式压力传感器、张力传感器本质上都是差分小信号输出CS1237的差分输入刚好匹配。做温度采集时配合热电偶放大电路也可以如果只把CS1237当高精度电压表用比如测量电池内阻压降、检测微小电流在采样电阻上的压降同样可行。整体来说这套方案是一个“小信号高精度采集”的基础框架换个传感器前端就能适配不同项目。2. 掌握CS1237的接口时序才能不踩坑2.1 CS1237引脚和内部结构CS1237是Sigma-Delta ADC内部结构比逐次逼近型ADC复杂一些但用户层面主要关心几个点差分模拟输入VINP和VINN、模拟电源AVDD和AGND、数字电源DVDD和DGND、串行时钟SCLK、数据口SDO。这里的电源和地有点讲究。AVDD和DVDD最好分开走线AGND和DGND在PCB上单点汇合。Sigma-Delta ADC内部有数字滤波器和振荡器数字电路的开头噪声如果串到模拟电源上会直接影响转换结果这在24位分辨率下尤其明显。STM32F103的3.3V电源如果纹波大建议给CS1237的AVDD加一级LC滤波或单独的LDO。和HX711不同CS1237没有专门的片选脚。SCLK由主机提供SDO是半双工数据线读取数据时它作为输出发送命令字时它作为输入。所以代码里需要切换GPIO方向这也是后面调试最容易出错的地方。2.2 类SPI时序拆解从DRDY到24位数据CS1237的通信时序可以简单分成三个阶段理解这三步就能自己写驱动。第一步是等待转换完成。芯片上电后开始连续转换每当一次转换结束SDO会被拉低。主机检测到SDO为低电平时就知道数据准备好了可以去读了。第二步是读取24位转换结果。主机开始输出SCLK时钟前24个SCLK下降沿CS1237把转换结果从最高位到最低位一位一位送出来数据格式是二进制补码。主机在SCLK上升沿采样SDO引脚就能完整拿到24位数据。第三步是发送命令字。读完数据后还不能结束紧接着要继续提供8个SCLK同时通过SDO把控制命令字发给CS1237。命令字决定了PGA增益、输出速率这些参数。命令字发完之后SDO恢复为输出状态等待下一次转换完成信号。这个时序没有片选脚所以主机和从机的动作必须严格按顺序来拉低SCLK启动读操作、读24位、继续打时钟、写8位命令、释放总线。任何一步漏掉或者时钟数不对芯片状态就错乱了。2.3 命令字配置与注意事项CS1237的控制命令字是8位主要包含增益设置和输出速率设置。不同批次芯片的固件版本可能存在细微差异我用的这颗芯片实测下来PGA128、速率1280Hz对应的命令字是0x3CPGA128、速率40Hz对应0x34。这两个值能稳定工作但我强烈建议你拿到芯片后先用逻辑分析仪对照官方数据手册确认一遍再定宏定义。这里有个容易忽略的坑命令字不是上电初始化时写一次就行而是每次读取完24位数据之后都要发送一次。换句话说CS1237的命令字是“边读边配置”的你读数据的这个周期配置的参数决定的是下一次转换的参数。如果代码里只在初始化时发一次命令后面每次读取都会沿用上电默认配置这是很多“读出来的值不对劲”问题的根源。时钟频率也要注意。SCLK频率别太高我用2MHz以下的时钟跑得比较稳。高速时钟下SDO信号边沿变陡长线传输容易产生振铃反而引入误码。对称重这种低速应用SCLK用1MHz左右足够完全没必要追求速度。3. 硬件连接从最小系统到模拟前端3.1 STM32F103与CS1237的引脚对应我这次用的是STM32F103C8T6最小系统板CS1237是SOP-8封装自己画了一个转接小板。接线方式如下CS1237引脚STM32F103引脚说明SCLKPB5推挽输出接串行时钟SDOPB4双向数据线需切换输入输出DVDD3.3V数字电源DGNDGND数字地AVDD3.3V模拟电源建议加滤波AGNDGND模拟地VINP传感器正输出差分正输入VINN传感器负输出差分负输入之所以选PB4和PB5是因为这两个脚在同一个GPIOB端口操作方便。但这里有个非常大的坑STM32F103的PB4和PB3默认不是普通GPIO它们被占用于JTAG功能。如果直接用默认配置初始化PB4根本拉不低也读不了数据。必须先把JTAG关闭、保留SWD代码里要执行AFIO重映射把PB4和PB3释放为普通IO。在HAL库下这个操作是在初始化GPIO之前调用__HAL_AFIO_REMAP_SWJ_NOJTAG();标准库则是GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);如果不做这一步程序大概率会死在等待SDO拉低的循环里。这个问题我在第一次调试时折腾了快两个小时排查到最后才发现是JTAG占用了引脚。3.2 电源设计和参考电压CS1237有内部基准标称约2.5V所以外围不需要额外接参考芯片这对降低成本很有帮助。但这不意味着电源可以随便接。24位ADC的LSB非常小在PGA128时1个LSB对应的输入电压是2.5V除以128再除以2的23次方约2.3nV。到了这个量级任何电源纹波、地弹噪声都会被放大到数据里。我的做法是DVDD直接接STM32F103的3.3VAVDD单独用一个LC滤波电路串联一颗10uH电感并联10uF和0.1uF两个电容到模拟地。AGND和DGND在PCB上一点相连连线尽量短粗。传感器激励电压也尽量从模拟电源取如果传感器需要5V激励要保证5V电源纹波足够小否则纹波会直接耦合进差分信号。这里有个要注意的地方CS1237的模拟输入是差分形式理论上能抑制共模干扰但如果传感器和ADC的参考地不是同一个地共模电压超出允许范围差分优势就没意义了。传感器地、ADC模拟地、单片机数字地一定要理顺单点接地原则不能破。3.3 差分输入和传感器接线我用的称重传感器是常见的四线制应变式电桥红黑两根是激励线绿白两根是信号线。信号线接CS1237的VINP和VINN激励线接电源和地。接线时两个信号线要尽量等长、靠近避免形成大的环路面积。如果项目对精度要求更高可以考虑用屏蔽双绞线屏蔽层在ADC端单点接地。传感器输出信号线不建议直接飞线到转接板上最好用杜邦线加双绞处理或者直接在PCB上走差分对。调试阶段用杜邦线方便但要注意杜邦线长度越短越好超过20cm就很容易引入噪声。我最初用30cm的杜邦线连接传感器数据跳变非常剧烈换成10cm短线之后明显改善。还有一个容易犯错的地方传感器信号线接反读出来的数据就是负值而且是满码方向相反。判断正反的方法很简单给传感器施加一个已知方向的力看读出值是正向增大还是反向减小。代码里把VINP和VINN对调或者软件里取反都可以解决。4. STM32F103驱动代码从读取到可用4.1 GPIO初始化和重映射配置CubeMX里的配置很简单PB5设为推挽输出PB4设为输入上拉可选。但不要忘了在初始化前关闭JTAG。代码入口处做三件事使能GPIOB时钟、AFIO时钟执行SWJ重映射然后初始化GPIO。顺序不能乱先重映射再初始化引脚否则初始化的时候PB4还是JTAG复用状态。PB4配置为上拉输入是因为CS1237的SDO空闲状态是高电平上拉能让电平状态明确。读取数据时通过循环检测这个引脚是否被拉低来判断转换是否完成。4.2 完整通信函数读取24位并发送命令字下面这段代码是我项目中实际使用的HAL库风格标准库思路完全一样。其中delay_us函数在72MHz主频下可以用简单的循环实现或者用定时器产生微秒延时我这里用了空循环加nop的写法实际使用需要根据自己的主频校准。#define CS1237_GPIO_PORT GPIOB #define CS1237_SCLK_PIN GPIO_PIN_5 #define CS1237_SDO_PIN GPIO_PIN_4 // 命令字高4位或特定位对应增益和速率以下为实测可用值 #define CS1237_CMD_PGA128_10HZ 0x3C #define CS1237_CMD_PGA128_40HZ 0x34 #define CS1237_CMD_PGA128_640HZ 0x2C #define CS1237_CMD_PGA128_1280HZ 0x3C static void delay_us(uint32_t us) { // 72MHz主频下粗略延时 uint32_t i; for (i 0; i us * 8; i) { __NOP(); } } uint8_t CS1237_ReadValue(int32_t *value) { uint32_t raw 0; uint8_t i; uint8_t cmd CS1237_CMD_PGA128_40HZ; uint16_t timeout 100000; GPIO_InitTypeDef gpio; // 第一步等待DRDYSDO拉低 while (HAL_GPIO_ReadPin(CS1237_GPIO_PORT, CS1237_SDO_PIN) GPIO_PIN_SET) { if (--timeout 0) return 0; // 超时返回失败 } // 第二步读取24位数据 for (i 0; i 24; i) { HAL_GPIO_WritePin(CS1237_GPIO_PORT, CS1237_SCLK_PIN, GPIO_PIN_SET); delay_us(2); raw 1; if (HAL_GPIO_ReadPin(CS1237_GPIO_PORT, CS1237_SDO_PIN) GPIO_PIN_SET) { raw | 1; } HAL_GPIO_WritePin(CS1237_GPIO_PORT, CS1237_SCLK_PIN, GPIO_PIN_RESET); delay_us(2); } // 第三步SDO切为输出发送命令字 gpio.Pin CS1237_SDO_PIN; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(CS1237_GPIO_PORT, gpio); for (i 0; i 8; i) { if (cmd 0x80) { HAL_GPIO_WritePin(CS1237_GPIO_PORT, CS1237_SDO_PIN, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(CS1237_GPIO_PORT, CS1237_SDO_PIN, GPIO_PIN_RESET); } cmd 1; HAL_GPIO_WritePin(CS1237_GPIO_PORT, CS1237_SCLK_PIN, GPIO_PIN_SET); delay_us(2); HAL_GPIO_WritePin(CS1237_GPIO_PORT, CS1237_SCLK_PIN, GPIO_PIN_RESET); delay_us(2); } // SDO恢复为输入 gpio.Pin CS1237_SDO_PIN; gpio.Mode GPIO_MODE_INPUT; gpio.Pull GPIO_PULLUP; HAL_GPIO_Init(CS1237_GPIO_PORT, gpio); // 24位补码转有符号数 if (raw 0x800000) { raw | 0xFF000000; } *value (int32_t)raw; return 1; }代码逻辑其实不复杂核心就是“等待SDO为低、打24个时钟读数据、SDO切输出、打8个时钟发命令、SDO切回输入”。这个流程每次转换都要执行一遍。我在代码开头设置了超时保护避免芯片没有正常工作时程序卡死调试阶段这个超时判断能省很多事。4.3 把原始码换算成实际电压和物理量CS1237输出的是24位二进制补码满量程对应的是输入电压等于参考电压除以PGA。以PGA128、内部参考2.5V为例ADC输入端的满幅电压范围约为±19.53mV。但注意24位ADC的代码是对整个正负量程编码的所以正满码是8388607负满码是-8388608。换算公式是实际输入电压 原始码 / 8388608 * (2.5V / PGA)例如原始码为1000000那么输入电压等于1000000 / 8388608 * 19.53mV ≈ 2.33mV换算到称重物理量时要结合传感器的灵敏度和激励电压。假设传感器灵敏度2.0mV/V激励电压5V那么满量程输出就是10mV。如果此时PGA128ADC满幅对应19.53mV传感器满量程10mV只占了约51.2%的ADC量程。3kg满量程对应的原始码大约是8388608 * (10mV / 19.53mV) ≈ 42949671个码对应的重量是3kg / 4294967 ≈ 0.7mg这个分辨率远远超过项目需求。实际使用中由于噪声存在有效分辨率到不了0.7mg这个量级但这也说明增益和量程匹配上还有余量如果传感器满量程输出更小可以保持PGA128如果传感器输出更大就要降低增益防止超量程。4.4 主循环里怎么稳定调用主程序里不要每次都从头初始化GPIO只需要在初始化阶段做一次重映射和引脚配置。读取数据放到主循环里按需执行或者用定时器以固定频率触发。如果要做连续采样再平均可以写一个简单的中断标志定时器每10ms置一次标志主循环里检测到标志后读取一次数据。我实测下来在1280Hz速率下调用一次完整读函数大约耗时几百微秒对主循环几乎没有压力。需要注意的一点是读取数据期间不要让高优先级中断打断太长时间因为CS1237的时序是连续时钟脉冲中间断了整个通信周期就废了。最好是关闭中断或者用临界区保护一下这段代码等命令字发完再开中断。5. 24位ADC也需要滤波噪声来源与处理5.1 为什么24位分辨率还是跳码很多新手觉得24位ADC读出来的数据就应该纹丝不动实际完全不是这么回事。Sigma-Delta ADC的24位是理论分辨率有效分辨率会受到噪声限制。这里的噪声来源主要有三个电源噪声、参考电压噪声、传感器本身的机械和电噪声。电源噪声是最普遍的。如果AVDD直接用STM32F103的3.3V而这个3.3V来自最小系统板上的AMS1117那么MCU数字电路频繁翻转时3.3V上的毛刺会直接进入ADC模拟电源。我实测在未加LC滤波时ADC读数末位跳动达到几十个码加滤波后降到了一两个码以内。传感器本身的噪声也不能忽略。应变式传感器内部是电阻应变片金属电阻本身就有热噪声和1/f噪声信号越微弱前端噪声占比越大。所以即使ADC本身很安静传感器噪声也会限制最终有效分辨率。5.2 实用的滤波方案对比针对不同场景我总结了几种滤波方案滤波方式特点适用场景算术平均采样N次求和取平均简单有效静态称重、压力稳定测量滑动平均每次更新一个点输出连续需要连续显示重量的场合中值滤波排序取中间值抗脉冲干扰有电磁干扰、偶尔尖峰的场景一阶低通加权递推响应平滑动态称重加静态平衡卡尔曼滤波状态估计响应和稳定兼顾高要求项目实现稍复杂称重项目我推荐滑动平均加中值滤波组合或者只做滑动平均。我先做8次滑动平均再对相邻两次平均结果做一阶低通效果不错。代码示例如下#define FILTER_LEN 8 int32_t filter_buf[FILTER_LEN]; uint8_t filter_index 0; int32_t MovingAverageFilter(int32_t new_value) { int32_t sum 0; uint8_t i; filter_buf[filter_index] new_value; if (filter_index FILTER_LEN) { filter_index 0; } for (i 0; i FILTER_LEN; i) { sum filter_buf[i]; } return sum / FILTER_LEN; }这里有个细节滤波窗口越大数据越平滑但响应速度越慢。电子秤如果放上重物要1秒才稳定体验就很差。8个点的滑动平均加上一阶低通稳定时间在200ms左右比较适中。5.3 去直流和归一化的小技巧热搜词里有人提到“ADC捕捉信号后去直流和归一化”这个在振动或动态信号测量里很常用。CS1237是差分输入如果零点偏置比较大可以在软件里先采集一段静态数据求平均作为零点基准之后的每个采样值都减去这个零点基准。这样能把零点漂移和放大器偏置电压的影响去掉。归一化则是把原始码转换成0到1或者工程单位的比例值。比如电子秤把空秤时的码值记为zero把满量程3kg对应码值记为span那么当前重量公式是weight (raw - zero) / (span - zero) * full_scale每次上电先做一次零点校准把zero更新掉如果做过砝码标定把span也存下来。这样比硬算电压再换算重量更可靠因为直接消除了传感器灵敏度和激励电压的误差。6. 调试全流程与常见问题排查6.1 从接线到出数的标准调试步骤拿到板子不要急着写满整个程序我建议按下面这个顺序来调试第一步检查电源。用万用表量CS1237的AVDD和DVDD确认为3.3V左右AGND和DGND电压接近0V。第二步用GPIO翻转法测SDO电平。写一个最简单的程序把PB4配置成输入主循环里读这个引脚状态并通过串口打印。正常情况下CS1237上电后SDO应该周期性拉低说明ADC在正常转换。如果串口一直打印高电平问题多半在硬件连接或者JTAG重映射。第三步写一个只读数据的函数不发送命令字看能不能拿到24位原始数据。很多代码例程要求发命令字但初次调试时可以简化先确认数据链路通不通。第四步加上命令字发送验证配置是否生效。比如把速率从40Hz改到1280Hz观察数据更新频率是否变快。如果速率没变化说明命令字没发进去要检查SDO切输出的时机和命令字位序。第五步接入传感器施加压力观察数据方向。这一步能验证VINP和VINN接线的正反。第六步加入滤波和标定逻辑完成最终功能。6.2 常见问题速查表我把实际调试中遇到的高频问题整理成了表格方便大家对照排查。现象可能原因排查方法SDO一直为高读不到低电平PB4被JTAG占用DVDD没供电芯片损坏确认重映射代码量DVDD电压换芯片测试数据超时函数返回失败通信时序错误SCLK频率过高芯片进入异常状态用逻辑分析仪抓SCLK和SDO波形降低时钟频率读出的数据恒定不变命令字没发送成功芯片配置被复位确认每次读完都发命令字检查延时是否足够数据跳变严重电源纹波大差分线太长传感器屏蔽层未接地加LC滤波缩短杜邦线检查接地数据全为负值VINP和VINN接反调换两根信号线软件取反数据满码或超量程输入电压超过PGA对应量程降低增益检查传感器激励电压是否过高数据有周期性波动传感器附近有交变电磁干扰电源50Hz工频干扰加屏蔽用中值滤波检查接地环路6.3 经验调试高精度ADC的几条心得踩过几次坑之后我总结出了几条值得分享的经验。第一调试时一定要用逻辑分析仪抓时序不要靠猜。CS1237的SCLK和SDO波形非常直观数一数时钟个数、看SDO切换方向基本能定位九成问题。我最初以为命令字没发出去后来用逻辑分析仪一看才发现SCLK多了两个边沿原来是延时循环写得太随意导致时序错位。第二电源和地线永远是最优先排查的对象。很多“数据不稳定”的锅其实是电源背的。我给板子加了一级LC滤波和0.1uF退耦电容后数据跳变直接少了一个数量级。调试高精度ADC万用表测电压只是第一步最好再用示波器看电源纹波。第三命令字一定不能只在初始化时发一次。CS1237的设计是每次读数据都要带命令字这个和普通SPI设备很不一样。如果代码里漏了芯片会沿用上一次的命令配置有些情况下也可能进入默认配置导致你设置的PGA和速率完全没生效。第四传感器的激励电压和ADC参考电压最好同源。称重传感器是比例式器件输出信号与激励电压成正比。如果激励电压有温漂ADC的参考电压没有跟着变化就会产生比例误差。这也是为什么很多称重方案用同一个基准同时驱动电桥和ADC参考的原因。CS1237内部参考和外部激励不同源严格来说会受到激励源温漂影响但做普通项目够用。7. 这套方案还能怎么扩展7.1 从称重到压力、扭矩、位移采集CS1237的本质是差分小信号ADC称重传感器只是其中一种前端。换用扩散硅压力传感器加上恒流源激励电路就能做气压或液压测量换用扭矩传感器可以测旋转轴的扭力换用应变式位移传感器可以做微小形变检测。所有这些都是差分电桥输出接口逻辑完全一样只改标定参数即可。这意味着你只要把硬件和驱动调通一次后面换传感器就是改改量程和标定系数的事。7.2 配合定时器中断做连续采样如果项目需要动态称重或者在线监测可以在STM32F103里开一个定时器中断比如1kHz频率触发一次CS1237读取读到的数据放进环形缓冲区主循环负责滤波、显示和通信。这样做的好处是采样间隔均匀不会因为主循环里其他任务阻塞导致采样抖动。我实测用定时器中断配合8点滑动平均重量数据既平顺又有实时性比单纯主循环轮询效果好很多。注意中断里调用CS1237读取函数时如果函数内部有延时循环中断占用时间会比较长。可以把读取操作拆成状态机每次中断只执行一步比如本次中断读8位下次中断再读8位避免长时间关中断影响系统实时性。对于称重这种低速应用其实直接在主循环里调用就够了不用过于纠结中断优化。7.3 后续可以做的标定与上位机真正好用的称重系统需要标定。建议程序里保存两组参数零点码值zero和满量程码值span通过串口命令触发标定而不是每次改代码重新烧录。我后来用STM32F103的Flash模拟EEPROM把标定参数存进去掉电不丢失这样一台设备和一组参数绑定现场调试方便很多。另外把原始码和换算后的重量通过串口发到PC配合串口助手或者Python脚本画实时曲线能非常直观地观察到滤波前后的差异。这一步对调参帮助极大尤其是判断滑动平均窗口长度和一阶低通系数是否合适。最后再分享一个小技巧CS1237这枚芯片虽然便宜但用之前认真看一遍数据手册的时序图和寄存器说明比到处抄代码可靠得多。芯片本身不复杂出问题基本都是细节把时序吃透了后面换用其他Sigma-Delta ADC也能很快上手。
返回列表