Loading

STM32学习记录(四):IIC驱动OLED

STM32中分别GPIO模拟IIC和使用IIC外设进行实验。

I²CInter-Integrated Circuit),是I²C总线的简称,中文名叫做集成电路总线

时序图

以下内容来自:维基百科

  1. Data transfer is initiated with a start condition (S) signalled by SDA being pulled low while SCL stays high.
  2. SCL is pulled low, and SDA sets the first data bit level while keeping SCL low (during blue bar time).
  3. The data is sampled (received) when SCL rises for the first bit (B1). For a bit to be valid, SDA must not change between a rising edge of SCL and the subsequent falling edge (the entire green bar time).
  4. This process repeats, SDA transitioning while SCL is low, and the data being read while SCL is high (B2 through Bn).
  5. The final bit is followed by a clock pulse, during which SDA is pulled low in preparation for the stop bit.
  6. A stop condition (P) is signalled when SCL rises, followed by SDA rising.
  1. SCL保持高电平时,SDA被master(主机)拉低,此时为开始状态(S)
  2. SCL被拉低,SDA在SCL保持低电平时设置第一位数据
  3. SDA的数据在只有在SCL保持高电平时才有效
  4. 重复这个过程,第2步过程
  5. SDA发送最后一个位后,下一个时钟脉冲SDA被拉低,为停止状态做准备
  6. 当SCL被拉高,SDA紧接着被拉高,会发出停止信号(P)

I²C协议大致流程

I²C协议下可以挂载多个设备,SDA,SCL都需要添加上拉电阻,

开漏输出(Open Drain)模式下MCU只有输出低电平(数据寄存器写入0)才具有驱动能力,此时I/O引脚输出VSS。

开漏输出(Open Drain)模式下MC输出数据寄存器中的逻辑"1",P-MOS和N-MOS都关闭,输出口的状态是高阻(既不是高电平也不是低电平),如果要实现高电平驱动,需要在I/O引脚处加一个上拉电阻。

在OLED模块的原理图中可以看到SCL、SDA两条线都加了4.7K的上拉电阻

I2C协议下主机控制多个外设

微控制器可以是STM32,STM32输出低电平时,SDA、SCL都被拉低;当STM32输出高电平时,STM32不能驱动SDA、SCL,这两条线是被外接的上拉电阻拉至高电平。

当多个开漏输出的引脚连接在同一根线上时,它们共同的结果相当于一个“与逻辑门”(AND gate):全1才为1,有0则0。只有当所有设备都输出高电平(实际上是不拉低,即释放总线)时,总线才为高电平。只要任何一个设备输出低电平(主动拉低总线),整条总线就被拉低为低电平。

设置页地址模式下地址

OLED显示原理

GDDRAM(Graphic Display Data RAM)与128*64像素的显示屏是一一映射关系,向GDDRAM中指定了页地址(Page Address)、列地址的位置写入数据,128*64点阵对应位置就会点亮。行方向上的64行像素点以8位为一组分为了8组,每一页(PAGE)最下面是最高有效位(MSB),最上面是最低位(LSB)。要在OLED中显示一个字符,只需向GDDRAM指定位置写入数据。如下图所示,要在页地址PAGE0,列地址为第123列的位置显示一个6*8像素的字符'A',显示数据是{0x00, 0x7C, 0x12, 0x11, 0x12, 0x7C},这个数据从取字模软件中获取,OLED使用字模库是阴码(高电平点亮)还是阳码(低电平点亮)参照商家的数据手册。

举个例子,在取字模软件获得8*16像素的字符'1',如下图所示,为什么这里字宽要输入16?这是因为这个取字模软件默认是取汉字,识别到ASCII码时,输出的字模数据才会变成8*16像素。

这个数据怎么来的呢?在ASCIIFlow绘制出字符'1'在GGDRAM中存放的样式,一列分为两个字节,先存PAGE0的数据,再取PAGE1中存放的数据。从PAGE0取1个字节(下面是最高位,上面是最低位),再从PAGE1取1个字节,就可以获得{0x00, 0x00, 0x00, 0x00, 0x10, 0x20, 0x10, 0x20, 0xF8, 0x3F, 0x00, 0x20,0x00,0x20,0x00,0x00}这样的数据

软件模拟I2实现OLED字符的显示

初始化GPIO

使用单片机STM32F103C8T6的引脚PB8、PB9作为SCL、SDA。

#define     SDA_WriteBit(x)     GPIO_WriteBit(GPIOB, GPIO_Pin_9, (BitAction)(x))    
#define     SCL_WriteBit(x)     GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(x))  

void OLED_I2C_Init()
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	//开启时钟
	
	GPIO_InitTypeDef GPIO_InitStructure;
 	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;    //开漏输出模式
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
 	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	SCL_WriteBit(1);        //拉高SCL和SDA(本质上是上拉电阻拉高的)
	SDA_WriteBit(1);
}

IIC协议开始和停止状态

参照:上面的时序图,进行编写

/* iic协议开始  */
void OLED_I2C_Start()
{
    SDA_WriteBit(1);
    SCL_WriteBit(1);
    SDA_WriteBit(0);
    SCL_WriteBit(0);    //SCL为0,SCL的下一个时钟脉冲时开始发送数据
}

/* iic协议停止 */
void OLED_I2C_Stop()
{
    SDA_WriteBit(0);
    SCL_WriteBit(1);
    SDA_WriteBit(1);
}

OLED模块初始化

void OLED_Init(void)
{
    delay_s(1);             //延时1s,稳定OLED上电状态
    OLED_I2C_Init();			//端口初始化
    
    /* 参照商家给的数据手册 */

	OLED_WriteCmd(0xAE);	//关闭显示
	
	OLED_WriteCmd(0xD5);	//设置显示时钟分频比/振荡器频率
	OLED_WriteCmd(0x80);
	
	OLED_WriteCmd(0xA8);	//设置多路复用率
	OLED_WriteCmd(0x3F);
	
	OLED_WriteCmd(0xD3);	//设置显示偏移
	OLED_WriteCmd(0x00);
	
	OLED_WriteCmd(0x40);	//设置显示开始行
	
	OLED_WriteCmd(0xA1);	//设置左右方向,0xA1正常 0xA0左右反置
	
	OLED_WriteCmd(0xC8);	//设置上下方向,0xC8正常 0xC0上下反置

	OLED_WriteCmd(0xDA);	//设置COM引脚硬件配置
	OLED_WriteCmd(0x12);
	
	OLED_WriteCmd(0x81);	//设置对比度控制
	OLED_WriteCmd(0xCF);

	OLED_WriteCmd(0xD9);	//设置预充电周期
	OLED_WriteCmd(0xF1);

	OLED_WriteCmd(0xDB);	//设置VCOMH取消选择级别
	OLED_WriteCmd(0x30);

	OLED_WriteCmd(0xA4);	//设置整个显示打开/关闭

	OLED_WriteCmd(0xA6);	//设置正常/倒转显示

	OLED_WriteCmd(0x8D);	//设置充电泵
	OLED_WriteCmd(0x14);

	OLED_WriteCmd(0xAF);	//开启显示

    OLED_Clear();
}

OLED写数据/写指令

SSD1306 是一款单片 CMOS OLED/PLED 驱动器。《SS1306技术手册》中说明了SS1306的I2C总线数据格式。I2C总线协议中,MCU作为master(主机)要访问slave(从机):

  1. 首先在开始条件(S)后发送从机地址(slave address),从机地址中SA0位由D/C#引脚决定,R/W#位规定是读还是写。发送完一个字节后,由从机发出ACK信号,这里没有处理ACK。ACK确认位将在接收到每个Control byte或Data byte后产生。
  2. 发送的第二字节是控制字节(Control byte),Co位
    • 1:说明后面发送的字节都要遵循先发送Control byte,再发送Data byte;
    • 0:则后面的字节都是只包括Data byte
  3. D/C#位:
    • 1:后续的Data byte作为指令(Command)
    • 0:后续的Data byte作为数据(Data)存放在GDDRAM中
  4. 发送n个字节后,在停止条件(P)后停止数据的发送。

/**
  * @brief  MCU向从机发送一个字节的数据
  * @param  byte 要发送的数据
  * @retval 无
  * 
  */
void OLED_SendByte(uint8_t byte)
{
    for(uint8_t i = 0; i < 8; i++)
    {
        SDA_WriteBit(byte & ((0x80) >> i));     //依次发送byte中每一位二进制数据
        SCL_WriteBit(1);	//SCL高电平时SDA数据有效
        SCL_WriteBit(0);	//拉低准备下一次SCL上升沿
    }
    SCL_WriteBit(1);	//额外的一个时钟,不处理应答信号
	SCL_WriteBit(0);
}


/**
  * @brief  OLED写指令
  * @param  cmd 要写入的指令
  * @retval 无
  * 
  */
void OLED_WriteCmd(uint8_t cmd)
{
    OLED_I2C_Start();
    OLED_SendByte(0x78);    //发送slave(从机)地址
    OLED_SendByte(0X00);    //控制字节:准备写指令,Co = 0, D/C# = 0
    OLED_SendByte(cmd);     //写指令
    OLED_I2C_Stop();
}

/**
  * @brief  OLED写数据
  * @param  data 要写入的数据
  * @retval 无
  * 
  */
void OLED_WriteData(uint8_t data)
{
    OLED_I2C_Start();
    OLED_SendByte(0x78);    //发送slave(从机)地址
    OLED_SendByte(0X40);    //控制字节:准备写数据,Co = 0, D/C# = 1
    OLED_SendByte(data);    //写数据
    OLED_I2C_Stop();
}

OLED显示字符/字符串

这里选择8*16像素的字模数据,即宽8个像素,高16个像素。由上面可知OLED将每行的像素分成了8个页,显示字符的时候要分别在两个PAGE上写入数据。

/**
  * @brief  OLED设置光标位置
  * @param  Y 以左上角为原点,向下方向的坐标,范围:0~7
  * @param  X 以左上角为原点,向右方向的坐标,范围:0~127
  * @retval 无
  * 
  * **************************************************
  *     128*64的点阵,从行方面来看64行的点阵分成为了8个PAGE,即PAGE0~PAGE7,这也是为什么y取0~7
  *     从列方面来看,共有128列,即SEG0~SGE127,所以x取值0~127
  * 
  */
void OLED_SetCursor(uint8_t Y, uint8_t X)
{
	OLED_WriteCmd(0xB0 | Y);					//设置PAGE起始地址
	OLED_WriteCmd(0x10 | ((X & 0xF0) >> 4));	//取x的高4位并右移4位,与固定的0x10按位或,设置列地址高4位
	OLED_WriteCmd(0x00 | (X & 0x0F));			//取x的低4位设置列地址低4位,与固定的0x00按位或,设置列地址低4位
}


/**
  * @brief  在OLED第row行, 第col列显示一个ascii字符
  * @param  row 行取值范围:1~4
  * @param  col 列取值范围:1~16
  * @param  ch  要显示的ascii字符
  * @retval 无
  * 
  * 行1~4 =====>  行0~7
  * x = 1, y = 0; x = 2, y = 2;
  *	y = kx+b 过(1, 0), (2, 2)解出y = 2x-2 = 2*(x - 1),即(row - 1) * 2
  *
  * 列1~16 =====>  列0~127
  * x = 1, y = 0; x = 2, y = 8 ===> 得到关系式 y = 8*x - 8 = 8*(x-1),即(col-1)*8
  * 
  *
  */
void OLED_ShowChar(uint8_t row, uint8_t col, uint8_t ch)
{
    if((ch - ' ') < 0) // 如果输入的ascii字符不在字模库里面,则不在OLED上显示
        return;
    OLED_SetCursor((row - 1) * 2, (col - 1)* 8);	//写上半部分
    for (int i = 0; i < 8; i++)
    {
        OLED_WriteData(OLED_F8x16[ch - ' '][i]);	//ch - ' '可以得到ch在字模库中索引位置
    }
    OLED_SetCursor((row - 1) * 2 + 1, (col - 1)* 8);	//写下半部分
    for (int i = 0; i < 8; i++)
    {
        OLED_WriteData(OLED_F8x16[ch - ' '][i + 8]);
    }
}

/**
  * @brief  在OLED第row行, 第col列显示字符串
  * @param  row 行取值范围:1~4
  * @param  col 列取值范围:1~16
  * @param  str 要显示的字符串
  * @retval 无
  * 
  */
void OLED_ShowString(uint8_t row, uint8_t col, uint8_t *str)
{
    for (uint8_t i = 0; str[i] != '\0'; i++)
    {
        OLED_ShowChar(row, col + i, str[i]);
    }  
}

/* OLED清屏  */
void OLED_Clear(void)
{
    for (uint8_t i = 0; i < 8; i++)
    {
        OLED_SetCursor(i, 0);   //光标设置到第i行第1列
        for (uint8_t j  = 0; j < 128; j++)
        {
            OLED_WriteData(0X00);
        } 
    }
}

使用方法

int main()
{
    OLED_Init();
    OLED_ShowChar(1, 1, 'A');
	OLED_ShowString(2, 1, "Hello World");
    while(1)
    {
        
    }
    
}

使用I2C外设实现OLED字符的显示

I2C外设初始化

前面使用的是PB8、PB9来当做I2C协议的SCL、SDA线,STM32F103C8T6中提供了I2C外设,参照芯片的引脚图。这里使用芯片上的I2C外设来驱动OLED,引脚PB6、PB7的复用功能分别作为SCL、SDA。

#include "i2c.h"
#include "stm32f10x.h"

/* 从机地址,这里是OLED模块的地址 */
#define SLAVE_ADDRESS 0x78

/* 包括GPIO、I2C外设的初始化 */
void MyI2C_init(void)
{
	/* 开启外设时钟 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
	
    /* 使用I2C外设,GPIO必须配置为复用开漏输出 */
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);

    /* i2c外设初始化 */
    I2C_InitTypeDef I2C_InitStructure;
    I2C_InitStructure.I2C_Mode = I2C_Mode_SMBusDevice;
    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
    I2C_InitStructure.I2C_OwnAddress1 = SLAVE_ADDRESS;
    I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
    I2C_InitStructure.I2C_ClockSpeed = 400000;
    I2C_Cmd(I2C1, ENABLE);
    I2C_Init(I2C1, &I2C_InitStructure);
}

I2C外设发送数据

单片机作为主机向从机OLED模块发送数据,使其显示指定数据,因此这里选择参照主机发送序列图表中的数据发送格式来编写程序

发送数据流程是这样的:

  1. 首先使用I2C_GenerateSTART(I2C1, ENABLE)产生一个开始条件,
  2. 接着检测状态while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))是否为EV5
  3. 发送从机地址I2C_Send7bitAddress(I2C1, SLAVE_ADDRESS, I2C_Direction_Transmitter)
  4. 继续检测状态while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))是否为EV6
  5. I2C发送数据I2C_SendData(I2C1, data[i]),并且检测状态EV8,while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));发送多个数据重复执行第5步
  6. 产生停止条件I2C_GenerateSTOP(I2C1, ENABLE)
/* I2C发送数据 */
void MyI2C_SendData(uint8_t *data, uint8_t len)
{
    I2C_GenerateSTART(I2C1, ENABLE);
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
    I2C_Send7bitAddress(I2C1, SLAVE_ADDRESS, I2C_Direction_Transmitter);    //发送7位地址
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
    for(int i = 0; i < len; i++)
    {
        I2C_SendData(I2C1, data[i]);
        while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
    }
    I2C_GenerateSTOP(I2C1, ENABLE);
}

while循环判断状态时,最好通过加一个超时时间,超时时间到,还没有处于某个事件状态状态,就可以直接退出循环,防止程序卡死在while循环中,改进后:

/**
 * @brief I2C外设发送数据
 * @param data 数据数组
 * @param len 数组长度
 * 
 * ****************************
 * 
 * I2C_CheckEvent用来检测每次发送完一个字节后的状态,参照《STM32F10x参考手册》、以及《STM32F103xx固件函数库手册》
 * OK = 1, ERROR = 0
 * 
 */
uint8_t MyI2C_SendData(uint8_t *data, uint8_t len)
{
    I2C_GenerateSTART(I2C1, ENABLE);
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
    {
        if((Timeout--) == 0) return _ERROR;
    }
    I2C_Send7bitAddress(I2C1, SLAVE_ADDRESS, I2C_Direction_Transmitter);    //发送7位地址
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
    {
        if((Timeout--) == 0) return _ERROR;
    }
    for(int i = 0; i < len; i++)
    {
        I2C_SendData(I2C1, data[i]);
        while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
        {
            if((Timeout--) == 0) return _ERROR;
        }
    }
    I2C_GenerateSTOP(I2C1, ENABLE);
    return _OK;
}

I2C外设接收数据

uint8_t MyI2C_ReadData(uint8_t *data)
{
    /* 发送I2C开始条件*/
    I2C_GenerateSTART(I2C1, ENABLE);

    /* 测试EV5 */    
    Timeout = LONG_TIMEOUT;
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)) 
    {
        if((Timeout--) == 0) return _ERROR;
    }

    /* 发送从机地址,方向为接收 */
    I2C_Send7bitAddress(I2C1, SLAVE_ADDRESS, I2C_Direction_Receiver);   

    /* 测试EV6 */
    Timeout = LONG_TIMEOUT;
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED))    
    {
        if((Timeout--) == 0) return _ERROR;
    }

    /* 失能ACK */
    I2C_AcknowledgeConfig(I2C1, DISABLE); 

    /* 发送I2C停止条件 */  
    I2C_GenerateSTOP(I2C1, ENABLE);     

    /* 测试EV7 */
    Timeout = LONG_TIMEOUT;
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED))   
    {
        if((Timeout--) == 0) return _ERROR;
    }

    /* 接收一个字节的数据 */
    *data = I2C_ReceiveData(I2C1); 

    /* 将应答恢复为使能,为了不影响后续可能产生的读取多字节操作 */
    I2C_AcknowledgeConfig(I2C1, ENABLE);

    return _OK;
}
posted @ 2025-08-22 13:34  记录学习的Lyx  阅读(108)  评论(0)    收藏  举报