跳转至

I2C 协议

本文的内容已经整合到知识库中。

背景

最近数设课上,同学们开始购买外设,其中就涉及到 I2C 协议,因此顺带写一下 I2C 协议的教程,帮助同学们进行理解。

接口

I2C 协议涉及到两个信号:

  • SCL: 时钟信号,Master -> Slave
  • SDA:数据信号,Master <-> Slave

由于只有一个数据信号,所以 SDA 由 Master 和 Slave 轮流输出。一次请求的开始条件是,SDA 从 1 变成 0,之后 SCL 从 1 变成 0。开始请求以后,每次 SCL 上升沿采样一位的数据。请求结束时,SCL 从 0 变成 1,然后 SDA 从 0 变成 1。一次请求的波形如下:

sclsdai2cidlestartdataackstop
  1. idle 阶段,scl 和 sda 都是 1
  2. start 阶段,首先是 sda 变成 0,之后是 scl 变成 0
  3. data/ack 阶段,在 scl 上升沿采样数据,在 scl 下降沿(准确来说,负半周期)修改数据
  4. stop 阶段,首先是 scl 变成 1,之后是 sda 变成 1

传输数据的时候,需要保证 sda 在 scl 正半周期的时候保持不变。如果变了,那就是 start 或者 stop。因此,在 data/ack 阶段,建议 sda 的变化相比 scl 下降沿有一个延迟(Hold Time,一般的要求是 Min 0us)。实现方法可能是在分频的时候,延迟一个周期。

这里的 data/ack 指的则是传输的具体内容:例如 master 要传输 7 位的地址和 1 位的读使能,响应地址的 slave 要返回 ack;之后,无论是 master 还是 slave 发送数据,接收的一方都要返回 ack。ack 是低有效,意味着 0 表示成功,1 表示失败。

由于 sda 带有上拉电阻,所以如果没有 slave 响应,ack 阶段的 sda 就会变成 1,意味着失败。

I2C 控制器实现

结合上面的 I2C 波形,可以设计一个状态机:

  1. INIT 状态:初始情况下,SCL 和 SDA 都为 1,要发送数据的时候,转移到 START1 状态
  2. START1 状态:设置 SDA=0,当达到分频条件时,转移到 START2 状态
  3. START2 状态:设置 SCL=0,当达到分频条件时,START 发送完成,转移到 SEND 状态
  4. SEND 状态:当达到分频条件时,SCL 取反,如果 SCL 要从 1 变成 0,延迟一个周期,再输出 1 位的数据到 SDA 上,保证 SDA 的变化在 SCL 的负半周期;同时统计传输位数,每传输 8 位,就要等待一个周期让 Slave 响应 ACK,此时要设置 inout 输出为高阻态;如果要转换传输方向,即 Master 要读取数据,那么转移到 RECV 状态;如果传输结束,转移到 STOP1 状态
  5. RECV 状态:当达到分频条件时,SCL 取反,如果 SCL 从 0 变成 1,对 SDA 进行采样并且保存下来;同时统计传输位数,每传输 8 位,就要在 SDA 输出一次 ACK;如果传输结束,转移到 STOP1 状态
  6. STOP1 状态:设置 SCL=1,当达到分频条件时,转移到 STOP2 状态
  7. STOP2 状态:设置 SDA=1,当达到分频条件时,转移到 INIT 状态

分频是因为一般 I2C 的频率比较低,是 kHz 的量级。需要按照控制逻辑的主频,结合外设能接受的 I2C 频率范围,计算出分频的倍数。

上层协议

在此基础上,可以设计上层协议,例如 WM8731,支持通过 I2C 写入内部寄存器,一次写操作分为以下步骤:

  1. start
  2. master 发送 7 位的设备地址和 0(表示写),slave 发送 ack
  3. master 发送 7 位的寄存器地址 和 1 位的寄存器数据,slave 发送 ack
  4. master 发送 8 位的寄存器数据,slave 发送 ack
  5. stop

这里的第二步发送的 7 位地址 + 读/写位是标准的,I2C Slave 都会根据 7 位地址来决定是否由自己来响应。此后的数据的定义,则是各个芯片按照各自的协议来进行。

为了让多个同型号 I2C 芯片可以同时使用,通常芯片提供了一些引脚来配置它的地址,那么在设计的时候,给不同的芯片设置不同的地址,就解决了地址冲突的问题。

I2C EEPROM

AT24C32/AT24C64 为例,它提供了一个 I2C 接口的 EEPROM,支持如下操作:

写入数据:start,7 位设备地址,W,ACK;写入地址的高 8 位,ack;写入地址的低 8 位,ack;数据的每个字节,ack;最终 stop。

读取数据:start,7 位设备地址,W,ack;读取地址的高 8 位,ack;读取地址的低 8 位,ack;start,7 位设备地址,R,ack;数据的每个字节,ack;最终不想读的时候 nack,stop。

可以看到,这里设计成写操作的时候,只有 Master 到 Slave 的数据传输,反过来读操作的时候,只有 Slave 到 Master 的数据传输。因此,为了传输读取的地址,要首先“写入”读取的地址,再进行一次读操作,把数据读出来。

I2C Audio Codec

上面的例子中的 WM8731 实际上就是一个 Audio Codec,可以通过 I2C 对其寄存器进行写入。WM8731 的寄存器地址有 9 位,每个寄存器有 8 位的数据,因此写入流程是:start,7 位设备地址,W,ack;7 位寄存器地址,1 位寄存器数据,ack;8 位寄存器数据,ack;stop。

I2C Sensor

举一个传感器的例子:PAJ7620U2: Integrated Gesture Recognition Sensor ,它也提供了一个寄存器读写的接口,支持如下操作:

单次写入:start,7 位设备地址,W,ack;8 位地址,ack;8 位数据,ack;stop。

单次读取:start,7 位设备地址,W,ack;8 位地址,ack;stop;start,7 位设备地址,R,ack;8 位数据,nack;stop。这里的读取也拆成了两步:第一步“写入”读取的地址,第二步读取出数据。最后的 nack 表示 master 不需要读取更多的数据。

如果要批量读取的话,只要在单次读取的基础上,读取数据的时候发 ack,等到不需要继续读的时候再发 nack,就可以连续读取多个寄存器的数据。

这些命令格式和上面的 I2C EEPROM 基本是一样的。

颜色传感器 TCS3472 的命令格式也是类似的。

评论