24章 I2C

简述:
使用两根线sda(数据线),scl(时钟线)
开始:sda数据线下降沿,scl高电平
数据传输:scl高电平时,sda为高(1)或低(0)。sda高电平低电平切换要在scl为低时切换
结束:sda上升沿,scl高电平
优缺点:优(线程少,简单)缺(速度慢)
scl高电平有效,无论是开始(sda下降沿)结束(sda上升沿),还是数据接收。
引脚都为开漏模式

I2C 物理层

物理层
它的物理层有如下特点:
(1) 它是一个支持设备的总线。“总线”指多个设备共用的信号线。在一个I2C 通讯总线中,可连接多个I2C 通讯设备,支持多个通讯主机及多个通讯从机。
(2) 一个I2C 总线只使用两条总线线路,一条双向串行数据线(SDA) ,一条串行时钟线(SCL)。数据线即用来表示数据,时钟线用于数据收发同步。
(3) 每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。
(4) 总线通过上拉电阻接到电源。当I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。
(5) 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线
(6) 具有三种传输模式:标准模式传输速率为100kbit/s ,快速模式为400kbit/s ,高速模式下可达3.4Mbit/s,但目前大多I2C 设备尚不支持高速模式。
(7) 连接到相同总线的IC 数量受到总线的最大电容400pF 限制。

协议层

协议内包含无用数据(非需要传输的数据,像是地址,读或者写…)多,所以缺点明显,数据传输慢。
I2C 的协议定义了通讯的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。
协议层
这些图表示的是主机和从机通讯时,SDA 线的数据包序列。
其中S 表示由主机的I2C 接口产生的传输起始信号(S),这时连接到I2C 总线上的所有从机都会接收到这个信号。
起始信号产生后,所有从机就开始等待主机紧接下来广播的从机地址信号(SLAVE_ADDRESS)。在I2C 总线上,每个设备的地址都是唯一的,当主机广播的地址与某个设备地址相同时,这个设备就被选中了,没被选中的设备将会忽略之后的数据信号。根据I2C 协议,这个从机地址可以是7 位或10 位。
在地址位之后,是传输方向的选择位,该位为0 时,表示后面的数据传输方向是由主机传输至从机,即主机向从机写数据。该位为1 时,则相反,即主机由从机读数据。
从机接收到匹配的地址后,主机或从机会返回一个应答(ACK) 或非应答(NACK) 信号,只有接收到应答信号后,主机才能继续发送或接收数据。
写数据模式
R/W为1时,接收到应答信号,开始发送数据(8位),发送完等待下一个应答信号,重复知道遇见停止信号P,结束。
读数据模式
类同写数据,只不过R/W为0
读和写数据复合模式
一般在第一次传输中,主机通过SLAVE_ADDRESS 寻找到从设备后,发送一段“数据”,这段数据通常用于表示从设备内部的寄存器或存储器地址(注意区分它与SLAVE_ADDRESS 的区别);在第二次的传输中,对该地址的内容进行读或写。也就是说,第一次通讯是告诉从机读写地址,第二次则是读写的实际内容。
通讯的起始和停止信号
前文中提到的起始(S) 和停止§ 信号是两种特殊的状态,见图起始和停止信号。当SCL 线是高电平时SDA 线从高电平向低电平切换(下降沿),这个情况表示通讯的起始。当SCL 是高电平时SDA 线由低电平向高电平切换(上升沿),表示通讯的停止。起始和停止信号一般由主机产生。i2c起始和停止
数据有效性
SDA 数据线在SCL 的每个时钟周期传输一位数据。 传输时,SCL 为高电平的时候SDA表示的数据有效,即此时的SDA 为高电平时表示数据“1”,为低电平时表示数据“0”。当SCL 为低电平时,SDA的数据无效,一般在这个时候SDA 进行电平切换,为下一次表示数据做好准备。
i2c数据有效性
地址及数据方向
主机发起通讯时,通过SDA 信号线发送设备地址(SLAVE_ADDRESS) 来查找从机。I2C 协议规定设备地址可以是7 位或10 位,实际中7 位的地址应用比较广泛。紧跟设备地址的一个数据位用来表示数据传输方向,它是数据方向位(R/W ),第8位或第11 位。数据方向位为“1”时表示主机由从机读数据,该位为“0”时表示主机向从机写数据。
响应

响应

I2C 的数据和地址传输都带响应。响应包括“应答(ACK)(低电平)”和“非应答(NACK)(高点平)”两种信号。作为数据接收端时,当设备(无论主从机) 接收到I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答(ACK)”信号,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答(NACK)”信号,发送方接收到该信号后会产生一个停止信号,结束信号传输。
总结:
请添加图片描述

STM32 的I2C 外设简介
STM32 的I2C 外设可用作通讯的主机及从机,支持100Kbit/s 和400Kbit/s 的速率,支持7 位、10位设备地址,支持DMA 数据传输,并具有数据校验功能。

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

STM32 的I2C 外设都挂载在APB1 总线上,使用APB1 的时钟源PCLK1,SCL信号线的输出时钟公式如下:
标准模式100Kbit/s :
Thigh=CCR×TPCKL1
Tlow = CCR×TPCLK1
快速模式400Kbit/s
中T:sub:‘low‘/T:sub:‘high‘=2 时:
Thigh = CCR×TPCKL1
Tlow = 2×CCR×TPCKL1
快速模式中T:sub:‘low‘/T:sub:‘high‘=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 的值:CCR = THIGH/TPCLK1 = 30
计算结果得出CCR 为30,向该寄存器位写入此值则可以控制IIC 的通讯速率为400KHz,其实即使配置出来的SCL 时钟不完全等于标准的400KHz,IIC 通讯的正确性也不会受到影响,因为所有数据通讯都是由SCL 协调的,只要它的时钟频率不远高于标准即可。

I2C 初始化结构体详解

typedef struct {
	uint32_t I2C_ClockSpeed; /*!< 设置SCL 时钟频率,此值要低于400000*/
	uint16_t I2C_Mode; /*!< 指定工作模式,可选I2C 模式及SMBUS 模式*/
	uint16_t I2C_DutyCycle; /* 指定时钟占空比,可选low/high = 2:1 及16:9 模式	*/
	uint16_t I2C_OwnAddress1; /*!< 指定自身的I2C 设备地址*/
	uint16_t I2C_Ack; /*!< 使能或关闭响应(一般都要使能) */
	uint16_t I2C_AcknowledgedAddress; /*!< 指定地址的长度,可为7 位及10 位*/
} I2C_InitTypeDef;

这些结构体成员说明如下,其中括号内的文字是对应参数在STM32 标准库中定义的宏:
(1) I2C_ClockSpeed
本成员设置的是I2C 的传输速率,在调用初始化函数时,函数会根据我们输入的数值经过运算后把时钟因子写入到I2C 的时钟控制寄存器CCR。而我们写入的这个参数值不得高于400KHz。实际上由于CCR 寄存器不能写入小数类型的时钟因子,影响到SCL 的实际频率可能会低于本成员设置的参数值,这时除了通讯稍慢一点以外,不会对I2C 的标准通讯造成其它影响。
(2) I2C_Mode
本成员是选择I2C 的使用方式, 有I2C 模式(I2C_Mode_I2C) 和SMBus 主、从模式(I2C_Mode_SMBusHost、I2C_Mode_SMBusDevice ) 。I2C 不需要在此处区分主从模式,直接设置I2C_Mode_I2C 即可
(3) I2C_DutyCycle
本成员设置的是I2C 的SCL 线时钟的占空比。该配置有两个选择,分别为低电平时间比高电平时间为2:1 ( I2C_DutyCycle_2) 和16:9 (I2C_DutyCycle_16_9)。其实这两个模式的比例差别并不大,一般要求都不会如此严格,这里随便选就可以。
(4) I2C_OwnAddress1
本成员配置的是STM32 的I2C 设备自己的地址,每个连接到I2C 总线上的设备都要有一个自己的地址,作为主机也不例外。地址可设置为7 位或10 位(受下面I2C_AcknowledgeAddress 成员决定),只要该地址是I2C 总线上唯一的即可。
STM32 的I2C 外设可同时使用两个地址,即同时对两个地址作出响应,这个结构成员I2C_OwnAddress1 配置的是默认的、OAR1 寄存器存储的地址,若需要设置第二个地址寄存器OAR2,可使用I2C_OwnAddress2Config 函数来配置,OAR2 不支持10 位地址,只有7 位。
(5) I2C_Ack_Enable
本成员是关于I2C 应答设置, 设置为使能则可以发送响应信号。本实验配置为允许应答(I2C_Ack_Enable), 这是绝大多数遵循I2C 标准的设备的通讯要求, 改为禁止应答(I2C_Ack_Disable) 往往会导致通讯错误。
(6) I2C_AcknowledgeAddress
本成员选择I2C 的寻址模式是7 位还是10 位地址。这需要根据实际连接到I2C 总线上设备的地址进行选择,这个成员的配置也影响到I2C_OwnAddress1 成员,只有这里设置成10 位模式时,I2C_OwnAddress1 才支持10 位地址。
配置完这些结构体成员值,调用库函数I2C_Init 即可把结构体的配置写入到寄存器中。

I2C—读写EEPROM 实验

在这里插入图片描述
EEPROM 芯片(型号:AT24C02) 的SCL 及SDA 引脚连接到了STM32 对应的I2C引脚中,结合上拉电阻,构成了I2C 通讯总线,它们通过I2C 总线交互。EEPROM 芯片的设备地址一共有7 位,其中高4 位固定为:1010 b,低3 位则由A0/A1/A2 信号线的电平决定,见图EEPROM 设备地址,图中的R/W 是读写方向位,与地址无关。

在这里插入图片描述
按照我们此处的连接,A0/A1/A2 均为0,所以EEPROM 的7 位设备地址是:101 0000b,即0x50。由于I2C 通讯时常常是地址跟读写方向连在一起构成一个8 位数,且当R/W 位为0 时,表示写方向,所以加上7 位地址,其值为“0xA0”,常称该值为I2C 设备的“写地址”;当R/W 位为1时,表示读方向,加上7 位地址,其值为“0xA1”,常称该值为“读地址”。
EEPROM 芯片中还有一个WP 引脚,具有写保护功能,当该引脚电平为高时,禁止写入数据,当引脚为低电平时,可写入数据,我们直接接地,不使用写保护功能。

软件设计

编程要点:
(1) 配置通讯使用的目标引脚为开漏模式;
(2) 使能I2C 外设的时钟;
(3) 配置I2C 外设的模式、地址、速率等参数并使能I2C 外设;
(4) 编写基本I2C 按字节收发的函数;
(5) 编写读写EEPROM 存储内容的函数;
(6) 编写测试程序,对读写数据进行校验。

初始化I2C 的GPIO:

 static void I2C_GPIO_Config(void)
 {
	 GPIO_InitTypeDef GPIO_InitStructure;
	/* 使能与I2C 有关的时钟*/
	 EEPROM_I2C_APBxClock_FUN ( EEPROM_I2C_CLK, ENABLE );
	 EEPROM_I2C_GPIO_APBxClock_FUN ( EEPROM_I2C_GPIO_CLK, ENABLE );
	
	 /* I2C_SCL、I2C_SDA*/
	GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 开漏输出
	GPIO_Init(EEPROM_I2C_SCL_PORT, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SDA_PIN;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 开漏输出
	GPIO_Init(EEPROM_I2C_SDA_PORT, &GPIO_InitStructure);
}

开启相关的时钟并初始化GPIO 引脚,函数执行流程如下:
(1) 使用GPIO_InitTypeDef 定义GPIO 初始化结构体变量,以便下面用于存储GPIO 配置;
(2) 调用库函数RCC_APB1PeriphClockCmd(代码中为宏EEPROM_I2C_APBxClock_FUN)使能I2C外设时钟,调用RCC_APB2PeriphClockCmd(代码中为宏EEPROM_I2C_GPIO_APBxClock_FUN)来使能I2C 引脚使用的GPIO 端口时钟,调用时我们使用“|”操作同时配置两个引脚。
(3) 向GPIO 初始化结构体赋值,把引脚初始化成复用开漏模式,要注意I2C 的引脚必须使用这种模式。
(4) 使用以上初始化结构体的配置,调用GPIO_Init 函数向寄存器写入参数,完成GPIO 的初始化。

配置I2C 的模式

 /**
 * @brief I2C 工作模式配置
 * @param 无
 * @retval 无
 */
 static void I2C_Mode_Configu(void)
 {
	 I2C_InitTypeDef I2C_InitStructure;	
	/* I2C 配置*/
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;	
	/* 高电平数据稳定,低电平数据变化SCL 时钟线的占空比*/
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;	
	I2C_InitStructure.I2C_OwnAddress1 =I2Cx_OWN_ADDRESS7;
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ;	
	/* I2C 的寻址模式*/
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;	
	/* 通信速率*/
	I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;	
	
	/* I2C 初始化*/
	I2C_Init(EEPROM_I2Cx, &I2C_InitStructure);	
	/* 使能I2C */
	I2C_Cmd(EEPROM_I2Cx, ENABLE);
}
 /**
 * @brief I2C 外设(EEPROM) 初始化
 * @param 无
 * @retval 无
 */
 void I2C_EE_Init(void)
 {
	 I2C_GPIO_Config();	
	 I2C_Mode_Configu();	
	 /* 根据头文件i2c_ee.h 中的定义来选择EEPROM 要写入的设备地址*/
	 /* 选择EEPROM Block0 来写入*/
	 EEPROM_ADDRESS = EEPROM_Block0_ADDRESS;
 }

向EEPROM 写入一个字节的数据

 /* 通讯等待超时时间*/
 #define I2CT_FLAG_TIMEOUT ((uint32_t)0x1000)
 #define I2CT_LONG_TIMEOUT ((uint32_t)(10 * I2CT_FLAG_TIMEOUT))
 /**
 * @brief I2C 等待事件超时的情况下会调用这个函数来处理
 * @param errorCode:错误代码,可以用来定位是哪个环节出错.
 * @retval 返回0,表示IIC 读取失败.
 */
static uint32_t I2C_TIMEOUT_UserCallback(uint8_t errorCode)
	 {
	 /* 使用串口printf 输出错误信息,方便调试*/
	 EEPROM_ERROR("I2C 等待超时!errorCode = %d",errorCode);
	 return 0;
	 }
 /**
 * @brief 写一个字节到I2C EEPROM 中
 * @param pBuffer: 缓冲区指针
 * @param WriteAddr: 写地址
 * @retval 正常返回1,异常返回0
 */
uint32_t I2C_EE_ByteWrite(u8* pBuffer, u8 WriteAddr)
	 {
	 /* 产生I2C 起始信号*/
	 I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);	
	 /* 设置超时等待时间*/
	 I2CTimeout = I2CT_FLAG_TIMEOUT;
	 /* 检测EV5 事件并清除标志*/
	 while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
	 {
		 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(0);
	 }
	 /* 发送EEPROM 设备地址*/
	 I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Transmitter);
	 I2CTimeout = I2CT_FLAG_TIMEOUT;
	 /* 检测EV6 事件并清除标志*/
	 while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
	 {
	 	if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(1);
	 }
	 /* 发送要写入的EEPROM 内部地址(即EEPROM 内部存储器的地址) */
	I2C_SendData(EEPROM_I2Cx, WriteAddr);
	I2CTimeout = I2CT_FLAG_TIMEOUT;
	 /* 检测EV8 事件并清除标志*/
	 while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
	 {
	 	if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);
	 }
	 /* 发送一字节要写入的数据*/
	 I2C_SendData(EEPROM_I2Cx, *pBuffer);
	 I2CTimeout = I2CT_FLAG_TIMEOUT;
	 /* 检测EV8 事件并清除标志*/
	 while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
	 {
	 	if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
	 }
	 /* 发送停止信号*/
	 I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
	 return 1;
 }

先来分析I2C_TIMEOUT_UserCallback 函数,它的函数体里只调用了宏EEPROM_ERROR,这个宏封装了printf 函数,方便使用串口向上位机打印调试信息,阅读代码时把它当成printf 函数即可。在I2C 通讯的很多过程,都需要检测事件,当检测到某事件后才能继续下一步的操作,但有时通讯错误或者I2C 总线被占用,我们不能无休止地等待下去,所以我们设定每个事件检测都有等待的时间上限,若超过这个时间,我们就调用I2C_TIMEOUT_UserCallback 函数输出调试信息(或可以自己加其它操作),并终止I2C 通讯。
了解了这个机制,再来分析I2C_EE_ByteWrite 函数,两个参数(u8* pBuffer缓冲区指针, u8 WriteAddr写地址)这个函数实现了前面讲的I2C 主发送器通讯流程。
(1) 使用库函数I2C_GenerateSTART 产生I2C 起始信号,其中的EEPROM_I2C 宏是前面硬件定义相关的I2C 编号;
(2) 对I2CTimeout 变量赋值为宏I2CT_FLAG_TIMEOUT,这个I2CTimeout 变量在下面的while 循环中每次循环减1,该循环通过调用库函数I2C_CheckEvent 检测事件,若检测到事件,则进入通讯的下一阶段,若未检测到事件则停留在此处一直检测,当检测I2CT_FLAG_TIMEOUT 次都还没等待到事件则认为通讯失败,调用前面的I2C_TIMEOUT_UserCallback 输出调试信息,并退出通讯;
(3) 调用库函数I2C_Send7bitAddress 发送EEPROM 的设备地址,并把数据传输方向设置为I2C_Direction_Transmitter(即发送方向),这个数据传输方向就是通过设置I2C 通讯中紧跟地址后面的R/W 位实现的。发送地址后以同样的方式检测EV6 标志;
(4) 调用库函数I2C_SendData 向EEPROM 发送要写入的内部地址,该地址是I2C_EE_ByteWrite函数的输入参数,发送完毕后等待EV8 事件。要注意这个内部地址跟上面的EEPROM 地址不一样,上面的是指I2C 总线设备的独立地址,而此处的内部地址是指EEPROM 内数据组织的地址,也可理解为EEPROM 内存的地址或I2C 设备的寄存器地址;
(5) 调用库函数I2C_SendData 向EEPROM 发送要写入的数据,该数据是I2C_EE_ByteWrite 函数的输入参数,发送完毕后等待EV8 事件;
(6) 一个I2C 通讯过程完毕,调用I2C_GenerateSTOP 发送停止信号。在这个通讯过程中,STM32 实际上通过I2C 向EEPROM 发送了两个数据,但为何第一个数据被解释为EEPROM 的内存地址?这是由EEPROM 的自己定义的单字节写入时序,见图EEPROM 单字节写入时序。

EEPROM 的单字节时序规定,向它写入数据的时候,第一个字节为内存地址,第二个字节是要写入的数据内容。所以我们需要理解:命令、地址的本质都是数据,对数据的解释不同,它就有了不同的功能。
多字节写入及状态等待
单字节写入通讯结束后,EEPROM 芯片会根据这个通讯结果擦写该内存地址的内容,这需要一段时间,所以我们在多次写入数据时,要先等待EEPROM 内部擦写完毕。

uint8_t I2C_EE_ByetsWrite(uint8_t* pBuffer,uint8_t WriteAddr, uint16_t NumByteToWrite)
 {
	 uint16_t i;
	 uint8_t res;
	for (i=0; i<NumByteToWrite; i++)
	{
	/* 等待EEPROM 准备完毕*/
	I2C_EE_WaitEepromStandbyState();
	/* 按字节写入数据*/
	res = I2C_EE_ByteWrite(pBuffer++,WriteAddr++);
}
return res;

EEPROM 的页写入
每页有8 个字节,I2C_EE_BufferWrite函数三个参数(u8* pBuffer, u8 WriteAddr,u16 NumByteToWrite)
类似的按页读函数I2C_EE_BufferRead(u8* pBuffer, u8 ReadAddr, u16 NumByteToRead)是一样的:
pBuffer:存放从EEPROM读取的数据的缓冲区指针;WriteAddr:接收数据的EEPROM的地址;NumByteToWrite:要从EEPROM读取的字节数。
从EEPROM 读取数据

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值