记录一下IIC,好记性不如烂笔头。
文章目录
一.I2C 简介
I2C 是很常见的一种总线协议, I2C是NXP公司设计的,I2C 使用两条线在主控制器和从机之间进行数据通信。一条是 SCL(串行时钟线),另外一条是 SDA(串行数据线),因为I2C这两条数据线是开漏输出的,所以需要接上拉电阻,总线空闲的时候 SCL 和 SDA 处于高电平。 I2C 总线标准模式下速度可以达到 100 Kb/s,快速模式下可以达到 400 Kb/s,在高速模式下可达到3.4 Mbit/s。一般通过I2C总线接口可编程时钟来实现传输速率的调整。
感兴趣的可以读《I2C总线规范和用户手册》
I2C 硬件框架
I2C总线上的每一个设备都可以作为主设备或者从设备,而且每一个从设备都会对应一个唯一的地址(可以从I2C器件的数据手册得知)。主从设备之间就通过这个地址来确定与哪个器件进行通信,在通常的应用中,我们把CPU带I2C总线接口的模块作为主设备,把挂接在总线上的其他设备都作为从设备。
⚫ 在一个芯片(SoC)内部,有一个或多个 I2C 控制器
⚫ 在一个 I2C 控制器上,可以连接一个或多个 I2C 设备。
I2C总线上可挂接的设备数量受总线的最大电容400pF限制,如果所挂接的是相同型号的器件,则还受器件地址的限制。一般I2C设备地址是7位地址(也有10位),地址分成两部分:芯片固化地址(生产芯片时候哪些接地,哪些接电源,已经固定),可编程地址(引出IO口,由硬件设备决定)。例如:某一个器件是7 位地址,其中1010xxx 高4位出厂时候固定了,低3位可以由设计者决定。则一条I2C总线上只能挂该种器件最少8个。如果7位地址都可以编程,那理论上就可以达到128个器件,但实际中不会挂载这么多。
⚫ I2C 总线只需要 2 条线:时钟线 SCL、数据线 SDA
⚫ 在 I2C 总线的 SCL、SDA 线上,都有上拉电阻
⚫ 总线数据长度:I2C总线上的主设备与从设备之间以字节(8位)为单位进行双向的数据传输。
如果玩过单片机,肯定对模拟I2C时序这个操作并不陌生(如果对IIC时序不清楚,可以自行补充I2C时序基础知识)。在Linux上,cpu会自带I2C控制器,有了这个I2C控制器之后,就不用模拟时序了,只需要关注怎么把数据写到寄存器和怎么从寄存器读数据即可,具体的时序都是由I2C控制器来自动完成。
I2C 软件框架
以 I2C 接口的存储设备 AT24C02 为例:
⚫ APP:
◼ 提出要求:把字符串"abcde"写入 AT24C02 地址 16 开始的地方
◼ 不关心底层实现的细节
◼ 只需要调用设备驱动程序提供的接口
⚫ AT24C02 驱动:
◼ 知道 AT24C02 要求的地址、数据格式
◼ 知道发出什么信号才能让 AT24C02 执行擦除、烧写工作
◼ 知道怎么判断数据是否烧写成功
◼ 构造好一系列的数据,发给 I2C 控制器
⚫ I2C 控制器驱动
◼ 根据 I2C 协议发出各类信号:I2C 设备地址、I2C 存储地址、数据
◼ 根据 I2C 协议判断
IIC 传输数据的格式
I2C基本时序信号
I2C 协议中数据传输的单位是字节,也就是 8 位。但是要用到 9 个时钟:前面 8 个时钟用来传输 8 数据,第 9 个时钟用来传输回应信号。传输时,先传输最高位(MSB)。注意:起始和结束信号总是由主设备产生。
⚫ 空闲状态:SCL和SDA都保持着高电平。
⚫ 开始信号(S):SCL 为高电平时,SDA 由高电平向低电平跳变,开始传送数据。在起始条件产生后,总线处于忙状态,由本次数据传输的主从设备独占,其他I2C器件无法访问总线。
⚫ 结束信号(P):SCL 为高电平时,SDA 由低电平向高电平跳变,结束传送数据
⚫ 响应信号(ACK):接收器在接收到 8 位数据后,在第 9 个时钟周期,拉低SDA。每个字节传输完成后的下一个时钟信号,在SCL高电平期间,SDA为低,则表示一个应答信号。
⚫ SDA 上传输的数据必须在 SCL 为高电平期间保持稳定,SDA 上的数据只能在SCL 为低电平期间变化
写操作流程
⚫ 主芯片要发出一个 start 信号
⚫ 然后发出一个设备地址(用来确定是往哪一个芯片写数据),方向(读/写,0表示写,1 表示读)
⚫ 从设备回应(用来确定这个设备是否存在),然后就可以传输数据
⚫ 主设备发送一个字节数据给从设备,并等待回应
⚫ 每传输一字节数据,接收方要有一个回应信号(确定数据是否接受完成),然后再传输下一个数据
⚫ 数据发送完之后,主芯片就会发送一个停止信号
⚫ 下图:白色背景表示"主→从",灰色背景表示"从→主"
读操作流程
⚫ 主芯片要发出一个 start 信号
⚫ 然后发出一个设备地址(用来确定是往哪一个芯片写数据),方向(读/写,0表示写,1 表示读)
⚫ 从设备回应(用来确定这个设备是否存在),然后就可以传输数据
⚫ 从设备发送一个字节数据给主设备,并等待回应
⚫ 每传输一字节数据,接收方要有一个回应信号(确定数据是否接受完成),然后再传输下一个数据。
⚫ 数据发送完之后,主芯片就会发送一个停止信号
⚫ 下图:白色背景表示"主→从",灰色背景表示"从→主"
二. Linux下IIC读写
APP 可以通过两类驱动程序访问设备, I2C 设备自己的驱动程序,或者内核自带的 i2c-dev.c 驱动程序。
⚫ I2C Device Driver
◼ I2C 设备自己的驱动程序
◼ 内核自带的 i2c-dev.c 驱动程序,它是 i2c 控制器驱动程序暴露给用户空间的驱动程序(i2c-dev.c)
⚫ I2C Controller Driver
◼ 芯片 I2C 控制器的驱动程序(称为 adapter)
◼ 使用 GPIO 模拟的 I2C 控制器驱动程序(i2c-gpio.c)
Linux把I2C控制器抽象成了一个i2c_adapter,我们只要来分配这个i2c_adapter,就可以得到一个I2C控制器。
先来看一下我们的系统里面都有哪些I2C的节点。如下所示:
(base) root@davinci-mini:~# ls /dev/i2c-*
/dev/i2c-0 /dev/i2c-13 /dev/i2c-3 /dev/i2c-6 /dev/i2c-8
/dev/i2c-1 /dev/i2c-2 /dev/i2c-5 /dev/i2c-7 /dev/i2c-9
Linux有一个非常重要的概念叫一切皆文件,应用层通过open这些节点来操作I2C来跟外设I2C通信的芯片进行一个数据交流。
通过查看自己板子的原理图先来确定一下 AT24C08 使用的是I2C2,对应的节点是dev下面的i2c-1。和 AT24C08进行通信,操作dev下的i2c-1这个节点就可以。
应用层操作I2C是以数据包进行交流的,所有我们在应用层就要进行封包的操作。数据包对应的结构体是i2c_rdwr_ioctl_data,这个结构体定义在include\uapi\linux\i2c-dev.h下面:定义
/* This is the structure as used in the I2C_RDWR ioctl call */
struct i2c_rdwr_ioctl_data {
struct i2c_msg __user *msgs; /* pointers to i2c_msgs */
__u32 nmsgs; /* number of i2c_msgs */
};
第一个结构体成员是我们要发送的数据包的指针,第二个结构体成员我们发送数据包的个数。i2c_msg结构体是定义在include\uapi\linux\i2c.h下面,定义如下:
struct i2c_msg {
__u16 addr; /* slave address */
__u16 flags;
#define I2C_M_TEN 0x0010 /* this is a ten bit chip address */
#define I2C_M_RD 0x0001 /* read data, from slave to master */
#define I2C_M_STOP 0x8000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART 0x4000 /* if I2C_FUNC_NOSTART */
#define I2C_M_REV_DIR_ADDR 0x2000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK 0x1000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NO_RD_ACK 0x0800 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_RECV_LEN 0x0400 /* length will be first received byte */
__u16 len; /* msg length */
__u8 *buf; /* pointer to msg data */
};
结构体成员addr是从机的地址,flags为读写标志位,如果flags为1,则为读,反之为0,则为写。len为buf的大小,单位是字节。当flags为1是,buf就是我们要接收的数据,当flags为0时,就是我们要发送的数据。
◼ i2c_msg 中的 flags 用来表示传输方向:bit 0 等于 I2C_M_RD 表示读,bit 0 等于 0 表示写
◼ 一个 i2c_msg 要么是读,要么是写
AT24C08简介
看板子引脚:
A0,A1,A2:硬件地址引脚
WP:写保护引脚,接高电平只读,接地允许读和写
SCL和SDA:IIC总线
举例:从设备地址为 0x50 的 EEPROM,要读取它里面存储地址为 0x10 的一个字节,应该构造2个 i2c_msg。
代码如下:
u8 data_addr = 0x10;
i8 data;
struct i2c_msg msgs[2];
// 第一个 i2c_msg 表示写操作,把要访问的存储地址 0x10 发给设备
msgs[0].addr = 0x50;
msgs[0].flags = 0;
msgs[0].len = 1;
msgs[0].buf = &data_addr;
// 第二个 i2c_msg 表示读操作
msgs[1].addr = 0x50;
msgs[1].flags = I2C_M_RD;
msgs[1].len = 1;
msgs[1].buf = &data;
三. 完整代码
/*
* @Author: topeet
* @Description: 应用程序与I2c通信
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>
#include <linux/input.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
int fd;
int ret;
/**
* @description: i2c_read_data i2c读数据
* @param {unsignedint} slave_addr:从机设备的地址
* @param {unsignedchar} reg_addr:寄存器的地址
* @return {*}
*/
int i2c_read_data(unsigned int slave_addr, unsigned char reg_addr)
{
unsigned char data;
//定义一个要发送的数据包i2c_read_lcd
struct i2c_rdwr_ioctl_data i2c_read_lcd;
//定义初始化i2c_msg结构体
struct i2c_msg msg[2] = {
[0] = {
.addr = slave_addr, //设置从机额地址
.flags = 0, //设置为写
.buf = ®_addr, //设置寄存器的地址
.len = sizeof(reg_addr)}, //设置寄存器的地址的长度
[1] = {.addr = slave_addr, //设置从机额地址
.flags = 1, //设置为读
.buf = &data, //设置寄存器的地址
.len = sizeof(data)}, //设置寄存器的地址
};
//初始化数据包的数据
i2c_read_lcd.msgs = msg;
//初始化数据包的个数
i2c_read_lcd.nmsgs = 2;
//操作读写数据包
ret = ioctl(fd, I2C_RDWR, &i2c_read_lcd);
if (ret < 0)
{
perror("ioctl error ");
return ret;
}
return data;
}
int main(int argc, char *argv[])
{
int TD_STATUS;
//打开设备节点
fd = open("/dev/i2c-1", O_RDWR);
if (fd < 0)
{
//打开设备节点失败
perror("open error \n");
return fd;
}
while (1)
{
//i2C读从机地址为0x50,寄存器地址为0x10的数据
TD_STATUS = i2c_read_data(0x50, 0x10);
// 打印TD_STATUS的值
printf("TD_STATUS value is %d \n", TD_STATUS);
sleep(1);
}
close(fd);
return 0;
}
其他补充资料
SMBus
SMBus 是基于 I2C 协议的,SMBus 要求更严格,SMBus 是 I2C 协议的子集。SMBus 协议:http://www.smbus.org/specs/
I2CTools
直接访问i2c-tools下载链接,可选择当前最新版本下载。