☰
STM32 驱动 CS5532 高精度 ADC:SPI 时序、校准与稳定读数实战
2026/10/27 18:16:14 网站建设 项目流程

简介:模数转换芯片CS5532的STM32驱动源码包,面向数据采集、高精度测量仪表及工业控制领域的嵌入式开发者,解决在STM32平台上快速驱动CS5532的问题,内含引脚配置、寄存器读写、时序控制等核心代码,移植性强。压缩包仅2个文件,包含一个C源文件和一个头文件,整体4KB,代码精简,便于阅读与二次开发,已有630人浏览学习,适合直接复制到自己的工程中快速验证芯片通信。头文件对SCK、SDI等引脚做了清晰宏定义,C源文件实现具体通信流程,并基于标准外设库编写,通过bsp.h/io.h分离硬件依赖,移植到项目时仅需修改宏定义即可。该资源可作为学习CS5532驱动原理的入门范例,也可嵌入现有项目节省底层调试时间,尤其适合做高精度模拟量采集的初、中级开发者参考。

1. CS5532 基于 STM32 的驱动程序:为什么读回来的数总像在乱跳

有人拿着一份 CS5532 基于 STM32 的驱动程序 C 源码,兴冲冲把工程建好,SPI 也通了,结果读回来的 24 位数据不是 0xFFFFFF 就是小范围乱跳,查半天不知道是命令字没写对还是校准没做。CS5532 这颗 24 位 ΔΣ ADC 本身不复杂,驱动难点在三点:SPI 时序必须以手册为准、命令字和寄存器配置不能想当然、每次上电后的偏移/增益校准不做稳,后面所有数据都是废的。这份驱动源码的价值,就是把 STM32 标准库/HAL 库下的 SPI 操作、CS5532 命令封装、校准和单次/连续转换流程揉到了一起。适合正在做称重、压力、桥式传感器采集,以及想从“能读到数”走到“读得准”的嵌入式工程师。

2. 接口基础:SPI 模式 0、片选时序与引脚分配

2.1 先搞清 CS5532 的通信对象:SPI 模式 0 与命令/应答结构

CS5532 的串口是 4 线 SPI,主机是 STM32,从机是 CS5532。第一次搞的人最容易在 SPI 模式和命令结构上吃亏。CS5532 手册里的串行时序对应的是 SPI Mode 0,也就是 CPOL=0、CPHA=0:SCLK 空闲为低,数据在上升沿被采样,下降沿切换。STM32 的 SPI 配置要把这两个位设对,否则读回来的数据会整体向左或向右错一位,而且这种错位是“稳定地错”,你不会马上发现,等算电压时才觉得量纲不对。

命令结构上,CS5532 不是那种“读一个寄存器就要先写寄存器地址”的普通器件。它更接近“命令字节 + 数据字节”的帧结构:主机先通过 SDI 发送命令字节,CS5532 在 SDO 上回数据;写配置寄存器时,命令字节后面跟的是寄存器数据和命令字;读转换结果时,主机发送读命令后,连续读回 3 个字节,24 位转换结果按二进制补码排列。命令字节本身不是 0x00、0xFF 这种随手定的,必须按数据手册里的 Command / Register Address 表查。这份驱动源码里这些命令值已经做成了宏,放到cs5532_cmd.h里,下面这段代码是把宏定义“翻译”成一次 SPI 帧传输的底层函数:

/* cs5532.c —— 底层 SPI 帧传输 */ static void CS5532_SpiFrame(const uint8_t *txBuf, uint8_t *rxBuf, uint16_t len) { uint8_t tx, rx; CS5532_CS_LOW(); /* 拉低片选,让 CS5532 开始接收 */ HAL_Delay(1); /* CS 建立时间,给从机留余量 */ for (uint16_t i = 0; i < len; i++) { tx = (txBuf != NULL) ? txBuf[i] : 0x00; rx = 0x00; HAL_SPI_TransmitReceive(&hspi1, &tx, &rx, 1, 10); if (rxBuf != NULL) rxBuf[i] = rx; } CS5532_CS_HIGH(); /* 拉高片选,结束一帧 */ HAL_Delay(1); /* CS 保持时间 */ }

这段代码做了三件事:拉低片选、逐字节完成 SPI 收发、拉高片选结束一帧。len是这一帧里的总字节数,读写共用;txBuf为空时发 0x00,正好用来产生时钟让 CS5532 把数据送到 SDO 上。HAL_SPI_TransmitReceive是全双工收发,STM32 发一个字节的同时能收到一个字节,CS5532 这种“边发命令边回数据”的器件必须要用这个函数,不能用分开的HAL_SPI_Transmit和HAL_SPI_Receive,否则很容易少采一个时钟周期。

2.2 引脚分配:STM32 的 SPI 引脚怎么接到 CS5532

CS5532 的串口引脚是 SCLK、SDI、SDO、CS,STM32 这边对应 SPI 的 SCK、MOSI、MISO 和一个普通 GPIO。我一般习惯用 SPI1,引脚分配是这样:

STM32 引脚CS5532 引脚方向说明
PA5 / SPI1_SCKSCLK主机输出SPI 时钟,建议压到 500kHz 以下
PA7 / SPI1_MOSISDI主机输出命令字节和数据写入
PA6 / SPI1_MISOSDO主机输入转换结果和寄存器回读
PB0CS主机输出软件片选,低电平有效
GNDAGND / DGND共地模拟地和数字地单点连接

注意 SDI 和 SDO 是分开的,不是三线串口。STM32 的 MOSI 必须接 SDI,MISO 必须接 SDO,接反以后读回来的值永远是 0xFF 或者整体取反后的结果。CS 引脚不要用硬件 NSS,SPI_NSS 软件管理,否则 NSS 自动拉高拉低会和手动片选打架。

STM32CubeMX 里初始化配置可以参考这段:

/* SPI1 初始化:主机模式、8bit、模式 0、低速 */ hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; /* CPOL=0 */ hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; /* CPHA=0 */ hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_128; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi1); /* CS 引脚 PB0 配成 push-pull 输出 */ GPIO_InitTypeDef gpio = {0}; gpio.Pin = GPIO_PIN_0; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Pull = GPIO_PULLUP; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, &gpio);

这里最关键的两个参数是CLKPolarity_LOW和CLKPhase_1EDGE,对应 CPOL=0、CPHA=0。波特率预分频用 128,以 STM32F103 的 72MHz 主频算,实际 SCLK 是 562.5kHz,比直接拉到 1MHz 更稳。CS5532 是高分辨率 ADC,采样率本身不高,SCLK 慢一点不损失性能,却能显著降低长线干扰。

2.3 硬件去耦与差分输入:高分辨率 ADC 的地和电源

CS5532 是 24 位 ADC,最后几位能不能稳住,软件只占一半,另一半在硬件。它的 AVDD 和 DVDD 最好分开供电,至少也要用电感或磁珠隔离;REF+ 和 REF- 之间要放 10uF 钽电容并联 0.1uF 陶瓷电容,并且尽量靠近芯片引脚。我的习惯是在 REF+ 对地再接一个 100nF,数字噪声会少很多。

输入侧更要注意:CS5532 的差分输入不是“悬空也能测”的,IN+ 和 IN- 必须有实实在在的共模回路。接桥式传感器时,传感器激励地和 ADC 的 AGND 要在靠近 ADC 的地方单点连接。输入线尽量短,最好加一级 RC 低通,电阻取 1kΩ 到 10kΩ,电容取 0.1uF 到 1uF,截止频率放在几百赫兹就行。放大器的输出阻抗如果比较高,RC 电容会让建立时间变长,采样率就要留出余量。

3. 驱动 C 源码实现:命令封装、校准序列与连续转换流程

3.1 驱动分层:把 SPI 硬操作和 ADC 业务分开

直接在主循环里写HAL_SPI_TransmitReceive当然也能跑,但代码会很乱。这份驱动的做法是分两层:底层只做 SPI 帧传输,上层负责命令字、寄存器配置和结果解析。这样换 STM32 型号的时候,只需要改底层CS5532_SpiFrame里的hspi1和 CS 引脚,业务代码完全不动。

上层代码通过一组带CS5532_CMD_前缀的宏拿到命令字,再调用底层函数。命令字的具体值在这份源码的cs5532_cmd.h里按数据手册定义好了,移植时第一件事就是对照你手上的数据手册确认宏定义没有抄错。下面这段是读转换结果的完整过程:

/* cs5532.c —— 单次转换并读回 24 位原始值 */ int32_t CS5532_ReadRaw(uint8_t ch) { uint8_t tx[4], rx[4]; int32_t raw; CS5532_SetChannel(ch); /* 切换通道,改配置寄存器 */ /* 第一步:发转换命令 */ tx[0] = CS5532_CMD_CONVERT; CS5532_SpiFrame(tx, NULL, 1); /* 第二步:等 SDO 变低,表示转换完成 */ uint16_t timeout = 10000; while (HAL_GPIO_ReadPin(CS5532_DRDY_GPIO_Port, CS5532_DRDY_Pin) == GPIO_PIN_SET) { if (--timeout == 0) return CS5532_ERR_TIMEOUT; } /* 第三步:发读数据命令,同时采集 3 个字节结果 */ tx[0] = CS5532_CMD_READ_DATA; tx[1] = 0; tx[2] = 0; tx[3] = 0; CS5532_SpiFrame(tx, rx, 4); /* 24 位补码转 int32 */ raw = ((int32_t)rx[1] << 16) | ((uint32_t)rx[2] << 8) | rx[3]; if (raw & 0x800000u) raw -= 0x1000000; /* 符号扩展 */ return raw; }

这段代码里,CS5532_SpiFrame一次拉低 CS、发完 4 个字节、再拉高 CS,把读命令和读结果放在同一个片选窗口里,避免了“命令是一帧、数据是另一帧”的边界问题。第一步发转换命令时只发 1 个字节;第二步轮询的是 DRDY 电平,我习惯把 CS5532 的 SDO 复用成 DRDY,这样少占一个 GPIO;第三步为什么是 4 个字节而不是 3 个,是因为主机要发 1 个字节的读命令,再用 3 个空字节产生时钟把结果移出来,rx[1]到rx[3]才是真正的 24 位数据。

timeout必须加。如果 CS5532 没收到转换命令,SDO 永远不变低,没有超时的话程序会死等。超时返回一个负数错误码,业务层判断一下再走重发逻辑。

3.2 初始化序列:复位、配置寄存器与校准

CS5532 上电之后不能直接读数据,必须先做三件事:复位、写配置寄存器、校准。校准是很多人跳过去的一步,因为不校准也能读到数,但那个数里带着芯片自身的偏移和增益误差,做高精度采集时不能直接用。

复位序列一般是这样:拉低 CS,发一个复位命令字节,再拉高 CS,然后延时 10 到 20ms。复位命令在源码里是宏CS5532_CMD_RESET,如果你手里的手册写的是“连续发多个 0xFF”,就在CS5532_SpiFrame外面循环多调几次。复位后配置寄存器,配置项包括通道选择、增益、字率、单极性/双极性等。之后做自偏移校准和自增益校准,每次校准命令发完都要等对应通道的转换完成信号。

/* cs5532.c —— 初始化序列 */ void CS5532_Init(void) { uint8_t cmd; uint8_t cfgBuf[4]; uint32_t cfg = CS5532_DEFAULT_CONFIG; /* 配置字,位域含义见数据手册 */ /* 1. 复位 */ cmd = CS5532_CMD_RESET; CS5532_SpiFrame(&cmd, NULL, 1); HAL_Delay(20); /* 2. 写配置寄存器:通道、增益、字率 */ cfgBuf[0] = CS5532_CMD_WRITE_CFG; cfgBuf[1] = (cfg >> 16) & 0xFF; cfgBuf[2] = (cfg >> 8) & 0xFF; cfgBuf[3] = cfg & 0xFF; CS5532_SpiFrame(cfgBuf, NULL, 4); HAL_Delay(10); /* 3. 自偏移校准 */ cmd = CS5532_CMD_CAL_OFFSET; CS5532_SpiFrame(&cmd, NULL, 1); uint16_t timeout = 10000; while (HAL_GPIO_ReadPin(CS5532_DRDY_GPIO_Port, CS5532_DRDY_Pin) == GPIO_PIN_SET) if (--timeout == 0) break; /* 4. 自增益校准 */ cmd = CS5532_CMD_CAL_GAIN; CS5532_SpiFrame(&cmd, NULL, 1); timeout = 10000; while (HAL_GPIO_ReadPin(CS5532_DRDY_GPIO_Port, CS5532_DRDY_Pin) == GPIO_PIN_SET) if (--timeout == 0) break; }

cfg这个配置字是 24 位,但 CS5532 的配置寄存器和增益、偏移寄存器位数不完全一样,具体哪些位代表通道、哪些位代表增益,要以数据手册的寄存器表为准。我在工程里会把常用配置做成几个默认宏,比如单通道、双通道、增益 1、增益 32,切换时直接改cfgBuf的前三个字节就行。

校准的时序要特别注意:自偏移校准和自增益校准都必须在一个通道下做完,切换通道后要重新校准。如果只校准了通道 1 就切到通道 2 去采数据,通道 2 的偏移和增益误差仍然是出厂值,读出来的数会有明显偏差。

3.3 连续转换:用内部指令还是外部轮询

单次转换适合低频采集,比如每秒几十个点。如果要做连续采样,可以每次循环都发一次转换命令,也可以用 CS5532 的连续转换模式。两种方式在驱动源码里都有接口,区别是连续模式配置完成后只发一次命令,之后每次 DRDY 变低就去读结果,不需要重复发转换命令。

连续模式的读取循环骨架:

/* cs5532.c —— 连续转换模式下的数据读取 */ void CS5532_ContinuousStart(void) { uint8_t cmd = CS5532_CMD_CONTINUOUS; CS5532_SpiFrame(&cmd, NULL, 1); } int32_t CS5532_ContinuousRead(void) { uint8_t tx[4], rx[4]; /* 等 DRDY 变低,表示新数据已经准备好 */ while (HAL_GPIO_ReadPin(CS5532_DRDY_GPIO_Port, CS5532_DRDY_Pin) == GPIO_PIN_SET); tx[0] = CS5532_CMD_READ_DATA; tx[1] = 0; tx[2] = 0; tx[3] = 0; CS5532_SpiFrame(tx, rx, 4); return ((int32_t)rx[1] << 16) | ((uint32_t)rx[2] << 8) | rx[3]; }

连续模式下,DRDY 每次变低代表一个新的转换结果已经放进输出寄存器。要注意的是,读数据命令必须在 DRDY 为低的时候尽快发出;如果你拖到下个周期转换完成,旧数据可能被覆盖。如果应用里主循环很忙,建议把 DRDY 接到 STM32 的外部中断引脚上,中断里只做一个标志位,主循环检测到标志再读数据。

4. 避坑排查:从时序翻车到基准噪声的五条记录

4.1 读回 0xFFFFFF 或 0x000000:SPI 相位和引脚接反

现象:CS5532 初始化正常,校准也做了,但读回来的数据固定是 0xFFFFFF 或者 0x000000,个别时候是 0x7FFFFF。

原因:最常见的是 SPI 模式不对。CS5532 对 SCLK 极性和相位很敏感,如果配置成了 Mode 1 或 Mode 2,主机和从机在时钟上差半个周期,SDO 上的数据被采样到错误的位置,读出来的就是全 1 或全 0。另一个常见原因是 SDI 和 SDO 接反,主机发命令没送到,从机也不会回数据,读回来就是一串无效电平。

解决:先用示波器或者逻辑分析仪抓 SCLK、CS、SDO 三根线。正常时序下,CS 拉低后 SCLK 以低电平为起点,上升沿附近 SDO 上能看到变化。如果 SDO 和 SDI 都确认接对了,就把 SPI 的 CLKPolarity / CLKPhase 挨个试一遍,同时读配置寄存器回读,能正确回读配置值的组合就是对的。回读配置比读转换结果更容易定位问题。

4.2 校准命令发完后一直等不到 DRDY

现象:程序停在while (HAL_GPIO_ReadPin(...) == GPIO_PIN_SET)里出不来,或者偶尔能出来但数据明显异常。

原因:校准命令没被正确接收。有些版本的驱动为了省事,校准命令和普通转换命令共用一个片选窗口,但 CS5532 的校准过程需要一定转换周期,如果片选拉高得太早,校准命令还没被完整锁存。另一个原因是配置寄存器里的增益和字率组合不合法,校准引擎没启动。

解决:每次发校准命令之前,先做一次完整复位,延时 20ms 再写配置。发给校准命令后,片选保持低电平到 DRDY 变低,不要中间拉高。如果还是卡死,把 DRDY 轮的等待逻辑改成带超时版本,并把超时值打印出来,对照数据手册里的校准时间估算是否合理。我一般把超时设在 500ms,正常校准时间在几十毫秒级别。

4.3 连续读数偶发错位:CS 被分割成多帧

现象:数据大部分时间是对的,但连续读几百个点后,偶尔出现一个值突然跳变到接近满量程,然后再恢复正常。用逻辑分析仪看,波形没有明显毛刺。

原因:读转换结果的 CS 窗口被分割了。比如先用HAL_SPI_Transmit发读命令,再调用HAL_SPI_Receive读数据,两次调用之间 CS 被拉高了,CS5532 命令状态机复位,第二次读到的 3 个字节不是同一结果的连续输出。还有可能是底层函数里 CS 建立/保持延时太短,高速下时序紊乱。

解决:读数据命令和结果读取必须放在同一个 CS 低电平窗口里。用本文上面写的CS5532_SpiFrame(tx, rx, 4)一次发完 4 个字节,而不是分开两次调用。如果不想改底层,也可以在两次 SPI 调用之间手动保持 CS 为低,但代码可读性会变差。

4.4 零点漂移到几百微伏:差分输入缺共模回路

现象:把 IN+ 和 IN- 短接到地,读出来的零点不是 0,而是稳定漂在几百微伏甚至几毫伏。改变供电电压时,零点也跟着变。

原因:CS5532 的差分输入前端是开关电容网络,需要输入引脚对地有一条直流通路。如果传感器或者前级运放只输出差分信号而没有共模回流路径,开关电容会把电荷泵到输入节点,零点就漂了。

解决:检查 IN+ 和 IN- 对 AGND 的直流阻抗。最稳妥的做法是把 IN- 直接接到 AGND,或者通过一个 10kΩ 电阻到 AGND。如果前端是仪用放大器,确认参考端不是悬空的。零点漂移问题在软件里能用偏移校准压掉一部分,但硬件上没有共模回路时,校准只能管住当前温度,温度一变又漂回来。

4.5 最后两位一直跳:基准和高频噪声

现象:输入信号不变,功耗也正常,但读数的最后一位或者最后两位来回跳,滤波后仍有周期性噪声。

原因:REF+ 引脚上的参考电压噪声直接进入转换结果,24 位 ADC 对参考电源的噪声几乎没有抑制能力。数字地和高频开关地离 REF 太近时,基准会被污染。

解决:检查 REF+ 到 AVDD 之间的去耦电容,10uF 和 0.1uF 一个都不能少。如果板子上有 DC-DC,CS5532 的 AVDD 和基准最好不要直接从 DC-DC 输出取电,用 LDO 单独供电。最后一级可以做一个均值滤波,在驱动里对连续采样的 N 个值做滑窗平均,通常 N 取 4 到 16 就能把最后两位明显稳住,但要保留一个开关,让需要高响应速度的应用可以关掉滤波。

5. 进阶验证:零点校准、多通道切换与采数稳定性检查

5.1 用系统校准把增益误差收回来

CS5532 的自偏移和自增益校准能校准芯片内部误差,但不能校准外部传感器和前端电路的误差。做称重应用时,我会在硬件装好后跑一次系统校准:把传感器空载时的输出作为零点,把标准砝码加载后的输出作为满量程,然后把这两个点换算成偏移和增益补偿系数。这个系数放在应用层里,不写在驱动里。驱动只负责把原始 24 位数据稳定读回来,业务层再做线性修正。

5.2 双通道切换的第一拍要丢

CS5532 有两个模拟输入通道,切换通道后,前端开关电容网络需要时间重新建立。就算驱动里写了CS5532_SetChannel(ch),切换后立刻发转换命令也会读到上一通道的残留电荷。我的习惯是切换后先做一次空读,把这一拍丢掉,再正式启动下一次转换。多通道轮流采样时,每个通道的第一次结果都不参与最终平均。

5.3 验收:用短接输入量和精密直流源量化性能

拿到驱动后先不要急着接传感器,用一个小工具验证驱动到底成不成熟。把 IN+ 和 IN- 短接到 AGND,连续采集 128 个点,计算最大值减最小值,这个差值应该远小于满量程的千分之一。再找一个精密直流源或者三节干电池串电阻,给一个稳定的差分电压,对比驱动计算出来的电压值和万用表读数,误差应该在增益和基准精度范围内。下面是我平时验收用的表:

测试项做法期望结果
零点稳定性输入短接,采 128 点峰峰值小于满量程 0.01%
增益一致性输入 1/4、1/2 满量程非线性误差在标称范围内
通道一致性双通道同时接同一输入两通道读数差小于 2 LSB
连续长时间采 10 分钟,看是否有偶发跳变无周期跳变,噪声包络稳定

最后一次因为我偷懒没做短接测试,直接把称重传感器接上去,结果零点漂了整整一个晚上,后来才发现是板子上一颗 0.1uF 基准去耦电容虚焊,白白排查了两天。从那以后我每做一套 CS5532 驱动,都会强制走一遍输入短接和 10 分钟连续采集,通过才允许接真实传感器。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询