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了。大家如果遇到其他奇奇怪怪的问题也可以评论区留言,一起探讨,我这次暂时没有遇到。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐