做嵌入式这几年,接触过不少ADC,从8位的逐次逼近型到24位的Σ-Δ型都摸过一轮。ADS1220是我在工业采集项目里真正定下来的一颗高精度ADC——它属于24位Δ-Σ架构,内置可编程增益放大器(PGA),内部2.048V基准电压源,还能直接读芯片内部温度,一颗芯片同时解决弱信号放大和精密采样两件事。这篇文章就把从SPI时序、寄存器配置、驱动分层设计,到滤波算法、问题排查的完整驱动开发过程拆开讲一遍,给正在调这颗芯片,或者准备入坑高精度ADC驱动开发的朋友一个可以照着抄的参考。
我也踩过不少坑,比如数据读出来全是满量程、DRDY死活不拉低、切换通道后前几十个点漂得离谱,这些问题我都会结合实际的排查思路说清楚。你不需要很深的数模混合背景,只要会用STM32的HAL库、看得懂C语言,跟着这篇文章走一遍,基本就能把ADS1220跑起来。
1. 项目背景与驱动设计思路
先搞明白这颗芯片为什么值得专门写一篇驱动开发实战。ADS1220不是那种“上电就出数据”的简单ADC,它给了你大量可配置空间:输入多路复用器支持2路差分或4路单端,PGA增益可以从1倍调到128倍,数据速率从5SPS一直到2000SPS可调,内置基准还能选择内部还是外部。这些特性对做电桥称重、热电偶测温、铂电阻采集、电池电压监测这类场景非常有用,因为信号往往很微弱,需要高增益和高分辨率才能把有效信号从噪声里捞出来。
但灵活也就意味着复杂。所有功能都要通过SPI往四个配置寄存器里写值,配置错了数据就是错的,而且错得五花八门。你要是不理解这颗芯片的“脾气”,很容易陷入“配置看起来都对,数据就是不对”的僵局。我的经验是:先把驱动分层想清楚,再动手写代码,省得后面来回推翻。
1.1 驱动架构怎么分层:做成积木而不是一坨代码
我见过不少驱动代码是“一份文件走天下”,SPI初始化、寄存器配置、数据读取、滤波、标定全塞在一个C文件里,函数之间互相调用,到处都是全局变量。芯片换一颗,整套代码基本重写。这种写法在你只调自己开发板的时候问题不大,但一旦进到正式项目,要接不同的MCU、要移植、要复用,就会非常痛苦。
我的习惯是把ADS1220驱动分成三层:
- 底层总线层:只负责SPI字节收发、CS引脚拉高拉低、超时等待,不关心ADS1220业务。
- 设备层:基于底层总线实现读写寄存器、发送命令、读取转换结果、配置初始化函数,这一层只依赖底层SPI接口,不依赖具体MCU型号。
- 应用层:负责数据处理,比如滤波、标度变换、阈值判断,这部分逻辑写在应用代码里,不直接碰硬件寄存器。
这样分完,底层SPI就算从F103换到F407,只需要改底层那一小部分,设备和应用层代码原封不动。后续如果有人说“项目要换成ADS1220的兄弟芯片ADS1225”,你只需要在设备层做适配,应用层完全不用动。这就是分层带来的最大收益,表面上看多写了几个文件,实际省掉的移植和调试时间远超想象。
1.2 选择这颗ADC背后的关键考量
很多人一看到高精度ADC就无脑上24位,但24位不等于“一定测得准”。ADS1220真正值钱的地方在于它把PGA、基准、温度传感器做进了一颗芯片里,省掉了一堆外围料,板子面积小很多,调试复杂度也下降不少。
我在选型时主要对比过几类方案:一是用MCU内部ADC,虽然免费,但12位分辨率在毫伏级信号面前完全不够看,更不用说内部基准精度和噪声水平。二是用外部多位ADC芯片自己做分立PGA,比如运放加16位ADC,这种方案调起来链路上的噪声源太多,每一级都要校准,项目周期很容易失控。三是用ADS1220这类集成度高的Σ-Δ ADC,内部PGA已经把增益和阻抗转换做完了,内部基准也做了精密修调,输出的数字结果就是完全可解释的电压值。
这个选型逻辑本质上是拿“灵活性”换“可靠性和开发效率”。ADS1220提供的配置项已经足够应付绝大多数工业采集场景,不至于因为自由度太大而无从下手。
2. 核心细节解析与实操要点
2.1 SPI时序和接线是第一个坑:CPOL/CPHA必须搞清楚
ADS1220用的是标准四线SPI:CS、SCLK、DIN(MOSI)、DOUT/DRDY(MISO)。注意这个DOUT/DRDY是复用引脚——芯片在工作时,如果CS为高,这个引脚输出的是DRDY状态信号,表示转换数据是否就绪;当CS拉低后,它切换为DOUT数据输出,用于把转换结果移出来。这个细节特别容易忽略,很多人用逻辑分析仪看波形,发现MISO上一直有电平变化,以为数据已经过来了,实际上人家在送DRDY状态,压根不是转换数据。
SPI模式上,ADS1220要求的是极性CPOL=0、相位CPHA=1,也就是SPI Mode 1。不少人在STM32的HAL库里习惯性配置成Mode 0(CPOL=0, CPHA=0),结果发现读回来的数据错位、丢字节、或者芯片根本不响应。为什么?因为Mode 0是在SCLK上升沿采样,而ADS1220要求在SCLK下降沿采样,位序正好反了半拍。
接线方面,我用的是一组普通GPIO模拟SPI来调试,跑通后再切到硬件SPI。这样排错最直接。如果你的MCU硬件SPI引脚正好不够,完全可以用GPIO模拟,因为ADS1220最高速率也就几兆赫兹,GPIO模拟完全够用。我实测用STM32F103的普通GPIO模拟SPI,时钟在1MHz左右跑得非常稳。
| ADS1220引脚 | 连接目标 | 说明 |
|---|---|---|
| CS | MCU GPIO | 片选,低有效,通信期间必须保持低电平 |
| SCLK | MCU SPI SCK | 空闲默认低电平,下降沿采样 |
| DIN | MCU MOSI | 命令和寄存器数据写入 |
| DOUT/DRDY | MCU MISO | CS高时输出DRDY,CS低时输出转换数据 |
| DVDD/AVDD | 电源 | 数字/模拟电源建议分开走,最后单点连接 |
| REFP/REFN | 基准引脚 | 用内部基准时这两个引脚悬空或接去耦电容 |
2.2 寄存器体系与命令格式:从WREG到RDATA
ADS1220的寄存器配置全是8位宽度,一共四个配置寄存器:地址从00h到03h,写入时用WREG命令把多个寄存器一次性写入,节省SPI通信次数。
WREG命令的格式是:第一个字节为0x40 |(寄存器起始地址 << 2)|(寄存器个数 - 1),之后紧跟寄存器数据字节。比如从地址00h开始写4个寄存器,第一个字节就是0x40 | (0x00 << 2) | 0x03,算出来是0x43,后面再跟4个字节的配置值。这个格式记不住也没关系,写代码时做成函数,参数传入地址和数据数组,命令字节由函数内部拼装,不容易出错。
读取转换数据时,最常用的命令是RDATA,也就是0x1C。发送这个命令后,从MISO线连续读出24位转换值。还有一个更常见的做法是:等DOUT/DRDY拉低表示转换完成,然后拉低CS,直接连续读3个字节,甚至不需要发RDATA命令。两种方式我都试过,稳定性差不多,区别在于前者语义更明确,适合用状态机管理。
配置寄存器里,我重点说CONFIG0和CONFIG2,其他两个寄存器按需求配置就行。CONFIG0里最关键的几个字段是:PGA使能位、输入多路选择配置(决定是差分还是单端)、基准电压选择位(选内部2.048V还是外部基准)、转换模式位(单次还是连续)。很多工程师写完配置发现数据不对,回去一查,十有八九是输入多路选择配错了,或者基准选择和你实际接的电压源对不上。
CONFIG2主要是增益位GAIN[2:0],从000到111对应1倍到128倍。还有一个容易被忽略的HWIM位,它是高性能模式的开关,打开后噪声更低但功耗略高。工业现场对噪声敏感,我的习惯是直接打开。
2.3 采样速率、增益与噪声之间的取舍
ADS1220的数据速率通过CONFIG1的DR[3:0]设置,从最低5SPS到最高2000SPS。这里有个所有Σ-Δ ADC都存在的铁律:采样速率越高,噪声越大,有效分辨率越低。你在数据手册能看到不同数据速率下的RMS噪声表,实测效果也和手册基本一致。
我的经验是:如果是电桥称重、温度采集这类慢变信号,把速率设在20SPS就够了,这个速率下我能做到接近16位的有效无噪声分辨率,峰峰值抖动很小,后续滤波压力也不大。如果要做动态响应要求高的电流波形录波,至少要200SPS以上,这时就别追求24位全精度,能稳定读出17到18位有效数据就很好了。
增益的选择同样有讲究。PGA增益并不是越高越好,高增益会把传感器的有用信号放大,也会把传感器和输入链路上的噪声一起放大。而且ADS1220内部PGA工作时对输入共模电压范围有约束,信号输入范围必须落在允许的窗口内,直接对地测量的传感器信号如果接近地电平,反而需要谨慎配置。
我在实际项目中调试称重电桥时,传感器输出满量程大约5mV左右,所以直接把增益配到128倍,加上2.048V内部基准,满量程对应的输入范围正好合适。如果你的信号是±2V级别,那增益配1倍就够了,强行拉到128倍只会让ADC瞬间饱和到0x7FFFFF。
3. 驱动代码实现:从底层到应用层完整流程
3.1 底层SPI读写函数与超时机制
先写最底层的字节收发函数。我用的是STM32 HAL库的SPI接口,定义如下:
static uint8_t ads1220_spi_rw(uint8_t tx_byte) { uint8_t rx_byte = 0; if (HAL_SPI_TransmitReceive(&hspi2, &tx_byte, &rx_byte, 1, 100) != HAL_OK) { return 0xFF; } return rx_byte; }底层通信本身很简单,难点在于什么时候发、什么时候收、什么时候拉CS。ADS1220的CS控制必须手动管理,不能依赖HAL库的自动片选。我做了一个组函数,把CS拉低、连续收发若干字节、CS拉高组装在一起,对外暴露“读N字节”和“写N字节”两个接口。
static void ads1220_cs_select(void) { HAL_GPIO_WritePin(ADS1220_CS_GPIO_Port, ADS1220_CS_Pin, GPIO_PIN_RESET); } static void ads1220_cs_release(void) { HAL_GPIO_WritePin(ADS1220_CS_GPIO_Port, ADS1220_CS_Pin, GPIO_PIN_SET); }读转换数据的函数这样写:
static int32_t ads1220_read_data_raw(void) { uint8_t buf[3] = {0}; uint32_t raw = 0; int32_t result = 0; ads1220_cs_select(); ads1220_spi_rw(0x1C); // RDATA命令 buf[0] = ads1220_spi_rw(0xFF); buf[1] = ads1220_spi_rw(0xFF); buf[2] = ads1220_spi_rw(0xFF); ads1220_cs_release(); raw = ((uint32_t)buf[0] << 16) | ((uint32_t)buf[1] << 8) | (uint32_t)buf[2]; // 24位二进制补码转有符号数 if (raw & 0x800000U) { result = (int32_t)raw - 0x1000000; } else { result = (int32_t)raw; } return result; }这里有个值得注意的点:ADS1220输出的24位数据是二进制补码格式,最高位是符号位。如果直接把raw当无符号数用,负电压输入会变成接近满量程的大正数,换算电压时必然出错。我最初在测试负压信号时就吃过这个亏,读数一直是0xFFFFFF这样的巨数,后来才意识到要做符号扩展。
我还会在读取时加一个超时保护,用DOUT/DRDY引脚来判断数据是否就绪。当CS为高电平时,DOUT/DRDY从高变低表示新的转换结果已经准备好。如果长时间没有下降沿,就说明芯片没启动转换,或者SPI配置有问题,这时不应该盲目读取数据。
uint8_t ads1220_wait_for_drdy(uint32_t timeout_ms) { while (HAL_GPIO_ReadPin(ADS1220_DRDY_GPIO_Port, ADS1220_DRDY_Pin) != GPIO_PIN_RESET) { if (timeout_ms-- == 0) { return 0; // 超时 } HAL_Delay(1); } return 1; }3.2 寄存器初始化与芯片自检流程
初始化函数把四个配置寄存器一次性写完。下面这个例子对应的是:PGA使能、差分输入AIN0-AIN1、内部2.048V基准、连续转换模式、20SPS采样速率、128倍增益。
#define ADS1220_CONFIG0 0x00 #define ADS1220_CONFIG1 0x01 #define ADS1220_CONFIG2 0x02 #define ADS1220_CONFIG3 0x03 void ads1220_init(void) { uint8_t cfg[4]; // CONFIG0: PGA使能, 差分输入AIN0/AIN1, 内部基准, 连续转换 cfg[0] = 0x02; // CONFIG1: 20SPS cfg[1] = 0x02; // CONFIG2: 增益128倍, 高性能模式 cfg[2] = (0x07 << 5) | (0x01 << 2); // CONFIG3: 默认 cfg[3] = 0x00; ads1220_cs_select(); ads1220_spi_rw(0x43); // WREG从地址00h写4个寄存器 for (int i = 0; i < 4; i++) { ads1220_spi_rw(cfg[i]); } ads1220_cs_release(); }写完配置后强烈建议做一次回读验证。高精度ADC项目里最尴尬的情况就是寄存器没写进去,数据上位机那边已经等了半天,最后发现是SPI通信就不通。回读很简单,用RREG命令把刚才写的寄存器值读回来逐一对比。我一般会在初始化函数里加上这段自检代码,输出一个布尔返回值,上层判断初始化是否成功。
uint8_t ads1220_verify_config(void) { uint8_t readback[4]; ads1220_cs_select(); ads1220_spi_rw(0x20 | (0x00 << 2) | 0x03); // RREG从地址00h读4个寄存器 for (int i = 0; i < 4; i++) { readback[i] = ads1220_spi_rw(0xFF); } ads1220_cs_release(); // 依次对比寄存器,失败就返回0 if (readback[0] != 0x02) return 0; if (readback[1] != 0x02) return 0; if (((readback[2] >> 5) & 0x07) != 0x07) return 0; return 1; }不要小看这段回读代码,它能把“芯片没应答”“配置写错”“SPI引脚接反”三类问题瞬间暴露出来。我的开发习惯是:新板子到手先跑这个回读,确认SPI链路完全正常后,再去做模拟信号标定,能省出一整天的调试时间。
3.3 多通道扫描与滤波算法工程实现
ADS1220的多路复用器支持切换输入通道,但通道切换后ADC输出并不会立刻稳定到新通道的值。PGA和Σ-Δ调制器需要经过几个采样周期才能完成建立。如果切换完立刻读数据,读到的往往还是上一个通道的残留值或过渡值。
工程上的标准做法是:切换通道后先丢弃前面几个无效样本。我实测,20SPS下切换通道后大约需要3到4个样本才能完全稳定,保险起见我直接丢前8个样本,绝不让过渡值进入滤波缓冲区。
多通道切换的核心逻辑:
uint8_t current_channel = 0; void ads1220_set_channel(uint8_t ch) { uint8_t cfg = 0x02; // 根据通道号修改CONFIG0的输入MUX字段 // 单端模式: ch=0对应AIN0, ch=1对应AIN1 cfg |= (ch << 4); ads1220_write_reg(ADS1220_CONFIG0, &cfg, 1); current_channel = ch; }滤波算法这里多说两句。只做滑动平均,遇到尖峰脉冲时会把尖峰散布到多个点上,造成虚假的“平台”数据。只做中值滤波,又不能有效平滑基线噪声。我实际用的是两级滤波:先做3点中值滤波去掉粗大误差,再做8点滑动平均压平随机噪声。这两个滤波器组合起来,既能处理脉冲干扰,又能稳定输出。
#define MA_WINDOW 8 static int32_t ma_buf[MA_WINDOW]; static uint8_t ma_index = 0; int32_t ads1220_median3(int32_t a, int32_t b, int32_t c) { if ((a >= b && a <= c) || (a <= b && a >= c)) return a; if ((b >= a && b <= c) || (b <= a && b >= c)) return b; return c; } int32_t ads1220_process_sample(int32_t raw) { static int32_t hist[3]; int32_t med; int32_t sum = 0; hist[0] = hist[1]; hist[1] = hist[2]; hist[2] = raw; med = ads1220_median3(hist[0], hist[1], hist[2]); ma_buf[ma_index++] = med; if (ma_index >= MA_WINDOW) ma_index = 0; for (int i = 0; i < MA_WINDOW; i++) sum += ma_buf[i]; return sum / MA_WINDOW; }这段代码看起来只是几十行的滤波逻辑,但把尖峰抑制和基线平滑两件事合在一起完成了。如果后续采样速率提高到200SPS以上,滑动平均窗口可以相应缩短,不然会把真实信号动态响应拖慢。比如温度测量可以接受较长窗口,而振动监测必须缩短窗口,这是要根据业务场景调的。
滤波后的原始码最终还要换算成实际物理量。ADS1220满量程输入电压由基准和增益决定,内部基准在2.048V时,满量程输入差分范围约为±2.048V/(增益)。比如增益128倍,输入范围约±16mV,24位分辨率下单LSB对应的电压为(2 × 2.048V / 128) / 2^24,大约2.44nV,这个量级已经远低于板级噪声,工程上不可能达到绝对精度,所以真正决定最终精度的是基准电压精度和系统噪声。
4. 现象、坑与排查技巧实录
4.1 输出始终保持满量程0x7FFFFF
这是我遇到过最多的问题。现象是无论输入怎么变化,读出来的原始值一直稳定在0x7FFFFF上下,看起来像是信号爆炸。排查思路从输入信号链开始:
先用万用表直接测量AINP、AINN引脚的电压,如果传感器输出只有几毫伏,而ADC读数已经满量程,说明增益或者配置可能不对。最常见的情况是:配置写的是单端模式,但实际接线是差分,或者增益配置位写错了,导致过大的有效信号把PGA推到了饱和。还有传感器断路时,输入阻抗极高,PGA输入悬空,读数也可能飘到满量程。给传感器加上下拉电阻,让输入在悬空时有个确定电平,这类问题能少掉一半。
4.2 DRDY不拉低,读取超时
芯片不启动转换或者DRDY信号不正常,往往不是芯片坏了,而是CS管脚控制出了问题。我遇到过两种情况:一是CS引脚没有拉低就发了SPI命令,芯片根本没把命令收进去;二是SPI模式配错,SCLK相位不对,芯片收到的是乱码命令,自然不会正常启动转换。
排查时先用逻辑分析仪看CS和SCLK波形,确认CS低电平持续时间完整覆盖命令发送过程。再看SPI初始化里的时钟极性配置,确认是Mode 1(CPOL=0, CPHA=1)。这两个点检查完,DRDY不工作的概率就很小了。
4.3 切换通道后前N个数据不准
前面提到过,通道切换后ADC输出需要建立时间。如果你是每切换一次通道就读一个值,读回来的数据十有八九是错的。我在项目里采用的方法是:每路信号持续采集几十个周期,用后面稳定的部分做平均,切换后的过渡样本直接丢弃。如果你做的是高速扫描,那就要在硬件上考虑多片ADC并行采样,或者换用带真正同步采样的芯片,单纯靠软件加速会牺牲精度。
4.4 噪声偏大、有效分辨率上不去
很多工程师把板子打回来第一件事就是加滤波电容,其实先要看布局。ADS1220这种高精度ADC对电源和地平面特别敏感,数字信号SCLK、DIN的高频跳变会耦合到模拟电源和基准上,造成测量噪声增大。我的布局经验是:模拟电源和数字电源分开走线,然后在AVDD和DVDD上各放一个0.1uF陶瓷电容再加一个10uF钽电容,基准引脚REFP/REFN之间放0.1uF和1uF的组合。SPI时钟尽量降到1MHz以下,够用就行,别一味追求速度,时钟越快噪声风险越大。
4.5 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读数满量程0x7FFFFF | 增益过高、输入悬空、PGA饱和 | 测AIN引脚电压,降低增益,加下拉电阻 |
| DRDY不拉低 | CS未控制好、SPI模式错误 | 逻辑分析仪看波形,确认CS时序和Mode 1配置 |
| 通道切换后数据跳动 | 未丢弃过渡样本,建立时间不足 | 切换后等待2~3样周期再采集 |
| 噪声偏大 | 电源去耦不足、SPI时钟过高 | 加去耦电容,降低SCLK频率 |
| 读数据符号错误 | 24位补码未做符号扩展 | 检查符号位处理逻辑 |
| 电压换算对不上 | 基准或增益配置与实际不符 | 回读寄存器验证配置,查基准电压来源 |
5. 一些真正值得记住的经验
反复调ADC之后,我最大的体会是:高精度ADC驱动的难点根本不在写代码,而在时序理解和数据链路验证。代码框架是几分钟就能搭出来的东西,最费时间的是排查那种“配置看起来都对、数据就是不对”的玄学问题。所以我把回读寄存器这个步骤看得特别重,每换一颗新ADC,第一件事就是写一个读ID或者读寄存器回读的测试,确认SPI链路是通的,再谈转换精度,别上来就一门心思抄数据手册里的配置模板,容易翻车。
另外,驱动代码里多留一些调试出口,比如把原始码、滤波后码、折算电压值都通过串口打出来。我在调试时会把原始码和滤波值同时输出,一眼就能看出滤波算法有没有压住噪声,而不是等系统联调时再手忙脚乱地加日志。
ADS1220的驱动本身并不复杂,把SPI时序、寄存器、建立时间这三件事处理明白,再配合合理的滤波策略,这套驱动就足够稳定了。后续如果要扩展,可以考虑用定时器触发周期性采样,配合DMA自动搬运数据,把CPU释放出来做更复杂的信号处理。开发高精度采集系统的路还长,先把ADS1220这一关过了,后面不管换什么ADC,思路都是通的。