☰
STM32 HAL库驱动CS1237高精度ADC:从原理图设计到调试踩坑实录
2026/9/28 12:33:40 网站建设 项目流程

CS1237这颗24位Sigma-Delta ADC,我在好几个称重项目里都踩过坑攒过经验。最近帮一个做工业衡器的朋友把HX711的板子换成CS1237,顺便把驱动从标准库迁到STM32 HAL库,整个流程走下来,从画原理图到最终稳定读数,中间遇到了一堆数据手册里写得不痛不痒、但实际调试时特别要命的问题。这篇东西就按我实际操作的顺序来写,从芯片选型、原理图设计、HAL库驱动实现,到最后拿示波器抓时序排坑,尽量把关键节点讲透,给正准备用CS1237做项目的人一条能直接抄的近路。

这颗芯片最吸引人的地方,不是它标称的24位分辨率,而是它在实际小信号采集场景下表现出来的稳定性和性价比。你用它做桥式传感器、压力变送器、或者热电偶冷端补偿之后的电压采集,只要外围电路不犯低级错误,出来的数据可以有效位数拉到18到20位,这在很多工业场景已经够用了。更重要的是,它内部把PGA、振荡器、基准源这些都集成进去了,外部电路可以做得非常精简,BOM成本压得很低。

先说一个我的总体判断:CS1237适合量程小、信号缓变、对成本和功耗敏感的应用,不太适合需要做高速采样的场景。它的输出速率最高也就1.28kHz,你要是拿去采集音频或者振动信号,明显不合适。但做称重、拉力、气压这种低频物理量,它比很多入门级ADC都要从容。

1. 项目定位与方案选型

1.1 这颗芯片解决什么问题

CS1237本质上是一个单通道差分输入的24位Sigma-Delta ADC,它内部集成了可编程增益放大器(PGA)、内部振荡器和1.2V基准源。你在使用中不需要给它提供外部时钟,也不需要单独买基准芯片,一个简单的稳压源、几个滤波电容,再加一颗MCU,就能拼出一套完整的高精度采集链路。

它的输出数据通过一个类SPI接口和主机通信,但和标准SPI有点差异。这个接口只有三根线:SCLK、DOUT/DRDY、CS,其中DOUT和DRDY是同一个引脚分时复用。转换完成后,这个引脚会拉低指示数据就绪;读取数据时,它又作为数据输出引脚。这种省引脚的设计对封装很友好,但也意味着你没法用STM32的硬件SPI直接接,只能靠GPIO模拟时序。

在实际工程项目里,CS1237经常被用来替代HX711。相比之下,CS1237的PGA增益有1、2、64、128四挡可选,而HX711固定为32或64倍。增益可选带来的直接好处是适用范围更宽,比如你既想接满量程20mV的桥式传感器,又想接输出幅度稍大的调理电路输出,无需改板,直接改配置寄存器就能切换。

还有一个工程师很关心的点,就是芯片的电源电压范围比较宽,DVDD和AVDD都可以在2.7V到5.5V之间工作。这意味着3.3V和5V系统都能直接兼容,不需要额外的电平转换电路。我做过一个用3.3V供电的便携测力计,整个模拟部分就一片CS1237加几颗电容,PCB面积非常紧凑。

1.2 为什么不用HX711或ADS1232

很多人一听说高精度ADC,第一反应就是HX711。确实,HX711在电子秤方案里占有率高,资料多,代码满天飞。但它有几个问题在实际项目中挺要命。

首先是增益和输出速率不可配置,HX711的数据率靠外部引脚电平选择,增益模块也固定,想换量程得改硬件。其次,HX711的启动时间比较长,我第一次用的时候,上电后数据稳定等了将近一秒钟,在需要快速响应或频繁唤醒的低功耗场景里很难受。

ADS1232是德州仪器的产品,性能确实比HX711好,但价格高,而且在国内采购周期和供货稳定性不如国产芯片有优势。CS1237作为国产器件,交期短、价格便宜,而且性能指标和ADS1232在同一水平线上。对成本敏感、又需要稳定供货的量产产品来说,这个优势是决定性的。

还有一个容易被忽略的点是CS1237的功耗。它在正常工作模式下的电流可以做到毫安级别以下,相比HX711动不动好几毫安的电流,在电池供电的设备里优势明显。我做过一个低功耗的无线称重标签,休眠时把CS1237完全断电,需要测量时才上电并等待稳定,整机平均功耗可以压得非常低。

所以在做方案选型时,我个人的判断标准很简单:如果项目对性能要求不高、产品形态成熟、不追求进一步优化成本,那继续用HX711没有任何问题;但如果想要灵活配置增益、需要更低功耗、或者想在小批量产品里压缩模拟部分的BOM,CS1237是更好的选择。

2. 原理图设计的几个关键决策

2.1 基准电压不是随便接的

高精度ADC的性能上限,往往不由ADC本身决定,而由基准电压的稳定度和噪声决定。CS1237内部集成了1.2V基准源,如果对精度要求不是极致,直接用内部基准就能工作。但你一定要理解一个约束:内部基准的精度和温漂水平是有限的,它并不能替代高端外部基准。

如果项目要求在全温度范围内误差小于0.1%,或者你需要在较宽温度范围内保持测量一致性,那就要考虑外部基准。但这里有个非常关键的坑:外部基准电压Vref必须小于或等于AVDD,否则ADC的输入范围和线性度会严重劣化。我见过一个调试现场,硬件同事把2.5V基准直接接到Vref引脚,AVDD却用了3.3V,理论上没问题,但由于基准芯片的驱动能力和去耦不达标,输出满量程附近的数据非线性特别明显。

还要考虑Vref引脚的等效输入阻抗不算高,如果用外部基准,最好在Vref引脚对地加一个10uF的钽电容和0.1uF的陶瓷电容并联,而且基准源的输出最好经过一个RC滤波再进来。这里RC的截止频率不需要太高,能压住带外噪声就行,我一般选100欧姆配1uF,大约1.6kHz的截止频率,足够稳定也足够安静。

如果用内部1.2V基准,AVDD的稳定性就直接影响基准稳定性,因为内部LDO是从AVDD取电调整的。所以AVDD的电源质量不能马虎,建议用一个精度1%以上的LDO单独供电,而不是直接从数字电源上拉过来。

2.2 电源与地的处理

高精度ADC对电源纹波和地噪声非常敏感,这是老生常谈,但实际布线时很多人还是草率了。我的建议是:AVDD和DVDD分开走,分别经过磁珠或RC滤波到各自引脚。磁珠选择时注意DC电阻别太大,否则负载电流会引起压降超标。我用过0欧姆电阻加电容的方式,效果也还可以,关键是模拟电源和数字电源之间不要在芯片附近直接大面积连通。

地平面的处理我也单独强调一下。CS1237这类Sigma-Delta ADC,内部有数字滤波器和调制器,数字开关噪声容易耦合到模拟部分。在PCB上,建议把模拟地和数字地单点连接,连接点放在芯片下方的AGND引脚附近,形成一个星型接地结构。如果板子上有大电流的数字逻辑,尽量让它们远离模拟区域。

另外特别注意SCLK和DOUT这两根线的走线,它们是数字信号,如果靠近模拟输入走线,噪声会通过寄生电容耦合进去。我踩过一次坑:双面板上,SCLK走线和传感器差分信号线在相邻层平行走了大概3厘米,结果采样数据里出现明显的周期性毛刺,后来把数字线拉远、中间加地线隔离,问题才消失。

电源去耦电容的位置也很重要,0.1uF的陶瓷电容要尽量靠近AVDD引脚,地端直接打过孔到模拟地平面。我看到很多工程师喜欢把电容放在板子角落,那是没用的,高频噪声在引脚处产生,必须就地吸收。

2.3 传感器接口与信号调理

CS1237的差分输入是真正意义上的差分,它测量的是AIN+和AIN-之间的电压差,对共模干扰有抑制能力。但你要注意,差分方式并不等于可以随便接,共模电压范围是有要求的,超过范围后ADC的输入级会饱和,读数完全失真。因此传感器桥路的激励方式、公共端电位,都要在原理图阶段仔细核算。

对于典型的应变式称重传感器,一般做法是:传感器的激励电压从AVDD或者一个专门的低噪声稳压源取,传感器的两个输出端分别接AIN+和AIN-,同时在每个输入端对地加一个几十纳法的滤波电容。这里电容容值不能太大,否则会明显增加ADC输入端的建立时间,影响转换结果。我一般用10nF或者22nF,配合一个几百欧姆的串联电阻,构成一个低通滤波器,既能滤除高频干扰,又不会让信号建立过慢。

还有一点容易被忽略:CS1237的输入端没有内置缓冲器,输入阻抗在PGA开启时并不是无穷大。如果传感器输出阻抗太高,或者信号源驱动能力弱,会直接影响精度。以前我用一个输出阻抗几十千欧的电位器做调试,读数怎么都不稳定,后来才知道是输入阻抗分压引起的。解决方案很简单,在信号源和ADC之间加一级运放跟随器,或者选择输出阻抗足够低的传感器。

如果项目信号本身就非常微弱,比如热电偶输出只有几十微伏,那你需要在前级增加仪表放大器,把信号放大到CS1237能达到最佳分辨率的范围,而不是靠CS1237内部那点PGA硬撑。PGA虽好,但增益太大时噪声也会被放大,信噪比不一定提升多少。

3. STM32 HAL库驱动编写

3.1 确定接口时序

CS1237的接口时序和标准SPI略有不同,它更像是一种位操作协议。你需要做的第一件事,是把数据手册上的时序图吃透,然后根据MCU的主频计算每个延时值。

通信开始前,CS引脚先拉高,SCLK空闲状态可以置高。转换完成后,DOUT/DRDY会自动拉低,主机检测到这个低电平后,把CS拉低,启动一次读取操作。读取过程中,主控在SCLK的下降沿准备好数据,在上升沿读取DOUT引脚的电平,连续读取24位数据,这才是ADC转换结果。之后还可以继续发送时钟,在第25个时钟周期内,DOUT会输出通道状态信息。读取结束后,拉高CS。

这里必须提醒一句:CS1237的时序参数中有一些最小脉宽和建立时间要求,不同批次芯片的时序略有差别,但总体差距不大。用GPIO模拟时,翻转SCLK之后最好加个几百纳秒的延时,尤其是SCLK由低变高之后、读取数据之前,这个建立时间如果太短,读出来的高位永远是错的。我见过很多开发者的代码读出来数据正常,但总是偶尔跳变,就是因为在SCLK上升沿后立即读了数据,边际时间不满足。

另外,上电时一定要等芯片初始化完成。CS1237内部有一个上电复位电路,上电后需要一段时间稳定,如果太快发命令,芯片可能不响应。标准做法是上电后延时至少100毫秒再开始通信,实测这个值越宽裕越安全。

3.2 GPIO初始化与底层封装

用HAL库驱动CS1237,底层无非是几个GPIO的读写操作。但我在写代码前会先做一个封装层,把所有和硬件相关的操作隔离出来,方便以后移植到其他MCU。

先看CS1237的GPIO初始化,用CubeMX配置三个引脚为普通推挽输出,DOUT引脚需要配置为输入模式。如果MCU引脚支持内部上拉,我建议在读取模式下打开上拉,这样可以保证未通信时DOUT引脚电平确定,防止误触发读取流程。CubeMX里把DOUT设置为输入模式,SCLK和CS设置为推挽输出,速度可以选Low,这种低速信号不需要摆率太高的配置。

底层封装我一般写成这样:

#define CS1237_CS_LOW() HAL_GPIO_WritePin(CS1237_CS_GPIO_Port, CS1237_CS_Pin, GPIO_PIN_RESET) #define CS1237_CS_HIGH() HAL_GPIO_WritePin(CS1237_CS_GPIO_Port, CS1237_CS_Pin, GPIO_PIN_SET) #define CS1237_SCLK_LOW() HAL_GPIO_WritePin(CS1237_SCLK_GPIO_Port, CS1237_SCLK_Pin, GPIO_PIN_RESET) #define CS1237_SCLK_HIGH() HAL_GPIO_WritePin(CS1237_SCLK_GPIO_Port, CS1237_SCLK_Pin, GPIO_PIN_SET) #define CS1237_DOUT_READ() HAL_GPIO_ReadPin(CS1237_DOUT_GPIO_Port, CS1237_DOUT_Pin)

其中CS1237_CS_GPIO_Port这类宏是CubeMX生成的引脚定义宏。封装之后,驱动代码里只调用这些宏,就算后期换到其他引脚甚至换MCU,只需要改这个头文件,驱动逻辑完全不用动。

微秒级延时函数也建议单独写好。HAL库自带的HAL_Delay是毫秒级,做位操作时序根本不够用。Cortex-M内核自带DWT计数器,可以非常方便地实现微秒延时。我用的是基于DWT的延时方案,精度和稳定性都比简单的NOP循环好很多,关键是不占用定时器资源。初始化代码里使能DWT:

CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

然后延时函数就是简单的周期计数。

3.3 读取转换结果的实现

读取转换结果是整个驱动最核心的部分。时序上要先等待DOUT变低,这个过程不能死等太久,因为如果芯片没工作或通信异常,程序可能会卡死在循环里。我习惯加一个超时保护,超过预定时间就返回错误标志,这样系统至少不会死机。

一个典型的读取流程是这样:

uint8_t CS1237_ReadData(int32_t *adc_value) { uint32_t timeout = 10000; uint8_t data_temp = 0; int32_t raw = 0; // 等待DOUT变低,表示转换完成 while (CS1237_DOUT_READ() != 0) { if (--timeout == 0) { return 1; } } CS1237_CS_LOW(); CS1237_SCLK_LOW(); delay_us(1); // 读取24位数据 for (uint8_t i = 0; i < 24; i++) { CS1237_SCLK_HIGH(); delay_us(1); raw <<= 1; if (CS1237_DOUT_READ()) { raw |= 1; } CS1237_SCLK_LOW(); delay_us(1); } // 第25个时钟周期,DOUT输出通道状态 CS1237_SCLK_HIGH(); delay_us(1); data_temp = CS1237_DOUT_READ(); CS1237_SCLK_LOW(); // 拉高CS,结束通信 CS1237_CS_HIGH(); // 24位结果是有符号数,转成int32 if (raw & 0x800000) { raw |= 0xFF000000; } *adc_value = raw; // 可以根据data_temp判断当前通道,多通道切换时有用 return 0; }

这段代码里最关键的操作是,每一位数据都是在SCLK高电平期间去读DOUT。数据手册上会说明DOUT在SCLK的哪个边沿更新,你必须保证采样时刻在数据稳定之后。我在SCLK高电平之后加了1微秒延时,这个时间足够满足绝大多数情况下的建立时间要求。

延时1微秒看起来很短,但对GPIO模拟的时序稳定很关键。如果MCU的主频高,GPIO翻转本身就很快,延时主要是为了保证DOUT输出数据的建立时间,尤其当CS1237的供电电压偏低时,内部逻辑速度会稍慢,这个延时更要给足。

24位数据转换成有符号数也不要忽略。CS1237输出的原始数据是二进制补码格式,最高位是符号位,所以如果最高位为1,需要在int32_t的高字节补上1,否则直接赋给int32_t会变成正数,后续计算的符号全乱。这一步很基础,但很多人第一次调的时候都会在这里绕一下。

3.4 配置寄存器与工作模式切换

CS1237支持通过命令配置工作模式,包括通道选择、PGA增益、输出速率等。虽然某些项目从上电到结束都只用默认配置,但实际做产品时通常会根据传感器量程调整增益。

写配置寄存器的时序和读数据类似,只是发送的数据内容不同。主机拉低CS,然后把配置命令按位发送到芯片。命令格式、寄存器地址和配置位定义都以芯片手册为准。

以配置寄存器为例,我一般封装一个写命令函数:

void CS1237_WriteCmd(uint8_t cmd) { CS1237_CS_LOW(); CS1237_SCLK_LOW(); delay_us(1); for (uint8_t i = 0; i < 8; i++) { CS1237_SCLK_HIGH(); delay_us(1); if (cmd & 0x80) { CS1237_DOUT_HIGH(); } else { CS1237_DOUT_LOW(); } CS1237_SCLK_LOW(); delay_us(1); cmd <<= 1; } CS1237_SCLK_HIGH(); delay_us(1); CS1237_CS_HIGH(); }

注意,写命令时DOUT引脚要切换为输出模式。在同一个引脚上进行输入输出切换时,必须先改变GPIO方向再操作数据,否则容易产生毛刺。HAL库切换IO方向可以用:

GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = CS1237_DOUT_Pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(CS1237_DOUT_GPIO_Port, &GPIO_InitStruct);

每次切换方向都初始化一遍GPIO,效率有点低,但胜在稳定。如果项目对效率要求高,可以使用LL库直接操作寄存器,只需要一条语句就能切换模式。HAL库的好处是工程可读性好,但在这个场景下确实有些笨重。

配置PGA增益和输出速率时,我通常会根据传感器的满量程输出电压来算。比如一个2mV/V的传感器,5V激励下满量程输出是10mV,内部1.2V基准,选择128倍增益后满量程对应1.28V,但传感器实际输出只有10mV,经过128倍放大是1.28V,已经接近满量程,这样有效分辨率最高。如果传感器输出幅度偏大,比如几十毫伏,选64倍更合适。

4. 调试过程与常见问题排查

4.1 第一次上电:数据全零或全满

我见过最典型的首次调电问题是读取数据全零或者全满。多数情况下不是因为CS1237坏了,而是通信时序根本没建立起来。

全零的原因通常是DOUT引脚配置有问题,或者读取时SCLK的采样边沿搞反了。CS1237数据手册规定,数据在SCLK下降沿更新,主机在SCLK高电平时采样。如果你的代码在SCLK下降沿读数据,读到的往往是下一位之前的状态,结果自然不对。还有一个常见原因是等待DOUT变低这一步卡死了,超时返回错误码,但主程序没处理错误,默认ADC值是0,看起来就像全零。

全满(0xFFFFFF)则多半是芯片没有真正转换,DOUT一直保持高电平。这时候要检查CS片选时序,以及上电后是否等待了足够长的稳定时间。我调试时习惯在初始化之后多发几次空读操作,让芯片内部状态机完全进入正常状态,然后再开始正式读取。

另外要注意CS引脚不能长期拉低。CS1237通信结束后,必须拉高CS,否则芯片可能持续输出数据,DOUT电平不受控,后续读取也必然错乱。

4.2 数据跳字、噪声超预期的处理

数据跳字是最磨人的问题。如果你发现CS1237的输出在稳定输入下仍然有较大的跳动,先别急着改代码,按以下顺序排查外围硬件。

先看电源。用示波器测量AVDD引脚上的纹波,如果纹波超过5mV,在5V系统里就比较危险了。解决办法是换LDO,或者增加一级LC滤波。我用过最简单的办法是在AVDD引脚前串一个10欧姆电阻,再并联一个100uF电容,对低频纹波抑制效果立竿见影。

再看传感器引线。差分信号线最好用双绞线或屏蔽线,屏蔽层单端接地。如果现场有电机、继电器这类干扰源,还要在ADC输入端加共模电感或者加强RC滤波。

软件层面上,CS1237是过采样ADC,内部数字滤波器已经滤掉了很多带外噪声,但它对50Hz工频干扰的抑制能力有限。如果采样速率设置在10Hz或40Hz,实际上可以看到明显的工频成分。这种情况下可以做一个滑动平均滤波或中值滤波,非常有效。我常用的是16点滑动平均,几行代码,就能把跳字从正负几十个LSB压到几个LSB以内。

滤波器代码大概长这样:

#define FILTER_N 16 static int32_t filter_buf[FILTER_N]; static uint8_t filter_idx = 0; static int32_t filter_sum = 0; int32_t ADC_Filter(int32_t new_value) { filter_sum -= filter_buf[filter_idx]; filter_buf[filter_idx] = new_value; filter_sum += new_value; filter_idx = (filter_idx + 1) % FILTER_N; return filter_sum / FILTER_N; }

注意,滑动平均滤波会引入滞后,如果你要做的系统对响应速度有要求,比如动态称重或者快变压力监测,这种滤波器就不太合适。可以先根据项目需求确定目标带宽,再决定滤波强度。

4.3 与HAL库配合时的几个坑

用HAL库做这套驱动,最大的麻烦不是GPIO读写本身,而是系统里其他优先级更高的中断抢占时序。CS1237的位操作通信并没有规定SCLK要跑多快,但它的时序要求SCLK的高电平和低电平时间不能太短,你不能用中断来拖长某个脉冲,结果就是时序被拉得到处都是毛刺。

我在一个项目里就遇到过,系统有一个1ms周期的定时器中断用于按键扫描,中断函数里做了一些浮点运算,跑到一半的时候正好撞上CS1237的SCLK翻转,DOUT采样点被推迟了几微秒,数据偶尔就跳一位。后来我把CS1237读取函数放到一个专用的高优先级中断服务函数里,读取期间屏蔽其他中断,才算彻底解决。

HAL库还有一个地方要注意,HAL_GPIO_ReadPin和HAL_GPIO_WritePin内部有断言检查和一些判断逻辑,直接调用会引入少量延时,但这个延时是固定的,只要你的时序设计时统一使用了这些延时,问题不大。怕的是你在我想高效读取时用寄存器直接操作,又在另一些地方用HAL函数,两边的延时不对称,就会导致位数据采样点漂移。

所以我的建议是,底层的引脚读写全部用宏定义封装,不要在循环里反复调用HAL_GPIO_ReadPin这种库函数,哪怕它只多消耗几个时钟周期,在高频主控上影响可能不明显,但时序一致性对这类传感器非常重要。

另外,如果你把CS1237的读取放在主循环里,前一个读取还没完成,后一个读取又开始了,会导致CS片选时序混乱。更好的做法是使用状态机,或者至少保证两个读取周期之间留出足够的间隔时间。

4.4 常见问题速查表

为了让你排查起来更快,我把实际项目中遇到的问题整理成一个表,方便对照。

现象可能原因快速排查方法
上电后一直读到0x000000DOUT引脚输入配置错误,或通信超时被忽略示波器量DOUT在转换完成后是否变低
上电后一直读到0xFFFFFF芯片未启动转换,或CS时序错误检查上电后是否延时100ms,CS是否拉高
数据在稳定输入下跳变较大AVDD纹波大、信号线引入干扰、采样边沿不对示波器测AVDD纹波,检查RC滤波,调整SCLK采样点
读数整体偏大或偏小Vref与AVDD关系不对,或PGA增益配置错误核对Vref电压,检查配置寄存器写入值与期望值
多通道切换后通道数据错乱切换通道后未丢弃第一个转换结果切换后丢弃至少一个采样点再取有效数据
代码卡死在等待DOUT变低芯片未正常供电、通信引脚接触不良用逻辑分析仪抓SCLK/DOUT波形,确认通信有响应
用HAL_Delay后采集数据偶发跳变延时函数被中断打扰,时序不稳定换成DWT微秒延时,读取期间屏蔽中断

调试这类芯片,工具很重要。我强烈建议手里常备逻辑分析仪,哪怕是十几块钱的8通道简易版,抓GPIO波形也够用。你只需要把SCLK、DOUT、CS三个信号挂上去,就能非常直观地看到通信时序是否符合手册要求。很多软件上的问题,在波形图面前一目了然,比对着代码猜要快得多。

还有一个经验,上电后第一次读取的数据,最好不要直接使用。CS1237内部滤波器和PGA需要几个转换周期才能达到稳定状态,特别在刚切换增益或通道之后,前几个数据点偏差可能非常大。正确的做法是,初始化时多读几次,丢掉前面的数据,取后面稳定的读数作为有效数据。这个逻辑我在所有项目里都会加进去,几乎零成本但能避免很多诡异的偶发问题。

如果你在调试中遇到“传感器没动,数据自己往上漂”的情况,先检查是不是环境温度变了导致传感器零点漂移,然后再怀疑ADC。你可以把CS1237输入端短接,看看输出是否接近零。如果输入端短接后依然有漂移,那就是ADC的失调或电源温漂问题,可以考虑在每次上电时做一个零点校准,把失调存下来,之后的所有读数都减去这个零点值。这类校准逻辑可以让系统在全温范围内表现稳定得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询