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 SCLKPA7SPI1_MOSIADS1220 DINPA6SPI1_MISOADS1220 DOUT/DRDYPA4GPIO输出ADS1220 CSPA2GPIO输入可选DOUT/DRDY分线用于中断检测注意我专门加了一路PA2把DOUT/DRDY信号同时引到两个引脚这个设计后面会解释用途。如果没有多余引脚也可以用切换MISO复用功能的方式解决二选一即可。2.2 SPI四线怎么接DRDY这个引脚最容易踩坑ADS1220的SPI接口有四个引脚SCLK、DIN、DOUT/DRDY、CS。其中DIN是输入接STM32的MOSIDOUT/DRDY是输出接STM32的MISO。SCLK空闲时的电平和采样沿决定了SPI模式ADS1220实际支持的模式不算苛刻我实测在STM32上配置为CPOL0、CPHA1也就是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_refI被约掉了激励电流的绝对精度和温漂都不影响测量结果只剩下R_ref的温度系数和精度影响。这个方法是我强烈推荐的比用固定2.048V内部基准再查表算电阻要稳定得多。3. CubeMX工程配置与SPI初始化3.1 一个能跑起来的CubeMX配置清单用STM32CubeMX生成工程时SPI部分这样配SPI ModeFull-Duplex Master数据帧大小8 Bit这一点很重要下面会说时钟极性CPOLLow时钟相位CPHA1 Edge严格对应SPI Mode 1NSSDisable片选完全用软件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对应CPOL0、CPHA1这是很多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最低数据速率是20SPS50ms一次转换完全足够。它唯一的风险在于切换引脚复用的一瞬间如果SPI外设正处于传输状态会产生不可预知的波形。所以在切换前必须保证SPI处于空闲状态读数据所有操作完成后才切回GPIO模式等待下一次DRDY。这个方案省了一个引脚代价是代码里多一段复用切换逻辑取舍看项目资源。4. ADS1220驱动代码与寄存器操作4.1 寄存器一览与命令格式ADS1220内部有4个配置寄存器编号0到3分别控制输入多路开关、PGA增益、数据速率、转换模式、参考电压、IDAC电流源和IDAC输出引脚。我的建议是初始化时把4个寄存器一次性写入这样上电状态明确不会因为芯片默认值不符合需求而出问题。芯片的命令字不多最常用的就这几个RESET0x06复位全部寄存器START/SYNC0x08启动转换POWERDOWN0x02进入低功耗RDATA0x10读取转换结果RREG0x20 | (addr 2)读指定寄存器WREG0x40 | (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); // 配置寄存器0AIN0-AIN1差分输入PGA增益1 ADS1220_WriteReg(0, 0x00); // 配置寄存器1连续转换模式数据速率100SPS ADS1220_WriteReg(1, 0x04); // 配置寄存器2内部2.048V参考IDAC1mA ADS1220_WriteReg(2, 0x10); // 配置寄存器3IDAC1输出到AIN2IDAC2关闭 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^23Vref如果是内部2.048VPGA增益为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 用精密信号源做两点校准的实操流程我调这块板子时用了一台台式万用表和一台可编程电压源做基准。流程如下用导线短接AIN0和AIN1连续采集100次取平均记录为code_zero。给AIN0和AIN1之间加一个200mV标准电压注意不要超过所选PGA增益下的满量程范围连续采集100次取平均记录为code_ref。把code_zero和code_ref写入STM32内部Flash或外部EEPROM每次上电读取自动校准。校准后的数据再用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”这几个关键段落比从头读效率高很多。配置完寄存器之后第一次读数据前务必泡一杯茶认真读一遍波形把时序核对清楚后面就一马平川了。
返回列表