
做嵌入式这几年有时候真不是MCU内部ADC不够用而是当信号本身就很小、噪声底又苛刻的时候内部12位ADC的短板会被放得非常明显。去年做多通道温度采集和称重信号采集模块我把方案定在TI的ADS1220上顺手把整套ADC驱动开发流程完整走了一遍。ADS1220是一颗24位Δ-Σ型ADC带SPI接口、内置PGA和双IDAC恒流源适合称重、热电偶、RTD测温这类低速但精度要求很高的场景。这篇文章把选型逻辑、硬件设计、寄存器配置、驱动架构和调试验证全盘复盘一遍给正好要在这颗芯片上做驱动开发的同学一个可以直接抄作业的参考。1. 选型逻辑为什么是ADS1220而不是别的ADC1.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从设备支持模式1CPOL0CPHA1SCLK空闲拉低数据在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和AIN1PGA增益128数据速率20SPS。uint8_t cfg0 0x60; /* AIN0/AIN1差分输入PGA128 */ 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配合PGA128时有效分辨率大概在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的配置不是固定答案必须结合你的扫描机制来选择。驱动开发不只是写完读写函数就算完成还要理解信号链路上的每个环节才能在一次又一次的排查中快速定位问题。