ARTICLE DETAIL

资讯详情

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

CS1237与STM32通信避坑指南:非标准SPI时序与数据稳定方案

CS1237与STM32通信避坑指南:非标准SPI时序与数据稳定方案 1. 被SPI时序坑惨的那三天CS1237到底该怎么接STM32CS1237这颗24位ADC芯片在电子秤、压力变送器、精密测量这些小信号采集场景里出镜率极高。价格便宜、外围简单、内置PGA看起来是入门级高精度采集的性价比之选。但它和STM32之间的通信却是我见过最容易让人翻车的一类组合——不是芯片难用而是它的SPI根本不是标准SPI。我第一次上手CS1237的时候脑子里默认它是一颗标准SPI从机SCLK、DIN、DOUT、CS四线制接上STM32的硬件SPI外设配置好CPOL和CPHA读数据就完事了。结果代码烧进去读出来的数据要么全是0xFFFFFF要么全是0x800000偶尔跳几个乱七八糟的值完全没有规律。示波器一挂SCLK有波形DOUT也有翻转看起来通信是通的但数据就是不对。问题的根源在于CS1237的通信协议是类SPI而不是标准SPI。它没有片选信号CS数据帧长度固定为24个时钟读或25/26个时钟写DOUT在SCLK下降沿更新、上升沿稳定DIN在SCLK上升沿采样。更关键的是它要求SCLK在两次转换之间保持高电平一旦你在不该拉低的时候拉低了SCLK芯片内部状态机就会错乱后续读出来的数据全部偏移一位甚至几位。这个偏移一位的现象非常隐蔽。因为24位数据偏移一位之后数值看起来还是合理范围内的数只是比真实值大了一倍或小了一半你如果拿它去做标定会发现线性度极差但又不是完全没反应。我当时的调试记录里写着数据在0附近跳动加载砝码后数值增大但不成比例怀疑是增益配置错误。实际上根本不是增益问题是时序错位。所以这篇文章我想把CS1237和STM32配合使用的完整链路拆开讲从硬件连接到SPI模式选择从时序细节到数据稳定性处理从单次读取到连续转换的节奏控制。适合正在用CS1237做小信号采集、被数据跳动折磨、或者准备选型这颗芯片的人。我会把踩过的坑、验证过的方法、以及最终稳定运行的配置都写清楚让你少走几天弯路。2. CS1237的SPI到底特殊在哪里2.1 它不是标准SPI从机别用硬件SPI外设硬套很多人看到CS1237的引脚定义里有SCLK和DOUT第一反应就是这是SPI接口。但如果你去翻它的数据手册会发现它压根没提SPI这个词官方描述是两线制串行接口。这个两线制指的是SCLK和DOUT用于读取DIN用于配置本质上是一个自定义的同步串行协议。标准SPI从机的行为是片选拉低后主机给时钟从机在时钟边沿输出或采样数据片选拉高后从机复位。CS1237没有片选它的帧同步完全靠时钟数量来界定——连续给24个时钟就是读一次转换结果给25或26个时钟就是写配置寄存器。如果你用STM32的硬件SPI外设去驱动它硬件SPI会在每次传输前后自动控制CS引脚即使你配置为软件CS时序上也可能多出不必要的时钟沿而且硬件SPI的时钟极性、相位配置很难精确匹配CS1237的要求。我实测下来最稳的方案是用GPIO模拟时序。SCLK用一个普通推挽输出引脚DOUT配置为上拉输入DIN配置为推挽输出。这样每一个时钟沿的时机、每一次电平保持的时长你都能精确控制。虽然牺牲了一点速度但CS1237的转换速率本来就不高10Hz到1.28kHz可配GPIO模拟完全够用。2.2 时钟极性与数据建立时间的硬性要求CS1237的时序要求里有几个参数是必须卡死的。SCLK的下降沿用于更新DOUT数据也就是说从机在下降沿把下一位数据推到DOUT线上SCLK的上升沿用于采样主机应该在上升沿之后的一个短延时内读取DOUT电平。数据手册给出的建立时间典型值是100ns量级对于STM32F103这种72MHz主频的芯片来说一条NOP指令大约14ns插几条NOP就能满足。但这里有个容易忽略的点SCLK的高电平和低电平持续时间都有最小值要求。如果你用GPIO翻转来模拟时钟翻转太快可能导致从机还没完成数据更新就被下一个上升沿采样了。我的做法是在每个时钟沿之后插入一个短延时实测2微秒左右非常稳妥再快也没必要因为CS1237最快也就1.28kHz输出速率一个完整读取周期也就几十微秒。还有一个坑两次读取之间SCLK必须保持高电平。如果你在读取完成后把SCLK拉低并保持芯片会认为你在发起一次新的通信内部状态机可能进入错误状态。我一开始就是读完之后SCLK停在低电平结果第二次读取的数据完全错乱。后来改成读取完成后SCLK拉高并保持问题消失。2.3 24位数据帧的位序与符号扩展CS1237输出的是24位二进制补码数据MSB先出。也就是说你连续读24个时钟第一个时钟读到的就是符号位。如果这个位是1表示负数需要做符号扩展如果是0表示正数直接取低23位即可。很多人在处理这个数据时犯的错误是把24位数据直接当成无符号数处理结果负值全部变成接近16777215的大数看起来就像数据乱跳。正确的做法是先把24位拼成一个int32_t然后判断第23位从0开始计数是否为1如果是则高8位补1否则补0。int32_t cs1237_read_raw(void) { uint32_t raw 0; for (int i 0; i 24; i) { SCLK_LOW(); delay_us(2); SCLK_HIGH(); delay_us(2); raw 1; if (DOUT_READ()) raw | 1; } // 符号扩展 if (raw 0x800000) { raw | 0xFF000000; } return (int32_t)raw; }这段代码看起来简单但delay_us的精度很关键。如果你用的是HAL_Delay或者系统滴答定时器最小延时可能是1毫秒那读取一次就要24毫秒太慢了。建议用DWT或者简单的for循环做微秒级延时STM32F103上循环几十次差不多就是1微秒。3. 硬件连接与STM32 GPIO配置的细节3.1 上拉电阻不是可选项是必须项CS1237的DOUT引脚是开漏输出结构这意味着它只能把线拉低不能主动拉高。如果你不在DOUT上接上拉电阻读到的数据永远是0。我见过不止一个人在这里翻车代码查了半天最后发现是硬件少了一个10k电阻。上拉电阻的阻值选择也有讲究。太大比如100k会导致上升沿变缓在高速时钟下数据还没稳定就被采样了太小比如1k会增加功耗而且如果从机拉低能力不足可能拉不到低电平。10k到47k之间是比较稳妥的范围我一般用10k。SCLK和DIN是主机输出不需要上拉。但如果你用的STM32引脚在复位后默认是浮空输入建议在初始化时明确配置为推挽输出避免上电瞬间的毛刺被CS1237当成有效时钟。3.2 电源与参考电压的稳定性直接影响数据跳动CS1237的供电范围是2.7V到5.5V参考电压就是供电电压。这意味着如果你的电源有纹波参考电压就跟着抖采集到的数据自然也会跳。我一开始用STM32板子上的3.3V给CS1237供电数据跳动大概在几十个LSB。后来换成独立的LDO供电并在电源引脚旁边加了一个10微法和一个100纳法电容跳动降到了几个LSB。如果你对精度要求更高可以考虑用外部基准源给CS1237供电比如REF3033这类低噪声基准。但要注意CS1237的供电电流不大基准源的输出能力要够。另外模拟地和数字地建议分开走线最后在电源入口处单点汇合这个在PCB布局时就要考虑好。3.3 模拟输入端的滤波与屏蔽CS1237的输入是差分输入但很多人只用了单端输入另一端接地。这时候接地的质量就很关键。如果地线上有数字信号的回流电流会直接串到模拟输入端表现为数据周期性跳动。我的做法是在输入端加一个简单的RC低通滤波电阻用100欧姆电容用100纳法截止频率大概在16kHz左右对于称重、压力这类低频信号完全够用。如果信号源阻抗比较高电阻可以适当减小但不要低于50欧姆否则滤波效果不明显。另外输入走线尽量短远离SCLK和DIN这些数字信号线。如果实在避不开中间用地线隔离一下。这些看起来是硬件细节但数据稳定性问题有一大半出在硬件上软件滤波只能补救不能根治。4. 从单次读取到连续转换节奏控制与数据就绪判断4.1 如何知道数据准备好了CS1237没有中断引脚也没有数据就绪标志位。你只能通过定时读取来判断数据是否更新。它的输出速率由配置寄存器决定常见的有10Hz、40Hz、640Hz、1.28kHz几档。如果你配置为10Hz那就意味着每100毫秒更新一次数据你在这100毫秒内读多次读到的都是同一个值。我见过有人用轮询方式疯狂读取读到的数据看起来在跳其实是把同一个值读了好几遍然后误以为数据不稳定。正确的做法是根据配置的输出速率用定时器产生一个略大于转换周期的中断在中断里读取一次数据。比如配置为10Hz定时器设为110毫秒这样每次读到的都是新数据。如果你不想用定时器也可以在两次读取之间加一个足够长的延时。但延时会阻塞CPU不适合多任务场景。用定时器中断或者RTOS的任务延时更合理。4.2 连续读取时的SCLK空闲电平前面提到过两次读取之间SCLK必须保持高电平。但如果你用定时器中断读取中断结束后SCLK的状态取决于你最后一次操作。我的做法是在读取函数的末尾显式把SCLK拉高确保进入空闲状态。void cs1237_read_task(void) { int32_t raw cs1237_read_raw(); // 确保SCLK回到高电平 SCLK_HIGH(); // 后续处理... }这个细节看起来微不足道但如果你忘了下一次读取时SCLK从低电平开始第一个时钟沿的极性就反了读出来的24位数据整体偏移表现为数值突然变大或变小。我当初就是被这个现象迷惑了很久以为是芯片坏了。4.3 配置寄存器的写入时机CS1237的配置寄存器包括增益、输出速率、通道选择等。写入配置需要25或26个时钟具体取决于你要写哪些位。写入过程中SCLK的时序要求和读取类似但DIN上的数据要在SCLK上升沿之前稳定。这里有个坑写入配置后芯片需要一段时间才能生效而且第一次转换结果可能是旧的配置产生的。我的做法是写入配置后延时至少两个转换周期再开始读取数据。比如配置为10Hz写入后延时250毫秒再读确保读到的是新配置下的稳定值。另外配置寄存器的写入不是必须每次上电都做。如果你用外部EEPROM或者STM32的Flash保存了配置可以在上电时写入一次之后就不用再动了。但要注意CS1237没有非易失存储掉电后配置丢失所以每次上电都要重新写入。5. 数据乱跳的排查链路从现象到根因5.1 先区分是硬件噪声还是时序错误数据乱跳的表现形式不同根因也不同。如果数据是在真实值附近小幅波动比如±几十个LSB那大概率是电源噪声或输入端干扰属于硬件问题。如果数据是成倍偏移、符号错误、或者完全无规律的大数跳动那基本是时序或数据解析问题。我一般的排查顺序是先用示波器看SCLK和DOUT的波形确认时钟数量是24个确认DOUT在SCLK下降沿后变化、上升沿时稳定。如果波形没问题再检查数据解析代码确认符号扩展正确。最后才去查电源和输入端的硬件滤波。这个顺序很重要因为时序问题用软件滤波是解决不了的你滤波滤掉的是真实数据。而硬件噪声用软件滤波可以改善但根治还是要靠硬件。5.2 用已知输入验证数据链路最有效的调试方法是给CS1237一个已知的输入信号比如短接输入端差分输入为0看读出来的原始值是否接近0。如果读出来是0x800000附近说明符号扩展正确零点也正常。然后给一个小的已知电压比如用电阻分压得到10mV看读出来的值是否和理论计算一致。CS1237的满量程输入是±VREF/GAINVREF就是供电电压。假设供电3.3V增益128那满量程就是±25.8mV。10mV对应的码值大概是10/25.8*8388608≈325万。如果你读出来是650万那说明数据左移了一位检查时序。如果读出来是162万那说明增益配置错了检查配置寄存器。这个方法能快速定位问题是出在模拟端还是数字端。我当初就是靠短接输入发现读出来不是0才意识到时序有问题。5.3 常见错误代码与现象对照现象可能原因排查方法数据恒为0xFFFFFFDOUT上拉缺失或SCLK无输出检查上拉电阻和SCLK波形数据恒为0x800000输入端短路或增益配置错误检查输入电压和配置寄存器数据成倍偏移时钟数量错误或位序颠倒示波器数时钟个数检查循环次数数据符号错误未做符号扩展检查24位拼接和符号位判断数据周期性跳动电源纹波或数字信号串扰示波器看电源纹波检查PCB布局第二次读取错乱SCLK空闲电平错误确保读取后SCLK拉高这张表是我在实际调试中总结出来的大部分问题都能对应上。如果你遇到的现象不在表里可以先把SCLK和DOUT的波形抓下来对照数据手册的时序图逐项检查。6. 让数据稳下来的软件滤波与校准方法6.1 滑动平均滤波的窗口选择即使硬件做得再好CS1237的原始数据还是会有几个LSB的跳动。这是ADC的固有噪声24位ADC的有效位数通常只有20位左右低几位本来就是噪声。对于称重这类应用你需要的是稳定到0.1克或者1克而不是追求24位全部稳定。滑动平均滤波是最简单有效的方法。窗口大小选择要看你的输出速率和响应要求。如果输出速率是10Hz窗口取10那响应时间就是1秒对于静态称重完全够用。如果输出速率是1.28kHz窗口取128响应时间还是0.1秒但平滑效果更好。我的经验是窗口大小不要超过输出速率除以你需要的响应频率。比如你需要每秒更新5次数据输出速率10Hz那窗口最大取2再大就响应不过来了。这时候应该提高输出速率比如用40Hz窗口取8既平滑又跟得上。6.2 中值滤波去脉冲干扰滑动平均对高斯噪声效果好但如果偶尔出现一个特别大的跳变比如附近有电机启动平均值会被拉偏。这时候可以先用中值滤波去掉脉冲再做滑动平均。中值滤波的实现很简单取最近N个数据排序后取中间值。N一般取3或5。对于CS1237这种低速ADC取3就够了。代码上可以用一个环形缓冲区每次新数据进来后复制一份排序取中间值输出。#define MEDIAN_N 3 int32_t median_filter(int32_t new_val) { static int32_t buf[MEDIAN_N] {0}; static uint8_t idx 0; buf[idx] new_val; idx (idx 1) % MEDIAN_N; int32_t tmp[MEDIAN_N]; memcpy(tmp, buf, sizeof(tmp)); // 简单冒泡排序 for (int i 0; i MEDIAN_N - 1; i) { for (int j 0; j MEDIAN_N - 1 - i; j) { if (tmp[j] tmp[j1]) { int32_t t tmp[j]; tmp[j] tmp[j1]; tmp[j1] t; } } } return tmp[MEDIAN_N / 2]; }这段代码在STM32F103上跑一次大概几十微秒对于10Hz的速率来说完全可以忽略。6.3 两点校准与零点跟踪CS1237的零点会随温度漂移尤其是增益设得比较高的时候。如果你的应用需要长时间稳定建议做零点跟踪在确认没有负载的时候定期把当前读数作为新的零点。两点校准则是用两个已知重量比如空载和满量程砝码来确定斜率和截距。具体做法是记录空载时的原始值raw0和对应重量w0记录加载砝码后的原始值raw1和对应重量w1然后计算重量 w0 (raw - raw0) * (w1 - w0) / (raw1 - raw0)这个公式假设CS1237的线性度足够好。实际上24位ADC的线性度通常在0.001%以内对于大多数应用完全够用。如果你发现线性度不够可能需要分段校准但那是另一个话题了。零点跟踪的实现要注意只有在读数稳定且接近零点时才更新零点否则会把真实负载当成零点漂移。我的做法是设置一个阈值比如读数在零点附近±100个LSB以内且连续10次读数变化不超过10个LSB才更新零点。7. 一些让我少走弯路的实操心得CS1237这颗芯片说难不难说简单也不简单。它的坑主要集中在它不是标准SPI这一点上。只要你接受了这个设定用GPIO模拟时序把SCLK的空闲电平、时钟数量、数据位序这几个点卡死通信本身是很稳定的。我在实际项目里最终用的配置是STM32F103C8T6SCLK用PA5DOUT用PA6DIN用PA7全部GPIO模拟。SCLK空闲高电平每个时钟沿延时2微秒读取24位后做符号扩展。输出速率配置为40Hz滑动平均窗口8中值滤波窗口3。电源用独立LDO输入端加RC滤波。这套配置在电子秤项目里连续跑了三个月零点漂移不超过5个LSB加载重复性在0.02%以内。还有一个细节如果你用STM32的HAL库GPIO翻转用HAL_GPIO_WritePin会有一定的函数调用开销大概几百纳秒。对于CS1237的速率来说完全够用但如果你追求更快的读取可以直接操作BSRR寄存器。不过说实话CS1237最快也就1.28kHzHAL库的开销完全可以接受没必要为了这点速度去牺牲代码可读性。最后说一个我踩过的坑有一次数据一直不对查了半天发现是杜邦线接触不良。DOUT的线松了有时候能读到数据有时候读不到表现就是数据随机跳动。所以如果你用的是杜邦线连接先检查线有没有插紧再查代码。这个教训让我后来所有调试都用焊接的排针再也不信杜邦线了。
返回列表