简介:面向嵌入式开发者的 ADS8688 STM32 驱动代码包,针对 TI 公司 8 通道、16 位分辨率、最高 1Msps 的高速 ADC 芯片,解决 STM32 经由 SPI 完成通道选择、采样启动、转换结果读取与中断处理等关键问题。代码包以 C 语言实现,共 2 个文件,.c 源文件涵盖 SPI 引脚与时钟初始化、ADS8688 内部寄存器配置、多通道数据采集以及超时与通信错误处理逻辑;.h 头文件则给出接口声明、参数宏和寄存器地址定义,方便开发者直接移植到基于 HAL 或 LL 库的 STM32 工程。压缩包仅 3KB,结构精简、无多余依赖。目前已有 5577 人学习下载。代码对逐次逼近型 ADC 的时序控制、转换完成中断和低功耗管理模式进行了清晰的模块划分,既适合中小型数据采集系统快速集成,也可作为学习高速 ADC 驱动开发的参考模板,集成后即可支持多路模拟信号同时采样,提升项目开发效率。
1. 为什么需要一颗这样的ADC芯片
做工业数据采集或者精密仪器类的项目时,ST官方给每个外设都准备好了示例代码,但ADS8688这种外部ADC芯片的驱动程序就得自己去写。很多工程师一看到数据手册里几十页的寄存器描述就头疼,其实摸清楚它的脾气之后,写一个稳定可靠的驱动并不难。
这次分享的是一个在STM32平台上驱动ADS8688的完整实现。ADS8688是TI推出的16位、8通道、内置基准源的逐次逼近型ADC,最大采样率500kSPS,输入通道支持多种模拟输入范围,包括±10.24V、±5.12V、±2.56V以及0~10.24V等,通过寄存器可灵活配置。它最吸引人的地方在于输入通道可以直接耐受高达±20V的电压,这意味着前端不需要额外的精密运放做电平搬移,省掉一大块模拟电路设计成本。我在这套驱动里做了通道轮询、数据换算、SPI时序适配、DMA方式读取这类核心逻辑,还踩了不少时序上的坑,把这台机器的脾气摸了个透,写出来给后面要用的朋友做个参考。
这套代码适合正在做多通道模拟量采集、PLC模拟量输入模块、电机驱动器电流电压回采、电池测试设备等项目的工程师直接抄作业,也适合刚接触SPI接口ADC的嵌入式开发者用来理解“怎么让一颗外挂ADC和MCU高效配合”这件事。
2. 整体方案思路与外围电路设计
2.1 ADS8688工作模式怎么选
拿到这颗芯片第一件事不是写代码,而是先对方案。ADS8688的工作模式分硬件模式(Hardware Mode)和软件模式(Software Mode)两种,默认状态下如果用硬件模式,系统会有一个固定默认配置,但灵活性不够。我的方案选择用软件模式,核心诉求有两点:一是要能动态切换量程,二是希望通过寄存器读取芯片内部状态,方便后续做诊断。
软件模式下,ADS8688通过SPI接口接收命令帧来配置寄存器,命令帧格式是8位寄存器地址加8位数据。系统上电后,芯片默认按默认配置运行,默认输入范围大概是±10.24V,各通道使能状态默认也覆盖到了,但既然我们要写驱动,就要把配置流程完整走一遍。
选择软件模式还有个重要原因:ADS8688的复位行为是寄存器复位后芯片不会自动恢复到你想要的特定量程配置,必须由MCU主动下发配置命令。写驱动时把配置封装成函数,后续换量程、改通道顺序都方便。
2.2 STM32与ADS8688的硬件连接
ADS8688是标准SPI接口,但有个很重要的细节——它支持与1.8V到5V逻辑电平直接连接,所以和STM32相连非常省事。如果你用3.3V供电的STM32,直接连就行,不用做电平转换。
硬件连接可参考以下引脚映射:
| ADS8688引脚 | 功能说明 | 连接目标 | 备注 |
|---|---|---|---|
| SCLK | SPI时钟输入 | STM32 SPI_SCK | 时钟极性可配置 |
| SDI | 命令/数据输入 | STM32 SPI_MOSI | 用于写入命令帧 |
| SDO | 转换结果输出 | STM32 SPI_MISO | 读取转换数据 |
| CS | 片选信号 | STM32 GPIO/SPI_NSS | 低电平有效 |
| RST | 复位 | 任一GPIO | 低电平复位 |
| REFIO/REFCAP | 基准输入/输出 | 电容旁路 | 内部基准已启用 |
| DVDD/AVDD | 数字/模拟电源 | 3.3V/5V | 按数据手册要求 |
| RVS | 忙输出 | 可悬空或GPIO | 用于忙检测 |
我在实际调试中发现,CS信号尽量用普通GPIO软控,不要用SPI硬件的NSS自动控制,因为ADS8688要求SCLK空闲时CS不能被拉高打断。STM32的SPI硬件NSS在某些配置下会自动翻转,容易触发误操作,软件片选更可控。
2.3 供电与基准源设计经验
ADS8688内部集成基准源,但也支持外部基准输入。默认情况下内部基准启动后大概要十几毫秒稳定时间,所以驱动初始化时一定要给一段延时。基准引脚处要放一个至少1uF的低ESR电容,我在实际设计里放了10uF钽电容并联0.1uF陶瓷电容,读取数据全程没有出现基准抖动引起的码值跳变。
模拟供电AVDD和数字供电DVDD如果使用同一个电源轨,建议分别用磁珠隔离,减少数字开关噪声耦合到模拟端的风险。这个细节在ADC数据采集类项目里极其重要,单独分一路LDO给模拟供电是比较稳妥的做法。
3. 驱动代码核心实现与关键细节
3.1 底层SPI读写函数封装
STM32端使用HAL库做底层封装,SPI配置时需要注意时钟极性CPOL和时钟相位CPHA的匹配。查了ADS8688数据手册时序图,SCLK空闲状态为低电平,数据在SCLK上升沿被芯片采集,所以SPI模式应该选择模式1(CPOL=0,CPHA=1)。
下面给出一个可直接移植的SPI底层封装样例:
/* SPI句柄声明 */ extern SPI_HandleTypeDef hspi2; /* 片选控制 */ #define ADS8688_CS_LOW() HAL_GPIO_WritePin(ADS_CS_GPIO_Port, ADS_CS_Pin, GPIO_PIN_RESET) #define ADS8688_CS_HIGH() HAL_GPIO_WritePin(ADS_CS_GPIO_Port, ADS_CS_Pin, GPIO_PIN_SET) /* 写1字节 */ static void ADS8688_WriteByte(uint8_t data) { uint8_t txData = data; uint8_t rxData = 0; HAL_SPI_TransmitReceive(&hspi2, &txData, &rxData, 1, 10); } /* 读1字节 */ static uint8_t ADS8688_ReadByte(void) { uint8_t txData = 0x00; uint8_t rxData = 0; HAL_SPI_TransmitReceive(&hspi2, &txData, &rxData, 1, 10); return rxData; }这段代码看起来简单,但有两点需要注意:一是HAL_SPI_TransmitReceive超时时间不能太短,我遇到过用10ms正常,但系统主频提高后超时异常的情况,所以建议使用50ms以上或者改为阻塞式发送;二是如果SPI外设开启了DMA,需要确保DMA传输完成后再拉高CS,否则CS提前拉高会直接终止转换结果输出。
3.2 寄存器配置与命令帧结构
ADS8688的寄存器配置命令帧是16位:高8位是命令字,低8位是数据。其中命令字里又包含寄存器地址和读/写标志位。我在此处把常用的寄存器配置封装成了专门函数,便于读代码时一目了然。
/* 寄存器地址定义 */ #define ADS8688_REG_AUTO_SEQ 0x01 #define ADS8688_REG_CH_PWR_DOWN 0x02 #define ADS8688_REG_RANGE_SEL 0x03 #define ADS8688_REG_OTR_RANGE 0x04 #define ADS8688_REG_DEVICE_ID 0x05 /* 通用写寄存器 */ static void ADS8688_WriteReg(uint8_t regAddr, uint8_t data) { uint16_t cmd = 0; cmd = (0x80 | regAddr) << 8; /* 写命令,bit15=1 */ cmd |= data; ADS8688_CS_LOW(); ADS8688_WriteByte((cmd >> 8) & 0xFF); ADS8688_WriteByte(cmd & 0xFF); ADS8688_CS_HIGH(); }这里补充说明一下命令帧的位定义。ADS8688命令帧的D15为读/写控制位,读操作时该位为0,写操作时该位为1;D14~D8是寄存器地址;D7~D0是写入的数据。也就是写命令时命令字高字节是0x80 | regAddr,读命令时高字节是regAddr。
3.3 初始化流程完整实现
ADS8688上电之后,建议的初始化顺序是:上电延时→复位→写通道掉电寄存器→设置量程→配置自动扫描序列→写使能命令。这个顺序有讲究:如果先配置量程再复位,复位会把配置清掉,白干。
uint8_t ADS8688_Init(void) { uint8_t id = 0; /* 复位芯片 */ ADS8688_RST_LOW(); HAL_Delay(10); ADS8688_RST_HIGH(); HAL_Delay(20); /* 读取设备ID,验证SPI通信 */ id = ADS8688_ReadReg(ADS8688_REG_DEVICE_ID); if ((id & 0xF0) != 0xA0) { return 1; /* 通信异常 */ } /* 将所有通道设置为正常工作,不关断 */ ADS8688_WriteReg(ADS8688_REG_CH_PWR_DOWN, 0x00); /* 通道0~7量程配置为±10.24V */ ADS8688_WriteReg(ADS8688_REG_RANGE_SEL, 0x00); ADS8688_WriteReg(ADS8688_REG_OTR_RANGE, 0x00); /* 自动扫描使能 */ ADS8688_WriteReg(ADS8688_REG_AUTO_SEQ, 0x80); /* 主机触发模式使能 */ ADS8688_WriteReg(ADS8688_REG_AUTO_SEQ, 0xC0); return 0; }设备ID校验这块建议保留。我遇到过SPI引脚虚焊导致通信完全异常,如果没有这一步ID读取,后面所有寄存器配置都会失败,而且很难快速定位问题。另外量程配置的对应关系上,ADS8688的 range 寄存器每一组4位对应一个通道,具体对应关系为0000表示±10.24V,0001表示±5.12V,0010表示±2.56V,0011表示±1.28V,0100表示0~10.24V,0101表示0~5.12V。如果后续要多通道不同量程混用,就需要按位去设置对应通道的4位数据,不要把整字节直接写成0x00。
3.4 单通道读取与转换结果计算
驱动代码的核心功能是读取各通道的转换结果。读取数据的时序是:先发送一条带通道地址的读命令帧,芯片会开始转换,转换完成后在下一个16个SCLK周期从SDO引脚输出结果。我封装的单通道读取函数如下:
uint16_t ADS8688_ReadChannel(uint8_t ch) { uint8_t cmd = 0; uint16_t rawData = 0; /* 通道地址编码,bit7必须为0代表读命令 */ cmd = (ch & 0x07) << 4; ADS8688_CS_LOW(); /* 发送命令帧,第一个16位 SCLK */ ADS8688_WriteByte(cmd); ADS8688_WriteByte(0x00); /* 第二个16位 SCLK读取结果 */ rawData = ADS8688_ReadByte(); rawData <<= 8; rawData |= ADS8688_ReadByte(); ADS8688_CS_HIGH(); return rawData; }有一点一定要提醒:读取结果的第二个16位周期中,前4位是状态标志,后12位才是有效的转换数据的高12位,加上第三个周期的低4位才是完整16位结果。所以上述读取函数在第二个周期读回的字节其实只包含了高8位数据,低8位需要再读一个字节。具体而言,ADS8688在/CS拉低后,第一个16位周期写入命令,第二个16位周期输出上一次转换结果的高8位,第三个16位周期输出低8位。我第一次调试时只读了16位,结果数据完全不对,后来用逻辑分析仪抓了时序才反应过来。
所以完整的读取流程应该发送命令后,读取两个字节,其中第一个字节是数据高字节,第二个字节是数据低字节。
/* 更准确的读取方式 */ uint16_t ADS8688_ReadChannelEx(uint8_t ch) { uint8_t cmd = (ch & 0x07) << 4; uint16_t data = 0; ADS8688_CS_LOW(); ADS8688_WriteByte(cmd); ADS8688_WriteByte(0x00); /* 第一个字节为高字节 */ data = ADS8688_ReadByte(); data <<= 8; /* 第二个字节为低字节 */ data |= ADS8688_ReadByte(); ADS8688_CS_HIGH(); return data; }3.5 实际电压换算以及负数处理
数据读回来是裸码,必须换算成真实电压,这一步必须结合输入量程来算。ADS8688的输出编码默认是二进制补码,中心点0V对应的码值是0x0000,正满量程对应0x7FFF,负满量程对应0x8000。这里有个新手容易犯的错误,直接把裸码除以65536再乘以满量程电压,得到的结果只对单极性有效,对双极性输入(比如±10.24V)会算错一半。
对应±10.24V量程的换算公式:
float ADS8688_ConvertToVoltage(uint16_t rawData, float fsr) { int32_t signedData = 0; float voltage = 0.0f; /* 将16位无符号转为有符号 */ if (rawData & 0x8000) { signedData = (int32_t)rawData - 65536; } else { signedData = (int32_t)rawData; } /* 电压换算,fsr取±量程值,如10.24V */ voltage = ((float)signedData / 32768.0f) * fsr; return voltage; }补充一个细节:这里除数不是65536而是32768,因为补码数据在双极性模式下,满量程只占用一半的码值空间。我用±10.24V量程测试下来,换算出来的电压和万用表实测值误差在1~2mV以内,效果不错。若还需要更高的精度,可以考虑用更多位的浮点数运算,但在嵌入式环境里浮点误差对多数场景不敏感。
3.6 多通道轮询与DMA方式优化
如果只是单通道读取,上面的函数完全够用。但8个通道全量采集时,一个一个调用会占用较长CPU时间。我在驱动里加了自动扫描模式配合DMA的批量读取方案。配置自动扫描后,芯片会在每次命令后自动递增通道,SDO引脚会连续输出当前通道的转换结果。
#define ADS8688_CHANNEL_COUNT 8 uint16_t g_adcResults[ADS8688_CHANNEL_COUNT]; void ADS8688_ReadAllChannels_DMA(void) { uint8_t cmd = 0x00; HAL_GPIO_WritePin(ADS_CS_GPIO_Port, ADS_CS_Pin, GPIO_PIN_RESET); /* 发送自动扫描触发命令 */ HAL_SPI_Transmit(&hspi2, &cmd, 1, 10); /* 连续读取16个字节,每通道2字节 */ HAL_SPI_Receive(&hspi2, (uint8_t *)g_adcResults, 16, 50); HAL_GPIO_WritePin(ADS_CS_GPIO_Port, ADS_CS_Pin, GPIO_PIN_SET); }注意这里HAL_SPI_Receive接收的16字节里,相邻两个字节组成一个通道的原始转换值,顺序从通道0到通道7。在实际项目里,如果MCU主频不够高或者系统任务繁忙,建议把这部分放到DMA中断里做,避免阻塞主循环。另外自动扫描模式下转换结果的顺序与寄存器配置顺序有关,建议通道顺序固定,不要中途切换扫描序列,否则解析数据时容易错位。
4. 踩坑记录与调试经验
4.1 读取数据全是0xFF的排查
这个问题是SPI通信失败的典型症状。我排查时先测了CS引脚的波形,发现CS被拉低后马上又被拉高,怀疑是SPI时钟没有正常工作。用逻辑分析仪抓SCLK引脚,发现SCLK没有输出时钟脉冲。检查初始化代码后发现SPI句柄优先级配置太高,导致DMA请求把SCLK时钟配置挤掉了,调整外设优先级后恢复正常。
如果遇到读取数据始终是0,建议按这个顺序排查:先确认CS时序是否正确→再确认SCLK有无时钟→再确认SDI上的命令字波形是否正常→最后确认SDO上是否有数据返回。可以先用STM32的SPI回环测试验证SPI外设本身正常,再去查ADS8688这边的问题。
4.2 数据跳动大、低几位不稳定
读数在低位跳个不停,这个很多时候不是芯片问题,而是硬件布局或基准源问题。如果基准引脚滤波电容离芯片太远,ESL会变大,内部基准噪声就会耦合到转换结果。我是在基准引脚处用了一个0805封装的0.1uF陶瓷电容紧贴芯片放置,再在稍远位置放一个10uF钽电容,问题基本消失。
软件层面则可以把SPI时钟频率降低一些,确保满足芯片的SCLK最大频率要求,同时给转换结果加一个滑动平均滤波。实测对50Hz工频干扰有较明显改善。
4.3 双极性输入负电压换算错误的教训
我第一次测试时输入一个-5V的电压,读回数值0xC000,按无符号数除以65536再乘10.24,算出来是7.68V,这显然是错的。后来才意识到补码数据的特殊性。换算时建议先把rawData转成int16_t,再除以32768乘以量程。这一步如果不用补码而直接用无符号,负电压的值会完全不对。我在代码注释里特意写清楚这个坑,避免后续维护的同事再踩。
4.4 通道隔离度不足的处理
多通道采集时,如果前一个通道电压很高,后一个通道会有微小串扰。这主要是输入多路复用器的导通电阻和源阻抗不匹配引起的。我的做法是在每个模拟输入通道前加一个RC低通滤波,R取1kΩ,C取1nF,截止频率约159kHz,既能滤掉高频噪声,又能减小通道间的串扰。这个RC参数并不是固定的,如果信号频率很低,可以适当增大C值,进一步滤除干扰。
5. 关于这套驱动代码的后续扩展
ADS8688驱动代码的核心部分就是初始化、配置寄存器、读取转换结果、电压换算这几块。把这几部分封装好之后,后续做多通道数据记录、闭环控制、阈值报警这些功能,都是在这个基础上加应用层逻辑。我后来在这个驱动上跑了PID温度控制,8个通道的温度采样率做到每通道50kSPS,CPU负载完全扛得住。
如果在使用自带HAL库的STM32工程里,需要注意SPI中断和DMA中断的优先级设置,保证采样节拍的稳定性。另外如果计划使用FreeRTOS,建议把ADC读取任务放在高优先级任务里,并用信号量同步转换完成事件,避免多任务并发读写同一个SPI句柄导致数据错乱。
我现在还在计划给这套驱动加一个基于状态机的自校准流程,可以通过内置自检寄存器判断通道是否开路或短路,这个在工业现场特别有用。等实现稳定了,再写一篇完整的分享出来。
本文还有配套的精品资源,点击获取