简介围绕ADS8688八通道ADC与模拟SPI通信的嵌入式工程资料面向需要完成ADC采样驱动、时序调试或课程项目复现的STM32开发者。压缩包共172个文件以70个.h头文件、27个.c源文件、27个.o目标文件、26个.d依赖文件为主并含少量工程配置、说明文档与烧录镜像整体约1.13MB结构覆盖驱动、核心代码、硬件描述及MDK-ARM工程。已有1873人浏览学习。资料重点展示在ADS8688不直接支持标准SPI时如何用GPIO模拟时钟与数据线完成通道与增益配置、启动转换、等待转换结束及读取采样值并给出数据校验与格式处理思路读者可据此对照时序、排查通信问题并在STM32G4平台上快速搭建可调试的采集工程。1. 从一块采样乱跳的板子说起ADS8688 模拟 SPI 采样的三个关键点板子上电示波器探头搭在 ADS8688 的 SDO 上波形干净得像教学图可数据一进主循环相邻两次采样能差出三四十个码换个通道又恢复正常。这种问题我在三块不同的板子上都遇到过最后查下来既不是芯片坏了也不是电源脏而是模拟 SPI 协议里某个时钟沿没对齐。ADS8688 是 8 通道、16 位、单通道最高 500kSPS 的 SAR 型 ADC片上带基准和输入前端支持从 ±10.24V 到 0~2.56V 共六档量程工业现场测变送器输出、测电池组电压、测传感器桥路都爱用它。但它只给 SPI没有并口很多板子又不愿意为它单独占一个硬件 SPI 外设于是用普通 GPIO 模拟 SPI 协议读 ADC 采样值就成了常见选择。这篇从一个实际驱动出发把命令帧、位翻转时序、码值换算和二次开发拆开讲适合手上正拿着一块 ADS8688 板子、想把它跑通再改的人。2. 摸清 ADS86888 通道、6 档量程与 16 位命令帧的对应关系2.1 先把量程和 LSB 算清楚再谈接线ADS8688 的每个通道都能独立配置输入范围六档分别是双极性 ±10.24V、±5.12V、±2.56V以及单极性 0~10.24V、0~5.12V、0~2.56V。这里的满量程数字不是随便定的它由内部 4.096V 基准经过前端增益组合出来所以外部不需要再挂精密基准源这一点在小体积板子上很省事。LSB 的算法决定了后面所有换算的精度先把公式摆出来双极性: LSB (2 × Vrange) / 65536 单极性: LSB Vrange / 65536以 ±10.24V 为例LSB 20.48 / 65536 ≈ 312.5µV换成 0~2.56V 单极性LSB 2.56 / 65536 39.06µV。同样是 16 位量程选得越窄实际分辨率越高代价是输入不能超出范围。我一般会按信号的最大摆幅往上留 20%~30% 余量来选档比如传感器输出 0~4V就选 0~5.12V 那档别为了追分辨率硬压到 0~2.56V现场一个浪涌就打饱和了。还有一点容易忽略双极性量程下输出是二进制补码格式0x0000 对应 0V0x7FFF 对应正满量程附近0x8000 对应负满量程。也就是说码值直接强转成int16_t再乘 LSB 就能得到带符号电压不需要手动减偏置。单极性则是直接二进制0x0000 是 0V0xFFFF 是满量程。这两种解释方式如果搞混测出来的电压会整体偏掉半个量程而且是那种看着有数、就是不对的偏很难一眼看出来。量程配置走的是寄存器从 0x04 开始每个通道占一个寄存器CH1 落在 0x04一路顺延到 CH8 的 0x0B寄存器低三位写量程编码。不同版本的手册里寄存器映射偶有合并写法比如一个字节管两个通道落地前建议先对着手上的 Register Map 核一遍地址别照抄别人代码里的宏。2.2 16 位命令帧NO_OP、AUTO_RST、MAN_Ch_n 各管什么ADS8688 的 SPI 是 16 位定长帧主机在 SDI 上移入命令的同时SDO 上移出这一帧对应的转换结果。命令种类不多但每个的作用边界得记牢否则会出现命令发了、数据没变的情况。命令编码作用NO_OP0x0000空操作只取数据不改状态STDBY0x8200进入待机功耗降下来但基准还在PWR_DN0x8300深度掉电唤醒后要重新配置RST0x8500复位所有寄存器到默认值AUTO_RST0xA000复位并直接进入自动扫描MAN_Ch_n0xC000 n×0x0400手动选通道 nn 取 0~7手动通道命令的编码规律值得记一下基数 0xC000通道号每加一命令字加 0x0400。这样写代码时不用维护一张八项的查找表一个宏就够了。我见过有同事把通道号直接加在低字节上写成0xC000 n结果通道 0 和通道 1 分别对应到两个完全不同的功能码上读回来的数据全是乱的查了大半天。自动扫描模式是这套命令里最实用的一个。发一次 AUTO_RST芯片内部就从 CH0 开始按顺序轮转每来一个 CS 上升沿就切到下一个通道主机只要连续发 NO_OP 收数据即可八个通道一轮下来完全不用干预。代价是没法单独跳过某个通道也不能在中途改量程适合固定序列采集的场景。2.3 时序账CS 上升沿启动转换数据要等下一帧真正决定成败的是帧间时序。ADS8688 的转换不是在 SCLK 上触发的而是在 CS 的上升沿启动也就是说这一帧 16 个时钟走完、CS 拉高芯片才开始真正采样转换结果要等到下一次 CS 拉低后的那一帧才能从 SDO 上读出来。这条规则带来两个直接后果。第一手动切通道时必然有一帧延迟。你发 MAN_Ch_3这一帧 SDO 上吐出来的还是上一个通道的结果新通道的数据在下一帧。所以标准写法是发完命令再补一帧 NO_OP 把数据取走中间那一帧丢掉。不少驱动在单通道测试时看着没问题一切多通道就串数据根源就在这里。第二帧率上限由CS 高电平时间 16 个 SCLK共同决定。手册给的 500kSPS 是指转换器本身的吞吐实际能不能跑到取决于你的接口速度。硬件 SPI 跑到十几 MHz 时接近满速模拟 SPI 用 GPIO 翻转一帧下来通常要三到五微秒折算下来两三百 kSPS 就到头了。做多通道轮询时这个账要提前算8 通道自动扫描每通道实际速率是总帧率除以 8若总线只跑到 200kSPS单通道就只有 25kSPS测 50Hz 工频信号够用测更快的动态信号就得换方案。3. 模拟 SPI 驱动实现4 根 GPIO 翻转出 16 位全双工时序3.1 为什么放着硬件 SPI 不用先说清楚选型理由免得被当成偷懒。硬件 SPI 外设当然是首选但在实际板子上经常撞上三种情况一是主控的 SPI 外设已经被 Flash 或屏幕占了加个 ADC 要重新分配引脚二是硬件 SPI 的片选行为不总是符合预期很多外设的 CS 在帧间必须拉高才能触发动作而硬件 SPI 在连续传输时会保持 CS 低电平得靠软件手动干预三是多片 ADS8688 并联时需要精确控制每片的 CS 先后顺序软件模拟反而更直观。模拟 SPI 的代价也很明确占用 CPU、速率上不去、时序靠延时保证。所以我的判断标准是采样率需求。单通道 10kSPS 以下的静态量测量模拟 SPI 完全够用代码还可移植要做 100kSPS 以上的动态采集老老实实上硬件 SPI 加 DMA。这条线划清楚后面就不会纠结。引脚连接上四根线固定CS、SCLK、SDI、SDO。SCLK 空闲电平为低数据在上升沿被从机采样、在下降沿更新输出等价于 SPI 模式 1。另外芯片还有一个复位/掉电引脚低电平有效上电时记得拉高否则芯片一直挂在复位态怎么发命令都没反应。3.2 位翻转函数一次 16 位全双工传输怎么写整个驱动的地基就一个函数把 16 位命令移出去同时把 16 位结果移进来。写法如下平台相关的只有最上面五个内联函数。/* 平台抽象层换芯片只改这五个函数体 */ static inline void cs_low(void) { gpio_set(PIN_CS, 0); } static inline void cs_high(void) { gpio_set(PIN_CS, 1); } static inline void sclk(int v) { gpio_set(PIN_SCLK, v); } static inline void sdi(int v) { gpio_set(PIN_SDI, v); } static inline int sdo(void) { return gpio_get(PIN_SDO); } /* * 16 位全双工传输 * tx : 本帧要发给 ADS8688 的命令字 * 返回: 同一帧 SDO 上移出的转换结果 * 时序: SCLK 空闲低上升沿从机采样 SDI下降沿从机更新 SDO */ static uint16_t ads8688_xfer16(uint16_t tx) { uint16_t rx 0; int i; cs_low(); /* 帧开始 */ for (i 15; i 0; i--) { /* 先摆好 SDI保证在上升沿之前稳定 */ sdi((tx (1u i)) ? 1 : 0); ds_delay(NS_TSU); /* tDSU: SDI 建立时间 */ sclk(1); /* 上升沿从机锁存 SDI */ ds_delay(NS_THIGH); sclk(0); /* 下降沿从机更新 SDO */ ds_delay(NS_TLOW); /* 下降沿之后采样此时 SDO 已经稳定 */ if (sdo()) rx | (1u i); } cs_high(); /* 帧结束锁存命令并启动下一次转换 */ return rx; }逐段说明。cs_low()之后从机开始输出第一位数据在第一个 SCLK 下降沿才出现在 SDO 上所以循环里必须先产生下降沿再读顺序反了读到的永远是上一位。SDI 的摆放放在上升沿之前是因为从机在上升沿锁存它如果放在下降沿之后摆某些主频较高的板子上会因为建立时间不足而丢位。返回值rx的位序和命令字一致都是高位在前。这里有个容易翻车的点有人在循环里用rx (rx 1) | sdo()累加同时循环变量从 0 递增到 15结果读出来的 16 位是反的。用rx | (1u i)配合i递减就不会有这个问题位序天然对齐。几个延时常量NS_TSU对应手册里的数据建立时间量级在几纳秒NS_THIGH和NS_TLOW是 SCLK 高低电平各占的时间如果 SCLK 跑 10MHz半周期就是 50ns。这几个值不需要精确到纳秒留一倍余量即可但绝不能省成零尤其是NS_TSU省掉之后在低温或者长排线的板子上偶发丢位属于典型的玄学故障。3.3 延时怎么给别用微秒级函数模拟 SPI 最容易写错的地方是延时粒度。如果图省事在每位之间调一次delay_us(1)16 位下来就是 32µs 一帧帧率直接掉到三十多 kSPS而且 CPU 全程被占死。正确做法是用纳秒级、可内联的短延时。/* 纳秒级短延时优先用内核周期计数器退而求其次用空循环 */ static inline void ds_delay(uint32_t ns) { #if USE_DWT_CYCLE_COUNTER uint32_t start dwt_cycle(); uint32_t ticks (ns * CPU_MHZ) / 1000u; while ((dwt_cycle() - start) ticks) { } #else /* 经验值主频 72MHz 时一次空循环约 2~3ns按实测标定 */ volatile uint32_t n ns / 3u; while (n--) { __NOP(); } #endif }周期计数器版本精度高主频 72MHz 时一个 tick 约 14ns用来卡 50ns 的半周期足够空循环版本依赖编译器优化等级换一个-O2就可能被优化掉所以循环变量必须加volatile并且实测标定一次。我一般会在示波器上看 SCLK 的实际频率反推延时是否合适比对着代码算靠谱。还有一个隐含的成本gpio_set()这类函数如果不是内联的一次调用可能要几十个周期。16 位帧里每位移两次 SCLK、一次 SDI、一次 SDO加起来六十多次调用光是函数开销就能吃掉一两微秒。所以这几个函数必须写成static inline或者直接用寄存器位操作别用带参数检查的通用 GPIO 接口。4. 采样链路闭环从原始码值到电压值的完整搬运4.1 单通道手动模式发命令、丢一帧、取数据手动模式适合只关心某一路的场景比如设备上只有一个温度通道需要高频监控。函数写起来短但顺序不能错。/* * 读取指定通道的一次转换结果 * ch: 0~7 * 返回: 原始 16 位码值 * 注意: 命令在 CS 上升沿生效本帧返回的仍是旧通道数据必须丢弃 */ uint16_t ads8688_read_ch(uint8_t ch) { if (ch 7) return 0; ads8688_xfer16(CMD_MAN_CH(ch)); /* 切通道本帧数据无效 */ return ads8688_xfer16(CMD_NO_OP); /* 下一帧才是目标通道的结果 */ }第一次调用ads8688_xfer16时返回的值被直接丢掉了因为那一帧 SDO 上还是上一个通道的电平。紧接着补一帧 NO_OP既取回了数据又启动了下一次转换。两次传输之间不需要额外加延时CS 上升沿之后芯片自己会完成采样保持只要主循环的节奏不超过转换时间就行。如果连续调用这个函数读同一通道每次都会多花一帧在丢数据上吞吐减半。优化办法是把命令和数据合并既然上一帧发过 MAN_Ch_n 之后通道已经切好后续就可以一直发 NO_OP 连续取数只有换通道的时候才需要多发一帧。写成一个ads8688_read_same_ch()之类的函数读固定通道时能省掉一半时间。4.2 自动扫描模式一次配置八通道轮转八通道都要采的时候手动轮询的帧开销太大改用自动扫描。配置流程只有两步先按通道配好量程寄存器再发 AUTO_RST 进入扫描。/* 配置 8 个通道的量程range_code 见量程编码表 */ void ads8688_setup_scan(const uint8_t range_code[8]) { uint8_t i; for (i 0; i 8; i) { /* 量程寄存器从 0x04 起每通道一个低 3 位为量程编码 */ ads8688_write_reg(0x04 i, range_code[i] 0x07); } ads8688_xfer16(CMD_AUTO_RST); /* 复位并进入自动扫描 */ ads8688_xfer16(CMD_NO_OP); /* 丢掉复位后的第一帧残留数据 */ } /* * 连续读一轮 8 通道结果写入 buf * 调用前必须已经执行过 ads8688_setup_scan() */ void ads8688_read_scan(uint16_t buf[8]) { uint8_t i; for (i 0; i 8; i) { buf[i] ads8688_xfer16(CMD_NO_OP); } }ads8688_read_scan里没有任何通道选择动作通道切换完全由芯片内部的序列发生器负责发 NO_OP 只是提供时钟并取走结果。八帧一轮帧与帧之间芯片自动递增通道号回到 CH0 后继续循环只要不断电就一直转下去。这里有个细节要注意自动扫描启动后芯片不再接受手动通道命令想切回单通道必须先发 RST 或重新上电。所以固件里如果两种模式都要用得做一个状态标志别在扫描过程中直接发 MAN_Ch_n那样只会把序列打乱。4.3 码值换算与两点校准拿到原始码值只是中间结果业务层要的是电压。换算函数按量程分双极性和单极性两条路。/* 各量程对应的满量程电压双极性取正半轴绝对值 */ static const float range_full[6] { 10.24f, 5.12f, 2.56f, /* 双极性 ± */ 10.24f, 5.12f, 2.56f /* 单极性 0~ */ }; /* code: 原始码值 range: 0~5 的量程索引 */ float ads8688_to_volt(uint16_t code, uint8_t range) { float lsb; if (range 5) return 0.0f; if (range 3) { /* 双极性补码解释 */ lsb (2.0f * range_full[range]) / 65536.0f; return (float)(int16_t)code * lsb; } else { /* 单极性直接二进制 */ lsb range_full[range] / 65536.0f; return (float)code * lsb; } }关键在(int16_t)code这个强转。双极性模式下负电压对应的码值在 0x8000 以上如果不转成有符号数0x8000 会被当成 32768 去乘 LSB算出来是一个很大的正电压量出来完全离谱。这个坑我在早期项目里踩过当时还以为是硬件接反了。理论换算之外实际板子还有零点漂移和增益误差尤其是我这种用普通 LDO 供电、基准没做单独处理的板子全量程误差能到十几个 LSB。要更准就做两点校准短接输入端采一组码值作为零点加一个接近满量程的精密参考采一组作为增益点把两个系数存进 Flash换算时先减零点再乘增益。typedef struct { int32_t zero_code; /* 输入端短接时的平均码值 */ float gain; /* 理论斜率 / 实测斜率 */ } cal_t; float ads8688_to_volt_cal(uint16_t code, uint8_t range, const cal_t *cal) { float raw ads8688_to_volt(code, range); return (raw - ads8688_to_volt((uint16_t)cal-zero_code, range)) * cal-gain; }校准值不需要每次上电重算出厂前标定一次写进参数区即可。但要注意零点会随温度漂如果设备工作温度跨度大建议加一个开机自动短接校准的流程或者至少留一个手动校准的命令入口。5. 避坑与排查5 条现场踩出来的记录5.1 读回全是 0x0000 或 0xFFFF现象CS、SCLK 波形都正常SDO 上却是一条直线读回恒定 0x0000 或 0xFFFF。原因绝大多数是 SDO 的方向配错了。ADS8688 的 SDO 是推挽输出但主控侧如果把它配成推挽输出而不是浮空输入两边会打架读到的就是主控自己驱动的电平。另一种可能是 CS 一直没拉低芯片的 SDO 处于高阻态上拉电阻把线拉成高电平。解决查 GPIO 初始化SDO 必须配成输入上拉或浮空都行别配下拉。用万用表量 CS 引脚确认空闲时是高、传输时确实被拉低。如果板上 SDO 有串联电阻还要确认焊接没虚。5.2 数据整体偏移半个量程现象输入 0V 时读出来是满量程附近的值正负输入都偏。原因双极性量程下把码值当无符号数处理了0x0000 被解释成正电压而不是 0V。也有可能是量程寄存器的编码写错实际配成了单极性档但换算还按双极性算。解决先确认(int16_t)强转有没有加再把量程寄存器的实际值读回来打印一遍和手册的量程编码表逐位对照。我一般会在初始化完成后把所有配置寄存器读回一次跟预期值做断言这一步能挡住八成配置类问题。5.3 多通道数据串位现象单通道测试完全正常切到自动扫描后CH3 的数据出现在 CH5 的位置整体错开一到两位。原因自动扫描启动后的第一帧是残留数据没丢掉导致后面所有通道整体偏移。另一个可能是扫描过程中插入了手动通道命令把内部序列打乱了。解决AUTO_RST之后固定丢弃一帧再开始收数据。检查代码里有没有在扫描循环中调用单通道读函数有的话用一个模式标志隔离开。5.4 高低温环境下偶发丢位现象常温测试一切正常放到高低温箱里跑几个小时偶尔出现某个码值突然跳变重启后又好了。原因模拟 SPI 的建立时间余量不够。GPIO 翻转速度随温度变化加上排线电容SDI 在 SCLK 上升沿前还没稳定从机锁存到的是上一位。解决把NS_TSU加大到原来的两到三倍宁可牺牲一点速率。另外检查 SDI 走线有没有和 SCLK 长距离并行串扰也会造成类似现象必要时在 SDI 上串一个 22Ω 电阻。5.5 采样率上不去主循环卡顿现象示波器测 SCLK 频率只有几百 kHz一帧要十几微秒主循环里其他任务明显被拖慢。原因延时函数粒度太粗或者 GPIO 操作没内联函数调用开销占了大头。还有一种情况是每帧之间加了不必要的delay_ms白白等掉时间。解决把延时换成周期计数器版本把 GPIO 操作全部内联或直接写寄存器去掉帧间多余等待。用示波器量 CS 的周期反推实际帧率跟理论值对不上就逐段注释掉代码定位耗时点。6. 二次开发把驱动改成可配置、能自检的模块驱动跑通只是起点真正交付的时候代码要经得起换量程、换通道顺序、换板子。我的做法是把硬编码拆成一张配置表通道顺序、量程、是否启用都放进结构体初始化时遍历一遍业务层只按名字取数据。typedef struct { uint8_t ch; /* 物理通道号 0~7 */ uint8_t range; /* 量程索引 0~5 */ uint8_t enabled; /* 是否参与扫描 */ float scale; /* 业务层额外系数如变送器 4~20mA 转工程量 */ float offset; } ads_ch_cfg_t; /* 上电自检扫描一轮检查各通道码值是否落在合理区间 */ int ads8688_self_test(const ads_ch_cfg_t *cfg, uint8_t n) { uint16_t buf[8]; uint8_t i; ads8688_setup_scan_range(cfg, n); ads8688_read_scan(buf); ads8688_read_scan(buf); /* 第二轮才是稳定值 */ for (i 0; i n; i) { if (!cfg[i].enabled) continue; /* 输入端悬空或短路时码值会贴近 0x0000 或 0xFFFF */ if (buf[i] 0x0000 || buf[i] 0xFFFF) return -1; } return 0; }把量程和通道顺序外置之后同一套驱动能直接用在不同板子上改的只是配置表。自检函数则是我强烈建议加的一环它能在产线测试阶段就把焊接问题、量程配错、通道串位挡下来而不是等到现场安装完才发现某一路上不去。再往上一层是数据链路。如果采样是在中断或定时任务里跑的建议用双缓冲把采集和业务解耦前台定时触发一轮扫描写进缓冲区 A后台处理缓冲区 B一轮结束后交换指针。这样业务层的浮点运算和滤波不会拖慢采样节奏也不会因为某次处理耗时波动导致丢帧。还有一个值得做的改造是把量程切换做成运行时可控的。固定量程在信号幅度变化大的场景下很吃亏小信号时分辨率浪费大信号时又饱和。可以做一套简单的自动换挡逻辑连续若干次采样接近满量程 90% 就升一档低于 10% 就降一档换挡后丢弃前两帧等前端稳定。从那以后我每次新画一块带 ADS8688 的板子都会先跑一遍这套自检流程确认 8 个通道的码值都能落在预期区间、SCLK 频率符合设计值再往上写业务逻辑。这个习惯帮我省掉了好几次返工。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?