4. I2C通讯

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

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

本章中,我们的重点是使用前面介绍的 python-peripheryAdafruit 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 Blinkapython-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模块为例,为大家演示一下,如何将 CircuitPython0.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中的从设备的地址。

CircuitPython0.96寸OLED显示屏SSD1306 模块提供的代码Demo如下图示例:

broken

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

python3 ssd1306_stats.py

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

broken