STM32F407 —— 硬件 I2C 驱动的步骤与应用


此篇文章将从理论到实践,对 I2C 通信方式进行理论介绍与实际应用。理论部分主要讲述 I2C 总线的物理结构与其协议中的相关规定及几种不同的通信过程。实践部分将以我们各种开发场合常用到的一个显示模块——4线 0.96寸 OLED 显示屏和本人正在进行的大创项目中用到的 MS5837 压力传感器为例,对在 STM32F407 上如何用 I2C 协议驱动模块进行介绍。这是本人(仅仅是一位刚入门嵌入式的平平无奇的大学生)在 CSDN 的第一篇博文,旨在自己总结相关知识,如有错误,欢迎各位技术大牛批评指正!本人VX:Cyy15880234628。如果能帮到他人,那再好不过啦!

一、I2C相关知识

1、物理层

(1) 定义

IIC 是 Inter-Integrated Circuit 的缩写,即两线式串行总线,有数据线 SDA 和时钟线 SCL 构成的串行总线,可进行数据的收发。总线结构如图所示:
在这里插入图片描述

(2) 通信方式

由定义可知,其数据线只有一条,所以 I2C 通信采用的是半双工通信,即数据在数据线上可以进行双向传输,但是不能同时收发。(可以想象成一根水管,水可以从左端流向右端,也可以从右端流向左端,但不能同时即从左流向右,又从右流向左,不可兼得哦!)

2、协议层

在协议层中,我们先对通信过程中所用到的几个名词所对应的相关规定做个了解。
(我也只懂这么多,哈哈哈哈,毕竟 I2C 协议不是一天两天读得完的。)

(1) 空闲状态

SDA 与 SCL 两条信号线同时处于高电平,这时规定总线处于空闲状态。

(2) 起始信号与停止信号的定义

起始信号: SCL 为高电平时,SDA 由高电平跳变为低电平;
停止信号: SCL 为高电平时,SDA 由低电平跳变为高电平。
光看文字解释似乎很抽象,来,咱们直接上图(本人自己画的丑图)!
在这里插入图片描述
start 表示起始信号,stop 表示停止信号,其实起始信号与停止信号的定义就是用 SDA 与 SCL 的不同组成来表示罢了。(当然,我对这俩规定的理解是,可以把SDA 想象成一个开关的刀,下,即合上,开始传输;上,即断开,停止传输。)

(3) 应答信号

发送器发生一个字节,在第九个时钟脉冲期间释放总线,接收器反馈一个应答信号。而应答信号又可分为两种,①有效应答(ACK):应答信号为低电平;②无效应答(NACK):应答信号为高电平。

(4) 数据传输

每位数据都有一个时钟脉冲来同步控制,即在 SCL 串行时钟配合下,SDA 逐位地串行传送每一位数据,传输是边沿出发的。

(5) 数据有效性

怎样的数据在传输过程中才算有效呢?在时钟信号高电平期间,数据线上的数据保持稳定。(或许又不好理解,那再上图!)
在这里插入图片描述

3、I2C 基本读写过程

主要包括三种通信过程:主机写数据到从机、主机读取从机数据、复合通信(既读又写)。在应用中,主要是前两种通信过程,复合通信很少见,这里我也只介绍前两种,后面驱动实战部分也是对这两种分别举例展示。

(1) 主机写数据到从机

通信过程如下图所示:
在这里插入图片描述
(有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送。A 表示应答,A 非表示非应答(高电平)。S 表示起始信号,P 表示终止信号。)
首先,主机产生一个起始信号,开始传输数据。接着,主机向从机发送地址位和读写位(0),确定是哪个从机要接收主机发送的数据。之后就开始从主机向从机发送有效数据,从机根据接收到的数据返回有效应答或无效应答。最后主机产生一个停止信号,表示此次传输结束。
整个过程就是这样的,我不知道自己是否表述清楚,其实大家也可以类比送快递的一个过程,要知道送去谁家,送什么东西,签收时给一个反馈,在淘宝确认收货表示派送成功,过程终止。这样可能形象一点。

(2) 主机读从机数据

通信过程如下图所示:在这里插入图片描述(有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送。A 表示应答,A 非表示非应答(高电平)。S 表示起始信号,P 表示终止信号。)
大体过程与主机写数据到从机上差不多,大家自行类比理解哈,要注意的点就是数据和应答信号是谁发给谁。

二、实战应用

1、I2C 驱动 4线 0.96寸 OLED 显示屏

这个模块相信大家并不陌生,小型而又低功耗,运用范围十分广泛。现在关于这个模块在STM32F1系列上的实现源码很多,但在STM32F4系列上的实现源码很少,还是有很多人不懂得移植或者移植不成功,也当作是一个半开源了吧。
话不多说,开始讲思路!
其实就是按照前面所讲的对应的通信过程和相关协议规定来配置,再配合OLED的数据手册,编写移植显示的函数即可。
驱动该模块的流程图就是主机写数据到从机的通信过程,再放一遍图吧(把良心打在公屏上)
在这里插入图片描述
通信过程的配置就按照下面这部分代码配置即可,注释个人觉得挺清楚的了。

/*********************OLED写数据************************************/ 
void OLED_WrDat(unsigned char IIC_Data)
{
	IIC_Start();   //起始信号
  IIC_Send_Byte(0x78);			//确定从机地址
	IIC_Wait_Ack();	
  IIC_Send_Byte(0x40);			//确定读写位,写为0
	IIC_Wait_Ack();	          //从机应答
  IIC_Send_Byte(IIC_Data);  //开始写数据到从机上
	IIC_Wait_Ack();           //从机应答
	IIC_Stop();               //停止信号
}

起始信号的定义:

void IIC_Start(void)
{
	SDA_OUT();     //sda线输出
	IIC_SDA=1;	  	  
	IIC_SCL=1;  
	delay_us(2);
 	IIC_SDA=0;     //START:when CLK is high,DATA change form high to low 
	delay_us(2);
	IIC_SCL=0;     //钳住I2C总线,准备发送或接收数据 
}	  

停止信号的定义:

void IIC_Stop(void)
{
	SDA_OUT();//sda线输出
	IIC_SCL=0;
	IIC_SDA=0;//STOP:when CLK is high DATA change form low to high
 	delay_us(2);
	IIC_SCL=1; 
	IIC_SDA=1;//发送I2C总线结束信号
	delay_us(2);							   					   	
}

等待应答信号:

//等待应答信号到来
//返回值:1,接收应答失败
//        0,接收应答成功

u8 IIC_Wait_Ack(void)
{
	u8 ucErrTime=0;
	SDA_IN();      //SDA设置为输入  
	IIC_SDA=1;delay_us(1);   
	IIC_SCL=1;delay_us(1);	
  
	while(READ_SDA)
	{
		ucErrTime++;
		if(ucErrTime>250)
		{
			IIC_Stop();
			return 1;
		}
	}
	IIC_SCL=0;//时钟输出0 	   
	return 0;  
} 

有效应答与无效应答的定义:

//产生ACK应答
void IIC_Ack(void)
{
	IIC_SCL=0;
	SDA_OUT();
	IIC_SDA=0;
	delay_us(4);
	IIC_SCL=1;
	delay_us(4);
	IIC_SCL=0;
}

//不产生ACK应答		    
void IIC_NAck(void)
{
	IIC_SCL=0;
	SDA_OUT();
	IIC_SDA=1;
	delay_us(4);
	IIC_SCL=1;
	delay_us(4);
	IIC_SCL=0;
}					 		

根据数据手册又可编写一些显示函数,如下:

void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 Char_Size)
{      	
	unsigned char c=0,i=0;	
		c=chr-' ';//得到偏移后的值			
		if(x>128-1){x=0;y=y+2;}
		if(Char_Size ==16)
			{
			OLED_Set_Pos(x,y);	
			for(i=0;i<8;i++)
			OLED_WrDat(F8X16[c*16+i]);
			OLED_Set_Pos(x,y+1);
			for(i=0;i<8;i++)
			OLED_WrDat(F8X16[c*16+i+8]);
			}
			else {	
				OLED_Set_Pos(x,y);
				for(i=0;i<6;i++)
				OLED_WrDat(F6x8[c][i]);
				
			}
}
//m^n函数
u32 oled_pow(u8 m,u8 n)
{
	u32 result=1;	 
	while(n--)result*=m;    
	return result;
}				  
//显示2个数字
//x,y :起点坐标	 
//len :数字的位数
//size:字体大小
//mode:模式	0,填充模式;1,叠加模式
//num:数值(0~4294967295);	 		  
void OLED_ShowNum(u8 x,u8 y,u32 num,u8 len,u8 size2)
{         	
	u8 t,temp;
	u8 enshow=0;						   
	for(t=0;t<len;t++)
	{
		temp=(num/oled_pow(10,len-t-1))%10;
		if(enshow==0&&t<(len-1))
		{
			if(temp==0)
			{
				OLED_ShowChar(x+(size2/2)*t,y,' ',size2);
				continue;
			}else enshow=1; 
		 	 
		}
	 	OLED_ShowChar(x+(size2/2)*t,y,temp+'0',size2); 
	}
} 
//显示一个字符号串
void OLED_ShowString(u8 x,u8 y,u8 *chr,u8 Char_Size)
{
	unsigned char j=0;
	while (chr[j]!='\0')
	{		OLED_ShowChar(x,y,chr[j],Char_Size);
			x+=8;
		if(x>120){x=0;y+=2;}
			j++;
	}
}

//------------显示中文------------------
void OLED_ShowCHinese(u8 x,u8 y,u8 no)
{      			    
	u8 t,adder=0;
	OLED_Set_Pos(x,y);	
    for(t=0;t<16;t++)
		{
				OLED_WrDat(Hzk[2*no][t]);
				adder+=1;
     }	
		OLED_Set_Pos(x,y+1);	
    for(t=0;t<16;t++)
			{	
				OLED_WrDat(Hzk[2*no+1][t]);
				adder+=1;
      }					
}

当然,要使用该模块之前要记得对其进行初始化哦!初始化函数如下:

/*********************OLED初始化************************************/
void OLED_Init(void)
{
	delay_ms(500);//初始化之前的延时很重要!
	OLED_WrCmd(0xae);//--turn off oled panel
	OLED_WrCmd(0x00);//---set low column address
	OLED_WrCmd(0x10);//---set high column address
	OLED_WrCmd(0x40);//--set start line address  Set Mapping RAM Display Start Line (0x00~0x3F)
	OLED_WrCmd(0x81);//--set contrast control register
	OLED_WrCmd(Brightness); // Set SEG Output Current Brightness
	OLED_WrCmd(0xa1);//--Set SEG/Column Mapping     0xa0左右反置 0xa1正常
	OLED_WrCmd(0xc8);//Set COM/Row Scan Direction   0xc0上下反置 0xc8正常
	OLED_WrCmd(0xa6);//--set normal display
	OLED_WrCmd(0xa8);//--set multiplex ratio(1 to 64)
	OLED_WrCmd(0x3f);//--1/64 duty
	OLED_WrCmd(0xd3);//-set display offset	Shift Mapping RAM Counter (0x00~0x3F)
	OLED_WrCmd(0x00);//-not offset
	OLED_WrCmd(0xd5);//--set display clock divide ratio/oscillator frequency
	OLED_WrCmd(0x80);//--set divide ratio, Set Clock as 100 Frames/Sec
	OLED_WrCmd(0xd9);//--set pre-charge period
	OLED_WrCmd(0xf1);//Set Pre-Charge as 15 Clocks & Discharge as 1 Clock   
	OLED_WrCmd(0xda);//--set com pins hardware configuration
	OLED_WrCmd(0x12);
	OLED_WrCmd(0xdb);//--set vcomh
	OLED_WrCmd(0x40);//Set VCOM Deselect Level
	OLED_WrCmd(0x20);//-Set Page Addressing Mode (0x00/0x01/0x02)
	OLED_WrCmd(0x02);//
	OLED_WrCmd(0x8d);//--set Charge Pump enable/disable
	OLED_WrCmd(0x14);//--set(0x10) disable
	OLED_WrCmd(0xa4);// Disable Entire Display On (0xa4/0xa5)
	OLED_WrCmd(0xa6);// Disable Inverse Display On (0xa6/a7) 
	OLED_WrCmd(0xaf);//--turn on oled panel
	OLED_Fill(0x00); //初始清屏
	OLED_Set_Pos(0,0);
} 

这样,OLED 显示屏就配置好了,我调试过了,亲测可用。

2、I2C 驱动 MS5837 压力传感器

由于大创需要,这个传感器可是我找了半天才找到符合需求的模块。也许大家用不到,但其驱动原理与其他模块是一样的。
该模块的驱动与OLED类似,只需要改写下地址位和读写位即可。

unsigned long MS583703BA_getConversion(uint8_t command)
{
 
			unsigned long conversion = 0;
			u8 temp[3];
	
	    IIC_Start();
			IIC_Send_Byte(0xEC); 		//写地址
			IIC_Wait_Ack();
			IIC_Send_Byte(command); //写转换命令
			IIC_Wait_Ack();
			IIC_Stop();

			delay_ms(10);
			IIC_Start();
			IIC_Send_Byte(0xEC); 		//写地址
			IIC_Wait_Ack();
			IIC_Send_Byte(0);				// start read sequence
			IIC_Wait_Ack();
			IIC_Stop();
		 
			IIC_Start();
			IIC_Send_Byte(0xEC+0x01);  //进入接收模式
			IIC_Wait_Ack();
			temp[0] = IIC_Read_Byte(1);  //带ACK的读数据  bit 23-16
			temp[1] = IIC_Read_Byte(1);  //带ACK的读数据  bit 8-15
			temp[2] = IIC_Read_Byte(0);  //带NACK的读数据 bit 0-7
			IIC_Stop();
			
			conversion = (unsigned long)temp[0] * 65536 + (unsigned long)temp[1] * 256 + (unsigned long)temp[2];
			return conversion;
 
}

关于 I2C 相关内容我就介绍这么多,也许文中有很多表述不清楚,还请大家指出或者联系我,大家一起进步,一起向嵌入式攻城狮迈进!!!

  • 5
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值