简介:一套STM32驱动AD7708的代码例程,面向需要接入16位Σ-Δ型模数转换器的嵌入式开发者,解决高精度模拟量采集时的通信与数据转换问题。AD7708支持单端/差分输入、内置PGA与数字滤波,例程覆盖初始化、寄存器配置、SPI读写、数据转换及错误检查,可直接集成到实际电路中。压缩包共3个文件,含2个头文件与1个C源文件,整体仅5KB;头文件分别负责接口声明和GPIO、时钟等硬件相关配置,C文件实现驱动逻辑及转换处理,便于移植到不同STM32型号。使用时修改硬件抽象层引脚定义,调用初始化与读取函数即可得到转换结果;代码还提供状态检查与中断处理逻辑,便于排查通信错误并保证转换稳定。整体结构清晰,适合传感器接口、工业控制、医疗设备等场景的快速验证。已有874人学习/下载,是轻量完整的参考实现。 做过多路模拟量采集的朋友应该都有体会:MCU内置ADC在低速、高精度、多通道场景下真的不够用,尤其是传感器输出是毫伏级信号时,内置12位ADC加上参考电压的噪声,直接把有效分辨率拉到个位数。我前阵子做一块8路压力采集板,选型时翻到了AD7708这颗芯片,SPI接口、16位分辨率、自带可编程增益放大,看起来完全对症。但真正动手写STM32驱动代码例程时才发现,网上中文资料少得可怜,只能对着英文数据手册一点点啃。这篇文章就是我把驱动调通之后的完整记录,从寄存器协议到初始化顺序,从24位数据拼接再到实测踩过的坑,一次性讲清楚。
1. 为什么要用AD7708而不是直接怼MCU内置ADC
先说说选型逻辑。如果你只是在做玩具级项目,ADC读数抖个几十LSB可能无所谓,但工业采集板上一个0~5V的压力变送器输出,换算到物理量可能1mV就对应几Pa的误差,这时候内置ADC的劣势就非常明显了。
首先是分辨率。STM32F103内置ADC是12位,理论上一共4096个码值,配合3.3V参考电压,每个码值对应的电压是0.8mV左右。看着还行,但实际测量中参考电压本身也有波动,加上输入通道切换时采样电容的充放电,末位两三个码基本全是噪声,实际有效位数往往只有10位上下。AD7708是16位Σ-Δ型ADC,配合内部数字滤波器,在低速率下能稳稳跑出14到15个有效位,这中间差了两个数量级。
其次是通道数量和信号调理。AD7708最多支持8路伪差分输入,也可以配置成4路全差分,关键是每个通道前面都带可编程增益放大器,增益范围能做到1到128倍。这意味着应变片、热电偶这类毫伏级信号可以直接怼到ADC输入端,省掉一级外部仪表放大器,BOM成本能压下来不少。
再就是抗混叠和滤波。Σ-Δ架构的ADC内部有数字低通滤波器,可以通过寄存器配置滤波字,把输出数据速率压到几赫兹到几十赫兹,对工频干扰有很强的抑制能力。我在这块采集板上测过,滤波字配大之后,50Hz工频串扰基本被滤干净,读数的峰峰值跳动从几十个码降到两三个码。
总结一句话:AD7708本质上是一个带数字滤波的多路精密测量前端,MCU只负责通过SPI配置它、读它的结果,所有模拟域的麻烦事都由这颗芯片包了。
2. 硬件连接:引脚不多,但参考电压和RDY引脚不能偷懒
AD7708和STM32之间的接口非常简洁,本质就是一个SPI从机加两个控制引脚。我这里用的是STM32F103C8T6的SPI1,引脚分配如下:
| STM32引脚 | AD7708引脚 | 功能说明 |
|---|---|---|
| PA4 | CS | SPI片选,低有效 |
| PA5 | SCLK | SPI时钟 |
| PA6 | DOUT/MISO | AD7708数据输出 |
| PA7 | DIN/MOSI | AD7708数据输入 |
| PA3 | RDY | 转换完成标志,低有效 |
| 3.3V | AVDD、DVDD | 数字和模拟电源 |
| GND | GND | 公共地 |
电源部分我直接把AVDD和DVDD都接在3.3V上,每个电源引脚旁边放了一颗0.1uF陶瓷电容加一颗10uF钽电容。注意去耦电容要尽量靠近芯片引脚,这是模拟电路的基本功,摆远了等于没放。
这里要重点说两个容易被忽略的引脚。
第一个是参考电压。AD7708支持内部参考,但内部基准的温漂和噪声都不如外部基准,既然项目是奔着高精度去的,就别省这颗料。我用的是ADR421的2.5V基准,输出直接接REFIN+,REFIN-接模拟地。基准芯片的输出端同样要加0.1uF和10uF去耦,否则基准噪声会直接耦合进ADC结果里。
第二个是RDY引脚。AD7708转换完成后会把RDY拉低,这个引脚在芯片内部是开漏输出,所以必须接一个10kΩ左右的上拉电阻到3.3V,然后连接到STM32的GPIO。我在代码里选择查询方式读取,如果你对实时性要求高,可以把这个引脚接到STM32的外部中断线上,RDY下降沿触发一次数据读取,这样MCU不用一直空转等待。
还有一个接地细节。STM32数字部分和AD7708模拟部分虽然共用一个3.3V电源,但PCB布局上要把模拟地和数字地做单点连接,不要让数字逻辑的开关电流串进模拟地回路。我第一次画板子时图省事直接大面积铺铜共地,结果数据显示固定的周期性跳变,后来改成单点接地才恢复正常。
3. 先把寄存器协议理顺:所有读写都先过通信寄存器这一关
AD7708的寄存器操作方式和普通SPI器件不太一样。它内部有多个寄存器,但你无法直接通过地址去选中它们,每次CS拉低后的第一个字节必须写通信寄存器,告诉芯片你接下来要操作哪个寄存器、是读还是写、操作哪个通道。通信寄存器本身是8位的,位分配大概是这样:
| 位 | 含义 |
|---|---|
| Bit7 | 读写方向,1为读,0为写 |
| Bit5-3 | 目标寄存器地址,共8个 |
| Bit2-0 | 通道选择,0~7对应AIN1~AIN8 |
寄存器映射表我整理了一份常用的:
| 地址 | 寄存器 | 说明 |
|---|---|---|
| 000 | 通信寄存器 | 每次操作的第一字节 |
| 001 | 状态寄存器 | RDY状态、校准状态 |
| 010 | ADC控制寄存器 | 模式、增益、极性、缓冲 |
| 011 | I/O控制寄存器 | 通用IO配置 |
| 100 | 滤波寄存器 | 配置数字滤波器 |
| 101 | 数据寄存器 | 24位转换结果 |
为什么要这样设计?我个人的理解是,ADI把寻址信息全部收进了一个统一入口,这样芯片引脚的复用效率更高,但代价就是代码里封装一个生成通信命令的函数非常有必要。我自己在工程里是这么写的:
// 生成通信寄存器命令 // rw: 1=读, 0=写 // reg: 寄存器地址 // ch: 通道号 uint8_t ad7708_comm_cmd(uint8_t rw, uint8_t reg, uint8_t ch) { return (rw << 7) | ((reg & 0x07) << 3) | (ch & 0x07); }底层SPI字节收发函数也不复杂,硬件SPI1模式,8位数据,MSB先行:
static uint8_t ad7708_spi_byte(uint8_t dat) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, dat); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); return SPI_I2S_ReceiveData(SPI1); }有了这两个基础函数,读写任意寄存器就变得很统一了:
void ad7708_write_reg(uint8_t reg, uint8_t ch, uint8_t val) { ad7708_cs_low(); ad7708_spi_byte(ad7708_comm_cmd(AD7708_RW_WRITE, reg, ch)); ad7708_spi_byte(val); ad7708_cs_high(); } uint8_t ad7708_read_reg(uint8_t reg, uint8_t ch) { uint8_t val; ad7708_cs_low(); ad7708_spi_byte(ad7708_comm_cmd(AD7708_RW_READ, reg, ch)); val = ad7708_spi_byte(0xFF); ad7708_cs_high(); return val; }这里有个细节要提醒:CS拉低之后,芯片会严格按照字节流来解析,所以第一次命令字节之后的每一个字节都会被当成当前选中寄存器的内容去处理。如果中途CS没有正确拉高,协议状态机就会错乱,后续读数全不对。我调试时遇到过几次这种情况,最后都是靠逻辑分析仪抓波形才发现是CS时序毛刺导致的。
4. 初始化流程:配置命令的先后顺序比你想的更讲究
AD7708上电之后的初始化顺序直接影响校准结果,我第一次直接把滤波器和增益一次性写完就读数,结果数据偏移很大,后来查手册才明白是漏了自校准这一步。
我的标准初始化流程分四步。
第一步,上电稳定等待。AD7708上电后内部电路需要时间稳定,我给了一个100ms的延时,这个时间完全够用。
第二步,写ADC控制寄存器,把芯片设成正常模式、增益1、双极性、使能输入缓冲。每一类传感器的增益需求不一样,比如应变片输出只有几毫伏,增益就要配大一些,但这个配置在正常模式下需要提前设定,不能在校准过程中随意切换。
第三步,写滤波寄存器。滤波字决定数字滤波器的时间常数,对应输出数据速率。滤波字越大,输出速率越低,噪声越低。我实际工程里用的滤波字是0x0A,输出速率大概几十赫兹,对压力信号来说够用,读数非常稳。
第四步,启动内部自校准。AD7708会在当前增益和极性配置下,自动完成失调和增益误差的修正,校准完成后RDY引脚会拉低。这一步非常关键,特别是增益大于1的时候,如果不校准,放大器的失调会被跟着放大,直接反映到输出码值上。
对应的初始化代码是这样:
void ad7708_init(void) { delay_ms(100); // 正常模式:增益1、双极性、输入缓冲开启 ad7708_write_reg(AD7708_REG_ADCCTRL, AD7708_CH0, AD7708_ADC_NORMAL); // 滤波字设为 0x0A,压低输出速率换取低噪声 ad7708_write_reg(AD7708_REG_FILTER, AD7708_CH0, 0x0A); // 启动自校准 ad7708_write_reg(AD7708_REG_ADCCTRL, AD7708_CH0, AD7708_ADC_SELFCAL); // 等待校准完成,RDY 拉低表示结束 while (ad7708_rdy_read() != 0) { } }自校准本质上就是芯片内部把输入短接到内部参考,测出一组校正系数存起来,之后每次转换都会自动套用。所以校准完成之后,不能再随便改动增益和极性配置,否则校准系数就失效了。如果你初始化之后发现读数整体偏大或偏小,先想想是不是改完配置忘了重新校准。
5. 读数据与电压换算:24位寄存器里有效数据到底在哪几位
AD7708的数据寄存器是24位的,但芯片本身是16位ADC,它把16位的转换结果左对齐存储在24位寄存器里,也就是高16位是有效数据,低8位固定为0。这个事实搞清楚之后,读数据就不会犯迷糊了。
读取时序是:先等RDY拉低,表示一次转换完成;然后CS拉低,发送“读数据寄存器”的通信命令;接着连续读取3个字节;最后CS拉高。代码如下:
uint32_t ad7708_read_raw(uint8_t ch) { uint32_t raw = 0; uint8_t buf[3]; uint8_t i; // 等待一次转换完成 while (ad7708_rdy_read() != 0) { } ad7708_cs_low(); ad7708_spi_byte(ad7708_comm_cmd(AD7708_RW_READ, AD7708_REG_DATA, ch)); for (i = 0; i < 3; i++) { buf[i] = ad7708_spi_byte(0xFF); } ad7708_cs_high(); raw = ((uint32_t)buf[0] << 16) | ((uint32_t)buf[1] << 8) | buf[2]; return raw; }如果你只想读两个字节,只取buf[0]和buf[1]其实也行,低8位反正是0。我习惯读满3字节再移位,这样逻辑统一。
电压换算是另一个容易踩坑的地方。我配置的是双极性模式,输入范围是-REF到+REF,参考电压2.5V。16位有符号数的范围是-32768到32767,所以每个LSB对应的电压是2.5V除以32768,大约是76.3uV。换算函数如下:
float ad7708_read_voltage(uint8_t ch, float vref) { uint32_t raw = ad7708_read_raw(ch); int16_t code = (int16_t)(raw >> 8); // 取高16位,按补码解释 return (float)code * vref / 32768.0f; }比如读出来是0x800000,右移8位得到0x8000,也就是-32768,对应电压就是-2.5V;读到0x7FFF80,右移8位得到0x7FFF,对应差不多+2.4999V。我实测把一个精密电压源输出2.5V接到输入端,读回来的电压是2.4998V,误差在1mV以内,线性度表现符合预期。
如果你把极性配置改成单极性,那换算公式也不同,16位无符号数范围是0~65535,对应0~REF,公式变成电压等于code乘vref再除以65536.0f。所以初始化时的极性配置和换算公式必须配套,这个一定要在代码注释里写清楚,不然过两个月你自己都会忘。
6. 实测踩坑记录:这些坑不踩一遍真的想不到
驱动能跑起来和能稳定跑是两码事。这套代码在实验室和老旧温控设备旁边各跑过一段时间,下面这几个问题是我实际遇到并且逐一解决的,列出来给大家参考。
第一个坑是SPI时钟极性和相位。AD7708数据手册的时序图读起来有点让人迷惑,我最初按照网上很多例程配成了SPI Mode 3,结果读状态寄存器返回的全是0xFF,CS、时钟、数据线都用逻辑分析仪抓过,波形看着也对,就是数据全是FF。后来把所有可行组合都试了一遍,最终发现我这块板子上SPI Mode 0是最稳的,也就是CPOL为低、CPHA选择第一个边沿采样。如果你的板子通信一上来就不对,先别怀疑代码,直接用枚举法把四种SPI模式全部试一遍,基本十分钟就能定位。
第二个坑是滤波器配置和输出速率的关系。我一开始图快,把滤波字配得很小,想让输出速率跑到几百赫兹,结果发现读数末位一直在跳,从几十个码到上百个码都有。后来把滤波字加大到0x0A,速率降下来之后,噪声肉眼可见地减少。所以AD7708这种Σ-Δ型ADC,本质上是用速度换精度,你追求高输出速率的时候,分辨率一定会打折。
第三个坑是RDY引脚的时序。AD7708在转换结束后会把RDY拉低,但如果你读数据寄存器读得太频繁,它会等下一次转换完成才再次拉低。我在连续采样模式下吃过大亏,代码里轮询RDY的时候没注意它是一边转换一边刷新,结果读回来的数据时快时慢。最后加了一个简单的时间戳判断,两次读数之间至少间隔一个转换周期,问题就消失了。
第四个坑是接地和参考电压噪声。我前面提到过,AD7708对参考电压噪声极其敏感,第一次测试时发现码值始终有周期性的波动,排查了很久才发现是板子上DC-DC电源的开关纹波通过地回路耦合进了参考电压。后来把基准电源从DC-DC的输出端独立出来,换成LDO供电,再加强去耦,噪声才压下去。AD7708的参考电压引脚最好用示波器看一下纹波,纹波超过几个毫伏,数据跳变基本无法避免。
关于模拟SPI和硬件SPI的区别,我也补充一句。AD7708的SPI速率并不高,所以GPIO模拟SPI完全可行,而且时序可以自己控制得更精确。我最终用的是STM32硬件SPI1,因为代码更简洁,但如果你在调试中遇到莫名的数据错位,不妨先换成GPIO模拟SPI试试,这样能排除硬件SPI的极性配置问题。两者代码结构一致,切换成本很低。
调试AD7708的过程中我还有一个体会:不要一上来就盯着数据寄存器读。先用状态寄存器自检,确认芯片通信正常、校准正常,再去看转换数据。AD7708的状态寄存器里包含RDY位和校准状态位,我封装了ad7708_read_reg(AD7708_REG_STATUS, ch)这个函数,调试时直接在命令行里打印状态值,比反复读数据猜问题高效得多。这套驱动代码我只用了几天就调通了,但后面做EMC测试时又暴露了几个硬件问题,让我意识到软件之外的部分同样不能忽视。如果你也在用这颗芯片,希望这些经验能帮你少走几步弯路。
本文还有配套的精品资源,点击获取