基于GPIO模拟SPI的ADS1220驱动实现:从时序到代码全解析 📅 发布时间:2026/9/9 15:26:34 👁 浏览次数: 简介面向STM32F103xx系列开发者这份驱动包以软件模拟方式实现ADS1220模数转换芯片的完整时序控制适用于测量仪器、工控现场等需要高精度、低成本模拟信号采集的场景。压缩包共172个文件容量5.22MB除了C源码和H头文件外还包含完整Keil MDK工程文件uvprojx/uvoptx、编译生成的o/crf/axf/hex中间文件以及SCT链接脚本和BAT批处理脚本可在Keil中直接打开、编译并烧录验证。驱动代码覆盖芯片初始化、工作模式配置、单次与连续转换、SPI通信等关键功能并集成STM32标准外设库的RCC、ADC、TIM、I2C、Flash等模块具体展示了通过GPIO模拟时序完成与ADS1220交互的软件实现可帮助开发者结合芯片手册理解寄存器配置与数据读取流程。已有788人学习下载适合需要快速上手ADS1220驱动开发、参考软件模拟SPI思路或对现有采集方案进行移植优化的嵌入式工程师。 做采集板这些年和ADS1220打交道的次数确实不少。这颗24位Δ-Σ ADC在工业称重、温度采集和便携仪表里出现频率极高功耗低、通道数够用、价格也友好。但真正让人头疼的不是芯片本身而是它在某些主控平台上压根没有可用的硬件SPI外设或者硬件SPI引脚被别的功能占光了。这个时候软件模拟就成了绕不开的方案。这篇文章想聊的就是这件事在不依赖硬件SPI的情况下用普通GPIO模拟SPI时序把ADS1220的驱动程序完整跑起来。适合正在做嵌入式采集、遇到引脚冲突或者想彻底搞懂ADS1220底层时序的工程师。哪怕你之前只用过现成的HAL库只要跟着把几个关键点吃透软件模拟的方法也能直接上手。1. 项目整体设计为什么非要用GPIO软拆SPI1.1 先聊聊ADS1220这颗芯片的脾气ADS1220本质上是颗四通道、带PGA可编程增益放大器的24位Δ-Σ ADC内部集成了参考电压源、温度传感器和两组激励电流源。它和MCU之间只有四根线SCLK、DIN、DOUT/DRDY、CS标准SPI从机接口。命令集也简单无非就是读写寄存器、启动转换、读数据这几类。但有个容易被忽略的细节ADS1220支持SPI模式1和模式3分别是空闲时钟低、第二个沿采集数据和空闲时钟高、第二个沿采集数据。两种模式虽然都能跑但对软件模拟来说模式3CPOL1, CPHA1在代码上更好写因为SCLK空闲为高下降沿更新数据、上升沿采样时序更像手工搭出来的样子。还有一个特点是DOUT引脚同时兼任数据输出和DRDY状态指示。数据准备好时DOUT/DRDY会拉低告诉主机可以来读了。这也意味着读取数据前必须先判定引脚状态不能上来就随便读。1.2 软件模拟的核心决策点选软件模拟SPI之前要先确认一个问题你是真的没有硬件SPI还是仅仅因为引脚冲突。如果是后者通常可以优先调整硬件SPI的引脚映射很多MCU的SPI外设都能重映射到多组IO。只有当你确定没有多余的外设模块、或者想把驱动层做得完全跟硬件无关时才值得上GPIO模拟。GPIO模拟的好处有四块引脚任意分配布线方便不受硬件SPI固定引脚限制协议时序完全可控可以在位级调整驱动强度代码可移植性高换MCU平台只需要改底层IO函数方便理解协议本身调试时可以用示波器逐位对照时序图。当下代价也很明显速度上不去。ADS1220最高数据速率也就2kSPS一个字节8个时钟周期模拟SPI的翻转频率只要达到几十kHz就绰绰有余MCU主频只要有几十MHz软件模拟的CPU占用并不会成为负担。这也是这颗芯片特别适合软件模拟的根本原因——它本身就跑不快。1.3 驱动架构怎么分层我的做法是分成三层底层IO层负责初始化GPIO、拉高拉低引脚、读取MISO状态SPI传输层完成字节级的收发对外提供soft_spi_read_write这样的函数设备驱动层实现ADS1220的命令解析、寄存器配置、数据读取和校准逻辑。这样分层有一个好处如果以后换成带硬件SPI的板子只需要替换中间那层SPI传输函数第三层设备驱动代码完全可以复用。实际项目中我甚至会用宏定义做切换同一个驱动文件同时支持软件模拟和硬件SPI两种模式。2. ADS1220的寄存器与时序不搞清楚没法写驱动2.1 四种命令必须烂熟于心ADS1220的命令不像某些芯片那样有一堆花哨的控制字日常使用就四个RREG读寄存器、WREG写寄存器、RDATA读转换结果和START/SYNC启动转换。RREG的命令格式是0x20加寄存器地址后面紧跟一个字节表示连续读几个寄存器减一WREG类似命令字是0x40加地址后面一个字节表示写几个寄存器减一再跟寄存器数据。举个例子读寄存器0的完整序列是发送0x20再发送0x00然后DOUT上就会输出寄存器0的值。写寄存器0则要发送0x40、0x00、然后一个字节的数据。RDATA命令更简单直接发0x10然后连续读三个字节得到24位转换结果。2.2 寄存器0和寄存器1是配置的核心寄存器0控制输入通道选择、PGA旁路和增益。MUX位决定是差分输入还是单端输入比如选择AIN0和AIN1差分MUX值就是0000。增益位从000到111对应1倍到128倍。对于称重传感器这类微弱信号场景通常会把PGA开到64或128对于已经放大过的信号开1倍增益就够了顺便还能省点功耗。寄存器1管理转换模式和数据速率。转换模式有单次转换和连续转换两种。单次转换模式下发完START命令后要等DRDY拉低再读数据连续转换模式则输出数据会周期性地更新。数据速率从20SPS到2kSPS默认值是20SPS。需要注意的是速率设置越高噪声越大精度反而下降所以做高精度采集时我一般锁定在20SPS或160SPS。2.3 时序细节软件模拟成败的关键ADS1220的CS引脚拉低后通信开始拉高则复位整个SPI接口。每个字节传输时先发最高位还是最低位ADS1220要求先发送最高位MSB first这个顺序要在字节收发函数里固定死不能照搬某些默认LSB first的库。还有个容易踩坑的细节当CS拉高时DOUT/DRDY引脚会变成高阻状态这时候如果外部没有上拉电阻读取引脚就会是悬空电平。所以硬件设计上DOUT脚上最好加一个10k左右的上拉电阻软件模拟时也更稳定。3. GPIO模拟SPI驱动实现的完整过程3.1 引脚初始化和基础延时我用的是STM32平台做示例但其实任何MCU都适用。首先定义四个引脚的宏#define ADS_CS_LOW() HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET) #define ADS_CS_HIGH() HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET) #define ADS_SCLK_LOW() HAL_GPIO_WritePin(SCLK_GPIO_Port, SCLK_Pin, GPIO_PIN_RESET) #define ADS_SCLK_HIGH() HAL_GPIO_WritePin(SCLK_GPIO_Port, SCLK_Pin, GPIO_PIN_SET) #define ADS_DIN_HIGH() HAL_GPIO_WritePin(DIN_GPIO_Port, DIN_Pin, GPIO_PIN_SET) #define ADS_DIN_LOW() HAL_GPIO_WritePin(DIN_GPIO_Port, DIN_Pin, GPIO_PIN_RESET) #define ADS_DOUT_READ() HAL_GPIO_ReadPin(DOUT_GPIO_Port, DOUT_Pin)延时函数用简单的循环即可关键是确认一个时钟周期内有足够的建立时间。ADS1220的SCLK最高能跑到几MHz但我们软件模拟一般不超过几百kHz所以延时长一点反而稳定。我习惯选用一个可调宏#define SPI_DELAY() for(volatile int i 0; i 10; i)初始化时把SCLK和CS先拉高DIN拉低DOUT配置为输入模式。CS默认高电平SCLK默认高电平这样在第一条命令发出之前总线处于一个确定的空闲状态。3.2 字节收发最核心的两个函数软件模拟SPI的字节收发每个位都要严格按照时序来走。我用的是模式3的时序逻辑先根据发送字节设置DIN电平然后拉高SCLK产生上升沿在上升沿后读取DOUT最后拉低SCLK产生下降沿准备下一轮。uint8_t soft_spi_read_write(uint8_t tx_byte) { uint8_t rx_byte 0; for (uint8_t i 0; i 8; i) { if (tx_byte 0x80) ADS_DIN_HIGH(); else ADS_DIN_LOW(); SPI_DELAY(); ADS_SCLK_HIGH(); // 上升沿ADS1220采样DIN SPI_DELAY(); if (ADS_DOUT_READ()) // 同时读取DOUT输出 rx_byte | 0x01; ADS_SCLK_LOW(); // 下降沿ADS1220更新DOUT SPI_DELAY(); tx_byte 1; if (rx_byte 0x01) // 把读到的位移动到正确位置 rx_byte | 0x80; rx_byte 1; } return rx_byte; }这里有两个细节我最初踩过坑。一是读取DOUT的时机必须在SCLK上升沿之后、下降沿之前否则会读到上一轮的旧数据。二是能收到数据的前提是CS已经拉低而且ADS1220已经处于数据输出模式。如果DOUT一直为高多半是命令没发对或CS时序有问题。3.3 读写寄存器命令的封装有了字节收发函数剩下的就是拼命令。读写寄存器本质上就是连续调用soft_spi_read_write发送若干个字节uint8_t ads1220_read_reg(uint8_t reg, uint8_t count) { uint8_t data 0; ADS_CS_LOW(); soft_spi_read_write(0x20 | (reg 0x07)); soft_spi_read_write(count - 1); data soft_spi_read_write(0x00); ADS_CS_HIGH(); return data; } void ads1220_write_reg(uint8_t reg, uint8_t count, uint8_t *data) { ADS_CS_LOW(); soft_spi_read_write(0x40 | (reg 0x07)); soft_spi_read_write(count - 1); for (uint8_t i 0; i count; i) { soft_spi_read_write(data[i]); } ADS_CS_HIGH(); }注意每次读写操作完成后都要把CS拉高。CS拉高的动作会让DOUT/DRDY引脚变成高阻下一次通信重新拉低CSDOUT才会恢复输出。如果不拉高CS连续多次读写后容易产生时序混乱DRDY状态位也可能丢失。3.4 启动转换与等待DRDY在单次转换模式下配置完寄存器后要真正启动一次转换。ADS1220提供了START命令很多人忽略这一步直接去读数据结果读到的永远是最初的无效值。启动转换的写法void ads1220_start_conversion(void) { ADS_CS_LOW(); soft_spi_read_write(0x08); // START/SYNC命令 ADS_CS_HIGH(); }启动后等待DOUT/DRDY拉低表示转换完成。如果等待时间超过设定数据速率的周期比如20SPS对应50毫秒就要考虑是否配置出错或测量通道悬空。我用循环加超时机制来等待uint8_t ads1220_wait_for_drdy(uint32_t timeout_ms) { uint32_t tick 0; while (ADS_DOUT_READ() ! 0) { HAL_Delay(1); tick; if (tick timeout_ms) return 1; // 超时 } return 0; }4. 数据采集、误差处理与调试实录4.1 完整读取24位数据的流程ADS1220转换结果是24位二进制补码符号位在前。读取时先发RDATA命令然后连读三个字节组合成32位有符号整数再进行符号扩展int32_t ads1220_read_data(void) { uint8_t buf[3]; int32_t raw 0; ADS_CS_LOW(); soft_spi_read_write(0x10); // RDATA buf[0] soft_spi_read_write(0xFF); buf[1] soft_spi_read_write(0xFF); buf[2] soft_spi_read_write(0xFF); ADS_CS_HIGH(); raw ((int32_t)buf[0] 16) | ((int32_t)buf[1] 8) | (int32_t)buf[2]; if (raw 0x800000) // 符号扩展 raw | 0xFF000000; return raw; }拿到raw值后换算成电压的公式是电压 raw × 满量程电压 ÷ (增益 × 2^23)。如果参考电压是2.048V增益是1那么每LSB对应的电压大约是2.048V / 8388608约等于0.244μV。4.2 参考电压和PGA设置直接决定精度上限实测下来ADS1220的内部参考电压精度虽然够用但温漂明显做高精度采集时最好用外部精密参考源。参考电压选2.048V还有个好处2的整数次幂换算方便移位就能得到结果降低了MCU的计算开销。PGA增益的设置会影响输入范围同时也影响有效分辨率。在128倍增益下折算到输入的噪声可能达到几μV级别而满量程只剩16mV左右噪声占比大幅上升。所以不是增益越高越好。做称重时我通常选择32倍增益再在固件里做一次数字滤波平均几十次后再换算重量值。4.3 常见问题速查表我在调试软件模拟ADS1220驱动时把遇到的典型问题整理成了速查表基本覆盖了大多数异常现象现象可能原因排查方法读回的寄存器全是0xFFDOUT上拉缺失或CS未拉低加10k上拉电阻确认CS时序读回的寄存器全是0x00字节时序不对DIN数据位序反了用示波器对照DIN与SCLK相位数据始终不变未发送START命令或处于连续模式的旧数据执行START等待DRDY后再读DRDY一直不拉低通道悬空或输入超范围检查输入通道降低PGA增益数据跳变剧烈参考电压噪声大或延时不够改用外部参考增大SPI_DELAY24位数据符号错误符号扩展没做检查raw 0x800000判断4.4 单次转换与连续转换的选择建议连续转换模式下ADC始终在跑DOUT/DRDY会周期性拉低。好处是响应快坏处是中断频率高MCU可能要频繁处理转换完成事件功耗下不来。单次转换模式则更省电发一次START等DRDY读数据芯片可以立刻进入低功耗待机状态。便携式仪表里几乎都采用单次转换模式。我做低功耗设备时一般把MCU和ADS1220都在两次转换之间休眠。转换完成后DOUT/DRDY引脚拉低会触发外部中断唤醒MCUMCU醒来读数据再接着睡整体平均功耗可以压得很低。5. 动手移植时的几个额外忠告最后再说几个软件模拟ADS1220移植时容易忽略的细节。第一个是关于中断。SPI模拟期间最好关掉不必要的中断或者至少避免在字节收发过程中插入耗时的中断处理。否则SCLK翻转时序被拉长虽然大多数时候不影响通信但在某些边界条件下可能触发DRDY误判。第二个是电源噪声。ADS1220是Δ-Σ ADC对电源纹波很敏感。模拟SPI的IO口翻转时如果电源走线不合理噪声会耦入模拟部分。建议模拟电源与数字电源之间加磁珠或π型滤波ADS1220的AVDD和DVDD也要各自就近放0.1μF去耦电容。第三个是初次上电的稳定时间。ADS1220从复位到稳定内部参考电压需要一定的建立时间。如果上电后立刻读写寄存器可能读到错误的配置。稳妥的流程是上电后延时200毫秒再开始初始化。如果是自己画板子DOUT/DRDY上的上拉电阻一定要留位置这个电阻不止是电平保障还能让DRDY中断的上升沿更干净。我见过好几块板子因为省了这颗电阻SPI读数据时偶发性地串入毛刺好不容易才定位到原因。软件模拟ADS1220驱动这件事原理不深贵在把时序和命令都理顺。只要寄存器配置正确、位序对了、CS和DRDY的状态时机抓准了即便不用硬件SPI这颗24位ADC也能跑得很稳。实际做产品时我已经把这套驱动从Cortex-M移植到了RISC-V内核的MCU上IO操作函数改了几行中间的协议层一行没动直接编译通过。这也算软件模拟方案带给我最大的回报了。本文还有配套的精品资源点击获取