ADS8698驱动开发实战:SPI通信与代码架构设计要点
2026/9/18 19:27:53 网站建设 项目流程

简介:在嵌入式高精度数据采集中,SAR ADC是工业信号链的核心器件,其工作原理决定了系统对时序、噪声和转换精度的要求。如何基于SPI接口完成寄存器配置与数据读取,实现一套可移植的驱动代码,是许多开发者的共性需求。本文从高精度采集、代码可移植性等基础技术概念出发,介绍一种将应用层、芯片驱动层与平台抽象层分离的设计方法,并结合实际调试经验,梳理SPI帧结构解析、24位数据帧处理、初始化时序等关键环节。这样的架构设计不仅便于在STM32、GD32等不同平台间快速移植,也简化了驱动代码的维护与复用。在工业变送器、电池组电压巡检等多通道模拟量采集场景中,这套思路可有效缩短开发周期,提升驱动代码的工程实用性。 前阵子整理硬盘,翻出一个存了几年的驱动包:ADS8698驱动代码.rar。这个包是我当年做多通道模拟量采集方案时留下来的,先后用在工业变送器测试台和电池组电压巡检板两个项目上。最近又有朋友问这颗芯片怎么调,正好把这套驱动代码的写法和调试中踩过的坑一次性整理出来,给需要做ADS8698驱动开发的朋友一份能直接“抄作业”的参考。

先说明一下,ADS8698这颗芯片在国产替代方案满天飞的今天,反而显得有点“老派”——它不会给你一堆花哨的外设,就是把SAR ADC该做的事情做扎实:18位分辨率、8通道多路输入、内置基准、可编程输入量程、SPI接口。你要搞定的事情也相对明确:SPI通信、寄存器配置、数据解析和平台移植。下面我就围绕这四点,把这套驱动代码从结构到细节完整拆开讲。

1. 拿到ADS8698驱动代码包之后:先弄懂这颗芯片的“脾气”

1.1 芯片定位:不是所有高精度采集都适合用ADS8698

ADS8698是一颗18位逐次逼近型(SAR)ADC,8通道多路复用输入,内部集成可编程增益放大器(PGA)和4.096V基准源,单电源供电,通过SPI接口与主控通信。它在TI的产品线里定位非常清楚:多通道、中等采样率、高分辨率、宽输入范围。

很多人看到“18位”就很兴奋,觉得这是一颗很高级的芯片。但在实际工程里,这颗芯片真正的价值在于它的输入范围设计。它的模拟前端可以直接处理最高±10.24V的双极性信号,也可以处理0~10.24V的单极性信号,而且这个量程是软件可配置的。这一点在做工业采集的时候太实用了,很多传感器输出是0~5V、0~10V或者±5V,如果用普通ADC,前面必须加运放做电平转换和衰减,而用ADS8698可以直接接进去,省掉一级信号调理电路,这对成本和PCB面积的帮助非常明显。

不过,用之前要认清一点:它的采样率不是强项。8通道连续巡检时,如果按几百kSPS的整片吞吐率来算,每个通道分到的有效采样率也就几十kSPS量级,所以它更适合“多通道、慢信号、高精度”的场景,比如温度、压力、流量、液位这种工业过程量采集。你要是想拿它做电力谐波分析或者振动信号采集,那就是选型方向的问题了。

1.2 驱动包该长什么样:从文件结构判断代码值不值得用

从网上下载的驱动代码包质量参差不齐。一个真正值得用的ADS8698驱动包,我认为至少要包含这几个部分:

ADS8698_driver/ ├── ads8698.h # 寄存器定义、错误码、API声明 ├── ads8698.c # 核心驱动:寄存器读写、通道配置、数据读取 ├── platform.h # 平台抽象层接口声明 ├── platform_stm32.c # 基于某个MCU平台的实现(SPI、GPIO、延时) ├── example/ │ ├── main_polling.c # 轮询模式示例 │ ├── main_interrupt.c # 中断模式示例 │ └── main_dma.c # DMA模式示例 └── README.md # 接线图、移植说明、版本记录

很多网上分享的代码包看起来文件名差不多,但打开之后全是在main函数里一堆HAL_GPIO_WritePinHAL_SPI_TransmitReceive的裸操作。这种代码不是不能跑,问题是换一颗MCU就得重写一遍,而且代码里混着业务逻辑和硬件操作,后期维护是灾难。

一个驱动包值不值得用,我有个快速的判断方法:看它有没有把“芯片操作”和“硬件操作”分开。芯片操作只关心寄存器怎么填、数据怎么解析,它调用的是一个抽象的SPI读写接口,而不是直接调用某个具体的HAL函数——这个分离做得好,移植到任何平台都只是改一个文件的事。

1.3 选型判断:什么情况下我会上ADS8698

结合我做过的两个项目,我整理了一个简单的选型判断标准,不一定绝对,但可以给正在犹豫的朋友参考:

需求特征是否适合ADS8698说明
通道数4~8路,不需要同步采样适合内部多路开关顺序切换,非同步采集
输入信号范围宽(0~5V、0~10V、±10V)很适合内置PGA可软件配置量程,省前端调理电路
采样率要求单通道100kSPS以上不太适合高速场景建议选逐通道并行或更高吞吐ADC
系统供电只有5V或3.3V很适合单电源供电,内部基准,外围元件很少
需要差分输入看情况ADS8698配置为伪差分输入,真差分需求需另选

2. 把驱动代码分层这件事讲透:为什么“好移植”不是靠堆代码

2.1 三层架构:应用层、芯片驱动层、平台抽象层

我在设计这套驱动的时候,坚持把代码分成三层,每一层有明确的边界。这不是什么高深的软件工程,纯粹是吃过亏之后长出来的经验。

第一层是应用层。这一层面向用户需求,提供类似ADS8698_ReadVoltage(channel)ADS8698_Init()这样的接口。调用者不需要知道寄存器地址,不需要关心SPI时序,他只关心“我调用这个函数能拿到多少伏的电压”。对于写业务逻辑的同事来说,这一层足够友好。

第二层是芯片驱动层。这一层实现ADS8698自身的所有逻辑:命令帧构造、寄存器读写、通道切换、量程配置、原始数据解析。这一层只通过平台抽象层的函数与外部通信,自己不直接操作任何MCU外设。

第三层是平台抽象层。这一层定义了芯片驱动层需要的一切底层资源:SPI收发函数、片选控制、转换启动脉冲、复位控制、延时函数、BUSY状态读取。

这样的分层带来的直接好处是:我在STM32F4上调通的代码,后来移植到GD32上,只改了platform文件,芯片驱动层一个字节都没动。

2.2 平台抽象层接口:其实只需要你实现5个函数

很多刚开始写驱动的朋友会把平台抽象想得很复杂,觉得要包一层RTOS、要搞DMA管理、要做超时机制。其实对于ADS8698这种接口简单的芯片,平台抽象层只要这5个函数就够了:

/* platform.h */ #ifndef __PLATFORM_H #define __PLATFORM_H #include <stdint.h> /* SPI读写:同时发送txBuf并接收rxBuf,len为字节数 */ void PLATFORM_SPI_TransmitReceive(uint8_t *txBuf, uint8_t *rxBuf, uint16_t len); /* 片选控制 */ void PLATFORM_CS_Set(uint8_t level); /* level: 0=拉低,1=拉高 */ void PLATFORM_CONVST_Pulse(void); /* 产生一个CONVST下降沿/脉冲 */ void PLATFORM_DelayUs(uint32_t us); /* BUSY引脚读取,返回1表示忙 */ uint8_t PLATFORM_BUSY_Read(void); #endif

就这5个函数。如果哪个平台要用中断模式做采样,可以再加一个BUSY外部中断回调注册函数,但核心功能上面5个足够了。

这里有个小经验:很多人会把CS_SetCONVST_Pulse混在一起用,其实没必要。ADS8698的片选是控制命令帧和数据帧的,CONVST是专门用来启动一次转换的,两个信号各管各的。把事务分清楚,代码逻辑会清晰很多。

2.3 头文件里的“干货”:寄存器定义和配置项枚举

芯片驱动层的头文件,我最看重的是定义是否清晰。这是从我实际项目中抽出来的关键定义,字段命名尽量贴近数据手册:

/* ads8698.h */ #ifndef __ADS8698_H #define __ADS8698_H #include <stdint.h> #include "platform.h" /* 寄存器地址 */ #define ADS8698_REG_NOOP 0x00u /* 空操作 */ #define ADS8698_REG_MAN_CH_CTRL 0x01u /* 手动通道选择 */ #define ADS8698_REG_CONFIG 0x04u /* 配置寄存器 */ #define ADS8698_REG_GAIN_CAL 0x06u /* 增益校准 */ #define ADS8698_REG_OFFSET_CAL 0x07u /* 偏移校准 */ /* 配置寄存器位定义(以手册寄存器映射为准,这里取的是工程中顺手用的几个) */ #define ADS8698_CFG_AUTO_MODE (0x04u) /* 自动通道切换 */ #define ADS8698_CFG_AUTO_RD (0x02u) /* 数据回读带通道号 */ /* 输入量程枚举 */ typedef enum { ADS8698_RANGE_BIP_10V24 = 0x00u, /* ±10.24V */ ADS8698_RANGE_BIP_5V12 = 0x01u, /* ±5.12V */ ADS8698_RANGE_BIP_2V56 = 0x02u, /* ±2.56V */ ADS8698_RANGE_UNI_10V24 = 0x04u, /* 0~10.24V */ ADS8698_RANGE_UNI_5V12 = 0x05u, /* 0~5.12V */ ADS8698_RANGE_UNI_2V56 = 0x06u /* 0~2.56V */ } ADS8698_Range_t; /* API声明 */ uint8_t ADS8698_Init(void); uint8_t ADS8698_WriteReg(uint8_t regAddr, uint16_t regData); uint8_t ADS8698_ReadReg(uint8_t regAddr, uint16_t *regData); uint8_t ADS8698_SelectChannel(uint8_t ch, ADS8698_Range_t range); uint8_t ADS8698_ReadChannel(uint8_t ch, int32_t *raw); float ADS8698_GetVoltage(uint8_t ch); #endif

这里有个细节值得说明:有些驱动会把寄存器地址直接写死在函数里,比如ADS8698_WriteReg(0x04, 0x02),读代码的人完全猜不到0x04和0x02是什么。我强烈建议把寄存器地址和配置位用宏定义出来,命名和手册保持一致。这个习惯在项目交接的时候特别值钱,接手的人不需要对照手册猜半天。

3. SPI底层打通:接线、时钟极性、全双工读数据的门道

3.1 引脚分配:先把这7根线接对

ADS8698的接口不算复杂,但每一根线都要接对,我把工程里实际用到的接线表整理在下面:

ADS8698引脚引脚作用接到MCU备注
SCLKSPI时钟SPI SCK建议加10Ω串联电阻
SDISPI数据输入(主发从收)SPI MOSI
SDOSPI数据输出(从发主收)SPI MISO
CS片选GPIO输出 / SPI NSS建议用软件GPIO控制
CONVST转换启动输入GPIO输出下降沿或脉冲触发,看手册
BUSY忙信号输出GPIO输入 / EXTI转换期间为高
RESET复位输入GPIO输出注意极性和脉宽要求

这里要特别说一句CS引脚:虽然很多MCU的SPI外设提供了硬件NSS功能,但我在这套驱动里强烈建议用普通GPIO软件控制。因为ADS8698的SPI帧长度是24位,而很多MCU的硬件SPI在NSS管理上对可变帧长支持得不好,尤其你还要在帧与帧之间插入通道切换命令,用软件CS反而更灵活、不容易出错。

3.2 SPI模式和时钟频率:不要上来就抄默认配置

ADS8698的SPI时序有一个很容易踩的坑:它的数据锁存边沿和普通SPI从机不完全一样。很多通用驱动默认用SPI Mode 0(CPOL=0,CPHA=0)能跑,但也有人一接上去就发现读回来的数据完全不对,大概率就是时钟极性和相位的问题。

我的做法是:先看数据手册里SCLK和SDI/SDO的时序图,确认“数据在哪个边沿被锁存”,然后据此设置CPOL和CPHA。从我实际项目的经验来看,这个驱动在多数平台上使用的是SPI Mode 1(CPOL=0,CPHA=1),即SCLK空闲为低,数据在上升沿被主机锁存。但我必须提醒你,不同使用场景、不同主控的SPI外设设计会有差异,最稳妥的办法是先把SCLK频率降下来,用逻辑分析仪抓一遍SDI和SDO的波形,确认数据采集窗口没有问题,再逐步提高时钟。

时钟频率方面,ADS8698支持较高的SCLK频率,但我不建议一上来就跑满。工程上的做法是先设1MHz跑通功能,再逐步提到4~8MHz验证波形质量。如果PCB布线不佳或者数字电源纹波偏大,SCLK跑太高会出现偶发数据错误,而且这种错误非常难排查——因为它不是每次都错,而是偶尔跳一个码。

3.3 全双工读取:只收不发,你连时钟都产生不了

ADS8698的数据读取有个容易忽视点:SPI是主从架构,主机必须发送时钟,从机才会输出数据。所以你想读SDO上的转换结果,MOSI线上不能闲着,你得同时发一些“空数据”比如0x00来产生SCLK时钟。这一点和I2C完全不同,如果之前只写过I2C器件驱动,到这里很容易懵。

我在platform层实现的SPI收发函数长这样(以STM32 HAL库为例):

/* platform_stm32.c */ #include "platform.h" #include "stm32f4xx_hal.h" extern SPI_HandleTypeDef hspi1; static void cs_low(void) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); } static void cs_high(void) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); } void PLATFORM_SPI_TransmitReceive(uint8_t *txBuf, uint8_t *rxBuf, uint16_t len) { cs_low(); HAL_SPI_TransmitReceive(&hspi1, txBuf, rxBuf, len, 10); cs_high(); } void PLATFORM_CONVST_Pulse(void) { HAL_GPIO_WritePin(CONVST_GPIO_Port, CONVST_Pin, GPIO_PIN_SET); PLATFORM_DelayUs(1); HAL_GPIO_WritePin(CONVST_GPIO_Port, CONVST_Pin, GPIO_PIN_RESET); PLATFORM_DelayUs(1); HAL_GPIO_WritePin(CONVST_GPIO_Port, CONVST_Pin, GPIO_PIN_SET); } void PLATFORM_DelayUs(uint32_t us) { /* 简单阻塞延时,实际工程可换成定时器或DWT */ for (volatile uint32_t i = 0; i < us * 10; i++) { __NOP(); } } uint8_t PLATFORM_BUSY_Read(void) { return HAL_GPIO_ReadPin(BUSY_GPIO_Port, BUSY_Pin); }

一个需要注意的细节:片选低有效之后,SPI收发执行期间CS必须保持低,收完之后再拉高。有些平台上的HAL_SPI_TransmitReceive函数内部有自己的时序要求,但整体事务边界仍然由CS控制。我在第一个平台上就是因为收完数据后CS拉高的时机不对,导致下一次命令丢失。

4. 24位数据帧背后的寄存器操作:指挥这颗ADC的正确姿势

4.1 命令帧和数据帧不是一回事

ADS8698这套接口有一个很有意思的设计:如果你想配置寄存器,比如切换通道、设置量程,你需要通过SDI发送一个16位的命令帧;而如果你想读转换结果,你不需要“发命令读取”,你只需要给SCLK时钟,SDO就会把上一次转换结果按24位帧返回。

这个设计带来的结果是:SPI总线上同时存在两种帧,一种是主机写入的命令帧,一种是从机返回的数据帧。写命令和读数据可以出现在同一次片选事务里,也可以分开。更常见的做法是:

  1. 拉低CS;
  2. 主机通过SDI发送“通道选择+量程配置”命令;
  3. 同时SDO返回上一个命令帧对应的状态或数据;
  4. 拉高CS。

也就是说,ADS8698在每帧都会从SDO回显一些信息,具体回显的是状态位、通道号还是转换结果,取决于你之前的配置。这个行为如果你不仔细读手册,第一次写驱动时会觉得好像“数据乱跳”,其实是帧结构没对齐。

4.2 读写寄存器的参考实现

我的驱动里,写寄存器的核心函数长这样:

/* ads8698.c */ #include "ads8698.h" static uint16_t build_command(uint8_t rw, uint8_t regAddr, uint16_t data) { /* 命令帧布局: [15] R/W 位,1=读寄存器,0=写寄存器 [14:12] 寄存器地址 [11:0] 数据字段(按寄存器不同位域生效) */ uint16_t cmd = 0; if (rw) { cmd |= (1u << 15); } cmd |= ((regAddr & 0x07u) << 12); cmd |= (data & 0x0FFFu); return cmd; } uint8_t ADS8698_WriteReg(uint8_t regAddr, uint16_t regData) { uint8_t txBuf[3]; uint8_t rxBuf[3]; uint16_t cmd = build_command(0, regAddr, regData); /* 将16位命令帧拆成两字节,再补一个字节的时钟用于接收回显 */ txBuf[0] = (uint8_t)(cmd >> 8); txBuf[1] = (uint8_t)(cmd & 0xFF); txBuf[2] = 0x00; PLATFORM_SPI_TransmitReceive(txBuf, rxBuf, 3); return 0; } uint8_t ADS8698_ReadReg(uint8_t regAddr, uint16_t *regData) { uint8_t txBuf[3]; uint8_t rxBuf[3]; uint16_t cmd = build_command(1, regAddr, 0); txBuf[0] = (uint8_t)(cmd >> 8); txBuf[1] = (uint8_t)(cmd & 0xFF); txBuf[2] = 0x00; PLATFORM_SPI_TransmitReceive(txBuf, rxBuf, 3); /* 读寄存器的返回数据通常在16位帧里 */ if (regData != NULL) { *regData = ((uint16_t)rxBuf[1] << 8) | rxBuf[2]; } return 0; }

你可能会问:为什么写寄存器要发3个字节,而不是2个字节?因为ADS8698的SDO在片选拉低期间也会持续输出,如果你不发第3个字节,你拿不到这个事务期间从机返回的信息。虽然写寄存器的时候你可能不关心回显,但如果你要保证每个事务都干净、帧对齐,第三字节是有意义的。实践里我也见过有人只发2个字节也能正常写寄存器,但后面读数据偶尔会错一位,最后定位下来就是帧长不一致导致的。

4.3 读取转换结果:18位数据在24位帧里的位置

ADS8698转换结果是18位。SPI数据帧是24位。那么18位数据到底放在24位的哪一部分,这是所有第一次写驱动的人都会纠结的问题,我当年也在这里卡了好几天。

我这边实测的波形和手册时序图对照之后,确认工程中可以按“数据位于24位帧的高18位”来解析,也就是读回的3个字节整体左移,取高18位作为有效数据。参考代码如下:

uint8_t ADS8698_ReadRawFrame(uint32_t *frame) { uint8_t txBuf[3] = {0x00, 0x00, 0x00}; uint8_t rxBuf[3] = {0x00, 0x00, 0x00}; PLATFORM_SPI_TransmitReceive(txBuf, rxBuf, 3); *frame = ((uint32_t)rxBuf[0] << 16) | ((uint32_t)rxBuf[1] << 8) | ((uint32_t)rxBuf[2]); return 0; } /* 解析24位帧,取出18位有符号原始值 */ int32_t extract_raw_18bit(uint32_t frame) { int32_t raw = (int32_t)(frame >> 6); /* 高18位对齐 */ if (raw & 0x20000) { /* bit17 为符号位 */ raw -= 0x40000; /* 扩展为int32补码 */ } return raw; }

转换成电压时要结合量程:12位有效精度算法很简单,满量程对应的补码是0x1FFFF(正满量程约131071)和0x20000(负满量程约-131072)。按双极性±10.24V量程来算,一个LSB对应:

LSB = 2 * 10.24V / 2^18 ≈ 78.125uV

所以某个通道的电压值可以这样算:

float ADS8698_GetVoltage(uint8_t ch) { int32_t raw = 0; float voltage = 0.0f; if (ADS8698_ReadChannel(ch, &raw) != 0) { return 0.0f; } switch (current_range[ch]) { case ADS8698_RANGE_BIP_10V24: voltage = (float)raw * 20.48f / 262144.0f; break; case ADS8698_RANGE_BIP_5V12: voltage = (float)raw * 10.24f / 262144.0f; break; case ADS8698_RANGE_UNI_10V24: voltage = (float)raw * 10.24f / 131072.0f; break; default: voltage = 0.0f; break; } return voltage; }

单极性量程的正向满量程为0x1FFFF,所以除数是2^18 / 2 = 2^17,也就是131072;而双极性量程的全摆幅是2 * 量程,对应2^18个码,所以除数是262144。这个换算关系看着简单,但确实有同事把单双极性的除数搞反过,最后电压读值差一倍,找了两天才发现是这里的问题。

5. 初始化流程:从复位到第一次出数据的完整时序

5.1 硬件最小系统:电源和基准引脚不能省

系统上电之前,先把ADS8698周边的电路检查一遍。这颗芯片虽然是单电源供电,但模拟部分对电源质量很敏感。我用的最小电路包括:

  • AVDD接5V,靠近引脚放1µF陶瓷电容和0.1µF陶瓷电容;
  • DVDD可以在3.3V或5V,取决于你的MCU电平,同样放去耦电容;
  • REFIO/REFCAP引脚按手册要求接旁路电容,我用的是1µF和0.1µF;
  • RESET引脚用GPIO控制,不要直接悬空;
  • CONVST引脚由GPIO驱动,不要接死。

如果REFIO引脚接了外部基准,要确保基准源噪声足够低,否则ADC的INL会被基准噪声吃掉。我在这两个项目里用的都是内置基准,外围就几个电容,简单可靠。

5.2 上电初始化代码:带校验的版本

初始化函数不能只是简单地把寄存器写一遍,我习惯在最后加一个寄存器回读校验。这样做的好处是:如果SPI接线错误或芯片没正常复位,初始化函数会直接报错,而不是等数据采集阶段拿到一堆

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询