GPIO模拟I2C通信协议(一)

版权声明:本文为博主原创文章,转载请附上博文链接! https://blog.csdn.net/ctyqy2015301200079/article/details/83830326


概要: 从本篇开始,我将用2篇博客的篇幅对I2C驱动的开发作总结。本节将首先介绍I2C协议的基本时序,然后给出用GPIO模拟实现I2C功能的C代码。最后介绍驱动开发的一些思路。
关键字: IO模拟; I2C协议; 驱动开发

1 I2C协议简介

   I2C总线是由Philips公司在上世纪80年代开发的一种简单、双向二线制同步串行总线。它只需要两根线即可在连接于总线上的器件之间传送信息。(本段来源于百度

   I2C既是一种总线,也是一种通信协议。总线和通信协议之间的关系类似于硬件和基于此硬件的软件,同一种总线上可以跑多种协议,如在RS485总线上可以跑莫迪康的MODBUS,松下的MEWTOCOL,西门子的profibus/DP等协议;同样地,同一种协议也可以跑在不同的总线上,如上述协议还可以跑在以太网上。一言以蔽之,总线涉及的是物理层的硬件,而协议可以认为是在物理层上传递信息的约定或规则。

   或者也可以这么说。在嵌入式开发中,通信协议可分为两层:物理层和协议层。物理层是数据在物理媒介传输的保障;协议层主要规定通信逻辑,如同一收发双方的数据打包、解包标准。打个比方,物理层相当于现实中的公路,而协议层则是交通规则,汽车可以在路上行驶,但是需要交通规则对行驶规则进行约束,不然将出现危险,也就是数据传输紊乱、丢包。(本段来源于博客

1.1 物理层

   I2C总线协议只需要2根信号线即可完成数据的传输,这两根线分别是时钟线SCL和信号线SDA。I2C线上有且只有1个主设备Master和若干个从设备Slave,区别Master和Slave的标准是SCL,即谁是SCL的提供者,谁就是Master,而与SDA无关。这点尤其需要注意,发送SDA不能作为区别Master和Slave的标准。I2C通信系统连接示意如图1所示:

图1 I2C通信系统连接示意

 
   关于I2C总线再作以下说明:

   1-两条总线SDA和SCL都必须接上拉电阻,这是为了确保两条总线在空闲时都是高电平,上拉电阻的经典取值是10kΩ;
   2-I2C总线上可以挂载多个主机和多个从机,但是同一时间只允许1个主机和1个从机进行通信;
   3-总线上每一个设备都有一个独立的地址,通过该地址实现通信;
   4-总线上的设备是“线与”的关系,即任一设备的管脚输出低电平都可以将该管脚所在总线的电平拉低,线与关系是时钟同步和总线仲裁的硬件基础;
   5-I2C传输速度有标准速度模式(SS Mode)、快速模式(FS Mode)和高速模式(HS Mode)三种,数据传输速率分别为100kbps、400kbps和3.4Mbps。

1.2 协议层

   协议层规约了通讯的起始停止信号、数据有效性、响应、总线仲裁、时钟同步、地址广播等内容。

1.2.1 总线空闲与信号起始终止

   I2C协议规定SDA和SCL都为高电平时总线空闲(not busy)。总线空闲如图2的(A)部所示。

   I2C协议规定SCL保持高电平、SDA由高变低为起始信号(start),所有命令和数据的传输必须以起始信号为首。起始信号如图2的(B)部所示。

   I2C协议规定SCL保持高电平、SDA由低变高为终止信号(stop)。所有命令和数据的传输必须以终止信号为尾。起始信号如图2的©部所示。

1.2.2 数据有效

   I2C协议规定在总线上出现起始信号start后,若SCL在高电平期间SDA保持电平不变,则SDA的状态表示有效数据(data valid)。在传输数据时SDA的改变必须只能发生在SCL为低电平期间,每一bit数据有1个时钟脉冲时长。数据有效如图2的(D)部所示。

图2 I2C串行总线上的数据传输时序

1.2.3 应答和非应答

   I2C协议规定每个被寻址设备在接收1字节数据后都必须向发送字节的设备发送应答(ACK)信号,确认的器件必须在应答时钟脉冲期间下拉SDA线,使得SDA线在应答相关时钟脉冲SCL为高电平期间稳定为低电平。

   I2C协议规定与ACK信号相反的信号为非应答(not ACK)信号。在主器件从从器件中读取数据时,主器件必须在读取的最后1字节数据后在SDA总线上产生not ACK信号以示意从器件停止发送数据。not ACK信号是在SCL为高电平期间保持SDA也为高电平。

1.2.4 地址广播

   地址广播是I2C协议规定的寻址方式。它是指主设备在产生start信号后,各个从设备开始关注总线SDA信号,此时主设备在总线上生成需接受/发送数据的从设备的地址(Address),相当于向总线上所有从设备广播了这一地址。每个从设备将总线上的地址与自己的地址相对比,不一致的退出接收,一致的继续接收,直到8bit地址数据广播完毕,仍然留下的那一个从设备就是主设备的寻址目标。

1.2.5 总线仲裁

   总线仲裁解决的是多个主设备竞争使用同一总线的问题。下面举例说明:

   假设主控器1要发送的数据DATA1为“101 ……”;主控器2要发送的数据DATA2为“1001 ……”总线被启动后两个主控器在每发送一个数据位时都要对自己的输出电平进行检测,只要检测的电平与自己发出的电平一致,他们就会继续占用总线。在这种情况下总线还是得不到仲裁。当主控器1发送第3位数据“1”时(主控器2发送“0” ),由于“线与”的结果SDA上的电平为“0”,这样当主控器1检测自己的输出电平时,就会测到一个与自身不相符的“0”电平。这时主控器1只好放弃对总线的控制权;因此主控器2就成为总线的唯一主宰者。(实例来自博客

   从中可以得出:参与仲裁的所有主控器都不会丢失数据;参与仲裁的所有主控器没有固定的优先级别,而是遵循低电平优先的原则。

1.2.6 时钟同步

   时钟同步是用来解决中控器和被控器的数据传输速率不相同的问题。

   被控器可以通过将SCL主动拉低并延长其低电平时间的方法来通知主控器,当主控器在准备下一次传送时发现SCL为低电平,就会等待,直至被控器完成操作并释放SCL线的控制控制权。这样,主控器实际上受到被控器的时钟同步控制。由此可见,SCL线上的低电平是由时钟低电平最长的器件决定,高电平的时间由高电平时间最短的器件决定。

   需要说明的是,不管是总线仲裁还是时钟同步,它们得以实现的基础是SDA总线的“线与”性质,而这是由I2C总线独特的IO结构决定的。另外,总线仲裁和时钟同步之间并不存在特定的先后关系,它们往往同时发生。

2 I2C协议的C代码实现

   下面将用C语言实现第1章所描述的I2C总线协议的各个动作,并将这些分散的动作整合起来实现字节的读写操作。

   首先需要在单片机上定义2个IO口以连接两根总线SDA和SCL,这个可随意取,只要是IO口都行。我的取值如下:

#define SDA IO_CONFIG_PB0
#define SCL IO_CONFIG_PB1

2.1 单个动作

2.1.1 初始化

   初始化的效果是SDA和SCL总线上全部呈现高电平,由于两根线都已连接上拉电阻,且端口的IO是开漏极,因此只需要将它们的方向都设为input即可。

void i2c_init(void)
{
	io_func_config(SDA, IO_FUNC_GPIO);	// choose IO_CONFIG_PB0 as GPIO
	io_func_config(SCL, IO_FUNC_GPIO);	// choose IO_CONFIG_PB1 as GPIO
	io_input(SDA);		// SDA input
	io_input(SCL);		// SCL input
	delay_us(20);
}

2.1.2 起始信号

   用GPIO模拟起始信号,SCL保持高电平、SDA由高变低。

void i2c_start(void)
{
	io_output(SDA);		// SDA output
	io_output(SCL);		// SCL output
	io_set_high(SDA);	// SDA=1
	io_set_high(SCL);	// SCL=1
	delay_us(5);
	io_set_low(SDA);	// SDA=0
	delay_us(5);
}

2.1.3 终止信号

   用GPIO模拟起始信号,SCL保持高电平、SDA由低变高。

void i2c_stop(void)
{
	io_output(SDA);		// set SDA as input
	//io_output(SCL);		// set SCL as input
	
	if (io_get(SCL) == 1)
		io_set_low(SCL);	// SDA=0
	if (io_get(SDA) == 1)
		io_set_low(SDA);	// SDA=0
	delay_us(5);
	io_set_high(SCL);	// SCL=1
	delay_us(5);
	io_set_high(SDA);	// SDA=1
	delay_us(5);
	// release SDA and SCL
	io_input(SDA);		// set SDA as input
	io_input(SCL);		// set SCL as input
}

2.1.4 主控器读取ACK

   单片机读取ACK对应的IO口处的电平。

uint8_t i2c_read_ack(void)
{
	uint8_t ack;
	io_input(SDA);		// SDA input
	io_set_high(SCL);	// SCL=1
	ack = io_get(SDA);
	delay_us(5);
	io_set_low(SCL);	// SCL=0
	delay_us(5);
	return ack;
}

2.1.5 主控器发送ACK

   单片机向ACK对应的IO口发送低电平。

void i2c_send_ack(void)
{
	io_output(SDA);
	io_set_low(SCL);	// SCL=0
	io_set_low(SDA);	// SDA=0
	delay_us(5);
	io_set_high(SCL);	// SCL=1
	delay_us(5);
	// TAKE CAREFULLY!!!These two orders below must be included
	// to pull down the SCL for the following opreations.
	io_set_low(SCL);	// SCL=0
	delay_us(5);
}

2.1.6 主控器发送not ACK

   单片机向ACK对应的IO口发送高电平。

void i2c_send_nack(void)
{
	io_output(SDA);
	io_set_low(SCL);	// SCL=0
	io_set_high(SDA);	// SDA=1
	delay_us(5);
	io_set_high(SCL);	// SCL=1
	delay_us(5);
	io_set_low(SCL);	// SCL=0
	delay_us(5);
}

2.1.7 主控器检查是否接收到ACK

   主控器在发完第1个地址字节后,按规定被控期需要向主控器回复一个ACK信号,主控器如果能在总线上检测到这个ACK,就继续向被控器传送数据,否则视为本次数据传送失败。

uint8_t i2c_ack_check(uint8_t ctrl_byte)
{
	i2c_start();
	i2c_write_single_byte(ctrl_byte);
	if(i2c_read_ack() == 0)
	{
		//sl_printf("i2c_read_ack() == 0.\n");
		// time delay here is not necessary, just to make waveforms more readable
		delay_us(30);
		//i2c_stop();
		io_input(SDA);		// set SDA as input
		io_input(SCL);		// set SCL as input
		return 0;
	}
	else
	{
		//sl_printf("i2c_read_ack() == 1.\n");
		// time delay here is to save computing resource
		delay_us(100);
		//io_input(SDA);		// set SDA as input
		//io_input(SCL);		// set SCL as input
		return 1;
	}
}

   以上就是所谓的“单个动作”,是形成I2C功能的最基本的单元。上述动作经过各种组合即可实现I2C的基本操作:单字节的读写。

2.2 组合动作:字节读写

   如2.1节所述,本节介绍2种最基本的“组合动作”——单字节的读写

2.2.1 单字节读

   单字节读的代码如下:

uint8_t i2c_read_single_byte(void)
{
	uint8_t i=8;
	uint8_t i2c_buff = 0x0;
	// 每次read,最开始总是高电平,即使MSB is low 也要先高再低(在SCL=0期间有一个小凸起)
	// Is that because of setting SDA as input, making it HIGH at the very beginning? Maybe so.
	delay_us(5);
	io_input(SDA);				// SDA input
	delay_us(5);
	while (i--)
	{
		i2c_buff = i2c_buff<<1;
		io_set_high(SCL);		// SCL=1
		//
		if(io_get(SDA)==1)
			i2c_buff |= 0x01;							// Write 1 to LSB of i2c_buff
		else
			i2c_buff &= 0xFE;							// Write 0 to LSB of i2c_buff
		delay_us(5);
		io_set_low(SCL);		// SCL=0
		delay_us(5);
		//i2c_buff = i2c_buff<<1;						// move to the next MSB(from MSB to LEB)
	}
	//sl_printf("i2cbuf=%d\n", i2c_buff);
	return i2c_buff;
}

   它的逻辑是:

   初始化—主控器发送起始信号—主控器逐bit读取SDA线上信号。

2.2.2 单字节写

   单字节写的代码如下:

void i2c_write_single_byte(uint8_t i2c_buff)
{
	uint8_t i=8;
	io_output(SDA);		// SDA output
	io_output(SCL);		// SCL output
	while (i--)
	{
		io_set_low(SCL);			// SCL=0
		delay_us(5);
		if(i2c_buff & 0x80)					// MSB(i2c_buff)==1
			io_set_high(SDA);		// SDA=1
		else
			io_set_low(SDA);		// SDA=0
		io_set_high(SCL);			// SCL=1
		delay_us(5);
		i2c_buff = i2c_buff<<1;							// move to the next MSB(from MSB to LEB)
	}
	// After transfer, release the SCL line
	io_set_low(SCL);				// SCL=0
	delay_us(5);
}

   它的逻辑是:

   初始化—主控器发送起始信号—主控器逐bit往SDA线上写数据。

3 小结

   本次内容只介绍了GPIO模拟I2C协议的最底层,即在总线上完成信号读写的时序。有以下几点需要注意:

   1-代码是从主控器的角度写的;
   2-这里没有ACK是因为这还远不是主从设备之间的数据传输;
   3-总线上单个字节的读写不等于主从设备之间单个字节的传输,主从设备之间单个字节的传输比这复杂,将在下节讲到。

   从驱动开发的角度来看,今天完成的正是驱动开发的最底层:完全关注于协议本身的时序逻辑而不涉及具体器件。比如主单片机通过I2C连接从设备诸如Flash、E2PROM、ADC、DAC、SRAM和从单片机等等,这些属于更高层的API,下次博客会讲到。

   最后需要提到的一点是所谓“分层的思想”,在整个实习过程中我感到这在驱动开发工作中是一个非常重要的思想。分层即封装,将一个完整的驱动设定为若干层,每一层只关注本层的内容,实现本层计划实现的功能,并给更高层的驱动代码提供API。这样一个复杂的驱动开发工作就会变得简单,并且也更利于多人协作。

4 后记

   博客粘贴的代码中提到的函数诸如io_set_low()、io_set_high()、io_output()、io_input()等都是对IO的基本操作,基本是顾名思义的。这些操作在每款开发板中都会有提供,或是位操作、或是寄存器操作、或是函数操作等等。有趣的是,这些提供的IO操作函数本身也属于API。

   下篇博客将涉及E2PROM作为I2C总线的被控器并实现E2PROM与主控器(单片机)之间的数据传输。

   转载时务必注明来源及作者。尊重知识产权从我做起。

猜你喜欢

转载自blog.csdn.net/ctyqy2015301200079/article/details/83830326