13. I2C通讯

本章介绍在应用层中使用I2C总线与外部设备的通讯,讲解Linux系 统总线类型设备驱动架构的应用。

在Linux内核文档的Documentation/i2c目录下有关于I2C驱动非常详细的说明。

本章节的示例代码目录为:base_code/linux_app/i2c

13.1. I2C通讯协议简介

I2C 通讯协议(Inter-Integrated Circuit)是由Phiilps公司开发的, 由于它引脚少,硬件实现简单,可扩展性强,不需要USART、CAN等通讯协议的外部收发设备, 被广泛地使用在多个集成电路(IC)间的通讯。

下面我们分别对I2C协议的物理层及协议层进行讲解。

13.1.1. I2C物理层

I2C通讯设备之间的常用连接方式如下图。

常见的I2C通讯系统

它的物理层有如下特点:

  • 它是一个支持多设备的总线。“总线”指多个设备共用的信号线。 在一个I2C通讯总线中,可连接多个I2C通讯设备,支持多个通讯主机及多个通讯从机。
  • 一个I2C总线只使用两条总线线路,一条双向串行数据线(SDA) ,一条串行时钟线 (SCL)。 数据线即用来表示数据,时钟线用于数据收发同步。
  • 每个连接到总线的设备都有一个独立的设备地址,主机可以利用这个地址进行不同设备之间的访问。 其中地址是一个七位或十位的数字。
  • 总线通过上拉电阻接到电源。当I2C设备空闲时,会输出高阻态,而当所有设备都空闲, 都输出高阻态时,由上拉电阻把总线拉成高电平。
  • 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。
  • 具有三种传输模式:标准模式传输速率为100kbit/s ,快速模式为400kbit/s , 高速模式下可达 3.4Mbit/s,但目前大多I2C设备尚不支持高速模式。
  • 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制 。

13.1.2. 协议层

I2C的协议定义了通讯的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。

13.1.2.1. I2C基本读写过程

先看看I2C通讯过程的基本结构,它的通讯过程常有如下三种方式。

主机写数据到从机 主机由从机中读数据 I2C通讯复合格式

图例:

  • i2cbus006 :数据由主机传输至从机
  • S : 传输开始信号
  • SLAVE_ADDRESS: 从机地址
  • i2cbus007 :数据由从机传输至主机
  • R : 传输方向选择位,1为读,0为写
  • A : 应答(ACK)或非应答(NACK)信号
  • P : 停止传输信号

这些图表示的是主机和从机通讯时,SDA线的数据包序列。

  1. 其中S表示由主机的I2C接口产生的传输起始信号(S),这时连接到I2C总线上的所有从机都会接收到这个信号。
  2. 起始信号产生后,所有从机就开始等待主机紧接下来广播 的从机地址信号 (SLAVE_ADDRESS)。 在I2C总线上,每个设备的地址都是唯一的,当主机广播的地址与某个设备地址相同时,这个设备就被选中了,没被选中的设备将会忽略之后的数据信号。 根据I2C协议,这个从机地址可以是7位或10位。
  3. 在地址位之后,是传输方向的选择位,该位为0,表示后面的数据传输方向是由主机传输至从机,即主机向从机写数据。该位为1,则相反,即主机由从机读数据。
  4. 从机接收到匹配的地址后,主机或从机会返回一个应答(ACK)或非应答(NACK)信号,只有接收到应答信号后,主机才能继续发送或接收数据。

写数据方向:

若配置的方向传输位为 “写数据” 方向,即第一幅图的情况,广播完地址,接收到应答信号后, 主机开始正式向从机 传输数据(DATA) ,数据包的大小为8位,主机每发送完一个字节数据, 都要等待从机的应答信号(ACK),重复这个过程,可以向从机传输N个数据,这个N没有大小限制。 当数据传输结束时,主机向从机发送一个停止传输信号(P),表示不再传输数据。

读数据方向:

若配置的方向传输位为 “读数据” 方向,即第二幅图的情况,广播完地址,接收到应答信号后, 从机开始向主机 返回数据(DATA) ,数据包大小也为8位,从机每发送完一个数据, 都会等待主机的应答信号(ACK),重复这个过程,可以返回N个数据,这个N也没有大小限制。 当主机希望停止接收数据时,就向从机返回一个非应答信号(NACK),则从机自动停止数据传输。

复合格式:

除了基本的读写,I2C通讯更常用的是 复合格式 ,即第三幅图的情况,该传输过程有 两次起始信号(S) 。 一般在第一次传输中,主机通过SLAVE_ADDRESS寻找到从设备后,发送一段“数据”, 这段数据通常用于表示从设备内部的寄存器或存储器地址(注意区分它与SLAVE_ADDRESS的区别); 在第二次的传输中,对该地址的内容进行读或写。也就是说,第一次通讯是告诉从机读写地址,第二次则是读写的实际内容。

以上通讯流程中包含的起始、停止、数据有效性、地址和数据方向以及响应的说明按小节如下。

13.1.2.2. 通讯的起始和停止信号

前文中提到的起始(S)和停止(P)信号是两种特殊的状态,起始和停止信号一般由主机产生。如下图。

  • 当 SCL 线是高电平时 SDA 线从高电平向低电平切换,这个情况表示通讯的起始。
  • 当 SCL 是高电平时 SDA 线由低电平向高电平切换,表示通讯的停止。

如下图:

起始和停止信号

13.1.2.3. 数据有效性

I2C使用SDA信号线来传输数据,使用SCL信号线进行数据同步,如下图。 SDA数据线在SCL的每个时钟周期传输一位数据。

  • 传输时,SCL为高电平的时候SDA表示的数据有效,即此时的SDA为高电平时表示数据“1”,为低电平时表示数据“0”。
  • 当SCL为低电平时,SDA的数据无效,一般在这个时候SDA进行电平切换,为下一次表示数据做好准备。

如下图:

数据有效性

每次数据传输都以字节为单位,每次传输的字节数不受限制。

13.1.2.4. 地址及数据方向

I2C总线上的每个设备都有自己的独立地址,主机发起通讯时,通过SDA信号线发送设备地址(SLAVE_ADDRESS)来查找从机。 I2C协议规定设备地址可以是7位或10位,实际中7位的地址应用比较广泛。

紧跟设备地址的一个数据位用来表示数据传输方向,它是数据方向位(R/),第8位或第11位。 数据方向位为“1”时表示主机由从机读数据,该位为“0”时表示主机向从机写数据。

如下图:

设备地址(7位)及数据传输方向
  • 读数据方向时,主机会释放对SDA信号线的控制,由从机控制SDA信号线,主机接收信号。
  • 写数据方向时,SDA由主机控制,从机接收信号。

13.1.2.5. 响应

I2C的数据和地址传输都带响应。响应包括“应答(ACK)”和“非应答(NACK)”两种信号。 作为数据接收端时,当设备(无论主从机)接收到I2C传输的一个字节数据或地址后:

  • 若希望对方 继续发送数据 ,则需要向对方发送 “应答(ACK)” 信号,发送方会继续发送下一个数据;
  • 若接收端希望 结束数据传输 ,则向对方发送 “非应答(NACK)” 信号,发送方接收到该信号后会产生一个停止信号,结束信号传输。如下图。

如下图:

响应与非响应信号

传输时主机产生时钟,在第9个时钟时,数据发送端会释放SDA的控制权,由数据接收端控制SDA,若SDA为高电平,表示非应答信号(NACK),低电平表示应答信号(ACK)。

13.2. 使能IIC通信接口

I.MX6ULL有4个I2C外设,MPU6050连接到了它的IIC 1接口,参考前面《fire-config工具简介》章节,使能IIC_1通信接口。

如下图:

未找到图片

13.3. 检查IIC 设备

使能IIC_1通信接口后,我们可以通过系统中的IIC设备文件来检查设备的一些基本信息,下面简单介绍几个查看命令。

1
2
#查看系统存在的I2C总线
ls  /sys/bus/i2c/devices

如下图,不同版本的Debian系统输出内容可能有差别。

未找到图片

IIC 1接口对应的驱动设备文件为上图中的i2c-0,这是因为驱动是从0开始编号的。 因此应用程序一般通过操作设备文件i2c-0实现对MPU6050的操作。不止MPU6050, 理论上任何连接到 IIC 1 接口上的设备(触摸、摄像头、oled)都可以通过 i2c-0 设备文件来实现具体的功能。

13.4. IIC 第三方工具- i2c-detect

使用i2c-tools工具包提供了一些非常方便的工具来对系统的I2C总线进行调试。使用之前要使用apt 命令安装i2c-tools。下面介绍i2c-tools的安装步骤并使用i2c-tools提供的命令查看系统IIC 设备。

13.4.1. i2c-detect工具安装

在开发板的控制台执行命令:

1
sudo apt install i2c-tools -y

安装后可使用的命令有i2cdetect、i2cdump、i2cset以及i2cget,它们分别用于扫描I2C总线上的设备、读写指定设备的寄存器内容。 命令介绍如下:

13.4.2. i2cdetect命令

i2cdetect :用于扫描I2C总线上的设备。它会打印一个表,其中包含了总线上检测到的设备。

相关命令语法:

i2cdetect [-y] [-a] [-q|-r] i2cbus [first last]:

参数说明:

  • 参数y:关闭交互模式,使用该参数时,不会提示警告信息。
  • 参数a:扫描总线上的所有设备。
  • 参数q:使用SMBus的“quick write”命令进行检测,不建议使用该参数。
  • 参数r:使用SMBus的“receive byte”命令进行检测,不建议使用该参数。
  • 参数i2cbus:指定i2c总线的编号
  • 参数first、last:扫描的地址范围

返回值说明:

  • ‘–’:表示该地址被检测,但没有芯片应答;
  • ‘UU’:表示该地址当前由内核驱动程序使用。
  • ‘**’:**表示以十六进制表示的设备地址编号,如 “2d”或“4e”。

13.4.3. i2cdetect 使用示例

i2cdetect主要用于查看当前总线上的设备,我们这里查看IIC 1接口上的设备,重点关注MPU6050、显示屏触摸芯片、oled显示屏(需要使用杜邦线外接)

首先不连接显示屏和oled 执行如下命令:

1
i2cdetect -a 0

参数 “-a” 扫描总线上的所有设备。 参数“0” 表示标号为0的IIC 总线,既 IIC 1 。

输出内容如下所示:

未找到图片

上图中 “68” 是MPU6050的设备地址。

连接显示屏(显示屏40pin 的排线中包含了触摸的IIC ,所以接上显示屏也就将触摸芯片接入 IIC 1 总线),再次执行查看设备命令,如下所示:

1
i2cdetect -a 0

如下图:

未找到图片

新增加的IIC 地址5d,就是我们新接入的触摸芯片IIC 地址。

13.4.3.1. i2cdetect其他命令

  • i2cdetect -F i2cbus:查询i2c总线的功能,参数i2cbus表示i2c总线
  • i2cdetect -V:打印软件的 -
  • i2cdetect -l:检测当前系统有几组i2c总线

13.4.4. i2cget命令

i2cget:读取指定IIC设备的某个寄存器的值

相关命令语法:

i2cget [-f] [-y] i2cbus chip-address [data-address [mode]]

参数说明:

  • 参数f:强制访问设备。
  • 参数y:关闭交互模式,使用该参数时,不会提示警告信息。
  • 参数i2cbus:指定i2c总线的编号
  • 参数chip-address:i2c设备地址
  • 参数data-address:设备的寄存器的地址
  • 参数mode:参考i2cdump命令。

13.4.5. i2cset命令

i2cget:写入指定IIC设备的某个寄存器的值

相关命令语法:

i2cset [-f] [-y] [-m mask] [-r] i2cbus chip-address data-address [value] … [mode]

参数说明:

  • 参数f:强制访问设备。
  • 参数y:关闭交互模式,使用该参数时,不会提示警告信息。
  • 参数m:
  • 参数r:写入后立即回读寄存器值,并将结果与写入的值进行比较。
  • 参数i2cbus:指定i2c总线的编号
  • 参数chip-address:i2c设备地址
  • 参数data-address:设备的寄存器的地址
  • 参数value:要写入寄存器的值
  • 参数mode:参考i2cdump命令

13.4.6. i2cdump命令

i2cdump:读取指定设备的全部寄存器的值。

相关命令语法:

i2cdump [-f] [-r first-last] [-y] i2cbus address [mode [bank [bankreg]]]

参数说明:

  • 参数r:指定寄存器范围,只扫描从first到last区域;
  • 参数f:强制访问设备。
  • 参数y:关闭人机交互模式;
  • 参数i2cbus:指定i2c总线的编号
  • 参数address:指定设备的地址
  • 参数mode:指定读取的大小, 可以是b, w, s或i,分别对应了字节,字,SMBus块, I2C块

i2cdump -V:打印软件的版本号

13.5. 读取陀螺仪传感器数据实验

13.5.1. 实验说明

本教程将通过IIC接口读取板载陀螺仪(MPU6050)的原始数据(MINI开发板没有板载陀螺仪,想要完成本实验需要参照Pro开发板外接MPU6050传感器)。 在测试程序中大约每一秒读取并显示一次MPU6050的原始数据。读取得到的原始数据并没有进行处理,所以不要误以为读取得到的是角度值。

查看IIC设备文件,确保IIC 1接口已经使能

1
ls  /sys/bus/i2c/devices

如下图:

未找到图片

“i2c-0” 就是MPU6050使用到的 IIC 1接口总线。

13.5.2. 程序分析

陀螺仪传感器数据读取程序完整源码,请参考本小节配套源码(位于 base_code/linux_app/i2c/mpu6050/sources 目录下)。

MPU6050示例(base_codelinux_appi2cmpu6050sourcesmain.c文件)
 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
/************************第一部分***********************/
//MPU6050初始化
static uint8 MPU6050_Init(void)
{
    fd  = open("/dev/i2c-0", O_RDWR);               // 以读写权限打开IIC 1设备文件

    if (fd < 0)
    {
        perror("Can't open /dev/MPU6050 \n");
        exit(1);
    }

    printf("open /dev/i2c-0 success !\n");

    if (ioctl(fd, I2C_SLAVE, Address) < 0)         //设置IIC设备地址
    {
        printf("fail to set i2c device slave address!\n");
        close(fd);
        return - 1;
    }

    printf("set slave address to 0x%x success!\n", Address);
    i2c_write(fd, PWR_MGMT_1, 0X00);               //配置电源管理,0x00,正常启动
    i2c_write(fd, SMPLRT_DIV, 0X07);               //设置MPU6050的输出分频既设置采样频率
    i2c_write(fd, CONFIG, 0X06);                   //配置数字低通滤波器和帧同步引脚采样
    i2c_write(fd, ACCEL_CONFIG, 0X01);             //设置量程和 X、Y、Z 轴加速度自检
    return (1);
}

/************************第二部分***********************/
//MPU6050 wirte byte
static uint8 i2c_write(int fd, uint8 reg, uint8 val)
{
    int retries;
    uint8 data[2];

    data[0] = reg;
    data[1] = val;

    for (retries = 5; retries; retries--)
    {
        if (write(fd, data, 2) == 2)
        {
            return 0;

        }

        usleep(1000 * 10);
    }

    return - 1;
}

/************************第三部分***********************/
//MPU6050 read byte
static uint8 i2c_read(int fd, uint8 reg, uint8 * val)
{
    int retries;

    for (retries = 5; retries; retries--)
    {
        if (write(fd, &reg, 1) == 1)
        {
            if (read(fd, val, 1) == 1)
            {
                return 0;
            }

        }

    }

    return - 1;
}

/************************第四部分***********************/
//get data
short GetData(unsigned char REG_Address)
{
    char H, L;

    i2c_read(fd, REG_Address, &H);
    usleep(1000);
    i2c_read(fd, REG_Address + 1, &L);
    return (H << 8) +L;
}

这部分代码由四个函数组成,它们都是由系统接口API(ioctl、read、write)函数实现的,结合代码简单说明如下:


1. 第一部分,MPU6050初始化函数。初始化的过程实际就是打开设备然后写入配置参数。代码第5行使用可读可写方式打开IIC 设备文件。 代码第14行,设置MPU6050的IIC 从地址。代码22到26行设置MPU6050采样精度和量程等等。

2. 第二部分,MPU6050写函数。写入成功,返回0,失败返回 -1。函数参数共有三个,fd,文件描述符。reg,要写入的MPU6050寄存器地址。 val, 要写入的值。在函数内部实际是使用 write 函数依次写入MPU6050寄存器地址和要写入的值。

3. 第三部分,MPU6050读函数。返回值与MPU6050写函数相同。在函数中使用 write 函数写入要读取的MPU6050寄存器地址, 然后使用read 函数读取即可得到MPU6050寄存器值。

4. 第四部分,获取MPU6050数据函数。返回值为读取得到的MPU6050数据,函数实现很简单, 使用 i2c_read 函数读取MPU6050数据寄存器即可。

读取MPU6050原始数据
 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
/************************第一部分*********************/
#define ACCEL_XOUT_H                                0x3B
#define ACCEL_XOUT_L                                0x3C
#define ACCEL_YOUT_H                                0x3D
#define ACCEL_YOUT_L                                0x3E
#define ACCEL_ZOUT_H                                0x3F
#define ACCEL_ZOUT_L                                0x40


#define GYRO_XOUT_H                                 0x43
#define GYRO_XOUT_L                                 0x44
#define GYRO_YOUT_H                                 0x45
#define GYRO_YOUT_L                                 0x46
#define GYRO_ZOUT_H                                 0x47
#define GYRO_ZOUT_L                                 0x48

/************************第二部分*********************/
// main
int main(int argc, char * argv[])
{
    MPU6050_Init();
    usleep(1000 * 100);

    while (1)
    {
        //printf("\033[2J");
        usleep(1000 * 200);
        printf("ACCE_X:%6d\n ", GetData(ACCEL_XOUT_H));
        usleep(1000 * 200);
        printf("ACCE_Y:%6d\n ", GetData(ACCEL_YOUT_H));
        usleep(1000 * 200);
        printf("ACCE_Z:%6d\n ", GetData(ACCEL_ZOUT_H));
        usleep(1000 * 200);
        printf("GYRO_X:%6d\n ", GetData(GYRO_XOUT_H));
        usleep(1000 * 200);
        printf("GYRO_Y:%6d\n ", GetData(GYRO_YOUT_H));
        usleep(1000 * 200);
        printf("GYRO_Z:%6d\n\n ", GetData(GYRO_ZOUT_H));
        sleep(1);
    }

    close(fd);
}

代码实现非常简单,第一部分是MPU6050数据寄存器,通过读取这些寄存器可以获得MPU6050的三轴加速度和三轴陀螺仪数据。 第二部分是在main函数中通过GetData函数读取数据并使用printf函数进行打印,读取间隔大约为1秒。

13.5.3. 下载验证

对于ARM架构的程序,可使用如下步骤进行编译:

1
2
3
#在主机的实验代码Makefile目录下编译
#编译arm平台的程序
make ARCH=arm

编译后生成的ARM平台程序为build_arm/mpu6050_demo,使用网络文件系统共享至开 发板,在开发板的终端上测试即可。

如下图:

未找到图片

13.6. OLED显示屏显示实验

本实验使用OLED显示屏如下所示

未找到图片

本实验的配套程序适配 IIC_1通信接口的 OLED 显示屏,分辨率128*64 。如果使用的是其他OLED显示屏必须保证支持IIC接口。

13.6.1. 硬件连接

开发板自身不带OLED显示屏,本实验需要用杜邦线将OLED显示屏连接到 IIC_1接口。 如果使用的是野火OLED 显示屏,OLED供电电压为3.3V(特别注意,OLED不兼容5V,接错电压可能烧坏OLED显示屏), OLED显示屏与Pro开发板的连接方法为:OLED显示屏的SDA、SCL引脚分别连接到开发板的IO1.29、IO1.28引脚, 其VCC、GND连接到开发板的3.3V、GND引脚。

如下图:

未找到图片

13.7. 使能IIC通信接口

I.MX6ULL有4个I2C外设,OLED显示屏连接到了它的IIC_1接口,参考前面《fire-config工具简介》章节,使能IIC_1通信接口。

13.7.1. 代码分析

oled程序完整源码,请参考本小节配套源码(位于 base_code/linux_app/i2c/oled/sources 目录下)。

oled 写函数 oled_i2c_write ,oled 写函数用于向oled发送命令(配置参数)和要显示的数据,函数原型如下所示:

oled(base_codelinux_appi2coledsourcesmain.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
static uint8 oled_i2c_write(int fd, uint8 reg, uint8 val)
{
    /*******************第一部分************************/
    int retries;
    uint8 data[2];
    int write_error = 0;

    data[0] = reg;
    data[1] = val;

    ioctl(fd, I2C_SLAVE, OLED_ADDRESS);
    /*******************第二部分************************/
    for (retries = 5; retries; retries--)
    {
        if (write(fd, data, 2) == 2)
        {
            return 0;
        }
        usleep(1000);
    }
    return -1;
}

函数oled_i2c_write共有三个参数。fd, 打开的设备文件描述符,成功打开IIC设备文件后得到。reg,指定发送到的类型,这里分为命令和数据,在本程序中 只有两个值可选,并且在程序中已经通过宏定义设置了,具体如下:

oled(base_codelinux_appi2coledsourcesmain.c文件)
1
2
#define OLED_COMMEND_ADDR 0x00
#define OLED_DATA_ADDR 0x40

reg = 0x00, 表示发送的是命令,更准确的说是OLED配置参数、控制参数。reg = 0x40, 表示发送的是数据。 val , 指定要发送的内容。

函数实现分为两部分。第一部分,将函数入口参数保存到局部变量 data[] 数组中,便于后面执行发送,调用ioctl 函数设置 IIC 从地址既oled 的地址,当oled检测到与自己对应的 地址时就会响应,这时就可通信了。 oled 地址定义如下所示:

oled(base_codelinux_appi2coledsourcesmain.c文件)
1
#define OLED_ADDRESS 0x3C   //通过调整0R电阻,屏可以0x78和0x7A两个地址 -- 默认0x78

野火 oled 显示屏默认的IIC从地址为0X78,通过调整电阻可以设置为0X7A,需要注意的是,这里的地址是8位地址,最后一位表示的是读或者写。而我们这里要发送的是IIC设备的 7位地址,如上代码所示,我们在宏定义中设置的IIC 地址是由0x78 左移一位得到的。

第二部分,执行发送,如果一次发送不成功则循环发送5次,都失败的情况下返回-1,有一次成功则返回 0 。如果使用的是其他OLED显示屏必须保证支持IIC接口。

oled 初始化函数OLED_Init。oled初始化函数实现很简单。只需要使用open 打开IIC_1接口的设备文件后,使用上面所讲的oled_i2c_write函数写入配置参数即可,部分代码如下所示 完整代码请参考本实验源码。

oled初始化(base_codelinux_appi2coledsourcesmain.c文件)
 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
void OLED_Init(void)
{
  /*---------------------第一部分---------------------*/
    fd = open("/dev/i2c-0", O_RDWR); // open file and enable read and  write

    if (fd < 0)
    {
        perror("Can't open /dev/i2c-0 \n"); // open i2c dev file fail
        exit(1);
    }

    /*发送 从设备地址, 这里就是 oled的地址*/
    if (ioctl(fd, I2C_SLAVE, OLED_ADDRESS) < 0)
    { //set i2c address
        printf("fail to set i2c device slave address!\n");
        close(fd);
    }

  /*---------------------第一部分---------------------*/
    oled_i2c_write(fd, OLED_COMMEND_ADDR, 0xAE); //display off
    oled_i2c_write(fd, OLED_COMMEND_ADDR, 0x20); //
    oled_i2c_write(fd, OLED_COMMEND_ADDR, 0x10); //00,Horizontal Addressing Mode;
    oled_i2c_write(fd, OLED_COMMEND_ADDR, 0xb0); //Set Page Start Address for Page Addressing Mode,0-7
    oled_i2c_write(fd, OLED_COMMEND_ADDR, 0xc8); //Set COM Output Scan Direction
}

结合以上代码讲解如下,

第一部分, 打开IIC 设备文件,使用ioctl函数发送 oled 设备地址。正常情况下 oled 设备会有响应。 第二部分,使用oled_i2c_write向 oled 发送配置信息和控制信息,这部分内容完成了oled的初始化。配置参数非常多这里只列出了部分初始化代码,完整内容请 参考源码。

全屏填充函数OLED_Fill,全屏填充函数和清屏函数相似,全屏填充函数点亮每一个像素点而清屏函数熄灭每一个像素点,在程序中前者是写入0xff,后者写入0x00, 函数实现如下所示:

oled填充(base_codelinux_appi2coledsourcesmain.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void OLED_Fill(unsigned char fill_Data) //全屏填充
{
    unsigned char m, n;
    /*---------------------第一部分---------------------*/
    for (m = 0; m < 8; m++)
    {
        oled_i2c_write(fd, OLED_COMMEND_ADDR, 0xb0 + m); //page0-page1
        oled_i2c_write(fd, OLED_COMMEND_ADDR, 0x00);     //low column start address
        oled_i2c_write(fd, OLED_COMMEND_ADDR, 0x10);     //high column start address
       /*---------------------第二部分---------------------*/
        for (n = 0; n < 128; n++)
        {
            // WriteDat(fill_Data);
            oled_i2c_write(fd, OLED_DATA_ADDR, fill_Data); //high column start address
        }
    }
}

全屏填充函数由两个嵌套的for 循环组成,默认情况下没每8行像素组成“一行”,oled显示屏一列有64个 像素点,所以外层循环可取0到7。内层循环用于设置“一行”,oled一行有 128个像素点,内层循环要执行128次,oled_i2c_write函数每次写入一个8位的数据代表8个像素点的状 态,准确的说,内层循环执行完成(循环128次)实际写入8行像素点。

oled 字符串显示函数OLED_ShowStr,oled字符串显示函数只能显示F6x8和F8X16两种字体,F6x8既每个 字符宽度为6个像素高度为8个像素,F6x8和F8X16都是通过字库生成软件手动生成的,有兴趣可以自己制作其他字库。 字符串显示函数如下所示:

oled显示(base_codelinux_appi2coledsourcesmain.c文件)
 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
void OLED_ShowStr(unsigned char x, unsigned char y, unsigned char ch[], unsigned char TextSize)
{
     unsigned char c = 0,i = 0,j = 0;
     switch(TextSize)
     {

    /*---------------------第一部分---------------------*/
             case 1:
             {
                     while(ch[j] != '\0')
                     {
                             c = ch[j] - 32;
                             if(x > 126)
                             {
                                     x = 0;
                                     y++;
                             }
                             oled_set_Pos(x,y);
                             for(i=0;i<6;i++)
                                     oled_i2c_write(fd, OLED_DATA_ADDR,F6x8[c][i]);
                             x += 6;
                             j++;
                     }
             }break;
    /*---------------------第二部分---------------------*/
             case 2:
             {
                     while(ch[j] != '\0')
                     {
                             c = ch[j] - 32;
                             if(x > 120)
                             {
                                     x = 0;
                                     y++;
                             }
                             oled_set_Pos(x,y);
                             for(i=0;i<8;i++)
                                     oled_i2c_write(fd, OLED_DATA_ADDR,F8X16[c*16+i]);
                             oled_set_Pos(x,y+1);
                             for(i=0;i<8;i++)
                                     oled_i2c_write(fd, OLED_DATA_ADDR,F8X16[c*16+i+8]);
                             x += 8;
                             j++;
                     }
             }break;
     }
}

函数共有四个参数, x, y 用于设置字符显示的位置,因字符编码的不同,x和y的取值 范围不是固定的,以F8X16为例,每写入一个字符 x需要自加8,根据oled分辨率可知 x最大可取128-8-1(像素点从零开始),每写入一个字符 Y 需要自增 16个像素。我们知 道8行像素组成“一行”,实际 y 可取 0到7,由于每写入一个字符 y 需要自增16个 像素,所以y可取 0到6。ch[],指定要写入的字符串。TextSize 指定字体大小,当前该函数只支持两 种字体,TextSize = 1 使用F6x8字体格式,TextSize = 2使用F8X16字体格式。

其他显示函数类似,这里不再一一介绍。

主函数(base_codelinux_appi2coledsourcesmain.c文件)
 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
int main(int argc, char *argv[])
{
    int i = 0; //用于循环

    OLED_Init(); //初始化oled
    usleep(1000 * 100);
    OLED_Fill(0xff); //全屏填充

    while (1)
    {
        OLED_Fill(0xff); //全屏填充
        sleep(1);

        OLED_CLS(); //清屏
        sleep(1);

        OLED_ShowStr(0, 3, (unsigned char *)"Wildfire Tech", 1);  //测试6*8字符
        OLED_ShowStr(0, 4, (unsigned char *)"Hello wildfire", 2); //测试8*16字符
        sleep(1);
        OLED_CLS(); //清屏

        for (i = 0; i < 4; i++)
        {
            OLED_ShowCN(22 + i * 16, 0, i); //测试显示中文
        }
        sleep(1);
        OLED_CLS(); //清屏

        OLED_DrawBMP(0, 0, 128, 8, (unsigned char *)BMP1); //测试BMP位图显示
        sleep(1);
        OLED_CLS(); //清屏
    }

    close(fd);
}

主函数的实现比较简单,直接调用前面讲解的函数即可。在while(1)死循环中依次执行全屏填充、清屏、显示英文字符、显示汉字、显示图片测试函数。

13.7.2. 下载验证

对于ARM架构的程序,可使用如下步骤进行编译:

1
2
3
#在主机的实验代码Makefile目录下编译
#编译arm平台的程序
make ARCH=arm

编译后生成的ARM平台程序为build_arm/oled_demo,使用网络文件系统共享至开 发板,在开发板的终端上测试即可。

如下图:

未找到图片

实物图:

未找到图片