news 2026/9/13 20:05:23

C51模拟I2C驱动AD7745电容传感器完整教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C51模拟I2C驱动AD7745电容传感器完整教程

简介:面向单片机与嵌入式开发者的AD7745/AD7746电容检测芯片驱动源码包,基于C51单片机通过I2C总线实现与芯片的通信,涵盖初始化、数据读取及错误处理等核心流程,可直接用于触摸按键、液位检测等电容测量场景。压缩包共13个文件,以C源码为主,并包含Keil工程文件(ewp/eww/ewd)、编译依赖与调试配置文件,整体仅31KB,结构简洁便于阅读与移植。资源已有995人学习,源码中通过主程序与驱动文件分离的方式展示工程组织思路,对熟悉STM32、ARM等平台的开发者同样具有参考价值,可快速理解电容检测芯片的驱动原理与I2C时序控制方法。

1. 用C51给AD7745写I2C驱动,比你想的更简单

做电容检测的工程师很多会选AD7745,因为它在小尺寸封装里做到了±4 fF分辨率,配合CIN引脚可以直接接传感器电容。而常见项目里用的还是STC89C52这类增强型8051,它们大多没有硬件I2C控制器,所以驱动AD7745的工作就是把这套I2C通信协议用GPIO一根线一根线地“抠”出来。本文从I2C通信协议讲起,给出C51模拟I2C的完整思路,再落到AD7745的寄存器配置和24位数据读取,最后聊几个调试时特别容易翻车的细节。如果你正在用51单片机做液位、湿度或接近检测,这篇可以直接照着写。

2. AD7745的硬件连接与I2C通信协议要点

2.1 AD7745/AD7746的引脚定义与I2C地址选择

先把芯片引脚看清楚。AD7745是单通道电容输入,AD7746是双通道,但两者的I2C寄存器映射几乎一致,驱动代码可以直接复用。关键引脚如下表:

引脚名功能说明连接建议
SCLI2C时钟线接C51的任意IO,需上拉4.7kΩ到VDD
SDAI2C数据线同上,开漏输出需要上拉
ADDR地址选择接地或接VDD
EXCA/EXCB激励输出接传感器激励电极
CIN1/CIN2电容测量输入接传感器感应电极
VDD/GND电源和地2.7~5.25V,C51常用5V或3.3V

I2C总线上的设备地址由ADDR引脚决定。当ADDR接地时,7位地址是0x48,写地址为0x90,读地址为0x91;当ADDR接VDD时,7位地址为0x49,写地址0x92,读地址0x93。在C51驱动里,我们一般把地址宏定义成7位,然后在读写函数里左移一位补上R/W位。

常见的一个坑是:AD7745的I2C地址不能像EEPROM那样用多个地址引脚设多个值,它只有两档。如果板子上挂多个AD7745,只能靠ADDR引脚区分,最多两片。所以设计时如果要多通道采集,要么用AD7746(双通道),要么换带硬件I2C的芯片或增加I2C切换器。

2.2 I2C时序图里的起始、停止与数据位

I2C通信协议里最要紧的是时序图。起始条件:SCL保持高电平时,SDA产生一个下降沿。停止条件:SCL高电平时,SDA产生一个上升沿。数据位:在SCL高电平期间SDA电平必须保持稳定,只能在SCL低电平时改变。这个规则在模拟I2C时尤其重要,因为任何一条引脚的毛刺都会被当成数据。

C51没有硬件I2C外设,所以用两根普通IO模拟。驱动核心是四个函数:I2CStart、I2CStop、I2CSendByte、I2CReceiveByte。这些函数里除了置位和清位,最关键的是延时。延时太短,芯片采样不到稳定电平;延时太长,通信速率低,但可靠性高。我一般会用空循环做微秒级延时,具体循环次数要根据晶振频率调整。

2.3 C51引脚定义与初始化

假设用P1.0作SCL,P1.1作SDA,C51代码里的定义如下:

#include <reg52.h> #include <intrins.h> sbit I2C_SCL = P1^0; sbit I2C_SDA = P1^1; #define I2C_DELAY() { _nop_(); _nop_(); _nop_(); _nop_(); }

这里的I2C_DELAY是一个内联的延时宏,在12MHz晶振下,四个_nop_()约4微秒,I2C时钟大概在100kHz左右。如果晶振更高或更低,需要调整_nop_()的个数。注意,SDA引脚要设置为准双向口(C51默认就是),SCL同样。如果使用STC系列,可以配置为开漏,但为了兼容最常见的情况,直接准双向即可。

这个初始化其实不需要额外代码,因为上电后P1口默认就是输入状态,我们只需要定义sbit。但提醒一点:如果SDA被外部设备拉住,上电时要先释放总线,可以在主程序开始调一个I2CStop函数。具体实现见下一章。

3. C51软件模拟I2C驱动核心函数

3.1 起始、停止、发送与接收字节的C51实现

下面这套函数是所有驱动的基础,我已经在实际项目里用过多次,直接放到C51工程就能跑。

//--------------------------------------------------------------------------------- // 函 数 名:I2C_Start // 功 能:产生I2C起始信号 //--------------------------------------------------------------------------------- void I2C_Start(void) { I2C_SDA = 1; I2C_SCL = 1; I2C_DELAY(); I2C_SDA = 0; // SCL高电平期间SDA下降沿 -> 起始条件 I2C_DELAY(); I2C_SCL = 0; // 拉低时钟,准备传输数据 } //--------------------------------------------------------------------------------- // 函 数 名:I2C_Stop // 功 能:产生I2C停止信号 //--------------------------------------------------------------------------------- void I2C_Stop(void) { I2C_SDA = 0; I2C_SCL = 1; I2C_DELAY(); I2C_SDA = 1; // SCL高电平期间SDA上升沿 -> 停止条件 I2C_DELAY(); } //--------------------------------------------------------------------------------- // 函 数 名:I2C_SendByte // 功 能:向总线发送一个字节(MSB先行) // 输入参数:dat 待发送字节 // 返 回 值:0=有ACK, 1=无ACK //--------------------------------------------------------------------------------- bit I2C_SendByte(uchar dat) { uchar i; bit ack; for (i = 0; i < 8; i++) { I2C_SCL = 0; if (dat & 0x80) I2C_SDA = 1; // 先打最高位 else I2C_SDA = 0; dat <<= 1; I2C_DELAY(); I2C_SCL = 1; // 在SCL高电平期间,数据必须稳定 I2C_DELAY(); } // 释放SDA,读取应答 I2C_SCL = 0; I2C_SDA = 1; I2C_DELAY(); I2C_SCL = 1; I2C_DELAY(); ack = I2C_SDA; // 正常从机应答时,SDA应为0 I2C_SCL = 0; I2C_DELAY(); return ack; } //--------------------------------------------------------------------------------- // 函 数 名:I2C_ReceiveByte // 功 能:从总线接收一个字节 // 输入参数:ack_bit 0=继续读下一个字节时发送应答, 1=最后字节发送非应答 // 返 回 值:接收到的字节 //--------------------------------------------------------------------------------- uchar I2C_ReceiveByte(bit ack_bit) { uchar i; uchar dat = 0; I2C_SCL = 0; I2C_SDA = 1; // 释放总线,准备接收 for (i = 0; i < 8; i++) { I2C_SCL = 1; I2C_DELAY(); dat <<= 1; if (I2C_SDA) dat |= 0x01; I2C_SCL = 0; I2C_DELAY(); } // 发送应答/非应答 I2C_SDA = ack_bit; I2C_DELAY(); I2C_SCL = 1; I2C_DELAY(); I2C_SCL = 0; I2C_DELAY(); I2C_SDA = 1; // 释放SDA return dat; }

逻辑说明:起始信号要求在SCL为高时把SDA拉低,所以代码先置高SCL,再在SDA写0。停止信号相反。发送字节时,每次先在SCL低电平期间准备好SDA,再拉高SCL让从机采样,高低电平之间各插一次延时,保证时序满足100kHz标准模式。接收字节时,主机在每个SCL高电平读取SDA,并先移位再写位,保证顺序正确。最后要发送应答位,对读多字节而言,最后一个字节应发NACK,告诉从机不再读。

参数说明:I2C_SendByte返回值为0表示收到ACK,1表示NACK。调用时可以用if (I2C_SendByte(addr << 1))来判断设备是否存在。I2C_ReceiveByteack_bit参数要在调用前想清楚,连续读取多字节时,前几个传0,最后一个传1。

3.2 ACK检测与总线异常的恢复

ACK是I2C通信协议中最容易出问题的一环。正常情况下,从机在第9个时钟周期把SDA拉低作为应答。如果设备不存在、地址错误、或从机忙,SDA保持高,就是NACK。在调试初期,我建议在C51的串口上打印I2C_SendByte的返回值,能直接判断设备是否在线。

还有一个很隐蔽的坑:如果主机发送完起始信号后,发现从机没应答,或执行过程中出错,总线可能停留在SDA为低的状态。这种时候必须连续发送9个时钟脉冲把总线“解锁”。常见做法是调用一个复位函数,把SCL翻转9次,然后发停止信号。但在C51模拟I2C里,更简单的做法是直接拉高SDA和SCL,多来几个停止信号。我在代码里会这样实现:

//--------------------------------------------------------------------------------- // 函 数 名:I2C_ResetBus // 功 能:异常时复位I2C总线,释放SDA //--------------------------------------------------------------------------------- void I2C_ResetBus(void) { uchar i; I2C_SDA = 1; for (i = 0; i < 9; i++) { I2C_SCL = 1; I2C_DELAY(); I2C_SCL = 0; I2C_DELAY(); } I2C_Stop(); }

这个函数的作用是:即使从机占住SDA,主机的9个时钟也会迫使它释放。注意,在循环里不要动SDA,保持输入状态。执行完以后,总线空闲,可以重新发起通信。另外,如果C51晶振偏低,比如6MHz,而AD7745要求SCL最低只能到10kHz左右,此时需要拉大I2C_DELAY中的空操作数量,把SCL频率降到10k~20k,通信会更稳定。

4. AD7745寄存器配置与电容读取

4.1 AD7745寄存器映射

AD7745内部有一组寄存器,读写都要通过I2C。先看关键寄存器:

寄存器地址名称功能描述
0x07CAPDAC电容DAC设置,用于抵消传感器固有电容
0x08CAPSETUP电容通道设置,如使能、选择CIN1/CIN2
0x09EXCSETUP激励设置,配置激励电压和频率
0x0ACONFIG配置转换模式、采样速率等
0x0BCAPDATA电容转换结果(24位,3个字节)
0x0FSTATUS状态寄存器,判断转换是否完成

注意,AD7745的寄存器地址是8位,读和写操作时,先发送设备地址,再发送寄存器地址,然后读或写数据。与普通EEPROM不同,AD7745支持单次写一个字节,也支持连续读。但24位数据必须分3次读,我们一般用连续读模式减少I2C通信协议开销。

4.2 配置测量模式的步骤与代码

配置AD7745的常见步骤是:

  1. 设置CAPDAC寄存器,清零或在需要时抵消大电容。
  2. 设置CAPSETUP,选择电容输入通道并使能。
  3. 设置CONFIG,选择连续转换模式并启动转换。
  4. 等待转换完成(读STATUS或延时)。
  5. 读取CAPDATA高、中、低三个字节,拼成24位有符号数。

下面是一段配置和读取代码:

//--------------------------------------------------------------------------------- // 函 数 名:AD7745_Init // 功 能:初始化AD7745,配置为连续转换模式 // 注意:0x0A的0xA2是连续转换+内部时钟,实际需按数据手册确认 //--------------------------------------------------------------------------------- void AD7745_Init(void) { I2C_Start(); I2C_SendByte(0x90); // AD7745写地址 (ADDR接地) I2C_SendByte(0x07); // CAPDAC寄存器 I2C_SendByte(0x00); // 初始不抵消电容 I2C_Stop(); I2C_Start(); I2C_SendByte(0x90); I2C_SendByte(0x08); // CAPSETUP寄存器 I2C_SendByte(0x01); // 使能CIN1,CAPDAC使能,内部温度传感器关闭 I2C_Stop(); I2C_Start(); I2C_SendByte(0x90); I2C_SendByte(0x0A); // CONFIG寄存器 I2C_SendByte(0xA2); // 连续转换,采样速率约16.6Hz,内部转换时钟 I2C_Stop(); } //--------------------------------------------------------------------------------- // 函 数 名:AD7745_ReadCapData // 功 能:读取24位电容转换结果(二进制补码) // 返 回 值:24位有符号数,低24位有效 //--------------------------------------------------------------------------------- long AD7745_ReadCapData(void) { long value = 0; uchar i; I2C_Start(); I2C_SendByte(0x90); I2C_SendByte(0x0B); // CAPDATA起始地址 I2C_Start(); // 重复起始,切换为读模式 I2C_SendByte(0x91); // 读地址 for (i = 0; i < 2; i++) { value = (value << 8) | I2C_ReceiveByte(0); // 前两字节发ACK } value = (value << 8) | I2C_ReceiveByte(1); // 最后一字节发NACK I2C_Stop(); return value; }

逻辑说明:AD7745_Init分三次写操作,分别配置CAPDAC、CAPSETUP和CONFIG。0x07寄存器的0x00表示不开启电容DAC,如果你的传感器静态电容有几十pF,后面可以增大这个值来抵消,让输出能回到零点附近。0x08的0x01只使能了CIN1通道,如果使用AD7746的CIN2,需要改成0x02。0x0A的0xA2是连续转换模式,但不同版本的AD7745对CONFIG寄存器位的定义可能略有差异,务必以你手里的数据手册为准。

读取函数中,连续读三个字节,注意重复起始信号的使用。I2C_ReceiveByte(0)是发送ACK,I2C_ReceiveByte(1)是发送NACK。这个细节一定要和I2C通信协议对应起来。还有一个容易错的地方:long在C51里是4字节,但这里只用了低24位,所以高位第4字节默认是0或符号扩展,需要根据你的计算逻辑决定是否做符号扩展。如果原始数据是补码且最高位为1,表示负电容,此时需要把long的高8位全部填1才能得到正确的有符号值。

4.3 将原始码值换算为电容值

AD7745的电容量程由内部设置决定,常见为±4 pF或±8 pF。24位数据的最大值对应正满量程,最小值对应负满量程。换算公式为:

实际电容 = 原始码值 / 8388608 * 满量程 + CAPDAC补偿值

这里8388608是2^23。CAPDAC补偿值由你写入0x07寄存器的数值决定,具体映射关系要看数据手册的CAPDAC表格。我一般会把换算过程单独写成一个函数,比如:

//--------------------------------------------------------------------------------- // 函 数 名:AD7745_GetCapacitance // 功 能:将原始码值换算为pF(示例,满量程取±8.192pF) //--------------------------------------------------------------------------------- float AD7745_GetCapacitance(void) { long raw = AD7745_ReadCapData(); // 符号扩展:如果bit23为1,则补齐高字节 if (raw & 0x800000) raw |= 0xFF000000; return (float)raw / 8388608.0f * 8.192f; }

注意,这里的满量程8.192pF并不是所有模式都适用,只是我项目里用到的值。你最好在初始化时先读取状态寄存器,确认转换已完成,再用这个函数取值。如果读数一直是一个固定的大数,多半是CAPDAC没有设置,或者传感器电容已经超出量程,需要先减少CAPDAC的补偿值。

5. 调试技巧与注意事项

5.1 排查I2C通信失败的第一步

先测波形。用示波器探头分别抓SCL和SDA,看起始、停止、字节时序是否正常。如果SCL没有时钟,检查延时函数是否被优化掉——C51的Keil编译器在优化级别高时,空循环可能会被删掉,导致完全没有延时。解决方法是把延时变量声明为volatile,或者用_nop_()。另一个常见问题是SCL和SDA接反,特别是用杜邦线连接时,先查线路再查代码。

5.2 上拉电阻与电平匹配

AD7745的SDA/SCL是开漏输出,必须接上拉电阻,否则SDA永远拉不低。3.3V供电的AD7745接5V的C51,需要确认引脚兼容。多数C51是5V IO,可以直接连接,但上拉到5V会给3.3V芯片的引脚施加5V电压,长期工作有风险。稳妥做法是让两者都上拉到3.3V,或者用电平转换芯片。我自己的板子上曾因为偷懒没加上拉电阻,I2C通信时好时坏,最后用示波器才发现SDA根本没有低电平。

5.3 电容读数的稳定处理

读数跳动时,不要急着改硬件。先在驱动里做多次采样平均,比如连续读8次,去掉最大最小值,再平均。另外,AD7745的转换是连续的,但每次读到的数据可能来自不同时刻,建议先读STATUS寄存器确认新数据就绪后再读CAPDATA。读STATUS的代码如下:

//--------------------------------------------------------------------------------- // 函 数 名:AD7745_ReadStatus // 功 能:读取状态寄存器,bit7为1表示转换完成 //--------------------------------------------------------------------------------- uchar AD7745_ReadStatus(void) { uchar status; I2C_Start(); I2C_SendByte(0x90); I2C_SendByte(0x0F); I2C_Start(); I2C_SendByte(0x91); status = I2C_ReceiveByte(1); I2C_Stop(); return status; }

如果转换未完成就读取,数据可能还是上一次的旧值,导致曲线出现毛刺。用状态位做软件握手后,读数会平滑很多。如果你把这段代码烧进去后I2C通信还是不稳定,优先检查电源纹波和复位电路,我踩过的坑里一半以上是因为电源纹波太大导致AD7745内部逻辑误动作。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 20:01:44

niri 中 Zen Browser 无法进行 DMABUF 屏幕投屏怎么开启?

niri 中 Zen Browser 无法进行 DMABUF 屏幕投屏怎么开启&#xff1f; 【免费下载链接】niri A scrollable-tiling Wayland compositor. 项目地址: https://gitcode.com/GitHub_Trending/ni/niri 在 niri 上投屏时&#xff0c;niri 的主投屏通道是 portals pipewire&…

作者头像 李华
网站建设 2026/9/13 19:59:28

互联网平台架构三要素:资源池、工具链与连接网络

1. 互联网平台的本质解构互联网平台早已成为现代数字经济的核心载体&#xff0c;但多数人对其认知仍停留在表面功能层面。从业十余年&#xff0c;我发现真正理解平台架构本质的开发者不足两成。一个健康的互联网平台本质上是由三大支柱构成的有机体&#xff1a;资源池、工具链和…

作者头像 李华