赞
踩
IIC协议简介—学习笔记_iic标准协议_越吃越胖的黄的博客-CSDN博客
I2C(Inter-Integrated Circuit)是一种串行通信协议,用于连接微控制器、传感器、存储器和其他外设。
I2C使用两条线(SDA和SCL)进行通信,可以连接多个设备,每个设备都有一个唯一的地址。I2C总线上的设备可以充当主设备或从设备。主设备负责发起通信,从设备负责响应通信请求。
I2C协议具有以下特点:
要能手撕出来
仲裁:SDA线的仲裁也是建立在总线具有线“与”逻辑功能(线与逻辑,即两个以上的输出端直接互连就可以实现“AND”的逻辑功能。两个一出一,一个一出零、没有一出零)的原理上的。节点在发送1位数据后,比较总线上所呈现的数据与自己发送的是否一致。是,继续发送;否则,进行比较,输出低电平进行发送,输出高电平退出。SDA线的仲裁可以保证I2C总线系统在多个主节点同时企图控制总线时通信正常进行并且数据不丢失。总线系统通过仲裁只允许一个主节点可以继续占据总线。
要能手撕出来
时序总结:
总线空闲状态 | SCL和SDA均为高电平,接上拉电阻。 |
---|---|
起始信号(START) | 在SCL保持高电平期间,SDA由高电平被拉低。由主控器发出。 |
数据位传送(DATA) | 在SCL保持高电平期间,SDA上的电平保持稳定,低电平为数据0、高电平为数据1。用法:主控器和被控器都可发出。 |
应答信号(ACK) | 在SCL保持高电平期间,SDA保持低电平。IIC总线上所有数据都是以8位字节传送的,发送器每发送一个字节,就在第9个时钟脉冲期间释放SDA(高电平),由接收器反馈一个ACK。 |
非应答信号(NACK) | 在SCL保持高电平期间,SDA保持高电平。如果接收器是主控器,则它在收到最后一个字节后,发送一个NACK,通知被控器结束数据发送,并释放SDA(高电平),以便主控器发送一个STOP。 |
停止信号(STOP) | 在SCL保持高电平时间,SDA由低电平被释放(拉高)。由主控器发出。 |
各时序部分的代码(要能手撕出来):
起始信号
void IIC_Start(void)
{
SDA_H; //拉高SDA线
SCL_H; //拉高SCL线
DelayUs(iicInfo.speed); //延时,速度控制
SDA_L; //当SCL线为高时,SDA线一个下降沿代表开始信号
DelayUs(iicInfo.speed); //延时,速度控制
SCL_L; //钳住SCL线,以便发送数据
}
停止信号
void IIC_Stop(void)
{
SDA_L; //拉低SDA线
SCL_L; //拉低SCL先
DelayUs(iicInfo.speed); //延时,速度控制
SCL_H; //拉高SCL线
SDA_H; //拉高SDA线,当SCL线为高时,SDA线一个上升沿代表停止信号
DelayUs(iicInfo.speed);
}
应答信号
//产生ACK应答,读取从机一字节数据后还要接着读的时候使用 //SCL在SDA一直为低电平期间完成低高电平转换 void IIC_Ack(void) { SCL_L; //拉低SCL线 SDA_L; //拉低SDA线<---- DelayUs(iicInfo.speed); SCL_H; //拉高SCL线 DelayUs(iicInfo.speed); SCL_L; //拉低SCL线 } //不产生ACK应答,读取从机一字节数据后不读了的时候使用 //SCL在SDA一直为高电平期间完成低高电平转换 void IIC_NAck(void) { SCL_L; //拉低SCL线 SDA_H; //拉高SDA线<---- DelayUs(iicInfo.speed); SCL_H; //拉高SCL线 DelayUs(iicInfo.speed); SCL_L; //拉低SCL线 } //等待ACK应答 //发送完一个字节后(释放SDA)的下一个时钟高电平时期,读取SDA电平,0为收到应答 _Bool IIC_WaitAck(unsigned int timeOut) { SDA_H;DelayUs(iicInfo.speed); //拉高SDA线 SCL_H;DelayUs(iicInfo.speed); //拉高SCL线 while(SDA_R) //如果读到SDA线为1,则等待。应答信号应是0 { if(--timeOut) { printf("WaitAck TimeOut\r\n"); IIC_Stop(); //超时未收到应答,则停止总线 return IIC_Err; //返回失败 } DelayUs(iicInfo.speed); } SCL_L; //拉低SCL线,以便继续收发数据 return IIC_OK; //返回成功 }
发送数据
void IIC_Send(unsigned char byte) { unsigned char count = 0; SCL_L; //拉低时钟开始数据传输 for(; count < 8; count++) //循环8次,每次发送一个bit { if(byte & 0x80) //【先发送最高位,大端传输】 SDA_H; else SDA_L; byte <<= 1; //byte左移1位 DelayUs(iicInfo.speed); SCL_H; DelayUs(iicInfo.speed); SCL_L; } }
接收数据
unsigned char IIC_Read(void) { unsigned char count = 0, receive = 0; SDA_H; //拉高SDA线,开漏状态下,需线拉高以便读取数据 for(; count < 8; count++ ) //循环8次,每次发送一个bit { SCL_L; //拉低,从机放数据 DelayUs(iicInfo.speed); SCL_H; //拉高,主机读数据 receive <<= 1; //左移一位 if(SDA_R) //如果SDA线为1,则receive变量自增,每次自增都是对bit0的+1,然后下一次循环会先左移一次 receive++; DelayUs(iicInfo.speed); } return receive; }
要能手撕软件模拟IIC通讯代码
一、写数据
字节写
void IIC_SEND_BYTE(u8 slaveaddr,u8 registeraddr,u8 byte);//发送到具体从机地址及相关寄存器一个字节
{
IIC_Start(); //通讯开始
IIC_Send(slaveaddr); //先发送从机的地址,寻址从机
IIC_WaitAck(); //得到回应,说明,电路中有这个外设器件
IIC_Send(registeraddr);//寻址这个器件中的相关寄存器
IIC_WaitAck(); //得到回应,说明这个寄存器存在
IIC_Send(byte); //发送一个字节
IIC_WaitAck(); //等待回应
}
页写
指定设备+指定地址+指定数据
void IIC_SEND_BYTES(u8 slaveaddr,u8 registeraddr,u8 *pbuffer,u16 num );//发送一串数据给具体从机的相关寄存器
{
IIC_Start(); //开始通讯
IIC_Send(slaveaddr); //寻址从机
IIC_WaitAck(); //等待回应
IIC_Send(registeraddr); //寻址寄存器
IIC_WaitAck(); //等待回应
for(t=0;t<num;t++) //发送数组中的数据
{
IIC_Send(*(pbuffer+t));
IIC_WaitAck(); //每发送一个字节,都需要一个应答
}
IIC_Stop(); //终止通讯
}
二、读数据
当前地址读
指定设备+从机当前地址指针指示的地址下,读从机的数据
指定地址读
指定设备+指定地址+读从机数据
指定设备写+指定地址(让从机指针先指过去)
指定设备读+读指定地址
void I2C_READ_BYTES(u8 slaveaddr,u8 registeraddr,u8 *pbuffer,u16 num );//读取具体从机的相关寄存器一串字节 { I2C_Start(); //开始通讯 I2C_Send(slaveaddr); //寻址从机 I2C_WaitAck(); //等待回应 I2C_Send(registeraddr);//寻址寄存器 I2C_WaitAck(); //等待回应 I2C_Start(); //重新通讯 I2C_Send(slaveaddr+1); //改为读数据 I2C_WaitAck(); //等待回应 for(t=0;t<num;t++) //存储数据 { *(pbuffer+t)=I2C_Read(); if (t== num-1) //字节没发完,必须给出应答,发完的给个非应答信号 { IIC_Ack(); } else { IIC_NAck(); } } IIC_Stop(); //通讯结束 }
IIC协议规定,在启动总线后第1字节的高7位是从节点的寻址地址,其中高四位为器件类型识别符,接着三位为片选,最后一位为读写位,当为1时为读操作,为0时为写操作,所以具体挂载多少个器件由I2C地址决定,7位寻址地址减去1个广播地址0x00不用,所以有2^7=128 - 1 = 127,那就是127个地址,所以理论上可以挂127个从器件。
每个设备都必须具有唯一的7位或10位地址(在7位地址模式下,地址是0-127之间的数字,在10位地址模式下,地址是0-1023之间的数字)。
通过GPIO控制IIC芯片上的地址选择引脚选择改变器件地址
或可以通过硬件上设计,控制器件是否挂载总线来实现(方法可用一个开关电路切断器件SDA或者SCL是否接入总线来实现)。
I2C总线具有多主控能力,有时会发生两个或多个主器件同时想占用总线的情况,这种情况叫做总线竞争。I2C可以对发生在SDA线上的总线竞争进行仲裁,I2C总线的仲裁逻辑是建立在线与功能上的,有一个拉低SDA总线就是低。
其仲裁原则是这样的:每一个主器件每次发送一位数据,然后比较总线上所呈现的电平与自己所发送的数据是否一致,如果一致,继续发送下一位数据,否则就退出竞争。
总线竞争的仲裁在两个层次上进行:首先是地址位的比较,如果主器件寻址同一个从器件,则进入数据位比较,从而确保竞争仲裁的可靠性。由于是利用IIC总线上的信息进行仲裁,不会造成信息的丢失。
如果一个设备收到了其他设备发送的仲裁比特和地址,那么它会停止发送数据并释放总线,让另一个设备继续控制总线。如果没有任何一个设备释放总线,则总线将一直处于锁定状态,直到有设备释放总线。如果I2C总线一直处于锁定状态,那么任何设备都无法进行数据传输,因为总线被其他设备占用。这意味着任何设备都不能发送数据,也不能接收数据。因此,设备之间的通信将被阻塞,直到总线被释放。如果总线被锁定的时间太长,可能会导致数据传输的失败和系统的异常。为了避免这种情况,需要在设计和实现I2C系统时仔细考虑总线仲裁机制,并确保在系统中合理地分配和协调I2C总线上的设备。
1、进行数据传送时,在SCL为高电平期间,SDA线上电平必须保持稳定,只有SCL为低时,才允许SDA线上电平改变状态。并且每个字节传送时都是高位在前。
2、对于应答信号,ACK=0时为有效应答位,说明从机已经成功接收到该字节,若为1则说明接受不成功。
3、如果从机需要延迟下一个数据字节开始传送的时间,可以通过把SCL电平拉低并保持来强制主机进入等待状态。
4、主机完成一次通信后还想继续占用总线在进行一次通信,而又不释放总线,就要利用重启动信号Sr。它既作为前一次数据传输的结束,又作为后一次传输的开始。
5、总线冲突时,按“低电平优先”的仲裁原则,把总线判给在数据线上先发送低电平的主器件。
6、在特殊情况下,若需禁止所有发生在I2C总线上的通信,可采用封锁或关闭总线,具体操作为在总线上的任一器件将SCL锁定在低电平即可。
7、SDA仲裁和SCL时钟同步处理过程没有先后关系,而是同时进行的。
任何IIC设备都有一个7位地址,理论上,现实中只能有127种不同的IIC设备。实际上,已有IIC的设备种类远远多于这个限制,在一条总线上出现相同的地址的IIC设备的概率相当高。为了突破这个限制,很多设备使用了双重地址——7位地址加引脚地址。IIC标准也预知了这种限制,提出10位的地址方案。
10位的地址方案对IIC协议的影响有两点:
除了10位地址标识,标准还预留了一些地址码用作其它用途,如下表:
在IIC通信中,主设备决定了时钟速度。因为时钟脉冲信号是由主设备显式发出的。但是,当从设备没办法跟上主设备的速度时,从设备需要一种机制来请求主设备慢一点,这种机制称为时钟拉伸。而基于IIC结构的特殊性,这种机制得到实现。当从设备需要降低传输的速度的时候,它可以按下时钟线,逼迫主设备进入等待状态,直到从设备释放时钟线,通信才继续。
在I2C总线上传送信息时的时钟同步信号是由挂接在SCL线上的所有器件的逻辑“与”完成的。SCL线上由高电平到低电平的跳变将影响到这些器件,一旦某个器件的时钟信号下跳为低电平,将使SCL线一直保持低电平,使SCL线上的所有器件开始低电平期。此时,低电平周期短的器件的时钟由低至高的跳变并不能影响SCL线的状态,于是这些器件将进入高电平等待的状态。当所有器件的时钟信号都上跳为高电平时,低电平期结束,SCL线被释放返回高电平,即所有的器件都同时开始它们的高电平期。其后,第一个结束高电平期的器件又将SCL线拉成低电平。这样就在SCL线上产生一个同步时钟。可见,时钟低电平时间由时钟低电平期最长的器件确定,而时钟高电平时间由时钟高电平期最短的器件确定。
如果被控器希望主控器降低传送速度可以通过将SCL主动拉低延长其低电平时间的方法来通知主控器,当主控器在准备下一次传送发现SCL的电平被拉低时就进行等待,直至被控器完成操作并释放SCL线的控制控制权。这样,主控器实际上受到被控器的时钟同步控制。可见SCL线上的低电平是由时钟低电平最长的器件决定;高电平的时间由高电平时间最短的器件决定。这就是时钟同步,它解决了I2C总线的速度同步。
彻底搞懂IIC总线(5)I2C总线传输速度详解 - 德力威尔王术平 - 博客园 (cnblogs.com)
(2条消息) spi、iic、can高速传输速度与选择_万_大_帅的博客-CSDN博客_spi速度
终于看懂了!通信协议 IIC 与 SPI 最全对比 - 知乎 (zhihu.com)
速度模式 | 最高比特率( bit/s) | 备注 |
---|---|---|
标准模式(Sm) | 100K | 双向传输,高速模式兼容低速模式 |
快速模式(Fm) | 400K | |
快速模式增强(Fm+) | 1M | |
高速模式(HSm) | 3.4M | |
超快速模式(UFm) | 5M | 单向传输,不兼容其他模式 |
高速模式
原理上讲,使用上拉电阻来设置逻辑1,会限制总线的最大传输速度。而速度是限制总线应用的因素之一。这也说明为什么要引入高速模式(3.4Mbps)。在发起一次高速模式传输前,主设备必须先在低速的模式下(例如快速模式)发出特定的“High Speed Master”信号。为缩短信号的周期和提高总线速度,高速模式必须使用额外的I/O缓冲区。另外,总线仲裁在高速模式下可屏蔽掉。
异步IIC是指同步串行通信的I2C(Inter-Integrated Circuit)协议的一种变体。在标准的I2C协议中,通信是同步的,而异步IIC则是一种基于异步通信方式的I2C协议。
在异步IIC中,时钟信号不是由主设备(Master)提供,而是由从设备(Slave)生成。这意味着从设备负责控制时钟,并在数据线(SDA)上发送数据。主设备则负责检测从设备发送的时钟信号和数据,并对数据进行解析。
异步IIC协议的工作原理如下:
从设备在数据线上生成时钟信号,并将数据发送到数据线上。
主设备检测数据线上的时钟信号,并在时钟信号的边沿进行数据采样。
主设备根据采样到的数据进行处理,如读取数据或向从设备发送命令。
主设备和从设备之间通过数据线进行双向通信。
异步IIC协议相对于标准的同步I2C协议,在实现上更为简单,因为主设备不需要提供时钟信号。然而,由于时钟信号是由从设备生成的,因此在异步IIC中可能存在一些时钟同步的挑战,例如时钟偏移和数据速率的不一致。因此,异步IIC通常用于一些特定的应用场景,而不是广泛应用于各种设备之间的通信。
SMBUS协议
SMBus: System Management Bus,系统管理总线。
SMBus是基于I2C协议的,SMBus要求更严格,SMBus是I2C协议的子集。
I2C-Tools的访问I2C设备的2种方式
官方留的应用层直接控制的接口
I2C-Tools可以通过SMBus来访问I2C设备,也可以使用一般的I2C协议来访问I2C设备。
使用一句话概括I2C传输:APP通过I2C Controller与I2C Device传输数据。
在APP里,有这几个问题:
使用GPIO模拟I2C的驱动程序
看看官方留的应用层直接控制的接口对应的驱动
i2c-dev.c注册过程分析
就是字符设备那一套
file_operations函数分析
i2cdev_ioctl: I2C_SLAVE/I2C_SLAVE_FORCE
用于传地址
i2cdev_ioctl: I2C_RDWR
读写方式1
i2cdev_ioctl: I2C_SMBUS
读写方式2
问题:Linux IIC子系统设备树中没有写那个IIC adapter,驱动怎么对应上呢❓
四种方法
1.在用户态生成
示例:
// 在I2C BUS0下创建i2c_client
# echo ap3216c 0x1e > /sys/bus/i2c/devices/i2c-0/new_device
// 删除i2c_client
# echo 0x1e > /sys/bus/i2c/devices/i2c-0/delete_device
2.编写代码
i2c_new_device:会创建i2c_client,即使该设备并不存在
static struct i2c_board_info sfe4001_hwmon_info = {
I2C_BOARD_INFO("max6647", 0x4e),
};
int sfe4001_init(struct efx_nic *efx)
{
(...)
efx->board_info.hwmon_client =i2c_new_device(&efx->i2c_adap, &sfe4001_hwmon_info);
(...)
}
i2c_new_probed_device
它成功的话,会创建i2c_client,并且表示这个设备肯定存在
I2C设备的地址可能发生变化,比如AT24C02的引脚A2A1A0电平不一样时,设备地址就不一样
可以罗列出可能的地址
i2c_new_probed_device使用这些地址判断设备是否存在
static const unsigned short normal_i2c[] = { 0x2c, 0x2d, I2C_CLIENT_END }; static int usb_hcd_nxp_probe(struct platform_device *pdev) { (...) struct i2c_adapter *i2c_adap; struct i2c_board_info i2c_info; (...) i2c_adap = i2c_get_adapter(2); memset(&i2c_info, 0, sizeof(struct i2c_board_info)); strscpy(i2c_info.type, "isp1301_nxp", sizeof(i2c_info.type)); isp1301_i2c_client = i2c_new_probed_device(i2c_adap, &i2c_info, normal_i2c, NULL); i2c_put_adapter(i2c_adap); (...) }
i2c_register_board_info
EXPORT_SYMBOL(i2c_register_board_info)
3.使用设备树生成
在某个I2C控制器的节点下,添加如下代码:
ap3216c@1e {
compatible = "lite-on,ap3216c";
reg = <0x1e>;
};
**4.(不推荐):由i2c_driver.detect函数来判断是否有对应的I2C设备并生成i2c_client **
它知道怎么发数据,不知道要发什么数据。要发的数据通过i2c_driver经ic_core过来
关心第几条IIC总线nr,IIC 算法i2c_algorithm,master_xfer函数
一般来说芯片厂商都写好了
master_xfer:这是最重要的函数,它实现了一般的I2C传输,用来传输一个或多个i2c_msg
编写i2c_adapter驱动程序
设备树
在设备树里构造I2C Bus节点:
i2c-bus-virtual {
compatible = "100ask,i2c-bus-virtual";
};
platform_driver
分配、设置、注册platform_driver结构体。
核心是probe函数,它要做这几件事:
i2c_apdater
i2c_apdater核心是master_xfer函数,它的实现取决于硬件,大概代码如下:
static int xxx_master_xfer(struct i2c_adapter *adapter, struct i2c_msg *msgs, int num) { for (i = 0; i < num; i++) { struct i2c_msg *msg = msgs[i]; { // 1. 发出S信号: 设置寄存器发出S信号 CTLREG = S; // 2. 根据Flag发出设备地址和R/W位: 把这8位数据写入某个DATAREG即可发出信号 // 判断是否有ACK if (!ACK) return ERROR; else { // 3. read / write if (read) { STATUS = XXX; // 这决定读到一个数据后是否发出ACK给对方 val = DATAREG; // 这会发起I2C读操作 } else if(write) { DATAREG = val; // 这会发起I2C写操作 val = STATUS; // 判断是否收到ACK if (!ACK) return ERROR; } } // 4. 发出P信号 CTLREG = P; } } return i; }
I2C Core就是I2C核心层,它的作用:
I2C总线-设备-驱动模型
,管理:I2C设备(i2c_client)、I2C设备驱动(i2c_driver)、I2C控制器(i2c_adapter)先有个整体框架
APP通过I2C Controller与I2C Device传输数据
APP通过i2c_adapter与i2c_client传输i2c_msg
使用i2c_adapter表示一个I2C BUS,或称为I2C Controller
里面有2个重要的成员:
nr:第几个I2C BUS(I2C Controller)
i2c_algorithm,里面有该I2C BUS的传输函数,用来收发I2C数据
怎么表示I2C Device
怎么表示要传输的数据
内核函数i2c_transfer
i2c_driver
知道发什么数据,不知道怎么发数据。如打开哪个设备、读写哪个寄存器
i2c_driver表明能支持哪些设备:
i2c_client
主要提供IIC设备名字和地址,以及设备挂载的i2c_adapter让驱动能对应上
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。