I²C 总线简介及其在 51 单片机上的应用

本文对 I²C 总线的协议做出详细的介绍,并对其在 51 单片机上的应用代码做出解析。如果同学们对文章内容有疑问,或发现文中有任何不妥之处,请在页面末尾评论区留言,我会及时回复并订正。

I²C 总线

集成电路间总线(Inter-Integrated Circuit Bus,I²C Bus,读做 I-squared-CI 方 C)是一种连接多个集成电路的总线。它由 Philips Semiconductors(现 NXP Semiconductors)于 1982 年开发。

I²C 总线的特点

  1. 多对多。在单个 I²C 总线上,可以有多个主设备(Master Device)和多个从设备(Slave Device)。主设备通常是单片机,而从设备通常是简单的外围设备(如传感器、单色显示屏),不过从设备也可以是单片机。通信总是由主设备发起并主导。通常我们仅在 I²C 总线上安排单个主设备,本文也只讨论有单个主设备的情形。如果存在多个主设备,那么它们需要进行较复杂的时钟同步和使用权仲裁,在此暂不讨论。
  2. 简单、易实现。I²C 总线仅需两个引脚就可连接多达 112 个设备,而同类总线,如 SPI 等,则需要更多的引脚。I²C 协议简单直接,不需要复杂的逻辑。
  3. 低速。大多数 I²C 设备仅支持在 100 kbit/s 的标准模式(Standard Mode)工作;少数设备最高支持 1Mbit/s 的快速模式+(Fast Mode Plus)。比起其它总线,I²C 的速度是比较慢的。
  4. 短距离。I²C 允许的走线距离较短,它通常被用于同一块 PCB 板上的 IC 间互联。I²C 总线不适用于长距离传输。

I²C 总线的常见应用

I²C 总线通常被用于将低速的外围设备通过短距离的线路连接到微处理器(单片机)上。常见的外围设备有温度传感器、电流电压传感器、单色显示屏等。这些设备结构简单,且传输的数据量很少,因此适用 I²C 总线。需要较高传输速度的设备(如高分辨率彩色显示屏)通常使用更高速度的连接方式(如 SPI)。

I²C 总线的引脚组成及电气连接

一个 I²C 总线由两条总线(Serial Clock, SCL 和 Serial Data, SDA)组成。I²C 总线上的设备都直接连接到这两个总线上(如图所示)。

I²C 总线的连接方式。μC 即单片机

所有连接到 I²C 总线的设备均使用开漏(Open-Drain,OD)连接。每个 I²C 设备的电路可以简化为如下图的结构:

开漏连接

可以将每个 P 沟道 MOSFET(MOSFET-P)想象为由单片机内部端口控制的“开关”:

开关

可以观察到,如果 I²C 总线上只连接了设备,那么 I²C 总线不可能处于高电平(因为此时总线没有连接到电源 Vdd)。我们需要在 SDA、SCL 与 Vdd 间各增加一个上拉电阻(如上图 Rp),当“断开开关”时,可以得到高电平;当“合上开关”时,可以得到低电平。

完整的连接示意图

在下文中,我们将合上开关(使相应总线处于低电平)的动作称为“拉低”;断开开关的动作称为“释放”。由于所有设备都并联在总线上,所以若总线上有任意设备拉低总线,总线即处于低电平状态;只有当总线上的所有设备释放总线时,总线才能处于高电平状态。这就是所谓的 Wired-And

I²C 通信协议概要

空闲状态

I²C 总线处于空闲状态时,所有设备都释放总线,SCL 与 SDA 均处于高电平状态。

起始条件

I²C 总线上的通信总是由主设备(Master)发起。当主设备希望通信时,它就会产生一个起始条件(Start Condition,S),作为开始通信的信号。

起始条件可以描述为:当 SCL 处于高电平时,SDA 由高电平向低电平转换。即,在
I²C 总线空闲状态下,主设备拉低 SDA,代表起始条件,开始一次通信。

在 I²C 协议中,传输总是以(frame)为单位进行的。一帧为 8bit,即一字节。主设备和从设备都可以发送帧。

在发送帧时,发送方会逐位 (bit) 发送一字节 (byte) 数据,且先发送最高位(most significant bit,MSB)。例如,如果发送方需要发送 183 = 10110111,则按 1,0,1,1,0,1,1,1(从左到右)的顺序发送。发送一位数据的过程如下:

  1. 主设备首先将 SCL 拉低。
  2. 发送方(主或从)在检测到 SCL 被拉低后,使 SDA 处于相应的电平:如果要发送 0,就拉低 SDA;如果要发送 1,就释放 SDA。
  3. 主设备等待一定的时长后,释放 SCL;此时发送方不改变 SDA 状态。
  4. 接收方在 SCL 为高电平时,读取 SDA 的状态,得到相应数据并存储下来。

发送完一位后,主设备再次拉低 SCL,进入下一个循环,发送方发送下一位数据,接收方读取下一位数据。如此循环 8 次,即完成发送一帧。

在发送完一帧的 8 字节后,主设备会将 SCL 再拉低一次。接收方在检测到 SCL 第 9 次被拉低后:

  • 如果接收当前帧成功,则会将 SDA 拉低,表示自己成功收到了数据,这称作一个 ACK 信号(acknowledgement signal)
  • 如果没有成功接收当前帧,则在 SCL 为低电平时,接收方不会作出任何动作,而使 SDA 保持在释放状态,这称作一个 NACK 信号(not acknowledgement signal)。NACK 信号并不是“主动发出”的,只要接收方没有作出任何反应,就代表一个 NACK 信号。

如果总线上没有任何设备,或者相应的接收方没有正常工作,则发送方会收到一个 NACK。

I²C 帧的传输流程

可以观察到,在传输帧时,SDA 仅在 SCL 为低电平时发生变化。如果当 SCL 为高电平时 SDA 发生变化,则会构成起始条件或停止条件。

停止条件与重复起始条件

在传输完一帧后,主设备可以选择继续传输帧,也可以选择停止传输,还可以选择从头开始一次新的传输。

如果它希望停止传输,则需要产生一个停止条件(Stop Condition,P),其可以描述为:当 SCL 处于高电平时,SDA 由低电平向高电平转换。

如果它希望从头开始新的传输,则需要产生一个重复起始条件(Repeated Start Condition,Sr)。重复起始条件与起始条件是相同的。

起始条件和停止条件

SCL、SDA 名称的含义

有同学可能会对 Serial Clock 和 Serial Data 名称的含义感到不解。

SCL 总是由主设备控制,且它以几乎固定的频率切换,因此它被称作 clock。SDA 则负责传输数据,因此被称作 data。

Serial 的含义是“串行”,即同一时间只能发送一位(one bit),而要传输的许多位数据“排队”被传输。与“串行”(serial)相对应的是“并行”(parallel)。并行总线通常由多根数据线组成,它一次能够发送多位数据。并行总线看似效率更高,但实际上它面临电平问题和串扰问题。

目前的计算机板卡互联协议中,串行协议占据统治地位。PCI-E、SATA 等都是串行传输的。一些较老的传输协议是并行协议,如 AGP、PCI 和 IDE。有关串行和并行传输方式的更多内容,可以参见这个知乎问题

I²C 从设备地址

每个 I²C 从设备均有一个(在单个总线上)独一无二的 7 位从设备地址(slave address),该地址由设备厂商决定,不过有时也可以由用户自行定义其中的某些位,以允许在同一个 I²C 总线上同时存在相同的多个设备。具体的地址可以在芯片数据手册中找到。

通常在从设备地址后会附加一个数据方向位(data direction bit),一共 8 位数据,构成一个完整的帧。通常数据方向位为 0 时表示主设备发送、从设备接收,称为 WRITE;数据方向位为 1 时表示主设备接收、从设备发送,称作 READ。

以蓝桥杯开发板 CT107D 上的 PCF8591 芯片为例。该芯片的作用将在下文应用实例一节讲述,我们先来看看它的 I²C 通信部分。

PCF8591 完整脚位图

在 PCF8591 数据手册 8.1 Addressing 一节及 Table 5, 6 中,我们可以找到有关芯片地址的详细信息。

如表格所示, PCF8591 的地址为 1001xxx,其中最后 3 位由 A2 – A0 脚(即脚位图中的 5 – 7 脚)决定。如果相应的脚为低电平,则对应的位为 0;否则对应的位为 1。例如如果这三个脚都是高电平,则芯片的地址是 1001111。

一次完整的 I²C 传输

  1. 主设备产生起始条件,代表一次通信的开始。
  2. 主设备发送一帧,表示需要通信的从设备地址和数据方向位。如果相应地址的从设备存在,则从设备将 SDA 拉低,发出 ACK 信号;如果没有相应地址的从设备存在,则主设备会收到 NACK 信号。
  3. 主设备和从设备以帧为单位进行通信;除最后一帧外,每帧传送完后,接收方需发送 ACK 信号以确认。具体的通信过程需要参照相应设备的数据手册,具体细节一般在数据手册的 Functional description 中。PCF8591 数据手册的第 8 节即为 Functional description。
  4. 在最后一帧传送完后,接收方不发出 ACK,此时主设备作出以下动作:
    • 若希望与其他从设备通信,则产生一个重复起始条件,然后继续进行第 2 步;
    • 若希望停止通信,则产生一个停止条件。产生停止条件后,两条数据线均被释放,I²C 总线进入空闲状态。

I²C 总线在 51 单片机上的应用实例

接下来我们以 PCF8591 和蓝桥杯 CT107D 开发板为例,看看如何使用 I²C 总线通信。

代码

该芯片的示例程序已经打包至附件,供同学们下载使用。在手机上阅读文章的同学也可以单击下面的链接,快速查看代码文件(ex10.c 和 i2c.c)。下文内容的行号均以网页中的行号为准。

查看 ex10.c

查看 i2c.c

PCF8591 简介

PCF8591 是一个采用 I²C 接口的模数转换芯片(Analog to Digital Converter,ADC),用于将模拟信号(analog signal,如电压大小)转换为数字信号(digital signal,由具体二进制表示的数字),相当于一个电压表(将电压转换为具体的读数)。

PCF8591 也带有数模转换(DAC)功能,但在开发板上没有作用,因此略去不表。

在 CT107D 开发板上,这个芯片连接了一个电位器(可调电阻)、一个光敏电阻和一个运算放大器,可以让单片机分别从这三者读取信号。光敏电阻可以感知周围光线亮度;电位器可以让用户通过旋钮控制单片机;而运算放大器的输入来自外部,可以感知外部差分信号的大小。

CT107D 开发板上的 PCF8591

芯片的 VDD(电源)脚连接到了开发板电源总线,并额外增加了两个退耦电容(C21 和 C22),减少电源噪声对采样带来的影响。AGND(模拟地)和 VSS(电源地)连接到 GND 网络。ADC 芯片需要振荡器产生时钟信号来驱动器工作,EXT 脚(内 / 外振荡器选择)接到 GND,表示芯片使用内部振荡器,因此 OSC(外置振荡器输入)悬空,不连接振荡器。

芯片地址选择脚(A0 – A2)全部连接到 GND,表示芯片地址的对应位全为 0,故此时芯片地址为 1001000。

AIN0 – 3 连接到了 4 组输入,上文已经描述。在电路图上查找相应网络名称,即可知道每个输入的具体功能。

例程:主程序

在示例程序(ex10.c)中,我们使用 PCF8591 读取电位器中心触点的电压值,并通过数码管显示这个电压。

ex10.c:31 – 36 (行号)首先配置定时器,并为定时器设置一个中断处理函数(ex10.c:isr_timer_0()),用于定时将采样数据显示到数码管。在中断函数中,我们可以看到重载定时器的代码(ex10.c:57 – 58),操作数码管显示采样数据的代码(ex10.c:display()),这些代码请到对应的教程中查看它们的具体含义,在此不再赘述。

ex10.c:38 初始化 PCF8591 芯片,这部分内容在下文讲述。

定时器中断每触发一次,ex10.c:59 – 63 就会递增一个计数器(intr),并在其满 50 时将 adc_flag 变量设置为 1。主函数中,ex10.c:42 一直等待 adc_flag 变为 1,并在其为 1 时进行一次采样(ex10.c:45),并将采样结果放入 dspbuf 中(ex10.c:46 – 49)。

例程:I²C

读取采样值

采样用到的函数 adc_pcf8591() 位于 i2c.c:250 处。

采样开始时,单片机首先产生起始条件(i2c.c:254)。产生起始条件的代码非常简单:先释放 SDA 和 SCL(i2c.c:33 – 34),经过短暂的延迟后,拉低 SDA(此时 SCL 处于高电平状态),即产生一个起始条件;再经过延迟后,拉低 SCL,为后续传输作好准备。

起始条件完成后,使用 i2c_sendbyte() 发送设备地址(1001000)及数据方向位(i2c.c:255)。在此我们需要从芯片读取数据,因此数据方向位为 1(WRITE),完整的一帧数据为二进制 10010001(16 进制:0x91)。i2c_sendbyte() 函数的具体实现与上述“帧”一节中的描述相同,同学们可以自己对照查看。

一帧传送完后,使用 i2c_waitack() 函数读取从设备的 ACK 信号(i2c.c:256)。这个函数有返回值,返回 0 表示设备发送了 ACK,返回 1 表示 NACK,即设备没有发送 ACK。理论上讲应该判断这个返回值,为 1 表示通信失败,但例程中假设传输始终成功,没有判断这个值。

紧接着单片机从 PCF8591 中读取一帧数据(i2c.c:257),表示采样值。读取完成后,我们发送一个 ACK 信号(按照 I²C 标准,这个 ACK 可以省略,因为这是最后一帧数据),然后产生停止条件,完成一次通信。读取到的采样值(temp)即作为函数的返回值。

初始化

上文略去的 init_pcf8591() 函数位于 i2c.c:232,它向 PCF8591 发送了一个字节,以控制芯片的行为。它的具体过程和上述采样过程类似,有所不同的是,此时我们向设备写入数据,因此数据方向位应为 0(READ),第一帧即为 0x90。第二帧数据就是实际的控制数据,写入 0x03 表示使用第三个 ADC 通道,这一值的具体含义可以参见 PCF8591 Datasheet 的 8.2 Control byte 节。

官方提供的 I²C 函数库

在竞赛时,同学们无需记住 I²C 通信的细节,而只需能够使用主办方给出的函数库即可。官方提供的函数库如下:

函数库中定义了 IIC_Start,IIC_Stop,IIC_Ack,IIC_WaitAck,IIC_SendByte,IIC_RecByte 这 6 个函数,同学们应当了解每个函数的具体功能,并能够读懂函数内容的含义。其中,somenop; 用于短暂延时,而 IIC_Ack 函数的参数用于确定是发送 ACK 还是 NACK,其余内容与上文所述基本相同。

结语

至此,同学们应对 I²C 协议的基本概念有了一些认知,并能够使用 I²C 总线与外围芯片通信。如对文章内容有疑问,请在下方留言,我会及时回复。

CC BY-SA 4.0 本作品使用基于以下许可授权:Creative Commons Attribution-ShareAlike 4.0 International License.

WordPress Appliance - Powered by TurnKey Linux