1. 项目缘起:一个被SPI外衣包裹的“非标准”ADC
CS1237这颗芯片,第一次拿到手看数据手册的时候,我差点直接把它当普通SPI从设备去写驱动。24位分辨率、内置PGA、两线制通信,看起来就是个高精度ADC该有的样子。但真正把STM32F103的SPI外设初始化好、片选拉低、时钟打出去之后,读回来的数据要么全0,要么全1,偶尔蹦出几个看似有意义的数值,下一帧又变成乱码。折腾了大半天才反应过来——CS1237的通信接口虽然引脚叫SCLK和DRDY/DOUT,但它压根不是标准SPI协议。
这个项目要解决的核心问题就两个:第一,用STM32的GPIO模拟时序去跟CS1237正确通信,把ADC原始码读出来;第二,解决读回来的数据乱跳问题,让称重或压力采样的数值稳定下来。适合正在做电子秤、压力变送器、微小信号采集的嵌入式开发者,尤其是那些被“SPI”两个字误导、在标准外设库里绕圈子的朋友。下面我把整个调试过程拆开讲,从协议理解到代码实现,再到数据稳定的处理,每一步都附上我实际踩过的坑。
2. 先搞懂CS1237到底怎么通信:别被SPI带偏了
2.1 两线制时序的本质:它更像一个“带时钟的串行移位”
CS1237只有两根信号线:SCLK(时钟)和DRDY/DOUT(数据就绪/数据输出复用引脚)。主机给SCLK提供时钟,芯片在时钟沿把数据移位输出。听起来跟SPI的MISO+SCLK很像,但关键差异在于:标准SPI有独立的片选(CS)和主从模式约定,而CS1237没有片选引脚,它的“片选”是靠SCLK的电平状态和时序来隐式管理的。
具体来说,CS1237在SCLK为低电平时,DRDY/DOUT引脚会输出数据就绪信号——当转换完成时,该引脚会从高变低,表示有新数据可以读。主机检测到这个下降沿后,开始给SCLK脉冲,每个时钟周期从DOUT读一位数据。24位数据读完后,如果继续给时钟,芯片会输出配置寄存器的内容。整个过程中,SCLK空闲时必须保持低电平,这跟标准SPI的CPOL=0、CPHA=0模式在形式上接近,但数据建立和保持时间的要求完全不同。
我实测下来,CS1237的SCLK高电平最小宽度约100ns,低电平最小宽度也差不多,这意味着STM32F103在72MHz主频下,用GPIO翻转完全能跑,但用硬件SPI外设反而容易出问题——因为硬件SPI的时钟频率和相位配置很难精确匹配这种非标准时序,而且片选管理逻辑对不上。
2.2 为什么硬件SPI外设会翻车:三个致命不匹配
第一个不匹配是片选逻辑。STM32的SPI外设通常配合NSS引脚或软件片选使用,片选拉低后才开始传输。但CS1237没有片选概念,它的DRDY信号本身就是数据就绪标志,如果按标准SPI流程先拉片选再等数据,时序完全对不上。
第二个不匹配是时钟极性相位。标准SPI有四种模式组合,CS1237要求SCLK空闲低电平、数据在上升沿被从机采样(或输出),这对应CPOL=0、CPHA=0或CPHA=1,但具体是哪个还要看数据手册的时序图。我试过用SPI模式0去读,数据偶尔对偶尔错,后来用逻辑分析仪抓波形才发现,CS1237的数据在SCLK下降沿变化、上升沿稳定,主机应该在上升沿采样,这跟SPI模式0的采样沿一致,但问题出在第一个时钟脉冲之前DRDY的状态判断上。
第三个不匹配是数据长度和帧结构。标准SPI可以配置8位或16位数据帧,但CS1237是24位数据加若干配置位,硬件SPI的移位寄存器长度不够,需要软件拼接。而且读完之后还要处理符号扩展和补码转换,这些都不是硬件SPI能自动完成的。
注意:如果你正在用STM32的硬件SPI去驱动CS1237,建议立刻停下来,改用GPIO模拟时序。这不是性能问题,是协议根本不兼容。
2.3 GPIO模拟时序的代码框架:从DRDY检测到24位拼接
我用的是STM32F103C8T6,GPIO口选的是PA5(SCLK)和PA6(DOUT),都是普通推挽输出和浮空输入模式。核心代码分三步:等待DRDY下降沿、移位读取24位、处理符号位。
#define CS1237_SCLK_H() GPIO_SetBits(GPIOA, GPIO_Pin_5) #define CS1237_SCLK_L() GPIO_ResetBits(GPIOA, GPIO_Pin_5) #define CS1237_DOUT_RD() GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6) uint32_t CS1237_ReadRaw(void) { uint32_t data = 0; uint8_t i; // 等待DRDY变低,超时退出 uint32_t timeout = 0; while(CS1237_DOUT_RD() == 1) { if(++timeout > 100000) return 0xFFFFFFFF; } // 读取24位数据,MSB先出 for(i = 0; i < 24; i++) { CS1237_SCLK_H(); __NOP(); __NOP(); __NOP(); // 约100ns延时 data <<= 1; if(CS1237_DOUT_RD()) data |= 1; CS1237_SCLK_L(); __NOP(); __NOP(); __NOP(); } // 补第25个时钟,让DOUT释放 CS1237_SCLK_H(); __NOP(); __NOP(); __NOP(); CS1237_SCLK_L(); // 符号扩展:24位补码转32位有符号 if(data & 0x800000) { data |= 0xFF000000; } return data; }这段代码里有个细节:第25个时钟脉冲是必须的。CS1237在24位数据输出后,如果SCLK继续给脉冲,它会输出配置寄存器内容,但如果你只给24个脉冲就停止,DOUT引脚可能保持最后一位的状态,影响下一次DRDY检测。我试过省掉第25个脉冲,结果下一次读数据时DRDY一直不拉低,芯片像死机了一样。加上这个脉冲后,DOUT在SCLK下降沿后释放,DRDY才能正常恢复高电平。
3. 数据乱跳的根源排查:从电源到参考电压再到滤波
3.1 电源纹波和参考电压:最容易被忽视的“隐形杀手”
数据能读出来了,但数值在±50个码值之间跳,对于24位ADC来说,50个码值相当于在5V满量程下约15μV的波动。一开始我以为是芯片本身噪声,后来用示波器交流耦合档测AVDD引脚,发现上面有约20mVpp的纹波,频率跟开关电源的开关频率一致。CS1237的电源抑制比虽然不错,但20mV的纹波足以让输出码值跳动几十个LSB。
解决办法是在AVDD和AGND之间并一颗10μF钽电容加一颗100nF陶瓷电容,位置尽量靠近芯片引脚。如果条件允许,AVDD用LDO单独供电,不要跟数字部分共用开关电源。我改用了一颗AMS1117-3.3给模拟部分供电后,纹波降到2mVpp以下,数据跳动立刻从±50降到±5以内。
参考电压方面,CS1237内置了参考电压,但也可以外接。内置参考的温漂和初始精度一般,如果对绝对精度要求高,建议外接一颗REF3033或类似的高精度基准。我实测内置参考在室温下短期稳定性还行,但手摸芯片表面温度变化几度,输出码值就漂了十几个LSB。后来外接了一颗2.5V基准,漂移明显改善。
3.2 模拟输入端处理:差分输入和共模电压的设置
CS1237是差分输入,但很多人只接单端信号,另一端直接接地。这样做不是不行,但共模电压会偏离最佳工作点。数据手册建议共模电压设置在AVDD/2附近,这样PGA的线性度最好。我试过把负输入端接到一个由两颗等值电阻分压得到的AVDD/2上,噪声比直接接地低了约30%。
输入端的RC滤波也很关键。我在IN+和IN-各串了一颗100Ω电阻,再对地并一颗100nF电容,截止频率约16kHz。对于称重传感器这种带宽很低的信号,这个滤波能有效抑制高频噪声。但要注意,电阻不能太大,否则会引入热噪声;电容也不能太大,否则影响建立时间。我试过用1kΩ+1μF的组合,结果每次通道切换后要等好几秒数据才稳定,响应太慢。
3.3 软件滤波:中位值平均滤波的实操参数
硬件上把噪声压到±5个码值后,软件再滤一道,基本能做到±1以内。我常用的是中位值平均滤波法:连续采N个样本,去掉最大和最小,剩下的求平均。N取多少有讲究——N太小滤波效果不明显,N太大响应变慢。
对于称重场景,我一般取N=8或N=16。以N=16为例,采样率如果设为10Hz,那么一次滤波输出需要1.6秒,对于静态称重完全够用。如果是动态称重,N要降到4或8。代码实现上,用一个长度为N的数组做滑动窗口,每次新数据进来后排序,去掉首尾各一个,再求平均。
#define FILTER_N 16 int32_t filter_buf[FILTER_N]; uint8_t filter_idx = 0; int32_t CS1237_Filter(int32_t new_val) { int32_t sum = 0; int32_t temp; uint8_t i, j; filter_buf[filter_idx] = new_val; filter_idx = (filter_idx + 1) % FILTER_N; // 复制到临时数组排序 int32_t sort_buf[FILTER_N]; for(i = 0; i < FILTER_N; i++) sort_buf[i] = filter_buf[i]; // 冒泡排序 for(i = 0; i < FILTER_N - 1; i++) { for(j = 0; j < FILTER_N - 1 - i; j++) { if(sort_buf[j] > sort_buf[j+1]) { temp = sort_buf[j]; sort_buf[j] = sort_buf[j+1]; sort_buf[j+1] = temp; } } } // 去掉最大最小各一个,求平均 for(i = 1; i < FILTER_N - 1; i++) sum += sort_buf[i]; return sum / (FILTER_N - 2); }这个滤波函数在STM32F103上跑一次大约几十微秒,对于10Hz到100Hz的采样率完全没压力。实测下来,原始数据±5码值跳动,滤波后能稳定在±1以内。
4. 完整调试流程:从硬件连接到数据输出
4.1 硬件连接检查清单
在写代码之前,先把硬件连接确认一遍。CS1237的引脚不多,但每个都不能接错:
| 引脚 | 连接 | 注意事项 |
|---|---|---|
| AVDD | 3.3V模拟电源 | 必须加10μF+100nF去耦,靠近引脚 |
| AGND | 模拟地 | 与数字地单点连接 |
| DVDD | 3.3V数字电源 | 可与AVDD共用,但建议分开走线 |
| DGND | 数字地 | 与AGND在芯片下方汇合 |
| SCLK | PA5 | GPIO推挽输出,空闲低电平 |
| DOUT | PA6 | GPIO浮空输入或上拉输入 |
| IN+ | 传感器正输出 | 串100Ω,对地100nF |
| IN- | 传感器负输出或AVDD/2 | 同上 |
| REF | 外接基准或悬空 | 用内置基准时悬空 |
我遇到过最坑的一个问题是DOUT引脚没有上拉电阻。CS1237的DOUT是开漏输出还是推挽输出,数据手册里写得比较模糊。实测发现,如果不加上拉电阻,DOUT在空闲时电平不确定,导致DRDY检测误触发。后来在DOUT和DVDD之间加了一颗10kΩ上拉电阻,问题解决。
4.2 STM32CubeMX配置要点
虽然CS1237用GPIO模拟时序,但STM32CubeMX里还是要配置好时钟和GPIO。我的配置如下:
- 系统时钟:HSE 8MHz,PLL倍频到72MHz
- PA5:GPIO_Output,推挽,无上拉下拉,初始低电平
- PA6:GPIO_Input,浮空或上拉
- 调试串口:USART1,PA9/PA10,115200波特率
- 定时器:TIM2,用于采样周期控制,1ms中断
CubeMX生成代码后,在main函数里初始化CS1237的GPIO,然后在一个定时器中断或主循环里调用读取函数。我习惯用定时器中断触发采样,这样采样周期精确,不受主循环其他任务影响。
4.3 采样率与建立时间的权衡
CS1237的采样率可以通过配置寄存器设置,从10Hz到1280Hz不等。采样率越高,噪声越大,这是ΔΣ ADC的固有特性。对于称重应用,我一般设10Hz或40Hz,此时有效分辨率最高。如果设到1280Hz,噪声会大很多,但响应快,适合动态测量。
配置寄存器的写入也是通过SCLK和DOUT完成的。写配置时,先给SCLK脉冲让DOUT输出配置位,然后在特定时钟沿把数据写到DOUT上。具体时序比较绕,我建议直接参考数据手册的配置流程图,用GPIO模拟实现。写配置的代码比读数据复杂,因为要双向切换DOUT的输入输出方向。
我实测下来,配置寄存器写入后需要等一段时间让芯片内部重新建立,大约需要几个转换周期。如果写完配置立刻读数据,前几帧可能是旧配置下的结果。我的做法是写完配置后延时100ms再开始读。
4.4 数据输出与校准
原始码值读出来后,要转换成实际物理量,需要做两步校准:零点校准和满量程校准。零点校准是在空载时读一组数据取平均,记为offset;满量程校准是在加已知重量时读一组数据取平均,记为full。实际重量 = (raw - offset) * known_weight / (full - offset)。
校准过程中要注意,传感器要预热。我用的称重传感器,刚上电时零点漂移比较大,一般要等5到10分钟才稳定。如果校准后数据还是漂,检查传感器供电是否稳定,以及机械结构是否有应力变化。
5. 常见问题速查与避坑经验
5.1 数据全0或全1:先查时序,再查电源
数据全0或全1是最常见的问题。我的排查顺序是:先用逻辑分析仪抓SCLK和DOUT波形,确认时钟频率和数量对不对;然后查DRDY是否正常拉低;最后查电源和参考电压。有一次我遇到数据全1,查了半天发现是DOUT引脚被配置成了推挽输出,跟芯片的输出冲突了,改成浮空输入就好了。
5.2 数据偶尔跳变:检查中断优先级和临界区
如果数据大部分稳定但偶尔跳一个很大的值,通常是中断打断导致的时序错乱。CS1237的读取过程不能被中断打断,否则SCLK脉冲宽度会变,数据可能移位错误。我的做法是在读取函数前后关中断,读完后立刻开中断。关中断时间大约几十微秒,对系统实时性影响很小。
__disable_irq(); raw = CS1237_ReadRaw(); __enable_irq();5.3 长时间运行后数据漂移:温漂和基准老化
连续运行几小时后数据慢慢漂移,一般是温漂引起的。CS1237内置基准的温漂典型值是50ppm/°C,如果环境温度变化10°C,基准电压变化0.05%,对于24位ADC来说就是几千个码值。解决办法是用外接基准,或者做温度补偿。我在产品里加了一颗DS18B20测环境温度,用查表法做补偿,效果不错。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 数据全0 | SCLK无输出 | 示波器测PA5 | 检查GPIO初始化 |
| 数据全1 | DOUT配置错误 | 测PA6电平 | 改为浮空输入 |
| 数据跳变大 | 电源纹波大 | 示波器测AVDD | 加去耦电容或LDO |
| 偶尔跳变 | 中断打断时序 | 关中断测试 | 读取时关中断 |
| 长时间漂移 | 温漂 | 测环境温度 | 外接基准或补偿 |
| DRDY不拉低 | 缺少第25时钟 | 逻辑分析仪 | 补第25个脉冲 |
| 配置写入无效 | 时序不对 | 对照数据手册 | 重新实现写时序 |
提示:逻辑分析仪是调试CS1237的必备工具,没有它基本靠猜。建议至少用8通道、24MHz采样率的型号,能同时抓SCLK、DOUT和DRDY。
5.5 一个容易被忽略的细节:PCB布局
CS1237的PCB布局对噪声影响很大。我的经验是:模拟部分和数字部分分区布局,AGND和DGND在芯片下方单点连接;AVDD的走线尽量短粗,去耦电容紧贴引脚;SCLK和DOUT的走线远离模拟输入,避免数字噪声耦合到模拟端。有一次我为了布线方便,把SCLK走线从IN+下面穿过,结果噪声大了好几倍,后来改到板子另一面就好了。
6. 从能读到读得准:我的调试心得
整个项目从最初被SPI误导,到最终数据稳定在±1码值以内,前后花了大约一周时间。其中大部分时间不是在写代码,而是在排查硬件和时序问题。我的体会是,CS1237这类非标准接口的ADC,硬件设计和时序调试比软件算法重要得多。电源干净、参考稳定、布局合理,软件上只要时序对了,数据自然就稳。
另外,不要迷信数据手册上的典型应用电路。手册上的电路是理想情况,实际产品里还要考虑成本、空间和EMC。我最后用的电路跟手册推荐的有不少差异,比如去耦电容从一颗100nF加到10μF+100nF,输入滤波从简单的RC改成两级RC,这些都是根据实测结果调整的。
如果你也在调CS1237,建议先别急着写代码,拿逻辑分析仪把时序抓清楚,把电源纹波测明白,后面的事情会顺利很多。数据乱跳的时候,先怀疑硬件,再怀疑时序,最后才怀疑算法。这个顺序能帮你少走很多弯路。