STM32F407寄存器操作(硬件I2C)
除了上述设置外I2C还有一些比较常用的设置我这里再多说几句其中一个是时钟的问题,在CCR里面,第14位是占空比,可以设置为1:1的也可有16:9的,然后是I2C模式,有标准I2C与快速I2C两种其核心区别在于最高速率,在手册里I2C的特性有讲,标准的I2C速率是最高是100K,快速速率最高是400K因为这里还是以能用上为主,所以不搞复杂的模式了除此之外还有两个OAR寄存器,这个是当MCU作为从机的
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了。大家如果遇到其他奇奇怪怪的问题也可以评论区留言,一起探讨,我这次暂时没有遇到。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)