用GPIO模拟SPI驱动ADS8688:多通道高精度ADC采集实战

用GPIO模拟SPI驱动ADS8688:多通道高精度ADC采集实战 简介面向嵌入式开发者的一份ADS8688驱动工程基于STM32G4系列MCU演示如何通过模拟SPI协议完成8通道ADC采样值的读取适用于数据采集、传感器接口与工业监测等场景。资源共172个文件压缩包约1.13MB主要包含C/H源码、Keil MDK-ARM工程文件、STM32CubeMX配置.ioc/.mxproject以及编译后的hex/axf/map文件覆盖从工程配置到生成固件的完整链路。目前已有1859人学习。工程中实现了模拟SPI时序解析、ADC寄存器配置、转换指令发送、转换完成检测与数据读取流程并配套核心驱动代码及清晰的目录结构Core/Hardware/Drivers便于直接参考、二次开发或移植到其他微控制器平台。 做多通道高精度数据采集时芯片内置ADC经常不够用。我这次碰到的是8路采集、需要较高精度、且输入可能是正负电压的场景最后选了TI的ADS8688——16位、8通道、双极性输入范围可选通信接口是SPI。考虑到主控资源的引脚布局我没有直接用硬件SPI而是用GPIO模拟SPI协议去读ADC采样值实测稳定可靠也踩了不少坑写出来给大家参考。1. 项目背景与方案选型1.1 为什么不用单片机内置ADC很多MCU自带ADC比如STM32F103的12位ADC采样速度也不慢常规场合确实够用。但这个项目有几个硬性条件内置ADC扛不住第一通道数要求。板子上需要同时监测8路模拟输入内置ADC多路扫描虽然也能做但通道切换和结果管理比较繁琐而且一路一路切换扫描时多路信号做不到真正同步采集。第二精度要求。12位分辨率在很多传感器信号调理场景下不够尤其是弱信号经过放大之后量化噪声会直接影响控制精度。第三输入范围。内置ADC一般只能采03.3V或05V的单极性信号但这个项目需要测量正负电压输入必须在前面加分压抬升电路。与其在模拟前端做一堆电平转换不如直接选一个支持双极性输入的ADC。ADS8688正好满足这些需求。它是一颗16位、8通道、支持多档双极性输入的ADC芯片软件可配置输入范围内部自带基准源和输入钳位保护基本一块芯片就把模拟前端的事情全干完了省掉了大量外围电路。1.2 模拟SPI与硬件SPI的取舍SPI是ADC芯片最常见的通信接口ADS8688也不例外。常规做法是直接用MCU的硬件SPI外设速度快、不占CPU为什么我还要用GPIO去模拟最直接的原因是引脚冲突。项目里MCU的硬件SPI引脚被一个TFT屏幕占用了如果为ADC再开一路硬件SPI就得用SPI2或者复用引脚重新布线很麻烦。软件模拟SPI则灵活得多任意四个GPIO就能拼出一路SPI想放哪个引脚就放哪个引脚PCB布局好安排。另一个原因是调试方便。模拟SPI的时序完全由自己控制哪一步不对直接看代码就能定位不像硬件SPI那样还要查寄存器配置、查引脚复用关系。特别是ADS8688这类芯片命令帧格式和时序要求比较细用模拟SPI调试一次就能把整个通信机制吃透后续换到硬件SPI也顺手。模拟SPI的缺点是速度上不去还要占用CPU。但ADS8688本身是逐次逼近型ADC单次转换最快也要几微秒我们的采样率需求也只有每秒200次左右模拟SPI完全够用。IO口没有电平转换问题主控3.3V和芯片供电完全兼容信号直连就行。2. ADS8688通信原理拆解2.1 芯片关键参数速览先简单过一遍ADS8688的核心信息方便后面理解代码实现。下面是这个项目里最常用的几个参数参数数值/说明分辨率16位通道数8通道单端输入输入范围软件可配置支持±10.24V、±5.12V、±2.56V、010.24V等内部基准4.096V精度0.1%SPI接口最高时钟约16MHz兼容3.3V/5V逻辑转换方式逐次逼近型典型转换时间4V/µs采样率下约2µs量级项目中输入信号是传感器调理后的±10V电压所以我把输入范围配置到±10.24V档。这档最大输入范围16位分辨率换算下来每LSB大约0.3125mV足够满足系统精度要求。ADS8688的SPI通信是标准的3线SPI加片选信号SCLK、DIN、DOUT、CS。CS低有效每次通信从CS拉低开始SCLK产生时钟脉冲主机通过DIN发送命令芯片通过DOUT返回数据SCLK结束后CS拉高一次帧传输完成。需要注意的是ADS8688的SPI是全双工设计主机发送命令和接收数据是同时进行的。也就是说发送一条命令帧的过程中芯片会同时从DOUT脚把之前转换好的数据或状态信息推出来。理解这一点非常关键后面代码里会体现。2.2 命令帧结构与读写时序ADS8688的寄存器操作通过16位命令帧完成。命令帧的最高位是读写标志接下来是寄存器地址或通道配置字段不同数据手册版本对位段的定义略有差异但整体结构都包含R/W标志、通道选择、配置数据这几部分。读取ADC转换结果的流程是先发送命令配置目标通道并启动转换转换完成后再发一帧NOP空操作命令同时把16位转换结果读回来。如果采用自动通道扫描模式芯片会按顺序把多个通道的结果放在数据寄存器里主机连续发NOP就能依次读出省去每次切换通道的繁琐。实际使用中读数据的时序是这样的CS拉低SCLK先处于低电平在SCLK上升沿前设置好DIN数据产生16个SCLK脉冲在每个上升沿由主机移出1位命令数据同时从DOUT读入1位数据16个时钟结束后CS拉高SCLK空闲时是低电平数据在上升沿被采样这是SPI模式的一种典型配置。模拟SPI实现时我们只需要按这个节奏“拉高SCLK→读数据→拉低SCLK→写数据”循环16次就行。2.3 手动通道切换与自动扫描ADS8688支持两种工作方式手动通道选择和自动扫描。手动模式适合需要精确控制采样顺序的场景。主控每次先写入一条配置命令指定通道等转换完成后再读结果。缺点是每采一个通道都要发两帧数据通信开销大一倍。自动扫描模式更省心。芯片内部会自动遍历已配置的通道主控只需要周期性发送读命令就能依次获得每个通道的转换结果。这种模式适合多通道连续监测。我的项目里8个通道都需要周期性采集但周期要求不高我选择了手动模式主要是代码逻辑直白出了问题容易排查。实际工程里如果对同步性要求高建议上自动扫描结合中断或DMA来实现。3. 基于HAL库的模拟SPI实现3.1 引脚分配与初始化我用的主控是STM32F407开发环境是Keil HAL库。四根通信线全部接到普通GPIO上没有做特殊复用。信号GPIO方向说明SCLKPB0输出SPI时钟DINPB1输出主机发送命令DOUTPB2输入读取ADC数据CSPB3输出片选低有效初始化代码很简单把三个输出脚配置为推挽输出即可输入脚注意设置为上拉输入避免浮空误读void ADS8688_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); // SCLK / DIN / CS 配置为推挽输出 GPIO_InitStruct.Pin GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_3; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); // DOUT 配置为上拉输入 GPIO_InitStruct.Pin GPIO_PIN_2; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); // 默认状态CS高、SCLK低、DIN低 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET); }GPIO速度选LOW就够了因为模拟SPI时钟频率本身不高太高的GPIO翻转速度反而容易引入毛刺。3.2 模拟SPI读写核心函数模拟SPI的核心就是一个16位的数据收发函数。ADS8688的命令帧和数据都是16位的所以直接写一个ADS8688_SPI_Transfer16统一处理发送和接收。uint16_t ADS8688_SPI_Transfer16(uint16_t txData) { uint16_t rxData 0; for (uint8_t i 0; i 16; i) { // SCLK拉低准备发送数据位 ADS_SCLK_LOW(); // 根据当前发送位设置DIN if (txData (0x8000 i)) ADS_DIN_HIGH(); else ADS_DIN_LOW(); // 数据建立时间 delay_us(1); // SCLK拉高上升沿芯片锁存DIN同时DOUT更新数据 ADS_SCLK_HIGH(); delay_us(1); // 读取DOUT注意是读当前这一位 if (ADS_DOUT_READ()) rxData | (0x8000 i); // SCLK拉低准备下一个时钟周期 ADS_SCLK_LOW(); delay_us(1); } return rxData; }很多初学者容易犯一个错误在第16个时钟结束后直接拉高CS。实际调试中发现如果最后一个SCLK下降沿后立即拉高CSDOUT上可能还有最后一位数据没稳定容易读到错误值。稳妥的做法是在第16位读取完成后让SCLK保持低电平约1µs再拉高CS确保数据完全稳定。延时函数我用的是delay_us(1)模拟SPI单时钟周期大约4µs对应SCLK频率在250kHz左右对ADS8688来说绰绰有余。实际中如果你觉得慢可以逐步减小延时但不要一上来就追求高速先把功能调通再优化速度。3.3 完整读取流程读取某个通道的ADC值按下面流程操作uint16_t ADS8688_ReadChannel(uint8_t ch) { uint16_t cmd 0x0000; uint16_t result 0; // 构造命令帧读写标志 通道地址 配置数据 // 具体位段按数据手册定义填写 cmd (0x8000) | ((ch 0x07) 12); ADS_CS_LOW(); // 发送命令同时丢弃接收到的无效数据 ADS8688_SPI_Transfer16(cmd); // 发送NOP读取转换结果 result ADS8688_SPI_Transfer16(0x0000); ADS_CS_HIGH(); return result; }这个函数在每次调用时都完成一次“配置通道读取结果”的完整操作。主循环中依次调用8次即可完成8个通道的采集for (uint8_t ch 0; ch 8; ch) { adc_val[ch] ADS8688_ReadChannel(ch); delay_ms(5); // 确保转换完成具体延时根据采样率调整 }读回的16位数据是原始二进制补码格式需要根据输入范围做换算。以±10.24V档为例满量程对应的数字范围是0x0000到0xFFFF0V对应0x8000附近实际电压值换算公式为float voltage (int16_t)adc_val[ch] * 10.24f / 32768.0f;注意这里先把uint16_t转成int16_t因为芯片输出的是有符号二进制补码直接按无符号数算出来的结果会完全不对。4. 采样数据处理滤波与校准4.1 ADC值起伏怎么办模拟SPI把数据读回来了不代表直接用就行。实际项目中ADC原始采样值往往带有噪声尤其是电机启停、开关电源负载突变这类场景采集到的数据会在真实值附近上下跳动。这种跳动的来源很复杂电源纹波、参考电压波动、地线阻抗、外部电磁干扰每一项都可能贡献几个LSB到十几个LSB的噪声。处理噪声最直接的方法是软件滤波。我常用的是滑动平均滤波维护一个长度为N的采样缓冲区每次读到的值放入队尾同时移除队首对缓冲区所有值求平均作为最终结果。N取8到16比较合适太大响应变慢太小滤波效果不明显。#define FILTER_LEN 8 uint32_t filter_buf[FILTER_LEN]; uint8_t filter_idx 0; uint32_t filter_sum 0; uint32_t ADC_Filter(uint32_t new_value) { filter_sum - filter_buf[filter_idx]; filter_buf[filter_idx] new_value; filter_sum new_value; filter_idx (filter_idx 1) % FILTER_LEN; return filter_sum / FILTER_LEN; }滑动平均是低通滤波的一种简单形式对高频噪声抑制效果明显而且实现简单、CPU开销极低。如果你的信号对实时性要求高可以考虑用一阶低通滤波y alpha * (x - y)调节alpha参数就能权衡平滑度和响应速度。4.2 外部ADC三点校准滤波解决了随机噪声但不能解决系统偏差。比如分压电阻精度不够、内部基准存在非线性这些都会导致读数系统性偏离真实值。对这个项目我做了三点校准效果非常好。校准的原理是标准ADC校准法给ADC输入几个已知电压建立“实际电压-ADC读数”的映射关系然后用这个映射关系去校正实际测量结果。为什么是三点而不是两点因为ADC的传递特性存在非线性两点线性校正在中间段误差大三点校准可以做分段线性校正把非线性误差也一起消除。具体做法分三步准备一个精确的直流电源或者高精度电压源比如ADR系列基准芯片配合精密电位器产生三个已知电压点覆盖量程的低、中、高比如-10V、0V、10V在每个电压点连续采集64次取平均得到对应的ADC原始值把三个点分成两段计算每段的斜率和截距存入系统的校准参数区typedef struct { int32_t raw; float volt; } CalPoint; CalPoint cal_pts[3] { {-32768, -10.24}, {0, 0}, {32767, 10.24} }; float ADC_ApplyCalibration(int16_t raw) { // 分段线性插值 if (raw cal_pts[1].raw) { // 在低段 float k (cal_pts[1].volt - cal_pts[0].volt) / (cal_pts[1].raw - cal_pts[0].raw); return cal_pts[0].volt k * (raw - cal_pts[0].raw); } else { // 在高段 float k (cal_pts[2].volt - cal_pts[1].volt) / (cal_pts[2].raw - cal_pts[1].raw); return cal_pts[1].volt k * (raw - cal_pts[1].raw); } }实际项目中我用的校准点不完全是理论值而是用万用表实测的电压值去填充这样做能彻底消除整个信号链路包括前端分压、基准源、系统偏移的累积误差。校准后的数据精度从原来的±0.5%以内提升到了±0.1%以内效果显著。5. 常见问题与排查技巧实录5.1 问题速查表这个项目从零调试到稳定运行大概花了一天时间中间遇到了几个典型问题整理成表格供大家参考现象可能原因排查思路读回数据全是0CS时序问题或DIN配置成输入用示波器看CS、SCLK波形确认CS低电平期间有16个完整时钟读回数据全是0xFFFFDOUT引脚配置错误或CS悬空检查DOUT对应GPIO是否配置为输入模式确认CS已拉低数据跳动幅度大电源纹波、参考地不稳检查供电ADC和主控共地必要时加去耦电容高低字节反了位序配置错误检查命令帧是MSB还是LSB优先ADS8688是MSB优先通道值错乱命令帧通道地址位移错误打印命令帧二进制内容逐位核对数据手册采集速度比预期慢SCLK频率过低或延时过长逐步减小delay_us时间观察波形确认SCLK频率5.2 调试心得最后分享几条我实战中的体会。调试SPI通信首推逻辑分析仪。几十块钱的USB逻辑分析仪就能很清楚地看到CS、SCLK、DIN、DOUT四路波形比示波器好用得多。把抓到的数据帧和代码里发送的命令帧做对比问题几乎瞬间定位。没有逻辑分析仪的话至少用示波器确认SCLK和CS的时序关系重点看CS拉低后是否有完整的16个SCLK脉冲。还有一个容易被忽略的细节上电后不要立刻发读命令。ADS8688有一个内部上电复位和启动时间数据手册上写着需要等待几毫秒。我在代码初始化里加了一个5ms延时如果省略这个延时经常会出现第一次读到的数据全是垃圾的情况。另外如果用了杜邦线连接ADC芯片和主控板注意杜邦线的信号完整性。杜邦线本身相当于一小段天线SCLK频率高一点波形就严重失真。我是把模拟SPI的时钟频率压到100kHz级别同时把杜邦线尽量缩短必要时用屏蔽线代替杜邦线问题就消失了。最后提醒一点不同厂家的ADC芯片即使是相同的SPI物理接口命令帧格式也完全不同调试前花十分钟把数据手册的时序图和寄存器说明看明白能省一晚上的排查时间。ADS8688的手册写得还算清楚特别留意它那几页关于命令帧位段的描述代码里的移位操作要和之一一对应。本文还有配套的精品资源点击获取