4. 用户态 I2C 编程
I2C 总线的设备文件通常为/dev/i2c-n( n=0、 1、 2...),每个设备文件对应一组 I2C 总
线。应用程序通过这些设备文件可以操作 I2C 总线上的任何从机器件。 EasyARM-i.MX283A
提供了一路 I2C 接口,设备文件为/dev/i2c-0。
root@EasyARM-iMX283 /# ls /dev/i2c*
/dev/i2c-0 /dev/i2c-1
4.1 I2C 编程接口
1. 打开设备
在操作 I2C 总线时,先调用 open()函数打开 I2C 设备获得文件描述符,代码如程序清单4.1 所示。
程序清单 4.1 打开 I2C 设备文件
int fd;
fd = open("/dev/i2c-0", O_RDWR);
if (fd < 0) {
perror("open i2c-1 \n");
}
2. 关闭设备
当操作完成后,调用 close()函数关闭设备:
close(fd);
3. 配置设备
当应用程序操作 I2C 总线上的从机器件时,必须先调用 ioctl()函数设置从机地址和从机地址的长度。
3.1 设置从机地址
设置从机地址是使用 I2C_SLAVE 命令,其定义为:
#define I2C_SLAVE 0x0703
该命令的参数为从机地址右移一位。设置从机地址为 0xA0 的示例代码为:
if (ioctl(fd, I2C_SLAVE, 0xA0 >> 1) < 0) {
perror("set slave address failed \n");
}
注意:地址需要右移一位,是因为地址的 Bit0 是读写控制位,在驱动中会将从机地址
命令参数左移一位,并补上读写控制位。
3.2 设置地址长度
设置从机地址的长度是使用 I2C_TENBIT 命令,其定义为:
#define I2C_TENBIT 0x0704
该命令的参数可选择为: 1 表示设置从机地址长度为 10 位; 0 表示设置从机地址长度为8 位。
设置从机地址长度为 10 位的示例代码为:
ioctl(fd, I2C_TENBIT, 1);
该命令是不会返回错误的。
如果不设置地址长度,则默认为 8 位地址。
4. 发送数据
应用程序调用 write()函数可以向 I2C 总线发送数据。例如在 I2C 总线发送“ hello”字符
串的代码如程序清单 4.2所示。
程序清单 4.2 在 I2C 总线发送数据
int len;
char buf[] = "hello";
len = write(fd, buf, sizeof(buf));
if (len < 0) {
printf("send data failed");
exit(-1);
}
write()函数调用成功后,返回成功发送数据的长度。在 write()函数调用时,数据发送过
程如下:
(1) 主机在 I2C 总线发送始起信号( S),然后发送从机地址( slave addr) ;
(2) 从机成功接收到属于自己的从机地址后,返回应答信号( ACK);
(3) 主机接收到应答信号后,把 buf 缓冲区中的数据逐个在 I2C 总线发送;
(4) 从机每成功接收到一个从主机发来的数据都返回应答信号;
(5) 当主机的数据发送完毕后,在 I2C 上发送结束信号( P)。
5. 接收数据
应用程序调用 read()函数可以在 I2C 总线接收数据。例如在 I2C 总线接收 10 个字节的代
码如程序清单 4.3 所示。
程序清单 4.3 在 I2C 总线读取数据
char buf[10];
int len;
len = read(fd, buf, 10);
if (len < 0){
printf("read i2c data failed");
exit(-1);
}
read()调用成功后,返回接收数据的长度。
read()函数调用时,数据接收过程如下:
(1) 主机在 I2C 总线发送始起信号( S),然后发送从机地址( slave addr) ;
(2) 从机成功接收到属于自己的从机地址后,返回应答信号( ACK);
(3) 主机接收到应答信号后,准备接收从机发来的数据;
(4) 从机把数据逐个向主机发送;
(5) 主机每成功接收到一个在从机发来的数据都返回应答信号;
(6) 当主机接收到最后一个数据时并不返回应答信号,而是在 I2C 总线上发送结束信号( P)。
4.2 编程范例
AP-283Demo 板上的 FM24C02A 是 I2C 接口的 EEPROM 芯片。 FM24C02A 是 2Kb( 256
字节)大小的 EEPROM,分为 32 个页,每页 8 字节。
这里通过演示读/写 I2C 接口的 EEPROM 来进一步说明应用程序如何使用 I2C 编程接口。
1. FM24C02A 的操作
1.1 从机寻址
当接收到起始信号后, FM24C02A 需要一个 8 位的从机地址来启动一次读/写操作,其
从机地址构成:
1010 A2 A1 A0 R/W
从机地址前 4 位的值固定不变,第 2、 3、 4 位的值分别由 FM24C02A 的 A0、 A1、 A2
引脚的输入电平决定(高电平为 1,低电平为 0)。从机地址的第 0 位为读/写启动选择位( R/W):
1 为启动读操作; 0 为启动写操作。
1.2 字节写
字节写操作为每次在 FM24C02A 内部储存器的指定地址写入 1 个字节的数据。主机先
发送起始信号和从机地址( R/W 位为 0)。在接收到 FM24C02A 返回的应答信号后,主机发
送需要写入的数据地址( 1 个字节),然后发送需要写入的数据。在收到 FM24C02A 返回的
应答信号后,主机发送结束信号。
1.3 页写
FM24C02A 支持在一次写操作中连续写入一页的数据( 8 个字节)。页写操作的启动
方式和字节写操作类似,只是主机发送了第 1 个字节的数据后并不是马上停止,而是继
续发送剩余的 7 个字节的数据。 FM24C02A 在每接收到主机发来的 1 个数据都返回 1
个应答信号。当主机的所有数据都发送完毕后,主机发送结束信号。每当 FM24C02A
接收到主机发来的 1 个数据时,数据地址的低三位加 1,而高五位不会变化,保持存储
器的页地址不变。当内部产生的数据地址达到页边界时,数据地址将会翻转,接下来的
数据的写入地址将置为同一页的最小地址。所以若有超过 8 个字节数据写入 FM24C02A,
数据地址将回到最先写入的地址,先前写入的数据将被覆盖。
1.4 当前地址读
FM24C02A 的内部数据地址计数器保留最后一次访问的地址,并自动加 1。只要
FM24C02A 处于上电状态,这个地址在操作运行期间始终有效。在读操作中,如果存
储器的最后一页的最后一个字节开始读,则读下一个字节时地址将会翻转到整个储存器
的最小地址。
主机发送起始信号和从机地址( R/W 位为 1)后, FM24C02A 返回答应信号,然后
向主机发送数据。这时主机接收到数据后,并不返回答应信号,而发送结束信号。
1.5 自由读
自由读需要通过假的字节写操作来获得数据地址。主机首先发送起始信号、从机地址(写操作)和
数据地址来定位需要读取的地址。当 FM24C02A 返回数据地址的应答信号之后,主机马上
重新发送起始信号和从机地址(读操作)。这时 FM24C02A 返回应答信号,然后发送数据。主机接收
到数据后,并不返回应答信号,而发送结束信号。
1.6 连续读
在自由读操作中,若主机在接收了 FM24C02A 发来的数据后,并不发送结束信号,而
是立即返回应答信号,那么 FM24C02A 则自动把数据地址加 1,并将新数据地址的数据发
送给主机。当储存器的数据地址达到最大时,数据地址将翻转到最小地址,并且继续进行连
续读操作。当主机不再返回应答信号,而是发送停止信号时, FM24C02A 停止发送数据。
2. 电路原理
AP-283Demo 板上的 FM24C02A 是连接到 I2C1 总线,见电路图。
在该电路图中, FM24C02A 的 A0、 A1、 A2 引脚电平被拉低,所以 FM24C02A 的从机
地址为 0xA0。
1010 A2 A1 A0 R/W = 1010 000 R/W = 0xA0 + R/W
3. 示例程序
在程序清单 4.4 所示的代码中,通过 I2C 总线在 FM24C02A 内部储存器的 0x00 ~ 0x07
地址连续写入 8 个字节的数据,然后在这些地址中把数据读出来,最后把写入数据和读出数
据进行对比,以检验程序的正确性。
程序清单 4.4 连续写/读程序代码
-
#include <stdio.h>
-
#include <stdlib.h>
-
#include <unistd.h>
-
#include <sys/types.h>
-
#include <sys/stat.h>
-
#include <fcntl.h>
-
#include <termios.h>
-
#include <errno.h>
-
-
#define I2C_SLAVE 0x0703
-
#define I2C_TENBIT 0x0704
-
#define I2C_ADDR 0xA0
-
#define DATA_LEN 8
-
#define I2C_DEV_NAME "/dev/i2c-1"
-
-
int main(int arg,char*args[])
-
{
-
unsigned int ret,len;
-
int i,flag=0;
-
int fd;
-
char tx_buf[DATA_LEN + 1]; /* 用于储存数据地址和发送数据 */
-
char rx_buf[DATA_LEN]; /* 用于储存接收数据 */
-
char addr[1]; /* 用于储存读/写的数据地址 */
-
addr[0] = 0; /* 数据地址设置为 0 */
-
-
fd = open(I2C_DEV_NAME, O_RDWR); /* 打开 I2C 总线设备 */
-
if(fd < 0) {
-
printf("open %s failed\n", I2C_DEV_NAME);
-
return -1;
-
}
-
-
ret = ioctl(fd, I2C_SLAVE, I2C_ADDR >> 1); /* 设置从机地址 */
-
if (ret < 0) {
-
printf("setenv address failed ret: %x \n", ret);
-
return -1;
-
}
-
-
/* 由于没有设置从机地址长度,所以使用默认的地址长度为 8 */
-
tx_buf[0] = addr[0]; /* 发数据时,第一个发送是数据地址 */
-
for (i = 1; i < (DATA_LEN + 1); i++) /* 初始化要写入的数据:0, 1, ..., 7 */
-
tx_buf[i] = i - 1; /* 总共为8个数据: 0 - 7 */
-
-
len = write(fd, tx_buf, DATA_LEN + 1); /* 把数据写入到 FM24C02A, */
-
if (len < 0) {
-
printf("write data failed \n");
-
return -1;
-
}
-
-
usleep(1000*100); /* 需要延迟一段时间才能完成写入 EEPROM */
-
-
/* 读取操作 */
-
len = write(fd, addr, 1); /* 设置要读取的数据地址 */
-
if (len < 0) {
-
printf("write data addr failed \n");
-
return -1;
-
}
-
-
len = read(fd, rx_buf, DATA_LEN); /* 在设置的数据地址连续读入数据 */
-
if (len < 0) {
-
printf("read data faile \n");
-
return -1;
-
}
-
-
printf("read from eeprom:");
-
for(i = 0; i < DATA_LEN; i++) { /* 对比写入数据和读取的数据 */
-
printf(" %x", rx_buf[i]);
-
if (rx_buf[i] != tx_buf[i+1])
-
flag = 1;
-
}
-
-
printf("\n");
-
-
if (!flag) { /* 如果写入/读取数据一致,打印测试成功 */
-
printf("eeprom write and read test sussecced!\r\n");
-
}
-
else { /* 如果写入/读取数据不一致,打印测试失败 */
-
printf("eeprom write and read test failed!\r\n");
-
}
-
return 0;
-
}
该代码可以交叉编译为 i2c_eeprom_test 程序文件,测试方法:
(1) 把 i2c_eeprom_test 上传到 EasyARM-i.MX283A 的任何目录;
(2) 执行 i2c_eeprom_test 程序。
若 i2c_eeprom_test 程序执行无误,将打印信息
注意:这里刚好是一整页的读写,如果错开了,就会覆盖低地址的数据。