STM32软件模拟IIC控制OLED显示——IIC篇

这是第一篇,主要介绍IIC通信协议,下一篇会介绍具体驱动OLED显示。

STM32软件模拟IIC 控制OLED显示——OLED篇(第二篇)

另外,关于时序,需要详细理解,因为实际写代码的时候,还是需要这部分基础,可以参考以下

从IIC实测波形入手,搞懂IIC通信
(有一点,就是从机的地址,确定之后,写数据的地址就是从机的地址,而读数据的地址是从机地址加一)

I2C中关于ACK和NACK的几点东西
正文开始

IIC硬件配置

介绍IIC的两篇博客,(最后都是读取AT24C02)

介绍IIC时序的博客
STM32 IIC

IIC硬件连接

在这里插入图片描述

模拟I2C 的GPIO配置

这里用到的是PC14 PC15
关于PC13.14.15,可以看这篇博客
STM32 PC13 PC14 PC15IO口的配置
在这里插入图片描述
在这里插入图片描述

/***********************************  重写IIC时序  *****************************************************/
//初始化GPIO
void OLED_IIC_GPIO_Init(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_AFIO,ENABLE);
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14|GPIO_Pin_15;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;          //GPIO_Mode_Out_OD       //软件模拟IIC配置成推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
	GPIO_Init(GPIOC, &GPIO_InitStructure); //初始化 GPIO
	GPIO_SetBits(GPIOC,GPIO_Pin_14|GPIO_Pin_15); 
	
	PWR_BackupAccessCmd(ENABLE);//允许修改RTC 和后备寄存器
	RCC_LSEConfig(RCC_LSE_OFF);//关闭外部低速外部时钟信号功能 后,PC13 PC14 PC15 才可以当普通IO用。
	BKP_TamperPinCmd(DISABLE);//关闭入侵检测功能,也就是 PC13,也可以当普通IO 使用
	PWR_BackupAccessCmd(DISABLE);//禁止修改后备寄存器
	
}

GPIO_Mode_Out_PP 这里配置成了推挽输出,OLED模块内部的SDA,SCL已经接了上拉电阻,这里配置成开漏输出或者推挽输出的效果其实是一样的。

IIC协议

IIC总线在传输数据的过程中一共有三种类型信号:开始信号、结束信号和应答信号。
起始信号是必需的,结束信号和应答信号,都可以不要。
(1)起始信号
当时钟线SCL为高期间,数据线SDA由高到低的跳变;

(2)停止信号
当时钟线SCL为高期间,数据线SDA由低到高的跳变;
在这里插入图片描述

(3)空闲状态
当IIC总线的数据线SDA和时钟线SCL两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。

(4)应答信号
发送器每发送一个字节(8个bit),就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号。
应答信号为低电平时,规定为有效应答位(ACK,简称应答位),表示接收器已经成功地接收了该字节;
应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。
在这里插入图片描述

模拟IIC时序函数


/**************************实现函数********************************************
*函数原型:		void IIC_Start(void)
*功  能:		产生IIC起始信号
*******************************************************************************/
//void OLED_IIC_Start(void)
//{
//	//SDA_OUT();     //sda线输出
//	OLED_IIC_SDA=1;
//	if(!OLED_WRITE_SDA)//return 0;	
//	OLED_IIC_SCL=1;
//	delay_us(1);
// 	OLED_IIC_SDA=0;//START:when CLK is high,DATA change form high to low 
//	if(OLED_WRITE_SDA)//return 0;
//	delay_us(1);
//	OLED_IIC_SCL=0;//钳住I2C总线,准备发送或接收数据 
//	//return 1;
//}

//起始信号
void OLED_IIC_Start(void)
{
	OLED_SDA_OUT();
	OLED_IIC_SCL = 1;
	OLED_IIC_SDA = 1;		//START:when CLK is high,DATA change form high to low
	delay_us(4);
	OLED_IIC_SDA = 0;
	delay_us(4);
	OLED_IIC_SCL = 0;		//钳住 I2C 总线,准备发送或接收数据
}

/**************************实现函数********************************************
*函数原型:		void IIC_Stop(void)
*功  能:	    //产生IIC停止信号
*******************************************************************************/	  
//void  OLED_IIC_Stop(void)
//{
//	//SDA_OUT();//sda线输出
//	OLED_IIC_SCL=0;
//	OLED_IIC_SDA=0;//STOP:when CLK is high DATA change form low to high
// 	delay_us(1);
//	OLED_IIC_SCL=1; 
//	OLED_IIC_SDA=1;//发送I2C总线结束信号
//	delay_us(1);							   	
//}

//终止信号
void OLED_IIC_Stop(void)
{
	OLED_SDA_OUT();
	OLED_IIC_SCL = 0;
	OLED_IIC_SDA = 0;		//STOP:when CLK is high DATA change form low to high
	delay_us(4);
	OLED_IIC_SCL = 1;
	OLED_IIC_SDA = 1;		//发送 I2C 总线结束信号
	delay_us(4);
}

//写一个字节的函数      //第九个时序用SCL或者wait_ack(回看博客)
void Write_IIC_Byte(unsigned char IIC_Byte)
{
//	unsigned char i;
//  for(i=0;i<8;i++)
//	{
//		if(IIC_Byte & 0x80)
//			OLED_IIC_SDA=1;
//		else
//			OLED_IIC_SDA=0;
//			OLED_IIC_SCL=1;
//      delay_us(1);  //必须有保持SCL脉冲的延时
//			OLED_IIC_SCL=0;
//			IIC_Byte<<=1;
//	}
//			OLED_IIC_SDA = 1;//原程序这里有一个拉高SDA,根据OLED的DATASHEET,此句必须去掉。
//			OLED_IIC_SCL=1;
//			delay_us(1);
//			OLED_IIC_SCL=0;
	u8 i;
	OLED_SDA_OUT();
	OLED_IIC_SCL = 0;  //拉低时钟开始数据传输
	for(i=0;i<8;i++)
	{
		OLED_IIC_SDA = (IIC_Byte & 0x80) >> 7;
		IIC_Byte <<= 1;
		delay_us(2);
		OLED_IIC_SCL = 1;
		delay_us(2);
		OLED_IIC_SCL = 0;
		delay_us(2);
	}
	//不加这一段就无法写成功,应该是因为第九个时序的问题
//	OLED_IIC_SCL = 1;
//	delay_us(2);
//	OLED_IIC_SCL = 0
//	delay_us(2);
}

//等待应答信号到来
//返回值:1,接收应答失败
// 0,接收应答成功
u8 OLED_IIC_Wait_Ack(void)
{
	u8 ucErrTime = 0;
	OLED_SDA_IN();		//SDA 设置为输入
	OLED_IIC_SDA=1;delay_us(1);    //OLED_I2C_SDA=1;是 BIT_ADDR(GPIOB_ODR_Addr,n)  //输出 ,直接操作ODR,不需要配置输入输出
	//程序中需要注意的地方是,虽然设置了GPIO的方向为输入,但是GPIO的电平还是可以设置的,没有矛盾,GPIO方向由CRL  CRH来决定,输出电平由ODR寄存器来决定
	OLED_IIC_SCL=1;delay_us(1);
	while(OLED_WRITE_SDA)
	{
		ucErrTime++;
		if(ucErrTime > 250 )
		{
			OLED_IIC_Stop();
			return 1;
		}			
	} 
	//读取到低电平才对
	OLED_IIC_SCL=0; //时钟输出 0   //钳位,方便下次传输
	return 0;
}	


/************************ 用不到这两个函数**********************************/
//从机可以选择产生应答信号,同样也可以选择不产生应答信号。
//产生应答信号就是从机将SDA线拉低,不产生应答信号就是SDA线一直保持高电平

//产生 ACK 应答
void OLED_I2C_ACK(void)
{
	//此时主机相当于在接收数据,是被动方
	OLED_IIC_SCL=0;
	OLED_SDA_OUT();
	OLED_IIC_SDA=0;
	delay_us(2);
	OLED_IIC_SCL=1;
	delay_us(2);
	OLED_IIC_SCL=0;
}

//不产生 ACK 应答
void OLED_I2C_NoACK(void)
{
	OLED_IIC_SCL=0;
	OLED_SDA_OUT();
	OLED_IIC_SDA=1;
	delay_us(2);
	OLED_IIC_SCL=1;
	delay_us(2);
	OLED_IIC_SCL=0;
}
/************************ 用不到上面这两个函数**********************************/

头文件


#ifndef __OLED_H
#define __OLED_H	 
#include "sys.h"


//IO方向设置           
#define OLED_SDA_IN()  {GPIOC->CRH&=0XF0FFFFFF;GPIOC->CRH|=(u32)8<<24;} //1000
#define OLED_SDA_OUT() {GPIOC->CRH&=0XF0FFFFFF;GPIOC->CRH|=(u32)2<<24;} //0010

#define OLED_IIC_SCL 	PCout(15) //SCL //串行时钟
#define OLED_IIC_SDA 	PCout(14) //SDA	 //串行数据
#define OLED_WRITE_SDA   PCin(14)  //输入SDA  		//输入SDA 

#define high 1
#define low 0

#define	Brightness	0xCF 
#define X_WIDTH 	128
#define Y_WIDTH 	64


///oled.c调用函数
void OLED_IIC_Start(void);// -- 开启I2C总线
void OLED_IIC_Stop(void);// -- 关闭I2C总线
void Write_IIC_Byte(unsigned char IIC_Byte);// -- 通过I2C总线写一个byte的数据
void OLED_WrDat(unsigned char dat);// -- 向OLED屏写数据
void OLED_WrCmd(unsigned char cmd);// -- 向OLED屏写命令
u8 OLED_IIC_Wait_Ack(void);
void OLED_I2C_ACK(void);//产生 ACK 应答
void OLED_I2C_NoACK(void);//不产生 ACK 应答

void OLED_IIC_GPIO_Init(void);//初始化GPIO

void OLED_Init(void);// -- OLED屏初始化程序,此函数应在操作屏幕之前最先调用
void OLED_Set_Pos(unsigned char x, unsigned char y);// -- 设置显示坐标
void OLED_Fill(unsigned char bmp_dat);// -- 全屏显示(显示BMP图片时才会用到此功能)
void OLED_CLS(void);// -- 复位/清屏
void OLED_P6x8Str(unsigned char x,unsigned char y,unsigned char ch[]);// -- 6x8点整,用于显示ASCII码的最小阵列,不太清晰
void OLED_P8x16Str(unsigned char x,unsigned char y,unsigned char ch[]);// -- 8x16点整,用于显示ASCII码,非常清晰
void OLED_P16x16Ch(unsigned char x,unsigned char y,unsigned int N);// -- 16x16点整,用于显示汉字的最小阵列,可设置各种字体、加粗、倾斜、下划线等
void Draw_BMP(unsigned char x0,unsigned char y0,unsigned char x1,unsigned char y1,unsigned char BMP[]);// -- 将128x64像素的BMP位图在取字软件中算出字表,然后复制到codetab中,此函数调用即可
void Draw_DATA(unsigned char x,unsigned char y,unsigned int N);

#endif


IIC写一个字节函数的debug

在这里插入图片描述

void Write_IIC_Byte(unsigned char IIC_Byte)
{
//	unsigned char i;
//  for(i=0;i<8;i++)
//	{
//		if(IIC_Byte & 0x80)
//			OLED_IIC_SDA=1;
//		else
//			OLED_IIC_SDA=0;
//			OLED_IIC_SCL=1;
//      delay_us(1);  //必须有保持SCL脉冲的延时
//			OLED_IIC_SCL=0;
//			IIC_Byte<<=1;
//	}
//			OLED_IIC_SDA = 1;//原程序这里有一个拉高SDA,根据OLED的DATASHEET,此句必须去掉。
//			OLED_IIC_SCL=1;
//			delay_us(1);
//			OLED_IIC_SCL=0;
	u8 i;
	OLED_SDA_OUT();
	OLED_IIC_SCL = 0;  //拉低时钟开始数据传输
	for(i=0;i<8;i++)
	{
		OLED_IIC_SDA = (IIC_Byte & 0x80) >> 7;
		IIC_Byte <<= 1;
		delay_us(2);
		OLED_IIC_SCL = 1;
		delay_us(2);
		OLED_IIC_SCL = 0;
		delay_us(2);
	}
	//不加这一段就无法写成功,应该是因为第九个时序的问题
	OLED_IIC_SCL = 1;
	delay_us(2);
	OLED_IIC_SCL = 0;
	delay_us(2);
}

注释的一段程序是可用的,与自己之前的程序对比,发现就最后少了一个SCL的从高到低的电平转换,也就是第九个电平,加上之后就可以了。这里考虑到,应该是这种写法(不仅仅是这一个函数,而是全部的IIC写OLED函数,一般都需要在第九个电平的时候wait_ack,那此时就不需要这第九个SCL,在写命令或数据时加上wait_ack即可,但是不用wait_ack就需要加上第九个电平,否则时序错误)。如下示例

  • 1.以下是用wait_ack来作为第九个电平的方式
void Write_IIC_Byte(unsigned char IIC_Byte)
{
	u8 i;
	OLED_SDA_OUT();
	OLED_IIC_SCL = 0;  //拉低时钟开始数据传输
	for(i=0;i<8;i++)
	{
		OLED_IIC_SDA = (IIC_Byte & 0x80) >> 7;
		IIC_Byte <<= 1;
		delay_us(2);
		OLED_IIC_SCL = 1;
		delay_us(2);
		OLED_IIC_SCL = 0;
		delay_us(2);
	}
}

//等待应答信号到来
//返回值:1,接收应答失败
// 0,接收应答成功
u8 OLED_IIC_Wait_Ack(void)
{
	u8 ucErrTime = 0;
	OLED_SDA_IN();		//SDA 设置为输入
	OLED_IIC_SDA=1;delay_us(1);    //OLED_I2C_SDA=1;是 BIT_ADDR(GPIOB_ODR_Addr,n)  //输出 ,直接操作ODR,不需要配置输入输出
	//程序中需要注意的地方是,虽然设置了GPIO的方向为输入,但是GPIO的电平还是可以设置的,没有矛盾,GPIO方向由CRL  CRH来决定,输出电平由ODR寄存器来决定
	OLED_IIC_SCL=1;delay_us(1);
	while(OLED_WRITE_SDA)
	{
		ucErrTime++;
		if(ucErrTime > 250 )
		{
			OLED_IIC_Stop();
			return 1;
		}			
	} 
	//读取到低电平才对
	OLED_IIC_SCL=0; //时钟输出 0   //钳位,方便下次传输
	return 0;
}	
/*********************OLED写数据************************************/ 
void OLED_WrDat(unsigned char IIC_Data)
{
	OLED_IIC_Start();
	Write_IIC_Byte(0x78);
	OLED_IIC_Wait_Ack();
	Write_IIC_Byte(0x40);			//write data
	OLED_IIC_Wait_Ack();
	Write_IIC_Byte(IIC_Data);
	OLED_IIC_Wait_Ack();
	OLED_IIC_Stop();
}
/*********************OLED写命令************************************/
void OLED_WrCmd(unsigned char IIC_Command)
{
	OLED_IIC_Start();
	Write_IIC_Byte(0x78);            //Slave address,SA0=0
	OLED_IIC_Wait_Ack();
	Write_IIC_Byte(0x00);			//write command
	OLED_IIC_Wait_Ack();
	Write_IIC_Byte(IIC_Command);
	OLED_IIC_Wait_Ack();
	OLED_IIC_Stop();
}
  • 2.以下是直接用SCL 的跳变作为第九个电平,相当于忽略了第九个电平从机的响应
void Write_IIC_Byte(unsigned char IIC_Byte)
{
	u8 i;
	OLED_SDA_OUT();
	OLED_IIC_SCL = 0;  //拉低时钟开始数据传输
	for(i=0;i<8;i++)
	{
		OLED_IIC_SDA = (IIC_Byte & 0x80) >> 7;
		IIC_Byte <<= 1;
		delay_us(2);
		OLED_IIC_SCL = 1;
		delay_us(2);
		OLED_IIC_SCL = 0;
		delay_us(2);
	}
	//不加这一段就无法写成功,应该是因为第九个时序的问题
	OLED_IIC_SCL = 1;
	delay_us(2);
	OLED_IIC_SCL = 0;
	delay_us(2);
}

/*********************OLED写数据************************************/ 
void OLED_WrDat(unsigned char IIC_Data)
{
	OLED_IIC_Start();
	Write_IIC_Byte(0x78);
	Write_IIC_Byte(0x40);			//write data
	Write_IIC_Byte(IIC_Data);
	OLED_IIC_Stop();
}
/*********************OLED写命令************************************/
void OLED_WrCmd(unsigned char IIC_Command)
{
	OLED_IIC_Start();
	Write_IIC_Byte(0x78);            //Slave address,SA0=0
	Write_IIC_Byte(0x00);			//write command
	Write_IIC_Byte(IIC_Command);
	OLED_IIC_Stop();
}

版权声明:本文为lgyLGY35原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。