IIC协议详解--Linux下I2C读取AT24C02例程

记录一下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 = &reg_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下载链接,可选择当前最新版本下载。

  • 28
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
以下是一个基于Arduino的iic读取AT24C02的示例代码: ``` #include <Wire.h> #define EEPROM_ADDR 0x50 // EEPROM地址 #define EEPROM_SIZE 256 // EEPROM容量 void setup() { Serial.begin(9600); Wire.begin(); // 初始化I2C总线 } void loop() { // 读取整个EEPROM byte data[EEPROM_SIZE]; readEEPROM(0, data, EEPROM_SIZE); // 输出EEPROM数据 for (int i = 0; i < EEPROM_SIZE; i++) { Serial.print(data[i], HEX); Serial.print(" "); if ((i+1) % 16 == 0) { Serial.println(); } } Serial.println(); delay(5000); } // 从EEPROM中读取数据 void readEEPROM(int addr, byte* data, int len) { Wire.beginTransmission(EEPROM_ADDR); Wire.write((byte)(addr >> 8)); // 地址高位 Wire.write((byte)(addr & 0xFF)); // 地址低位 Wire.endTransmission(); Wire.requestFrom(EEPROM_ADDR, len); for (int i = 0; i < len && Wire.available(); i++) { data[i] = Wire.read(); } } ``` 在上面的示例代码中,我们定义了EEPROM的地址为0x50,容量为256字节。在`setup()`函数中,我们初始化了I2C总线,然后在`loop()`函数中,我们读取整个EEPROM的数据,并输出到串口。在`readEEPROM()`函数中,我们先通过I2C总线发送地址和要读取的起始地址,然后通过`Wire.requestFrom()`函数请求读取数据。最后,我们通过`Wire.read()`函数读取数据并保存到`data`数组中。 要使用此代码,您需要将AT24C02连接到Arduino的I2C总线上,并将A0、A1和A2引脚连接到GND或VCC以设置EEPROM的I2C地址。最后,您可以使用Arduino IDE的串口监视器查看EEPROM的内容。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一码当前

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值