IIC笔记

IIC笔记

I2C总线的特点

1、它是一个支持多设备的总线。总线上多个设备共用的信号线,在一条I2C的总线钟,可链接多个I2C的通讯设备,支持多个主机和多个从机

2、一条I2C总线只使用两条线路,一条是双向串行数据显(SDA),另外一条是串行时钟线(SCL)。双向串行数据线用于传输数据,串行时钟线用于数据收发同步

3、每一个连接到总线的设备都有一个独一无二的地址,主机可以通过这些地址进行不同设备的访问

4、总线通过上拉电阻接到电源。当I2C设备空闲时,会输出高组态,而当所有设备都空闲时,由上拉电阻把总线拉高成高电平

5、多个主机同时使用总线时,为了防止数据冲突,会利用仲裁的方式决定由哪个设备占用总线。

6、具有三种传输模式,标准模式下传输速率为100kps,快速模式下为400kps,高速模式下可达3.4Mbps,但目前大多数IIC设备不支持高速模式

这里其实我一直有一个问题,这个应答位到底是如何运行的??是一根线在等另外一根线的的反应吗?

在I2C总线上,数据传输是由时钟信号(SCL)驱动的。当发送8位数据后,SDA上的数据已经被发送完毕,然而,SDA并不是在SCL给出反应后再等待。相反,SDA上的每个数据位都在SCL的边沿上稳定。

具体来说,当传输8位数据时,每个数据位都在SCL的上升沿(rising edge)或下降沿(falling edge)上稳定。发送方(通常是主设备)在每个时钟边沿将一个位发送到SDA上,而接收方(通常是从设备)在每个时钟边沿读取SDA上的位。

所以,SDA并不是在等待SCL的反应。SDA上的数据在SCL的边沿上稳定,而总线的协议规定了数据的传输时机。整个I2C通信的节奏由SCL控制,而SDA上的数据在每个SCL的边沿进行相应的变化。

IIC约定

发送一个字节的数据后,对方(接收方)必须要返回一个ACK(应答位)

ACK:是在SDA数据线上的第9个时钟周期,(接收方)必须给SDA一个低电平。

低电平		表示		应答

高电平		表示		没有应答

发送方在发送完第8bit的数据后,一般要释放SDA线(SDA=1)、

这里我一直碰到一个问题,我使用的是正点原子的stm32F4,我使用软件IIC控制MPU6050,就使用了简单的应答实验:起始信号,发送0xA0,读取应答位,停止信号;这里我想读取这个应答位,发现一直为1; 是不是因为我没有释放SDA线???

数据如何发送呢?? 高电平/低电平

在何时发送?? 何时采集呢??

在SCL时钟线低电平的时候,可以改变SDA数据线的电平;

在SCL时钟线高电平点伤害,SDA数据线保持稳定。

===》

	在时钟线SCL的下降沿,发送数据

	在时钟线SCL的上升线,采集数据

数据的发送,MSB先发送,最高bit先发送。

IIC时钟频率,一般在 400k 以内,低速总线。


那么这里就有一个问题了,SCL时钟线由谁来控制呢??谁产生时钟呢??

其实谁来控制都无所谓,只要不是同时控制就好!!但是有很多设备,不具备控制时钟的能力,因为它可能没有时钟输出单元。

信号

起始信号

SCL时钟线保持高电平
SDL数据线从高到低的跳变

停止信号

SCL 保持高电平
SDA 从低到高电平的跳变

在这里插入图片描述

数据收发

Data: User Data , Device Addr

从物理层面来说,数据和地址,都是“数据”。
这个数据包括用户的真正的数据,也包括设备的地址,因为总线上有多个设备,其中一个发起起始信号,表示它要跟总线上的某个或多个设备通信。

	它到底要和谁通信呢? 

	如果不指定,总线上所有设备都可以收到数据。 => IIC协议规定,每个IIC总线上的设备都必须有一个IIC设备地址(7bits/10bits),并且同一个IIC总线上的设备的地址必须不一样。

IIC给定数据收发是以字节为单位:8bits

设备地址 = 7 bits + R/W#(读写位,b0)

				R:	发送方	读你的数据					1

				W#:发送方	写数据给你				0

在这里插入图片描述

应答位

1、从机发送应答位给主机,来不及接收主机数据时发出非应答位,主机终止数据传输;而从机空闲时,从机发出应答(在每接收完一个字节的数据后,从机发送一个应答位,数据第九位为0),即主机根据从发机的应答位判断从机是否成功接收数据(主机发送数据时,通过检测应答位)。
2、主机收到最后一个数据后,向从机发送结束传送的信号(对丛机发送非应答,过程中会将SDA拉低),然后从机释放SDA线(防止SDA拉低以后主机无法将其拉高),从而允许主机产生终止信号(主机接收数据时)(发送应答)其次,主机在发送应答后,也要释放SDA线,防止将其拉低后影响从机后续的数据传送

在这里插入图片描述

具体的组合形式

1、主机发送数据

2、主机接收数据

在这里插入图片描述

3、主机先发送数据在接收数据
在这里插入图片描述

IIC笔记

I2C总线的特点

1、它是一个支持多设备的总线。总线上多个设备共用的信号线,在一条I2C的总线钟,可链接多个I2C的通讯设备,支持多个主机和多个从机

2、一条I2C总线只使用两条线路,一条是双向串行数据显(SDA),另外一条是串行时钟线(SCL)。双向串行数据线用于传输数据,串行时钟线用于数据收发同步

3、每一个连接到总线的设备都有一个独一无二的地址,主机可以通过这些地址进行不同设备的访问

4、总线通过上拉电阻接到电源。当I2C设备空闲时,会输出高组态,而当所有设备都空闲时,由上拉电阻把总线拉高成高电平

5、多个主机同时使用总线时,为了防止数据冲突,会利用仲裁的方式决定由哪个设备占用总线。

6、具有三种传输模式,标准模式下传输速率为100kps,快速模式下为400kps,高速模式下可达3.4Mbps,但目前大多数IIC设备不支持高速模式

这里其实我一直有一个问题,这个应答位到底是如何运行的??是一根线在等另外一根线的的反应吗?

在I2C总线上,数据传输是由时钟信号(SCL)驱动的。当发送8位数据后,SDA上的数据已经被发送完毕,然而,SDA并不是在SCL给出反应后再等待。相反,SDA上的每个数据位都在SCL的边沿上稳定。

具体来说,当传输8位数据时,每个数据位都在SCL的上升沿(rising edge)或下降沿(falling edge)上稳定。发送方(通常是主设备)在每个时钟边沿将一个位发送到SDA上,而接收方(通常是从设备)在每个时钟边沿读取SDA上的位。

所以,SDA并不是在等待SCL的反应。SDA上的数据在SCL的边沿上稳定,而总线的协议规定了数据的传输时机。整个I2C通信的节奏由SCL控制,而SDA上的数据在每个SCL的边沿进行相应的变化。

IIC约定

发送一个字节的数据后,对方(接收方)必须要返回一个ACK(应答位)

ACK:是在SDA数据线上的第9个时钟周期,(接收方)必须给SDA一个低电平。

低电平		表示		应答

高电平		表示		没有应答

发送方在发送完第8bit的数据后,一般要释放SDA线(SDA=1)、

这里我一直碰到一个问题,我使用的是正点原子的stm32F4,我使用软件IIC控制MPU6050,就使用了简单的应答实验:起始信号,发送0xA0,读取应答位,停止信号;这里我想读取这个应答位,发现一直为1; 是不是因为我没有释放SDA线???

数据如何发送呢?? 高电平/低电平

在何时发送?? 何时采集呢??

在SCL时钟线低电平的时候,可以改变SDA数据线的电平;

在SCL时钟线高电平点伤害,SDA数据线保持稳定。

===》

	在时钟线SCL的下降沿,发送数据

	在时钟线SCL的上升线,采集数据

数据的发送,MSB先发送,最高bit先发送。

IIC时钟频率,一般在 400k 以内,低速总线。


那么这里就有一个问题了,SCL时钟线由谁来控制呢??谁产生时钟呢??

其实谁来控制都无所谓,只要不是同时控制就好!!但是有很多设备,不具备控制时钟的能力,因为它可能没有时钟输出单元。

信号

起始信号

SCL时钟线保持高电平
SDL数据线从高到低的跳变

停止信号

SCL 保持高电平
SDA 从低到高电平的跳变

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

数据收发

Data: User Data , Device Addr

从物理层面来说,数据和地址,都是“数据”。
这个数据包括用户的真正的数据,也包括设备的地址,因为总线上有多个设备,其中一个发起起始信号,表示它要跟总线上的某个或多个设备通信。

	它到底要和谁通信呢? 

	如果不指定,总线上所有设备都可以收到数据。 => IIC协议规定,每个IIC总线上的设备都必须有一个IIC设备地址(7bits/10bits),并且同一个IIC总线上的设备的地址必须不一样。

IIC给定数据收发是以字节为单位:8bits

设备地址 = 7 bits + R/W#(读写位,b0)

				R:	发送方	读你的数据					1

				W#:发送方	写数据给你				0

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


应答位

1、从机发送应答位给主机,来不及接收主机数据时发出非应答位,主机终止数据传输;而从机空闲时,从机发出应答(在每接收完一个字节的数据后,从机发送一个应答位,数据第九位为0),即主机根据从发机的应答位判断从机是否成功接收数据(主机发送数据时,通过检测应答位)。
2、主机收到最后一个数据后,向从机发送结束传送的信号(对丛机发送非应答,过程中会将SDA拉低),然后从机释放SDA线(防止SDA拉低以后主机无法将其拉高),从而允许主机产生终止信号(主机接收数据时)(发送应答)其次,主机在发送应答后,也要释放SDA线,防止将其拉低后影响从机后续的数据传送

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

具体的组合形式

1、主机发送数据

2、主机接收数据

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3、主机先发送数据在接收数据

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在I2C通信中,主机先发送数据再接收数据的情况通常用于读操作。在I2C协议中,读写的方向是通过通信的起始阶段确定的。如果主机希望从从机(设备)中读取数据,它首先会发送一个包含从机地址和读方向的起始帧,然后在之后的通信中,主机将发送时钟脉冲并接收从机发送的数据。

具体的步骤如下:

  1. 起始帧: 主机发送起始帧,包括从机地址和读方向的位(通常是最低有效位设置为1)。
  2. 从机应答: 从机收到主机的地址后,会发出应答信号。
  3. 主机发送数据: 主机在接收到从机的应答后,会继续发送一个或多个数据字节给从机。
  4. 从机应答: 每发送一个数据字节后,从机都会发出应答信号,表示已成功接收。
  5. 切换方向: 在发送完要读取的数据后,主机发出停止帧,然后重新发起一个起始帧,但这次将方向位设为读。
  6. 从机发送数据: 从机在接收到主机的读方向后,会发送数据给主机。
  7. 主机应答/非应答: 主机在接收到从机的每个数据字节后,可以选择发出应答信号或非应答信号。如果主机希望继续接收数据,它发送应答信号;如果主机不想再接收数据,它发送非应答信号,并通常跟随着停止帧。

这种先发送数据再接收数据的顺序用于读取从机的数据。


IIC软件模拟

个人认为下面这种IIC软件模拟的方式最好!!


/*引脚配置层*/

/**
  * 函    数:I2C写SCL引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
  */
void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);		//根据BitValue,设置SCL引脚的电平
	Delay_us(10);												//延时10us,防止时序频率超过要求
}

/**
  * 函    数:I2C写SDA引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SDA的电平,范围0~0xFF
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SDA为低电平,当BitValue非0时,需要置SDA为高电平
  */
void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);		//根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
	Delay_us(10);												//延时10us,防止时序频率超过要求
}

/**
  * 函    数:I2C读SDA引脚电平
  * 参    数:无
  * 返 回 值:协议层需要得到的当前SDA的电平,范围0~1
  * 注意事项:此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1
  */
uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);		//读取SDA电平
	Delay_us(10);												//延时10us,防止时序频率超过要求
	return BitValue;											//返回SDA电平
}

/**
  * 函    数:I2C初始化
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,实现SCL和SDA引脚的初始化
  */
void MyI2C_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	//开启GPIOB的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);					//将PB10和PB11引脚初始化为开漏输出
	
	/*设置默认电平*/
	GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);			//设置PB10和PB11引脚初始化后默认为高电平(释放总线状态)
}

/*协议层*/

/**
  * 函    数:I2C起始
  * 参    数:无
  * 返 回 值:无
  */
void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);							//释放SDA,确保SDA为高电平
	MyI2C_W_SCL(1);							//释放SCL,确保SCL为高电平
	MyI2C_W_SDA(0);							//在SCL高电平期间,拉低SDA,产生起始信号
	MyI2C_W_SCL(0);							//起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}

/**
  * 函    数:I2C终止
  * 参    数:无
  * 返 回 值:无
  */
void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);							//拉低SDA,确保SDA为低电平
	MyI2C_W_SCL(1);							//释放SCL,使SCL呈现高电平
	MyI2C_W_SDA(1);							//在SCL高电平期间,释放SDA,产生终止信号
}

/**
  * 函    数:I2C发送一个字节
  * 参    数:Byte 要发送的一个字节数据,范围:0x00~0xFF
  * 返 回 值:无
  */
void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i ++)				//循环8次,主机依次发送数据的每一位
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));	//使用掩码的方式取出Byte的指定一位数据并写入到SDA线
		MyI2C_W_SCL(1);						//释放SCL,从机在SCL高电平期间读取SDA
		MyI2C_W_SCL(0);						//拉低SCL,主机开始发送下一位数据
	}
}

/**
  * 函    数:I2C接收一个字节
  * 参    数:无
  * 返 回 值:接收到的一个字节数据,范围:0x00~0xFF
  */
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;					//定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
	MyI2C_W_SDA(1);							//接收前,主机先确保释放SDA,避免干扰从机的数据发送
	for (i = 0; i < 8; i ++)				//循环8次,主机依次接收数据的每一位
	{
		MyI2C_W_SCL(1);						//释放SCL,主机机在SCL高电平期间读取SDA
		if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}	//读取SDA数据,并存储到Byte变量
														//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0
		MyI2C_W_SCL(0);						//拉低SCL,从机在SCL低电平期间写入SDA
	}
	return Byte;							//返回接收到的一个字节数据
}

/**
  * 函    数:I2C发送应答位
  * 参    数:Byte 要发送的应答位,范围:0~1,0表示应答,1表示非应答
  * 返 回 值:无
  */
void MyI2C_SendAck(uint8_t AckBit)
{
	MyI2C_W_SDA(AckBit);					//主机把应答位数据放到SDA线
	MyI2C_W_SCL(1);							//释放SCL,从机在SCL高电平期间,读取应答位
	MyI2C_W_SCL(0);							//拉低SCL,开始下一个时序模块
}

/**
  * 函    数:I2C接收应答位
  * 参    数:无
  * 返 回 值:接收到的应答位,范围:0~1,0表示应答,1表示非应答
  */
uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;							//定义应答位变量
	MyI2C_W_SDA(1);							//接收前,主机先确保释放SDA,避免干扰从机的数据发送
	MyI2C_W_SCL(1);							//释放SCL,主机机在SCL高电平期间读取SDA
	AckBit = MyI2C_R_SDA();					//将应答位存储到变量里
	MyI2C_W_SCL(0);							//拉低SCL,开始下一个时序模块
	return AckBit;							//返回定义应答位变量
}

IIC模块(硬件IIC)

所谓硬件I2C对应芯片上的I2C外设,有相应I2C驱动电路,其所使用的I2C管脚也是专用的;软件I2C一般是用GPIO管脚,用软件控制管脚状态以模拟I2C通讯波形。

硬件I2C的效率要远高于软件的,而软件I2C由于不受管脚限制,接口比较灵活。

模拟I2C 是通过GPIO,软件模拟寄存器的工作方式,而硬件(固件)I2C是直接调用内部寄存器进行配置。如果要从具体硬件上来看,可以去看下芯片手册。因为固件I2C的端口是固定的,所以会有所区别。

至于如何区分它们:

可以看底层配置,比如IO口配置,如果配置了IO口的功能(IIC功能)那就是固件IIC,否则就是模拟
可以看IIC写函数,看里面有木有调用现成的函数或者给某个寄存器赋值,如果有,则肯定是固件IIC功能,没有的话肯定是数据一个bit一个bit模拟发生送的,肯定用到了循环,则为模拟。
根据代码量判断,模拟的代码量肯定比固件的要大。

  1. 硬件IIC用法比较复杂,模拟IIC的流程更清楚一些。
  2. 硬件IIC速度比模拟快,并且可以用DMA
  3. 模拟IIC可以在任何管脚上,而硬件只能在固定管脚上。

软件i2c是程序员使用程序控制SCL,SDA线输出高低电平,模拟i2c协议的时序。一般较硬件i2c稳定,但是程序较为繁琐,但不难。

硬件i2c程序员只要调用i2c的控制函数即可,不用直接的去控制SCL,SDA高低电平的输出。但是有些单片机的硬件i2c不太稳定,调试问题较多

硬件IIC也有优点:

硬件IIC的实现相对简单,无需编写复杂的代码;速度快,可以实现高速通信;实现简单,无需编写复杂的代码;稳定性好,不容易出现通信错误。

实现硬件IIC通讯

实现硬件IIC的代码需要使用STM32的内部硬件模块,具体步骤如下:配置GPIO用于IIC通讯,将SCL和SDA引脚分别配置为复用推挽输出模式;配置I2C控制器,包括I2C时钟频率,I2C地址,I2C工作模式等参数;启动I2C控制器,并发送数据或接收数据。


STM32F4XX IIC固件库的使用

(1) 初始化IIC引脚:GPIO复用
		RCC_xxxxx : 使能时钟
		GPIO_Init :   初始化GPIO口  
		GPIO_PinAFConfig  : 配置GPIO的复用功能
		
	(2) 初始化I2C控制器
		RCC_xxxx  :使能I2C时钟
		I2C_Init  :配置I2C
		void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);
			I2Cx: 指定要初始的I2C控制器,如: I2C1, I2C2,...
			I2C_InitStruct: 指向初始化结构体,其成员变量如下:
				typedef struct
				{
				  uint32_t I2C_ClockSpeed;          
					指定I2C总线的时钟频率,如:
						100000   //100K

				  uint16_t I2C_Mode;               
					指定I2C协议还是SMBUS协议	
						I2C_Mode_I2C    : 主模式
						I2C_Mode_SMBusDevice  : 从模式
						I2C_Mode_SMBusHost
				  在主模式下,你可以作为主设备发起I2C通信。在从模式下,你通常是一个I2C外设。

				  uint16_t I2C_DutyCycle;          
					当i2C是fast mode,指定 Tlow: Thigh
						I2C_DutyCycle_16_9 
						I2C_DutyCycle_2  : 通常选择这个
				  

				  uint16_t I2C_OwnAddress1;         
					指定i2c控制器自身的地址,随你定,只要不跟同一i2c总线上的
					其他设备相冲突就可以啦。
					
					什么时候需要指定i2c设备地址呢?
						当作为从设备时,需要I2C设备地址。
					
				  
				  
				  uint16_t I2C_Ack;                 
						i2c控制器收到数据时,是否应答。
						I2C_Ack_Enable 
						I2C_Ack_Disable
				  

				  uint16_t I2C_AcknowledgedAddress; 
						指定应答地址是7bits还是10bits
						I2C_AcknowledgedAddress_7bit
						I2C_AcknowledgedAddress_10bit
				  
				  
				}I2C_InitTypeDef;


	(3) 配置I2C控制器的一些功能(如: 是否应答,中断使能,...)
	
	
	(4) 开启I2C控制器
		I2C_Cmd
		

初始化IIC

void I2C_Configuration(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    I2C_InitTypeDef I2C_InitStructure;

    // 使能GPIOB时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);

    // 配置PB8和PB9为复用模式
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    // 配置PB8和PB9的复用功能为I2C
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource8, GPIO_AF_I2C1);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource9, GPIO_AF_I2C1);

    // 使能I2C1时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
    
    // 配置I2C速率
    I2C_InitStructure.I2C_ClockSpeed = 100000; // I2C时钟速率为100kHz
    I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
    I2C_InitStructure.I2C_OwnAddress1 = 0x00;
    I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
    I2C_Init(I2C1, &I2C_InitStructure);

    // 使能I2C1
    I2C_Cmd(I2C1, ENABLE);
}

写一个AT24C02的驱动层,为上提供操作AT24C02的接口函数

#include "stm32f4xx.h"
#include "stm32f4xx_i2c.h"

#define EEPROM_ADDR 0xA0 // AT24C02的I2C地址

void I2C_Configuration(void);
void EEPROM_WriteByte(uint16_t addr, uint8_t data);
uint8_t EEPROM_ReadByte(uint16_t addr);
void EEPROM_WritePage(uint16_t addr, uint8_t *data, uint8_t length);
void EEPROM_ReadPage(uint16_t addr, uint8_t *data, uint8_t length);

int main(void)
{
    // 初始化系统时钟等
    SystemInit();

    // 配置I2C
    I2C_Configuration();

    // 在这里使用EEPROM的接口函数进行操作
    uint16_t address = 0x0000;
    uint8_t data_to_write = 0xAA;

    EEPROM_WriteByte(address, data_to_write);

    uint8_t data_read = EEPROM_ReadByte(address);

    while (1)
    {
        // 在这里编写你的主程序逻辑
    }
}

void I2C_Configuration(void)
{
    // I2C初始化代码
}

void EEPROM_WriteByte(uint8_t addr, uint8_t data)
{
    while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
    //I2C_FLAG_BUSY是一个标志位,当它为1时,表示I2C线程正在忙中。
    //因此,这个while循环会一直等待,直到I2C_FLAG_BUSY变成0,即I2C中断不再停止。

    // 生成START条件
    I2C_GenerateSTART(I2C1, ENABLE);

    // 等待START条件生成
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    // 发送EEPROM的I2C地址,选择写模式
    I2C_Send7bitAddress(I2C1, EEPROM_ADDR, I2C_Direction_Transmitter);

    // 等待地址发送完成
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

    // 发送要写入的地址
    I2C_SendData(I2C1, addr); 
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));


    // 发送要写入的数据
    I2C_SendData(I2C1, data);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    // 生成STOP条件
    I2C_GenerateSTOP(I2C1, ENABLE);
}

uint8_t EEPROM_ReadByte(uint8_t addr)
{
    uint8_t data;

    while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY))
        ;

    // 生成START条件
    I2C_GenerateSTART(I2C1, ENABLE);

    // 等待START条件生成
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    // 发送EEPROM的I2C地址,选择写模式
    I2C_Send7bitAddress(I2C1, EEPROM_ADDR, I2C_Direction_Transmitter);

    // 等待地址发送完成
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

        // 发送要写入的地址
    I2C_SendData(I2C1, addr);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    // 生成RESTART条件
    I2C_GenerateSTART(I2C1, ENABLE);

    // 等待RESTART条件生成
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    // 发送EEPROM的I2C地址,选择读模式
    I2C_Send7bitAddress(I2C1, EEPROM_ADDR, I2C_Direction_Receiver);

    // 等待地址发送完成
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));

    // 等待数据接收
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
    data = I2C_ReceiveData(I2C1);

    // 生成STOP条件
    I2C_GenerateSTOP(I2C1, ENABLE);

    return data;
}

void EEPROM_WritePage(uint16_t addr, uint8_t *data, uint8_t length)
{
    // 在这里实现写一页数据的操作
}

void EEPROM_ReadPage(uint16_t addr, uint8_t *data, uint8_t length)
{
    // 在这里实现读一页数据的操作
}

在I2C通信中,主机先发送数据再接收数据的情况通常用于读操作。在I2C协议中,读写的方向是通过通信的起始阶段确定的。如果主机希望从从机(设备)中读取数据,它首先会发送一个包含从机地址和读方向的起始帧,然后在之后的通信中,主机将发送时钟脉冲并接收从机发送的数据。

具体的步骤如下:

  1. 起始帧: 主机发送起始帧,包括从机地址和读方向的位(通常是最低有效位设置为1)。
  2. 从机应答: 从机收到主机的地址后,会发出应答信号。
  3. 主机发送数据: 主机在接收到从机的应答后,会继续发送一个或多个数据字节给从机。
  4. 从机应答: 每发送一个数据字节后,从机都会发出应答信号,表示已成功接收。
  5. 切换方向: 在发送完要读取的数据后,主机发出停止帧,然后重新发起一个起始帧,但这次将方向位设为读。
  6. 从机发送数据: 从机在接收到主机的读方向后,会发送数据给主机。
  7. 主机应答/非应答: 主机在接收到从机的每个数据字节后,可以选择发出应答信号或非应答信号。如果主机希望继续接收数据,它发送应答信号;如果主机不想再接收数据,它发送非应答信号,并通常跟随着停止帧。

这种先发送数据再接收数据的顺序用于读取从机的数据。


IIC软件模拟

个人认为下面这种IIC软件模拟的方式最好!!


/*引脚配置层*/

/**
  * 函    数:I2C写SCL引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
  */
void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);		//根据BitValue,设置SCL引脚的电平
	Delay_us(10);												//延时10us,防止时序频率超过要求
}

/**
  * 函    数:I2C写SDA引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SDA的电平,范围0~0xFF
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SDA为低电平,当BitValue非0时,需要置SDA为高电平
  */
void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);		//根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
	Delay_us(10);												//延时10us,防止时序频率超过要求
}

/**
  * 函    数:I2C读SDA引脚电平
  * 参    数:无
  * 返 回 值:协议层需要得到的当前SDA的电平,范围0~1
  * 注意事项:此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1
  */
uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);		//读取SDA电平
	Delay_us(10);												//延时10us,防止时序频率超过要求
	return BitValue;											//返回SDA电平
}

/**
  * 函    数:I2C初始化
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,实现SCL和SDA引脚的初始化
  */
void MyI2C_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	//开启GPIOB的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);					//将PB10和PB11引脚初始化为开漏输出
	
	/*设置默认电平*/
	GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);			//设置PB10和PB11引脚初始化后默认为高电平(释放总线状态)
}

/*协议层*/

/**
  * 函    数:I2C起始
  * 参    数:无
  * 返 回 值:无
  */
void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);							//释放SDA,确保SDA为高电平
	MyI2C_W_SCL(1);							//释放SCL,确保SCL为高电平
	MyI2C_W_SDA(0);							//在SCL高电平期间,拉低SDA,产生起始信号
	MyI2C_W_SCL(0);							//起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}

/**
  * 函    数:I2C终止
  * 参    数:无
  * 返 回 值:无
  */
void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);							//拉低SDA,确保SDA为低电平
	MyI2C_W_SCL(1);							//释放SCL,使SCL呈现高电平
	MyI2C_W_SDA(1);							//在SCL高电平期间,释放SDA,产生终止信号
}

/**
  * 函    数:I2C发送一个字节
  * 参    数:Byte 要发送的一个字节数据,范围:0x00~0xFF
  * 返 回 值:无
  */
void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i ++)				//循环8次,主机依次发送数据的每一位
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));	//使用掩码的方式取出Byte的指定一位数据并写入到SDA线
		MyI2C_W_SCL(1);						//释放SCL,从机在SCL高电平期间读取SDA
		MyI2C_W_SCL(0);						//拉低SCL,主机开始发送下一位数据
	}
}

/**
  * 函    数:I2C接收一个字节
  * 参    数:无
  * 返 回 值:接收到的一个字节数据,范围:0x00~0xFF
  */
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;					//定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
	MyI2C_W_SDA(1);							//接收前,主机先确保释放SDA,避免干扰从机的数据发送
	for (i = 0; i < 8; i ++)				//循环8次,主机依次接收数据的每一位
	{
		MyI2C_W_SCL(1);						//释放SCL,主机机在SCL高电平期间读取SDA
		if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}	//读取SDA数据,并存储到Byte变量
														//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0
		MyI2C_W_SCL(0);						//拉低SCL,从机在SCL低电平期间写入SDA
	}
	return Byte;							//返回接收到的一个字节数据
}

/**
  * 函    数:I2C发送应答位
  * 参    数:Byte 要发送的应答位,范围:0~1,0表示应答,1表示非应答
  * 返 回 值:无
  */
void MyI2C_SendAck(uint8_t AckBit)
{
	MyI2C_W_SDA(AckBit);					//主机把应答位数据放到SDA线
	MyI2C_W_SCL(1);							//释放SCL,从机在SCL高电平期间,读取应答位
	MyI2C_W_SCL(0);							//拉低SCL,开始下一个时序模块
}

/**
  * 函    数:I2C接收应答位
  * 参    数:无
  * 返 回 值:接收到的应答位,范围:0~1,0表示应答,1表示非应答
  */
uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;							//定义应答位变量
	MyI2C_W_SDA(1);							//接收前,主机先确保释放SDA,避免干扰从机的数据发送
	MyI2C_W_SCL(1);							//释放SCL,主机机在SCL高电平期间读取SDA
	AckBit = MyI2C_R_SDA();					//将应答位存储到变量里
	MyI2C_W_SCL(0);							//拉低SCL,开始下一个时序模块
	return AckBit;							//返回定义应答位变量
}

IIC模块(硬件IIC)

所谓硬件I2C对应芯片上的I2C外设,有相应I2C驱动电路,其所使用的I2C管脚也是专用的;软件I2C一般是用GPIO管脚,用软件控制管脚状态以模拟I2C通讯波形。

硬件I2C的效率要远高于软件的,而软件I2C由于不受管脚限制,接口比较灵活。

模拟I2C 是通过GPIO,软件模拟寄存器的工作方式,而硬件(固件)I2C是直接调用内部寄存器进行配置。如果要从具体硬件上来看,可以去看下芯片手册。因为固件I2C的端口是固定的,所以会有所区别。

至于如何区分它们:

可以看底层配置,比如IO口配置,如果配置了IO口的功能(IIC功能)那就是固件IIC,否则就是模拟
可以看IIC写函数,看里面有木有调用现成的函数或者给某个寄存器赋值,如果有,则肯定是固件IIC功能,没有的话肯定是数据一个bit一个bit模拟发生送的,肯定用到了循环,则为模拟。
根据代码量判断,模拟的代码量肯定比固件的要大。

  1. 硬件IIC用法比较复杂,模拟IIC的流程更清楚一些。
  2. 硬件IIC速度比模拟快,并且可以用DMA
  3. 模拟IIC可以在任何管脚上,而硬件只能在固定管脚上。

软件i2c是程序员使用程序控制SCL,SDA线输出高低电平,模拟i2c协议的时序。一般较硬件i2c稳定,但是程序较为繁琐,但不难。

硬件i2c程序员只要调用i2c的控制函数即可,不用直接的去控制SCL,SDA高低电平的输出。但是有些单片机的硬件i2c不太稳定,调试问题较多

硬件IIC也有优点:

硬件IIC的实现相对简单,无需编写复杂的代码;速度快,可以实现高速通信;实现简单,无需编写复杂的代码;稳定性好,不容易出现通信错误。

实现硬件IIC通讯

实现硬件IIC的代码需要使用STM32的内部硬件模块,具体步骤如下:配置GPIO用于IIC通讯,将SCL和SDA引脚分别配置为复用推挽输出模式;配置I2C控制器,包括I2C时钟频率,I2C地址,I2C工作模式等参数;启动I2C控制器,并发送数据或接收数据。


STM32F4XX IIC固件库的使用

(1) 初始化IIC引脚:GPIO复用
		RCC_xxxxx : 使能时钟
		GPIO_Init :   初始化GPIO口  
		GPIO_PinAFConfig  : 配置GPIO的复用功能
		
	(2) 初始化I2C控制器
		RCC_xxxx  :使能I2C时钟
		I2C_Init  :配置I2C
		void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);
			I2Cx: 指定要初始的I2C控制器,如: I2C1, I2C2,...
			I2C_InitStruct: 指向初始化结构体,其成员变量如下:
				typedef struct
				{
				  uint32_t I2C_ClockSpeed;          
					指定I2C总线的时钟频率,如:
						100000   //100K

				  uint16_t I2C_Mode;               
					指定I2C协议还是SMBUS协议	
						I2C_Mode_I2C    : 主模式
						I2C_Mode_SMBusDevice  : 从模式
						I2C_Mode_SMBusHost
				  在主模式下,你可以作为主设备发起I2C通信。在从模式下,你通常是一个I2C外设。

				  uint16_t I2C_DutyCycle;          
					当i2C是fast mode,指定 Tlow: Thigh
						I2C_DutyCycle_16_9 
						I2C_DutyCycle_2  : 通常选择这个
				  

				  uint16_t I2C_OwnAddress1;         
					指定i2c控制器自身的地址,随你定,只要不跟同一i2c总线上的
					其他设备相冲突就可以啦。
					
					什么时候需要指定i2c设备地址呢?
						当作为从设备时,需要I2C设备地址。
					
				  
				  
				  uint16_t I2C_Ack;                 
						i2c控制器收到数据时,是否应答。
						I2C_Ack_Enable 
						I2C_Ack_Disable
				  

				  uint16_t I2C_AcknowledgedAddress; 
						指定应答地址是7bits还是10bits
						I2C_AcknowledgedAddress_7bit
						I2C_AcknowledgedAddress_10bit
				  
				  
				}I2C_InitTypeDef;


	(3) 配置I2C控制器的一些功能(如: 是否应答,中断使能,...)
	
	
	(4) 开启I2C控制器
		I2C_Cmd
		

初始化IIC

void I2C_Configuration(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    I2C_InitTypeDef I2C_InitStructure;

    // 使能GPIOB时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);

    // 配置PB8和PB9为复用模式
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    // 配置PB8和PB9的复用功能为I2C
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource8, GPIO_AF_I2C1);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource9, GPIO_AF_I2C1);

    // 使能I2C1时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
    
    // 配置I2C速率
    I2C_InitStructure.I2C_ClockSpeed = 100000; // I2C时钟速率为100kHz
    I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
    I2C_InitStructure.I2C_OwnAddress1 = 0x00;
    I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
    I2C_Init(I2C1, &I2C_InitStructure);

    // 使能I2C1
    I2C_Cmd(I2C1, ENABLE);
}

写一个AT24C02的驱动层,为上提供操作AT24C02的接口函数

#include "stm32f4xx.h"
#include "stm32f4xx_i2c.h"

#define EEPROM_ADDR 0xA0 // AT24C02的I2C地址

void I2C_Configuration(void);
void EEPROM_WriteByte(uint16_t addr, uint8_t data);
uint8_t EEPROM_ReadByte(uint16_t addr);
void EEPROM_WritePage(uint16_t addr, uint8_t *data, uint8_t length);
void EEPROM_ReadPage(uint16_t addr, uint8_t *data, uint8_t length);

int main(void)
{
    // 初始化系统时钟等
    SystemInit();

    // 配置I2C
    I2C_Configuration();

    // 在这里使用EEPROM的接口函数进行操作
    uint16_t address = 0x0000;
    uint8_t data_to_write = 0xAA;

    EEPROM_WriteByte(address, data_to_write);

    uint8_t data_read = EEPROM_ReadByte(address);

    while (1)
    {
        // 在这里编写你的主程序逻辑
    }
}

void I2C_Configuration(void)
{
    // I2C初始化代码
}

void EEPROM_WriteByte(uint8_t addr, uint8_t data)
{
    while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
    //I2C_FLAG_BUSY是一个标志位,当它为1时,表示I2C线程正在忙中。
    //因此,这个while循环会一直等待,直到I2C_FLAG_BUSY变成0,即I2C中断不再停止。

    // 生成START条件
    I2C_GenerateSTART(I2C1, ENABLE);

    // 等待START条件生成
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    // 发送EEPROM的I2C地址,选择写模式
    I2C_Send7bitAddress(I2C1, EEPROM_ADDR, I2C_Direction_Transmitter);

    // 等待地址发送完成
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

    // 发送要写入的地址
    I2C_SendData(I2C1, addr); 
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));


    // 发送要写入的数据
    I2C_SendData(I2C1, data);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    // 生成STOP条件
    I2C_GenerateSTOP(I2C1, ENABLE);
}

uint8_t EEPROM_ReadByte(uint8_t addr)
{
    uint8_t data;

    while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY))
        ;

    // 生成START条件
    I2C_GenerateSTART(I2C1, ENABLE);

    // 等待START条件生成
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    // 发送EEPROM的I2C地址,选择写模式
    I2C_Send7bitAddress(I2C1, EEPROM_ADDR, I2C_Direction_Transmitter);

    // 等待地址发送完成
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

        // 发送要写入的地址
    I2C_SendData(I2C1, addr);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    // 生成RESTART条件
    I2C_GenerateSTART(I2C1, ENABLE);

    // 等待RESTART条件生成
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    // 发送EEPROM的I2C地址,选择读模式
    I2C_Send7bitAddress(I2C1, EEPROM_ADDR, I2C_Direction_Receiver);

    // 等待地址发送完成
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));

    // 等待数据接收
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
    data = I2C_ReceiveData(I2C1);

    // 生成STOP条件
    I2C_GenerateSTOP(I2C1, ENABLE);

    return data;
}

void EEPROM_WritePage(uint16_t addr, uint8_t *data, uint8_t length)
{
    // 在这里实现写一页数据的操作
}

void EEPROM_ReadPage(uint16_t addr, uint8_t *data, uint8_t length)
{
    // 在这里实现读一页数据的操作
}

在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要配置IIC,你可以按照以下步骤进行操作: 1. 打开CubeMX并创建一个新的工程。 2. 在左上角输入你的芯片型号(比如STM32F103ZET6)。 3. 在出现的结果中,点击具体的芯片型号,进入mcu硬件资源配置的界面。 4. 在资源配置界面的第一部分(Pinout & Configuration)中,找到IIC(或者叫做I2C)的配置选项。根据你的需求,选择要使用的引脚和功能,并进行相应的配置。 5. 在时钟配置(Clock Configuration)中,设置适当的时钟频率和分频系数,以满足IIC通信的要求。 6. 完成配置后,生成代码并导入到你选择的IDE中进行开发。 以上是通过CubeMX进行IIC配置的一般步骤。具体的操作细节可能会根据不同的芯片型号和CubeMX版本有所差异。你可以参考引用和引用中提供的具体信息来进行配置。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [STM32CubeMX学习笔记5——I2C配置(OLED显示)](https://blog.csdn.net/qq_42967008/article/details/95675740)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [使用STM32CubeMX创建STM32工程(串口,硬件IIC配置)](https://blog.csdn.net/weixin_42887621/article/details/128087643)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值