STM32F407寄存器操作(硬件I2C)

1.前言

虽说都吐槽ST的I2C有问题,但是谁叫ST的片子价格便宜,用处多呢。软件I2C里面有太多延迟了,在平常使用的时候会出问题的,尤其是高速场景下。这两天折腾了一下硬件,网上关于硬件I2C的教程真的少,尤其使用寄存器来操作,我就没找到,最后找到一些hal库的,结合手册总是搞出来了,暂时没出什么硬件bug。

2.初始化

这里以I2C1来介绍了

void init_i2c(void)
{
	RCC->APB1ENR|=1<<21;	//使能时钟
	
	//初始化端口
	GPIOB->MODER|=2<<16;
	GPIOB->MODER|=2<<18;
	
	GPIOB->OSPEEDR|=1<<16;
	GPIOB->OSPEEDR|=1<<18;
	
	//!!!一定要设置成开漏输出模式!!!
	GPIOB->OTYPER|=1<<8;
	GPIOB->OTYPER|=1<<9;
	
	GPIOB->AFR[1]|=4<<0;
	GPIOB->AFR[1]|=4<<4;
	
	//设置I2C主频与通信速率
	I2C1->CR2|=0x2A;
	I2C1->TRISE=0x2B;
	I2C1->CCR|=0xD2;
	
	I2C1->CR1&=~(1<<1);	//I2C模式
	I2C1->CR1|=1<<7;	//禁止时钟延长
	I2C1->CR2|=1<<8;		//开启错误中断
	I2C1->CR1=0x01;			//开启I2C通信
}

第一行是开I2C的时钟,I2C的时钟是挂在低速总线APB1上的,第21位

2.1 GPIO设置

然后是设置初始化I2C的管脚,这里用的正点原子的引脚PB8,PB9了,这里我没有开时钟,因为我主程序里开过了,速度,模式,端口复用就不多说了,大家参考手册上写就行。

这里一定要注意,把端口设置成开漏模式!!I2C总线上的设备都是要设置成开漏模式的,否则总线会出现异常,无法让从机发送ACK信号(我就是那个憨憨,在NACK里卡了一天,搞的人都傻了,最后对比别人的程序发现是端口的问题)

2.2 I2C时钟

之后我们就可以开始设置I2C的时钟了

CR2的低5位是用来设置I2C的时钟频率,如下图所示

然后TRISE是I2C的上升时间,因为我用的是逻辑分析仪,暂时没看到有什么变化

然后CCR是通信速率

具体的计算大家可以去野火那边看,里面说的非常详细了,这里我就不多介绍了(我懒,hh),这次我以100K为例,上面的参数就是100K的速率

链接在这:1. I2C — [野火]STM32模块例程介绍 文档 (embedfire.com)

2.3 I2C设置

这里注意一下,所有的设置都应在开启I2C通信前设置,否则需要重新再设置

下面我们一句句来看

首先说一下CR1的第15位

这是I2C的软件复位,可以让你在出现总线异常等情况下复位总线,你也可以在初始化的时候先复位一下,这里我就简单点了

之后我们要先设置I2C的模式,在CR1的第一位

ST给I2C硬件配置了两种模式,一种是标准I2C,还有一种是SMBus模式,我暂时用不上,这里就不多介绍了。因此这里CR1的第一位要置0,即标准I2C模式。

然后是CR1的第7位

 用于控制时钟延长的,我们这里用不到,可以置1

然后是CR2的第8位错误中断使能

 考虑到有很多人说I2C有bug,还是开启为妙,当然如果你比较自信也可以不开

2.4 额外介绍

除了上述设置外I2C还有一些比较常用的设置我这里再多说几句

其中一个是时钟的问题,在CCR里面,第14位是占空比,可以设置为1:1的也可有16:9的,然后是I2C模式,有标准I2C与快速I2C两种

其核心区别在于最高速率,在手册里I2C的特性有讲,标准的I2C速率是最高是100K,快速速率最高是400K

因为这里还是以能用上为主,所以不搞复杂的模式了

除此之外还有两个OAR寄存器,这个是当MCU作为从机的时候自身地址的设置

在特性里也有说到,支持地址可编程与双地址应答

3.通信准备

在正式开始前要说一下ST的I2C与其他厂商I2C的区别

虽然ST也知道地址是7位的,但是传输的时候地址是以8位的形式传输出去,因此WR位也被视为地址位

我们在逻辑分析仪里面看到地址被分为shifted与unshifted就是这个区别

4.发送

设置完毕后我们就可以正式开始写程序了,整体还比较复杂的

发送一个字节的程序如下

void i2c_WriteOneChar(unsigned char i2c_address,unsigned char i2cin)
{
	I2C1->CR1|=1<<8;							//发出起始信号
	while((I2C1->SR1&(1<<0))==0);	//等待起始信号发送完毕
	
	I2CEV(5);
	I2C1->DR=i2c_address;					//写入地址
	while((I2C1->SR1&(1<<1))==0);	//等待地址发送完毕
	
	I2CEV(6);	
	I2C1->DR=i2cin;								//EV8
	while((I2C1->SR1&(1<<7))==0);	//等待数据发送完毕
	
	I2C1->CR1|=1<<9;							//写入停止位
}

这里我们要对照着ST的手册来说,首先是发送起始信号

4.1 起始信号

在CR1寄存器里,第8位。此位置1生成开始信号

I2C1->CR1|=1<<8;							//发出起始信号

4.2 等待起始信号发送完毕(EV5)

也就是手册上说的EV5

当起始信号发送完毕后SB就等于1

SB是SR1的第0位

我们通过来测试一下,可以看到当起始信号发送出去后SB置1了

波形上起始信号也出来了

4.3 清标志位与发送从机地址

我们需要先读取SR寄存器,然后写入DR寄存器来清除

 程序如下

这里注意一下,这里的地址也就是从机的地址

4.4 等待地址发出(EV6&EV8)

手册如下

其实是两个事件,因为EV6表示地址发送完毕,EV8表示可以写入数据,但是由于程序非常连贯,因此这两个信号是一起到的。

我们来测试一下

可以看到ADDR和TxR都置1了,逻辑分析仪上地址也出现了,ACK正常。

4.5 清EV6标志位

想清除EV6的标志位,只要读SR1与SR2即可

程序如下

我们来测试一下

可以看到ADDR正常清掉了

4.6 发送数据(清EV8标志位)

写入DR寄存器即可

这里注意一下,与地址一样,我们这里写入的数据,就是我们要发送的数据

程序如下

逻辑分析仪如下,可以看到数据正常出去了

4.7 多数据发送

因为发送是需要时间的,正常情况下我们需要等待TxE为1,表示数据寄存器为空,也就是说上一个数据正常出去了,然后我们再发送下一个数据

4.8 停止信号

向CR1的第9位写入1即可

程序如下

5.接收

接收这块这里写的不够详细,包括还有些bug,详细情况大家可以去这篇文章看看STM32的I2C补充说明-CSDN博客

程序如下

unsigned char i2c_ReadOneChar(unsigned char i2c_address)
{
	unsigned char readtemp;
	I2C1->CR1|=1<<8;							//发出起始信号
	while((I2C1->SR1&(1<<0))==0);	//等待起始信号发送完毕
	
	I2CEV(5);
	I2C1->DR=i2c_address;					//写入地址
    while((I2C1->SR1&(1<<1))==0);	//等待地址发送完毕
	
	I2CEV(6);
	readtemp=I2C1->DR;						//EV7
	while((I2C1->SR1&(1<<6))==0);	//等待数据接收完毕
	
	I2C1->CR1|=1<<9;							//写入停止位
	
	return readtemp;
}

过程如下

与发送过程类似,前面EV5,EV6没啥变换,这里说说EV7

在写入中,是将写入DR寄存器视为EV8,清空标志位

在读里面,是读取DR寄存器视为EV7,清空标志位

因此唯一要变的就是将写寄存器变为读寄存器即可

6.程序

i2c.c

void init_i2c(void)
{
	RCC->APB1ENR|=1<<21;	//使能时钟
	
	//初始化端口
	GPIOB->MODER|=2<<16;
	GPIOB->MODER|=2<<18;
	
	GPIOB->OSPEEDR|=1<<16;
	GPIOB->OSPEEDR|=1<<18;
	
	//!!!一定要设置成开漏输出模式!!!
	GPIOB->OTYPER|=1<<8;
	GPIOB->OTYPER|=1<<9;
	
	GPIOB->AFR[1]|=4<<0;
	GPIOB->AFR[1]|=4<<4;
	
	//设置I2C主频与通信速率
	I2C1->CR2|=0x2A;
	I2C1->TRISE=0x2B;
	I2C1->CCR|=0xD2;
	
	I2C1->CR1&=~(1<<1);	//I2C模式
	I2C1->CR1|=1<<7;		//禁止时钟延长
	I2C1->CR2|=1<<8;		//开启错误中断
	I2C1->CR1=0x01;			//开启I2C通信
}

void I2CEV(unsigned char EVin)
{
	unsigned char EVTemp;
	switch(EVin)
	{
		case 5:
			EVTemp=I2C1->SR1;	//读取sr1寄存器
			break;
		
		case 6:
			EVTemp=I2C1->SR1;	//读取sr1寄存器
			EVTemp=I2C1->SR2;	//读取sr2寄存器
		break;
		
		case 8:
			;
		break;
	}
}

void i2c_WriteOneChar(unsigned char i2c_address,unsigned char i2cin)
{
	I2C1->CR1|=1<<8;							//发出起始信号
	while((I2C1->SR1&(1<<0))==0);	//等待起始信号发送完毕
	
	I2CEV(5);
	I2C1->DR=i2c_address;					//写入地址
	while((I2C1->SR1&(1<<1))==0);	//等待地址发送完毕
	
	I2CEV(6);	
	I2C1->DR=i2cin;								//EV8
	while((I2C1->SR1&(1<<7))==0);	//等待数据发送完毕
	
	I2C1->CR1|=1<<9;							//写入停止位
}

unsigned char i2c_ReadOneChar(unsigned char i2c_address)
{
	unsigned char readtemp;
	I2C1->CR1|=1<<8;							//发出起始信号
	while((I2C1->SR1&(1<<0))==0);	//等待起始信号发送完毕
	
	I2CEV(5);
	I2C1->DR=i2c_address;					//写入地址
    while((I2C1->SR1&(1<<1))==0);	//等待地址发送完毕
	
	I2CEV(6);
	readtemp=I2C1->DR;						//EV7
	while((I2C1->SR1&(1<<6))==0);	//等待数据接收完毕
	
	I2C1->CR1|=1<<9;							//写入停止位
	
	return readtemp;
}

main.c 

unsigned char i2ctemp;;

int main(void)
{ 
	Stm32_Clock_Init(336,8,2,7);//设置时钟,168Mhz
	NVIC_SetGroup(1);//设置中断分组,分组1
	init_PinClock();//初始化所有时钟
	delay_init(168);//初始化延时
    init_i2c();	//硬件IIC初始化
	i2c_WriteOneChar(0xA0,0x05);//发送一个0x05从机地址是0xA0
	i2ctemp=i2c_ReadOneChar(0xA1);//读取一个字节从机地址是0xA1

	while(1)
	{
	}	
}

7.测试

实物

波形

从机

MCU读取到的数据

8.结语

总的来说,ST的I2C还是有点复杂的,但也不是完全不能用,虽然NXP的协议不错,但是中端的MCU还是ST做的好,这也是我回头来研究STM32F407的原因,NXP找不到替代品啊。至于国内的厂商都是抄ST的,bug也抄过去了。我这次使用暂时没有遇到什么bug,也可能是我用的功能和速率比较简单,不过400k的速率还是低了一点,前面测试NXP的804,IIC随便都能跑到1Mhz了。大家如果遇到其他奇奇怪怪的问题也可以评论区留言,一起探讨,我这次暂时没有遇到。

  • 26
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
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通信的具体实现可能会因为设备地址、寄存器地址、数据长度、传输模式等参数的不同而有所变化。因此在实际应用中需要根据具体情况进行调整。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值