# I2C总线接口

  - [概要](#概要)
    - [什么是I2C](#什么是i2c)
    - [通信原理](#通信原理)
      - [物理接线](#物理接线)
      - [数据有效性](#数据有效性)
      - [起始条件和停止条件](#起始条件和停止条件)
      - [数据格式](#数据格式)
        - [响应ACK](#响应ack)
        - [PDU](#pdu)
    - [I2C传输通用方法集合](#i2c传输通用方法集合)
  - [I2C引脚定义](#i2c引脚定义)
  - [machine.I2C API详解](#machinei2c-api详解)
    - [类](#类)
      - [class I2C(id,scl,sda,freq)](#class-i2cidsclsdafreq)
      - [构造I2C](#构造i2c)
    - [函数](#函数)
        - [扫描I2C地址](#扫描i2c地址)
        - [写数据](#写数据)
        - [读数据](#读数据)
  - [示例](#示例)

## 概要

​   本节简要介绍I²C总线协议,并以一个简单的例程介绍如何在通过 `Python` 使用 `OpenHarmony` 的 I²C 接口

### 什么是I2C

- I²C(Inter-integrated Circuit)最早是飞利浦在1982年开发设计的一种简单、双向二线制同步串行总线协议。
- I²C总线支持设备之间的短距离通信,用于处理器和一些外围设备之间的接口,它只需要两根信号线来完成信息交换。

### 通信原理

#### 物理接线

![](Image/i2c.png)

​   I²C最少只需要两根线,和异步串口类似,但可以支持多个从(slave)设备,和SPI不同的是,I²C可以支持多主机(mul-master)系统,允许有多个master并且每个master都可以与所有的slaves通信(master之间不可通过I²C通信,并且每个master只能轮流使用I²C总线)。master是指启动数据传输的设备并在总线上生成时钟信号以驱动该传输,而被寻址的通信设备都作为slaves。

​   I²C通讯只需要2条双向总线:

| 功能编号                       | 含义                                                |
| ------------------------------ | --------------------------------------------------- |
| SDA(serial data:串行数据线)  | 传输数据,SDA线传输数据是大端传输,每次传输一个字节 |
| SCL(serial clock:串行时钟线) | 同步数据收发                                        |

​   一个主设备和两个从设备的连接示意图如下图所示:

![](Image/I2C物理连线示意图.png)

​   I²C总线上的每一个设备都可以作为主设备或者从设备,而且每一个设备都会对应一个唯一的地址,当主设备需要和某一个从设备通信时,通过广播的方式,将从设备地址写到总线上,如果某个从设备符合此地址,将会发出应答信号,建立传输。

#### 数据有效性

​   SDA 线上的数据必须在时钟的高电平周期保持稳定,数据线的高或低电平状态只有在 SCL 线的时钟信号是低电平时才能改变。

​   换言之,SCL为高电平时表示有效数据,SDA为高电平表示“1”,低电平表示“0”;SCL为低电平时表示无效数据,此时SDA会进行电平切换,为下次数据表示做准备。如图为数据有效性的时序图。

![](Image/i2c-data-change.png)

#### 起始条件和停止条件

​   起始条件S:当SCL高电平时,SDA由高电平向低电平转换;

​   停止条件P:当SCL高电平时,SDA由低电平向高电平转换。

​   起始和停止条件一般由主机产生。总线在起始条件后处于busy的状态,在停止条件的某段时间后,总线才再次处于空闲状态。如下图为起始和停止条件的信号产生时序图。

![](Image/i2c-sp-condition.png)

#### 数据格式

![](Image/i2c-sp-1byte.png)

​   I²C数据的传输必须以一个起始信号作为开始条件,以一个结束信号作为传输的停止条件。

​   I²C传输的数据以字节为单位,每个字节必须为8位,可以传输任意多个字节,上图中以一个字节的数据为例进行分析,I2C的数据格式具有以下特点:

- 每个字节后必须跟一个响应位 ACK(如上图中的SCL上ACK),因此实际上传输一个字节(8位)的数据需要花费9位的时间。
- SDA上首先传输字节的最高位,从上图中我们可以看出,位数编号的发送顺序从左至右 是 Bit7-Bit0

##### 响应ACK

​   数据接收方收到传输的一个字节数据后,需要给出响应,此时处在第九个时钟,发送端释放SDA线控制权 ,将SDA电平拉高,由接收方控制。

​   接收方表示成功的接收到了8位一个字节的数据,便将SDA拉低为低电平,即ACK信号,表示应答

![](Image/i2c-ack.png)

##### PDU

​   I²C的PDU(Protocol Data Unit:协议数据单元,即数据格式):

![](Image/i2c-pdu-read.png)

![](Image/i2c-pdu-read1.png)

![](Image/i2c-pdu-r&w.png)

###  I2C传输通用方法集合

- I²C接口定义了完成I²C传输的通用方法集合,包括:
- I²C控制器管理: 打开或关闭I2C控制器
- I²C消息传输:通过消息传输结构体数组进行自定义传输

## I2C引脚定义

waffle nano 共有7个引脚可以用作 I²C 的通讯引脚。拥有 2 个 I²C 总线端口,根据用户的配置,总线端口可以用作 I²C 主机。

> 其中板载内置传感器连接到引脚:`Pin9,Pin10`。所以在操作板载传感器时需配置 `0` 号端口 I²C 总线对象,比如 `i2c = I2C(0, sda=Pin(9), scl=Pin(10)`。

| I²C 端口号 | SDA          | SCL           |
| --------- | ------------ | ------------- |
| I2C 1   | pin 0        | pin 1         |
| I2C 0   | pin 9        | pin 10        |
| I2C 0   | pin 13        | pin 14         |

## machine.I2C API详解

  使用`from machine import I2C`导入`machine`模块的I2C总线接口类`I2C`

  再使用`TAB` 按键来查看`I2C`中所包含的内容:

```python
>>>from machine import I2C
>>>I2C.
read            write           readfrom_mem    scan
writeto_mem
```

### 类

#### class I2C(id, scl, sda, freq=400000)

- `id` — I²C设备组号,可以为0或1
- `scl` — I²C设备时钟引脚对象
- `sda` — I²C设备数据线引脚对象
- `freq` — SCL时钟频率 0 <`freq`≤ 500000(Hz)

#### 构造I2C

&emsp;&emsp;用I2C对象的构造器函数

&emsp;&emsp;示例:

```python
>>> from machine import I2C, Pin
>>> i2c = I2C(0, sda=Pin(9), scl=Pin(10),freq=100000)#构建I2C0,时钟频率100000(时钟屏幕根据具体传感器而定,一般使用100000或400000)
>>> i2c = I2C(1, sda=Pin(0), scl=Pin(1),freq=100000)##用pin1引脚当I2C1的scl功能成功
```

&emsp;&emsp;使用ID直接构造

&emsp;&emsp;I2C的id只能取`0`或`1`,可以通过id来直接构造这两组接口。无法直接使用第1组I2C资源,默认I2C的SCL功能是由pin4引脚实现,会报`invalid scl pin(4)`错误,因为`IO3`、`IO4`被编程串口所占用。

&emsp;&emsp;示例:

```python
>>> from machine import I2C, Pin
>>> i2c0=I2C(0)#构建I2C0
>>> i2c0
I2C(0, scl=10, sda=9, freq=400000)
>>> i2c1=I2C(1)#构建I2C1会报ValueError
Traceback (most recent call last):
  File "<stdin>", in <module>
ValueError: invalid scl pin(4)
>>> i2c1
I2C(1, scl=4, sda=3, freq=400000)
```

### 函数

&emsp;&emsp;在接下来的示例中, 构造`id=0`的`I2C`对象来列举I2C对象的函数。

```python
>>>from machine import I2C
>>>i2c = I2C(0)#构建I2C0
```

##### 扫描I2C地址

&emsp;&emsp;`i2c.scan()`

&emsp;&emsp;函数说明:扫描I²C地址,并返回设备列表

```python
>>>from machine import I2C
>>>i2c = I2C(0)#构建I2C0
>>> i2c.scan()#扫描I²C地址
[104, 118]
```

&emsp;&emsp;waffle nano本身有104和118两个地址的I²C总线,104是陀螺仪传感器,118是温湿度传感器。

&emsp;&emsp;I²C读写数据需要先扫描I²C地址,在相应的地址上读写数据

##### 写数据

&emsp;&emsp;`i2c.write(addr,data)`

&emsp;&emsp;函数说明:向I2C写入(发送)数据,返回True或False表示是否写入成功.

&emsp;&emsp;`addr`I²C设备地址,可由scan函数读取出来

&emsp;&emsp;`data`: 需要写入(发送)的数据

&emsp;&emsp;示例:

```python
>>>from machine import I2C
>>>i2c = I2C(0)#构建I2C0
>>>i2c.write(104,b'\x01\x00\x09\x08')#向I2C0的104地址写入数据`\x01\x00\x09\x08`
True
```

&emsp;&emsp;注意:

&emsp;&emsp;一般的IIC传感器都有寄存器的概念。视不同传感器而言,一般发送的数据前一位为寄存器地址,也就是`\x01\x00\x09\x08`中`0x01`为其寄存器地址,向`0x01`寄存器(含)往后将`x00\x09\x08`数据依次写入寄存器中去。也有些传感器使用的寄存器比较多,需要用两个字节表示寄存器地址,那么此时`\x01\x00\x09\x08`中`\x01\x00`表示其寄存器地址。向`0x01\x00`寄存器(含)往后将`x09\x08`数据依次写入寄存器中去。

##### 读数据

&emsp;&emsp;`i2c.read(addr,length)`

&emsp;&emsp;函数说明:从指定地址设备读取数据,返回读取对象,这个对象与I²C设备有关

&emsp;&emsp;`addr`: I²C设备地址,可由scan函数读取出来

&emsp;&emsp;`length`: 读入的字节数

```python
  from machine import I2C
  i2c = I2C(0)#构建I2C0
  i2c.write(118,b'\x08')#从第0x08寄存器开始读取
  print(i2c.read(118,10))#读取118地址上0x08寄存器往后的10个字节的数据
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
```

##### 从寄存器中读取数据

&emsp;&emsp;`i2c.readfrom_mem(addr,register,length)`

&emsp;&emsp;函数说明:从指定地址设备的指定寄存器开始往后连续读取各寄存器数据。

&emsp;&emsp;`addr`:从机设备IIC地址。`register`:操作的起始寄存器地址。`length`: 读入的字节数。

&emsp;&emsp;注意:此方法仅仅适用于寄存器用一个字节描述的IIC元器件

##### 向寄存器写入数据

&emsp;&emsp;`i2c.writeto_mem(addr,register,data)`

&emsp;&emsp;函数说明:向指定地址设备的指定寄存器开始往后连续将数据写入各寄存器中。

&emsp;&emsp;`addr`:从机设备IIC地址。`register`:操作的起始寄存器地址,`data`: 需要写入(发送)的数据。

&emsp;&emsp;注意:此方法仅仅适用于寄存器用一个字节描述的IIC元器件

## 示例

&emsp;&emsp;将waffle nano的一些GPIO引脚构造成I2C总线,读写相应数据(需要连接相应的传感器)。

```python
from machine import I2C, Pin
i2c = I2C(1, sda=Pin(0), scl=Pin(1),freq=100000) #构造IIC对象,将1号引脚设置为scl,将0号引脚设置为sda
print(i2c.scan()) #扫描IIC总线上的传感器,返回传感器ID

i2c.write(74,b'\x03') #向74号ID的传感器发送0x03数据

print(i2c.read(74,10)) #从74号ID的传感器读取10个字节数据

i2c.writeto_mem(105,0x03,b'\x01\x02') #向105号ID的传感器的0x03寄存器(含)以及其往后的寄存器中依次填入数据`0x01`、`0x02,每个寄存器分到1个字节。

print(i2c.readfrom_mem(105,0x80,128)) #从105号ID的传感器的0x80寄存器开始(含)依次读取128个寄存器的数据,并返回。
```