简介:AD7745高精度24位Σ-Δ ADC官方驱动与参考例程,主要面向需要实现精密数据采集、传感器接口与工业测量功能的嵌入式开发者,尤其适合希望直接调用成熟驱动、规避底层通信调试问题的用户。资源包为RAR格式,整体仅14KB,共5个文件,包含2个C源文件、2个头文件以及1个zip示例压缩包;源码覆盖AD7745/AD7746的底层通信接口、寄存器读写、连续/单次转换模式及中断管理等模块,头文件注释较完整,便于快速移植到MCU工程中。已有710人学习下载,可用于数据采集、温度测量、压力检测等场景。压缩包内不仅提供官方驱动函数,还附带通用参考示例,能够帮助开发者根据具体应用需求调整增益、滤波与电源管理设置,正确处理转换结果与错误状态,显著提升高精度模拟量采集的可靠性,并减少底层驱动开发的重复工作量。
1. AD7745 拿到手先别急着读数据,驱动分层决定了后续开发效率
第一次调 AD7745 时,我直接拿万用表量 CIN1 引脚对地电容,再读寄存器,读数却在一百多 pF 上下乱跳。折腾了半个下午才明白,问题不在芯片,而在驱动里少了一步对状态寄存器的处理。AD7745 是带电容测量前端的 24 位 Σ-Δ ADC,I2C 接口,电容通道能做到约 4 fF 有效分辨率,常用于环境湿度、电容式液位、材料形变这类微弱电容变化场景。官方驱动压缩包把 Communication.c 和 AD7746.c 分得很开,这套分层逻辑看懂之后,不只 AD7745 能用,后续换 AD7746、AD7747 甚至其他 Σ-Δ ADC 都能沿用。对刚接手这类器件的嵌入式工程师来说,最值得花时间的不是调通 I2C,而是把寄存器时序和驱动骨架理清楚。
2. AD7745 的 I2C 从机协议与 Communication.c 读写实现
2.1 7 位地址与重复起始位的读时序
AD7745 的 I2C 从机地址是固定的,7 位地址 0x48,不含读写方向位。换算成总线上的字节,写方向是 0x90,读方向是 0x91。很多人在手写 I2C 驱动时习惯把 0x90 直接发给硬件,结果收不到 ACK,原因就是某些 MCU 的 I2C 外设(比如 STM32 HAL 库)只接受 7 位地址,由库内部再补一位方向位。地址一旦写成 0x90,HAL 库会认为你要访问地址 0x24,总线上一片死寂。
读取单个寄存器的标准时序是:发起起始位,发送写方向从机地址,发送寄存器地址,再发一次重复起始位,然后切读方向收数据。这里必须用重复起始位而不是先 STOP 再 START,因为 AD7745 在 STOP 之后会结束当前传输上下文,某些固件版本下会导致下一次读操作返回上一笔数据。
官方驱动 Communication.c 里通常就把这套逻辑封装成一个函数。下面是按位操作方式实现的版本:
#define AD7745_I2C_ADDR 0x48 /* 7 位从机地址 */ uint8_t AD7745_ReadReg(uint8_t reg_addr) { uint8_t val = 0; i2c_start(); I2C_SendByte((AD7745_I2C_ADDR << 1) | 0x00); /* 写方向:0x90 */ I2C_SendByte(reg_addr); /* 寄存器地址 */ i2c_start(); /* 重复起始位 */ I2C_SendByte((AD7745_I2C_ADDR << 1) | 0x01); /* 读方向:0x91 */ val = I2C_RecvByte(0); /* 0 表示回 NACK */ i2c_stop(); return val; }代码里 I2C_RecvByte 的参数 0 表示接收最后一个字节后主机回 NACK,这是单字节读的标准收尾方式。如果回 ACK,从机还会继续把下一寄存器的数据往 SDA 上推,总线状态就乱了。这个函数是后续所有高级操作的地基,建议单独编译进驱动层,不要和应用代码混在一起。
2.2 地址自增的批量读取方式
电容转换结果是 24 位数据,存放在 0x01、0x02、0x03 三个连续寄存器中,高位在前。AD7745 支持地址自增,主机连续读时内部地址自动加一。利用这个特性,一次突发读三字节比三次单字节读省掉两次从机地址交互,更重要的是避免在读与读之间被更高优先级的中断插入,导致三字节数据跨了两轮转换周期,拼出一个错误值。
void AD7745_ReadCapData(uint32_t *cap_raw) { uint8_t buf[3] = {0}; i2c_start(); I2C_SendByte((AD7745_I2C_ADDR << 1) | 0x00); I2C_SendByte(0x01); /* 电容数据起始地址 */ i2c_start(); I2C_SendByte((AD7745_I2C_ADDR << 1) | 0x01); buf[0] = I2C_RecvByte(1); /* 回 ACK */ buf[1] = I2C_RecvByte(1); /* 回 ACK */ buf[2] = I2C_RecvByte(0); /* 最后一字节回 NACK */ i2c_stop(); *cap_raw = ((uint32_t)buf[0] << 16) | ((uint32_t)buf[1] << 8) | (uint32_t)buf[2]; }注意 buf 内字节是大端排列:buf[0] 是最高字节,buf[2] 是低字节。拼接成 32 位无符号数后,bit23 对应 buf[0] 的最高位。这个拼接顺序如果写反,读数会出现类似“数值每 256 跳变一次”的规律性错误,排查起来非常隐蔽。
2.3 与单片机硬件 I2C 对接时的注意事项
如果直接用 STM32 的 HAL 库,不必自己拼重复起始位,I2C_Mem_Read 内部会处理好。但需要留意两个参数:MemAddress 传寄存器地址,DevAddress 传 7 位地址 0x48,千万别传 0x90。另一个问题是 DMA 模式下,HAL 会把寄存器地址当作第一个发送字节写入总线,如果本地缓冲区没有先清零,首次 DMA 读取会在最高位字节里混入上一次内存里的残留数据。我一般会在每次读取前先 memset 缓冲区,或者在 DMA 完成回调里丢弃第一包数据,只保留后续报文。
3. AD7746.c 初始化序列:寄存器映射、工作模式与通道配置
3.1 AD7745/AD7746 寄存器映射速览
AD7745 与 AD7746 寄存器布局完全一致,所以官方驱动把源文件命名成 AD7746.c 一点不奇怪。先看寄存器地图,后面初始化代码才读得懂。
| 地址 | 寄存器名称 | 功能说明 |
|---|---|---|
| 0x00 | STATUS | 转换完成标志、激励错误标志 |
| 0x01-0x03 | CAP_DATA | 电容通道转换结果,24 位,大端 |
| 0x04-0x06 | VT_DATA | 温度 / 电压通道结果,24 位,大端 |
| 0x07 | CAP_SETUP | 电容通道使能、PGA 增益设置 |
| 0x08 | VT_SETUP | VT 通道模式:片内温度、外部温度、VDD、外部电压 |
| 0x09 | EXC_SETUP | 激励输出电平、时钟使能 |
| 0x0A | CFG | 转换模式、斩波、同步控制 |
| 0x0B | CAPDACA | 正向电容补偿 DAC |
| 0x0C | CAPDACB | 负向电容补偿 DAC |
| 0x0D-0x0E | CAP_OFFSET | 电容偏移校准字 |
| 0x0F-0x10 | CAP_GAIN | 电容增益校准字 |
CAP_DATA 是驱动最常读的寄存器,初始化阶段反而很少动它。VT_DATA 是 24 位温度或电压数据,具体代表什么物理量由 0x08 寄存器决定。
3.2 初始化步骤:从空闲模式到连续转换
AD7745 上电后默认处于空闲模式,必须先完成配置再启动转换。常见初始化顺序是:先读一次状态寄存器清掉上电残留标志,然后配置电容通道使能和增益,再配置激励源,设置转换模式,最后关闭 CAPDAC 或按需求设置补偿值。
void AD7745_Init(void) { uint8_t status; status = AD7745_ReadReg(0x00); /* 清除上电残留状态 */ (void)status; /* CAP_SETUP:bit7=1 使能电容通道,bit6:5=00 增益 1 倍 */ AD7745_WriteReg(0x07, 0x80); /* EXC_SETUP:bit1=1 激励输出使能,bit5:2=0010 激励电压 VDD/2 */ AD7745_WriteReg(0x09, 0x84); /* CFG:bit1:0=10 连续转换模式,bit2=1 使能斩波 */ AD7745_WriteReg(0x0A, 0x06); /* 先关掉 CAPDAC,让数据裸奔一遍确认基线 */ AD7745_WriteReg(0x0B, 0x00); AD7745_WriteReg(0x0C, 0x00); }这段初始化里 CFG 寄存器写成 0x06,含义是连续转换加斩波使能。斩波(CAPCHOP)位推荐打开,它能抑制运放失调和低频噪声,对 24 位 ADC 来说收益非常明显。代价是每次转换时间会变长一点,具体延迟取决于数字滤波器配置,数据手册给出的典型转换时间在 20ms 到 90ms 之间。如果你的应用需要更高刷新率,可以关掉斩波,但读数噪声会明显变大,对后续数据处理要求更高。
3.3 CAPDAC 与激励电压的配置逻辑
这是 AD7745 驱动里最值得深入讲的部分。芯片输入端有一个约 4 pF 的固定寄生电容,而测量量程只有 ±4 pF(单端)或 ±8 pF(差分)。当待测传感器静态电容是 10 pF 或 20 pF 时,直接测肯定超量程。CAPDAC 的作用就是把静态电容“顶掉”,相当于给测量结果做一次数字层面的电平搬移。
CAPDAC 是 6 位可编程 DAC,每个 LSB 约 21 fF,正负方向由符号位决定。初始化时估算一下传感器静态电容再设置 CAPDACA 的数值:
/* 假设传感器在无负载状态下电容约 12.5 pF */ float cap_offset_pf = 12.5f; uint8_t capdac_code; capdac_code = (uint8_t)(cap_offset_pf / 0.021f); /* 21 fF 对应 0.021 pF */ if (capdac_code > 63) capdac_code = 63; AD7745_WriteReg(0x0B, 0x80 | capdac_code); /* bit7=1 表示正方向 */0x0B 寄存器最高位是方向控制,低 6 位是补偿数值。补偿精度不需要太高,残留的偏差可以通过后面的软件校准消除。注意 CAPDAC 设置完成后要重新校准后才能保证线性度,官方驱动里一般会在初始化末尾读几次当前输出,把平均值作为软件基线保存下来。
4. AD7745 数据读取路径、中断管理与错误恢复
4.1 单次转换与连续转换的读取路径
AD7745 支持单次转换和连续转换两种模式,驱动里最常用的切换点就在 CFG 寄存器的 bit1:0。单次转换适合电池供电的设备:把模式位设为 01,启动转换后芯片在完成转换后自动切回空闲,典型电流消耗比连续模式低很多。连续转换则适合需要持续监测的场景,比如湿度记录仪,配置成模式 10 后芯片不停转换,主机按自己的节奏来读。
数据读取路径上,我通常的做法是主循环里轮询状态寄存器:
uint8_t AD7745_StartSingle(void) { uint8_t cfg = AD7745_ReadReg(0x0A); cfg &= ~0x03; /* 清掉模式位 */ cfg |= 0x01; /* 置为单次转换 */ AD7745_WriteReg(0x0A, cfg); return 0; } uint8_t AD7745_ReadCapDataBlocking(uint32_t *raw, uint32_t timeout_ms) { uint32_t tick = 0; while (tick < timeout_ms) { uint8_t st = AD7745_ReadReg(0x00); if ((st & 0x08) == 0) { /* bit3 RDYCAP=0 表示转换完成 */ AD7745_ReadCapData(raw); return 0; } DelayMs(1); tick++; } return 1; /* 超时 */ }状态寄存器 0x00 的 bit3 是 RDYCAP,转换完成后自动清零。这个轮询函数在 while 循环里调用 AD7745_ReadReg 去读状态,每次 I2C 交互大概耗时几百微秒,对大多数传感器应用足够了。如果系统里还有 Modbus 帧接收、其他传感器轮询任务,可以把这个超时值设成 100ms,避免阻塞主任务太久。
4.2 外部 RDY 引脚触发中断读取
AD7745 有一颗 RDY 输出引脚,转换完成后拉低。这颗引脚可以直接接到单片机的 EXTI 外部中断输入端,让中断服务函数去读取转换结果。相比轮询,这种方式在连续转换模式下能把 CPU 占用降到最低,同时保证每次数据都能及时取走,不会出现轮询间隔过密或过疏导致的读数抖动。
void EXTI2_IRQHandler(void) /* 假设 RDY 接在 GPIO2 */ { uint32_t raw; if (EXTI_GetITStatus(EXTI_Line2)) { EXTI_ClearITPendingBit(EXTI_Line2); AD7745_ReadCapData(&raw); /* 中断里只读寄存器 */ cap_raw_latest = (uint32_t)raw; /* 放入全局变量 */ } }把 volatile 修饰的全局变量 cap_raw_latest 留给主循环消费,中断服务函数里不要做滤波、格式转换这类耗时操作。24 位数据的拼接是几条位运算指令,在中断里执行没有风险。但 I2C 读取本身如果走的是阻塞等待,要注意优先级分配:如果 I2C 中断优先级低于 EXTI,而 EXTI 服务函数里又调用了需要等下 I2C 中断完成的阻塞读,死锁条件就成立了。解决办法是只设置一个标志位,把真正的 I2C 读放到主循环里做,或者确认 I2C 中断优先级比 EXTI 高。
4.3 错误标志与超时恢复机制
AD7745 状态寄存器里还有一个错误标志位,负责提示激励配置问题。ECERR 位被置 1 时,通常意味着激励输出配置与输入通道不匹配,比如把激励关掉了但电容通道还开着。驱动里需要给这个标志位处理留一条路径:检测到错误后,强制软件复位,重新执行初始化序列,而不是继续读数据。否则芯片会一直输出无意义的铺底数据,从应用层看就是“读回的值漂移很大”。
超时恢复也是驱动不可少的部分。I2C 总线偶尔会因为外部干扰进入异常状态,从机不响应 ACK,这时如果驱动直接卡死在读操作里,整个任务就挂了。我之前在批量测试里遇到过一次,一个机械接触不良的电容夹具导致 I2C 总线每次读到固定地址就 NACK,整条产线的测试程序全部卡死。后来在驱动里给所有 I2C 操作加了超时计数和复位逻辑,问题才解决。
void AD7745_Recovery(void) { /* 尝试释放总线:产生 9 个 SCL 时钟脉冲 */ for (uint8_t i = 0; i < 9; i++) { i2c_start(); i2c_stop(); } AD7745_WriteReg(0x0A, 0x00); /* 切回空闲模式 */ AD7745_Init(); /* 重新初始化 */ }这段恢复逻辑的思路是:总线卡死时,通过 9 个起始-停止脉冲让从机内部的位同步状态机复位,然后重新按正常流程初始化。实际项目中我还会在恢复前后各读一次状态寄存器,把内容打印到日志里,方便现场定位是硬件还是软件问题。
5. AD7745 驱动移植到 STM32 时最容易被忽略的四个坑
5.1 移植前先把 Communication.c 隔离出来
官方驱动里的 Communication.c 直接操作寄存器,移植到 STM32 时不要改这个文件的逻辑,而是新增一个 stm32_i2c_adapter.c,把里头的 I2C_Start、I2C_SendByte、I2C_RecvByte 全部映射到 STM32 HAL 库函数上。这样 AD7746.c 上层逻辑一行不用动,后续换 NXP、GD32 或者 ESP32 平台,只需要重写 adapter 层的六个函数。
5.2 用逻辑分析仪验证首笔读时序
移植完成后第一件事不是读数据,而是抓 I2C 波形。逻辑分析仪触发条件设为 Start 条件,抓完整笔初始化序列,检查几个关键点:从机地址字节的 ACK 是否出现、寄存器地址是否正确、重复起始位后读方向字节的 ACK、以及最后一个字节是不是 NACK。常见问题是 HAL 库在 I2C_Mem_Read 时对地址的处理和官方驱动不同,导致读出来的全是 0xFF。
5.3 地址移位、字符合并与 RDY 电平边沿
地址移位问题前面 2.1 节提过,这里补充一个验证方法:直接读 0x00 状态寄存器,如果返回 0x04 或 0x20 这类非 0xFF 的数值,说明地址配置没问题。如果一直是 0xFF,优先怀疑地址位和方向位搞反了。
虚拟机场景下如果碰到 I2C 总线插入外部设备导致地址冲突,可以直接用 i2cdetect 扫一遍挂载在总线上的所有设备地址,确认 0x48 没有被同一总线上的其他外设占用。RDY 引脚边沿选择上,注意 AD7745 的 RDY 是开漏输出,外部必须接上拉电阻,配置 EXTI 时选下降沿触发。如果选的上升沿,中断永远不会来,因为引脚被拉低后保持低电平。
5.4 用已知电容验证测量链路
最后我一般会在传感器焊盘上并一个标称值 5 pF、精度 1% 的贴片电容,然后隔一小时读一百次取平均,看偏差有没有超过 0.1 pF。这个验证方法能同时检验驱动程序、I2C 时序和电源噪声水平。如果读数跳动超过 0.2 pF,先检查激励电压设置是否稳定,再量 VDD 纹波;最后才怀疑驱动代码。整个验证过程不改一行驱动代码,只通过调整 EXC_SETUP 和 CAPDAC 两个寄存器来排查,这也是官方驱动把寄存器访问接口单独暴露出来的意义所在。
本文还有配套的精品资源,点击获取