4. I2C通讯

I2C(IIC)协议是在电子设备中常用的通讯协议,通过它,我们可以对各种各样的电子器件进行控制(注:一般受控的设备为从机), 如常见的 姿态传感器MPU6050 、 温度图像传感器MLX90640 、 0.96寸OLED显示屏SSD1306 等等, 都有通过I2C通讯协议来和板卡(注:一般主动发起控制的为主机)进行通讯的。

关于I2C协议的具体内容,我们在此不作过多探究,大家可以自行搜索I2C通讯协议的相关知识学习。

本章中,我们的重点是使用前面介绍的 python-periphery 及 Adafruit Blinka 两个Python库, 来使用到我们鲁班猫板卡上的I2C主机。

4.1. I2C实验

一般情况下,我们需要一个具体的通讯对象来完成本节实验,即前面提到的各种类型的电子器件。

考虑到大家手中的模块或I2C设备各不相同,所以对上述Python库的使用介绍分为两部分, 一部分为库官方示例Demo介绍,另一部分则为使用库中对应的API,在实际的模块中使用介绍。

笔者使用的是 0.96寸OLED显示屏模块SSD1306 野火【OLED屏_I2C_0.96寸】 , 编写使用示例。 实际的使用中,鲁班猫板卡上的I2C主机能通讯的器件类型并不受限制。 因此笔者会在代码中对相关的代码进行详细注释,如大家手中没有相同的模块, 也可进行类似的操作,对不同的模块进行实验代码编写。

4.2. 实验准备

4.2.1. 板卡添加I2C资源

在板卡上的GPIO可能没有复用为I2C,可通过加载设备树插件来启用, LubanCat_RK系列板卡的引脚参考下: 《LubanCat-RK系列-40pin引脚对照图》

下面以LubanCat 2为例开启下i2c的设备树插件(LubanCat 2引出的引脚有两个i2c):

# 不同板卡的配置文件和i2c不同,以LubanCat 2为例,配置文件为uEnvLubanCat 2.txt,
# 可以开启I2c3、i2c5
# 登录系统终端,打开/boot/uEnv/uEnvLubanCat 2.txt文件

sudo vim /boot/uEnv/uEnvLubanCat 2.txt
#进入编辑模式,取消I2c3前面的注释,保存并退出文件,重启系统
broken

重启后,查看板卡的I2C资源是否加载:

# 在终端中输入如下命令,可以查看到I2C资源:
ls -l /dev/i2c-*

板卡上的I2C资源示例,仅供参考:

broken

一些简单i2c相关的测试参考下 《I2C通讯》

4.2.2. 硬件连接

鲁班猫LubanCat 2板卡,板上无I2C器件可进行相关实验现象展示, 故本节实验通过在硬件层面连接外围器件 0.96寸OLED显示屏SSD1306 为大家展示,使用的是I2c3。

连接如图,仅供参考(LubanCat 2的I2c3), oled屏和板卡40pin引脚连接表如下:

OLED屏幕引脚

对应GPIO

LubanCat 2板卡引脚

SDA

GPIO1_A0

I2C3_SDA

SCL

GPIO1_A1

I2C3_SCL

GND

GND

VCC

3.3V

4.3. 使用python-periphery

python-periphery 库支持的I2C功能是基于Linux的I2C系统实现的,所以要想利用该库使用到I2C的功能, 需要板卡提供支持。像鲁班猫板卡,就可以完美使用 python-periphery 库I2C通讯功能。 这样一来,就不需要我们在软件层面上利用GPIO模拟I2C主机功能了。

4.3.1. 安装 python-periphery

重要

如在前面小节中安装了此库,务必跳过此操作。

python-periphery 的安装方式如下:

# 在板卡使用如下命令安装
sudo pip3 install python-periphery

4.3.2. periphery使用I2C功能

先参考学习下 python-periphery 库进行I2C功能使用的示例代码:

官方提供的code example -i2c(学习参考)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from periphery import I2C

# 打开i2c1主机
i2c = I2C("/dev/i2c-1")

# 以EEPROM器件为例,从EEPROM内存地址0x100处读取数据,EEPROM器件的I2C设备地址为0x50
msgs = [I2C.Message([0x01, 0x00]), I2C.Message([0x00], read=True)]
# 开启I2C传输
i2c.transfer(0x50, msgs)
# 打印接受到的消息
print("0x100: 0x{:02x}".format(msgs[1].data[0]))
# 操作完成,关闭I2C主机以释放资源。
i2c.close()

代码说明:

  • 第4行,申请I2C资源,占用I2C1主机

  • 第7行,构造消息结构I2C.Message。I2C.Message中第一成员为发送的数据列表, 第二个指定此消息的作用,默认为I2C的写命令,在写命令的情况下,会将数据列表中的数据发送出去, 如果指定 read=True ,则此条消息对应为I2C的读命令,会将读取到的数据存放在data成员中。

  • 第9行,开启I2C传输,发送I2C从机设备的地址与构造好的消息结构

  • 第17行,释放I2C资源,释放I2C1主机

通过 python-periphery 库中给出的对EEPROM器件操作的代码示例, 我们可以掌握该库对I2C主机使用的基本方法,那么接下来就可以针对具体的器件展开编程使用了。

这里笔者使用前面所述的 0.96寸OLED显示屏SSD1306 模块编程展示:

配套代码 io/i2c/i2c_test_periphery.py文件内容
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
""" periphery i2c 测试 使用0.96寸OLED模块 """
import time
from periphery import I2C

# 打开 i2c-3 控制器
i2c = I2C("/dev/i2c-3")

# 设备从机地址0x3c,即OLED模块地址
I2CSLAVEADDR = 0x3C

# 使用periphery i2c库读取功能测试
def i2c_read_reg(devregaddr):
    """
    使用periphery i2c库读取功能测试
    """
    # 构造数据结构
    # 第一个Message为要读取的设备地址
    # 第二个Message用于存放读取回来的消息,注意其中添加的read=True
    msgs = [I2C.Message([devregaddr]), I2C.Message([0x00], read=True)]
    # 发送消息,发送到i2cSlaveAddr
    i2c.transfer(I2CSLAVEADDR, msgs)
    print("从寄存器 0x{:02x} 读取出: 0x{:02x}".format(devregaddr, msgs[1].data[0]))


def oled_write_cmd(cmd):
    """
    使用periphery i2c库发送功能测试
    """
    msgs = [I2C.Message([0x00, cmd])]
    i2c.transfer(I2CSLAVEADDR, msgs)


def oled_write_data(data):
    """
    使用periphery i2c库发送功能测试
    """
    msgs = [I2C.Message([0x40, data])]
    i2c.transfer(I2CSLAVEADDR, msgs)


def oled_init():
    """
    使用OLED模块进行代码功能测试 0.96寸OLED模块初始化
    """
    time.sleep(1)
    oled_write_cmd(0xAE)
    oled_write_cmd(0x20)
    oled_write_cmd(0x10)
    oled_write_cmd(0xB0)
    oled_write_cmd(0xC8)
    oled_write_cmd(0x00)
    oled_write_cmd(0x10)
    oled_write_cmd(0x40)
    oled_write_cmd(0x81)
    oled_write_cmd(0xFF)
    oled_write_cmd(0xA1)
    oled_write_cmd(0xA6)
    oled_write_cmd(0xA8)
    oled_write_cmd(0x3F)
    oled_write_cmd(0xA4)
    oled_write_cmd(0xD3)
    oled_write_cmd(0x00)
    oled_write_cmd(0xD5)
    oled_write_cmd(0xF0)
    oled_write_cmd(0xD9)
    oled_write_cmd(0x22)
    oled_write_cmd(0xDA)
    oled_write_cmd(0x12)
    oled_write_cmd(0xDB)
    oled_write_cmd(0x20)
    oled_write_cmd(0x8D)
    oled_write_cmd(0x14)
    oled_write_cmd(0xAF)


def oled_fill(filldata):
    """
    清空OLED屏幕
    """
    for i in range(8):
        oled_write_cmd(0xB0 + i)
        # page0-page1
        oled_write_cmd(0x00)
        # low column start address
        oled_write_cmd(0x10)
        # high column start address
        for j in range(128):
            oled_write_data(filldata)


# 代码测试
try:
    # 初始化OLED屏幕,SSD1306必要
    oled_init()
    # 清空OLED屏幕
    oled_fill(0xFF)
    # 读取寄存器测试
    i2c_read_reg(0x10)
finally:
    print("测试正常结束")
    # 释放资源
    i2c.close()

关于通过I2C主机发送何种命令才能去控制 0.96寸OLED显示屏SSD1306 模块不是本教程的重点, 教程目的是通过提供的示例代码,让大家对相关库的API功能使用有个参考对象。

4.3.2.1. 实验操作

示例代码使用LubanCat 2板卡,操作如下:

# 在板卡i2c_test.py所在的目录执行如下命令
sudo python3 i2c_test_periphery.py

# 可看到连接的OLED屏幕被清空显示色块,并在终端中打印出了读取的寄存器信息

显示参考(随意拿的一个旧的屏幕~):

broken

4.4. 方式二:使用Adafruit Blinka

Adafruit Blinka 与 python-periphery 功能类似,I2C通讯也是基于Linux的I2C子系统实现的。 只是在代码上的用法上有较大差异。

4.4.1. 安装Adafruit Blinka

重要

如在前面小节中安装了此库,务必跳过此操作。

Adafruit Blinka 的安装方式如下:

# 在板卡使用如下命令安装
sudo apt -y install python3-libgpiod python-periphery # 注:如已安装,忽略此步
sudo pip3 install Adafruit-Blinka

sudo pip3 install machine

4.4.2. Adafruit-Blinka使用I2C主机

使用这个 Adafruit Blinka 进行输入输出的示例代码如下:

官方提供的code example - i2c
1
2
3
4
5
6
7
8
9
import busio
from board import *

# 申请I2C资源,占用I2C1主机
i2c = busio.I2C(SCL, SDA)
# 打印扫描到的挂载在I2C1主机下的从机设备
print(i2c.scan())
# 操作完成,关闭I2C主机以释放资源
i2c.deinit()

Adafruit Blinka 库提供的I2C使用示例较为简单,仅仅只是提供了扫描挂载在I2C1主机下的设备, 其他的API功能具体的使用方法大家可以参考: Blinka I2C 下给出的介绍。

这里直接展示使用I2C1主机与 0.96寸OLED显示屏模块SSD1306 模块通讯的程序:

配套代码 io/i2c/i2c_test_adafruit.py文件内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
""" blinka i2c 测试 使用0.96寸OLED模块 """
import board
import busio

# 初始化OLED命令字节数组
OledInitBuf = bytes(
    [
        0xAE,
        0x20,
        0x10,
        0xB0,
        0xC8,
        0x00,
        0x10,
        0x40,
        0x81,
        0xFF,
        0xA1,
        0xA6,
        0xA8,
        0x3F,
        0xA4,
        0xD3,
        0x00,
        0xD5,
        0xF0,
        0xD9,
        0x22,
        0xDA,
        0x12,
        0xDB,
        0x20,
        0x8D,
        0x14,
        0xAF,
    ]
)

# 数据发送、接受缓冲区
OutBuffer = bytearray(1)
InBuffer = bytearray(1)

try:
    # 申请i2c资源
    i2c = busio.I2C(board.I2C3_SCL, board.I2C3_SDA)
    # 扫描I2C设备地址,测试
    print("挂载I2C总线上的I2C设备地址有")
    for i in i2c.scan():
        print("0x%02x " % i)

    # IIC发送命令测试(writeto),初始化OLED
    # 初始化OLED,初始命令存放于字节数组oledInitBuf中
    i2c.writeto(0x3C, OledInitBuf)

    # IIC读取命令测试(writeto_then_readfrom),读取OLED的0x10寄存器
    # 发送数据,发送数据完成后不产生停止信号,并重新生产起始信号进行读取。

    # 发送数据内容存放在字节数组outBuffer中,内容为OLED寄存器地址:0x10
    OutBuffer[0] = 0x10
    # 发送,发送寄存器地址0x10,并将寄存器地址0x10下的内容读取到inBuffer中
    i2c.writeto_then_readfrom(0x3C, OutBuffer, InBuffer)
    # 打印数据信息
    print("(writeto_then_readfrom)读取到的内容为:0x%02x" % InBuffer[0])

    # IIC读取测试(readfrom_into),从设备内部的地址读,则不需要指定读取的地址
    i2c.readfrom_into(0x3C, InBuffer)
    # 打印数据信息
    print("(readfrom_into)读取到的内容为:0x%02x" % InBuffer[0])

    # OLED清屏
    for i in range(8):
        i2c.writeto(0x3C, bytearray([0x00, 0xB0 + i]))  # page0-page1
        i2c.writeto(0x3C, bytearray([0x00, 0x00]))  # low column start address
        i2c.writeto(0x3C, bytearray([0x00, 0x10]))  # high column start address
        for j in range(128):
            i2c.writeto(0x3C, bytearray([0x40, 0xFF]))

finally:
    # 释放i2c总线资源
    i2c.deinit()

代码说明:

  • 第45行,申请I2C资源,占用I2C1主机

  • 第53行,将字节数组OledInitBuf里的数据,发送到挂载在I2C1主机下的从设备,从设备地址为0x3C

  • 第59~61行,将字节数组OutBuffer里的数据,发送到挂载在I2C1主机下的从设备, 并在从设备寄存器地址0x10处读取一个数据到字节数组InBuffer,从设备地址为0x3C

  • 第66行,在从设备内部的当前地址读,读取一个数据到字节数组InBuffer

  • 第80行,释放I2C资源,释放I2C1主机

实验操作与上一小节相同,将程序在板卡上运行即可。

代码片段的 busio.I2C(SCL, SDA) 中的SCL、SDA是在Blinka库定义好的LubanCat板卡资源。

更详细的引脚定义可直接查看 Adafruit Blinka的源码 :

4.4.3. Adafruit_CircuitPython_SSD1306

在前面我们通过Blinka库,实现了通过I2C主机控制 0.96寸OLED显示屏SSD1306 模块, Blinka库是基于 CircuitPython 重新实现的实时组件对象。 简而言之,就是利用Blinka库开发的Python程序,可以实时去使用到板卡上的硬件资源。 如果对 CircuitPython 熟悉的同学,就会知道基于 CircuitPython 库实现的、 对各种各样的电子器件实施控制的代码Demo是非常多的。

那么也就是说,是不是我们的鲁班猫板卡适配好了Blinka库,即可利用 CircuitPython 丰富的 各种传感器的控制Demo呢?以笔者做上述实验时使用的模块为例, 0.96寸OLED显示屏SSD1306 模块的Demo可以正常使用。

那么下面笔者就以该SSD1306模块为例,为大家演示一下,如何将 CircuitPython 为 0.96寸OLED显示屏SSD1306 模块提供支持的程序, 运行在我们的鲁班猫板卡上。

首先找到对应的仓库,这里为大家演示的是 0.96寸OLED显示屏SSD1306, 那么大家可以在 Adafruit的Github上 Adafruit Github 搜索关键字 SSD1306, 可以看到 Adafruit_CircuitPython_SSD1306, 这就是要使用到的仓库了。当然大家如果需要, 也可以搜索其他常见的模块比如dht11、mpu6050等等等等,就需要大家自行探索了。

总之更多Demo仓库 关注 Adafruit Github

在对应仓库下方,我们可以看到对应库的添加方法:

# 在板卡使用如下命令安装对应的传感器库,下列语句二选一:

# 在root用户下输入:
pip3 install adafruit-circuitpython-ssd1306
# 如为普通用户输入:
sudo pip3 install adafruit-circuitpython-ssd1306

或者大家可以通过其他途径将下载好的仓库上传到板卡中,并进入仓库目录执行以下语句安装:

sudo python3 setup.py install

由于板卡性能和网络原因,安装过程会十分漫长,请耐心等待。如果安装过程中发送错误, 可以更换上述方法中的另外一种再次尝试。

安装完成后,即可使用仓库中提供的各种示例Demo了!需要注意的是部分Demo中从设备的地址 并非能完全适配你手中的模块,所以当出现报错信息时,可能需要修改代码Demo中的从设备的地址。

CircuitPython 为 0.96寸OLED显示屏SSD1306 模块提供的代码Demo如下图示例:

broken

最后在终端中运行任意Demo,查看一下效果:

sudo python3 ssd1306_stats.py

即可实时显示鲁班猫板卡运行状态等信息:

broken