I2C通信的实践,学习笔记

本文是我自己实际工作中,对I2C通信协议的学习,实现过程的一个总结。它记录了我从对I2C一无所知到最终能够熟练实现I2C协议的一个过程。希望能够帮到不了解I2C通信协议却正好要使用I2C的一些小伙伴们。叙述的方式还是一点一点来,尽量简单,用到哪儿再详细说哪儿。

一提到通信我们自然会想到要有两个设备,在它们之间相互传递数据的过程就叫通信。那么它们怎么传递数据呢?硬件上怎么连接?什么时候开始发送数据?什么时候结束发送?先发送高位还是低位?等等这一系列问题都要事先约定好,设备双方才能进行通信,那么这一系列事先约定好的通信规则就叫做通信协议,这里我们介绍的通信协议叫I2C通信协议

 

我们要将协议中规定的这些规则告诉这两个设备,以便它们在互相通信时能按照这套规则进行运转,最终达到收发数据的目的。怎么告诉它们呢?设备听不懂中国话,英国话......,但是它们能听懂C语言(当然最终会由编译器转换为机器指令),我们使用C语言编写程序,程序的功能是让这两个设备按照I2C协议规定的规则进行收/发数据,那么这个过程就叫做在这两个设备上实现I2C通信协议。为了分辨两个设备,我们把一个叫做主机(Master),另一个叫做从机(Slaver),它们的行为不太一样,所以所需要的程序也不同,一个叫做主机端的I2C协议实现,另一个叫做从机端的I2C协议实现,也就是说我们要写两套程序。注意:在实际项目中一般我们只涉及到一端。

 

对于通信协议的实现,其实最终的表现形式就是提供几个用于通信的接口函数,以便主程序通过调用这些接口来实现在两个设备间通信的目的。在I2C中一般应提供如下最基本的通信接口函数:

void i2c_master_init(char addr); /*设置从机的地址,即,对方的地址*/

void i2c_master_read(char* buf, int len);

void i2c_master_write(char* buf, int len);

void i2c_slaver_init(char addr); /*初始化自己的地址*/

void i2c_slaver_read(char* buf, int len);

void i2c_slaver_write(char* buf, int len);

通过函数名称我们可以知道它们的作用。例如,如果Master想要向Slaver发送数据,就可以调用i2c_master_write(...)函数,如果想从Slaver接收数据,就调用i2c_master_read(...)。I2C的主机一般无需设置地址,随着对I2C的逐步理解,我们自然会明白为什么。

讲到这里,我们要开始学习一点协议了,因为我们要让设备按照协议中规定的规则运转,那么作为开发人员首先我们自己应该懂协议,才能使用C语言编写程序来控制设备。在I2C协议中,物理上在两个设备之间由两条线连接——数据线(SDA),时钟线(SCL)。GND和VCC那是硬件上的事,和协议无关,编程时当然也不涉及,所以我们不管。

那么这两条线有什么作用呢?SDA用来传递数据——高电平代表1,低电平代表0。SCL用来控制时序——按照一定频率荡起来。

我们再来聊一聊时钟线的作用,为什么在通信过程中时钟线一定要均匀稳定的荡起来才行?只用一根数据线行不行?如果你传输的是0101010...或101010...那还是可以的,但如果传输了一串0或一串1,那么接收方怎么知道你到底传输了多少位0或1呢。如果有了时钟就可以解决这个问题了,在一个时钟周期内采样得到的数据就是对方要传输的位数据,如果一共进行了4个周期的采样,那对方就是发送了4个bit,也就是说我们可以以时钟的脉冲为单位去对数据线采样,I2C协议规定在SCL为高电平时对SDA进行采样。在I2C协议中,主机可以控制(拉低或者释放)SDA和SCL这两条线,而从机只能控制SDA线。当主机发送数据时,从机会适时地将SDA拉低或释放(拉高)。主机向从机发送数据前要先发送一个起始信号,发送完数据后发送一个结束信号。

总结:SCL是单向的,由Master控制。而SDA是双向的,Master可以控制,Slaver也可以控制(前提是SCL为低电平时)。

下面我们来看看start和stop的波形,找找感觉。

                       起始信号波形

如何编程实现这个start呢,我们来看看(虚线框住的部分)

void i2c_master_start()
{
	i2c_delay(cycle);
	i2c_delay(cycle);
	sda_clr_bit();
	i2c_delay(half_cycle);

                           停止信号波形

 

停止信号的代码实现

static void i2c_master_stop()
{
	scl_clr_bit();
	sda_set_out();
	nop5();
	sda_clr_bit();
	i2c_delay(half_cycle);
	scl_set_bit();
	i2c_delay(half_cycle);
	sda_set_bit();
	i2c_delay(half_cycle);
}

总结:I2C通信协议规范中规定,起始信号和停止信号必须由主机发出。

发送1个字节的代码实现

static byte_t i2c_master_sendByte(byte_t ch)
{
	byte_t i;
	byte_t ret;
	DISABLE_ALL_ISRS();
	scl_clr_bit();
	sda_set_out();
	for (i = 0; i < 8; i++)
	{
		i2c_delay(I2C_DELAY4);
		if (ch & 0x80)
		{
			sda_set_bit();
		}
		else
		{
			sda_clr_bit();
		}
		i2c_delay(I2C_DELAY4);
		scl_set_bit();
		i2c_delay(I2C_DELAY2);
		scl_clr_bit();

		ch <<= 1;
	}

	sda_set_bit();
	sda_set_in();
	i2c_delay(I2C_DELAY2);
	scl_set_bit();
	i2c_delay(I2C_DELAY2);

	if (sda_get_bit() == 0)
	{
		ret = 1; //ack
	}
	else
	{
		ret = 0; //nak
	}
	scl_clr_bit();
	i2c_delay(I2C_DELAY2);

	ENABLE_ALL_ISRS();
	return ret;
}

根据上面的代码,画出对应的波形

                         发送1个字节,注意这里有9个脉冲

接收1个字节的代码实现

static byte_t i2c_master_recieveByte(byte_t isAck)
{
	byte_t rdata = 0, i;

	DISABLE_ALL_ISRS();
	scl_clr_bit();
	sda_set_in();

	for (i = 0; i < 8; i++)
	{
		i2c_delay(I2C_DELAY2);
		scl_set_bit();
		i2c_delay(I2C_DELAY2);

		rdata <<= 1;
		if (sda_get_bit())
		{
			rdata |= 1;
		}
		nop5();
		scl_clr_bit();
	}

	sda_set_out();
	nop5();
	if (1 == isAck)
	{
		sda_clr_bit();
	}
	else
	{
		sda_set_bit();
	}
	i2c_delay(I2C_DELAY2);
	scl_set_bit();
	i2c_delay(I2C_DELAY2);

	ENABLE_ALL_ISRS();
	return rdata;
}

根据上面代码,画出对应的波形

                        接收1个字节,注意这里有9个脉冲


确切的时序应该是这样的:

当主机要发送一个start时,Master会将SDA拉低,这就可以了,因为此时的SCL一定是高电平,这就是Master发送了一个start。好了,一个start就这样发出去了。而Slaver也会发现这个start信号的发生,Slaver便会准备好接收接下来的数据了。紧接着,Master要发送一个Byte的数据了,一位一位的发出这8个bits(I2C协议规定,在传输一个字节时,先传输高位)。这时Master会先将SCL拉低,然后在SCL为低的状态下将一个bit放到SDA上(比如要发送一个 0,Master就会通过拉低SDA来放好这个0),然后Master会把SCL拉高(释放),此时Slaver会立刻检测到SCL的变化,由此聪明的Slaver便知道Master已经将要发送的那个bit准备好了,Slaver便会在这个SCL的高电平期间尽快(Maser不会等你很久的哦)去读取一下SDA,嗯读到了一个0,Slaver就把这个0放到自己的移位寄存器中待后续处理。Master会在一个设定好的时间后把SCL再次拉低,然后在SCL为低电平期间把下一个bit放到SDA上,然后再把SCL拉高,然后Slaver在SCL的高电平期间再去读SDA......如此反复8次,一个Byte的传输便告结束。当这8个bit发完后,SCL是处于低电平的(被Master拉低的),SDA是处于高电平的(Master已经释放了SDA)。

 

上面说了,当一个字节发送完毕后,Master会释放SDA(拉高)并拉低SCL。为什么要这样呢?这里就引出了接收应答(ACK)的概念。此时Slaver如果打算发出一个ACK的话,它必须在这个SCL被Master拉低的短暂时间内去主动将SDA拉低并保持住 (此前我们说过,SDA此时已经被Master释放,所以Slaver才有机会去拉低这个SDA)。Master会在一个确定的时间后再次将SCL拉高,并在拉高的期间去读取SDA线的状态,如果读到低电平,则认为收到了来自Slaver的响应(ACK),否则认为Slaver没有响应(NACK)刚才发送的那一个Byte。这个过程就是我们说的I2C通讯中的第9个时钟周期。当Master读完这个ACK / NACK 后,会再次将SCL拉低,用以通知Slaver:第9个时钟周期已经结束,你现在可以释放SDA了。而此时Master也可以向SDA上准备下一个Byte的第一个bit。继而重复上述过程。。。。。或者,Master也许想在接下来发送一个stop过去,那么Master会在这个SCL为低的时间内将SDA拉低,而后再将SCL拉高,在SCL为高的期间再将SDA释放(拉高) 。这样,一个stop位就产生了。你会发现此后的SDA和SCL都是高,这就是所谓的总线空闲了!

 

在实现I2C协议时要始终牢记:SCL为高电平期间SDA要保持稳定,这时可以对SDA进行采样,在SCL为低电平期间可以对SDA进行设置。(开始信号和结束信号例外)!

另外,需要注意的是,并非每传输8位数据之后,都会有ACK信号,有以下3种例外:

(1)当从机不能响应从机地址时(例如它正忙于其他事而无法相应I2C总线的操作,或者这个地址没有对应的从机),在第9个SCL周期内SDA线没有被拉低,即没有ACK信号。这时,主机发出一个P信号终止传输或者重新发出一个S信号开始新的传输。一般都是定期重复发送起【始信号+地址字节】,一直等待从机的响应。

(2)如果从机接收器在传输过程中不能接收更多的数据时,它也不会发出ACK信号。这样,主机就可以意识到这点,从而发出一个P信号终止传输或者发出一个S信号开始新的传输。

(3)主机接收器在接收到最后一个字节后,也不会发出ACK信号。于是,从机发送器释放SDA线,以允许主机发出P信号结束传输。(可以看看下面i2c_recieveData(...)中对最后一个字节的处理)

将上面i2c_master_sendByte(...)和i2c_master_receiveByte(...)封装一下,就得到了最终提供给主程序使用的接口。在这一层需要注意对起始信号,停止信号,应答的处理。

 

第一层封装

static void i2c_sendData(byte_t* buffer, word_t len)
{
	byte_t i;
	for (i = 0; i < len; i++)
	{
		if (i2c_sendByte(*(buffer + i)) != 1)
		{
			break; //no ack
		}
	}
	i2c_stop();
}

static void i2c_recieveData(byte_t* buffer, word_t len)
{
	byte_t i;
	for (i = 0; i < len; i++)
	{
		if (i == (len - 1))
		{
			buffer[i] = i2c_recieveByte(0);
		}
		else
		{
			buffer[i] = i2c_recieveByte(1);
		}
	}
	i2c_stop();
}

最后的封装(加上对【起始信号+地址字节】的处理)

void i2c_master_write(byte_t* buffer, word_t len)
{
REDO:
	// If the ACK of the slave is not received, the address information is always sent here.
	i2c_start();
	if (!i2c_sendByte(WRITR_ADDR))
	{
		i2c_delay(I2C_DELAY);
		sda_set_out();
		i2c_delay(I2C_DELAY2);
		sda_set_bit();
		scl_set_bit();
		wait(1000); //1000 =~1ms
		goto REDO;
	}

	i2c_sendData(buffer, len);
}

void i2c_master_read(byte_t* buffer, word_t len)
{
REDO:
	// If the ACK of the slave is not received, the address information is always sent here.
	i2c_start();
	if (!i2c_sendByte(READ_ADDR))
	{
		i2c_delay(I2C_DELAY);
		sda_set_out();
		i2c_delay(I2C_DELAY2);
		sda_set_bit();
		scl_set_bit();
		wait(1000); //1000 =~1ms
		goto REDO;
	}

	i2c_recieveData(buffer, len);
}

在i2c_master_write/read(...)的实现中,在Master发送完start后,分别向I2C总线上发送了WRITE_ADDR和READ_ADDR,这两个当然是常量定义了,那么为什么在通信的开始要先发送这个呢?其实,I2C协议中规定,在I2C总线上可以有多个设备(一个Master多个Slaver,每个设备都可以作为Master或Slaver),那么Master在向Slaver发送数据时,首先要将Slaver的地址放到I2C总线上,然后各个Slaver会从总线上读取这个地址,当Slaver读取到的地址和自己匹配时,这个Slaver就要准备从I2C总线上开始接收数据了。

地址用1个字节来表示,高7位为地址,最低位用来表示读写。例如:某个从机的地址为0x28,那么Master要向这个Slaver发送数据时,首先要发送地址信息:0x50,如果Master要从这个Slaver读数据时,首先要发送地址信息:0x51。这里要好好体会0x28怎么就变成0x50和0x51了呢!

根据上面的实际经验,我们再来学习一点协议。这里我们从Master的角度去介绍。一般有三种情况。

第一种情况:Master向Slaver写数据

第二种情况:Master从Slaver读数据

第三种情况:
1)Master向Slaver写数据,然后重启起始条件,紧接着从Slaver读数据;
2)Master从Slaver读数据,然后重启起始条件,紧接着向Slaver写数据。

 

总结一下I2C主机端通信的流程

一、Master向Slaver写数据

1.首先Master要发送【起始信号+地址字节(要向哪个Slaver写数据)】。如果发送完地址字节后(地址字节最低位肯定为0),Slaver并没有返ACK,那么这个过程要重复继续。

2.发送数据。如果在发送起始信号和地址字节后,收到了Slaver的ACK,那么就可以向Slaver发送数据了。

 

二、Master从Slaver读数据

1.首先Master要发送【起始信号+地址字节(要从哪个Slaver读数据)】。如果发送完地址字节后(地址字节最低位肯定为1),Slaver并没有返ACK,那么这个过程要重复继续。

2.接收数据。如果在发送起始信号和地址字节后,收到了Slaver的ACK,那么就可以从Slaver读取数据了。

 

总结一下I2C从极端通信的流程

一、Slaver向Master写数据

1.不断从I2C总线上捕获【起始信号+地址字节】。即,一旦捕获到起始信号,立即收一个字节,这个字节就是地址字节(最低位肯定为1),看看这个地址是不是自己的地址,如果不是,那么这个过程要重复继续。

2.如果从I2C总线上收到的地址字节正是自己的地址,就说明Master想从自己这里读数据了,就可以开始向I2C总线上写数据了。

二、Slaver从Master读数据

1.不断从I2C总线上捕获起【始信号+地址字节】。即,一旦捕获到起始信号,立即收一个字节,这个字节就是地址字节,看看这个地址是不是自己(最低位肯定为0),如果不是,那么这个过程要重复继续。

2.如果从I2C总线上收到的地址字节正是自己的地址,就说明Master想向自己这里写数据了,就可以开始从I2C总线上读数据了。

I2C器件地址

我们再来看看当我们为一个器件实现I2C通信时,这个器件的地址是如何定义的。应该说可以随意定义,只要通信双方能对应上即可。就像我们在微信群里群聊似的,当我@张三时,我就是在对他说话,其实张三也可以改成其他名字,前提是我们俩之间要事先约定好。当然,也有一些器件的地址是固定的,例如AT24C64这个EEPROM芯片,它的地址固定为0xA0(根据接线方式的不同还可能为其他的几个值)。
 

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值