STM32驱动0.96寸OLED基于 “软/硬IIC协议”

一、简介

            本章讲解模拟IIC和硬件IIC驱动方式,软件IIC可以使用任意GPIO进行模拟,比较灵活,但是速率和稳定性不如硬件IIC,硬件IIC由单片机硬件自主完成时序,并支持多种速率模式,在资源充足情况下推荐使用硬件IIC。

二、0.96寸OLED模块介绍

        2.1 简介:

                 相信大家都认识此屏幕模块,特别上学时期,这里就简述;屏幕一般有黄蓝、白色和蓝色三种颜色选择,其中黄蓝屏为分段式色彩分布,而白屏和蓝屏则是纯色黑底显示;屏幕支持IIC(仅需两根线)、SPI(3或4线)以及6800/8080并行接口等多样化的连接方式,且采用SSD1306驱动IC;我们本章以IIC接口模块进行演示。         

三、IIC协议原理

        3.1 简介:

             IIC通信协议 总线占用引脚少,可扩展性强,挂载多个设备时,由仲裁器配置优先级,只占用两个引脚,一根SCL时钟和一根SDA数据线,组成;数据以字节为单位传送,每次传输的字节数不受限制直到接收到停止信号才停止。

        3.2 IIC物理层:

                    IIC是一个支持多设备的总线,支持多主多从,每个挂载到总线的设备都有一个独立的地址,主机可以利用地址进行不同设备之间访问;当线上某设备空闲时,就会输出高阻态也就是三态门状态(就理解为类似断开了),全部空闲时上拉电阻会把总线拉成高电平。

                  3.2.1 传输模式:

                                1.标准模式 (Standard Mode):

                                    1.最大传输速率为100 kbit/s。

                                    2.这是最早的IIC规范所支持的速度。

                               2.快速模式 (Fast Mode):

                                    1.支持的最大传输速率提升到了400 kbit/s。

                                    2.此模式在后续的IIC规范中被引入以支持更高的数据传输需求

                               3.快速模式Plus (Fast Mode Plus, Fm+):

                                    1.该模式将最大传输速率进一步提高到1 Mbit/s。

                                    2.并不是所有的设备都支持这个速度,这取决于设备的硬件设计。

                               4.高速模式 (High Speed Mode, Hs-mode):

                                    1.最大传输速率可达3.4 Mbit/s。

                                    2.高速模式需要额外的硬件支持来处理高速下的信号完整性问题,如电流源

                                        输出 驱动器和高速使能信号(HS)。

        3.3 IIC协议层:

                    此点讲解协议帧,我们先认识3.3.1~3.3.2基本的读写过程,它们是由3.3.3~3.3.5组成的一个过程,后续代码中就是按照此帧来进行通讯。

                   3.3.1 基本读写帧:

                                首先有个起始信号S,随后是从机地址SLAVE ADDRESS,随后是读写位R/W,随后是应答位A,随后是数据传输段(n字节+响应位),最后是停止位P。

                               广播地址后每个设备与自己的地址作比较,比较正确设备会发出应答信号,不正确的发出非应答信号,主机接收后才判断是发送还是不发送,发送数据时每发完一个字节都要等待从机的应答信号,如此循环,当从机发出非应答信号或者主机数据发送完给出停止信号才不传输了。

                   3.3.2  复合格式读写帧(常用):

                                其实差不多是两个读/写过程拼合,一般使用的先写:主机产生起始信号和设备地址和读写位发出,设备接收到后应答并接收主机写的数据,这个数据一般是一个从机设备的属性,某个单位地址,让从机设备知道主机是要访问这个内存单位,随后主机再发送一次起始信号和设备地址和读/写位,随后等候设备应答,随后对该地址读写数据,完成后发送停止信号; 第一次通讯是告诉从机读写地址,第二次则是读写的实际内容。

                   3.3.3 通讯的起始和停止信号:

                                当SCL线是高电平时SDA线从高电平向低电平切换,这个情况表示通讯的起始。当SCL是高电平时SDA线由低电平向高电平切换,表示通讯的停止。起始和停止信号由主机产生。

                   3.3.4 数据的有效性:

                                SDA数据线在SCL的每个时钟周期传输一位数据。传输时,SCL为高电平的时候SDA表示的数据有效,即在此的SDA为高电平时表示数据“1”,低电平时数据为“0”。当SCL为低电平时,SDA的数据无效,一般在这个时候SDA进行数据位的切换,为下一次数据做好准备。

                   3.3.5 地址及数据方向:

                                I2C总线上的每个设备都有自己的独立地址,主机发起通讯时,通过SDA信号线发送设备地址来查找从机。I2C协议规定设备地址可以是7位或10位,实际中7位的地址应用比较广泛,紧跟着设备地址的一个数据位用来表示数据传输方向,数据方向位为1表示读数据0表示写数据。读数据方向时由从机控制SDA信号线,主机接收信号;写数据方向时,SDA信号线由主机控制,从机接收信号。MSB与LSB是设置高位先行还是低位先行。

                          上面说了每个设备都有自己的独立地址,我们也知道OLED是采用SSD1306驱动IC,因此我们就要去驱动IC手册查看地址说明,可见手册里说明了设备地址由三部分组成:6bit固定位 + 1bit拓展位 + 1bit读写位; 可见下表;因此默认情况下写地址为0x78 读地址为079。

6bit固定位:011110
1bit拓展位:根据芯片D/C#引脚配置,D/C#接地则此位为0,反则为1。默认为0。
1bit读写位:0为“写”;1为“读”。

                 3.3.6 响应:

                             I2C的数据和地址传输都带响应。响应包括“应答(ACK)”和“非应答(NACK)”两种信号。作为数据接收端时,当设备(无论主从机)接收到I2C传输的一个字节数据或者地址后,若希望对方继续发送数据,则需要向对方发送“应答(ACK)”信号,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答信号(NACK)”信号,发送方接收到非应答信号后会产生一个停止信号,结束信号传输。

                         传输时主机产生时钟,在第九个时钟时,数据发送端会释放SDA的控制权,由数据接收端控制SDA,若SDA为高电平,表示非应答信号(NACK),低电平表示应答信号(ACK)。

四、软件IIC解析

            此方式选取STM32的任意两个空闲GPIO作为SDA/SCL线即可,通过第三点的IIC协议原理我们知道还存在有多种信号,因此我们需要编写多个产生这种信号的函数,我们按以下流程图梳理一番:     

五、硬件IIC解析

            此方式就比较简单了,因为官方的库函数已经封装好了,我们直接拿来调用即可,但是标准库下它启动、地址、数据标志位函数都是独立的所以它需要组成一个写byte函数,并不像HAL库更加的抽象已经帮你封装好了。

        5.1 标准库IIC编写流程图:

        5.2 HAL库IIC编写流程图:

六、软件IIC代码

/************************************************************************** 
** -------------------------------------------------------------------- **
** @name          : I2C_GPIO_Config
** @brief         : 配置I2C总线的GPIO,采用模拟IO的方式实现
** @param         : None
** @retval        : None
** @author data   : 轩哥	2023-05-09
** @attention     : None
** -------------------------------------------------------------------- **
**************************************************************************/

void I2C_GPIO_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	/* 打开GPIO时钟 */

    GPIO_InitStructure.GPIO_Pin = I2C_SCL_PIN | I2C_SDA_PIN;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
    GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStructure);

    /* 给一个停止信号, 复位I2C总线上的所有设备到待机模式 */
    i2c_Stop();
}

/************************************************************************** 
** -------------------------------------------------------------------- **
** @name          : i2c_Start
** @brief         : 这是一个IIC启动信号
** @param         : None
** @retval        : None
** @author data   : 轩哥	2023-05-09
** @attention     : None
** -------------------------------------------------------------------- **
**************************************************************************/
void i2c_Start(void)
{
    /* 当SCL高电平时,SDA出现一个下降沿表示I2C总线启动信号 */
    I2C_SDA_H;
    I2C_SCL_H;
    Delay();  //一个普通小延时(t=50 t--)
    I2C_SDA_L;
    Delay();
    I2C_SCL_L;
}

/************************************************************************** 
** -------------------------------------------------------------------- **
** @name          : i2c_Stop
** @brief         : 这是一个IIC停止信号
** @param         : None
** @retval        : None
** @author data   : 轩哥	2023-05-09
** @attention     : None
** -------------------------------------------------------------------- **
**************************************************************************/
void i2c_Stop(void)
{
    /* 当SCL高电平时,SDA出现一个上升沿表示I2C总线停止信号 */
    I2C_SDA_L;
    I2C_SCL_H;
    Delay();
    I2C_SDA_H;
    Delay();
    I2C_SCL_L;
}
/************************************************************************** 
** -------------------------------------------------------------------- **
** @name          : i2c_SendByte
** @brief         : 这是一个发送单字节函数
** @param         : None
** @retval        : None
** @author data   : 轩哥	2023-05-09
** @attention     : None
** -------------------------------------------------------------------- **
**************************************************************************/
void i2c_SendByte(uint8_t _ucByte)
{
    uint8_t i;

    /* 先发送字节的高位bit7 */
    for (i = 0; i < 8; i++)
    {
        if (_ucByte & 0x80)
        {
            I2C_SDA_H;
        }
        else
        {
            I2C_SDA_L;
        }
        Delay();
        I2C_SCL_H;
        Delay();
        I2C_SCL_L;
        if (i == 7)
        {
            I2C_SDA_H; // 释放总线
        }
        _ucByte <<= 1;	/* 左移一个bit */
        Delay();
    }
}


/************************************************************************** 
** -------------------------------------------------------------------- **
** @name          : i2c_WaitAck
** @brief         : CPU产生一个时钟,并读取器件的ACK应答信号
** @param         : None
** @retval        : 返回0表示正确应答,1表示无器件响应
** @author data   : 轩哥	2023-05-09
** @attention     : None
** -------------------------------------------------------------------- **
**************************************************************************/
uint8_t i2c_WaitAck(void)
{
    uint8_t re;

    I2C_SDA_H;
    Delay();
    I2C_SCL_H;	/* CPU驱动SCL = 1, 此时器件会返回ACK应答 */
    Delay();
    if (I2C_SDA_READ)	/* CPU读取SDA口线状态 */
    {
        re = 1;
    }
    else
    {
        re = 0;
    }
    I2C_SCL_L;
    Delay();
    return re;
}

/************************************************************************** 
** -------------------------------------------------------------------- **
** @name          : i2c_Ack
** @brief         : CPU产生一个ACK信号
** @param         : None
** @retval        : None
** @author data   : 轩哥	2023-05-09
** @attention     : None
** -------------------------------------------------------------------- **
**************************************************************************/
void i2c_Ack(void)
{
    I2C_SDA_L;	/* CPU驱动SDA = 0 */
    Delay();
    I2C_SCL_H;	/* CPU产生1个时钟 */
    Delay();
    I2C_SCL_L;
    Delay();
    I2C_SDA_H;	/* CPU释放SDA总线 */
}


/************************************************************************** 
** -------------------------------------------------------------------- **
** @name          : i2c_Ack
** @brief         : CPU产生1个NACK信号
** @param         : None
** @retval        : None
** @author data   : 轩哥	2023-05-09
** @attention     : None
** -------------------------------------------------------------------- **
**************************************************************************/
void i2c_NAck(void)
{
    I2C_SDA_H;	/* CPU驱动SDA = 1 */
    Delay();
    I2C_SCL_H;	;	/* CPU产生1个时钟 */
    Delay();
    I2C_SCL_L;
    Delay();
}

/************************************************************************** 
** -------------------------------------------------------------------- **
** @name          : WriteCmd
** @brief         : 这是一个IIC写命令函数
** @param         : u8 command 命令
** @retval        : None
** @author data   : 轩哥	2023-05-09
** @attention     : None
** -------------------------------------------------------------------- **
**************************************************************************/
void WriteCmd(u8 command)
{
    i2c_Start();
    i2c_SendByte(0x78);//OLED地址
    i2c_WaitAck();      //可能需要检测返回
    i2c_SendByte(0x00);//寄存器地址
    i2c_WaitAck();
    i2c_SendByte(command);
    i2c_WaitAck();
    i2c_Stop();
}
/************************************************************************** 
** -------------------------------------------------------------------- **
** @name          : WriteData
** @brief         : 这是一个IIC写数据函数
** @param         : u8 data 数据
** @retval        : None
** @author data   : 轩哥	2023-05-09
** @attention     : None
** -------------------------------------------------------------------- **
**************************************************************************/
void WriteData(u8 data)
{
    i2c_Start();
    i2c_SendByte(0x78);//OLED地址
    i2c_WaitAck();      //可能需要检测返回
    i2c_SendByte(0x40);//寄存器地址
    i2c_WaitAck();
    i2c_SendByte(data);
    i2c_WaitAck();
    i2c_Stop();
}

七、硬件IIC代码

        7.1 标准库:

/************************************************************************** 
** -------------------------------------------------------------------- **
** @name          : I2C_Configuration
** @brief         : 这是一个用于初始化IIC总线的GPIO,配置IIC结构体的函数
** @param         : None
** @retval        : None
** @author data   : 轩哥	2023-05-13
** @attention     : None
** -------------------------------------------------------------------- **
**************************************************************************/
void I2C_Configuration(void)
{
		GPIO_InitTypeDef GPIO_InitStructure; // 定义一个GPIO初始化结构体变量,用于设置GPIO引脚。
		I2C_InitTypeDef I2C_InitStructure;   // 定义一个I2C初始化结构体变量,用于设置I2C外设。

		RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); // 使能I2C1外设时钟(APB1总线)。
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 使能GPIOB端口时钟(APB2总线),因为SCL和SDA引脚位于GPIOB上。

		// 下面三行配置GPIOB上的引脚为I2C功能:
		// PB6——SCL (串行时钟) PB7——SDA (串行数据)
		GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_OD; // 设置引脚模式为复用开漏输出(Open-Drain),这是I2C所需的。
		GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_6 | GPIO_Pin_7; // 指定要配置的引脚,这里是PB6和PB7。
		GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置引脚的最大输出速度为50MHz,这通常是足够快的以满足I2C的速度需求。

		GPIO_Init(GPIOB, &GPIO_InitStructure); // 根据上面定义的参数初始化GPIOB端口。

		I2C_DeInit(I2C1); // 将I2C1外设寄存器重置为其默认复位值,确保没有遗留配置影响新设置。
		
		// 下面五行配置I2C外设的具体参数:
		I2C_InitStructure.I2C_Ack                 = I2C_Ack_Enable; // 启用应答(ACK),表示接收到的数据被正确接收。
		I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; // 设置地址匹配模式为7位地址。
		I2C_InitStructure.I2C_ClockSpeed          = 400000; // 设置I2C通信时钟频率为400kHz,即快速模式。
		I2C_InitStructure.I2C_DutyCycle           = I2C_DutyCycle_2; // 设置占空比为2,适用于标准模式和快速模式。
		I2C_InitStructure.I2C_OwnAddress1         = 0x32; //主机地址(写一个与从设备不一样的就行)
		
		I2C_Init(I2C1, &I2C_InitStructure);
		I2C_Cmd(I2C1, ENABLE);
}


/************************************************************************** 
** -------------------------------------------------------------------- **
** @name          : I2C_WriteByte
** @brief         : 这是一个用于向OLED指定寄存器地址写一个byte的数据函数
** @param         : uint8_t addr 地址
										uint8_t data 数据
** @retval        : None
** @author data   : 轩哥	2023-05-13
** @attention     : None
** -------------------------------------------------------------------- **
**************************************************************************/
void I2C_WriteByte(uint8_t addr, uint8_t data)
{
		// 等待直到I2C总线不忙(即当前没有正在进行的传输)
		while( I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY) ); // 检查BUSY标志位

		// 发送START条件,开始一次I2C传输
		I2C_GenerateSTART(I2C1, ENABLE); // 使能START信号生成
		
		// 等待直到主模式被选择(检测到START已被处理且接收到应答ACK)
		while( !I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT) ); // 检查是否为主机模式选择事件
		
		// 发送从设备地址(写入操作),OLED_ADDRESS应为预先定义的7位从机地址
		I2C_Send7bitAddress(I2C1, OLED_ADDRESS, I2C_Direction_Transmitter); // 设置方向为发送
		
		// 等待直到主机发送器模式被选择(确认地址已成功发送并接收ACK)
		while( !I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) ); // 检查是否为主机发送模式选择事件
		
		// 发送内存或寄存器地址(在某些设备中是需要的)
		I2C_SendData(I2C1, addr); // 发送数据字节,这里是内部地址
		
		// 等待直到开始传输数据字节
		while( !I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING) ); // 检查是否为主机正在发送数据事件
		
		// 发送实际要写入的数据字节
		I2C_SendData(I2C1, data); // 发送数据字节
		
		// 等待直到数据字节传输完成,并接收到应答ACK
		while( !I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED) ); // 检查是否为主机已经发送数据事件
		
		// 发送STOP条件,结束一次I2C传输
		I2C_GenerateSTOP(I2C1, ENABLE); // 使能STOP信号生成
}

/************************************************************************** 
** -------------------------------------------------------------------- **
** @name          : WriteCmd
** @brief         : 这是一个用于向OLED设定寄存器地址写命令函数
** @param         : uint8_t I2C_Command 命令
** @retval        : None
** @author data   : 轩哥	2023-05-13
** @attention     : None
** -------------------------------------------------------------------- **
**************************************************************************/
void WriteCmd(uint8_t I2C_Command)
{
		I2C_WriteByte(0x00,I2C_Command);//从0x00开始写指令
}

/************************************************************************** 
** -------------------------------------------------------------------- **
** @name          : WriteData
** @brief         : 这是一个用于向OLED设定寄存器地址写数据函数
** @param         : uint8_t I2C_Data 数据
** @retval        : None
** @author data   : 轩哥	2023-05-13
** @attention     : None
** -------------------------------------------------------------------- **
**************************************************************************/
void WriteData(uint8_t I2C_Data)
{
		I2C_WriteByte(0x40,I2C_Data);//从0x40开始写数据
}

        7.2 HCL库:

/************************************************************************** 
** -------------------------------------------------------------------- **
** @name          : OLED_GPIO_Init
** @brief         : 这是一个OLED引脚初始化顺便配置IIC的函数
** @param         : None
** @retval        : None
** @author data   : 轩哥	2023-05-16
** @attention     : None
** -------------------------------------------------------------------- **
**************************************************************************/
void OLED_GPIO_Init(void)
{
	
	// 使能GPIOB时钟,确保可以配置PB6和PB7引脚
	__HAL_RCC_GPIOB_CLK_ENABLE();
    // 使能I2C1时钟,确保I2C1外设可以被初始化和使用
    __HAL_RCC_I2C1_CLK_ENABLE();

    // 初始化GPIO初始化结构体
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    // 配置GPIO引脚:PB6 -> SCL, PB7 -> SDA
    GPIO_InitStruct.Pin = I2C_SCL_PIN | I2C_SDA_PIN; // 设置要初始化的引脚为SCL和SDA
    GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; // 设置模式为复用开漏输出
    GPIO_InitStruct.Pull = GPIO_NOPULL; // 不使用上拉或下拉电阻
    GPIO_InitStruct.Alternate = GPIO_AF4_I2C1; // 设置复用功能为I2C1
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 设置引脚速度为高速
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); // 根据上述配置初始化GPIOB

    // 初始化I2C1
    hi2c1.Instance = I2C1; // 指定使用的I2C实例为I2C1
    hi2c1.Init.ClockSpeed = 400000; // 设置I2C时钟频率为400kHz
    hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; // 设置占空比为2 (50%)
    hi2c1.Init.OwnAddress1 = 0x32; // 设置本机地址(这里仅作为示例,具体值应根据实际情况设定)
    hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; // 使用7位寻址模式
    hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; // 禁用双地址模式
    hi2c1.Init.OwnAddress2 = 0; // 第二个地址设置为0(未使用)
    hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; // 禁用通用呼叫模式
    hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; // 禁用无延展模式
    
    if (HAL_I2C_Init(&hi2c1) != HAL_OK) // 尝试初始化I2C并检查是否成功
    {
        // Initialization Error // 如果初始化失败,则执行错误处理代码
    }
}
/************************************************************************** 
** -------------------------------------------------------------------- **
** @name          : OLED_WriteCommand
** @brief         : 这是一个OLED写命令的函数
** @param         : Command 要写入的命令值,范围:0x00~0xFF
** @retval        : None
** @author data   : 轩哥	2024-05-16
** @attention     : None
** -------------------------------------------------------------------- **
**************************************************************************/
void OLED_WriteCommand(uint8_t Command)
{
    uint8_t buffer[2];
    buffer[0] = 0X00; // 寄存器地址
    buffer[1] = Command; // 要发送的数据
    HAL_I2C_Master_Transmit(&hi2c1, 0x78, buffer, 2, HAL_MAX_DELAY);
}

/************************************************************************** 
** -------------------------------------------------------------------- **
** @name          : OLED_WriteData
** @brief         : 这是一个OLED在指定起始位置写入指定的数据数量
** @param         : None
** @retval        : None
** @author data   : 轩哥	2023-05-16
** @attention     : None
** -------------------------------------------------------------------- **
**************************************************************************/
void OLED_WriteData(uint8_t *Data, uint8_t Count)
{
		uint8_t i;
		uint8_t buffer[Count + 1];
		buffer[0] = 0x40; // 寄存器地址
		for (i = 0; i < Count; i++) {
				buffer[i + 1] = Data[i]; // 将所有数据放入缓冲区
		}
		HAL_I2C_Master_Transmit(&hi2c1, 0x78 , buffer, Count + 1, HAL_MAX_DELAY);
}

八、实验示例

            为了彰显软件IIC和硬件IIC在速率上的差别,笔者设置了硬件快速模式Plus模式,可见实验中的硬件IIC帧率明显高出很多。

        7.1 软件IIC驱动:

        7.2 硬件IIC驱动:

九、总结

           通过以上学习我们弄懂了IIC的原理,它可靠之处在于有应答机制,在支持IIC的设备中我们只要弄懂它的从机地址,支持什么模式,还有一些基本的寄存器,我们就可以随心所欲的使用了,最后点个赞再走吧~

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值