【STM32-I2C学习总结】STM32:硬件-IIC详解 , 固件库编程 , 手把手教你实现IIC

一 、I2C物理层

I2C 通讯设备之间的常用连接方式见图:

常见的 I2C 通讯系统
有以下特点:(参考数据手册:上拉电阻一般4.7k~10k ,一般4.7k)
(1)由两条总线控制:一条双向串行数据线(SDA) ,一条串行时钟线 (SCL)。数据线即用来表示数据,时钟线用于数据收发同步。
(2)I2C总线上可挂在多个 I2C通讯的设备,如图所示。
(3)每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之
间的访问。
(4)总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空
闲,都输出高阻态时,由上拉电阻把总线拉成高电平。
(5)多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用
总线。
(6)具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式
下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式。

二、协议层

1、I2C基本读写过程:阴影部分代表数据由主机传输至从机,无阴影部分相反
(1)主机写数据到从机
在这里插入图片描述

(2)主机由从机中读数据
![主机由从机中读数据](https://img-blog.csdnimg.cn/20210206143318556.png

(3)I2C 通讯复合格式
在这里插入图片描述

简单理解,当配置I2C完成后,发出开始信号(S)。主机开始广播某特定地址的从机(SLAVE ADDERSS),并给出要做出的操作(R/W),当从机收到主机的广播地址后,会通过内部地址比较器与自身地址比较,如果主机不是呼叫自己,那么自身保持“高阻”不接受应答。若从机发现主机呼叫的是自己,那么将SDA和SCL总线拉低,表示占用总线,接受响应并产生应答信号。执行读写操作。
若执行的是写操作,参考图(1)当从机发出应答信号给主机,主机得到了应答后,开始给该从机发送数据,从机获取数据,应答主机(告诉主机 :自己收到了数据)。如此循环主机一直发送数据给从机,直到从机发出“非应答信号”(数据够了,我不要了,停止吧)主机发出停止信号,表示停止发送。
若执行的是读操作,参考图(2)当从机发出应答信号给主机,并发送主机需要的数据DATA,当主机接受到来自从机的数据DATA后,会应答从机(表示:好的,你的数据我已接受,你继续)。如此循环,一直接受数据。知道主机发出“非应答信号”(表示:感谢,我的数据已经够了,你停把)。然后给从机发出停止信号。
对于复合操作,参考(1)(2).

2、通讯的起始和停止位

起始(S)和停止§信号是两种特殊的状态。当 SCL 线是高电平时 SDA 线从高电平向低电平切换,这个情况表示通讯的起始。当 SCL 是高电平时 SDA 线由低电平向高电平切换,表示通讯的停止。起始和停止信号一般由主机产生。
在这里插入图片描述
3、数据有效性
I2C 使用 SDA 信号线来传输数据,使用 SCL 信号线进行数据同步。SDA数据线在 SCL 的每个时钟周期传输一位数据。传输时,SCL 为高电平的时候 SDA 表示的数据有效,即此时的 SDA 为高电平时表示数据“1”,为低电平时表示数据“0”。当 SCL为低电平时,SDA 的数据无效,一般在这个时候 SDA 进行电平切换,为下一次表示数据做好准备。
![数据切换](https://img-blog.csdnimg.cn/20210206150554889.png
4、地址及数据方向

I2C 总线上的每个设备都有自己的独立地址,主机发起通讯时,通过 SDA 信号线发送设备地址(SLAVE_ADDRESS)来查找从机。I2C 协议规定设备地址可以是 7 位或 10 位,实际中 7 位的地址应用比较广泛。紧跟设备地址的一个数据位用来表示数据传输方向,它是数据方向位(R/W),第 8 位或第 11 位。数据方向位为“1”时表示主机由从机读数据,该位为“0”时表示主机向从机写数据。(下图以七位地址为例)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210206151211301.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ1Njg5Nzkw,size_16,color_FFFFFF,t_70)以7位地址为例
5、响应
I2C 的数据和地址传输都带响应。响应包括“应答(ACK)”和“非应答(NACK)”两种信号。作为数据接收端时,当设备(无论主从机)接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答(ACK)”信号,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答(NACK)”信号,发送方接收到该信号后会产生一个停止信号,结束信号传输。
在这里插入图片描述
传输时主机产生时钟,在第 9 个时钟时,数据发送端会释放 SDA 的控制权,由数据接
收端控制 SDA,若 SDA 为高电平,表示非应答信号(NACK),低电平表示应答信号(ACK)。

三、 通讯过程

对于实现I2C通讯,该部分十分重要。编程过程基本都是按照下图进行操作
1、主发送器
在这里插入图片描述
流程:
(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 设备产生一个停止信号(P),这个时候会产生EV8_2 事件,SR1 的 TXE 位及 BTF 位都被置 1,表示通讯结束。
假如我们使能了 I2C 中断,以上所有事件产生时,都会产生 I2C 中断信号,进入同一个中断服务函数,到 I2C 中断服务程序后,再通过检查寄存器位来判断是哪一个事件。

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

看到这里的小伙伴,恭喜你,你的耐力不错,竟然看完了这么多的理论知识,你可能也会感慨,这I2C也忒难搞了吧。一会发送这个东西,一会又检测另一个事件。要搞完一次 I2C通讯,岂不是要肝到天荒地老,哈哈哈哈。不要担心,其实很多工作,完全不需要我们做,官方已经为我们写好了大量的库,我们只需要进行“CV编程开发”即可,再加上一点小小的理解。就可以很轻松的实现I2C通讯了。

按着步骤走很简单的:
I2C_InitTypeDef 结构体:

typedef struct
{
    
    
  uint32_t I2C_ClockSpeed; 
  uint16_t I2C_Mode;               
  uint16_t I2C_DutyCycle;           
  uint16_t I2C_OwnAddress1;                                            
  uint16_t I2C_Ack;                                          
  uint16_t I2C_AcknowledgedAddress;                                     
}I2C_InitTypeDef;

把每个结构体成员都配置一下就好,具体怎么配置,.h文件里都有对应的宏,Ctrl+CV即可

OKK说完这些,程序猿的事情,当然要代码交流了!
(1)void I2C_GPIO_Config(void)

void I2C_GPIO_Config(void)
{
    
    
	 GPIO_InitTypeDef GPIO_InitStruct;
	//初始化 IIC_GPIO 时钟
	RCC_APB2PeriphClockCmd(I2Cx_GPIO_SCL_Clk|I2Cx_GPIO_SDA_Clk,ENABLE);
	//初始化IIC_SCL
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
	GPIO_InitStruct.GPIO_Pin = I2Cx_GPIO_SCL_Pin;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	
	GPIO_Init(I2Cx_GPIO_SCL_Port,&GPIO_InitStruct);
	
	//初始化IIC_SDA	
	GPIO_InitStruct.GPIO_Pin = I2Cx_GPIO_SDA_Pin;
	
	GPIO_Init(I2Cx_GPIO_SDA_Port,&GPIO_InitStruct);
}

(2)void I2C_GPIO_Config(void)

void I2C_GPIO_Config(void)
{
    
    
	 GPIO_InitTypeDef GPIO_InitStruct;
	//初始化 IIC_GPIO 时钟
	RCC_APB2PeriphClockCmd(I2Cx_GPIO_SCL_Clk|I2Cx_GPIO_SDA_Clk,ENABLE);
	//初始化IIC_SCL
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
	GPIO_InitStruct.GPIO_Pin = I2Cx_GPIO_SCL_Pin;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	
	GPIO_Init(I2Cx_GPIO_SCL_Port,&GPIO_InitStruct);
	
	//初始化IIC_SDA	
	GPIO_InitStruct.GPIO_Pin = I2Cx_GPIO_SDA_Pin;
	GPIO_Init(I2Cx_GPIO_SDA_Port,&GPIO_InitStruct);
}

(3)void I2C_Config(void)

void I2C_Config(void)
{
    
    
	I2C_InitTypeDef I2C_InitStruct;
	I2C_GPIO_Config();//配置SDA和SCL GPIO
	//初始化IIC外设时钟
	RCC_APB1PeriphClockCmd(DEBUG_I2Cx_Clk,ENABLE);
	
	//初始化IIC 结构体
	I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;			    //使能应答
	I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;	//IIC 7 位寻址
	I2C_InitStruct.I2C_ClockSpeed = DEBUG_I2C_ClockSpeed;//通信速率 400k
	I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;		//占空比 1/2
	I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;			  //选择IIC模式
	I2C_InitStruct.I2C_OwnAddress1 = DEBUG_I2Cx_Addr;//输入主机地址,与从机有所区别即可正常通信

	I2C_Init(DEBUG_I2Cx_Port,&I2C_InitStruct);
	//使能IIC
	I2C_Cmd(DEBUG_I2Cx_Port, ENABLE);
}

*(4)void I2C_ByteWrite(uint8_t pBuffer, uint8_t WriteAddr)

void I2C_ByteWrite(uint8_t *pBuffer, uint8_t WriteAddr)
{
    
    
	//读一个字节
	while(I2C_GetFlagStatus(DEBUG_I2Cx_Port, I2C_FLAG_BUSY));
	//发送Start信号
	I2C_GenerateSTART(DEBUG_I2Cx_Port,ENABLE);
	//等待EV5事件:IIC开始信号已经发出 (I2C_SR1内SB位置1)
	while(I2C_CheckEvent(DEBUG_I2Cx_Port,I2C_EVENT_MASTER_MODE_SELECT)==ERROR);
	
	//发送7位“EEPROM地址”
	I2C_Send7bitAddress(DEBUG_I2Cx_Port,DEBUG_EEPROM_Addr,I2C_Direction_Transmitter);
	//等待EV6事件:表示地址已经发送
	while(I2C_CheckEvent(DEBUG_I2Cx_Port,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)==ERROR);
	
	//写入EEPROM内将要写入的地址数据
	I2C_SendData(DEBUG_I2Cx_Port,WriteAddr);
	//等待EV8事件:返回SET则数据寄存器DR为空	
	while(I2C_CheckEvent(DEBUG_I2Cx_Port,I2C_EVENT_MASTER_BYTE_TRANSMITTED)==ERROR);
	
	//写入数据
	I2C_SendData(DEBUG_I2Cx_Port,*pBuffer);
	//等待EV8事件:返回SET则数据寄存器DR为空
	while(I2C_CheckEvent(DEBUG_I2Cx_Port,I2C_EVENT_MASTER_BYTE_TRANSMITTED)==ERROR);
	
	//一个字节发送完成,发送Stop信号
  I2C_GenerateSTOP(DEBUG_I2Cx_Port, ENABLE);
}

*(5)void I2C_ByteRead(uint8_t pBuffer, uint8_t ReadAddr);

/**
  * @brief   从EEPROM里面读取一块数据 
  * @param   
  *		@arg pBuffer:存放从EEPROM读取的数据的缓冲区指针
  *		@arg WriteAddr:接收数据的EEPROM的地址
  * @retval  无
  */
void I2C_ByteRead(uint8_t *pBuffer, uint8_t ReadAddr)
{
    
    
	//发送Start信号
	I2C_GenerateSTART(DEBUG_I2Cx_Port,ENABLE);
	//等待EV5事件:IIC开始信号已经发出 (I2C_SR1内SB位置1)
	while(I2C_CheckEvent(DEBUG_I2Cx_Port,I2C_EVENT_MASTER_MODE_SELECT)==ERROR);
	
	//发送7位“EEPROM地址”
	I2C_Send7bitAddress(DEBUG_I2Cx_Port,DEBUG_EEPROM_Addr,I2C_Direction_Transmitter);
	//等待EV6事件:表示地址已经发送
	while(I2C_CheckEvent(DEBUG_I2Cx_Port,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)==ERROR);
	
	//写入EEPROM内存“单元地址”
	I2C_SendData(DEBUG_I2Cx_Port,ReadAddr);
	//等待EV8事件:数据寄存器DR为空	,地址数据已经发送
	while(I2C_CheckEvent(DEBUG_I2Cx_Port,I2C_EVENT_MASTER_BYTE_TRANSMITTED)==ERROR);
	
	//重新发送Start信号
	I2C_GenerateSTART(DEBUG_I2Cx_Port,ENABLE);
	//等待EV5事件
	while(I2C_CheckEvent(DEBUG_I2Cx_Port,I2C_EVENT_MASTER_MODE_SELECT)==ERROR);
	
	//发送7位“EEPROM地址”
	I2C_Send7bitAddress(DEBUG_I2Cx_Port,DEBUG_EEPROM_Addr,I2C_Direction_Receiver);//注意方向
	//等待EV6事件(接收):表示地址已经发送
	while(I2C_CheckEvent(DEBUG_I2Cx_Port,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)==ERROR);//注意方向
	
	//产生非应答
	I2C_AcknowledgeConfig(DEBUG_I2Cx_Port, DISABLE);
	//发送Stop信号
	I2C_GenerateSTOP(DEBUG_I2Cx_Port, ENABLE);
	//等待EV7事件, BUSY, MSL and RXNE flags
	while(I2C_CheckEvent(DEBUG_I2Cx_Port, I2C_EVENT_MASTER_BYTE_RECEIVED)==ERROR); 
	
	*pBuffer = I2C_ReceiveData(DEBUG_I2Cx_Port);

	//重新初始化 为下次做准备
	I2C_AcknowledgeConfig(DEBUG_I2Cx_Port, ENABLE);
}

(6)函数测试

#include "stm32f10x.h"
#include "bsp_i2c.h"
#include "bsp_usart.h"

uint8_t I2C_Buf_Write[256];
uint8_t I2C_Buf_Read[256];
uint16_t i=0;
void delay(uint32_t count)
{
    
    
	while(count--);
}
int main(void)
{
    
    
	USART_Config();
	I2C_Config();
	for(i=0;i<256;i++)
	{
    
    
		I2C_Buf_Write[i] = i;
	}
	printf("\r\n 这是一个I2C外设(AT24C02)读写测试例程 \r\n");
	I2C_ByteWrite(&I2C_Buf_Write[2],0x55);
	delay(0xffff);//因为STM32处理速度远大400k 所以,等待写入完成。
	I2C_ByteRead(&I2C_Buf_Read[2],0x55);
	 printf("0x%x ", I2C_Buf_Read[2]);
	while(1);
}
//读写函数为测试学习使用,所有还有很多不完善的地方,改进中.....

在这里插入图片描述

在实现过程中,对应的I2Cx、时钟、GPIO选取等,全都在"bsp_i2c.h"、"bsp_usart.h"
你们可以结合自己的硬件,配置对应的参数。串口配置,是为了调试方便。

#ifndef _BSP_I2C_H
#define _BSP_I2C_H

#include "stm32f10x.h"

#define I2Cx_GPIO_SCL_Port					GPIOB
#define I2Cx_GPIO_SCL_Pin					GPIO_Pin_6
#define I2Cx_GPIO_SCL_Clk					RCC_APB2Periph_GPIOB

#define I2Cx_GPIO_SDA_Port					GPIOB
#define I2Cx_GPIO_SDA_Pin					GPIO_Pin_7
#define I2Cx_GPIO_SDA_Clk					RCC_APB2Periph_GPIOB

#define DEBUG_I2Cx_Addr						0xB0
#define DEBUG_EEPROM_Addr					0xA0  //7位地址1010000

#define DEBUG_I2C_ClockSpeed			    400000

#define DEBUG_I2Cx_Port					    I2C1
#define DEBUG_I2Cx_Clk						RCC_APB1Periph_I2C1

void I2C_GPIO_Config(void);
void I2C_Config(void);
void I2C_ByteWrite(uint8_t *pBuffer, uint8_t WriteAddr);
void I2C_ByteRead(uint8_t *pBuffer, uint8_t WriteAddr);
#endif /*_BSP_I2C_H*/

参考书籍:《【野火®】零死角玩转STM32—F103霸道_V2》。有需要的小伙伴,可以留下你的邮箱,我会第一时间发过去。《AT24C02数据手册》。

关于 模拟 “软件I2C” ,过几天我会再写一篇,大家可以先点个关注(⊙o⊙)
欢迎交流探讨。

/********************************* 更新 *************************************/

其实I2C通讯过程,理解时序,可以帮助你了解I2C的工作过程。这个时候,你可以参考AT24C02数据手册,看下他的时序图。然后参考软件实现I2C,他是如何生成各种信号。大概过程如下(仅帮助理解,我当时梳理思路的时候写的,不能运行的那种,有一点需要补充**DELAY,**其实就是 WaitAck() 等待应答,当时随手写的)

//EEPROM 送入一个字节
void EE_Send_Byte(uint8_t data)
{
    
    
	uint8_t i = 0;
	
	for(i = 0; i < 8; i++)
	{
    
    
		if(data&0x80)
		{
    
    
			SDA = 1;
		}
		else
		{
    
    
			SD = 0;
		}
		DELAY;
		SCL = 1;
		DEALY;
		SCL = 0;
		if(i == 7)
		{
    
    
			SDA = 1;
		}
		data <<= 1;
		DELAY;
		
	}	
}
//EEPROM 读一个字节
uint8_t EE_Read_Byte(void)
{
    
    
	uint8_t i = 0;
	uint8_t value = 0;
	
	for(i = 0; i < 8; i++)
	{
    
    
		value <<= 1;
		SCL = 1;
		DEALY;
		if(SDA_Read == 1)
		{
    
    
			value++;
		}
		SCL = 0;
		DEALY;	
	}	
	return value;
}
/* 开始信号*/
void I2C_Start(void)
{
    
    
	SDA = 1;
	SCL = 1;
	DEALY;
	SDA = 0;
	DELAY;
	SCL = 0;
	DELAY;
}

/* 停止信号*/
void I2C_Stop(void)
{
    
    
	SDA = 0;
	SCL = 1;
	DEALY;
	SDA = 1;
}

/* 应答信号*/
void I2C_Ack(void)
{
    
    
	SDA = 0; //释放总线
	DEALY;
	SCL = 1;
	DEALY;
	SCL = 0;
	DEALY;
	SDA = 1;
}
/* 非应答信号*/
void I2C_NAck(void)
{
    
    
	SDA = 1; //释放总线
	DEALY;
	SCL = 1;
	DEALY;
	SCL = 0;
	DEALY;
}
uint8_t I2C_WaitAck(void)
{
    
    
	uint8_t re;
	
	SDA = 1; //释放总线
	DEALY;
	SCL = 1;
	DEALY;
	if(Read_SDA)
	{
    
    
		re = 1;
	}
	else 
	{
    
    
		re = 0;
	}
	SCL = 1;
	DEALY;
	return re;
}


猜你喜欢

转载自blog.csdn.net/qq_45689790/article/details/113725196