☰
STM32驱动24位ADC CS5532:称重数据跳动问题的完整解决方案
2026/10/15 16:55:53 网站建设 项目流程

简介:这是一套面向嵌入式开发者的STM32驱动24位高精度ADC芯片CS5532的完整程序代码,适用于工业测量、医疗设备、音频采集等需要高精度模数转换的场景。代码已实际调试通过,可直接移植使用,并配有大量注释,便于理解SPI通信时序、寄存器配置与数据采集处理流程,对刚接触高精度ADC的开发者尤为友好。资源包共243个文件,约5.17MB,以h头文件、c源文件为主,辅以工程编译产生的o、d、crf等中间文件,以及sct分散加载脚本、uvproj与uvopt工程文件、hex烧录文件和pdf说明文档,覆盖从驱动层到工程配置的完整结构。目前已有3045人学习下载,说明其在同类资源中具有较高参考价值。读者可从中获得CS5532的IO模拟时序、初始化与读写函数、主程序调用示例及调试排错思路,省去从零搭建驱动的时间,快速完成高精度采集功能的验证与集成。

1. 24位ADC CS5532在STM32上的落地:为什么你的称重数据总在跳

如果你正在做电子秤、压力变送器、精密配料系统,大概率绕不开一个名字:CS5532。这颗24位ΔΣ模数转换器在工业称重圈子里几乎是“标配”,但真正把它在STM32上跑稳的人不多。我见过太多项目,硬件焊好了,程序也能读出数,可数据就是跳得没法用——末位几位永远在抖,标定完零点漂移,换个温度就偏。问题往往不在芯片本身,而在于CS5532的配置逻辑和STM32的SPI时序没对齐。这篇笔记就围绕“24位高精度ADC CS5532在STM32上的程序代码”这个主题,把已调试可用的方案拆开讲清楚:从寄存器配置、SPI读写时序、数据滤波到标定流程,每一步都给出可复现的代码和参数说明。适合正在选型称重方案的嵌入式工程师,也适合手里已经有CS5532板子但数据不稳的开发者。

2. CS5532的寄存器地图与STM32 SPI底层驱动

2.1 先搞清楚CS5532的通信协议不是标准SPI

很多人第一反应是“CS5532是SPI接口”,然后直接拿STM32的硬件SPI去怼,结果读出来全是0xFF或者0x00。CS5532的串行接口更接近SPI模式0,但它有一个关键区别:命令字节和数据字节之间需要插入一个“同步”周期,而且它的SDI和SDO是分开的,但时钟极性要求SCLK空闲为低,数据在SCLK上升沿被采样。STM32的SPI配置成CPOL=0、CPHA=1或者CPOL=1、CPHA=0都不完全对。我一般直接用GPIO模拟SPI,时序完全可控,反而比硬件SPI稳定。

先看CS5532的命令字节格式。一个命令字节由两部分组成:高3位是命令类型,低5位是寄存器地址或通道号。比如写配置寄存器是0x03,读数据是0x0E,复位是0xFE。注意,CS5532的寄存器是24位的,读写都要按24位操作。

// cs5532_gpio.h - GPIO模拟SPI引脚定义 #define CS5532_SCLK_PORT GPIOB #define CS5532_SCLK_PIN GPIO_PIN_13 #define CS5532_SDI_PORT GPIOB #define CS5532_SDI_PIN GPIO_PIN_15 #define CS5532_SDO_PORT GPIOB #define CS5532_SDO_PIN GPIO_PIN_14 #define CS5532_CS_PORT GPIOB #define CS5532_CS_PIN GPIO_PIN_12 // 微秒级延时,STM32F103主频72MHz时约1us #define CS5532_DELAY() do { for(uint8_t i=0;i<8;i++) __NOP(); } while(0) static void cs5532_sclk_high(void) { HAL_GPIO_WritePin(CS5532_SCLK_PORT, CS5532_SCLK_PIN, GPIO_PIN_SET); } static void cs5532_sclk_low(void) { HAL_GPIO_WritePin(CS5532_SCLK_PORT, CS5532_SCLK_PIN, GPIO_PIN_RESET); } static void cs5532_sdi_high(void) { HAL_GPIO_WritePin(CS5532_SDI_PORT, CS5532_SDI_PIN, GPIO_PIN_SET); } static void cs5532_sdi_low(void) { HAL_GPIO_WritePin(CS5532_SDI_PORT, CS5532_SDI_PIN, GPIO_PIN_RESET); } static uint8_t cs5532_sdo_read(void) { return HAL_GPIO_ReadPin(CS5532_SDO_PORT, CS5532_SDO_PIN); }

这段代码把SPI的四个信号线全部用GPIO控制,SCLK空闲低电平,SDI在SCLK上升沿前建立,SDO在SCLK下降沿后读取。CS5532_DELAY的延时长度需要根据实际主频调整,原则是保证SCLK周期不低于1us,CS5532最高支持2MHz的SCLK,但GPIO模拟时建议控制在500kHz以下,避免时序余量不足。

2.2 复位与配置寄存器的写入顺序

CS5532上电后必须执行一次复位序列,否则内部状态机可能卡在未知状态。复位方法是:拉低CS,发送0xFE命令字节,然后发送0xFF、0xFE、0xFF三个字节,最后拉高CS。这个序列不是随便写的,0xFE是复位命令,后面三个字节是同步填充,CS5532只认这个固定模式。

// cs5532.c - 复位与寄存器写函数 void cs5532_reset(void) { CS5532_CS_LOW(); cs5532_write_byte(0xFE); // 复位命令 cs5532_write_byte(0xFF); // 同步字节1 cs5532_write_byte(0xFE); // 同步字节2 cs5532_write_byte(0xFF); // 同步字节3 CS5532_CS_HIGH(); HAL_Delay(10); // 等待内部振荡器稳定 } // 写24位寄存器,reg_addr为寄存器地址,data为24位数据 void cs5532_write_reg(uint8_t reg_addr, uint32_t data) { uint8_t cmd = 0x03 | (reg_addr << 3); // 写命令 + 寄存器地址 CS5532_CS_LOW(); cs5532_write_byte(cmd); cs5532_write_byte((data >> 16) & 0xFF); // 高8位 cs5532_write_byte((data >> 8) & 0xFF); // 中8位 cs5532_write_byte(data & 0xFF); // 低8位 CS5532_CS_HIGH(); CS5532_DELAY(); }

这里有个容易翻车的点:命令字节的构成。0x03是写操作的基础值,reg_addr左移3位后与0x03做或运算。比如配置寄存器0的地址是0x00,命令就是0x03;配置寄存器1的地址是0x01,命令就是0x0B。很多人直接把寄存器地址当命令发,结果写进去的全是错位数据。另外,CS5532的寄存器是24位,但实际有效位可能只有16位或20位,高位补0即可,不要试图写32位数据。

2.3 配置寄存器的关键位域怎么设

CS5532有两个主要配置寄存器:配置寄存器0和配置寄存器1。配置寄存器0控制通道选择、增益、字速率等;配置寄存器1控制校准模式、滤波器类型等。对于称重应用,我一般用以下配置:

  • 通道选择:AIN1+和AIN2-作为差分输入,对应配置寄存器0的bit 23-21设为0x01
  • 增益:64倍或128倍,对应bit 20-17,称重传感器输出通常只有几mV,需要高增益
  • 字速率:选择15Hz或30Hz,对应bit 16-13,速率越低噪声越小
  • 滤波器:sinc3滤波器,对应配置寄存器1的bit 15
// 初始化配置,适用于称重传感器差分输入 void cs5532_init(void) { cs5532_reset(); // 配置寄存器0:通道AIN1+ AIN2-,增益64,字速率15Hz // bit23-21=001, bit20-17=0110(增益64), bit16-13=0010(15Hz) uint32_t cfg0 = (0x01 << 21) | (0x06 << 17) | (0x02 << 13); cs5532_write_reg(0x00, cfg0); // 配置寄存器1:sinc3滤波器,单次转换模式关闭 uint32_t cfg1 = (0x01 << 15); cs5532_write_reg(0x01, cfg1); HAL_Delay(50); // 等待配置生效 }

增益和字速率的组合需要根据实际传感器灵敏度调整。比如传感器灵敏度是2mV/V,激励电压5V,满量程输出10mV,CS5532的参考电压如果是5V,那么满量程输入范围是±5V/64≈±78mV,10mV信号只用了约1/8的量程,浪费了分辨率。这时候可以把增益提到128,满量程范围变成±39mV,信号占比提高到1/4。但增益越高,噪声也越大,需要权衡。

3. 单次转换与连续转换的代码实现

3.1 单次转换模式的完整流程

单次转换模式适合低功耗场景,每次需要数据时触发一次转换,转换完成后CS5532会拉低SDO或者通过状态寄存器标志位通知。我一般用查询方式,因为STM32的中断资源要留给其他任务。

// 单次转换,返回24位有符号数据 int32_t cs5532_read_single(void) { uint8_t cmd = 0x0E; // 读数据命令 uint32_t raw = 0; int32_t result; // 发送单次转换命令 CS5532_CS_LOW(); cs5532_write_byte(0x00); // 单次转换命令 CS5532_CS_HIGH(); // 等待转换完成,查询SDO引脚 while (cs5532_sdo_read() == 1) { // 超时处理,避免死循环 static uint32_t timeout = 0; if (++timeout > 100000) { timeout = 0; return 0x80000000; // 错误标志 } } // 读取24位数据 CS5532_CS_LOW(); cs5532_write_byte(cmd); raw |= (uint32_t)cs5532_read_byte() << 16; raw |= (uint32_t)cs5532_read_byte() << 8; raw |= (uint32_t)cs5532_read_byte(); CS5532_CS_HIGH(); // 24位有符号数扩展为32位 if (raw & 0x800000) { result = (int32_t)(raw | 0xFF000000); } else { result = (int32_t)raw; } return result; }

这段代码的关键在于等待转换完成的逻辑。CS5532在转换期间SDO会保持高电平,转换完成后拉低。但注意,SDO拉低后需要尽快读取数据,否则下一次转换开始时会覆盖。超时计数器的阈值需要根据字速率调整,15Hz字速率下转换时间约66ms,100000次循环在72MHz主频下约几十毫秒,基本够用。

3.2 连续转换模式与数据就绪中断

连续转换模式适合高速采样场景,CS5532会以设定的字速率自动转换,每次转换完成后SDO会输出一个下降沿。STM32可以用外部中断捕获这个下降沿,然后在中断服务函数里读取数据。

// 连续转换模式初始化 void cs5532_start_continuous(void) { // 配置寄存器0的bit 23-21设为001,bit 16-13设为0010 // 同时将配置寄存器1的bit 15设为0,启用连续转换 uint32_t cfg0 = (0x01 << 21) | (0x06 << 17) | (0x02 << 13); cs5532_write_reg(0x00, cfg0); uint32_t cfg1 = 0x000000; // 连续转换,sinc3滤波器 cs5532_write_reg(0x01, cfg1); // 发送连续转换命令 CS5532_CS_LOW(); cs5532_write_byte(0x00); // 连续转换命令 CS5532_CS_HIGH(); } // 外部中断服务函数,连接CS5532的SDO引脚 void EXTI15_10_IRQHandler(void) { if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_14) != RESET) { __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_14); // 读取数据并存入全局缓冲区 g_adc_raw = cs5532_read_data(); g_adc_ready = 1; } }

连续转换模式下,SDO的下降沿频率就是字速率。15Hz字速率下每66ms中断一次,中断服务函数里只做数据读取,滤波和标定放到主循环。注意,中断服务函数里不能做浮点运算或耗时操作,否则会丢数据。

3.3 读数据时的时序细节

读数据命令是0x0E,发送后CS5532会在接下来的24个SCLK周期内输出数据,高位在前。这里有个坑:读数据命令发送后,第一个SCLK上升沿输出的是数据的最高位,但很多人在发送完命令后多等了一个周期,导致数据整体左移一位,读出来的值永远是实际值的两倍。

// 读24位数据,严格按CS5532时序 uint32_t cs5532_read_data(void) { uint32_t data = 0; CS5532_CS_LOW(); cs5532_write_byte(0x0E); // 读数据命令 // 命令发送后立即读取,不要额外延时 for (int8_t i = 23; i >= 0; i--) { cs5532_sclk_high(); CS5532_DELAY(); if (cs5532_sdo_read()) { data |= (1UL << i); } cs5532_sclk_low(); CS5532_DELAY(); } CS5532_CS_HIGH(); return data; }

这段代码的循环是从23到0,每次先拉高SCLK,然后读SDO,再拉低SCLK。注意SCLK拉高后需要短暂延时再读SDO,因为CS5532的输出建立时间约100ns,GPIO模拟时这个延时就是CS5532_DELAY。如果读出来的数据总是偏大或偏小,先检查这个循环的起始位对不对。

4. 避坑与排查:CS5532调试中最容易翻车的5个点

4.1 现象:读出的数据永远是0xFFFFFF或0x000000

原因:SPI时序极性错误,或者CS片选没有正确拉低。CS5532要求CS在整個命令和数据传输期间保持低电平,很多人只在发送命令时拉低CS,读数据时CS已经拉高了,导致SDO处于高阻态,读回来全是1。

解决:用示波器抓CS、SCLK、SDI、SDO四根线,确认CS在命令字节和数据字节之间没有跳变。如果CS有跳变,检查代码里CS5532_CS_LOW()和CS5532_CS_HIGH()的配对。

4.2 现象:数据末位跳动超过10个码值

原因:参考电压不稳定或者模拟电源纹波太大。CS5532的分辨率是24位,但有效位数取决于参考电压的噪声。如果参考电压用的是STM32的3.3V,纹波可能超过10mV,24位ADC的1LSB只有3.3V/2^24≈0.2uV,10mV纹波相当于50000个码值的跳动。

解决:参考电压必须用低噪声LDO单独供电,比如REF5025或ADR4525,输出噪声在10uVpp以下。模拟电源和数字电源用磁珠隔离,模拟地单点接地。

4.3 现象:标定后零点漂移,每隔几分钟偏几十个码值

原因:CS5532内部有斩波稳定放大器,但外部输入端的偏置电流会流过传感器内阻产生漂移。称重传感器的桥路电阻通常是350Ω或1000Ω,偏置电流1nA时产生的漂移是350nV,相当于几个码值。

解决:在CS5532的输入端加RC低通滤波,截止频率设为10Hz以下。同时启用CS5532的内部斩波模式,配置寄存器1的bit 14设为1。

4.4 现象:连续转换模式下数据更新频率不对

原因:字速率配置错误。CS5532的字速率由配置寄存器0的bit 16-13决定,但很多人忽略了bit 12-11的滤波器类型选择。sinc3滤波器的建立时间是3个转换周期,如果字速率设为15Hz,实际数据更新率只有5Hz。

解决:查数据手册的字速率表,确认bit 16-13的值。15Hz对应0010,30Hz对应0011,60Hz对应0100。如果要用sinc3滤波器,第一个数据需要丢弃,从第二个开始才是有效数据。

4.5 现象:STM32复位后CS5532不工作,重新上电才行

原因:STM32复位时GPIO处于浮空输入状态,CS5532的SCLK引脚可能收到干扰脉冲,导致内部状态机锁死。CS5532没有上电复位电路,必须由MCU发送复位序列。

解决:在STM32初始化代码的最开始,先配置GPIO为推挽输出,拉高CS,拉低SCLK,然后执行cs5532_reset()。不要在GPIO初始化之前做任何其他外设初始化,避免SCLK引脚被其他功能复用。

5. 从原始码值到稳定重量:标定与滤波的工程技巧

5.1 两点标定法的代码实现

CS5532输出的是24位有符号数,需要转换成实际重量。最常用的是两点标定:空载时记录零点码值,加载标准砝码时记录满量程码值,然后线性插值。

// 标定参数结构体 typedef struct { int32_t zero_raw; // 空载码值 int32_t full_raw; // 满量程码值 float full_weight; // 满量程重量,单位g float scale; // 比例系数 } calib_t; calib_t g_calib; // 计算比例系数 void calib_compute(void) { g_calib.scale = g_calib.full_weight / (float)(g_calib.full_raw - g_calib.zero_raw); } // 将原始码值转换为重量 float raw_to_weight(int32_t raw) { return (float)(raw - g_calib.zero_raw) * g_calib.scale; }

标定时要注意:空载码值和满量程码值必须取多次平均,单次采样受噪声影响太大。我一般取100次采样,去掉最大和最小的10个,剩下的80个求平均。满量程砝码的重量要接近传感器量程的80%,不要用满量程,避免非线性误差。

5.2 滑动平均与中值滤波的组合

CS5532的原始数据即使硬件做得再好,也会有随机噪声。我一般用“中值滤波+滑动平均”的组合:先取5个连续采样,去掉最大和最小,剩下3个求平均;然后再对连续10个这样的平均值做滑动平均。这样既能抑制脉冲干扰,又能平滑随机噪声。

#define MEDIAN_SIZE 5 #define MOVING_SIZE 10 int32_t median_filter(int32_t new_val) { static int32_t buf[MEDIAN_SIZE] = {0}; static uint8_t idx = 0; int32_t temp[MEDIAN_SIZE]; buf[idx] = new_val; idx = (idx + 1) % MEDIAN_SIZE; // 复制到临时数组排序 for (uint8_t i = 0; i < MEDIAN_SIZE; i++) { temp[i] = buf[i]; } // 冒泡排序 for (uint8_t i = 0; i < MEDIAN_SIZE - 1; i++) { for (uint8_t j = 0; j < MEDIAN_SIZE - 1 - i; j++) { if (temp[j] > temp[j+1]) { int32_t t = temp[j]; temp[j] = temp[j+1]; temp[j+1] = t; } } } // 取中间值 return temp[MEDIAN_SIZE / 2]; } float moving_average(int32_t new_val) { static int32_t buf[MOVING_SIZE] = {0}; static uint8_t idx = 0; static int32_t sum = 0; sum -= buf[idx]; buf[idx] = new_val; sum += new_val; idx = (idx + 1) % MOVING_SIZE; return (float)sum / MOVING_SIZE; }

中值滤波的窗口大小取5比较合适,太大响应慢,太小滤不掉脉冲。滑动平均的窗口取10,对应15Hz字速率下约0.67秒的响应时间,对于静态称重足够。如果是动态称重,窗口要缩小到3-5。

5.3 温度漂移的软件补偿

CS5532本身的温漂很小,但称重传感器的温漂可能达到0.02%FS/°C。如果应用环境温度变化超过10°C,必须做温度补偿。最简单的方法是在不同温度下记录零点码值,拟合一条温度-零点曲线。

// 温度补偿系数,通过高低温试验获得 #define TEMP_COMP_COEFF -0.5f // 码值/°C float g_current_temp = 25.0f; // 当前温度,由外部温度传感器读取 int32_t compensate_zero(int32_t raw) { float temp_delta = g_current_temp - 25.0f; int32_t comp = (int32_t)(temp_delta * TEMP_COMP_COEFF); return raw - comp; }

温度补偿系数需要实际测试:把传感器放在恒温箱里,从-10°C升到50°C,每隔10°C记录一次空载码值,然后线性拟合。注意,补偿只针对零点,满量程的温漂通常比零点小一个数量级,可以忽略。

5.4 用STM32的DMA减轻CPU负担

如果CS5532配置为连续转换模式,每次数据就绪都要进中断读24位数据,CPU开销不小。可以用STM32的SPI DMA功能,把CS5532的SDO引脚接到SPI的MISO上,配置DMA在SPI接收时自动搬运数据。但CS5532的时序和标准SPI不完全兼容,DMA方式需要额外处理命令字节的发送时机。我一般只在字速率高于100Hz时才用DMA,15Hz到60Hz用GPIO模拟完全够用。

5.5 一个实用的调试习惯

每次修改CS5532的配置寄存器后,不要直接跑主程序,先写一个简单的测试函数:读一次配置寄存器0和配置寄存器1的值,通过串口打印出来。CS5532的读寄存器命令是0x08 | (reg_addr << 3),读回来的24位数据应该和写入的一致。如果读回来不对,说明写时序有问题,先解决写再解决读。这个习惯帮我省了很多抓波形的时间。

希望帮到你。

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

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

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

立即咨询