先认识I2C通信
基本概述
IIC(Inter-Integrated Circuit)其实是IICBus简称,所以中文应该叫集成电路总线,它是一种串行通信总线,使用多主从架构,由飞利浦公司在1980年代为了让主板、嵌入式系统或手机用以连接低速周边设备而发展。I²C的正确读法为“I平方C”("I-squared-C"),而“I二C”("I-two-C")则是另一种错误但被广泛使用的读法。自2006年10月1日起,使用I²C协议已经不需要支付专利费,但制造商仍然需要付费以获取I²C从属设备地址。
I2C总线是一种同步、半双工,带数据应答的二线制串行总线。它只需要两根线即可在连接于总线上的器件之间传送信息。
主器件用于启动总线传送数据,并产生时钟以开放传送的器件,此时任何被寻址的器件均被认为是从器件。
在总线上主和从、发和收的关系不是恒定的,而取决于此时数据传送方向。
如果主机要发送数据给从器件,则主机首先寻址从器件,然后主动发送数据至从器件,最后由主机终止数据传送;如果主机要接收从器件的数据,首先由主器件寻址从器件,然后主机接收从器件发送的数据,最后由主机终止接收过程。在这种情况下.主机负责产生定时时钟和终止数据传送。
IIC是为了与低速设备通信而发明的。
比如:本来高速设备一个周期发送1位,但是低速设备5个周期才能接收1位。两者速率无法同步,现在通过主设备控制1个时钟频率,使得主从设备能够在同一个时钟频率下工作。
IIC的传输速率比不上SPI。
通信速率一般(kbps级别),不适合语音、视频等信息类型。
主要用途:SoC和周边外设之间的通信(典型的如EEPROM、电容触摸IC、各种sensor等)
物理接口:SCL + SDA
SCL(serial clock):时钟线,传输CLK信号,一般是I2C主设备向从设备提供时钟的通道。
SDA(serial data):数据线,通信数据都通过SDA线传输。通信特征:串行、同步、非差分、低速率
- I2C属于串行通信,所有的数据以位为单位在SDA线上串行传输。
- 同步通信就是通信双方工作在同一个时钟下,一般是通信的A方通过一根CLK信号线传输A自己的时钟给B,B工作在A传输的时钟下。所以同步通信的显著特征就是:通信线中有CLK。
- 非差分。因为I2C通信速率不高,而且通信双方距离很近,所以使用电平信号通信。
- 低速率。I2C一般是用在同一个板子上的2个IC之间的通信,而且用来传输的数据量不大,所以本身通信速率很低(一般几百Kbps,不同的I2C芯片的通信速率可能不同,具体在编程的时候要看自己所使用的设备允许的I2C通信最高速率,不能超过这个速率)
突出特征1:主设备+从设备
I2C通信的时候,通信双方地位是不对等的,而是分主设备和从设备。通信由主设备发起,由主设备主导,从设备只是按照I2C协议被动的接受主设备的通信,并及时响应。
谁是主设备、谁是从设备是由通信双方来定的(I2C协议并无规定),一般来说一个芯片可以只做主设备、也可以只做从设备、也可以既当主设备又当从设备(软件配置)。突出特征2:可以多个设备挂在一条总线上(从设备地址)
I2C通信可以一对一(1个主设备对1个从设备),也可以一对多(1个主设备对多个从设备)。
主设备来负责调度总线,决定某一时间和哪个从设备通信。注意:同一时间内,I2C的总线上只能传输一对设备的通信信息,所以同一时间只能有一个从设备和主设备通信,其他从设备处于“休眠”状态,不能出来捣乱,否则通信就乱套了(广播然后匹配的思路)。
每一个I2C从设备在通信中都有一个I2C从设备地址,这个设备地址是从设备本身固有的属性,然后通信时主设备需要知道自己将要通信的那个从设备的地址,然后在通信中通过地址来甄别是不是自己要找的那个从设备。(这个地址是一个电路板上唯一的,不是全球唯一的)关于I2C的上拉电阻:I2C协议规定,总线空闲时两根线都必须为高。
小对比:
SPI是通过片选来一对多的(需要多根片选线),I2C是通过地址识别来一对多的(用SDA来实现即可,无需额外的地址线)。
时序:起始和结束
I2C总线上有2种状态;空闲态(所有从设备都未和主设备通信,此时总线空闲)和忙态(其中一个从设备在和主设备通信,此时总线被这一对占用,其他从设备必须歇着)。
整个通信分为一个周期一个周期的,两个相邻的通信周期是空闲态。每一个通信周期由一个起始位开始,一个结束位结束,中间是本周期的通信数据。
起始位并不是一个时间点,起始位是一个时间段,在这段时间内总线状态变化情况是:SCL线维持高电平,同时SDA线发生一个从高到低的下降沿。
与起始位相似,结束位也是一个时间段。在这段时间内总线状态变化情况是:SCL线维持高电平,同时SDA线发生一个从低到高的上升沿。
时序:数据传输
每一个通信周期的发起和结束都是由主设备来做的,从设备只有被动的响应主设备,没法自己自发的去做任何事情。
主设备在每个通信周期会先发8位的数据(其中7位是从设备地址,还有1位表示主设备下面要写入还是读出,所以最多能连接2^7即128个从设备)到总线。然后总线上的每个从设备都能收到这个地址,并且收到地址后和自己的设备地址比较看是否相等。如果相等说明主设备本次通信就是给我说话,如果不相等说明这次通信与我无关,不用听了不管了。
时序:ACK应答
ACK,Acknowledge character,确认字符
发送方发送一段数据后,接收方需要回应一个ACK。这个响应本身只有1个bit位,不能携带有效信息,只能表示2个意思(要么表示收到数据,即有效响应;要么表示未收到数据,无效响应)
在某一个通信时刻,主设备和从设备只能有一个在发(占用总线,也就是向总线写),另一个在收(从总线读)。应答信号是可以被配置有或者没有的。
数据格式如下:
I2C通信时的基本数据单位也是以字节为单位的,每次传输的有效数据都是1个字节(8位)。
起始位及其后的8个clk都是主设备在发送(主设备掌控总线),此时从设备只能读取总线来得知主设备发给它的信息;然后到了第9周期,按照协议规定从设备需要发送ACK给主设备,所以此时主设备必须释放总线(也就是主设备把SDA总线置为高电平然后不要动),同时从设备试图拉低总线发出ACK。如果从设备拉低总线失败,或者从设备根本就没有拉低总线,则主设备看到的现象就是总线在第9周期仍然一直保持高,这对主设备来说,意味着我没收到ACK,主设备就认为刚才给从设备发送的8字节不对(接收失败)
写数据和读数据
发送一个字节:
SCL低电平期间,主机将数据位依次放到SDA线上(高位在前),然后拉高SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。
接收应答:
在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答。
接收一个字节:
SCL低电平期间,从机将数据位依次放到SDA线上(高位在前),然后拉高SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节。
发送应答:
在接收完一个字节之后,主机在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
如果是读取数据,我们就会知道是否读到数据,所以一般无需发送应答。
数据传输顺序
SPI可以选择从最低位或者最高位开始发送,之前学DS1302的时候是从最低位开始发的,可是在学习SPI的时候又说是从最高位开始发送的。不知道哪个是对的。后面查资料,又说其实是可以配置的,具体情况具体对待的,看手册就知道了,不必纠结。
虽然很多器件都是用了某种接口协议,比如SPI I2C等等,但是对这些接口协议的封装会根据具体功能有所差异,不是完全一致的。另一方面,有些协议是要收费的,所以制造商只能借鉴,不能完全使用。只是说,用了那些接口协议的思想。I2C一般是从最高位开始传输的。
关于释放总线
某个引脚如果接了地,那么无法将其拉高;如果引脚是高电平,那么可以控制其拉高或者拉低。因为接地的控制力比高电平高。
释放总线,就是将总线置1,为什么,因为只有置1,其他设备才有可能改变其状态。如果是置0,那么其它设备就无法将其从接地中改变状态。
关于边沿触发和电平触发
在之前学习SPI时,是边沿触发。这里的EEPROM,或者说I2C,是高电平触发。
关于触发方式,在硬件上来说,涉及到触发器和锁存器。如果器件处于锁存状态,那么输出就不受输入影响。如果想要某时刻的输入作用后能影响到输出,就要触发,那么,什么时候触发,就是个问题。
有些电路设计让电平改变的瞬间触发,过了这个点,就会进入锁存状态。这就是边沿触发,分为上升沿触发、下降沿触发以及双边触发。
有些电路设计,只要电平处于某种状态,比如高电平或者低电平,就可以一直触发。比如当处于高电平时,某电路只要输入一变,输出就会随之改变。
更多可以参考:下降沿触发和低电平触发的区别 - 知乎
我们进一步来考虑编程中的处理。
比如,上升沿触发,和高电平触发,在编程时有何区别?
这里很容易搞错,比如上升沿触发是SCL = 0;SCL = 1;高电平触发也是SCL = 0; SCL = 1;
这里,一个是关注瞬间,一个是关注阶段。
写数据时:
先将数据放到数据线上,然后边沿触发就会在改变的瞬间写入;高电平触发在高电平期间就会起作用。按照原理来看,高电平触发可以先放数据然后再将电平拉高,也可以先将电平拉高然后再放数据。有的设计中,要求高电平至少持续一段时间才能将数据写入。
读数据时:
主设备先拉高电平,在电平拉高的瞬间,从设备的数据就会被发送到总线上,然后主设备去获取该数据。如果是电平触发,那么,主设备要先拉高电平,在高电平期间从设备就可以将数据放到总线上,然后主设备就可以去读取。读取完就可以将电平拉低。有的设计中,要求高电平至少持续一段时间从设备才能将数据放到总线上。
在两种方式时,到达高电平之后,都会有一段延时。虽然都是延时,但是作用不同。
边沿触发后的延时是为了构建正常的时钟信号,对延时的时长没有具体要求。
电平触发的延时是为了构建起作用的条件,有时,还会对延时时间有要求。
//写入/// //边沿触发 SDA = 1; SCL = 0; SCL = 1; //电平触发 SDA = 1; SCL = 0; SCL = 1; Delay(); //或者 SCL = 0; SCL = 1; SDA = 1; Delay(); //读数据/// //边沿触发 SCL = 0; SCL = 1; DAT = SDA; //电平触发 SCL = 0; SCL = 1; DAT = SDA; Delay(); SCL = 0;
具体用什么方式来触发,一般取决于从设备所使用的协议要求。
另外,是否需要延时,延时多久,都需要取决于从设备的协议要求,如果不延时,频率过快,从设备可能承受不了这么高的频率,从而导致数据获取错误。
EEPROM
ROM:Read-Only Memory,只读存储器,断电后也能保存数据。
EEPROM:Electrically Erasable Programmable Read-Only Memory,电可擦除可编程只读存储器,最小读写单位为字节,读写速度很慢。
EEPROM存在系统中的2种形式:内置在单片机内部,外部扩展。
EEPROM如何编程?
- I2C接口底层时序
- 器件定义的寄存器读写时序
24C02
相关内容查看数据手册,这里提供一篇参考文章:
24C02是一个2Kbit的串行EEPROM存储芯片,可存储256个字节数据。工作电压范围为1.8V到6.0V,具有低功耗CMOS技术,自定时擦写周期,1000000次编程/擦除周期,可保存数据100年。24C02有一个16字节的页写缓冲器和一个写保护功能。通过I2C总线通讯读写芯片数据,通讯时钟频率可达400KHz。
可以通过存储IC的型号来计算芯片的存储容量是多大,比如24C02后面的02表示的是可存储2Kbit的数据,转换为字节的存储量为2*1024/8 = 256Byte;又比如24C04后面的04表示的是可存储4Kbit的数据,转换为字节的储存量为4*1024/8 = 512Byte;以此来类推其它型号的存储空间。
24C02的管脚图如下:VCC和VSS是芯片的电源和地,电压的工作范围为:+1.8V~+6.0V。
A0、A1、A2是IC的地址选择脚。
WP是写保护使能脚。
SCL是I2C通讯时钟引脚。
SDA是I2C通讯数据引脚。
下图为芯片从地址:
以看出对于不同大小的24Cxx,具有不同的从器件地址。由于24C02为2k容量,也就是说只需要参考图中第一行的内容。芯片的寻址:
AT24C设备地址为如下:前四位固定为1010,A2~A0为由管脚电平。AT24CXX EEPROM Board模块中默认为接地。A2-A0=000,最后一位表示读写操作。所以AT24Cxx的读地址为0xA1,写地址为0xA0。也就是说:
写24C02的时候,从器件地址为10100000(0xA0);
读24C02的时候,从器件地址为10100001(0xA1)。片内地址寻址(注意从设备地址和内部寻址的区别):
芯片寻址可对内部256B中的任一个进行读/写操作,其寻址范围为00~FF,共256个寻址单位。
具体解释:
由于24C02只有256个字节的存储空间,所以只需要1个字节就可以寻址完24C02的存储空间,但是无法寻址完更大容量的存储IC,比如24C04的存储容量是512字节,需要9个bit的地址位才能寻址完。那怎么办呢?该芯片的解决方法是将A0作为其中一个地址位,由上图可以看到,24C04的设备地址内是没有A0参数的,也就是说24C04的A0引脚是不起作用的,这样也就造成了在I2C总线上只能同时挂载4个24C04芯片。其它存储器如24C08、24C16也可以这么类推。24C02的WP引脚是写保护引脚,当WP引脚接高电平时,24C02只能进行读取操作,不能进行写操作。只有当WP引脚悬空或接低电平时,24C02才能进行写操作。
读写EEPROM
在写EEPROM的时候,要连续写入三个字节,第一个字节为从设备地址,第二个字节为要寻址的内存地址,第三个字节为要写入的数据;
同理,读的时候也要先写入从设备地址,然后就是寻址地址,然后开始读数据。
至于是读还是写,上面说了,用从设备地址的最后一位来表示,1表示读,0表示写。
页写:只发一次从设备地址和寻址首地址,之后直接写入多个数据,就会从首地址开始,按连续地址依次写入。
读写代码实现
注意,截至至2022年7月28日凌晨0点46分,此代码是有问题的,没法跑通,编译没问题,就是从设备怎么都没法应答成功,相对应的应该是数据没有写入到ROM中,不知道哪里出了问题,先放这,等查查资料再看看。
eeprommain.c
/************************************************************ *日期:2022年7月27日 *作者:星辰 *文件内容:eeprom功能程序入口 **************************************************************/ #include "uart.h" #include "eeprom.h" /************************************************************* * *函数入口 * **************************************************************/ void main(void) { uchar flag1 = 0, flag2 = 0; uchar uartArr[1] = {0}; I2cStart(); flag1 = I2cWrite(0xA0) && I2cWrite(0x22) && I2cWrite('A'); //写入数据 I2cStop(); if(flag1) { I2cStart(); flag2 = I2cWrite(0xA0) && I2cWrite(0x22); if(flag2) { I2cStart(); I2cWrite(0xA1); uartArr[0] = I2cRead(); //读出的数据 } I2cStop(); } UartInit(); SendSomeChar(uartArr, 1); }
eeprom.c
/************************************************************ *日期:2022年7月27日 *作者:星辰 *文件内容:eeprom读写 **************************************************************/ #include "eeprom.h" #include "somedelay.h" sbit SDA = P2^0; sbit SCL = P2^1; /************************************************************ * *开始标志 * **************************************************************/ void I2cStart() { SDA = 1; Delay10us(); SCL = 1; Delay10us(); SDA = 0; Delay10us(); SCL = 0; Delay10us(); } /************************************************************ * *结束标志 * **************************************************************/ void I2cStop() { SDA = 0; Delay10us(); SCL = 1; Delay10us(); SDA = 1; Delay10us(); } /************************************************************ * *按字节写入 * **************************************************************/ uchar I2cWrite(uchar dataToWrite) { uchar i = 0; SCL = 0; for(i; i < 8; i++) { SDA = dataToWrite >> 7; //从最高位开始传输 dataToWrite <<= 1; Delay10us(); SCL = 1; Delay10us(); SCL = 0; Delay10us(); } SDA = 1; //释放总线 Delay10us(); SCL = 1; Delay10us(); if(SDA == 0) { SCL = 0; Delay10us(); return 1; //返回1表示写入成功 } SCL = 0; Delay10us(); return 0; //返回0表示失败 } /************************************************************ * *按字节读取 *读之前也要先写入从设备地址和寻址地址 **************************************************************/ uchar I2cRead() { uchar charGeted = 0; uchar i = 0; SCL = 0; for(i; i < 8; i++) { SCL = 1; Delay10us(); charGeted |= SDA; if(i != 7) { charGeted <<= 1; } SCL = 0; Delay10us(); } //读的时候不用发送ACK吧?我只要看有没有读出数据就可以了呀。 return charGeted; }
uart.c
/************************************************************ *日期:2022年7月27日 *作者:星辰 *文件内容:串口调试 **************************************************************/ #include "uart.h" #include "somedelay.h" /************************************************************* * *初始化串口 * **************************************************************/ void UartInit() { SCON = 0x50; //设置使用模式1,波特率可变的8位UART,接收模式可用 TMOD = 0x20; //配置定时器1处于模式3,8位自动重载,用作波特率发生器 PCON = 0x80; //使用波特率加倍 TH1 = TL1 = 243; //设置波特率为4800Hz TR1 = 1; //打开定时器1 } /************************************************************* * *串口发送字符串 * **************************************************************/ void SendSomeChar(uchar charArr[], int len) { while(1) { int i = 0; for(i; i < len; i++) { SBUF = charArr[i]; //直接把数据扔给硬件即可,之后的由硬件完成 while(!TI); //等待上一个数据发完再发下一轮 TI = 0; //软件复位 Delay100us(); //必须要延时,速度太快,会出错 } SBUF = '\r'; //直接把数据扔给硬件即可,之后的由硬件完成 while(!TI); //等待上一个数据发完再发下一轮 TI = 0; //软件复位标志位 Delay1s(); //目标对象之间的延时 } }
其他的一些延时代码就不放了~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
代码的分层设计
经过SPI和I2C等协议的学习,可以看出,有些外设使用了这些协议来实现功能,那么,这里就涉及到了两个层次。
底层用的就是这些协议,涉及到这些协议的基本读写。
高层,也就是具体的外设,也有一些具体的协议,通过读写特定的数据,实现具体的功能。
所以,在编程时,通常底层时序单独一个文件,高层时序单独一个文件,然后在main函数中,使用这些封装好的高层函数,而不直接使用底层函数。