做嵌入式这几年,有时候真不是MCU内部ADC不够用,而是当信号本身就很小、噪声底又苛刻的时候,内部12位ADC的短板会被放得非常明显。去年做多通道温度采集和称重信号采集模块,我把方案定在TI的ADS1220上,顺手把整套ADC驱动开发流程完整走了一遍。ADS1220是一颗24位Δ-Σ型ADC,带SPI接口、内置PGA和双IDAC恒流源,适合称重、热电偶、RTD测温这类低速但精度要求很高的场景。这篇文章把选型逻辑、硬件设计、寄存器配置、驱动架构和调试验证全盘复盘一遍,给正好要在这颗芯片上做驱动开发的同学一个可以直接抄作业的参考。
1. 选型逻辑:为什么是ADS1220而不是别的ADC
1.1 高精度从哪来:Δ-Σ架构和内部PGA
首先要搞清楚一个概念:24位分辨率不等于你一定能拿到24位有效数据。ADS1220内部是Δ-Σ调制器加数字滤波器,它不像SAR型ADC那样“一次到位”,而是用极高的过采样率反复量化输入信号,再用片内数字滤波器把带内噪声压下去。这个过程有点像拍夜景照片时的多帧合成——单张噪点很大,但叠加足够多张之后,暗部细节就出来了。
这颗芯片的另一个杀手锏是内置PGA,增益支持1、2、4、8、16、32、64、128倍。这个对实际项目太关键了,因为像热电偶输出长期只有几毫伏到几十毫伏,MCU内部ADC直接采基本是在噪声里捞信号。先用PGA把小信号放大到ADC量程的主要区段,再交给24位转换器量化,有效精度提升非常明显。
有人可能会问,既然MCU内部也有12位或者16位ADC,为什么还要外挂?在工业现场,电源纹波、数字开关噪声、参考源漂移都会直接影响ADC性能。MCU受成本限制,内部参考电压精度和温度系数往往一般,而ADS1220这类独立ADC可以自由选择高精度外部基准,布线上也能把模拟电源域隔离开,精度上限完全不是一个级别。
1.2 哪些场景适合上它,哪些场景根本不用它
ADS1220的数据速率范围是20SPS到2000SPS,这决定了它不适合高速采样场景。比如要做音频采样、电机电流环、高速振动分析,那别选它,这些场景需要的是SAR型或流水线型ADC,或者直接上MCU内部的高速ADC。
适合的场景很明确:称重传感器、压力传感器、热电偶测温、PT100/PT1000 RTD测温、电池电压巡检、4-20mA电流环采集。这些应用的特点是信号变化慢、精度要求高、对噪声底特别敏感。ADS1220的电流消耗在几百微安级别,低功耗模式下更适合电池供电的数据记录仪。
这里插一句:如果你的输入源输出阻抗特别大,比如某些电化学传感器,需要关注PGA输入偏置电流。ADS1220的输入结构在启用PGA后是类似仪表放大器的前端,输入阻抗比较高,但也不是无穷大。此时最好在输入引脚前加一个缓冲器,或者选择更高输入阻抗的器件,否则增益误差会大得让你怀疑人生。
2. 硬件设计:高精度ADC驱动开发的一半功夫在板子上
2.1 最小电路:参考电压、电源去耦和输入滤波
很多人拿到ADS1220就开始写代码,结果发现噪声怎么都压不下去,最后查了一圈,问题全在硬件。ADC驱动开发,软件只是最后一步,前端硬件决定了噪声底。
先说参考电压。如果做精密测量,千万别图省事把AVDD当基准用。ADS1220有内部2.048V基准,标称精度尚可,但温漂和长期稳定性跟专用基准芯片比还有差距。我在这个项目里用了外部基准REF5025,输出2.5V,温漂低,噪声极低。基准输出到REFP引脚之间的走线要短,旁边放0.1uF和10uF两级去耦电容。
然后是模拟电源。AVDD和DVDD建议分开走,中间用磁珠隔离。AVDD引脚旁放10uF胆电容加0.1uF陶瓷电容,而且0.1uF要尽量靠近引脚。数字部分的地和模拟部分的地在ADC下方单点汇合,不要大面积覆铜连通。我第一次画板子时图省事,整个地平面直接铺通了,结果数字SPI信号把噪声耦合进模拟地,采样数据的末位跳动非常厉害。
输入滤波也得仔细算。ADS1220的PGA输入端一般要加RC滤波,典型做法是串联电阻加对地电容。串联电阻不能太大,因为输入偏置电流会在电阻上产生压降,形成额外误差。常见的组合是100Ω到1kΩ的串联电阻加0.1uF对地电容。如果你用的是RTD测量模式,还要考虑IDAC恒流源激励时导线电阻造成的误差,这时候最好用四线制接法。
2.2 PCB布局与布线的几个约束
SPI信号线时钟SCLK和数据DIN/DOUT尽量等长,避免走太远。模拟输入信号线和SPI线要隔开,最好用地线隔离,不平行走线。DRDY脚是数据就绪信号,它连接MCU的外部中断或GPIO,这条线也不要太靠近模拟输入。
还有一个坑:ADS1220底部焊盘如果存在,必须焊接并接到模拟地。这个焊盘承担了一部分散热和地回流功能,悬空会导致内部地参考不稳,表现就是采样值漂移。PCB的铺铜连接要牢固,过孔尽量打在焊盘附近。
在打样回来准备调试之前,建议先做一个简单的自检清单:万用表确认AVDD电压和DVDD电压,确认REFP对REFN的电压,确认SPI四根线没有接反,确认CS上拉电阻和DRDY上拉电阻焊接正常。这套检查五分钟就能做完,能省掉半天调试时间。
3. 驱动框架设计:寄存器抽象和SPI通信
3.1 先把SPI时序搞定
ADS1220是标准SPI从设备,支持模式1(CPOL=0,CPHA=1),SCLK空闲拉低,数据在SCLK的下降沿被器件锁存,输出则在上升沿稳定。我的主控平台是STM32系列,直接用HAL库的SPI外设配置,分频后时钟给到1MHz。其实手册允许更高时钟,但1MHz足够稳定,也减少了数字噪声耦合进模拟测量的风险,实测下来很稳。
这里要专门提一句:SPI控制ADC时,CS片选下拉后发送命令字节,注意MCU的SPI外设在传输完成后可能会提前拉高CS。如果CS释放太快,ADS1220可能收不下最后一个字节。很多SPI从设备都有这个敏感点,保险做法是在CS释放前加一个微秒级的延时,或者用GPIO手动控制CS,而不是让SPI外设自动控制。吃不准的情况下,我建议手动控制CS,稳定性优先。
3.2 寄存器读写与数据读取的代码骨架
驱动底层的核心就三件事:写寄存器、读寄存器、读转换结果。代码不复杂,但边界条件一定要想清楚。先看寄存器读写。
#define ADS1220_CMD_RREG 0x10 #define ADS1220_CMD_WREG 0x40 #define ADS1220_CMD_RDATA 0x1F static int ads1220_write_reg(uint8_t reg, uint8_t value) { uint8_t tx[2]; tx[0] = ADS1220_CMD_WREG | (reg << 1); tx[1] = value; spi_cs_low(); spi_delay_us(1); spi_transfer(tx, NULL, 2); spi_delay_us(1); spi_cs_high(); return 0; } static int ads1220_read_reg(uint8_t reg, uint8_t *value) { uint8_t tx[2]; uint8_t rx[2]; tx[0] = ADS1220_CMD_RREG | (reg << 1); tx[1] = 0x00; /* dummy byte,时钟用来把数据移出来 */ spi_cs_low(); spi_transfer(tx, rx, 2); spi_cs_high(); *value = rx[1]; return 0; }命令字节最低位是A0,用来选择寄存器,而命令本身的高四位区分读寄存器、写寄存器和读数据。位定义要在具体实现时对着手册的Table逐项确认,不同软件库的宏命名方式可能不一样,但操作逻辑是一致的。
读取24位转换结果的代码更考验细节。ADS1220输出的数据是24位二进制补码,最高位是符号位。如果直接拼成一个无符号int,后续换算电压时正负号会出问题。我的读写函数如下:
static int ads1220_read_data(int32_t *raw) { uint8_t tx[3]; uint8_t rx[3]; int32_t val; tx[0] = ADS1220_CMD_RDATA; tx[1] = 0x00; tx[2] = 0x00; spi_cs_low(); spi_transfer(tx, rx, 3); spi_cs_high(); val = ((int32_t)rx[0] << 16) | ((int32_t)rx[1] << 8) | ((int32_t)rx[2]); if (val & 0x800000) { val |= ~0xFFFFFF; /* 手动扩展到32位有符号数 */ } *raw = val; return 0; }注意这里不能用简单的“按位或”之后就当作正数处理,因为如果测量结果是负电压,最高位为1,拼出来的值已经超过24位正数范围,直接转成float会算出一个错误的正值。
3.3 驱动分层:platform层与chip层分离
驱动代码写多了就会意识到,最好把平台相关部分和芯片逻辑分开。ADS1220的上层逻辑是:写配置、查DRDY、读数据、换算电压。而底层是SPI收发、CS控制、GPIO读取。这两部分如果混在一个文件里,换主控平台时就要大改。
我习惯分成两层:ads1220_platform.c里实现spi_transfer、spi_cs_low/high、drdy_read这几个接口;ads1220.c只依赖这几个接口完成芯片控制。这样从STM32换到其他单片机,甚至把SPI换成软件模拟,都只需要重写platform层。如果做Linux下的驱动开发,这套分层思路同样适用,只是platform层换成内核spidev或SPI控制器驱动提供的接口,寄存器操作部分完全复用。
4. 寄存器配置逐项拆解:增益、采样率、参考源怎么设
4.1 四个配置寄存器里的关键字段
ADS1220内部有四个配置寄存器,分别是CONFIG0、CONFIG1、CONFIG2、CONFIG3。上电后默认值就能跑,但要适合具体项目,必须重新写一遍。
CONFIG0里最关键的是输入通道选择MUX和PGA增益。MUX决定了当前转换的是哪两个输入引脚,可以是差分输入,也可以配置成单端。PGA增益字段决定模拟前端放大倍数,增益越高,满量程电压范围越小,但能分辨的微小信号能力越强。我用在称重传感器上,传感器输出满量程约10mV,供电激励5V,搭配增益128时满量程约为2.5V/128,也就是约19.5mV,刚好覆盖输出范围。
CONFIG1里选数据速率和滤波模式。数据速率字段直接决定有效分辨率,20SPS时噪声最低,2000SPS时吞吐最快但有效位数掉很多。滤波模式字段在需要切换输入通道的场景下非常重要,后面我会专门讲。
CONFIG2里设置IDAC恒流源电流大小,以及是否启用内部基准检测。如果做RTD测温,IDAC是这个芯片最大的价值,两颗恒流源可以配置成一路激励、一路补偿,非常方便。不做RTD时就全部关闭,避免无谓的功耗和对测量回路的影响。
CONFIG3主要是低侧开关、比较器、GPIO等辅助功能。常规采集场景保持默认即可,但要确认低侧开关没有意外开启,否则输入共模电压状况会改变,读数会莫名漂移。
4.2 配置组合实例和有效分辨率对照
我用一个实际配置来说明,目标是最低噪声的称重模式。假设外部基准2.5V,差分输入AIN0和AIN1,PGA增益128,数据速率20SPS。
uint8_t cfg0 = 0x60; /* AIN0/AIN1差分输入,PGA=128 */ uint8_t cfg1 = 0x00; /* 20SPS */ uint8_t cfg2 = 0x00; /* IDAC关闭 */ uint8_t cfg3 = 0x00; /* 默认 */ ads1220_write_reg(ADS1220_REG_CFG0, cfg0); ads1220_write_reg(ADS1220_REG_CFG1, cfg1); ads1220_write_reg(ADS1220_REG_CFG2, cfg2); ads1220_write_reg(ADS1220_REG_CFG3, cfg3);理论上24位LSB对应的电压是2.5V除以128再除以2^23,算出来约18.6nV。但实际噪声底由PGA的输入噪声、参考电压噪声和数字滤波器决定,不可能达到理论的24位。以数据手册的典型值来看,20SPS配合PGA=128时有效分辨率大概在19位左右,2000SPS时可能会掉到14位上下。这就带来一个设计原则:数据速率够用就好,不要为了“快”而牺牲精度。
4.3 单次转换与连续转换的取舍
ADS1220支持单次转换和连续转换两种方式。连续模式下,芯片每隔一个转换周期自动开始下一次转换,DRDY引脚每次数据准备好都会拉低。适合实时监测类应用,比如温度巡回显示。
单次模式更省电,每次转换完成后自动停止,DRDY拉低表示数据可读,读出后进入空闲状态。电池供电的数据采集设备建议用这个模式,采集一次、读一次、睡一觉再采集下一次。需要注意的是,在单次模式下,写入配置寄存器也会启动一次转换,这点用好了可以省掉单独的启动命令。
我的经验是:连续模式适合跑上位机实时看波形,单次模式适合做后台定时采集。如果你的项目里既想要高精度又对功耗敏感,优先选单次模式,配合MCU的睡眠唤醒机制,能把平均功耗压得很低。
5. 数据读取与软件处理:从原始码到工程值
5.1 等DRDY再读,别自己数时间
不少新手会犯一个错:配置好寄存器以后,用延时函数估摸着“差不多转换完了”就去读数据。这种估算在低速下还能蒙对,但一旦功耗变化、温度变化、主频变化,转换周期也会微调,很容易读到重复数据或者读到转换中间态。
正确做法只有一个:等待DRDY信号。DRDY是开漏输出,外部要接上拉电阻。转换完成时DRDY拉低,读取数据后自动拉高。用MCU的外部中断捕获下降沿,或者用GPIO轮询这个引脚。无论哪种方式,都必须加上超时保护,防止ADC异常时程序死在等DRDY的死循环里。
轮询版代码大概是这个逻辑:
uint32_t timeout = HAL_GetTick(); while (HAL_GPIO_ReadPin(DRDY_GPIO_PORT, DRDY_GPIO_PIN) != GPIO_PIN_RESET) { if (HAL_GetTick() - timeout > 100) { return -1; /* 超时,ADC可能没工作 */ } } ads1220_read_data(&raw);如果开了连续模式,DRDY每个转换周期都会出现一次,处理不当可能触发连续中断风暴。这时候要在中断里快速置标志位,真正的SPI读取放到主循环或者高优先级但可被打断的任务里执行。
5.2 24位补码到电压值的换算
ADS1220输出是二进制补码格式,正满量程接近0x7FFFFF,负满量程接近0x800000。换算成实际电压的公式是:
电压 = 原始码 * VREF / (PGA * 2^23)注意下面这个换算和很多人直觉不一样。虽然ADC是24位的,但因为有符号,实际量化区间是-1到+1,每个LSB代表的电压是VREF除以PGA再除以2^23,不是2^24。如果你按2^24算,所有测量结果都会比实际偏小一半,这个坑我曾经亲眼看到同事踩过。
换算成浮点数的代码:
double ads1220_raw_to_volt(int32_t raw, double vref, double gain) { return (double)raw * vref / (gain * 8388608.0); }用这个函数把原始码转成电压后,再乘上传感器灵敏度系数,就能得到最终工程物理量。比如称重传感器灵敏度是2mV/V,激励电压5V,则满量程输出10mV,对应毛重500kg。算起来很简单,但每个单位都要统一,我项目里就是直接定义了带单位的宏,避免换算时的小数点错误。
5.3 软件滤波:什么时候用均值,什么时候用滑动滤波
ADS1220片内已经有数字滤波器,速度越慢效果越好。但在实际现场,50Hz工频干扰、机械振动、电源闪变都会叠加进来,片内滤波未必够用,软件还得再做一道。
我的做法分两级。第一级是剔除粗大误差:连续采5次,去掉最大值和最小值,剩下3次求平均。这个对偶发的脉冲干扰特别有效。第二级是滑动平均:维护一个长度为8或16的环形缓冲区,每来一个新数据就覆盖最老的数据,取平均值。这样输出曲线平滑,又不会像简单平均一样产生明显滞后。
要提醒的是,软件滤波不是替代硬件滤波,而是补充。如果我前面提到的RC滤波和基准去耦没做好,软件滤波只能把表现“美化”,治标不治本。真正要解决噪声,还得回到电源、基准、布线上找原因。
6. 调试与排障:我在这颗ADC上踩过的坑
6.1 噪声偏高:从电源到基准的一路排查
第一次上电测试时,我把AIN0和AIN1直接短接,理论上输入差分电压为零,原始码应该在0附近小幅波动。结果读出来的数据在几百个LSB之间跳,换算成电压差不多是几十微伏的噪声。这个数值显然不正常。
排查顺序不要乱。第一步用示波器看AVDD纹波,发现有一个约几十毫伏的尖峰,频率正好和SPI通信频率一致。问题出在数字电源和模拟电源没有隔离,SPI翻转时的电流变化通过共同路径干扰了模拟电源。解决方法是加磁珠隔离DVDD和AVDD,并且把去耦电容位置调近。改完以后再测,噪声底降了一个数量级。
第二步查基准。外部基准输出端对地并了一个10uF电容,容量偏大导致基准环路不稳定,产生了低频振荡。把电容换成0.1uF后恢复正常。这个案例说明:基准不是去耦电容越大越好,要根据基准芯片的数据手册推荐值来选。
6.2 读值漂移和增益误差:校准补偿
有次测一个恒定的标准电压,读数一开始稳定,但半小时内缓慢漂移了若干个LSB。排查了电源和基准之后,发现是环境温度引起的。我的输入信号源本身是精密源,问题不在源头,那就只能用校准来矫正。
最有效的办法是两点校准。输入一个低电压和一个高电压,分别记录原始码,然后计算增益系数和偏移量。校准代码非常简单:
uint32_t raw_low, raw_high; double volt_low, volt_high; double scale, offset; scale = (volt_high - volt_low) / ((double)raw_high - (double)raw_low); offset = volt_low - scale * (double)raw_low;之后每次测量,实际电压等于raw * scale + offset。把这两组校准系数存到EEPROM或Flash里,每次开机读出来使用。工业测量设备基本都这么做,生产环节用一个标准源校准,然后固件里存校准系数。
6.3 SPI读数异常:时序与命令细节
ADS1220偶尔会读到全0xFFFFFF或者连续多次读到相同的数据。我排查过几类原因。第一是SPI时钟太快,线缆又长,信号反射导致数据错位。降频到500kHz后问题消失。第二是CS释放太快,最后一个字节没有被正确锁存。这类问题最坑的地方在于不是每次都复现,可能跑很久才出现一次。
解决办法是给CS时序留足余量,并在每次写配置寄存器后做一次回读验证。比如写入CONFIG0后立刻读回来,确认值一致再继续后面的流程。如果写和读不一致,先别盲目读数据,应该复位通信状态机或者重新初始化SPI。这套自检逻辑看起来多花几十微秒,但在强干扰环境里能避免灾难性的错误数据。
另外,RDATA命令读数据时会同时把这个命令字节作为普通字节发送到DOUT?实际上发送命令时DOUT上也有输出,有些情况下需要忽略第一个字节。我用的驱动里直接发送三个字节并读取三个字节,取后两字节拼数,实测结果正确。具体时序请务必对照数据手册的图来核对,不同的MCU SPI库在收发方向和行为上存在一点差异。
6.4 多通道扫描时的稳定时间问题
如果项目里要切多个通道采集,请特别注意滤波器的稳定时间。ADS1220的片内数字滤波器在通道切换后需要几个转换周期才能完全稳定,如果在切换后立刻读第一个结果,读出来的值往往是上一次通道的残留数据混着本次通道的数据,误差非常诡异。
处理方式有两种。第一种是在切换通道后丢弃前几个转换结果,等数据稳定后再采样。第二种是改用SySSTR快速稳定滤波模式,牺牲一点点有效分辨率,换取更快的通道切换速度。我在四通道测温板上采用了后者,采样周期缩短了不少,实际测量的温度精度依然满足0.1摄氏度要求。
这个案例给我们的启示是:高分辨率ADC的配置不是固定答案,必须结合你的扫描机制来选择。驱动开发不只是写完读写函数就算完成,还要理解信号链路上的每个环节,才能在一次又一次的排查中快速定位问题。