
做单片机采集最怕什么不是GPIO不够用而是信号太弱、噪声太大。STM32F103内置的12位ADC测个电池电压、电位器分压没毛病可一旦换成称重传感器、应变片、热电偶这类mV级微弱信号12位分辨率加内部参考电压凑合出来就是一个字飘。这时候就得外挂高精度ADC芯片了。CS1237就是一颗很典型的24位Sigma-Delta ADC价格便宜、两线接口STM32F103驱动它轻轻松松。这篇文章就按我实际做项目的顺序把硬件连接、通信时序、驱动代码和调试踩坑完整过一遍。看完你也能在两小时内把CS1237点亮。1. 为什么是CS1237这颗24位ADC到底比内置ADC强在哪1.1 STM32F103内置ADC的短板很多人一开始都会想STM32F103内部不是已经有ADC了吗PA0到PA7那么多通道直接用不就行了确实内置ADC用起来很省事CubeMX里点几下就能采集但实际测过微弱信号的人都知道它有多尴尬。STM32F103内置ADC是12位分辨率参考电压一般是3.3V理想情况下LSB是3.3V / 4096 ≈ 0.8mV。注意这是理想值实际电路里还要算上ADC的DNL、INL以及电源噪声12位有效位能做到10位就不错了。也就是说你分辨一个0.8mV的变化都很勉强更别说去测称重传感器这种满量程输出才10mV左右的信号了。有人会说“那我在前面加一级仪表放大器不就行了”可以但仪表放大器本身有失调电压、温漂和噪声成本也上去了。更麻烦的是后级ADC的噪声会直接叠加到放大后的信号上你放大倍数越高噪声也越吓人。内置ADC还有一个问题它的输入阻抗不高对高阻抗信号源不友好信号源一拉电流读数就往下掉。所以真正要做高精度采集的时候我基本上不会用内置ADC而是直接上外挂高分辨率ADC芯片。1.2 CS1237的核心特性CS1237是国产高精度ADC里比较有代表性的一颗。它的核心架构是Sigma-Delta也就是过采样加噪声整形。简单说它用很高的采样率对输入信号反复采样然后通过数字滤波器把带外噪声压掉把低速但高精度的结果吐出来。这种架构天生适合低频、高精度的测量场景比如称重、压力、温度、应变测量。这颗芯片的几个关键参数值得说一说第一分辨率24位输出格式是二进制补码。别看24位很吓人实际有效分辨率取决于输出速率。10SPS下能做到接近20位无噪声位数240SPS以上有效位数就掉到16位左右了。所以用CS1237有个基本原则不追求速度追求精度的时候用低速档追求速度就牺牲分辨率。第二内置PGA可编程增益放大器增益1到128倍可选。这个太关键了等于把外部仪表放大器省掉了。差分输入模式下信号的负端也有参考点CMRR比单端接法好很多。第三数字接口只有两根线SCK和DOUT。DOUT这个引脚很特别它既能输出数据也能接收命令所以24位ADC的通信被压缩到两线上。引脚占用少特别适合STM32F103这种“外设多但引脚紧张”的场景。CS1237的兄弟芯片CS1238、ADS1232、HX711也都有人用但CS1237胜在价格低、文档清晰、接口简单很多淘宝模块直接做成称重模块拿回来焊上传感器就能跑。如果你的项目需要采集多路微弱信号CS1237还可以多片级联使用每片占两个IO资源消耗很低。2. 硬件连接全解引脚定义、电源与参考电压的3个关键点2.1 最小系统接线方式CS1237常见的封装是SOP-16或者模块化小板。裸片引脚比较多但真正要接的没几个。核心引脚如下引脚功能说明VDD电源正2.7V到5.5V实测3.3V最稳VREF参考电压接外部基准或按模块设计接法处理AINP/AINN差分输入正/负接传感器信号输出SCK时钟输入由MCU控制DOUT数据输出/命令输入双向引脚需要MCU方向切换GND电源地与MCU共地如果买的是现成模块一般只引出VCC、SCK、DOUT、GND四个引脚再多两个就是传感器接口。不管哪种形式SCK和DOUT接STM32F103任意两个GPIO就行。建议在DOUT引脚对地接一个10kΩ下拉电阻或者对上拉电阻具体方向要看模块原理图。我习惯给DOUT接一个10kΩ上拉电阻到VDD这样空闲状态电平更稳定也方便写命令时总线处于确定状态。2.2 电源和参考电压的坑给CS1237供电我强烈建议用3.3V而不是5V。原因有二第一如果VDD接5VDOUT输出高电平接近5VSTM32F103不是所有引脚都能容忍5V。PA、PB大部分口可以但PC13到PC15是标准CMOS输入不能接5V接上去有损坏风险。第二CS1237内部基准的温漂和电源电压有一定关系3.3V下工作更稳定。VREF引脚的处理是硬件设计里最容易出错的地方。CS1237内置了带隙基准很多模块直接把VREF悬空或接到VDD用内部基准干活。自己画板子的话建议给VREF接一颗高精度基准芯片比如REF3125或者TL431加跟随器能显著改善温漂。我用过模块默认的内部基准做称重实验常温下漂移不明显但放到温箱里曲线就有点难看了。所以对温度稳定性有要求的项目外部基准是必须的。电源去耦也不能省。VDD引脚旁边放一颗0.1μF陶瓷电容再加一颗10μF电解电容或者钽电容。ADC对电源纹波非常敏感Sigma-Delta内部虽然有电源抑制能力但高频噪声还是会通过电源引脚耦合进模拟前端。另外CS1237的地千万不要和电机、继电器、PWM驱动之类的大电流地共用一根长走线这是很多数据跳变问题的根源。2.3 和称重传感器的连接要点CS1237最常见的使用场景是配合惠斯通电桥传感器。以称重传感器为例它一般引出四根线E激励正、E-激励负、S信号正、S-信号负。E和E-接激励电源S和S-分别接CS1237的AINP和AINN。这里有个很容易搞反的点差分输入的正负接反了读出来的数据就是负值也就是0x800000附近的码值而不是正常的零位附近。如果你接上传感器后发现数据是负满量程或者跳变极大第一步不是查代码而是检查S和S-是不是接反了。传感器和ADC之间连线建议用屏蔽双绞线屏蔽层单端接地。称重现场往往有电机、变频器这类强干扰源布局上让信号线远离功率线。如果项目用的是电池供电那干扰问题会好很多如果是开关电源供电我建议先给传感器和ADC统一用线性稳压供电再用RC滤波把激励电源的纹波压一压。这块做得越细后面数据滤波的压力就越小。3. 通信协议实战拆解读懂26个SCK脉冲背后的逻辑3.1 DOUT的双重身份数据输出和命令输入CS1237的DOUT引脚是整个通信协议的难点因为它身兼两职。上电后CS1237自动开始转换转换完成后DOUT从高电平跳变到低电平告诉MCU“数据准备好了”。这个行为和HX711很像。但是写完命令的时候DOUT又变成了输入引脚MCU要主动驱动它的电平与SCK配合把8位命令字写进芯片。也就是说MCU端的GPIO方向必须来回切换。这正是CS1237不方便直接用硬件SPI的原因。硬件SPI本身做读操作不错但DOUT既要读又要写还要在写完后恢复成输入模式外设控制起来非常别扭。实测下来GPIO模拟时序加上几十微秒的延时稳定性和灵活性都最好。3.2 读取数据的26个SCK脉冲先讲读数据。MCU检测到DOUT为低后开始发送SCK脉冲。完整读一次需要26个脉冲很多新手在这里卡住为什么不是24个前24个SCK上升沿DOUT上的数据依次移出高位在前得到24位转换结果。第25个SCK上升沿DOUT输出的是命令字状态位这一位能告诉你上一次写命令是否成功为1表示成功为0表示失败。第26个SCK下降沿整个读取过程结束DOUT恢复为高电平等待下一次转换或命令写入。我画过很多次这个时序的简化版本还是用文字描述最直接第一步等待DOUT由高变低。第二步循环24次发送SCK高脉冲每次读取DOUT的电平移位拼出24位原始数据。第三步发送第25个SCK脉冲读取状态位。第四步发送第26个SCK脉冲注意是等到下降沿结束不要急着立刻去读数据。第26个脉冲结束后DOUT会变为高电平这时候芯片就准备好接收命令了。所以实际驱动里读数据的最后一个动作必须是把SCK拉低、结束第26个脉冲然后才能决定是写命令还是等下一次转换。读出来的24位数据是无符号整数形式需要自己扩展成有符号数。判断最高位第23位是否为1如果是把高8位置0xFF否则置0x00这就得到了32位有符号数。具体会在代码部分展示。3.3 命令字的构成与常用配置CS1237的命令字是8位各位含义如下位功能bit7通道选择0为通道11为通道2bit6-bit4输出速率配置bit3-bit0PGA增益配置速率配置值如下值速率00010 SPS00130 SPS01060 SPS011240 SPS1001.2 kSPS1012.4 kSPS1104.8 kSPS1117.2 kSPS增益配置值如下值增益000010001200104001180100160101320110640111128我常用的配置是通道110SPSPGA128。这个配置算出来命令字是0x07。计算过程很简单bit7是0bit6到bit4是000bit3到bit0是0111拼接起来就是0000 0111也就是0x07。写命令的时序也要注意必须在DOUT为高电平时写入。正常读完第26个脉冲后DOUT已经变高所以标准的流程就是“读数据 - 读完后直接写命令 - 等待下次转换”。如果你在错误的时间点写命令芯片不会响应状态位会在下一次读取时告诉你有没有成功。4. STM32F103驱动代码实现GPIO模拟时序完整示例4.1 引脚初始化与方向切换封装驱动CS1237不需要开SPI外设也不需要DMA纯粹用GPIO模拟就够了。我用STM32F103标准库来写HAL库的思路完全一样就是把GPIO读写函数换成HAL_GPIO_ReadPin和HAL_GPIO_WritePin。引脚宏定义如下#define CS1237_SCK_PORT GPIOA #define CS1237_SCK_PIN GPIO_Pin_5 #define CS1237_DOUT_PORT GPIOA #define CS1237_DOUT_PIN GPIO_Pin_6 #define CS1237_SCK_H() GPIO_SetBits(CS1237_SCK_PORT, CS1237_SCK_PIN) #define CS1237_SCK_L() GPIO_ResetBits(CS1237_SCK_PORT, CS1237_SCK_PIN) #define CS1237_DOUT_READ() GPIO_ReadInputDataBit(CS1237_DOUT_PORT, CS1237_DOUT_PIN)DOUT方向切换是关键我封装成两个函数static void CS1237_DOUT_Mode_Input(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin CS1237_DOUT_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 上拉输入 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(CS1237_DOUT_PORT, GPIO_InitStructure); } static void CS1237_DOUT_Mode_Output(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin CS1237_DOUT_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(CS1237_DOUT_PORT, GPIO_InitStructure); }初始化函数如下void CS1237_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin CS1237_SCK_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(CS1237_SCK_PORT, GPIO_InitStructure); CS1237_DOUT_Mode_Input(); CS1237_SCK_L(); DelayMs(100); // 写入配置通道110SPSPGA128 CS1237_WriteCommand(0x07); // 等待第一次转换完成 DelayMs(150); }注意初始化后一定要延时一段时间。CS1237上电后需要稳定时间内部基准和PGA都要建立起来我一般至少等100ms然后写一次命令再等一个完整的转换周期10SPS下就是100ms。急着读数据容易读到上电毛刺。4.2 读数据函数和写命令函数读数据函数要注意超时保护否则DOUT一直为高芯片没工作、线没接好时程序会卡死在等待循环里int32_t CS1237_ReadRaw(void) { uint32_t raw 0; uint32_t timeout 0xFFFFFF; uint8_t status 0; uint8_t i; int32_t value; // 等待DOUT拉低表示转换完成 while (CS1237_DOUT_READ() Bit_SET) { if (--timeout 0) { return 0; // 超时返回0调用方需要判断 } } // 发送26个SCK脉冲 for (i 0; i 24; i) { CS1237_SCK_H(); raw 1; if (CS1237_DOUT_READ() Bit_SET) { raw | 1; } CS1237_SCK_L(); } // 第25个SCK读取状态位 CS1237_SCK_H(); DelayUs(1); status CS1237_DOUT_READ(); CS1237_SCK_L(); // 第26个SCK结束本次读取 CS1237_SCK_H(); DelayUs(1); CS1237_SCK_L(); // 24位补码转有符号数 if (raw 0x800000) { value (int32_t)(raw | 0xFF000000); } else { value (int32_t)(raw 0x00FFFFFF); } // 状态位为0说明上一次配置命令没有写成功可在这里做告警或重新配置 if (status 0) { // 此时建议重新执行一次写命令 CS1237_WriteCommand(0x07); } return value; }这里有一点要解释SCK翻转之间我加了DelayUs(1)。STM32F103主频72MHzGPIO翻转本身不到200ns不加延时也能跑。但CS1237的SCK时序有最小脉宽要求而且线缆长了之后信号边沿会变缓加1微秒延时换来的是更强的抗干扰能力。实测在20cm杜邦线下不加延时偶尔会错位加了就没事。SCK频率实际在几百kHz对多数应用完全够用。写命令函数如下void CS1237_WriteCommand(uint8_t cmd) { uint8_t i; CS1237_SCK_L(); // DOUT切换为输出模式 CS1237_DOUT_Mode_Output(); // 8位命令高位在前SCK上升沿写入 for (i 0; i 8; i) { if (cmd 0x80) { GPIO_SetBits(CS1237_DOUT_PORT, CS1237_DOUT_PIN); } else { GPIO_ResetBits(CS1237_DOUT_PORT, CS1237_DOUT_PIN); } cmd 1; CS1237_SCK_H(); DelayUs(1); CS1237_SCK_L(); DelayUs(1); } // 写完立即恢复输入模式 CS1237_DOUT_Mode_Input(); }写命令的两个细节一是DOUT切到输出模式前SCK必须保持低电平二是写完最后一位后DOUT要马上恢复输入模式因为下一秒芯片就可能把DOUT拉低表示数据就绪了。恢复晚了相当于你和芯片同时在驱动一根线轻则数据错误重则影响引脚寿命。4.3 数据换算与软件滤波原始码值读出来了怎么变成电压或者重量最严谨的做法是实物标定而不是查手册算理论系数。因为CS1237的满量程输入范围、内部基准的绝对精度每个片子和每个批次都有偏差。假设你用称重传感器标定方法很简单空载时读一个值zero_raw放一个已知重量的砝码比如500g再读一个值span_raw。那么斜坡系数就是float weight (float)(raw - zero_raw) / (float)(span_raw - zero_raw) * 500.0f;把zero_raw和span_raw存在Flash里每次上电自动校准就能得到不错的重量值。这个方法比理论换算靠谱得多因为它把传感器灵敏度、ADC基准误差、PGA增益误差、机械安装误差全部一次性校准掉了。至于软件滤波CS1237在10SPS下本身数据已经很稳但机械振动、电源毛刺还是会混进来。我常用滑动平均滤波窗口大小根据系统动态响应要求来定。称重类应用用16点滑动平均很合适#define FILTER_N 16 float CS1237_GetWeight(void) { static int32_t buf[FILTER_N]; static uint8_t index 0; static uint8_t count 0; int32_t sum 0; uint8_t i; int32_t raw; raw CS1237_ReadRaw(); buf[index] raw; index (index 1) % FILTER_N; if (count FILTER_N) { count; } for (i 0; i count; i) { sum buf[i]; } return (float)sum / (float)count; }如果现场有脉冲式干扰比如高频焊接、电机启停滑动平均可能压不掉那就要上中位值平均滤波先排序去掉最大值最小值再取平均。STM32F103主频72MHz跑16个点排序也就几十微秒完全没压力。4.4 main函数示例串口打印测量结果完整示例很简单串口输出标定后的重量和原始码值int main(void) { int32_t raw; float weight; Delay_Init(); USART1_Init(115200); CS1237_Init(); printf(CS1237 Test Start\r\n); while (1) { raw CS1237_ReadRaw(); weight CS1237_GetWeight(); printf(raw%d, weight%.2f g\r\n, raw, weight); DelayMs(100); } }如果你用的是HAL库加CubeMX思路完全一致CubeMX里配置两个GPIO作为输出和输入System Clock配到72MHz串口开一个然后把CS1237_Init、CS1237_ReadRaw、CS1237_WriteCommand这三个函数原样搬进去把GPIO操作改成HAL_GPIO_WritePin和HAL_GPIO_ReadPin就行。5. 调试实录5个常见异常的排查与修复5.1 数据一直是0xFFFFFF或者负满量程这个现象在CS1237调试里特别常见。0xFFFFFF转成有符号数就是-1负满量程。出现这种情况先查输入信号是否接反或者超量程。我遇到过一次很典型的情况传感器信号正常电压表测AINP和AINN之间只有几毫伏但ADC输出就是0xFFFFFF。最后发现是传感器激励的E和E-接反了导致电桥输出极性翻转。另外如果PGA128输入信号超过±20mV左右ADC也会因为输入超出量程而饱和到满码。排查顺序用万用表测AINP和AINN之间的电压应该是毫伏级别然后交换S和S-试试如果还不行把AINP和AINN直接短接看输出是否回到零位附近。短接后读数还不对那问题就在ADC或通信不在传感器。5.2 DOUT一直为高读取超时程序卡在等待DOUT低电平的地方大概率是芯片没有正常工作。先量VDD是不是3.3V再量DOUT引脚是不是有上拉电阻。有的模块DOUT是开漏输出必须外部上拉才能产生高电平。还要注意SCK在芯片上电时不能悬空或者乱跳。CS1237对上电时的SCK状态有要求SCK最好由MCU控制稳定为低如果SCK悬空芯片可能进入错误状态。我用GPIO配置SCK为推挽输出初始化时先拉低这个坑基本就不会遇到了。如果VDD正常、SCK也正常DOUT还是高用示波器或者逻辑分析仪抓一下DOUT波形看它有没有瞬间拉低的脉冲。如果连脉冲都没有芯片可能没工作需要检查焊接特别是SOP-16封装脚距小很容易连锡。5.3 数据跳变太大读数不稳定CS1237在10SPS下理论上非常稳如果读数还在大幅跳动先怀疑硬件再怀疑软件。检查供电纹波用示波器量VDD如果纹波超过30mV就要加强去耦或者在VDD前面加一级LC滤波。开关电源供电的话这个问题几乎是必现的。然后是参考电压。很多模块直接用内部基准内部基准虽然方便但抗电源噪声能力有限。我做过对比模块改用外部REF3125基准后数据跳动幅度能减小一半。对精度有要求的项目这一步值得做。软件上也要看你的滤波策略。原始码值拿来直接显示不跳动才怪。至少做一次16点滑动平均再显示。如果做了滤波还是跳那就要怀疑现场有强干扰源比如变频器、继电器、无线模块。把信号线换成屏蔽线或者调整布线走向比写一百行滤波代码都有用。5.4 读数有规律性漂移漂移分两种短期漂移和长期漂移。短期漂移比如上电后前几分钟数据一直在慢慢变这是芯片自热效应。CS1237内部有放大器上电后功耗导致芯片温度上升零位就会跟着漂。解决方法是上电预热或者做上电后的零点校准。长期漂移和环境温度强相关。如果发现早晨和中午零位不一样大概率是传感器或者参考电压的温漂问题。传感器温漂要靠传感器的温度补偿曲线去修正ADC这部分外部基准能帮上很大的忙。总之高精度ADC的调试很大一部分在调理模拟链路而不是在写代码。5.5 用逻辑分析仪看时序调试CS1237有个逻辑分析仪会舒服很多。把SCK和DOUT都接上采样率25MHz就够了抓一段波形看三个点第一DOUT有没有从高变低的下降沿第二SCK是不是连续的26个脉冲第三第25个SCK上升沿读到的状态位是高还是低。我第一次调的时候代码看起来没问题但数据就是不对。用逻辑分析仪一看发现SCK的第24个脉冲和第25个脉冲之间被串口调试代码打断中间多了一个毛刺数据整个错位了。后来把所有不必要的操作挪出读取过程时序就干净了。这也是为什么我强烈建议在读取函数里不要做打印、不要做耗时的浮点运算先把数据锁存下来再去做后续处理。6. 写在最后我的个人建议CS1237这类高精度ADC单独拿出来说难度不在芯片而在整个模拟链路的配合。电源、参考电压、传感器接线、代码时序、滤波策略任何一环掉链子最后的数据都会很难看。我的经验是调试时先把“能读数”作为第一步目标不管数值准不准先把波形和时序稳定下来第二步再去做标定和滤波第三步才是优化功耗、增加多通道、甚至配合DMA做连续采集。最后送一个小技巧CS1237读完数据后状态位为0时不要慌重新执行一次写命令就行。我在批量测试中发现个别芯片在上电瞬间或强干扰下会丢掉配置导致输出异常。在读取函数里加一个状态位判断发现配置丢失就自动重新配置整机稳定性会明显提升。这个方法我用了很多项目几乎每个用到CS1237的产品都会加上。