拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32硬件SPI驱动ADS1220高精度ADC完整方案与避坑指南

STM32硬件SPI驱动ADS1220高精度ADC完整方案与避坑指南

做高精度数据采集,最怕的就是传感器信号本身已经够微弱,到了ADC这一级还被噪声、参考电压漂移和时序问题再“扣”掉一截精度。ADS1220这颗24位Delta-Sigma ADC就是专门解决这类问题的:片内集成PGA可编程增益放大器、2.048V基准、两路恒流源IDAC,甚至还能直接驱动PT100/PT1000,省掉一堆外围电路。我这次用STM32的硬件SPI把ADS1220完整跑通,实现了从寄存器配置、数据读取到电压换算和软件校准的全流程,过程中踩了不少坑,尤其是硬件SPI与DOUT/DRDY复用引脚的配合,几乎每个第一次用这颗芯片的人都会在这里卡住。这篇文章就把我的完整方案和避坑记录都写出来,给准备用STM32做高精度采集的朋友一个可以直接抄作业的参考。

1. 方案选型与整体设计思路

1.1 为什么选ADS1220而不是其他ADC

市面上24位ADC不少,想省事还有各种现成模块,但ADS1220在这个定位里属于“刚刚好”的一颗芯片。它的核心优势其实不是单纯分辨率高,而是集成度太适合做仪表类前端了。

先说PGA。ADS1220内部增益支持1到128倍,对于常见的毫伏级传感器信号(应变片电桥、热电偶、PT100)非常友好。比如PT100在0℃时电阻100Ω,若用1mA激励电流,电压只有100mV,经过PGA放大后可以充分用满ADC的输入量程。这个集成PGA还有一个附带好处——外部不需要搭仪放,节省板面积,也少了一级噪声源。

再就是内部基准。ADS1220内部有2.048V基准电压,温漂典型值很低,对大多数应用完全够用。当然,如果追求极限精度,也可以把外部基准接到REFP/REFN引脚,芯片是支持两种模式的。我的建议是:前期调试用内部基准,简单可靠,等系统定型且需要更高精度时再换外部基准,这样能减少变量。

另外,它内部带两个可编程IDAC恒流源,这个功能做RTD测温非常关键。用恒流源激励PT100,再用同一个电流源在精密参考电阻上产生ADC的参考电压,就构成了比率式测量,激励电流的温漂和波动会被自动抵消掉。这个方案在后面的硬件连线和校准部分我会展开讲。

1.2 硬件SPI与软件模拟SPI,为什么我坚持选硬件

SPI驱动ADS1220,最简单粗暴的方式是用GPIO软件模拟时序。优点就是灵活:引脚随便选,DOUT/DRDY这个复用引脚可以直接用普通GPIO去轮询等待,逻辑清晰,网上大量Arduino例程都是这么写的。但缺点同样明显:CPU全程忙等,SCLK翻转节奏受中断影响,采样率稍高一点就吃力,而且代码里全是循环拉电平,移植到不同MCU要重新改。

这次项目里我选择STM32硬件SPI,思路很明确:需要稳定且可扩展的采集链路。硬件SPI的SCLK由外设自动产生,时钟频率和相位可控,传输过程不占用CPU,后续就算要接DMA或做多通道轮询,扩展空间也大得多。不过硬件SPI有个绕不过去的坎——ADS1220的DOUT/DRDY是同一个引脚,而它恰好又要接到STM32的SPI_MISO上。ISR中断、数据就绪检测、主从通信之间的关系一开始让我绕了不少弯,这部分细节我会在2.2节和3.3节重点说明。

1.3 系统整体架构与数据流

整个数据采集系统分成三级:

  • 传感级:以PT100测温为例,ADS1220的IDAC1输出1mA恒流,流过PT100后在AIN0/AIN1两端产生随温度变化的差分电压。
  • 转换级:STM32通过SPI配置ADS1220的输入通道、增益、数据速率和参考电压,ADC完成Delta-Sigma转换,结果存到芯片内部数据寄存器,DOUT/DRDY引脚拉低表示数据就绪。
  • 处理级:STM32检测到数据就绪后,通过SPI发送RDATA命令读取24位转换结果,再做符号扩展、电压换算和滤波,最后通过串口或显示屏输出物理量。

这里面的关键点在于SPI通信的时序结构,ADS1220比较特殊:它使用命令字来触发读寄存器、写寄存器和读取转换结果,而不是像普通SPI从机那样直接靠片选拉低就读数据。搞清楚命令字和字节顺序,驱动就完成了一半。

2. 硬件连接与PCB避坑要点

2.1 最小系统:电源、去耦与引脚分配

ADS1220的供电我习惯用3.3V单电源,AVDD和DVDD分别供电,中间串一个10Ω左右的磁珠或电阻做隔离,防止数字部分开关噪声窜到模拟电源。模拟地和数字地在芯片附近单点连接,这是高精度板的基本素养。

电源引脚旁的去耦电容不能省,AVDD引脚放一个10μF钽电容再加一个0.1μF陶瓷电容,陶瓷电容尽量靠近引脚。REFP和REFN如果使用内部基准,则这两个引脚只需各加一个0.1μF去耦电容;如果使用外部基准,电容要选低ESR的,且基准源本身的噪声直接影响转换结果,这一点很多人会忽略。

我这次用的引脚分配如下:

STM32引脚功能连接目标
PA5SPI1_SCKADS1220 SCLK
PA7SPI1_MOSIADS1220 DIN
PA6SPI1_MISOADS1220 DOUT/DRDY
PA4GPIO输出ADS1220 CS
PA2GPIO输入(可选)DOUT/DRDY分线,用于中断检测

注意我专门加了一路PA2,把DOUT/DRDY信号同时引到两个引脚,这个设计后面会解释用途。如果没有多余引脚,也可以用切换MISO复用功能的方式解决,二选一即可。

2.2 SPI四线怎么接,DRDY这个引脚最容易踩坑

ADS1220的SPI接口有四个引脚:SCLK、DIN、DOUT/DRDY、CS。其中DIN是输入(接STM32的MOSI),DOUT/DRDY是输出(接STM32的MISO)。SCLK空闲时的电平和采样沿决定了SPI模式,ADS1220实际支持的模式不算苛刻,我实测在STM32上配置为CPOL=0、CPHA=1,也就是SPI Mode 1,通信稳定无异常。

最大的坑出在DOUT/DRDY这个引脚上。这个引脚在片选拉低时会输出“数据就绪”信号,转换完成时由高拉低;但在SPI读取数据时,它又会变成数据输出引脚,把24位转换结果按位送出来。也就是说,一个引脚同时承担了“中断通知”和“数据回传”两个职责。

这就引出一个问题:如果MISO被SPI外设占用了,怎么在数据就绪时及时检测DRDY低电平?我试过几种方案,最稳定的做法是把DOUT/DRDY同时接到两个MCU引脚,一个复用成SPI_MISO,另一个配置成外部中断或GPIO输入。读取数据前屏蔽外部中断,读完数据再打开。这样既能保证SPI收发正常,又能用中断方式低延迟感知转换完成。

如果引脚不够,也可以把MISO引脚在“等待就绪”阶段临时配置成GPIO输入,轮询到低电平后再切回SPI复用功能,把命令和数据读回来。这个方案省引脚,但每次都要调用GPIO初始化函数切换模式,代码稍微繁琐,实际测试也可以用。

2.3 前端信号调理:RC滤波与比率式PT100接法

ADC前端如果直接裸接传感器,转换结果大概率会抖得惨不忍睹。我的做法是在AIN0/AIN1输入脚各串一个1kΩ电阻,再对地接一个0.1μF电容,组成低通RC滤波。这个滤波器的截止频率大约1.6kHz,对ADS1220最高2kSPS的采样率来说是合理的,能把高频干扰压下去,又不至于让有效信号衰减过多。

PT100接法用比率式最实用。原理是:IDAC1输出1mA电流,流经PT100,在AIN0和AIN1之间产生电压差。同一路电流也流过外部精密参考电阻R_ref(比如1.5kΩ,1%甚至0.1%精度),R_ref两端电压作为ADC参考电压Vref。这样ADC的输出比例是:

code / 2^23 = (I * R_pt100) / (I * R_ref) = R_pt100 / R_ref

I被约掉了,激励电流的绝对精度和温漂都不影响测量结果,只剩下R_ref的温度系数和精度影响。这个方法是我强烈推荐的,比用固定2.048V内部基准再查表算电阻要稳定得多。

3. CubeMX工程配置与SPI初始化

3.1 一个能跑起来的CubeMX配置清单

用STM32CubeMX生成工程时,SPI部分这样配:

  • SPI Mode:Full-Duplex Master
  • 数据帧大小:8 Bit(这一点很重要,下面会说)
  • 时钟极性CPOL:Low
  • 时钟相位CPHA:1 Edge(严格对应SPI Mode 1)
  • NSS:Disable(片选完全用软件GPIO控制)
  • 波特率预分频:先设 /32,也就是在72MHz主频下SPI时钟为2.25MHz

数据帧大小必须选8位,不要选16位。ADS1220命令和数据都是按字节处理的,如果用16位帧去发两个字节,命令响应和字节顺序会变得非常别扭,我一开始改成16位帧后读取结果全乱,换回8位就正常了。

NSS关掉是另一个重点。STM32硬件NSS自动片选模式用于普通SPI从机可能是好事,但用在ADS1220上会遇到时序控制粒度不足的问题,因为READ_DATA命令和后续读取之间可能需要片选状态保持或再次拉低。用普通GPIO控制CS反而最灵活,高电平拉高拉低完全由自己掌控。

3.2 SPI参数的坑:时钟极性与相位、片选怎么选

SPI Mode 1对应CPOL=0、CPHA=1,这是很多ADS1220驱动里默认的配置,实测稳定。但有一个细节容易被忽略:ADS1220的DI输入是在SCLK的上升沿采样还是下降沿采样,手册里有明确的时序图。我当时因为偷懒没仔细看手册,沿用某个例程的Mode 0配置,结果数据读出来偶尔会错位一位,排查了半天。所以拿到芯片后第一步先确认时序模式,不要凭经验盲猜。

关于片选,我在工程里把CS配置成普通推挽输出GPIO,初始化为高电平。每次通信前拉低,通信结束后拉高。不要在SPI初始化后一直把CS拉低,除非你只用ADS1220一个从机且不担心供电和复位时序,否则养成“用完就释放片选”的习惯,方便后续扩展。

3.3 用GPIO切换去读DRDY,省一个引脚的方法

如果你不想像我一样单独分配一个中断引脚给DRDY,可以试试MISO复用切换方法。等待数据就绪时,CS拉低,此时DOUT/DRDY输出的是DRDY状态,把MISO引脚从SPI复用切换为GPIO输入,轮询读取:

GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); // MISO切到GPIO输入,读DRDY状态 GPIO_InitStruct.Pin = GPIO_PIN_6; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_6) != GPIO_PIN_RESET); // DRDY拉低,切回SPI复用 GPIO_InitStruct.Pin = GPIO_PIN_6; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

实测这个方法的时序开销在几十微秒级别,而ADS1220最低数据速率是20SPS,50ms一次转换,完全足够。它唯一的风险在于切换引脚复用的一瞬间,如果SPI外设正处于传输状态,会产生不可预知的波形。所以在切换前必须保证SPI处于空闲状态,读数据所有操作完成后才切回GPIO模式等待下一次DRDY。这个方案省了一个引脚,代价是代码里多一段复用切换逻辑,取舍看项目资源。

4. ADS1220驱动代码与寄存器操作

4.1 寄存器一览与命令格式

ADS1220内部有4个配置寄存器,编号0到3,分别控制输入多路开关、PGA增益、数据速率、转换模式、参考电压、IDAC电流源和IDAC输出引脚。我的建议是初始化时把4个寄存器一次性写入,这样上电状态明确,不会因为芯片默认值不符合需求而出问题。

芯片的命令字不多,最常用的就这几个:

  • RESET:0x06,复位全部寄存器
  • START/SYNC:0x08,启动转换
  • POWERDOWN:0x02,进入低功耗
  • RDATA:0x10,读取转换结果
  • RREG:0x20 | (addr << 2),读指定寄存器
  • WREG:0x40 | (addr << 2),写指定寄存器

注意RREG和WREG的地址编码是左移两位。比如要写寄存器0,命令字节是0x40;写寄存器1,命令字节是0x44。有些网上代码写成0x40 | addr,结果写在地址0和地址1时看起来没问题,一旦写寄存器2以上就全乱了。

4.2 读写寄存器函数封装

使用HAL库时,读写寄存器最简单的做法是直接调用HAL_SPI_Transmit和HAL_SPI_TransmitReceive,配合GPIO片选控制:

void ADS1220_WriteReg(uint8_t reg, uint8_t val) { uint8_t tx[2]; tx[0] = 0x40 | (reg << 2); tx[1] = val; ADS1220_CS_LOW(); HAL_SPI_Transmit(&hspi1, tx, 2, 10); ADS1220_CS_HIGH(); } uint8_t ADS1220_ReadReg(uint8_t reg) { uint8_t tx = 0x20 | (reg << 2); uint8_t rx = 0; ADS1220_CS_LOW(); HAL_SPI_TransmitReceive(&hspi1, &tx, &rx, 1, 10); tx = 0x00; HAL_SPI_TransmitReceive(&hspi1, &tx, &rx, 1, 10); ADS1220_CS_HIGH(); return rx; }

两个函数的逻辑都是从机命令的经典用法:先发命令字节,再在下一个SPI周期读取或写入数据。注意CS在整个事务过程中必须保持低电平,不能中间拉高,否则从机会认为命令结束。

4.3 单次转换和连续转换完整流程

ADS1220可以配置成单次转换模式或连续转换模式。单次转换更省电,适合电池供电或不需要高频采样的场景;连续转换则适合需要波形采集和实时监测的应用。我的代码里这两套流程都有,切换配置很简单,主要看CONFIG1寄存器里的模式位。

初始化代码:

void ADS1220_Init(void) { ADS1220_CS_HIGH(); // 复位芯片,清除所有寄存器到默认值 ADS1220_CS_LOW(); uint8_t cmd = 0x06; HAL_SPI_Transmit(&hspi1, &cmd, 1, 10); ADS1220_CS_HIGH(); HAL_Delay(10); // 配置寄存器0:AIN0-AIN1差分输入,PGA增益=1 ADS1220_WriteReg(0, 0x00); // 配置寄存器1:连续转换模式,数据速率100SPS ADS1220_WriteReg(1, 0x04); // 配置寄存器2:内部2.048V参考,IDAC=1mA ADS1220_WriteReg(2, 0x10); // 配置寄存器3:IDAC1输出到AIN2,IDAC2关闭 ADS1220_WriteReg(3, 0x00); // 启动转换 cmd = 0x08; ADS1220_CS_LOW(); HAL_SPI_Transmit(&hspi1, &cmd, 1, 10); ADS1220_CS_HIGH(); }

单次转换模式读数据时,可以等待DRDY变低后,发送RDATA命令,读取3个字节的有效数据加1个字节状态。读取函数如下:

int32_t ADS1220_ReadData(void) { uint8_t cmd = 0x10; uint8_t rx[4] = {0}; ADS1220_CS_LOW(); HAL_SPI_TransmitReceive(&hspi1, &cmd, &rx[0], 1, 10); HAL_SPI_Transmit(&hspi1, NULL, 0, 10); // 产生SCLK ADS1220_CS_HIGH(); return 0; }

但上面这段是示意,真正读24位数据需要连续读取4个字节,并且第二个字节开始才是数据。下面是我实际使用的读取逻辑:

int32_t ADS1220_ReadData(void) { uint8_t cmd = 0x10; uint8_t tx[4] = {cmd, 0x00, 0x00, 0x00}; uint8_t rx[4] = {0}; ADS1220_CS_LOW(); HAL_SPI_TransmitReceive(&hspi1, tx, rx, 4, 10); ADS1220_CS_HIGH(); // rx[0]通常是命令响应或状态字节,rx[1]~rx[3]是24位数据 int32_t raw = ((int32_t)rx[1] << 16) | ((int32_t)rx[2] << 8) | rx[3]; // 符号扩展,24位数据转成32位有符号数 if (raw & 0x800000) { raw |= 0xFF000000; } return raw; }

注意这里要显式做符号扩展。因为ADS1220输出是二进制补码,最高位是符号位,如果不把bit23扩展到32位int的高字节,负数会被读成很大的正数,后面换算温度和电压时完全对不上。

4.4 把原始码换算成电压和温度

拿到24位原始码后,电压换算公式是:

电压 = raw_code / 8388608.0 * (Vref / PGA_gain)

其中8388608是2^23,Vref如果是内部2.048V,PGA增益为1,则满量程范围是-2.048V到+2.048V。这个公式的前提是输入属于差分输入范围。如果给的是单端信号,负输入接GND,量程会变为0到Vref/PGA,但计算方式不变。

换算成温度时用查表法最稳妥。PT100温度与电阻的关系可以用标准分度表,把-50℃到300℃的电阻值做成一个const数组,然后用线性插值求温度。更快但更粗略的方法是直接用公式计算,但为了精度我还是推荐查表加插值,代码量也不大。

我建议把原始码、电压、电阻、温度四级转换封装成独立函数,方便以后换传感器时只改中间一层,不用动底层SPI逻辑。

5. 校准方法:让数据真正“准”起来

5.1 常规校准流程:零点偏移和增益误差

很多人在ADC数据采集项目里,代码调通就算完了,读出的电压和万用表对不上就怀疑芯片坏了。实际上ADC转换精度再好,系统级仍会引入零点偏移和增益误差。零点偏移来源于运放失调电压、接触电势、PCB漏电;增益误差则来源于参考电压偏差和PGA增益波动。

校准的第一步是测零点偏移。把ADC差分输入两端短接,采集一段数据取平均值,记作code_zero。这个值可能是正几十、负几十,正常范围内。然后给一个已知标准电压,比如用精度0.01%的电压源给100mV,采集平均值记作code_ref。接下来系统的校准公式就是:

真值 = (raw_code - code_zero) * (标准电压 / (code_ref - code_zero))

这种两点校准法能把大部分系统误差修掉。如果传感器是PT100,还可以反过来把校准后的电压和标准温度源对比,做温度域的线性修正。

5.2 用精密信号源做两点校准的实操流程

我调这块板子时用了一台台式万用表和一台可编程电压源做基准。流程如下:

  1. 用导线短接AIN0和AIN1,连续采集100次取平均,记录为code_zero。
  2. 给AIN0和AIN1之间加一个200mV标准电压,注意不要超过所选PGA增益下的满量程范围,连续采集100次取平均,记录为code_ref。
  3. 把code_zero和code_ref写入STM32内部Flash或外部EEPROM,每次上电读取,自动校准。
  4. 校准后的数据再用50mV、100mV、150mV几个点回测,确认线性度。

曾经踩过的坑是:如果信号源内阻太大,和输入RC滤波电容形成分压,会导致校准电压不准。所以校准源的输出阻抗越低越好,最好直接是电压源输出模式,不要串大电阻模拟。

5.3 数字滤波与均值处理,别让数据乱跳

即使硬件和校准都做对了,最后的读数还是会有一点小波动,这是Delta-Sigma ADC的正常噪声。好消息是ADS1220在低数据速率下本身噪声极低,再加上简单的软件滤波就够用。

我常用两种滤波方式:

  • 中值滤波:连续采集5次,取中间值,能有效剔除偶发的尖峰干扰。
  • 滑动平均:取8次或16次采样平均,适合稳定温度或缓慢变化信号,响应速度损失不大。

不建议一来就上卡尔曼滤波或IIR低通,因为参数调不好反而让数据“黏糊糊”的。先把中值滤波加滑动平均用起来,绝大部分场景都能满足。

6. 我实际踩过的坑与排查建议

6.1 数据总为0x7FFFFF或0x800000

这是我最开始遇到的情况,读数永远是满量程正或满量程负。原因通常有两种:一是输入悬空或超过ADC输入共模范围,导致PGA输出饱和;二是SPI读取的字节顺序错位,把状态字节当成了数据高位。

排查办法很简单:先短接输入看是否接近0;如果短接后数值正常,那问题在前端信号;如果短接后依然满量程,重点检查SPI读取顺序和符号扩展逻辑。

6.2 读出来的数值一直乱跳

如果是短接输入后数值还是在正负几百甚至几千之间跳,大概率是参考电压或电源噪声问题。解决办法是给AVDD加更大容量的去耦电容,同时检查SPI的SCLK线上是否和模拟输入产生串扰。PCB布局上,SPI信号线尽量远离AIN输入走线,不能在顶层平行走太长。

另外检查一下PGA增益设置。如果信号只有几十毫伏,PGA增益设成1,那么有效分辨率会被浪费,读数的码值看起来也会“乱跳”。正确做法是把增益调到能让信号占据80%以上满量程,信噪比明显改善。

6.3 SCLK速度、上拉电阻这些“小问题”

SPI时钟速度不是越快越好。ADS1220虽然是SPI从机,但内部移位逻辑有最短时钟周期要求。我最后把SPI时钟设在2.25MHz,稳妥优先。如果调试中发现数据偶发错位,可以试试降到1MHz,很多诡异问题会自己消失。

还有个容易忽略的点:DIN和SCLK是否需要上拉电阻。如果MCU的MOSI和SCK是推挽输出,不需要上拉;但如果你的板子有飞线、杜邦线连接,建议在从机侧加10kΩ上拉到VDD,能显著提高抗干扰能力,尤其在SCLK线长超过10cm时。

6.4 硬件SPI调试工具和排查顺序

从头调这个驱动,我的建议排查顺序是:先用逻辑分析仪抓SPI波形,确认SCLK、MOSI、MISO和CS时序跟数据手册对照;然后读寄存器,回读配置是否和写入的一致;最后再运行ADS1220的RDATA命令看数据。

工具上,一个24MHz以上的逻辑分析仪就够了,没必要上来就上示波器。我自己最常用的方式是抓WRITE_REG命令的波形,因为写寄存器能直接看到MOSI上的命令和数值,能快速判断SPI模式对不对。如果波形里MISO总在高阻或一直高,再检查CS控制逻辑和DRDY复用问题。

最后说点实在的

这块板子从画原理图到SPI驱动跑通,我前后折腾了三个晚上,最耗时间的不是寄存器配置,而是DOUT/DRDY这根的复用处理。如果你也准备用硬件SPI做ADS1220,我建议先把引脚规划想清楚,要么像我一样多留一个GPIO接DRDY,要么写一段可靠的MISO复用切换函数,不要等到程序写一半再改板子。

另外提醒一句,ADS1220数据手册里关于SPI时序和寄存器位的描述很详细,但PDF很长,看的时候直接用关键词搜“RDATA”“WREG”“DOUT/DRDY”这几个关键段落,比从头读效率高很多。配置完寄存器之后,第一次读数据前务必泡一杯茶,认真读一遍波形,把时序核对清楚,后面就一马平川了。

返回列表