I2C通信协议详细讲解

I2C协议讲解

实验准备

一块STM32最小系统板,BH1750模块,一块PCB转换板模块、串口TTL转USB模块
图片:
《I2C通信协议详细讲解》
《I2C通信协议详细讲解》

PCB转换板原理图及PCB电路图

《I2C通信协议详细讲解》
《I2C通信协议详细讲解》

讲解流程

我们为什么要学习I2C通信

Stm32的最常用的板间通信有很多,有I2C、SPI、CAN;I2C通信协议是我们stm32板间通信比较常用的、也是比较简单的。

I2C协议简介:

I2C 通讯协议(Inter-Integrated Circuit)是由 Phiilps 公司开发的,由于它引脚少,硬件实现简单,可扩展性强,不需要 USART、 CAN 等通讯协议的外部收发设备,所以被广泛使用。

I2C物理层特点

(1)它是一个支持设备的总线。“总线”指多个设备共用的信号线。在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。
(2)一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA) ,一条串行时钟线(SCL)。数据线即用来表示数据,时钟线用于数据收发同步。
(3)每一个连接总线的设备都有一个独立的地址,主机可以通过这个地址进行选择连接总线的设备与之通信。
(4)总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。
(5)多个主机同时使用总线时,为了防止多个设备发送数据冲突,会利用仲裁方式决定由哪个设备占用总线。
(6) 具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式。
(7) 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制 。
备注:
仲裁:SDA线的仲裁也是建立在总线具有线“与”逻辑功能(线与逻辑,即两个以上的输出端直接互连就可以实现“AND”的逻辑功能。两个一出一,一个一出零、没有一出零)的原理上的。节点在发送1位数据后,比较总线上所呈现的数据与自己发送的是否一致。是,继续发送;否则,进行比较,输出低电平进行发送,输出高电平退出。SDA线的仲裁可以保证I2C总线系统在多个主节点同时企图控制总线时通信正常进行并且数据不丢失。总线系统通过仲裁只允许一个主节点可以继续占据总线。

《I2C通信协议详细讲解》

I2C协议层

《I2C通信协议详细讲解》

S:由主机的 I2C 接口产生的传输起始信号(S),这时连接到 I2C 总线上的所有从机都会接收到这个信号。
SLAVE ADDRESS:从机地址信号。 在 I2C 总线上,每个设备的地址都是唯一的, 当主机广播的地址与某个设备地址相同时,这个设备就被选中了,没被选中的设备将会忽略之后的数据信号。从机地址一般是 7 位或 10 位。
R/W:是传输方向的选择位,该位为 0 时,表示后面的数据传输方向是由主机传输至从机,即主机向从机写数据。该位为 1 时,则相反,即主机由从机读数据。
A | A/:一个应答(ACK)或非应答(NACK)信号。
P:停止传输信。
图中背景有填充的矩形表示 数据由主机传输至从机

写数据

讲得简单一点,即主机发送信息、从机阅读信息。配置为方向为“写数据”方向,接受的数据包大小为8位(一个byte),主机每发送完一个字节数据,都要等待从机的应答信号(ACK),重复这个过程,数据的多少可以理论是可以随便的。当数据传输结束时,主机向从机发送一个停止传输信号§,表示不再传输数据。

读数据

讲得简单一点,即主机阅读信息、从机发送信息。若配置的方向传输位为“读数据”方向,发送的数据包大小也为 8 位,从机每发送完一个数据,都会等待主机的应答信号(ACK),重复这个过程,数据的多少可以理论也是可以随便的。当主机希望停止接收数据时,就向从机返回一个非应答信号(NACK),则从机自动停止数据传输。

读和写数据

除了基本的读写,I2C 通讯更常用的是复合格式,该传输过程有两次起始信号(S)。一般在第一次传输中,主机通过 SLAVE_ADDRESS 寻找到从设备后,发送一段“数据”,这段数据通常用于表示从设备内部的寄存器或存储器地址(注意区分它与 SLAVE_ADDRESS 的区别);在第二次的传输中,对该地址的内容进行读或写。也就是说,第一次通讯是告诉从机读写地址,第二次则是读写的实际内容。

通讯的起始和停止信号

《I2C通信协议详细讲解》

当 SCL 线是高电平时 SDA 线从高电平向低电平切换,这个情况表示通讯的起始。当 SCL 是高电平时 SDA线由低电平向高电平切换,表示通讯的停止。起始和停止信号一般由主机产生。
《I2C通信协议详细讲解》

I2C 使用 SDA 信号线来传输数据,使用 SCL 信号线进行数据同步。见图 24-6。 SDA数据线在 SCL 的每个时钟周期传输一位数据。传输时, SCL 为高电平的时候 SDA 表示的数据有效,即此时的 SDA 为高电平时表示数据“1”,为低电平时表示数据“0”。当 SCL为低电平时, SDA 的数据无效,一般在这个时候 SDA 进行电平切换,为下一次表示数据做好准备。
每次数据传输都以字节为单位,每次传输的字节数不受限制。

地址及数据方向

I2C 总线上的每个设备都有自己的独立地址,主机发起通讯时,通过 SDA 信号线发送设备地址(SLAVE_ADDRESS)来查找从机。 I2C 协议规定设备地址可以是 7 位或 10 位,实际中 7 位的地址应用比较广泛。紧跟设备地址的一个数据位用来表示数据传输方向,它是数据方向位(R/),第 8 位或第 11 位。数据方向位为“1”时表示主机由从机读数据,该位为“0”时表示主机向从机写数据。
《I2C通信协议详细讲解》

再提一嘴,读数据方向时,主机会释放对 SDA 信号线的控制,由从机控制 SDA 信号线,主机接收信号,写数据方向时, SDA 由主机控制,从机接收信号。切记,不要混淆。

时钟控制逻辑

SCL 线的时钟信号,由 I2C 接口根据时钟控制寄存器(CCR)控制,控制的参数主要为时
钟频率。配置 I2C 的 CCR 寄存器可修改通讯速率相关的参数:

  • 可选择 I2C 通讯的“标准/快速”模式,这两个模式分别 I2C 对应 100/400Kbit/s 的
    通讯速率。

  • 在快速模式下可选择 SCL 时钟的占空比,可选 Tlow/Thigh=2 或 Tlow/Thigh=16/9
    模式,我们知道 I2C 协议在 SCL 高电平时对 SDA 信号采样, SCL 低电平时 SDA
    准备下一个数据,修改 SCL 的高低电平比会影响数据采样,但其实这两个模式的
    比例差别并不大,若不是要求非常严格,这里随便选就可以了。

  • CCR 寄存器中还有一个 12 位的配置因子 CCR,它与 I2C 外设的输入时钟源共同
    作用,产生 SCL 时钟, STM32 的 I2C 外设都挂载在 APB1 总线上,使用 APB1 的
    时钟源 PCLK1, SCL 信号线的输出时钟公式如下:

     	标准模式:
     	Thigh=CCR*TPCKL1
     	Tlow=CCR*TPCLK1
     	 
     	快速模式中 Tlow/Thigh=2 时:
     	Thigh = CCR*TPCKL1 
     	Tlow = 2*CCR*TPCKL1
     	
     	快速模式中 Tlow/Thigh=16/9 时:
     	Thigh = 9*CCR*TPCKL1 
     	Tlow = 16*CCR*TPCKL1
    

例如,我们的 PCLK1=36MHz,想要配置 400Kbit/s 的速率,计算方式如下:
PCLK 时钟周期: TPCLK1 = 1/36000000
目标 SCL 时钟周期: TSCL = 1/400000
SCL 时钟周期内的高电平时间: THIGH = TSCL/3
SCL 时钟周期内的低电平时间: TLOW = 2*TSCL/3
计算 CCR 的值: C C R = T H I G H T P C L K 1 = T S C L 3 1 36000000 = 1 400000 3 1 36000000 = 36000000 1200000 = 30 CCR ={THIGH\over TPCLK1} ={ {TSCL\over 3}\over {1\over 36000000} }={ { {1\over 400000}\over 3}\over {1\over 36000000} }={36000000\over 1200000}=30 CCR=TPCLK1THIGH=3600000013TSCL=36000000134000001=120000036000000=30

注:计算结果得出 CCR 为 30,向该寄存器位写入此值则可以控制 IIC 的通讯速率为400KHz,其实即使配置出来的 SCL 时钟不完全等于标准的 400KHz,IIC 通讯的正确性也不会受到影响,因为所有数据通讯都是由 SCL 协调的,只要它的时钟频率不远高于标准即可。

通讯过程

使用 I2C 外设通讯时,在通讯的不同阶段它会对“状态寄存器(SR1 及 SR2)”的不同数
据位写入参数,我们通过读取这些寄存器标志来了解通讯状态。

主发射器

《I2C通信协议详细讲解》

主发送器发送流程及事件说明如下:

  1. 控制产生起始信号(S),当发生起始信号后,它产生事件“EV5”,并会对 SR1 寄
    存器的“SB”位置 1,表示起始信号已经发送;
  2. 紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件“ EV6”及
    “EV8”,这时 SR1 寄存器的“ADDR”位及“TXE”位被置 1, ADDR 为 1 表
    示地址已经发送, TXE 为 1 表示数据寄存器为空;
  3. 以上步骤正常执行并对 ADDR 位清零后,我们往 I2C 的“数据寄存器 DR”写入
    要发送的数据,这时 TXE 位会被重置 0,表示数据寄存器非空, I2C 外设通过
    SDA 信号线一位位把数据发送出去后,又会产生“EV8”事件,即 TXE 位被置 1,
    重复这个过程,就可以发送多个字节数据了;
  4. 当我们发送数据完成后,控制 I2C 设备产生一个停止信号§,这个时候会产生
    EV8_2 事件, SR1 的 TXE 位及 BTF 位都被置 1,表示通讯结束。

假如我们使能了 I2C 中断,以上所有事件产生时,都会产生 I2C 中断信号,进入同一
个中断服务函数,到 I2C 中断服务程序后,再通过检查寄存器位来判断是哪一个事件。

主接收器

《I2C通信协议详细讲解》

主接收器接收流程及事件说明如下:

  1. 同主发送流程,起始信号(S)是由主机端产生的,控制发生起始信号后,它产生事
    件“EV5”,并会对 SR1 寄存器的“SB”位置 1,表示起始信号已经发送;
  2. 紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件“ EV6”这时
    SR1 寄存器的“ADDR”位被置 1,表示地址已经发送。
  3. 从机端接收到地址后,开始向主机端发送数据。当主机接收到这些数据后,会产
    生“EV7”事件, SR1 寄存器的 RXNE 被置 1,表示接收数据寄存器非空,我们
    读取该寄存器后,可对数据寄存器清空,以便接收下一次数据。此时我们可以控
    制 I2C 发送应答信号(ACK)或非应答信号(NACK),若应答,则重复以上步骤接收
    数据,若非应答,则停止传输;
  4. 发送非应答信号后,产生停止信号§,结束传输。

在发送和接收过程中,有的事件不只是标志了我们上面提到的状态位,还可能同时标
志主机状态之类的状态位,而且读了之后还需要清除标志位,比较复杂。我们可使用
STM32 标准库函数来直接检测这些事件的复合标志,降低编程难度。

通讯引脚

引脚I2C1I2C2
SCLPB5/PB8(重映射)PB10
SDAPB6/PB9(重映射)PB11

I2C程序讲解

讲了怎么多的理论知识,相信大部分人都困了,对I2C通信还有诸多疑问,但没关系,看不懂可以多看几次就可以看懂了,或者通过一个程序理解。那么我们来写一个使用I2C通信的传感器设备BH1750。BH1750光强度传感器的介绍此传感器可以直接输出环境光强的数值(单位为lx),其内部有16位AD转换,即可表示1lx-65535lx,通过IIC输出其数值。此传感器有3种分辨率模式,他们的分辨率分别为:4lx,1lx和0.5lx。他们的测量时间分别为:16ms,120ms,120ms。如果对测量时间要求不高的话,建议使用0.5lx分辨率的。

初始化BH1750光照度传感器的大致流程为:




GPIO初始化 发送打开模块等待测量指令0x01 发送重置数据寄存器值0x07但仅在PowerOn模式下有效 发送选择连续L分辨率模式指令0x13 获取光照度并转发串口

器件安装

《I2C通信协议详细讲解》

程序源码

看过了流程,那就将流程图转代码吧。

main函数

int main(void)
{ 	
	delay_init();	    	 //延时函数初始化
	uart_init(115200);		 //串口函数初始化
	Light_Init();		 	 //GPIO初始化
	bh_data_send(BHPowOn);	 //发送打开模块等待测量指令0x01
	bh_data_send(BHReset);	 //发送重置数据寄存器值0x07但仅在PowerOn模式下有效
	bh_data_send(BHModeL);	 //发送选择连续L分辨率模式指令0x13
	delay_ms(180);
	
	while(1)
	{ 
		printf("%d\r\n",bh_data_read()); //获取光照度并通过串口转发
		delay_ms(100);
	}
}

Light_Init函数:

void Light_Init(void)
{ 
	GPIO_InitTypeDef  GPIO_InitStructure;
 	

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	 	//使能A端口时钟
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;	 	//设置PB6和PB7
 	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		 	//推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;			//速度50MHz
 	GPIO_Init(GPIOB, &GPIO_InitStructure);	 				    //初始化GPIOB6,7
 	GPIO_SetBits(GPIOB,GPIO_Pin_6|GPIO_Pin_7);	 				//设置PB6和PB7输出高电平

	Single_Write_BH1750(0x01); 									//发送设备地址
}

//发送地址值函数
void Single_Write_BH1750(uchar REG_Address)
{ 
   IIC_Start();                  //起始信号
   IIC_Send_Byte(BHAddWrite);   //发送设备地址+写信号
   IIC_Send_Byte(REG_Address);    //内部寄存器地址,
  // BH1750_SendByte(REG_data); //内部寄存器数据,
   IIC_Stop();                   //发送停止信号
}

bh_data_send函数:

void bh_data_send(u8 command)
{ 
    do{ 
		IIC_Start();                   //iic起始信号
		IIC_Send_Byte(BHAddWrite);     //发送器件地址
    }while(IIC_Wait_Ack());            //等待从机应答
    IIC_Send_Byte(command);            //发送指令
    IIC_Wait_Ack();                    //等待从机应答
    IIC_Stop();                        //iic停止信号
}

看不懂这个函数为什么这么做,可以对照主发送器通讯过程
《I2C通信协议详细讲解》

首先发送iic起始信号和器件地址,等待从机返回应答信号,然后发送指令,继续等待从机返回应答信号,如果没有指令要继续发送的话,那么可以发送iic停止信号而不发送指令。那么一个发送函数就算完成了。关于iic函数我会下面进行讲解的。

bh_data_read函数:

//读取传感器发送的数据,从而获取光照度函数
u16 bh_data_read(void)
{ 
	u16 buf;
	IIC_Start();                       //iic起始信号
	IIC_Send_Byte(BHAddRead);         //发送器件地址+读标志位
	IIC_Wait_Ack();                     //等待从机应答
	buf=IIC_Read_Byte(1);              //读取数据
	buf=buf<<8;                        //读取并保存高八位数据
	buf+=0x00ff&IIC_Read_Byte(0);      //读取并保存低八位数据
	IIC_Stop();                        //发送停止信号 
	return buf; 
}

看不懂这个读取函数,同样可以对照主接受器接收过程
《I2C通信协议详细讲解》

先声明一个无符号16位的buf变量准备存储数据,然后发送iic起始信号和器件地址,等待从机返回应答信号,然后将读取的高八位数据赋予给buf,因为是高八位数据,但buf里的数据并没有在高八位而在低八位,那么我们就要就数据左移八位将数据放在高八位,有了高八位数据但没有低八位数据怎么行,那么我们就在获取一次放在buf里就可以了,获取数据后,就没有其他事情干了,发送IIC停止信号就可以结束了。

前面的发送函数和接收函数里发送和接收的原理还没有说呢,现在新建一个iic.c文件 和 iic.h文件,对照着下面的两张时序图去写代码。

《I2C通信协议详细讲解》
《I2C通信协议详细讲解》

代码大致如下:
iic.c文件

#include "IIC.h"
#include "sys.h"

typedef   unsigned char BYTE;
//BYTE BUF[8]; //接收数据缓存区 

//产生IIC起始信号
void IIC_Start(void)
{ 
	SDA_OUT();     //sda线输出
	IIC_SDA=1;	  	  
	IIC_SCL=1;
	delay_us(4);
 	IIC_SDA=0;//START:when CLK is high,DATA change form high to low 
	delay_us(4);
	IIC_SCL=0;//钳住I2C总线,准备发送或接收数据 
}	  
//产生IIC停止信号
void IIC_Stop(void)
{ 
	SDA_OUT();//sda线输出
	IIC_SCL=0;
	IIC_SDA=0;//STOP:when CLK is high DATA change form low to high
 	delay_us(4);
	IIC_SCL=1; 
	IIC_SDA=1;//发送I2C总线结束信号
	delay_us(4);							   	
}
//等待应答信号到来
//返回值:1,接收应答失败
// 0,接收应答成功
u8 IIC_Wait_Ack(void)
{ 
	u8 ucErrTime=0;
	SDA_IN();      //SDA设置为输入 
	IIC_SDA=1;delay_us(1);	   
	IIC_SCL=1;delay_us(1);	 
	while(READ_SDA)
	{ 
		ucErrTime++;
		if(ucErrTime>250)
		{ 
			IIC_Stop();
			return 1;
		}
	}
	IIC_SCL=0;//时钟输出0 
	return 0;  
} 
//产生ACK应答
void IIC_Ack(void)
{ 
	IIC_SCL=0;
	SDA_OUT();
	IIC_SDA=0;
	delay_us(2);
	IIC_SCL=1;
	delay_us(2);
	IIC_SCL=0;
}
//不产生ACK应答 
void IIC_NAck(void)
{ 
	IIC_SCL=0;
	SDA_OUT();
	IIC_SDA=1;
	delay_us(2);
	IIC_SCL=1;
	delay_us(2);
	IIC_SCL=0;
}					 				     
//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答 
void IIC_Send_Byte(u8 txd)
{                         
    u8 t;   
	SDA_OUT(); 	    
    IIC_SCL=0;//拉低时钟开始数据传输
    for(t=0;t<8;t++)
    {               
        //IIC_SDA=(txd&0x80)>>7;
		if((txd&0x80)>>7)
			IIC_SDA=1;
		else
			IIC_SDA=0;
		txd<<=1; 	  
		delay_us(2);   //对TEA5767这三个延时都是必须的
		IIC_SCL=1;
		delay_us(2); 
		IIC_SCL=0;	
		delay_us(2);
    }	 
} 	    
//读1个字节,ack=1时,发送ACK,ack=0,发送nACK 
u8 IIC_Read_Byte(unsigned char ack)
{ 
	unsigned char i,receive=0;
	SDA_IN();//SDA设置为输入
    for(i=0;i<8;i++ )
	{ 
        IIC_SCL=0; 
        delay_us(2);
		IIC_SCL=1;
        receive<<=1;
        if(READ_SDA)receive++;   
		delay_us(1); 
    }					 
    if (!ack)
        IIC_NAck();//发送nACK
    else
        IIC_Ack(); //发送ACK 
    return receive;
}

iic.h文件

#ifndef __IIC_H
#define __IIC_H

//库名
#include "stm32f10x.h"
#include "delay.h"

#define SDA_IN() {GPIOB->CRL&=0X0FFFFFFF;GPIOB->CRL|=(u32)8<<28;}
#define SDA_OUT() {GPIOB->CRL&=0X0FFFFFFF;GPIOB->CRL|=(u32)3<<28;}



#define IIC_SCL PBout(6) //SCL
#define IIC_SDA PBout(7) //SDA 
#define READ_SDA PBin(7) //输入SDA 
#define ADDR 0x23//0100011
#define uchar unsigned char 

#define BHAddWrite 0x46 //从机地址+最后写方向位
#define BHAddRead 0x47 //从机地址+最后读方向位
#define BHPowDown 0x00 //关闭模块
#define BHPowOn 0x01 //打开模块等待测量指令
#define BHReset 0x07 //重置数据寄存器值在PowerOn模式下有效
#define BHModeH1 0x10 //高分辨率 单位1lx 测量时间120ms
#define BHModeH2 0x11 //高分辨率模式2 单位0.5lx 测量时间120ms
#define BHModeL 0x13 //低分辨率 单位4lx 测量时间16ms
#define BHSigModeH 0x20 //一次高分辨率 测量 测量后模块转到 PowerDown模式
#define BHSigModeH2 0x21 //同上类似
#define BHSigModeL 0x23 // 上类似

//IIC所有操作函数
void IIC_Init(void);                //初始化IIC的IO口 
void IIC_Start(void);				//发送IIC开始信号
void IIC_Stop(void);	  			//发送IIC停止信号
void IIC_Send_Byte(u8 txd);			//IIC发送一个字节
u8 IIC_Read_Byte(unsigned char ack);//IIC读取一个字节
u8 IIC_Wait_Ack(void); 				//IIC等待ACK信号
void IIC_Ack(void);					//IIC发送ACK信号
void IIC_NAck(void);				//IIC不发送ACK信号 

#endif 

看了怎么多代码,相信还是懵懵懂懂,但只要多看几遍就会理解了的。

    原文作者:Hello_STM32
    原文地址: https://blog.csdn.net/Hello_STM32/article/details/111086472
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞