STM32驱动CS1237高精度ADC:非标准SPI时序调试与数据稳定方案
1. 项目缘起一个被SPI外衣包裹的国产ADCCS1237这颗芯片第一次拿到手的时候我下意识把它当成了一颗标准SPI接口的ADC。毕竟引脚上明明白白写着SCLK、DRDY/DOUT看起来就是时钟加数据线的经典组合。结果上电一跑数据要么纹丝不动要么疯狂乱跳完全不是预期中的稳定读数。后来把时序图翻出来逐帧对照才发现这颗芯片的通信协议跟标准SPI压根不是一回事——它更像是一种类SPI的定制串行协议时钟极性、数据建立沿、通道切换时序都有自己的一套规矩。这个项目要解决的问题很具体用STM32F103去驱动CS1237这颗24位高精度ADC把称重传感器或者压力桥路的微弱差分信号稳定地读出来。听起来简单但实际调试过程中踩的坑一个接一个——从SPI模式配置错误导致数据全错到DRDY信号处理不当造成数据错位再到电源噪声和参考电压不稳引起的数据跳动每一个环节都能让你怀疑人生。这篇文章适合两类人看一类是正在用STM32做毕业设计或者小项目需要接高精度ADC但被时序搞得头大的朋友另一类是有一定嵌入式基础但对非标准串行协议调试经验不多的工程师。我会把整个调试过程拆开揉碎从协议理解、硬件连接、软件时序、数据滤波到问题排查一步步讲清楚。你看完之后应该能直接把这套方案复现到自己的板子上少走我走过的弯路。2. 先搞清楚CS1237到底是不是SPI2.1 标准SPI和CS1237的本质区别很多人看到CS1237的引脚定义就默认它是SPI设备这个误解是后面所有问题的根源。标准SPI是四线制SCLK、MOSI、MISO、CS主机控制时钟数据在时钟边沿采样全双工通信。而CS1237只有两根线跟MCU交互SCLK和DRDY/DOUT双向复用加上一根CS或者直接接地。关键差异在于CS1237的DRDY/DOUT引脚是双向的。在空闲状态它输出高电平表示数据未准备好当转换完成它拉低表示数据就绪这时候MCU需要主动发时钟把数据读出来。读完之后它又回到高电平。这个数据就绪指示和数据输出复用同一根线的设计跟标准SPI的MISO是纯输出完全不同。再一个CS1237的时钟极性和采样沿是固定的。数据在SCLK的下降沿输出MCU应该在上升沿采样。如果你用STM32的硬件SPI去配CPOL和CPHA的组合必须精确匹配否则读出来的就是移位错位的数据。我一开始用CubeMX默认的SPI模式0CPOL0CPHA0读出来的24位数据高8位永远是0xFF或者0x00低位全是噪声就是这个原因。2.2 为什么硬件SPI反而容易出问题STM32F103的硬件SPI外设是为标准SPI协议设计的它的NSS片选管理、时钟相位控制、数据帧格式都是围绕四线制来的。当你用它去驱动CS1237这种半双工、单线双向、非标准时序的设备时硬件SPI的很多特性反而成了累赘。比如NSS引脚硬件SPI在NSS有效时会自动控制片选但CS1237根本不需要片选信号它靠DRDY来指示数据就绪。如果你把NSS接上去反而可能引入额外的时序约束。再比如硬件SPI的时钟频率STM32F103的SPI1挂在APB2上72MHz主频下分频系数最小是2也就是36MHz这个速度对CS1237来说太快了。CS1237的SCLK最高频率在数据手册里通常标的是1MHz左右具体看供电电压你拿36MHz去怼它它根本反应不过来。所以我的建议是用软件模拟SPI时序。GPIO翻转虽然看起来低级但对于CS1237这种时序要求明确、速率不高的设备软件模拟反而最可控。你可以精确控制每个时钟的高低电平持续时间可以在DRDY拉低之后延迟一段时间再发时钟可以在读完之后手动把时钟拉回空闲状态。这些灵活性是硬件SPI给不了的。2.3 软件模拟SPI的GPIO配置要点用STM32的GPIO模拟SPI配置上有几个细节要注意。SCLK引脚配置为推挽输出初始状态根据CS1237的要求设为低电平或者高电平看数据手册的空闲电平定义。DRDY/DOUT引脚在MCU侧要配置为输入模式但这里有个坑当MCU发时钟读数据的时候这个引脚是CS1237在驱动当空闲的时候它也是CS1237在驱动输出高电平。所以MCU侧应该配置为浮空输入或者上拉输入绝对不能配成推挽输出否则会跟CS1237的输出打架严重的话可能烧芯片。我一般用上拉输入因为CS1237的输出高电平驱动能力有限加个内部上拉能保证空闲时读到稳定的高电平。速度配置选50MHz或者2MHz都行反正软件模拟的时钟频率也就几百KHzGPIO翻转速度绰绰有余。注意如果你用的是STM32F103C8T6这种最小系统板PA5和PA6默认是SPI1的引脚但你可以把它们当普通GPIO用。CubeMX里不要开SPI外设直接配GPIO就行。3. 硬件连接与电源处理数据稳定的地基3.1 最小系统连接方案CS1237的典型应用电路不复杂但每个元件都有讲究。先列一下我实际用的连接方式CS1237的VDD接3.3VVSS接地。电源引脚旁边必须放一个0.1uF的陶瓷电容做高频去耦再并一个10uF的钽电容做低频储能。这两个电容要尽量靠近芯片引脚走线越短越好。REF引脚是参考电压输入这是决定ADC精度的关键。我一开始图省事直接接VDD结果数据跳动很大。后来换成一颗独立的2.5V基准源比如REF3025或者LM4040数据稳定性立刻上了一个台阶。参考电压的噪声直接会调制到ADC输出上所以基准源的选型和去耦不能马虎。模拟输入AIN和AIN-接传感器桥路的差分输出。如果是称重传感器通常输出差分电压在0到20mV之间CS1237的PGA增益可以设到128倍这样满量程对应大概±20mV分辨率能到24位。SCLK和DRDY/DOUT接MCU的GPIO中间可以串22欧姆到100欧姆的电阻做阻抗匹配和限流保护尤其是DRDY/DOUT这根双向线串个电阻能防止两边同时驱动时电流过大。3.2 电源噪声对数据跳动的实际影响我做过一个对比实验同一块板子同一颗CS1237同样的软件配置只改变电源方案看ADC读数的峰峰值噪声。电源方案参考电压读数峰峰值噪声24位原始码折合输入噪声USB直接供电VDD做REF3.3V约8000码约15uVLDO稳压VDD做REF3.3V约3000码约5.6uVLDO稳压独立2.5V基准2.5V约500码约0.9uV电池供电独立2.5V基准2.5V约200码约0.37uV这个表很能说明问题。USB供电的时候电脑那边的开关电源噪声会通过USB线传导过来ADC读数跳得厉害。加一级LDO之后改善明显但还不够。真正质变的是换独立基准源因为参考电压的噪声被直接抑制了。最后电池供电是终极方案但实际项目里不一定方便所以至少要做到LDO加独立基准。3.3 模拟地和数字地的处理CS1237有独立的模拟地和数字地引脚吗看具体封装有些型号是共地的。如果是共地PCB布局上要注意模拟部分的地线和数字部分的地线在芯片下方分开走最后在电源入口处单点汇合。不要让数字电流流过模拟地区域否则开关噪声会串进去。我实际画板的时候CS1237下面的地平面是完整的但MCU和数字器件的地线从另一侧走两者在LDO输出电容的接地端汇合。这个细节看起来小但对高精度ADC来说地线处理不好前面电源做得再干净也白搭。4. 软件时序把数据从DRDY线上抠出来4.1 完整的数据读取流程CS1237的数据读取流程可以拆成几个明确的阶段我用状态机的思路来组织代码逻辑最清晰。第一阶段等待DRDY拉低。MCU循环检测DRDY/DOUT引脚的电平直到读到低电平。这个等待时间取决于CS1237的转换速率通常配置在10Hz到1.28kHz之间。如果你设的是10Hz那每100ms才有一次数据就绪MCU在这期间可以干别的事不用死等。第二阶段发时钟读数据。DRDY拉低之后MCU开始发SCLK脉冲。CS1237会在每个SCLK的下降沿把数据位输出到DRDY/DOUT线上MCU在上升沿采样。24位数据需要24个时钟脉冲。注意第一个时钟脉冲到来之前DRDY/DOUT线上已经是最高位MSB的数据了所以第一个上升沿采到的就是bit23。第三阶段处理符号位和通道。24位数据读完之后如果CS1237配置为差分输入第24位是符号位。有些模式下还会有额外的通道选择位或者状态位具体看寄存器配置。我一般读25个时钟第25个时钟用来把DRDY/DOUT线恢复到空闲高电平状态。第四阶段数据拼接和转换。把24个bit拼成一个32位整数然后做符号扩展。如果最高位是1表示负数需要把高8位补1。最后根据参考电压和PGA增益换算成实际电压值。4.2 关键时序参数的实测与调整数据手册上给的时序参数是理论值实际调试的时候要用示波器或者逻辑分析仪抓一下波形确认MCU发出的时钟和CS1237的响应是否匹配。我实测下来STM32F103在72MHz主频下用GPIO翻转模拟SCLK一个完整的时钟周期高电平加低电平大概在1到2微秒左右对应500kHz到1MHz的时钟频率。这个速度CS1237能跟上但如果你把延时函数写得太激进比如用__NOP()堆几个空指令就想产生时钟那频率可能跑到几MHzCS1237就来不及响应了。我的做法是在SCLK的高电平和低电平之间各插入一个短延时用for循环空转或者__NOP()都可以具体循环次数用示波器测一下调到时钟频率在200kHz到500kHz之间。这个范围比较稳妥既不会太慢影响采样率也不会太快导致时序违规。还有一个细节DRDY拉低之后不要立刻发时钟。CS1237需要一个短暂的建立时间数据手册上一般标的是几十纳秒到几百纳秒。我习惯在检测到DRDY低电平之后再延时1微秒左右再发第一个时钟这样最保险。4.3 代码实现软件SPI读CS1237的完整函数下面是我实际用的读取函数基于STM32 HAL库的GPIO操作你可以直接移植到自己的工程里。#define CS1237_SCLK_PIN GPIO_PIN_5 #define CS1237_SCLK_PORT GPIOA #define CS1237_DOUT_PIN GPIO_PIN_6 #define CS1237_DOUT_PORT GPIOA // 短延时具体循环次数根据主频调整 static void cs1237_delay(void) { for(volatile int i 0; i 10; i); } // 读取24位ADC数据返回带符号的32位整数 int32_t cs1237_read(void) { uint32_t raw 0; int32_t result; // 等待DRDY拉低 while(HAL_GPIO_ReadPin(CS1237_DOUT_PORT, CS1237_DOUT_PIN) GPIO_PIN_SET); // 建立时间延时 cs1237_delay(); // 读24位数据 for(int i 0; i 24; i) { // SCLK拉高 HAL_GPIO_WritePin(CS1237_SCLK_PORT, CS1237_SCLK_PIN, GPIO_PIN_SET); cs1237_delay(); // 在上升沿采样数据 raw 1; if(HAL_GPIO_ReadPin(CS1237_DOUT_PORT, CS1237_DOUT_PIN) GPIO_PIN_SET) { raw | 1; } // SCLK拉低 HAL_GPIO_WritePin(CS1237_SCLK_PORT, CS1237_SCLK_PIN, GPIO_PIN_RESET); cs1237_delay(); } // 第25个时钟恢复DRDY/DOUT到空闲状态 HAL_GPIO_WritePin(CS1237_SCLK_PORT, CS1237_SCLK_PIN, GPIO_PIN_SET); cs1237_delay(); HAL_GPIO_WritePin(CS1237_SCLK_PORT, CS1237_SCLK_PIN, GPIO_PIN_RESET); cs1237_delay(); // 符号扩展24位有符号数转32位 if(raw 0x800000) { result (int32_t)(raw | 0xFF000000); } else { result (int32_t)raw; } return result; }这段代码里cs1237_delay()的循环次数需要根据你的STM32主频来调。72MHz下10次空循环大概对应几百纳秒配合GPIO翻转本身的时间SCLK频率大概在300kHz左右。你可以用逻辑分析仪抓一下SCLK波形确认频率在合适范围内。注意while等待DRDY拉低的那行代码如果CS1237没有正常输出数据会死循环在这里。实际项目里建议加一个超时计数器超过一定时间就返回错误码避免程序卡死。5. 数据乱跳的根源排查与滤波处理5.1 数据跳动的常见原因分类数据乱跳不一定是软件问题也不一定是硬件问题往往是两者叠加。我按照排查顺序把可能的原因列成一个表你可以对照着逐项检查。现象可能原因排查方法解决措施数据全为0或全为0xFFFFFFSCLK或DOUT线接反/虚焊万用表测通断示波器看波形重新焊接确认引脚定义数据高8位固定低16位变化SPI模式错误采样沿不对逻辑分析仪抓时序调整采样沿改用软件模拟数据偶尔跳变幅度很大电源噪声或参考电压不稳示波器看VDD和REF纹波加去耦电容换独立基准数据缓慢漂移温度变化或传感器蠕变恒温环境测试软件做温度补偿数据周期性跳动工频干扰或数字电路串扰看跳动频率是否50Hz/100Hz加屏蔽远离数字走线数据随机跳动无规律地线处理不当或GPIO配置错误检查地平面和GPIO模式单点接地改浮空输入5.2 用示波器抓时序一次真实的排查记录有一次我遇到数据每隔几秒就跳一次大数跳完之后又恢复正常。用示波器抓DRDY和SCLK的波形发现跳变的时候SCLK上有一个额外的窄脉冲宽度大概几十纳秒。这个脉冲不是我的代码产生的后来查出来是MCU的某个中断服务程序里操作了同一个GPIO端口导致位操作的时候影响了SCLK引脚。STM32的GPIO是16位一组如果你用BSRR或者ODR寄存器直接操作写的是整个端口。比如GPIOA-ODR | GPIO_PIN_5这种写法读-改-写的过程可能被中断打断导致其他引脚的状态被意外改变。正确的做法是用BSRR寄存器它是原子操作或者用HAL_GPIO_WritePin它内部也是用BSRR实现的。这个坑很隐蔽因为大部分时候中断不会恰好发生在GPIO操作的瞬间但一旦发生就会产生一个毛刺。对于CS1237这种对时钟敏感的设备一个毛刺就可能导致数据错位。所以我的建议是操作CS1237的GPIO时关掉全局中断或者确保没有其他中断会操作同一个GPIO端口。5.3 软件滤波从原始数据到稳定读数即使硬件做得再好ADC的原始数据也会有少量噪声。CS1237是24位ADC有效位数通常在20到21位左右低位几个bit本来就是噪声。你需要根据实际应用场景做滤波。最简单的滤波是滑动平均。维护一个长度为N的数组每次新数据进来替换最老的数据然后求平均。N越大数据越稳但响应越慢。对于称重这种慢速应用N取16或者32都行。对于需要快速响应的场景N取4或者8。稍微好一点的是中值滤波加滑动平均。先取最近N个数据去掉最大值和最小值剩下的求平均。这样能有效抑制偶发的脉冲干扰。我一般用N8去掉最大最小后剩6个求平均效果比纯滑动平均好。如果你对数据的实时性要求不高可以用一阶低通滤波。公式是filtered alpha * new_value (1 - alpha) * filtered。alpha越小滤波效果越强但相位滞后越大。alpha取0.1到0.3之间比较合适。下面是我常用的一个组合滤波函数#define FILTER_N 8 int32_t filter_buf[FILTER_N]; int filter_index 0; int32_t cs1237_filter(int32_t new_data) { int32_t temp[FILTER_N]; int32_t sum 0; int32_t max, min; // 存入新数据 filter_buf[filter_index] new_data; filter_index (filter_index 1) % FILTER_N; // 复制到临时数组 for(int i 0; i FILTER_N; i) { temp[i] filter_buf[i]; } // 冒泡排序找最大最小 max temp[0]; min temp[0]; for(int i 1; i FILTER_N; i) { if(temp[i] max) max temp[i]; if(temp[i] min) min temp[i]; } // 去掉最大最小后求和 int count 0; for(int i 0; i FILTER_N; i) { if(temp[i] ! max temp[i] ! min) { sum temp[i]; count; } } return sum / count; }这个函数每次调用返回一个滤波后的值。注意如果最大值或最小值有多个相同值这个逻辑会全部去掉导致count偏小。更严谨的做法是排序后去掉首尾各一个但那样代码复杂一些。对于一般应用上面这个版本够用了。5.4 校准把原始码变成有意义的物理量CS1237输出的是24位原始码你需要通过校准把它转换成电压或者重量。校准分两步零点校准和满量程校准。零点校准在传感器空载的时候读一组数据取平均记为offset。满量程校准在传感器加上已知重量比如1kg砝码的时候读一组数据取平均记为full。那么实际重量 (raw - offset) * known_weight / (full - offset)。这个过程看起来简单但有几个细节要注意。零点校准和满量程校准的时候要等数据稳定了再读通常上电后等几分钟让传感器和电路热稳定。校准用的砝码要准确至少比你要达到的精度高一个等级。如果传感器有非线性可以在多个点校准然后用分段线性或者多项式拟合。我一般会在代码里存一组校准参数上电的时候从Flash读出来。校准的时候通过串口发命令触发把新的参数写回Flash。这样不用每次上电都重新校准。6. 常见问题速查与避坑经验6.1 调试过程中遇到的典型问题问题一读出来的数据一直是0xFFFFFF或者0x000000。这个最常见的原因是DRDY/DOUT线没有正确连接或者MCU的GPIO配置成了输出模式。检查方法用万用表测DRDY/DOUT引脚在空闲时的电压正常应该是高电平接近VDD。如果一直是低电平可能是CS1237没有正常工作检查电源和晶振如果有。问题二数据能读出来但数值明显不对比如一直是满量程或者负满量程。检查参考电压和PGA增益配置。如果PGA设得太大输入信号被放大后超出了量程就会饱和。如果参考电压接错比如应该接2.5V结果接了3.3V换算出来的电压就会偏大。问题三数据偶尔跳变跳变的幅度不大但很频繁。这种通常是电源噪声或者地线干扰。先检查去耦电容有没有漏焊再检查模拟地和数字地有没有分开走线。如果条件允许用电池给CS1237供电看跳动是否消失。如果消失了就是电源问题。问题四SCLK发出去之后DRDY/DOUT没有反应。检查SCLK的频率是否太快CS1237来不及响应。降低SCLK频率试试。另外检查SCLK的空闲电平有些CS1237型号要求SCLK空闲时为低电平如果你配成了高电平第一个时钟沿可能被忽略。问题五读出来的数据跟实际输入对不上比如输入正电压读出来是负数。检查符号位处理。24位数据的最高位是符号位如果你没有做符号扩展正数会被当成负数。另外检查差分输入的极性AIN和AIN-接反了读数符号也会反。6.2 几个容易忽略的细节第一个细节CS1237的转换速率配置。这颗芯片通常有几种转换速率可选通过寄存器或者引脚配置。速率越高噪声越大。如果你对速度要求不高尽量选低速档数据会稳很多。我一般用10Hz或者40Hz对于称重应用足够了。第二个细节上电顺序。CS1237的VDD和REF的上电顺序有要求吗大部分情况下没有严格顺序但如果REF先上电而VDD还没上可能会有漏电流。稳妥的做法是VDD和REF同时上电或者在VDD之后再加REF。实际调试的时候如果发现上电后数据异常重新上电又正常可能就是上电顺序的问题。第三个细节GPIO的初始状态。在MCU初始化的时候SCLK和DRDY/DOUT引脚的初始状态要设对。SCLK初始为低电平或者根据CS1237要求设DRDY/DOUT初始为浮空输入。如果初始化的时候SCLK输出高电平而CS1237正好在等待时钟可能会误触发一次读取。第四个细节中断优先级。如果你用中断来触发ADC读取注意中断优先级不要跟其他高频中断冲突。比如定时器中断和串口中断如果优先级太高可能会打断ADC读取的时序导致数据错位。我一般把ADC读取放在主循环里用状态机轮询不用中断。6.3 一个实用的调试技巧用LED指示数据状态调试的时候串口打印数据有时候不方便尤其是板子已经装到设备里的时候。我的做法是加一个LED用PWM或者闪烁频率来指示数据的状态。比如数据稳定的时候LED常亮数据跳动的时候LED闪烁数据超量程的时候LED快速闪。这样不用接串口就能大致判断ADC的工作状态。具体实现每次读取数据后判断数据的峰峰值或者标准差根据大小设置不同的LED闪烁模式。这个技巧在現場调试的时候特别有用能快速定位问题。7. 从能读到读得准进阶优化方向7.1 提高有效分辨率的几个手段CS1237标称24位但实际有效位数受噪声限制。如果你想要更高的有效分辨率可以从几个方面入手。降低转换速率。转换速率越低内部积分时间越长噪声越小。10Hz下的有效位数通常比1.28kHz下高2到3位。增加硬件滤波。在AIN和AIN-之间加一个RC低通滤波截止频率根据信号带宽来定。比如称重信号带宽通常小于10HzRC截止频率设100Hz就够了。电容用C0G或者薄膜电容不要用X7RX7R有压电效应会产生额外噪声。多次采样求平均。如果转换速率是10Hz你每秒能读10次把这10次求平均噪声能降低根号10倍大约3倍。有效位数能提高1.5位左右。斩波稳零。有些高精度ADC支持斩波模式能消除1/f噪声和失调漂移。CS1237如果支持这个模式打开它低频噪声会明显改善。7.2 温度漂移的补偿思路高精度ADC的读数会随温度变化主要来源是参考电压的温漂和传感器的温漂。如果你的应用环境温度变化大需要做温度补偿。最简单的补偿方法是查表法。在几个温度点比如0度、25度、50度分别测出ADC读数和实际值的偏差做成表格。运行时根据温度传感器读到的温度查表插值出补偿量。稍微复杂一点的是多项式拟合。把温度偏差拟合成一个二次或者三次多项式运行时直接计算补偿量。这种方法适合温度范围宽、精度要求高的场景。如果传感器本身有温度输出比如一些称重传感器内置温度电阻可以直接用那个信号做补偿比外接温度传感器更准。7.3 长期稳定性定期校准和自检产品做出来之后长期稳定性是个大问题。传感器会蠕变参考电压会老化PCB会受潮。我的做法是加一个自检电路用一个模拟开关切换一个已知的参考信号到ADC输入端定期测量这个参考信号如果读数偏离预期就触发校准或者报警。这个自检电路的成本不高一个模拟开关加两个电阻就行。但能提前发现很多问题避免设备在现场出故障。另外建议在产品里存一份出厂校准参数用户使用过程中如果发现偏差可以通过按键或者命令触发重新校准。校准的时候用标准砝码或者标准电压源按照前面说的零点加满量程两步法操作。8. 写在最后一些个人体会调试CS1237和STM32的通信最大的感受就是不要假设。不要假设它跟标准SPI兼容不要假设数据手册上的典型电路就够用不要假设读出来的数据就是对的。每一步都要验证每一个异常都要追根究底。我踩过的坑里最耗时间的是SPI模式错误和电源噪声这两个。SPI模式错误导致数据完全不可用但至少你能知道有问题电源噪声导致数据偶尔跳变这种间歇性问题最难查因为大部分时候数据是好的你很容易忽略它。后来我养成了一个习惯每次调试新的ADC电路先用示波器看电源纹波再看参考电压纹波最后才看时序。电源干净了后面的事情就顺了。还有一个体会是软件模拟SPI虽然看起来笨但真的省心。硬件SPI的配置项太多CPOL、CPHA、NSS、波特率预分频、数据帧格式任何一个配错都可能导致通信失败。而软件模拟SPI你完全掌控每一个时钟沿想怎么调就怎么调。对于CS1237这种非标准协议软件模拟是更稳妥的选择。最后分享一个小技巧如果你手头没有逻辑分析仪可以用STM32的另一个定时器来测量SCLK的频率和占空比或者用DMA把GPIO的状态搬到内存里事后分析。虽然不如逻辑分析仪直观但至少能定量地看到时序参数。调试这种事情有数据总比瞎猜强。