GPIO模拟SPI驱动ADS8698:帧结构、时序与调试实践
简介基于STM32的模拟SPI驱动ADS869x高精度ADC程序源码面向需要采集模拟信号的嵌入式开发工程师适用于无硬件SPI或无合适引脚复用时的场景。ADS869x系列支持可编程双极输入范围、18位分辨率及多档采样率ADS8691/8695/8699集成模拟前端、过压保护与低漂移片上基准常用于测试测量和电池组监视。压缩包内共2个文件包含ADS869x.c与ADS869x.hC文件实现SPI时序、寄存器读写及低功耗模式配置头文件提供接口声明整体约4KB代码精简便于直接移植。程序已实际验证可行可帮助读者快速搭建基于ADS869x的数据采集驱动层理解模拟SPI时序构造与ADC控制流程。截至目前已有2717人学习浏览适合对SPI通信和SAR ADC驱动有兴趣的开发者参考交流。 前阵子做了一套工业测控设备的前端采集板主控是STM32F103板上资源其实挺紧张W5500占了SPI1串行Flash占了SPI2SD卡和一块TFT屏幕把剩下能用的SPI外设也瓜分完了。但现场测试节点还差8路模拟量而且信号范围比较野从零点儿几伏的传感器小信号到正负10V的调速器输出都有。翻了一圈器件库最后定了TI的ADS8698。这芯片一个很省事的特点是内置4.096V基准、支持双极性宽范围输入连前置调理电路都能省掉大半。问题是硬件SPI已经被占干净了只能走GPIO模拟SPI这条老路。这篇文章就记录一下我用STM32 GPIO模拟SPI驱动ADS8698的完整过程包括引脚规划、24位帧结构、时序细节、寄存器配置以及调试中遇到的那些读回全0、数据错位、边沿采样不清的坑。如果你也遇到硬件SPI不够用、还得再接一颗中速ADC的场景这篇应该能帮你少走不少弯路。1. ADS869x是什么一颗不用配基准源的8通道16位ADC1.1 这芯片适合什么场景ADS869x系列里面ADS8694是4通道ADS8698是8通道分辨率16位核心卖点不是跑得多快而是前端省事。它集成了4.096V低漂移基准源模拟输入范围可以通过硬件引脚或寄存器配置成多种档位最大能到±10.24V。也就是说工业现场常见的0~10V、±10V、4~20mA经过采样电阻转成的电压基本都能直接怼到芯片输入脚上不用在前面加一堆运放和钳位电路。我做项目时最看重的一点是它的输入阻抗特性模拟输入端内部有可编程增益放大器和输入保护结构可以直接接高阻信号源不用担心负载效应太明显。这对传感器直接输出的小信号尤其友好。当然实际接线时还是建议在输入端加RC低通滤波既能抗高频干扰也能抑制ADC内部的电荷注入回灌。1.2 几个关键参数速览参数项典型值说明分辨率16位ADS8695为14位选型时看清后缀通道数48694/ 88698单端输入公共地内部基准4.096V无需外部基准芯片模拟输入范围可配置最大±10.24V也可通过RANGE引脚硬件配置SPI帧格式24位8位命令 16位数据转换启动方式读操作自动触发每次读数据都会启动下一轮转换这里特别说一句ADS869x不是那种你拉一下CS、发个0x00就能直接拿数据的普通SPI ADC它的SPI协议有自己的帧结构后面我会专门拆开讲。如果你直接拿硬件SPI按16位字长去发大概率会得到一堆乱七八糟的数据。2. 为什么非要用模拟SPI硬件SPI被抢光之后的取舍2.1 模拟SPI和硬件SPI的差距到底在哪很多人一听到模拟SPI就皱眉觉得太Low、速度慢、时序不稳。这种印象在几十兆赫兹的高通量场景下是对的但放在ADS8698这种中速ADC上其实没那么严重。硬件SPI的优势是时钟由外设自动产生可以配合DMACPU几乎不参与一个字节发完硬件自己就把状态位置位了。缺点是引脚是固定的除非用引脚重映射否则很容易被其他外设占用。而且调试硬件SPI时如果遇到时序问题你还要去翻寄存器配置到底是CPOL不对还是CPHA不对出问题后定位链路更长。模拟SPI的优势就一条任意GPIO都能当SCLK、MOSI、MISO用。这在板子已经定稿、引脚资源枯竭的时候几乎是唯一解。它的缺点也很明显每个bit的翻转都要CPU亲自操作SCLK频率上不去而且如果有中断频繁打断时序可能会抖动。我这次选择模拟SPI还有一个实际原因ADS8698挂在板子上离主控不远SCLK跑个1MHz完全没问题而1MHz对这颗芯片来说绰绰有余后面会有具体估算。2.2 模拟SPI到底能不能跑够ADS869x的吞吐先算一笔账。ADS869x的一帧是24个SCLK也就是读一次数据至少要24个时钟周期。如果模拟SPI的SCLK稳定在1MHz那么一帧耗时24微秒换算下来每秒能读约4万次也就是理论单通道约40kSPS。如果做8通道轮询每个通道每秒能分到约5k次采样。对于工业现场的温度、压力、流量这类慢变信号5kSPS的通道采样率完全够用。而且ADS8698本身支持自动序列模式你只要配置好通道顺序每次读数据它会自动切到下一个通道省掉反复写通道选择寄存器的开销。所以结论很明确如果你的采样速率需求在每通道几千SPS以内模拟SPI完全顶得住。如果你要的是每通道几十上百kSPS那还是老老实实把硬件SPIDMA的通道让出来别在这上面较劲。3. 时序与帧结构GPIO模拟SPI怎么和ADS869x对上3.1 引脚规划和GPIO初始化ADS869x的SPI接口有几个关键引脚SCLK、SDI对应MOSI、SDO对应MISO、CS以及RVS转换完成指示。我用的引脚分配是STM32引脚对应信号模式PA5SCLK推挽输出PA7SDI/MOSI推挽输出PA6SDO/MISO浮空输入PA4CS推挽输出PB0RVS外部中断/输入初始化代码我用的是HAL库核心是这几步void ADS_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); // SCLK、MOSI、CS推挽输出默认拉高 GPIO_InitStruct.Pin GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // SDO输入 GPIO_InitStruct.Pin GPIO_PIN_6; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // RVS输入用于判断转换是否完成 GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); // 默认状态CS高、SCLK低 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); }这里有一个细节SDO引脚的上下拉我建议不要配置成上拉。因为SDO是芯片推挽输出脚读数据时它自己会拉高拉低如果外部再配上拉电阻电平判断通常没事但会稍微增加上升沿时间高速模拟SPI时不划算。3.2 24位帧读写函数核心代码ADS869x的SPI帧是固定的24位高8位是命令字节低16位是寄存器数据或保留位。主机在发送命令的过程中芯片会在SDO上同步把这帧对应的16位转换结果输出。所以写一个通用的24位读写函数就能同时完成发命令和收数据两件事。我用的代码长这样#define ADS_CS_L() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define ADS_CS_H() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) #define ADS_SCLK_H() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET) #define ADS_SCLK_L() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET) #define ADS_MOSI_H() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET) #define ADS_MOSI_L() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET) #define ADS_MISO_READ() HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_6) static uint32_t ADS_SPI_ReadWrite24(uint32_t txData) { uint32_t rxData 0; uint8_t i; ADS_CS_L(); for (i 0; i 24; i) { // 从最高位开始发送 if (txData (0x800000UL i)) ADS_MOSI_H(); else ADS_MOSI_L(); // SCLK上升沿 ADS_SCLK_H(); // 延时让信号稳定 delay_ns(200); // 在SCLK高电平期间采样MISO rxData 1; if (ADS_MISO_READ()) rxData | 1; // SCLK下降沿 ADS_SCLK_L(); delay_ns(200); } ADS_CS_H(); return rxData; }delay_ns在STM32上可以用for循环数空拍实现也可以在定时器里做一个忙等函数。我的经验是SPI时钟频率不需要精确到某个值只要SCLK高电平和低电平的持续时间足够长、且两边都稳定就行所以用简单的空循环延时完全没问题。这里有个容易犯迷糊的点采样位置。我在代码里把MISO的读取放在SCLK拉高之后、拉低之前。对于低速模拟SPI只要采样点不落在信号翻转边沿附近Mode 0和Mode 1的细微区别在这种速度下基本不影响结果。如果你用的是硬件SPI外设再去严格匹配CPOL和CPHA就行。3.3 上电唤醒与RVS引脚处理ADS869x上电后芯片处于掉电模式第一次把CS拉低并产生第一个SCLK边沿时芯片会被唤醒。我的初始化流程是void ADS_Init(void) { ADS_GPIO_Init(); HAL_Delay(50); // 等待电源稳定 // 发送一帧NOP唤醒芯片并丢弃返回数据 ADS_SPI_ReadWrite24(ADS_CMD_NOP); HAL_Delay(10); }ADS_CMD_NOP就是0x000011这只是约定好的命令字你可以把它当成不操作把当前转换结果给我的意思。第一次读返回的数据是不可用的直接扔掉。关于RVS引脚如果你用的是连续转换模式不接RVS也能跑每次读数据自带触发下一轮转换的效果。但如果你追求精确的转换周期控制或者想让数据更稳定建议把RVS接到STM32的外部中断上每次RVS跳变后再发起新一轮读取。RVS的具体电气行为不同型号略有差别建议看手册里Figure这样的时序图确认一下。4. 单通道读取和寄存器配置从最简案例到自动序列4.1 不写寄存器也能出数据如果只想快速出数据根本不用动任何寄存器。把RANGE引脚硬件接到对应电平选择好输入范围然后循环读就行uint16_t ADS_ReadData(void) { uint32_t frame ADS_SPI_ReadWrite24(ADS_CMD_NOP); return (uint16_t)(frame 0xFFFF); }这个函数每调用一次就会返回上一次转换的结果同时触发下一次转换。实测下来在1MHz SCLK下这个读函数稳定可靠数据不跳、不错位。这里要特别注意ADS869x在双极性模式下输出的是二进制补码格式。比如±10.24V量程下0V对应的读数是0x0000正满量程接近0x7FFF负满量程接近0x8000。如果你在单极性模式下用输出才是普通二进制格式。选型、配置之后先用一个已知电压源校准一下符号位别一上来就按无符号数处理。4.2 写寄存器配置输入范围和自动序列ADS869x的寄存器写操作同样走24位帧高8位是写命令和寄存器地址低16位是要写入的寄存器数据。比如要配置自动序列寄存器大致流程是#define ADS_CMD_WRITE_REG 0x00 // 具体命令位按手册定义 #define ADS_REG_AUTO_SEQ 0x03 #define ADS_REG_CONFIG 0x04 void ADS_WriteReg(uint8_t regAddr, uint16_t regData) { uint32_t frame ((uint32_t)regAddr 16) | regData; ADS_SPI_ReadWrite24(frame); }这里我不把具体寄存器位写死因为不同型号8694、8698甚至8695的寄存器和位定义有差别照抄网上代码容易踩坑。我的建议是打开对应型号的数据手册找到Register Map那一章先确认R/W位、寄存器地址、以及你想配置的位是哪些再填进上面的框架里。以ADS8698为例我当初配置了自动序列模式让芯片自动循环扫描8个通道不用每次手动改通道号。具体做法是把AUTO_SEQ寄存器配成使能自动序列通道0到通道7然后每次调用ADS_ReadData()拿到的就是当前通道的数据。至于当前读的是哪个通道可以在帧里带上通道ID信息或者按顺序手动记录读的次数。4.3 电压换算读出0xFFFF之后怎么办拿到16位原始码值之后最关键的一步是换算成实际电压。不同输入范围、不同工作模式换算公式不同。双极性模式下的通用公式是实际电压 ADC码值 / 32768 × 满量程电压比如配置成±10.24V量程读回码值是0x4000即16384那么电压就是16384 / 32768 × 10.24V约5.12V。如果读回0x8000-32768那就是-10.24V。如果你用了单极性范围公式会变成实际电压 ADC码值 / 65536 × 满量程电压这个换算看似简单但项目里最容易出的问题不是公式而是量程搞混。比如你把输入范围通过寄存器设成了±5.12V但代码里还按10.24V去换算结果所有电压都偏了一倍。建议在代码里把所有量程定义成宏或枚举并加一个setRange()函数统一管理别在多个地方硬编码。5. 实测排查读回全0、数据错位这类典型的SPI坑5.1 读回全0和全1的检查顺序我调试这套驱动时第一个碰到的问题就是读回全0。当时第一反应是芯片坏了后来逐一排查才发现是连线问题。这个坑很典型而且排查顺序很有讲究我总结的检查顺序是这样的。第一先量芯片供电和基准输出。ADS869x如果没正常上电SDO大概率拉不起来。第二查SDI和SDO有没有接反。ADS869x的SDI是输入、SDO是输出很多转接板丝印模糊接反后主控发出去的命令永远进不了芯片读回来的就是全0或全1。第三查CS有没有被拉低。如果CS悬空或没被正确控制芯片压根不会进入SPI通信状态。第四查SDO引脚是不是配成了输出模式。用HAL库初始化错误的话MISO读到的一直是引脚自身的输出状态。读回全1的情况通常是CS没拉低或者SDO引脚被外部上拉电阻拉高而芯片没有正常驱动它。确认CS波形能正常拉低后这个现象一般会消失。5.2 数据错位和偶发多一位的根因如果数据不是全0全1而是看起来有点对但偶尔错位那基本是时序边沿问题。我当时在逻辑分析仪上看到的波形是SDO输出的第一位数据比预期晚了一个SCLK周期导致整体数据右移了一位读出来的码值差了一倍。排查后确认是我在SCLK下降沿附近才去采样MISO而SDO的信号变化正好发生在下降沿附近采样点落在信号跳变的窗口期里。解决办法很简单把MISO采样放到SCLK高电平的中间位置。代码里我用的是SCLK拉高-延时-采样-拉低的顺序这样采样点稳定落在高电平中段不再吃边沿。还有一个容易踩的坑是CS时序。如果CS拉低之后立刻就开始第一个SCLK有些器件会不理解第一个字的边界。ADS869x对CS建立时间有一定要求我会在CS拉低后加一个极小的延时几百纳秒就够再开始推SCLK。帧结束拉高CS后也要等一小段时间再拉低发起下一帧给芯片一点内部处理时间。5.3 示波器和逻辑分析仪的使用技巧调试这类模拟SPI建议手边有一个逻辑分析仪哪怕是几十块钱的8通道USB逻辑分析仪也够用。接上SCLK、CS、SDO三根线抓一帧数据和ADS869x手册里的SPI时序图对比基本能一眼看出问题。我常用的抓取方式是把触发条件设置为CS下降沿抓一整个24位帧的波形。然后对照时序图数一下SCLK数量确认是不是24个上升沿再看SDO的第一位数据是出现在第几个上升沿之后最后检查一下数据位顺序是不是MSB先出。这三步都对了时序基本就没问题。还有个偏门但实用的技巧发送一个已知的命令帧去读寄存器比如读一个出厂默认值的寄存器把读回的值和手册里默认值对比能快速验证SPI链路是否通而不需要依赖模拟输入电压。5.4 关于驱动能力和电平匹配ADS869x的SDO输出不是强推挽如果你的STM32引脚上并了多个负载或者线上电容太大SDO的边沿会变缓。模拟SPI对这种缓边沿容忍度还行但也不能完全不管。我建议SDO线上不要加太长走线也不要在上面额外并联滤波电容。如果板子布局已经定了走线比较长可以把SCLK频率调低一点或者把采样点再往中间推一些。还有一个很多人会忽略的点电平匹配。ADS869x如果供电是3.3VSDO输出高电平也是3.3VSTM32能直接识别。但如果你用的是5V供电版本或主控是1.8V逻辑就必须加电平转换或选支持宽压的型号。我这次用的模块明确标了支持3.3V逻辑所以可以直接连。最后说几句实在话从硬件SPI被占满到最后用模拟SPI把ADS8698跑稳整个过程大概花了一个下午。模拟SPI这件事本身不复杂真正决定成败的是对帧结构和时序细节的理解。我后来在另一块板子上也遇到了同样的需求就吸取了这次经验一开始就预留了GPIO模拟SPI的接线位置代码也直接拷贝过来稍作修改就能跑省了不少事。如果你也在犹豫要不要用模拟SPI接这颗ADC我的建议是只要采样率需求在每通道几千SPS以内大胆用先把功能跑通再优化不迟。要是后续真的需要高吞吐再考虑把硬件SPI让出来也不亏毕竟模拟SPI版驱动代码改造成硬件SPI版本并不难。另外调试这类低速模拟接口真正省时间的关键就两条一是有个逻辑分析仪能看时序二是拿到芯片先翻数据手册的时序图。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →