ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

工业八路±10V采集:ADS8688软SPI与16位ADC实现

工业八路±10V采集:ADS8688软SPI与16位ADC实现

简介:围绕ADS8688八通道高精度ADC与模拟SPI通信展开的嵌入式实践资料,面向需要将模拟采样接入微控制器、掌握时序级驱动编写的开发者与学习者。资源包共172个文件,约1.13MB,包含大量C/H源文件、编译中间文件与工程配置,另有少量TXT说明、HTML页面及Keil工程文件,覆盖驱动代码、核心程序、硬件描述与ARM工程配置等模块,便于直接查阅和二次移植。已有1873人学习下载。内容重点呈现不依赖硬件SPI外设、通过GPIO模拟时钟与数据线完成命令发送、转换等待和采样值读取的思路,并配套ADS8688初始化、通道与增益配置、数据处理及STM32G4 HAL相关工程结构,可帮助读者理解Σ-Δ ADC的访问流程、排查时序问题并搭建可复用的采集方案。

1. 八路 ±10V 信号压在一颗 16 位 ADC 上:ADS8688 软 SPI 采样方案落地

工业板子上经常是这么个局面:现场八路模拟量要同时采,信号摆幅动辄 ±10V,而主控的硬件 SPI 早被板载 Flash 占死了,剩下的只有几个空闲 GPIO。ADS8688 正好是为这种场景设计的——8 通道、16 位、单通道 500 kSPS、内置 4.096V 基准、±10.24V 输入范围,前端不用再做衰减就能直接扛工业信号。剩下唯一的问题,是怎么用几根普通 IO 把它的模拟 SPI 协议(bit-bang 软 SPI)逐位模拟出来。这篇拆的就是这份 ADS8688 采样工程:从引脚接法、32 位帧结构,到逐位模拟 SCLK 的时序细节,再落到 8 通道自动扫描和码值转电压。它适合手里已经有 Cortex-M 板子、要给数据采集卡或控制器补模拟量入口的开发者;也适合硬件 SPI 被占用、又不想改板的人。

2. 硬件与协议底座:ADS8688 的引脚、时序和寄存器地图

软 SPI 写不出来,十次里有八次不是代码问题,是对从机时序和帧结构的理解有偏差。ADS8688 不是一个“拉 CS 就吐数据”的傻瓜 ADC,它内部有命令寄存器、程序寄存器、序列控制器,一条帧里同时发生着“发命令”和“收数据”两件事。把这些先捋清楚,后面的 GPIO 翻转才有意义。

2.1 引脚与电源:AVDD、DVDD、REFIO 三处都能让片子装死

ADS8688 的供电是分开的:AVDD 是模拟电源(常见 5V),DVDD 是数字接口电源(3.3V 或 5V),REFIO/REFGND 是基准引脚。内部 4.096V 基准默认使能,REFIO 上一般要挂 10µF 钽电容加 0.1µF 瓷片做去耦,这颗电容省掉,读出来的数据会带着几十个码的慢漂,很难查。

接口侧一共五根线:CS、SCLK、SDI、SDO,加上可选的 RST/DAISY。RST 是低有效复位,如果板上没接,就只能靠软件命令复位,初始化时那条0x8500就不能省。DAISY 用于菊花链,单片刻不需要管它,接法上要把 FEATURE_SEL 对应的位保持默认。

电平匹配是最容易被忽略的一处。DVDD 接 3.3V 时,SDO 输出 3.3V 电平,直连 Cortex-M 的 IO 没问题;但如果 DVDD 接了 5V(有些人为了兼容老设备这么干),SDO 会把 5V 灌进主控的输入脚,短时间能跑,时间长了 IO 就废了。这种情况要么改 DVDD 到 3.3V,要么在 SDO 上串 1kΩ 再加个 3.3V 钳位。

提示:SCLK 空闲电平必须是低。软 SPI 初始化的最后一句一定是把 SCLK 拉低,否则第一帧的上升沿从机看不见,读回来的数据会整体右移一位。

2.2 帧结构:一条 16 位帧里发生的三件事

ADS8688 走的是标准全双工 SPI,一次 16 个时钟算一帧。CS 下降沿启动一帧,SCLK 上升沿从机把下一位数据摆到 SDO 上,下降沿从机采样 SDI 上的位,第 16 个时钟之后 CS 拉高结束。这个时序对应 SPI 模式 1(CPOL=0,CPHA=1),软 SPI 里控制好“先摆数据、再打时钟、在高电平期间采样”就能对上。

更关键的是帧的语义。发送的 16 位不是数据而是命令字,接收的 16 位才是从机的回应。命令字大致分三类:

命令类型命令字形态后面跟什么
单帧命令NO_OP0x0000、STDBY0x8200、PWR_DN0x8300、RST0x8500、AUTO_RST0xA000无,帧结束即生效
手动通道0xC000 | (ch << 10),CH0 是 0xC000,CH7 是 0xDC00下一帧读出该通道转换结果
寄存器访问读(addr << 8);写0x2000 | (addr << 8)第二个 16 位是数据(读时是回应,写时是要写进去的值)

寄存器访问是 32 位一帧:前 16 位给命令,后 16 位给数据,中间 CS 不能抬。读寄存器时,第一个 16 位从 SDO 上移出来的是上一帧的残留,别当数据用;第二个 16 位才是真正的寄存器值。这一点在调试时特别容易误判——很多人第一次读 AUTO_SEQ_EN,看到第一个字是 0x0000 就以为通信挂了,其实答案在下一帧。

注意:命令字常量的位定义按手册 Command Register 那张表抄,不同修订版的 datasheet 排版有差异,抄完先核对一遍再烧板子。命令字错一个 bit,读回来的就永远是 0xFFFF。

2.3 寄存器地图:四个真正需要动的地址

ADS8688 的程序寄存器有十几个,但一份采样工程真正要写的不超过四个:量程、通道上电、通道使能、特性开关。剩下的 ALARM 阈值、GPO 控制之类,用不上就先别碰,写错反而把片子搞进奇怪状态。

地址名称位宽作用
0x01AUTO_SEQ_EN8bit0~bit7 对应 CH0~CH7,置 1 参与自动扫描
0x02CH_PWR_DN8通道掉电控制,置 1 掉电,全 0 为全部上电
0x03FEATURE_SEL8菊花链、ALARM、GPO 等特性开关
0x05~0x0CRANGE_SEL_REG4CH0~CH7 各自的输入量程

量程这个寄存器值得单独记一下,因为它是“码值换算”的唯一依据,写错了电压就全错:

RANGE_SEL 值输入范围输出编码
0x00±10.24V双极性补码
0x01±5.12V双极性补码
0x02±2.56V双极性补码
0x050~10.24V单极性直码
0x060~5.12V单极性直码
0x090~2.56V单极性直码

双极性档位输出的是二进制补码,负电压读出来是一个很大的无符号数(比如 -1V 在 ±10.24V 档下是 0xFC00 附近),必须强转int16_t再算;单极性档位是直码,除数用 65536 而不是 32768。这两件事混在一起,是后面第 5 章那个“电压换算对不上”的根源。

3. 模拟 SPI 层实现:用 GPIO 把 16 位时序抠出来

软 SPI 的代码其实很短,短到很多人写完觉得“这不就完了”,然后卡在数据不对上。真正的工程量在分层:把和 MCU 相关的引脚操作全部关进一个头文件,把时序和协议分成两层,后面换芯片、换引脚、换主频,改动量能压到几十行。

3.1 引脚抽象层:把换 MCU 的成本压到一个头文件

先来看ads8688_port.h里这部分。它只做一件事:用宏把四根线的操作映射到具体寄存器。这里用 BSRR/BRR 而不是“读-改-写”的GPIO_SetBits,原因是软 SPI 的翻转频率很高,读改写会多出一次读端口的动作,两个引脚在同一个端口时还可能出现竞态。

/* ads8688_port.h —— 换 MCU 只改这个文件 */ #include "stm32f10x.h" #define ADS_PORT GPIOA #define ADS_CS_PIN GPIO_Pin_4 #define ADS_SCLK_PIN GPIO_Pin_5 #define ADS_MOSI_PIN GPIO_Pin_7 #define ADS_MISO_PIN GPIO_Pin_6 /* BSRR 置位、BRR 复位,单条指令完成,不读端口 */ #define ADS_CS_LOW() (ADS_PORT->BRR = ADS_CS_PIN) #define ADS_CS_HIGH() (ADS_PORT->BSRR = ADS_CS_PIN) #define ADS_SCLK_LOW() (ADS_PORT->BRR = ADS_SCLK_PIN) #define ADS_SCLK_HIGH() (ADS_PORT->BSRR = ADS_SCLK_PIN) #define ADS_MOSI_LOW() (ADS_PORT->BRR = ADS_MOSI_PIN) #define ADS_MOSI_HIGH() (ADS_PORT->BSRR = ADS_MOSI_PIN) #define ADS_MISO_READ() ((ADS_PORT->IDR & ADS_MISO_PIN) ? 1 : 0) /* SCLK 半周期延时,1MHz 对应 500ns */ static inline void ads_delay(void) { __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); }

ADS_MISO_READ读的是 IDR,返回 0/1 而不是原始位掩码,是为了让后面的循环里不用再写移位判断,编译器也能把它优化成一条测试指令。ads_delay里的八个__NOP在 72MHz 主频下大约 100ns 量级,实际半周期不够就往上加,或者干脆换成一个 DWT 周期计数器做精确延时——主频一变,空指令的时长就漂了,这是软 SPI 移植时最常见的翻车点。

初始化函数除了配 GPIO,还有两个必须做的动作:MISO 配成上拉输入(从机在 CS 高时 SDO 是高阻,没上拉读出来就是随机值),以及把 CS 拉高、SCLK 拉低,先把总线摆到空闲态再上电配置芯片。

static void ads_port_init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin = ADS_CS_PIN | ADS_SCLK_PIN | ADS_MOSI_PIN; gpio.GPIO_Mode = GPIO_Mode_Out_PP; /* 推挽输出,软 SPI 必须推挽 */ gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(ADS_PORT, &gpio); gpio.GPIO_Pin = ADS_MISO_PIN; gpio.GPIO_Mode = GPIO_Mode_IPU; /* 上拉输入,防高阻读飞 */ GPIO_Init(ADS_PORT, &gpio); ADS_CS_HIGH(); /* 空闲态:CS 高、SCLK 低 */ ADS_SCLK_LOW(); ADS_MOSI_LOW(); }

输出模式一定要选推挽,不能选开漏。开漏输出靠外部上拉拉高,上升沿被 RC 拉成一条缓坡,几 MHz 的 SCLK 根本跑不起来,示波器上看就是一堆三角波。这个坑我在别的 SPI 器件上也踩过,改一个枚举就解决了,但找了两天。

3.2 逐位收发:ads_spi_xfer16 的 12 行循环

核心函数只有十几行,但每一行都有它的理由。注意这个函数故意不碰 CS——CS 的边界由上层决定,因为寄存器访问需要一次 CS 低电平里塞两帧 16 位,如果函数内部自己拉 CS,32 位帧就被切断了,从机会把第二帧当成新命令。

/* 收发一帧 16 位,调用者负责 CS */ static uint16_t ads_spi_xfer16(uint16_t tx) { uint16_t rx = 0; uint8_t i; for (i = 0; i < 16; i++) { /* 1. SCLK 低电平期间把本位摆到 MOSI 上 */ if (tx & 0x8000) ADS_MOSI_HIGH(); else ADS_MOSI_LOW(); tx <<= 1; ads_delay(); /* 数据建立时间 t_su */ /* 2. 上升沿:从机在此把下一位摆到 SDO 上 */ ADS_SCLK_HIGH(); ads_delay(); /* 3. 时钟高电平期间采样 MISO,此时数据最稳 */ rx <<= 1; if (ADS_MISO_READ()) rx |= 0x01; /* 4. 下降沿:从机在此采样 MOSI */ ADS_SCLK_LOW(); ads_delay(); } return rx; }

逐段看。第一段是“先摆数据再打时钟”,这是 CPHA=1 时唯一正确的顺序;如果在时钟高电平期间才改 MOSI,从机采样到的是上一次的位,整个字会右移一位,现象是读数永远是实际值的两倍或一半。第二段的延时是给数据建立时间留余量,手册上这个值在几十 ns 量级,但走线上有电容,MCU 输出也有斜率,实际留 100~200ns 比较保险。第三段在高电平期间采样,对应“从机在上升沿更新 SDO”,此时数据已经稳定;如果在下降沿之后才采,理论上也行,但紧挨着从机的下一次更新,裕量更小。第四段的下降沿是从机采样时刻,之后的延时是保持时间。

参数上唯一要注意的是tx:它是发送字,MSB 先出。所以拼命令字的时候,命令的高位必须在 bit15 那一侧,0xC000 | (ch << 10)这种写法本身就已经对齐好了。返回的rx是全双工同时收到的 16 位,发命令的同时也在收上一帧的结果,这个特性在手动通道读取里正好用得上。

注意:主频越高、延时越短,读出来的数据越容易飘。调试阶段先把 SCLK 压到 500kHz~1MHz,等数据稳定了再往上提,比一上来就冲 8MHz 省时间。

3.3 命令封装:读寄存器、写寄存器与手动通道

有了ads_spi_xfer16,上面就是三层薄封装。命令字常量单独放一个头文件,和ads8688_port.h分开,方便不同项目复用。

/* ads8688_cmd.h */ #define ADS_CMD_NOOP 0x0000 #define ADS_CMD_RST 0x8500 /* 软件复位,寄存器回默认值 */ #define ADS_CMD_AUTO_RST 0xA000 /* 重启自动扫描序列 */ #define ADS_CMD_MAN_CH(ch) ((uint16_t)(0xC000 | ((ch) << 10))) #define ADS_CMD_RD(addr) <p> <a href="https://download.csdn.net/download/m0_64605336/88152728" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进