12. I2C通讯

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

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

本章的示例代码目录为:base_linux/i2c

12.1. I2C通讯协议简介

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

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

12.1.1. I2C物理层

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

常见的I2C通讯系统

它的物理层有如下特点:

  • 它是一个支持多设备的总线。“总线”指多个设备共用的信号线。 在一个I2C通讯总线中,可连接多个I2C通讯设备,支持多个通讯主机及多个通讯从机。

  • 一个I2C总线只使用两条总线线路,一条双向串行数据线(SDA) ,一条串行时钟线 (SCL)。 数据线即用来传输数据,时钟线用于数据收发同步。

  • 每个连接到总线的设备都有一个独立的设备地址,主机可以利用这个地址进行不同设备之间的访问。 其中地址是一个七位或十位的数字。

  • 总线通过上拉电阻接到电源。当I2C设备空闲时,会输出高阻态,而当所有设备都空闲, 都输出高阻态时,由上拉电阻把总线拉成高电平。

  • 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。

  • 具有三种传输模式:标准模式传输速率为100kbit/s ,快速模式为400kbit/s , 高速模式下可达 3.4Mbit/s,但目前大多I2C设备尚不支持高速模式。

  • 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制 。

12.1.2. 协议层

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

12.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的区别); 在第二次的传输中,对该地址的内容进行读或写。也就是说,第一次通讯是告诉从机读写地址,第二次则是读写的实际内容。

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

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

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

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

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

如下图:

起始和停止信号

12.1.2.3. 数据有效性

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

  • 传输时,SCL为高电平的时候SDA表示的数据有效,即此时的SDA为高电平时表示数据“1”,为低电平时表示数据“0”。

  • 当SCL为低电平时,SDA的数据无效,一般在这个时候SDA进行电平切换,为下一次表示数据做好准备。

如下图:

数据有效性

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

12.1.2.4. 地址及数据方向

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

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

如下图:

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

  • 写数据方向时,SDA由主机控制,从机接收信号。

12.1.2.5. 响应

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

  • 若希望对方 继续发送数据 ,则需要向对方发送 “应答(ACK)” 信号,发送方会继续发送下一个数据;

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

如下图:

响应与非响应信号

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

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

12.2. LubanCat板卡i2c引脚

本章主要围绕带有26Pin引脚的LubanCat-全志系列的板子,如下

  • LubanCat-A1

LubanCat-H618系列板子至多有2个I2C外设,分别是i2c-2,i2c-4。

I2C

引脚

功能

I2C4-SDA

3

i2c2的数据线

I2C4-SCL

5

i2c2的时钟信号线

I2C2-SDA

8

i2c4的数据线

I2C2-SCL

10

i2c4的时钟信号线

以鲁班猫A1为例,I2C引脚如下图所示:

未找到图片

对应实物的26pin接口

12.2.1. 使能IIC通信接口

IIC接口在默认情况是关闭状态的,需要使能才能使用

12.2.1.1. 方法一

可以通过打开 /boot/uEnv.txt 查看是否启用了i2c相关设备设备树插件。

这里以激活 I2C-2I2C-4 为例,选择需要开启的设备树插件,粘贴到 overlays= 后面,多个设备树插件开启,设备树插件之间要用空格隔开,如下图:

未找到图片

然后reboot重启激活设备

注解

如果是通过直接拔插电源的方式重启,会有可能出现文件没能做出修改 (原因:文件未能及时从内存同步到存储设备中,解决方法,在终端上输入 “sync” 同步修改之后再拔插电源重启)

12.3. 检查IIC 设备

可以通过一下命令查看i2c总线有没有开启

1
ls /dev/i2c-*

如下图:

未找到图片

12.4. 连接设备

注解

以mpu6050为例

将mpu6050接入到i2c-2的总线上,可以按照下列线序用杜邦线进行连接:

1
2
3
4
5
6
7
#板卡与mpu6050连接

 板子    ------  mpu6050
5V(4)    ------  VCC
GND(6)   ------  GND
SCL(8)   ------  SCL
SDA(10)  ------  SDA

12.5. IIC 第三方工具- i2c-tools

使用i2c-tools工具包提供了一些非常方便的工具来对系统的I2C总线进行调试, 在板卡的终端中可直接执行以下命令进行安装:

1
sudo apt install i2c-tools wget gcc -y

安装后可使用的命令有i2cdetect、i2cdump、i2cset以及i2cget,用于扫描I2C总线上的设备、读写指定设备的寄存器等。

12.5.1. i2cdetect其他命令

  • i2cdetect -F i2cbus:查询i2c总线的功能,参数i2cbus表示i2c总线

  • i2cdetect -V:打印软件的 -

  • i2cdetect -l:检测当前系统有几组i2c总线

12.5.1.1. i2cget命令

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

相关命令语法:

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

参数说明:

  • 参数f:强制访问设备。

  • 参数y:关闭交互模式,使用该参数时,不会提示警告信息。

  • 参数i2cbus:指定i2c总线的编号

  • 参数chip-address:i2c设备地址

  • 参数data-address:设备的寄存器的地址

  • 参数mode:参考i2cdump命令。

12.5.1.2. i2cset命令

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

相关命令语法:

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

参数说明:

  • 参数f:强制访问设备。

  • 参数y:关闭交互模式,使用该参数时,不会提示警告信息。

  • 参数m:掩码参数(如果指定)描述哪些值位将实际写入数据地址。掩码中设置为 1 的位取自值,而设置为 0 的位将从值读取数据地址,因此由操作保留。

  • 参数r:写入后立即回读寄存器值,并将结果与写入的值进行比较。

  • 参数i2cbus:指定i2c总线的编号。

  • 参数chip-address:i2c设备地址。

  • 参数data-address:设备的寄存器的地址。

  • 参数value:要写入寄存器的值。

  • 参数mode:参考i2cdump命令。

12.5.1.3. 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:打印软件的版本号

然后查看挂载在i2c-2上的器件情况,输出内容如下图所示:

结果如下图:

未找到图片2|

其中 “68” 是为MPU6050的设备地址,常用的命令还有以下几个。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#检测当前系统有几组i2c总线
i2cdetect -l

#查看i2c-2接口上的设备
i2cdetect -a 2

#读取指定设备的全部寄存器的值。
i2cdump  -f -y 2 0x68

#读取指定IIC设备的某个寄存器的值,如下读取地址为0x68器件中的0x01寄存器值。
i2cget -f -y 2 0x68 0x01

#写入指定IIC设备的某个寄存器的值,如下设置地址为0x68器件中的0x01寄存器值为0x6f;
i2cset -f -y 2 0x68 0x01 0x6f

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

12.6.1. 实验说明

本教程将通过IIC接口读取陀螺仪(MPU6050)的原始数据。 本次实验会以i2c-2做为示例,i2c-4的操作和i2c-2的一样, 当然,如果没有mpu6050模块,可以通过学习操作mpu6050的方式操作想要操作的其他i2c设备, 在测试程序中大约每一秒读取并显示一次MPU6050的原始数据

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

结果如下图:

未找到图片2|

其中“i2c-2”就是MPU6050使用到的 IIC 2接口总线。

12.6.2. ioctl函数

在编写应用程序时需要使用ioctl函数设置i2c相关配置,其函数原型如下

1
2
3
 #include <sys/ioctl.h>

 int ioctl(int fd, unsigned long request, ...);

其中对于形参request的值常用的有以下几种

I2C_RETRIES

设置收不到ACK时的重试次数,默认为1

I2C_TIMEOUT

设置超时时限的jiffies

I2C_SLAVE

设置从机地址

I2C_SLAVE_FORCE

强制设置从机地址

I2C_TENBIT

选择地址长度0为7位地址,非0为10位

12.6.3. 编写应用程序

根据ioctl相关参数即可编写与i2c相关的接口函数,读取mpu6050原始数据程序如下

base_linux/i2c/i2c_mpu6050/i2c_mpu6050.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
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>

/*寄存器地址*/
#define SMPLRT_DIV      0x19
#define PWR_MGMT_1      0x6B
#define CONFIG          0x1A
#define ACCEL_CONFIG    0x1C

#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

//从机地址 MPU6050地址
#define Address         0x68

//MPU6050操作相关函数
static int mpu6050_init(int fd,uint8_t addr);
static int i2c_write(int fd, uint8_t addr,uint8_t reg,uint8_t val);
static int i2c_read(int fd, uint8_t addr,uint8_t reg,uint8_t * val);
static short GetData(int fd,uint8_t addr,unsigned char REG_Address);

int main(int argc,char *argv[] )
{
    int  fd;
    fd = I2C_SLAVE;

    if(argc < 2){
        printf("Wrong use !!!!\n");
        printf("Usage: %s [dev]\n",argv[0]);
        return -1;
    }

    fd = open(argv[1], O_RDWR); // open file and enable read and  write
    if (fd < 0){
        printf("Can't open %s \n",argv[1]); // open i2c dev file fail
        exit(1);
    }

    //初始化MPU6050
    mpu6050_init(fd,Address);
    while(1){
        usleep(1000 * 10);
        printf("ACCE_X:%6d\n ", GetData(fd,Address,ACCEL_XOUT_H));
        usleep(1000 * 10);
        printf("ACCE_Y:%6d\n ", GetData(fd,Address,ACCEL_YOUT_H));
        usleep(1000 * 10);
        printf("ACCE_Z:%6d\n ", GetData(fd,Address,ACCEL_ZOUT_H));
        usleep(1000 * 10);
        printf("GYRO_X:%6d\n ", GetData(fd,Address,GYRO_XOUT_H));
        usleep(1000 * 10);
        printf("GYRO_Y:%6d\n ", GetData(fd,Address,GYRO_YOUT_H));
        usleep(1000 * 10);
        printf("GYRO_Z:%6d\n\n ", GetData(fd,Address,GYRO_ZOUT_H));
        sleep(1);
    }

    close(fd);

    return 0;
}

static int mpu6050_init(int fd,uint8_t addr)
{
    i2c_write(fd, addr,PWR_MGMT_1,0x00);  //配置电源管理,0x00,正常启动
    i2c_write(fd, addr,SMPLRT_DIV,0x07);  //设置MPU6050的输出分频既设置采样
    i2c_write(fd, addr,CONFIG,0x06);  //配置数字低通滤波器和帧同步引脚
    i2c_write(fd, addr,ACCEL_CONFIG,0x01);  //设置量程和 X、Y、Z 轴加速度自检

    return 0;
}

static int i2c_write(int fd, uint8_t addr,uint8_t reg,uint8_t val)
{
    int retries;
    uint8_t data[2];

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

    //设置地址长度:0为7位地址
    ioctl(fd,I2C_TENBIT,0);

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

    //设置收不到ACK时的重试次数
    ioctl(fd,I2C_RETRIES,5);

    if (write(fd, data, 2) == 2){
        return 0;
    }
    else{
        return -1;
    }

}

static int i2c_read(int fd, uint8_t addr,uint8_t reg,uint8_t * val)
{
    int retries;

    //设置地址长度:0为7位地址
    ioctl(fd,I2C_TENBIT,0);

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

    //设置收不到ACK时的重试次数
    ioctl(fd,I2C_RETRIES,5);

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

static short GetData(int fd,uint8_t addr,unsigned char REG_Address)
{
    char H, L;

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

12.6.3.1. 编译

方法1:

1
2
#编译
make

方法2:

1
2
#编译
gcc i2c_mpu6050.c -o mpu6050

12.6.3.2. 运行

1
2
3
4
#运行
sudo ./mpu6050 /dev/i2c-2

#可将/dev/i2c-2 改为 /dev/i2c-4 这取决于你是否存在其他IIC串口

效果如下图所示

未找到图片show

12.6.4. 代码分析

i2c写函数
 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
static int i2c_write(int fd, uint8_t addr,uint8_t reg,uint8_t val)
{
    int retries;
    uint8_t data[2];

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

    //设置地址长度:0为7位地址
    ioctl(fd,I2C_TENBIT,0);

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

    //设置收不到ACK时的重试次数
    ioctl(fd,I2C_RETRIES,5);

    if (write(fd, data, 2) == 2){
        return 0;
    }
    else{
        return -1;
    }

}
  • MPU6050写函数。写入成功返回0,失败返回 -1。 函数参数共有四个,

    1. fd :文件描述符。

    2. addr :i2c设备的地址

    3. reg :要写入的MPU6050寄存器地址。

    4. val : 要写入的值。

  • 第3-7行,将寄存器和寄存器的修改值填进一个数组里面,发送时统一发送

  • 第10-17行,设置i2c地址的长度以及设置要发送的i2c地址

  • 第20-27行,设置i2c的重发次数以及把数组发送到设备上去

i2c读函数
 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
static int i2c_read(int fd, uint8_t addr,uint8_t reg,uint8_t * val)
{
    int retries;

    //设置地址长度:0为7位地址
    ioctl(fd,I2C_TENBIT,0);

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

    //设置收不到ACK时的重试次数
    ioctl(fd,I2C_RETRIES,5);

    if (write(fd, &reg, 1) == 1){
        if (read(fd, val, 1) == 1){
                return 0;
        }
    }
    else{
        return -1;
    }
}
  • MPU6050读函数。读取成功返回0,失败返回 -1。 函数参数共有四个,

    1. fd :文件描述符。

    2. addr :i2c设备的地址

    3. reg :要写入的MPU6050寄存器地址。

    4. val : 要写入的值。

  • 第3-13行,设置i2c地址的长度以及设置要接受的i2c地址

  • 第14-20行,设置i2c的重发次数以及把接受到的数据存放到指针里

mpu6050数据获取
1
2
3
4
5
6
7
8
9
static short GetData(int fd,uint8_t addr,unsigned char REG_Address)
{
    char H, L;

    i2c_read(fd, addr,REG_Address, &H);
    usleep(1000);
    i2c_read(fd, addr,REG_Address + 1, &L);
    return (H << 8) +L;
}
  • MPU6050读函数。读取成功返回0,失败返回 -1。 函数参数共有三个,

    1. fd :文件描述符。

    2. addr :i2c设备的地址

    3. REG_Address :要读取的MPU6050寄存器地址。

  • 第5-7行,通过i2c地址以及寄存器获取相应的值,因为数据是16位的, i2c的传输格式是八位,因此要读取两次。

  • 第8行,返回16位的数值

mpu6050初始化
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static int mpu6050_init(int fd,uint8_t addr)
{
    i2c_write(fd, addr,PWR_MGMT_1,0x00);  //配置电源管理,0x00,正常启动
    i2c_write(fd, addr,SMPLRT_DIV,0x07);  //设置MPU6050的输出分频既设置采样
    i2c_write(fd, addr,CONFIG,0x06);  //配置数字低通滤波器和帧同步引脚
    i2c_write(fd, addr,ACCEL_CONFIG,0x01);  //设置量程和 X、Y、Z 轴加速度自检

    return 0;
}

  • MPU6050初始化函数。初始化的过程实际就是打开设备然后写入配置参数。 0**addr** 为i2c要发送的地址。

  • 第3行-第7行,设置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
int main(int argc,char *argv[] )
{
    int  fd;
    fd = I2C_SLAVE;

    if(argc < 2){
        printf("Wrong use !!!!\n");
        printf("Usage: %s [dev]\n",argv[0]);
        return -1;
    }

    fd = open(argv[1], O_RDWR); // open file and enable read and  write
    if (fd < 0){
        printf("Can't open %s \n",argv[1]); // open i2c dev file fail
        exit(1);
    }

    //初始化MPU6050
    mpu6050_init(fd,Address);
    while(1){
        usleep(1000 * 10);
        printf("ACCE_X:%6d\n ", GetData(fd,Address,ACCEL_XOUT_H));
        usleep(1000 * 10);
        printf("ACCE_Y:%6d\n ", GetData(fd,Address,ACCEL_YOUT_H));
        usleep(1000 * 10);
        printf("ACCE_Z:%6d\n ", GetData(fd,Address,ACCEL_ZOUT_H));
        usleep(1000 * 10);
        printf("GYRO_X:%6d\n ", GetData(fd,Address,GYRO_XOUT_H));
        usleep(1000 * 10);
        printf("GYRO_Y:%6d\n ", GetData(fd,Address,GYRO_YOUT_H));
        usleep(1000 * 10);
        printf("GYRO_Z:%6d\n\n ", GetData(fd,Address,GYRO_ZOUT_H));
        sleep(1);
    }

    close(fd);

    return 0;
}
  • 第5-16行,打开i2c总线接口,获取总线

  • 第19行,初始化mpu6050

  • 第20-34行,获取数据并将其打印出来

12.7. OLED显示屏显示实验

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

未找到图片

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

1
2
3
4
5
6
7
#板卡与OLED 显示屏连接

 板子    ------  OLED 显示屏
5V(4)    ------  VCC
GND(6)   ------  GND
SCL(8)   ------  SCL
SDA(10)  ------  SDA

12.7.1. 编译&运行

代码位置
1
base_linux/i2c/i2c_oled
验证
1
2
3
4
5
#编译
make

#在build目录下运行
./i2c_oled /dev/i2c-2

实物展示图:

未找到图片

12.7.2. 代码分析

代码结构如下:

未找到图片

oled_app.c 的函数如下

未找到图片
oled_app.h
1
2
#define OLED_COMMEND_ADDR 0x00
#define OLED_DATA_ADDR 0x40
  • reg = 0x00:表示发送的是命令,更准确的说是OLED配置参数、控制参数。

  • reg = 0x40: 表示发送的是数据。

  • val :指定要发送的内容。

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

main.c
1
#define Address         0x3c   //通过调整0R电阻,屏可以0x78和0x7A两个地址 -- 默认0x78

野火 oled 显示屏默认的IIC从地址为0X78,通过调整电阻可以设置为0X7A。

注意

这里的地址是8位的地址,最后一位表示的是读或者写。而这里要发送的是IIC设备的7位地址,如上代码所示,我们在宏定义中设置的IIC 地址是由0x78 左移一位得到的。

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

oled填充–oled_app.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void OLED_Fill(unsigned char addr,unsigned char fill_Data) //全屏填充
{
    unsigned char m, n;
    for (m = 0; m < 8; m++)
    {
        i2c_write(fd,addr ,OLED_COMMEND_ADDR, 0xb0 + m); //page0-page1
        i2c_write(fd,addr ,OLED_COMMEND_ADDR, 0x00);     //low column start address
        i2c_write(fd,addr ,OLED_COMMEND_ADDR, 0x10);     //high column start address

        for (n = 0; n < 128; n++)
        {
            // WriteDat(fill_Data);
            i2c_write(fd,addr,OLED_DATA_ADDR, fill_Data); //high column start address
        }
    }
}

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

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

oled显示英文
 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
void OLED_ShowStr(unsigned char addr,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(addr,x,y);
				for(i=0;i<6;i++)
					i2c_write(fd, addr,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(addr,x,y);
				for(i=0;i<8;i++)
					i2c_write(fd,addr, OLED_DATA_ADDR,F8X16[c*16+i]);
				oled_set_Pos(addr,x,y+1);
				for(i=0;i<8;i++)
					i2c_write(fd, addr,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字体格式。

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

oled/sources/main.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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <errno.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>

#include "oled_app.h"

#define Address         0x3c

extern int fd;
extern const unsigned char BMP1[];

/* 主函数  */
int main(int argc, char *argv[])
{
    int i = 0; //用于循环

    fd = open("/dev/i2c-2", O_RDWR); // open file and enable read and  write

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

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

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

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

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

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

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

    close(fd);
}

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