I2C学习笔记

本文介绍了I2C协议的基础,包括时序图中的上升沿、下降沿和应答信号,以及Linux下的I2C驱动结构,如总线、驱动、设备和适配器之间的关系。I2C协议通过两线传输数据,采用主从架构,文中详细阐述了写操作时序和数据格式。在Linux中,I2C驱动通过I2C总线与设备进行通信,涉及驱动注册、设备匹配及数据传输过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、时序图

1.1 时序图基础

方波

在这里插入图片描述

上升沿与下降沿

上升箭头即在上升沿触发,反之则是下降沿触发
在这里插入图片描述

Either Or信号

非此即彼信号表示,既有可能是高也可能是低。
在这里插入图片描述

灰色区域

灰色区域表示三态:低电平,高电平,高阻态。
在这里插入图片描述

F省略

类似f的图形表示省略符号
在这里插入图片描述

二、I2C协议

IIC(Inter-Integrated Circuit)是一种具有两线传输的串行通信总线,使用多主从架构,由飞利浦公司在1980年为了让主板、嵌入式系统或手机连接低速周边设备而提出,适用于数据量不大且传输距离短的场合。IIC串行总线由两根信号线组成,一根是双向的数据线SDA,另一根是单向的时钟线SCL,在空闲状态时,SDA和SCL线都置’1‘,为高电平
IIC为同步的半双工通信方式
常见的传输速率有:100kb/s、300kb/s、3.4Mkb/s

2.1.传输协议

IIC由两根通信信号线组成,SCL是由主模块输入的时钟信号,是单向的信号,而SDA是由主机或从机控制的数据信号,是双向信号。

2.1.1 基本信号介绍

写操作时序

在进行写操作时,SCL一直保持时钟的信号,SDA线的传输以8位为一个单位,在进行第一个8bit的传输后,若从设备接收到传输信号,则会返回一个应答信号ack,然后拉低SDA线,进行下一步的数据写入,写周期时序图如下图所示 ,twr 为进行数据写入的时钟周期。
在这里插入图片描述

数据有效性

SCL保持高电平时,SDA数据必须稳定。SCL为低电平时,SDA才可以改变数据。认为SCL高电平时,数据有效。
在这里插入图片描述

开始结束信号

当SCL为高电平时,SDA由高电平降为低电平代表START信号,SDA由低电平升为高电平代表STOP信号。
在这里插入图片描述

从机应答信号

检测到start信号后,在随后的8个时钟周期,SDA线进行一次8bit的数据传输,若接收到相应的8bit信号,则在拉低SDA信号,并视为一次ack应答信号。
在这里插入图片描述

2.1.2 数据格式

进行一次IIC传输时,由指示当前数据传输开始,由指示当前的数据传输结束,IIC传输格式每一次传输都是以8bit为一个基本传输单位,一次完整的IIC传输包含

Trans_data = Start + n * Bytes+ n * ack + Stop

在这里插入图片描述

当检测到Start信号时,**主机输出8bit的信号,其中前7bit表示从机的地址,为选中的从机信息,第8bit表示当前进行的读写操作,为’1’表示读操作,为’0’表示写操作,然后第9位为从机的应答ack信号,**表示指定从机已接收到地址信号,以进行后续的传输;后续的传输以 8 bit + ack 的重复,即 n * Bytes + n * ack 信号来进行数据的传输,最后主机发出Stop信号,即当前的一次IIC传输结束

2.2 Linux下的I2C驱动

2.2.1 I2C的几个对象

1. I2C总线

I2C总线对应着/bus下的一条总线,这个i2c总线结构体维护着两个链表(I2C驱动、I2C设备),管理着i2c设备与I2C驱动的匹配,删除等操作,I2C总线会调用i2c_device_match函数看I2C设备和I2C驱动是否匹配,如果匹配就调用i2c_device_probe函数,进而调用I2C驱动的probe函数。

struct bus_type i2c_bus_type = {
	.name		= "i2c",
	.match		= i2c_device_match,
	.probe		= i2c_device_probe,
	.remove		= i2c_device_remove,
	.shutdown	= i2c_device_shutdown,
	.pm		= &i2c_device_pm_ops,
};
2. I2C驱动

对应I2C设备的驱动程序

struct i2c_driver {
	int (*probe)(struct i2c_client *, const struct i2c_device_id *); //probe函数
	struct device_driver driver; //表明这是一个驱动
	const struct i2c_device_id *id_table; //要匹配的从设备信息(名称)
	int (*detect)(struct i2c_client *, struct i2c_board_info *); //设备探测函数
	const unsigned short *address_list; //设备地址
	struct list_head clients; //设备链表
};
3. I2C设备

是具体硬件设备的一个抽象

struct i2c_client {
	unsigned short addr; //设备地址
	char name[I2C_NAME_SIZE]; //设备名称
	struct i2c_adapter *adapter; //设配器,值I2C控制器
	struct i2c_driver *driver; //设备对应的驱动
	struct device dev; //表明这是一个设备
	int irq; //中断号
	struct list_head detected; //节点
};
4. I2C适配器

经过上面的介绍,知道有I2C驱动和I2C设备,我们需要通过I2C驱动去和I2C设备通讯,这其中就需要一个I2C适配器,I2C适配器对应的就是SOC上的I2C控制器。

struct i2c_adapter {
	unsigned int id; //设备器的编号
	const struct i2c_algorithm *algo; //算法,发送时序
	struct device dev; //表明这是一个设备
};

struct i2c_algorithm {
    /* 作为主设备时的发送函数 */
	int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,
			   int num);

    /* 作为从设备时的发送函数 */
	int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,
			   unsigned short flags, char read_write,
			   u8 command, int size, union i2c_smbus_data *data);
};

5. 总线,设备,驱动之间关系

总线代表同类设备需要共同遵守的时序,不同总线硬件的通信时序也是不同的
设备代表真实存在的物理器件,每个器件有自己不同的通信时序
驱动代表操作设备的方式和流程,以应用来说,在程序open设备时,接着read这个这个设备,驱动就是实现应用访问的具体过程。驱动就是一个通信官和翻译官,一是通过对soc的控制寄存器编程,按总线要求输出相应时序的命令,与设备交互,一是对得到数据进行处理,给上层提供特定格式数据。

设备与驱动匹配上后。驱动程序需要通过i2c_transfer来与设备通讯,此时就需要指定i2c适配器,故驱动程序是通过i2c适配器向设备发送数据的!

以注册I2C驱动为例,简单讲解I2C总线的运行机制(I2C设备道理相同) 1、注册I2C驱动
2、将I2C驱动添加到I2C总线的驱动链表中
3、遍历I2C总线上的设备链表,根据i2c_device_match函数进行匹配,如果匹配调i2c_device_probe函数
4、i2c_device_probe函数会调用I2C驱动的probe函数

6. adapter,driver(驱动),client(设备),algorithm四个结构体关系介绍
(1)adapter与algorithm之间关系

adapter对应一个适配器,而algorithm对应的则是一套通讯方法。适配器需要通信函数来控制适配器产生合适的访问周期。否则适配器什么也做不了。

(2)driver与client

client对应的是真实的物理设备,每个物理设备都需要一个client来描述。而一个驱动可以对应多个client,即一个driver可以对应多个同类型的client。

(3)adapter与client

client依附于adapter。一个适配器可以连多个设备

2.2.2 Linux I2C体系结构

在这里插入图片描述

I2C体系结构分为三个部分:I2C核心,I2C设备驱动,I2C总线(适配器)驱动。

1. 核心层

提供一组不依赖硬件平台的接口函数,一般不需要被修改。

/*增加/删除i2c_adapter*/
int i2c_add_adapter(struct i2c_adapter *adap);
void i2c_del_adapter(struct i2c_adapter *adap);
/*增加/删除i2c_driver*/
int i2c_register_driver(struct module *owner, struct i2c_driver *driver);
void i2c_del_driver(struct i2c_driver *driver);
/*I2C传输发送与接受: master中包含了transfer*/
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
int i2c_master_send(const struct i2c_client *client, const char *buf, int count);
int i2c_master_recv(const struct i2c_client *client, char *buf, int count);

i2c_transfer() 函数本身不具备驱动适配器物理硬件以完成消息交互的能力,它只是寻找到与 i2c_adapter 对应的 i2c_algorithm, 并使用 i2c_algorithm 的 master_xfer() 函数真正驱动硬件流程。

2. I2C适配器驱动层

对应Soc的I2C控制器,把I2C控制器看做一个设备,实现I2C控制器的驱动代码,和具体的Soc相关

适配器驱动资源的初始化与注册

由于IIC总线控制器通常是在内存上的,所有它本身也连接在platform总线上,要通过paltform_driver 和paltform_device 的匹配来执行。在该paltform_driver 的probe函数中,通常完成两个工作:(1)初始化I2C适配器所使用的硬件资源,如申请i/o地址、中断号、时钟等。(2)通过i2c_add_adapter接口向I2C总线注册了一个适配器。

I2C总线的通信方法

为特定的I2C适配器提供通信方法,主要是实现i2c_algorithm的functionality()函数与master_xfer()函数。

static int i2c_adapter_xxx_xfer(struct i2c_adapter * adap, struct i2c_msg *
msgs, int num)
{
    ......
    for (i = 0; i < num; i++) {
        i2c_adapter_xxx_start();							/* 产生开始位 */
        /* 是读消息 */
        if (msgs[i]->flags & I2C_M_RD) {
			i2c_adapter_xxx_setaddr((msgs->addr << 1) | 1);	/* 发送从设备地址 */
			i2c_adapter_xxx_wait_ack();						/* 获得从设备的ack */
			i2c_adapter_xxx_readbytes((msgs[i]->buf, msgs[i]->len));/* 读取len长度的数据 */ 
		} else {											/* 是写消息 */
			i2c_adapter_xxx_setaddr(msgs->addr << 1);		/* 发送从设备写地址 */
			i2c_adapter_xxx_wait_ack();						/* 获得从设备的ack */
			i2c_adapter_xxx_writebytes(msgs[i]->buf, msgs[i]->len);	/* 写入len长度的数据 */
		}
    }
	i2c_adapter_xxx_stop();									/* 产生停止位 */
}

上述代码实际上给出了一个 master_xfer() 函数处理 I2C 消息数据的流程,对于数组中的每个消息,都是按照该流程跑(硬件的时序),模板中调用的各个函数,用于完成适配器的底层硬件操作,与 I2C 适配器和 CPU 的具体硬件直接相关,根据芯片手册实现。

3.I2C设备驱动层

具体I2C接口的外设相关,每种外设都有自己的专属I2C驱动代码;

static struct i2c_driver yyy_driver = {
    .driver = {
        .name = "yyy",
    },
    .probe			= yyy_probe,
    .remove			= yyy_remove,
    .id_table		= yyy_id,
};
I2C设备驱动的模块加载与卸载
static int __init yyy_init(void)
{
	return i2c_add_driver(&yyy_driver);
}
module_initcall(yyy_init);

static void __exit yyy_exit(void)
{
	i2c_del_driver(&yyy_driver);
}
module_exit(yyy_exit);
I2C设备驱动的数据传输
struct i2c_msg msgs[2];
/* 第一条消息是写消息 */
msgs[0].addr = client->addr;
msgs[0].flags = 0;
msgs[0].len = 1;
msgs[0].buf = &offs;
/* 第二条消息是读消息 */
msgs[1].addr = client->addr;
msgs[1].flags = I2C_M_RD;
msgs[1].len = sizeof(buf);
msgs[1].buf = &buf[0];

i2c_transfer(client->adapter, msg, 2);

2.3 GPIO口模拟I2C总线协议(代码)

/*GPIO口模拟I2C总线协议 */

#define SDA 254                         //定义SDA所对应的GPIO接口编号
#define SCL 255                         //定义SCL所对应的GPIO接口编号
#define OUTP 1                          //表示GPIO接口方向为输出
#define INP 0                           //表示GPIO接口方向为输入

/* I2C起始条件 —————— S信号:SCL保持高电平,SDA由高到低*/
int i2c_start()
{
	//初始化GPIO口
	set_gpio_direction(SDA, OUTP);          //设置SDA方向为输出
	set_gpio_direction (SCL, OUTP);         //设置SCL方向为输出
	set_gpio_value(SDA, 1);                //设置SDA为高电平
	set_gpio_value(SCL, 1);                 //设置SCL为高电平
	delay();                            //延时

	//起始条件
	set_gpio_value(SDA, 0);                 //SCL为高电平时,SDA由高变低
	delay();
}

/* I2C终止条件 ————— P信号:SCL保持高电平,SDA由低到高*/
void i2c_stop()
{
	set_gpio_value(SCL, 1);
	set_gpio_direction(SDA, OUTP);
	set_gpio_value(SDA, 0);
	delay();
	set_gpio_value(SDA, 1);             //SCL高电平时,SDA由低变高
}

/*
	*并非每传输8位数据之后,都会有ACK信号,有以下3种例外
	*1.当从机不能响应从机地址时(例如它正忙于其他事而无法响应IIC总线的操作,或者这个地址没有对应的从机),在第9个SCL周期内SDA线没有拉低,即没有ACK信号。这时,主机发出一个P信号终止传输或者重新发出一个S信号开始新的传输。
	*2.如果从机接收器在传输过程中不能接收更多的数据时,它不会发出ACK信号。这样,主机就可以意识到这点,从而发出一个P信号终止传输或者重新发出一个S信号开始新的传输。
	*3.主机接收器在接收到最后一个字节后,也不会发出ACK信号。于是,从机发送器释放SDA线,以允许主机发出P信号结束传输。
*/

/*
	*应答信号ACK
	*发送器每发送一个字节,就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号。
	*应答信号低电平为有效应答位,高电平为非应答位
	*对于反馈有效应答ACK的要求是,接收器在第9个时钟脉冲之前的低电平期间将SDA拉低,并且确保在该时钟的高电平期间为稳定的低电平
*/

/*
I2C读取ACK信号(写数据时使用)
返回值 :0表示ACK信号有效;非0表示ACK信号无效
*/
unsigned char i2c_read_ack()
{
	unsigned char r;
	set_gpio_direction(SDA, INP);           //设置SDA方向为输入
	set_gpio_value(SCL,0);              // SCL变低
	r = get_gpio_value(SDA);                //读取ACK信号
	delay();
	set_gpio_value(SCL,1);              // SCL变高
	delay();
	return r;
}

/* I2C发出ACK信号(读数据时使用) */
int i2c_send_ack()
{
	set_gpio_direction(SDA, OUTP);          //设置SDA方向为输出
	set_gpio_value(SCL,0);              // SCL变低
	set_gpio_value(SDA, 0);             //发出ACK信号
	delay();
	set_gpio_value(SCL,1);              // SCL变高
	delay();
}

/* I2C字节写 */
void i2c_write_byte(unsigned char b)
{
	int i;
	set_gpio_direction(SDA, OUTP);          //设置SDA方向为输出
	for (i = 7; i >= 0; i--) {
		set_gpio_value(SCL, 0);             // SCL变低
		delay();
		set_gpio_value(SDA, b & (1<<i));        //从高位到低位依次准备数据进行发送
		set_gpio_value(SCL, 1);             // SCL变高
		delay();
	}
	i2c_read_ack();                 //检查目标设备的ACK信号
}

/* I2C字节读 */
unsigned char i2c_read_byte()
{
	int i;
	unsigned char r = 0;
	set_gpio_direction(SDA, INP);           //设置SDA方向为输入
	for (i=7; i>=0; i--) {
		set_gpio_value(SCL, 0);         // SCL变低
		delay();
		r = (r <<1) | get_gpio_value(SDA);      //从高位到低位依次准备数据进行读取
		set_gpio_value(SCL, 1);         // SCL变高
		delay();
	}
	i2c_send_ack();                 //向目标设备发送ACK信号
	return r;
}

/*
I2C读操作
addr:目标设备地址
buf:读缓冲区
len:读入字节的长度
*/
void i2c_read(unsigned char addr, unsigned char* buf, int len)
{
	int i;
	unsigned char t;
	i2c_start();                        //起始条件,开始数据通信
	//发送地址和数据读写方向
	t = (addr << 1) | 1;                    //低位为1,表示读数据
	i2c_write_byte(t);
	//读入数据
	for (i=0; i<len; i++)
		buf[i] = i2c_read_byte();
	i2c_stop();                     //终止条件,结束数据通信
}
/*
I2C写操作
addr:目标设备地址
buf:写缓冲区
len:写入字节的长度
*/
void i2c_write (unsigned char addr, unsigned char* buf, int len)
{
	int i;
	unsigned char t;
	i2c_start();                        //起始条件,开始数据通信
	//发送地址和数据读写方向
	t = (addr << 1) | 0;                    //低位为0,表示写数据
	i2c_write_byte(t);
	//写入数据
	for (i=0; i<len; i++)
		i2c_write_byte(buf[i]);
	i2c_stop();                     //终止条件,结束数据通信
}
### 关于蓝桥杯嵌入式比赛中的I2C学习资料和技术笔记 在准备蓝桥杯嵌入式的比赛中,掌握I2C协议及其应用是非常重要的。以下是针对I2C相关内容的学习建议以及参考资料: #### 一、基础知识 I2C(Inter-Integrated Circuit)是一种用于短距离通信的串行总线协议,广泛应用于嵌入式系统中设备间的通信。其特点包括支持多主控和多从机模式,通过SDA(数据线)和SCL(时钟线)实现双向传输[^1]。 #### 二、具体应用场景 在蓝桥杯嵌入式竞赛中,通常涉及以下两个典型场景: 1. **读取24C02 EEPROM存储器** 使用I2C协议与24C02芯片交互,完成数据的写入和读取操作。这需要理解地址帧结构、起始条件、停止条件等基本概念[^2]。 2. **控制MCP4017可编程电阻** MCP4017是一款基于I2C接口的数字电位计,能够通过软件调整阻值。参赛者需熟悉如何配置寄存器并发送命令字节来改变输出电压。 #### 三、推荐学习资源 1. **博客文章** - @maosql 提供了一系列有关蓝桥杯嵌入式比赛的技术分享,其中包含了对常见硬件模块的操作指南及调试经验总结。 - 文章链接:视频教程 2. **官方文档或开源项目** - 参考 `iic-hal.c` 和 `iic-hal.h` 文件作为驱动开发的基础框架,在实际工程项目中可根据需求对其进行适配修改[^3]。 - 将上述源码迁移到 BSP (Board Support Package) 目录下,并遵循团队内部统一命名规范以便维护管理。 3. **实践案例分析** 结合理论知识动手搭建实验平台验证功能正确性至关重要。例如尝试编写一段简单的程序初始化 I²C 总线并与外设建立连接;或者模拟真实赛场环境测试时间效率等问题解决能力提升效果显著。 #### 四、注意事项 - 编程过程中要特别留意端口方向设置及时序参数调节以确保信号质量稳定可靠; - 防止因拉高/拉低电流过大损坏器件引脚; - 对异常情况进行充分预判处理以免影响整体性能表现。 ```c // 示例代码片段展示基础收发流程 #include "iic_hal.h" void iic_init(void){ // 初始化GPIO及相关寄存器状态... } uint8_t iic_start(uint8_t slave_addr){ // 发送启动信号并指定目标设备地址返回应答标志位 } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值