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 ,对上述提到的库进行使用示例的编写。 实际的使用中,鲁班猫板卡上的I2C主机能通讯的器件类型并不受限制。 因此笔者会在代码中对相关的代码进行详细注释,如大家手中没有相同的模块, 也可进行类似的操作,对不同的模块进行实验代码编写。

4.2. 实验准备

在开始实验前,需要做一些准备。

4.2.1. 添加I2C资源

在板卡上的部分资源可能默认未被开启,在使用前请根据需要修改 /boot/uEnv.txt 文件, 可添加对应设备树插件的加载,重启系统,以在系统中添加对应资源。

如本节实验中,通常情况下在鲁班猫系统中默认使能 I2C 的功能, 如大家发现I2C的设备树插件未加载,请做相应修改。

同时如果大家在使用中发现I2C功能引脚被复用或有冲突,请要将对应占用I2C引脚资源的设备树插件取消加载, 否则I2C资源可能无法使用。若已开启对应资源,忽略以下步骤。

方法参考如下:

broken

添加I2C设备树插件,以使系统支持I2C功能,如下:

broken
# 以鲁班猫i.MX6ULL板卡I2C设备树插件内容为例:
dtoverlay=/usr/lib/linux-image-4.19.35-imx6/overlays/imx-fire-i2c1.dtbo
dtoverlay=/usr/lib/linux-image-4.19.35-imx6/overlays/imx-fire-i2c2.dtbo

如若运行代码时出现“Device or resource busy”或者运行代码卡死等等现象, 请按上述情况检查并按上述步骤操作。

如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。

# 在终端中输入如下命令,可以查看到I2C资源:
ls /sys/bus/i2c/devices

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

broken

4.2.2. 硬件连接

注:硬件连接小节,需要大家根据自身开发环境等情况对本章内容进行参考调整。 如您使用的鲁班猫板卡上已经板载了I2C设备,无需再外接设备也可进行实验。

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

本章实验中使用的I2C主机为鲁班猫i.MX6ULL MINI板卡上的I2C1主机, 对应的引脚可在 《鲁班猫板卡Pinout》 找到。 大家可根据自身鲁班猫板卡实际设备情况,调整使用的主机和器件连接等等。

笔者连接如图,仅供参考:

broken

4.3. 方式一:使用python-periphery

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

关于I2C子系统相关内容,参考 《Linux 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/periphery/i2c_test.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-1 控制器
i2c = I2C("/dev/i2c-1")

# 设备从机地址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设备的从机地址,关于设备从机地址如何去获取, 可以参考 《Linux I2C通讯》 章节。

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

4.3.2.1. 实验操作

示例代码使用LubanCat i.MX6ULL MINI板卡,操作如下:

# 确认已安装python-periphery包
# 确认使能了I2C设备树插件

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

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

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

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/blinka/i2c_test.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模块 """
from board import *
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(SCL, 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板卡资源。

当我们申请对应的板卡资源的时候,它会检测板卡型号,然后根据板卡信息加载相应的资源定义, 所以我们可以直接使用定义好的资源名, 使用起来相对 python-periphery 会更直观。

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

这些文件里定义的引脚与板卡的关系可查看:敬请期待

4.5. 拓展内容

在前面我们通过Blinka库,实现了通过I2C1主机控制 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,查看一下效果:

python3 ssd1306_stats.py

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

broken