充分了解STM32F407硬件\内部I2C(主模式)

此篇文章将带领你理解并学习STM32F4硬件\内部的I2C主模式去驱动slave I2C设备

基础知识

I2C模式

i2c一般有四个模式:
    ● 从发送器–|一般用于利用i2c进行设备通信
    ● 从接收器–|
    ● 主发送器------|
    ● 主接收器------|一般用于驱动一个i2c设备(传感器类)

I2C通信流程

在主模式下, I2C 接口会启动数据传输并生成时钟信号。串行数据传输始终是在出现起始位时开始,在出现停止位时结束。起始位和停止位均在主模式下由软件生成。
在从模式下,该接口能够识别其自身地址( 7 或 10 位)以及广播呼叫地址。广播呼叫地址检测可由软件使能或禁止。
通信数据格式为数据和地址均以 8 位字节传输, MSB 在前。起始位后紧随地址字节(7 位地址占据一个字节;10 位地址占据两个字节)。地址始终在主模式下传送
在字节传输 8 个时钟周期后是第 9 个时钟脉冲,在此期间接收器必须向发送器发送一个应答位

总结一下就是:在通信时在主设备发送起始信号过后,串行数据开始传输,在第九个时钟时从设备要向主设备发送一个应答位。在串行数据传输时均是MSB在前

I2C地址

对于I2C地址很多人都分不清,i2c的地址组成一般是7+1,其中最后1位为读写位(一般0为写、1为读),注意这里7位很重要,一般数据手册中给出的都是这7位地址,换句话说也就是7前7位表示8位的一个数值。举个例子:mpu6050陀螺仪模块在数据手册中说7位设备地址为0X68(AD0接GND时,AD0接VCC时为0X68)。可能会有很多人这样说mpu的写地址为0X68、读地址为0X69,就是改最后一位嘛,0就是写、1就是读。很显然这是错的,因为它指的是7位地址,不加最后一位。换成二进制解释0X68地址: 110 1000读写位,这里的0x68指不加读写位的1101000,那么实际对mpu写操作的地址是1101 0000 即0xD0,读地址为0xD1。同理对于0X69时也一样
总结一下:关于i2c地址,一定要看清数据手册所给的是7位地址(不包含读写位的,需要在最后加上一位表示读写位)还是已经包含了读写位的地址(如果包含一般给出的是写地址即+1就是读地址)

I2C起始位与停止位

在i2c工作中,有效数据即SDA只允许在SCLK低电平期间变化,在SCLK高电平期间不允许改变。所以,对于两类特殊的命令信号都是在SCLK在高电平期间SDA发生改变。
起始位如下图:在SCLK高电平期间SDA由高到低变化将被理解为起始位。
i2c起始位
停止位如下图:在SCLK高电平期间SDA由低到高变化将被理解为起始位。
i2c停止位

使用STM32F407系列硬件/内部I2C

首先我们先看主发送

单字节发送

对于内部外设的开发,我建议我们要习惯看开发手册,这点很重要。下面截取自开发手册中的一个图:
主发送框图
看到上图我们很好理解想要发送数据一般步骤为:
生成起始位->等待EV5事件->发送设备地址(写地址)->等待EV6事件->发送数据->等待EV8事件->…->等待EV8_2事件->发送停止位。那么转化为流程图就是下图:
在这里插入图片描述
注意:在主发送时,注意停止位发送时刻,数据手册上对于主发送数据的结束通信有这么一句话
在这里插入图片描述
对于这句话我个人觉得可能会误导我们在发送最后一个数据时,在写DR后就发送停止位,这样理解是错误的,对于这样的不好理解的我们可以直接看英文原版数据手册可能有所帮助,如下图对应英文原版:
在这里插入图片描述
看注意我们就可以知道停止位应当在EV8_2期间产生,也就是说在写DR后要等待一个TxE或者是BTF时其后才可以生成停止位,而非写DR后就生成停止位总结一点就是对于主发送我们可以直接按照上述流程进行,无需多虑。

多字节发送

对于多字节发送,完全按照上述流程走即可。

然后我们看主接收

对于主接收可能有点麻烦,一般而言对于主I2C接收操作会有一个“伪写”操作,所谓的“伪写”就是先告诉我将要操作i2c设备中的哪个内部寄存器或者说哪一部分,即定位过程。不明白的的话我们可以看两个例子:
例1,AT24C256 EEPROM的随机读操作时序见下图,可以看到随机读写需要一个DUMMY Write。
AT24C256 EEPROM的随机读操作时序
例2,MPU6050陀螺仪在读过程中也是需要一个“伪写”过程,如下图
MPU6050陀螺仪在读过程
从上述两个设备来看,想要读取时都需要一个定位过程,换句话说也就是先告诉设备我准备读取你的哪个寄存器或者哪一部分。此时我们再看手册中关于主I2C读取框图(流程)如下图:
主接收框图
同过上面的流程我们可以很明显的看出,此读取序列流程,并没有“伪写”过程,因此真正的主接收流程图应该是主发送部分加主接收这个序列图,即下图:
主读取全部过程
当然了在第一部分的写过程,不仅仅可以写一个数据可以写多个,根据不同设备确定,和真正的写唯一区别就是不用生成停止位仅此而已。有了以上了解我们开始进行代码段设计,因为涉及到硬件规则缘故以及效率问题,我们在读过程中,一般分为单字节读取、两字节读取以及多字节读取,之所以区分这三个是因为它们结束通信方式是不一样的,以下我将分别讲述这三个操作过程中结束通信方式以及注意点。

单字节读取

在单字节读取过程中,结束通信过程如下:应当在EV6期间(图244里的)在 ADDR 标志清零之前,禁止应答并在EV6 之后生成停止位。有细心的同志,就可以发现如果我们使用库函数开发,在调用I2C_CheckEvent函数检查事件时,查完后就会清除ADDR,那么是不是不行啊!针对这个问题我测试了,如果按这样理解就是在I2C_Send7bitAddress函数后就关闭自动应答,这种操作有时会带来问题,为了安全起见,我做法是等待EV6事件后在生成停止条件,毕竟中间时间也想差不了多少。所以如果在规定时间内有EV6事件,就先执行关闭自动应答,在生成停止位,最后等待EV7事件后读取一个字节的数据

两字节读取

在手册中对于两字节读取建议操作步骤如下图:
两字节读取
那么我们就直接按照这个图来理解。这里涉及到一个POS位,先了解下POS位,
POS位
在我们理解中当关闭自动应答过后,当接收到一个字节后就不应答了,言外之意就是值可以接收到一个字节数据,想要接受两个字节数据就必须在最后一个字节时关闭自动相应,有人可能就会有这个想法我直接在单字节读取过程中,在第一个字节接收后再生成停止位不可以吗,不可以会丢失第二个数据,这里涉及到一个关键点就是移位寄存器,对于移位寄存器在多字节接收会有更好的理解,这里接单理解下就是,当我们接收>1个字节的数据时,当检测到EV7事件时(此前未生成停止条件),第一个数据在DR中第二个数据已经在与移位寄存器中,如果在读DR后生成停止条件的话移位寄存器中的值面临丢失或者说总线错误的问题,为解决此问题便有了POS位,对于POS位我可以简单理解成,在接收到第一个数据时即使ACK为0也不产生NACK,只有在接收第二个字节后在生成NACK,由硬件帮我们决定在接收第二个字节后给与NACK,保证时序准确定。
这里有个注意点:按照图中第四步在等待了BTF标志位后,我们不能在读取第一个字节后立马读第二个字节,因为第二个字节还在移位寄存器中,我们需要等待一个时钟周期在读取就可以了,也就是在读取两个字节间随便加一个操作就可以了,比如赋值语句;当然了也可以监测RxNE标志。

多字节读取

下面到了最后一个,多字节读取,也就是至少接收三个数据。在理解这个后你将会对移位寄存器有个很好的理解。
多字节接收
同理中间出来一个BTF我们先查看下对此位的描述,
BTF位
结合两者我相信应该都可以明白如何写对应的代码了。这里在最后一步读取第N-1数据和第N数据时注意下留一个时钟周期或者检测RxNE操作,理由在于给与移位寄存器数据到DR中一个时间间隔。

总结:关于上述读取,尤其看了第三个关于>2字节读取时,进一步明白了移位寄存器后,都会对两字节接收存在疑问,按照同样的思路设计2字节读取的就是了。我也有疑问,我也想直接测试下,奈何代码都写好了,懒得动了。又想了想,既然手册上都区分,我们就按照他们规定的来,以免出现不必要的错误,毕竟这是硬件帮我们产生的时序,不按照规定的来最终出现啥问题也是不得而知的。不过有兴趣的可以都测试下看看,这样对于进一步理解这个I2C肯定会有很大的帮助。

代码设计

通过上面的讲述可能还比较迷,那么直接结合代码看比较好。
这里会有个getI2C1TimeLimit函数,主要用来等数据时的一个最大时间限制,以免程序卡死,此函数是获取一个延时函数的数值,可以根据自身的判断设置。

/**
* @brief: 获取i2c超时值获取-时间大约值不一定准确
* @args:None
* @return:超时值(us)

* @add:原理1、是一般i2c最迟响应时间在10ms内。
*		   2、一个while(--i)语句时间大约是0.3us。
*	   在使用过程中先获取此值,然后直接用while(--i),就是i*0.3 us了。
*/
static uint32_t getI2C1TimeLimit(void)
{
   
	RCC_ClocksTypeDef RCC_Clocks;
	RCC_GetClocksFreq(&RCC_Clocks);
	return RCC_Clocks.SYSCLK_Frequency/10000;//168000000/10000=16800 5.40ms
}

写数据

写字节代码

/**
* @brief: 利用硬件i2c1向目标寄存器写入一个数据
* @args:三个参数:寄存器地址,寄存器地址是否是16位,需要写入的数据
*		@arg1:目标设备地址(写地址就好了)
*		@arg2:目标寄存器地址
*		@arg3:寄存器地址是否是16位的
*		@arg4:一个字节数据
* @return:
*		  >0 成功
*		  <0 失败
*		  	-1 i2c busy
*		  	-2 i2c send start fail
*         	-3 i2c send address failed
*         	-4 i2c high regAdder failed
*         	-5 i2c send low regAdder failed
*         	-6 i2c send data failed
*/
int32_t i2c1WriteByte(uint8_t devAdder, uint16_t regAdder, bool is16BitRegAdder, uint8_t num)
{
   
	uint32_t timeLimit = getI2C1TimeLimit();
	uint32_t tmp 	   = timeLimit;
	
	//--判断当前i2c是否忙
	while((--tmp)&&(I2C_GetFlagStatus(I2C1_NAME, I2C_FLAG_BUSY)));
	if(tmp == 0)
		return -1;//i2c busy
	
	//--生成起始位(SB未清零-I2C_CheckEvent读了SR1,在后续发送地址时I2C_Send7bitAddress写了DR后清除SB位)
	I2C_GenerateSTART(I2C1_NAME, ENABLE);//发送起始位后,自动转换为主设备
	tmp 	   = timeLimit;
	while((--tmp)&&(!I2C_CheckEvent(I2C1_NAME,I2C_EVENT_MASTER_MODE_SELECT)));//EV5事件
	if(tmp == 0)
		return -2;//i2c send start fail
	
	//--发送7位设备地址.调用I2C_CheckEvent会清除EV6事件(先读SR1再读SR2)
	I2C_Send7bitAddress(I2C1_NAME, devAdder, I2C_Direction_Transmitter);//七位地址,非十位地址
	tmp 	   = timeLimit;
	while((--tmp)&&(!I2C_CheckEvent(I2C1_NAME,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)));//EV6事件
	if(tmp == 0)
		return -3;//i2c send address failed
	
	//--发送要写入的目标寄存器
	if(is16BitRegAdder)
	{
   
		I2C_SendData(I2C1_NAME, regAdder>>8);//发送高8位地址
		tmp 	   = timeLimit;
		while((--tmp)&&(!I2C_CheckEvent(I2C1_NAME,I2C_EVENT_MASTER_BYTE_TRANSMITTING)));//EV8事件-写DR清除
		if(tmp == 0)
			return -4;//i2c high regAdder failed
	}
	I2C_SendData(I2C1_NAME, regAdder%256);//发送低8位地址
	tmp 	   = tim
  • 3
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
STM32F407ZET6芯片内置了多个I2C接口,其中I2C1和I2C2可以通过外部引脚进行访问。以下是使用硬件I2C通信的一些基本步骤: 1. 配置GPIO引脚为I2C功能并使能时钟。 2. 配置I2C控制器的时钟频率、I2C地址、传输模式等参数。 3. 初始化I2C控制器并开启I2C总线。 4. 发送起始信号,并发送I2C设备地址和读/写控制位。 5. 发送数据或接收数据,并等待传输完成。 6. 发送停止信号以结束I2C通信。 以下是一个简单的示例代码,实现了通过I2C1接口向设备地址为0x50的EEPROM芯片写入一个字节的数据: ```c #include "stm32f4xx.h" #define I2C_SPEED 100000 // I2C时钟频率 #define EEPROM_ADDRESS 0x50 // EEPROM设备地址 void I2C1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; I2C_InitTypeDef I2C_InitStructure; // 使能GPIOB和I2C1时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); // 配置PB6和PB7为复用功能,并开启开漏输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; 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); // 配置GPIO复用映射 GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_I2C1); GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_I2C1); // 配置I2C控制器 I2C_InitStructure.I2C_ClockSpeed = I2C_SPEED; 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); // 开启I2C总线 I2C_Cmd(I2C1, ENABLE); } void I2C1_WriteByte(uint8_t data) { // 发送起始信号 I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)) {} // 发送设备地址和写控制位 I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) {} // 发送数据 I2C_SendData(I2C1, data); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)) {} // 发送停止信号 I2C_GenerateSTOP(I2C1, ENABLE); } int main(void) { I2C1_Init(); uint8_t data = 0x55; I2C1_WriteByte(data); while (1) {} } ``` 需要注意的是,I2C通信的具体实现可能会因为设备地址、寄存器地址、数据长度、传输模式等参数的不同而有所变化。因此在实际应用中需要根据具体情况进行调整。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值