ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ADS8698驱动开发实战:SPI通信与代码架构设计要点

ADS8698驱动开发实战:SPI通信与代码架构设计要点 简介在嵌入式高精度数据采集中SAR ADC是工业信号链的核心器件其工作原理决定了系统对时序、噪声和转换精度的要求。如何基于SPI接口完成寄存器配置与数据读取实现一套可移植的驱动代码是许多开发者的共性需求。本文从高精度采集、代码可移植性等基础技术概念出发介绍一种将应用层、芯片驱动层与平台抽象层分离的设计方法并结合实际调试经验梳理SPI帧结构解析、24位数据帧处理、初始化时序等关键环节。这样的架构设计不仅便于在STM32、GD32等不同平台间快速移植也简化了驱动代码的维护与复用。在工业变送器、电池组电压巡检等多通道模拟量采集场景中这套思路可有效缩短开发周期提升驱动代码的工程实用性。 前阵子整理硬盘翻出一个存了几年的驱动包ADS8698驱动代码.rar。这个包是我当年做多通道模拟量采集方案时留下来的先后用在工业变送器测试台和电池组电压巡检板两个项目上。最近又有朋友问这颗芯片怎么调正好把这套驱动代码的写法和调试中踩过的坑一次性整理出来给需要做ADS8698驱动开发的朋友一份能直接“抄作业”的参考。先说明一下ADS8698这颗芯片在国产替代方案满天飞的今天反而显得有点“老派”——它不会给你一堆花哨的外设就是把SAR ADC该做的事情做扎实18位分辨率、8通道多路输入、内置基准、可编程输入量程、SPI接口。你要搞定的事情也相对明确SPI通信、寄存器配置、数据解析和平台移植。下面我就围绕这四点把这套驱动代码从结构到细节完整拆开讲。1. 拿到ADS8698驱动代码包之后先弄懂这颗芯片的“脾气”1.1 芯片定位不是所有高精度采集都适合用ADS8698ADS8698是一颗18位逐次逼近型SARADC8通道多路复用输入内部集成可编程增益放大器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_WritePin和HAL_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并接收rxBuflen为字节数 */ 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_Set和CONVST_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 MOSISDOSPI数据输出从发主收SPI MISOCS片选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 0CPOL0CPHA0能跑但也有人一接上去就发现读回来的数据完全不对大概率就是时钟极性和相位的问题。我的做法是先看数据手册里SCLK和SDI/SDO的时序图确认“数据在哪个边沿被锁存”然后据此设置CPOL和CPHA。从我实际项目的经验来看这个驱动在多数平台上使用的是SPI Mode 1CPOL0CPHA1即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总线上同时存在两种帧一种是主机写入的命令帧一种是从机返回的数据帧。写命令和读数据可以出现在同一次片选事务里也可以分开。更常见的做法是拉低CS主机通过SDI发送“通道选择量程配置”命令同时SDO返回上一个命令帧对应的状态或数据拉高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µFRESET引脚用GPIO控制不要直接悬空CONVST引脚由GPIO驱动不要接死。如果REFIO引脚接了外部基准要确保基准源噪声足够低否则ADC的INL会被基准噪声吃掉。我在这两个项目里用的都是内置基准外围就几个电容简单可靠。5.2 上电初始化代码带校验的版本初始化函数不能只是简单地把寄存器写一遍我习惯在最后加一个寄存器回读校验。这样做的好处是如果SPI接线错误或芯片没正常复位初始化函数会直接报错而不是等数据采集阶段拿到一堆本文还有配套的精品资源点击获取
返回列表