OLED屏幕

OLED屏幕

一.OLED屏幕介绍

OLED (Organic Light Emitting Diode) 屏幕是一种先进的显示技术,它采用有机薄膜发光二极管作为发光材料。与传统的液晶显示技术相比,OLED 屏幕具有更高的对比度、更快的响应速度、更广的视角范围和更低的功耗。
我们平时使用的oled屏幕(0.96寸,128*64)一般使用的驱动芯片是(SSD1306/SSD1315),部分不同的芯片可以兼容,我们本次实验使用的芯片是SSD1306Z,
在这里插入图片描述

二.SSD1306介绍

SSD1306是一款OLED/PLED点阵显示屏的控制器,可以嵌入在屏幕中,用于执行接收数据、显示存储、扫描刷新等任务

驱动接口:128个SEG引脚和64个COM引脚,对应128*64像素点阵显示屏

内置显示存储器(GDDRAM):12864 bit (1288 Byte)SRAM

供电:VDD=1.65~3.3V(IC 逻辑),VCC=7~15V(面板驱动)
由数据手册我们得知oled需要两个电压,一个是3.3v的逻辑电平,负责用于与SSD1306通信,还有一个是屏幕的供电电平-7-15v,这个是用来给屏幕供电用的,为了方便我们使用,oled的厂商在芯片里添加了降压电路和升压电路,我们只需外接一个3.3v或者5v的电压就可以驱动这块oled屏幕
在这里插入图片描述

通信接口:8位6800/8080并行接口,3/4线SPI接口,I2C接口。
注意:8080和6800是既可以写入DDSRAM,也是可以读取DDSRAM的,而2线i2c则只能写入数据

2.1 SSD1306引脚说明

在这里插入图片描述
由引脚定义可知,SSD1306这个芯片可以选择4种通信协议。6800/8080并口,i2C,spi。
由图我们可以看出,他的组成结构,我们通过I2C协议向SSD1306发送数据或者命令,如果发送的是数据,就会将数据写入到DDSRAM,然后通过显示控制器将你写入的数据显示到oled的屏幕上。如果你是写命令,则会将数据写入到命令解码器中。
在这里插入图片描述

本节实验采用了i2c的通信协议,对应的引脚如下
在这里插入图片描述
在这里插入图片描述

2.2 SSD1302_i2C协议详解

在6800/8080并口协议中,我们是通过D/C#这个引脚来控制是写命令还是写数据。而在i2C协议下,这个引脚是指定i2C从机地址最低位。那i2C是怎么区别命令和数据的呢,我们可以打开数据手册,查看i2C的时序图
在这里插入图片描述
首先,先发送一个起始位,后面紧跟着从机地址和一位读写位(7位从机地址和一个读写位),将RW#位设置为逻辑"0",即可建立写模式,最后再跟着一个应答位。
在这里插入图片描述
从机地址的最低位可以使用SA0控制。
发送完从机地址之后后面就是控制位,控制位。当co为1时,进入连续模式,在每次发送命令/数据前,都会发送一个控制位,由D/C来决定是写数据还是写命令。
当C0为0时,后面所有的都是数据位。
当你发送完数据之后,需要跟一个停止位。

2.3 执行逻辑框图

在这里插入图片描述
在0.96寸oled屏幕中,像素范围是128*64位个像素点,通过一个bit位(0或者1)来控制这个像素点是否被点亮。
比如我们向SSDRAM写入一个数据0xff,假设此时的刷新模式是从左到右,从上到下并且初始坐标设置位(0,0)。此时,x轴的第一个坐标被点亮,y轴的8个像素点被点亮,低位在前。在x轴上,我们可以任意定义点亮哪一个像素点,但在y轴上,我们必须8位对齐,不能自由的选择想要点亮的像素点。但是我们可以通过缓存区的方式来实现自由点亮y轴上的像素点。

2.4 常用命令

在这里插入图片描述

2.4.1 基础命令

1.设置页寻址模式下的列起始地址(00h~0Fh)
该命令指定页寻址模式下显示数据RAM的8位列起始地址的低位。每次数据访问时,列地址都将递增。
2.页面寻址模式设置较高的列起始地址(10h~1Fh)
3.设置内存寻址模式(20h)
SSD1306有三种不同的内寻址模式:页面寻址模式,水平寻址模式和垂直寻址模式。
(1)页面寻址模式(A[1:0] = 10xb)
在页面寻址模式下,读/写完显示RAM后,列地址指针会自动加1,如果列地址指针达到列结束地址,列地址指针会重置为列起始地址,页面地址指针不会改变。用户必须设置新的页地址和列地址,才能访问下一页的RAM内容,页地址模式下的移动顺序如图所示
在这里插入图片描述
在这里插入图片描述
(2)水平寻址模式(A[1:0] = 00b)
在水平寻址模式下,读/写显示RAM后,列地址指针自动加1。如果列地址指针到达列结束地址,列地址指针将重置为列起始地
址,页面地址指针加1。水平寻址模式下页面和列地址点的移动顺序如图103所示。当列地址指针和页面地址指针都到达结束地
址时,指针会被重置为列起始地址和页面起始地址。
在这里插入图片描述
(3)垂直寻址模式(A[1:0] = 00b)
…具体查看数据手册,就不多介绍了
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.5 代码编写

I2C的初始化代码这里就不作详细说明了,我们直接进入编写OLED函数的过程。

2.5.1 初始化代码

初始化部分的代码是跟据厂商推荐的初始化来编写的,这里就不做具体说明


/**
  * 函    数:OLED初始化
  * 参    数:无
  * 返 回 值:无
  * 说    明:使用前,需要调用此初始化函数
  */
void OLED_Init(void)
{
	OLED_I2C_Init();			//先调用底层的端口初始化
	
	OLED.len = 128;
	OLED.wight =64;
	
	/*写入一系列的命令,对OLED进行初始化配置*/
	OLED_WriteCommand(0xAE);	//设置显示开启/关闭,0xAE关闭,0xAF开启
	
	OLED_WriteCommand(0xD5);	//设置显示时钟分频比/振荡器频率
	OLED_WriteCommand(0x80);	//0x00~0xFF
	
	OLED_WriteCommand(0xA8);	//设置多路复用率
	OLED_WriteCommand(0x3F);	//0x0E~0x3F
	
	OLED_WriteCommand(0xD3);	//设置显示偏移
	OLED_WriteCommand(0x00);	//0x00~0x7F
	
	OLED_WriteCommand(0x40);	//设置显示开始行,0x40~0x7F
	
	OLED_WriteCommand(0xA1);	//设置左右方向,0xA1正常,0xA0左右反置
	
	OLED_WriteCommand(0xC8);	//设置上下方向,0xC8正常,0xC0上下反置

	OLED_WriteCommand(0xDA);	//设置COM引脚硬件配置
	OLED_WriteCommand(0x12);
	
	OLED_WriteCommand(0x81);	//设置对比度
	OLED_WriteCommand(0xCF);	//0x00~0xFF

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

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

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

	OLED_WriteCommand(0xA6);	//设置正常/反色显示,0xA6正常,0xA7反色

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

	OLED_WriteCommand(0xAF);	//开启显示
	
	OLED_Clear();				//清空显存数组
}

2.5.2 设置坐标函数

要设置OLED的显示起始坐标,就需要指定相对应的命令
在这里插入图片描述
由命令表我们可以得知,设置列起始坐标的命令可以解析成以下部分
在设置列地址低位的命令中
前四位:0000(命令前四位)
后四位:A1 A2 A3 A4(低位坐标)
由这两个部分共同组成命令,后四位 A1 A2 A3 A4组成的最大坐标就是0XF,所以就解释了为什么设置第位列起始坐标的命令是从(00-0F)了。
设置高位和设置行地址也一样,只不过行地址只有8行,所以只有3位。

void OLED_SetCursor(uint8_t X, uint8_t Y)
{
	if(X>=OLED.len)return ;   //判断是否越界
	if(Y>=OLED.wight)return ;
	
	OLED_WriteCommand(0XB0 | Y); 
	OLED_WriteCommand(0X00 | (X & 0X0F)); //x坐标低4位
	OLED_WriteCommand(0X10 | (X & 0XF0)>>4); //x坐标高4位
}

2.5.3 清屏函数

要想达到清屏的效果,只需要给所有的像素点写0就行了。

void OLED_Clear()
{
	for(uint8_t i = 0;i<OLED.wight;i++){   //清除每一页
		OLED_SetCursor(0,i);
		for(uint8_t j = 0;j<OLED.len;j++){   //清除每一页里的所有数据
			OLED_WriteData(0x00);
		}
	}
}

2.5.4 显示字符函数

要想显示字符,我们就需要在OLED屏幕上点亮多个像素点,使这些像素点组成一个字符。所以,我们就需要用到取模软件了。本节使用使用的字符大小是8x16,长度是8,宽度是16;
但是我们知道,oled屏幕的宽度是8位对齐的,我们只能一页一页的写入。16的宽度需要我们使用2页,这里的方法就是,先绘制第一页,也就是字符的上半部分,绘制完之后再绘制第二页,字符的下半部分,一组成就可以绘制一个完整的8x16的字符了。

void OLED_ShowChar(uint8_t X, uint8_t Y, char Char)
{
	if(X>=OLED.len)return ;
	if(Y>=OLED.wight)return ;
	
		OLED_SetCursor(X,Y);         //设置坐标
	for(uint8_t i = 0;i<8;i++)   	//先绘制字符的上班部分
		{	
			OLED_WriteData(OLED_F8x16[Char - ' '][i]);   //字符索引
		}
		OLED_SetCursor(X,++Y);    //设置坐标
		for(uint8_t i = 0;i<8;i++)//绘制字符的下半部分
		{	
			OLED_WriteData(OLED_F8x16[Char - ' '][i+8]);  //字符索引
		}
}

2.5.5 显示字符串函数

绘制字符串其实就是调用绘制字符函数绘制多个字符。

void OLED_ShowString(uint8_t X, uint8_t Y, char *String)
{
	if(X>=OLED.len)return ;
	if(Y>=OLED.wight)return ;
	for(uint8_t i =0;String[i] != '\0';i++)
	{
		OLED_ShowChar(X+i*8, Y, String[i]);
	}
}

2.5.5 图形函数

void OLED_Show_image(uint8_t x,uint8_t y,uint8_t len,uint8_t size ,char *chinese)
{
	if(x>=OLED.len)return ;
	if(y>=OLED.wight)return ;
	for(uint8_t j = 0;j<size/8;j++){
		OLED_SetCursor(	x, y+j);
		for(uint8_t i = 0;i<len;i++)
		{
			OLED_WriteData(chinese[i+j*len]);
		}
	}
}

2.5.6 显示汉字函数

我们要想显示汉字,就必须先知道汉字的显示原理。
在这里插入图片描述
本实验采用的是UTF-8编码,在UTF-8的编码里,用3个字节表示一个汉字(0x00是结束标志位)。所以就算我们只写入一个汉字,也必须要用" "双引号。
在这里我们采用的是索引汉字的方法来实现显示汉字的功能。
首先,我们先遍历我们的字库,索引我们字库里的汉字,看有没有和我们输入的汉字是一样的。如果存在,我们就调用显示图像函数,将这个汉字的字模给显示出来


//第一步,先索引汉字
//第二步,判断索引的汉字存不存在
//第三步,显示汉字(使用的UTF-8编码)
void OLED_Show_Chinese(uint8_t x,uint8_t y,char *chinese)
{
	if(x>=OLED.len)return ;   //越界判断
	if(y>=OLED.wight)return ;
	
	uint8_t temp_chinese[4] = {0};    //临时存储区
	uint8_t chinese_cnt = 0;
	uint16_t pindex = 0;
	for(uint8_t i = 0; chinese[i] != '\0';i++){   //先将输入的字符保存下来
		temp_chinese[chinese_cnt] = chinese[i];
		chinese_cnt++;
		
		if(chinese_cnt >=CODE_LEN){     //因为是UTF-8的编码,3个字节表示一位汉字,当等于3时,说明输入的字符已被提取完毕
			chinese_cnt = 0;
			for(pindex = 0;strcmp(" ",chinese_font_16x16[pindex].index)!=0;pindex++)   //遍历字库
			{
				if(strcmp(temp_chinese,chinese_font_16x16[pindex].index) == 0)  //找到对应的字模
				{
					break;
				}
			}
			OLED_Show_image(x-16+((i+1)/3)*16,y,16,16,chinese_font_16x16[pindex].chinese);  //调用显示函数显示字模
		}
	}
}


typedef struct{
	uint8_t index[4];
	uint8_t chinese[32];
}Chinese;

const Chinese chinese_font_16x16[] = {

	"你",
	0x00,0x80,0x60,0xF8,0x07,0x40,0x20,0x18,0x0F,0x08,0xC8,0x08,0x08,0x28,0x18,0x00,
	0x01,0x00,0x00,0xFF,0x00,0x10,0x0C,0x03,0x40,0x80,0x7F,0x00,0x01,0x06,0x18,0x00,/*"你",0*/

	"好",
	0x10,0x10,0xF0,0x1F,0x10,0xF0,0x00,0x80,0x82,0x82,0xE2,0x92,0x8A,0x86,0x80,0x00,
	0x40,0x22,0x15,0x08,0x16,0x61,0x00,0x00,0x40,0x80,0x7F,0x00,0x00,0x00,0x00,0x00,/*"好",1*/

	"世",
	0x20,0x20,0x20,0xFE,0x20,0x20,0xFF,0x20,0x20,0x20,0xFF,0x20,0x20,0x20,0x20,0x00,
	0x00,0x00,0x00,0x7F,0x40,0x40,0x47,0x44,0x44,0x44,0x47,0x40,0x40,0x40,0x00,0x00,/*"世",2*/

	"界",
	0x00,0x00,0x00,0xFE,0x92,0x92,0x92,0xFE,0x92,0x92,0x92,0xFE,0x00,0x00,0x00,0x00,
	0x08,0x08,0x04,0x84,0x62,0x1E,0x01,0x00,0x01,0xFE,0x02,0x04,0x04,0x08,0x08,0x00,/*"界",3*/
	
	" ",
	0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
	0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff/* 索引结束位 */
};

2.6 代码编写(利用缓冲区实验)

上面我们使用的方法是直接写入GDDRAM的方式,我们上面所编写的代码我们可以看出,y轴只能指定0-7页的地址,每次最少点亮8个像素点,我们无法做到点亮一个像素点的操作。但是,我们只要利用缓冲区就可以实现点亮一个像素点的效果,y轴不再是0-7页,而是0-63个坐标点。
为了解决这个问题,我们使用写缓冲区的方式来进行写入,那什么是写缓冲区呢。就是我们先定义一个128*8的数组,先把要写入的内容写入这个数组里,最后再将这个数组刷到GDDRAM里,那为什么要用这种方法呢?因为4线ii2的无法读取GDDRAM里的数据,使用这种缓冲区的方法就可以解决这个问题,我们要想知道GGDRAM里数据的话直接读取缓冲区数组就可以了。

清屏函数


uint8_t OLED_Buff[8][128] = {0};

void OLED_Clear()
{
	for(uint8_t i = 0;i<8;i++)
	{
		for(uint8_t j = 0;j<128;j++)
		{
			OLED_Buff[i][j] = 0x00;
		}
	}
}

在更新函数里,我们就是将缓冲区数据全部写入到GDDRAM里,在这里我们可以使用i2c的连续写入模式,这样可以加快屏幕的刷新率。


//OLED写数据
void OLED_WriteData(uint8_t *Data,uint8_t Count)
{
	OLED_I2C_Start();
	OLED_I2C_SendByte(0x78);  //发送从机地址 0111 1000
	OLED_I2C_SendByte(0x40);   //连续写入模式,写数据 // 0100 0000
	for(uint8_t i = 0;i<Count;i++)
	{
		OLED_I2C_SendByte(Data[i]); 
	}
	OLED_I2C_Stop();
}

void OLED_Update()
{
	for(uint8_t i = 0;i<8;i++)
	{
		OLED_SetCursor(0,i);
		OLED_WriteData(OLED_Buff[i],128);
	}
}

显示图像函数(重点)
在显示图像之前,我们需要知道为什么使用缓冲区的设计就可以一个像素点一个像素点的改变,例如,我们缓冲区OLED_Buff[0][1]第0页的第一个数据是0XFF。我们想熄灭第一个像素点,我们只需要,OLED[0][1] |= 0X01; 或等于的操作是,其他位不变,将某一位置0。所以OLED[0][1]里的数据就变成了0XFE(1111 1110),第一个像素点也就被熄灭了。再了解我们这一步操作后,我们就可以开始编写图像显示的代码了。
我们来整理一下思路,我们以下面这幅图为例
在这里插入图片描述
由图片我们可以看到image[0]的数据是0xff,但是我要求他显示在第0页y轴的第3个坐标,所以此时我们就需要取0xff低5位的数数据来显示在第0页,也就是(默认OLED_Buff初始化时全是0)OLED_Buff[0][2] |= image[0] <<3,将高3位的数据舍弃,相对于的,高三位的数据显示在下一页OLED_Buff[1][2] |= image[0] >>(8-3),此时就可以将image[0]这个数据显示出来了,并且显示可以跨页,y轴是0-63的范围内。

void OLED_Show_image(uint8_t x, uint8_t y, uint8_t len, uint8_t size, char *image)
{
    if (x >= OLED.len) return;
    if (y >= OLED.wight) return;            

    for (uint8_t j = 0; j < (size - 1) / 8 + 1; j++)   // (size - 1) / 8 + 1判断需要多少页,例如高为15,(15 - 1)/8 +1 = 2需要2页
    {
        for (uint8_t i = 0; i < len; i++)  //长度
        {
            // 如果x坐标加上图像宽度超过屏幕宽度,将图像显示到左边界
            if (x + i >= 128) {
                OLED_Buff[(y / 8 + j) % (OLED_HEIGHT / 8)][(x + i - OLED_WIDTH)] |= image[i + j * len] << (y % 8);
                OLED_Buff[(y / 8 + 1 + j) % (OLED_HEIGHT / 8)][(x + i - OLED_WIDTH)] |= image[i + j * len] >> (8 - y % 8);
            }
            else {
                OLED_Buff[(y / 8 + j)][x + i] |= image[i + j * len] << (y % 8);
                OLED_Buff[(y / 8 + 1 + j)][x + i] |= image[i + j * len] >> (8 - y % 8);
            }
        }
    }
}

剩下的一些代码就不过介绍了

#include "stm32f1xx_hal.h"
#include "OLED_Font.h"
#include <stdarg.h>
#include <string.h>
#include "OLED.h"

#define OLED_WIDTH 128
#define OLED_HEIGHT 64


uint8_t OLED_Buff[8][128] = {0};



//SDA  PB13  SCL  PB15
/*引脚配置*/
#define OLED_W_SCL(x) HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, (GPIO_PinState)(x))
#define OLED_W_SDA(x) HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, (GPIO_PinState)(x))

oled OLED;

/*引脚初始化*/
void OLED_I2C_Init(void)
{
	__HAL_RCC_GPIOB_CLK_ENABLE();
		
	GPIO_InitTypeDef GPIO_Initstruct;
    GPIO_Initstruct.Mode = GPIO_MODE_OUTPUT_OD;
    GPIO_Initstruct.Pin = GPIO_PIN_13 | GPIO_PIN_15;
	GPIO_Initstruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB,&GPIO_Initstruct);
	
	OLED_W_SCL(1);
	OLED_W_SDA(1);	
}

/**
  * @brief  I2C开始
  * @param  无
  * @retval 无
  */
void OLED_I2C_Start(void)
{
	OLED_W_SDA(1);
	OLED_W_SCL(1);
	OLED_W_SDA(0);
	OLED_W_SCL(0);
}

/**
  * @brief  I2C停止
  * @param  无
  * @retval 无
  */
void OLED_I2C_Stop(void)
{
	OLED_W_SDA(0);
	OLED_W_SCL(1);
	OLED_W_SDA(1);
}

///**
//  * @brief  I2C发送一个字节
//  * @param  Byte 要发送的一个字节
//  * @retval 无
//  */
void OLED_I2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i++)
	{
		OLED_W_SDA(Byte & (0x80 >> i));
		OLED_W_SCL(1);
		OLED_W_SCL(0);
	}
	OLED_W_SCL(1);	//额外的一个时钟,不处理应答信号
	OLED_W_SCL(0);
}

//OLED写命令
void OLED_WriteCommand(uint8_t Command)
{
	OLED_I2C_Start();
	OLED_I2C_SendByte(0x78);  //发送从机地址 0111 1000
	OLED_I2C_SendByte(0x00);   //不连续模式,写命令
	OLED_I2C_SendByte(Command); 
	OLED_I2C_Stop();
	
}

//OLED写数据
void OLED_WriteData(uint8_t *Data,uint8_t Count)
{
	OLED_I2C_Start();
	OLED_I2C_SendByte(0x78);  //发送从机地址 0111 1000
	OLED_I2C_SendByte(0x40);   //连续写入模式,写数据 // 0100 0000
	for(uint8_t i = 0;i<Count;i++)
	{
		OLED_I2C_SendByte(Data[i]); 
	}
	OLED_I2C_Stop();
}
	

/**
  * 函    数:OLED初始化
  * 参    数:无
  * 返 回 值:无
  * 说    明:使用前,需要调用此初始化函数
  */
void OLED_Init(void)
{
	OLED_I2C_Init();			//先调用底层的端口初始化
	
	OLED.len = 128;
	OLED.wight =64;
	
	/*写入一系列的命令,对OLED进行初始化配置*/
	OLED_WriteCommand(0xAE);	//设置显示开启/关闭,0xAE关闭,0xAF开启
	
	OLED_WriteCommand(0xD5);	//设置显示时钟分频比/振荡器频率
	OLED_WriteCommand(0x80);	//0x00~0xFF
	
	OLED_WriteCommand(0xA8);	//设置多路复用率
	OLED_WriteCommand(0x3F);	//0x0E~0x3F
	
	OLED_WriteCommand(0xD3);	//设置显示偏移
	OLED_WriteCommand(0x00);	//0x00~0x7F
	
	OLED_WriteCommand(0x40);	//设置显示开始行,0x40~0x7F
	
	OLED_WriteCommand(0xA1);	//设置左右方向,0xA1正常,0xA0左右反置
	
	OLED_WriteCommand(0xC8);	//设置上下方向,0xC8正常,0xC0上下反置

	OLED_WriteCommand(0xDA);	//设置COM引脚硬件配置
	OLED_WriteCommand(0x12);
	
	OLED_WriteCommand(0x81);	//设置对比度
	OLED_WriteCommand(0xCF);	//0x00~0xFF

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

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

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

	OLED_WriteCommand(0xA6);	//设置正常/反色显示,0xA6正常,0xA7反色

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

	OLED_WriteCommand(0xAF);	//开启显示
	
	OLED_Clear();				//清空显存数组
}

void OLED_SetCursor(uint8_t X, uint8_t Y)
{
	if(X>=OLED.len)return ;
	if(Y>=OLED.wight)return ;
	
	OLED_WriteCommand(0XB0 | Y);          //Y坐标
	OLED_WriteCommand(0X00 | (X & 0X0F)); //x坐标低4位
	OLED_WriteCommand(0X10 | (X & 0XF0)>>4); //x坐标高4位
}


void OLED_Clear()
{
	for(uint8_t i = 0;i<8;i++)
	{
		for(uint8_t j = 0;j<128;j++)
		{
			OLED_Buff[i][j] = 0x00;
		}
	}
}

void OLED_Update()
{
	for(uint8_t i = 0;i<8;i++)
	{
		OLED_SetCursor(0,i);
		OLED_WriteData(OLED_Buff[i],128);
	}
}


void OLED_ShowChar(uint8_t X, uint8_t Y, char Char)
{
		OLED_Show_image(X,Y,8,16 ,OLED_F8x16[Char - ' ']);
}

void OLED_ShowString(uint8_t X, uint8_t Y, char *String)
{
	if(X>=OLED.len)return ;
	if(Y>=OLED.wight)return ;
	for(uint8_t i =0;String[i] != '\0';i++)
	{
		OLED_ShowChar(X+i*8, Y, String[i]);
	}
}

void OLED_Show_image(uint8_t x, uint8_t y, uint8_t len, uint8_t size, char *image)
{
    if (x >= OLED.len) return;
    if (y >= OLED.wight) return;            

    for (uint8_t j = 0; j < (size - 1) / 8 + 1; j++)   // (size - 1) / 8 + 1判断需要多少页,例如高为15,(15 - 1)/8 +1 = 2需要2页
    {
        for (uint8_t i = 0; i < len; i++)  //长度
        {
            // 如果x坐标加上图像宽度超过屏幕宽度,将图像显示到左边界
            if (x + i >= 128) {
                OLED_Buff[(y / 8 + j) % (OLED_HEIGHT / 8)][(x + i - OLED_WIDTH)] |= image[i + j * len] << (y % 8);
                OLED_Buff[(y / 8 + 1 + j) % (OLED_HEIGHT / 8)][(x + i - OLED_WIDTH)] |= image[i + j * len] >> (8 - y % 8);
            }
            else {
                OLED_Buff[(y / 8 + j)][x + i] |= image[i + j * len] << (y % 8);
                OLED_Buff[(y / 8 + 1 + j)][x + i] |= image[i + j * len] >> (8 - y % 8);
            }
        }
    }
}



//第一步,先索引汉字
//第二步,判断索引的汉字存不存在
//第三步,显示汉字(使用的UTF-8编码)
void OLED_Show_Chinese(uint8_t x,uint8_t y,char *chinese)
{
	if(x>=OLED.len)return ;   //越界判断
	if(y>=OLED.wight)return ;
	
	uint8_t temp_chinese[4] = {0};    //临时存储区
	uint8_t chinese_cnt = 0;
	uint16_t pindex = 0;
	for(uint8_t i = 0; chinese[i] != '\0';i++){   //先将输入的字符保存下来
		temp_chinese[chinese_cnt] = chinese[i];
		chinese_cnt++;
		
		if(chinese_cnt >=CODE_LEN){     //因为是UTF-8的编码,3个字节表示一位汉字,当等于3时,说明输入的字符已被提取完毕
			chinese_cnt = 0;
			for(pindex = 0;strcmp(" ",chinese_font_16x16[pindex].index)!=0;pindex++)   //遍历字库
			{
				if(strcmp(temp_chinese,chinese_font_16x16[pindex].index) == 0)  //找到对应的字模
				{
					break;
				}
			}
			OLED_Show_image(x-16+((i+1)/3)*16,y,16,16,chinese_font_16x16[pindex].chinese);  //调用显示函数显示字模
		}
	}
}

uint8_t OLED_GetPoint(uint8_t x,uint8_t y)
{
		if (OLED_Buff[y / 8][x] & 0x01 << (x % 8)){
		return 1;
	}
	return 0;
}

void OLED_DrawPoint(uint8_t x,uint8_t y)
{
	if(x>=OLED.len)return ;   //越界判断
	if(y>=OLED.wight)return ;
	
	OLED_Buff[y/8][x] |= 0x01 << (y%8);
}

void OLED_ClearArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
	uint8_t i, j;
	
	/*参数检查,保证指定区域不会超出屏幕范围*/
	if (X > 127) {return;}
	if (Y > 63) {return;}
	if (X + Width > 128) {Width = 128 - X;}
	if (Y + Height > 64) {Height = 64 - Y;}
	
	for (j = Y; j < Y + Height; j ++)		//遍历指定页
	{
		for (i = X; i < X + Width; i ++)	//遍历指定列
		{
			OLED_Buff[j / 8][i] &= ~(0x01 << (j % 8));	//将显存数组指定数据清零
		}
	}
}

// bresenham直线算法
void OLED_DrawLine(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2)
{	
    // 计算差值
    int dx = x2 - x1;
    int dy = y2 - y1;

    int abs_dx = (dx < 0) ? -dx : dx;  // 取绝对值
    int abs_dy = (dy < 0) ? -dy : dy;

    // 初始误差
    int err = (dx > dy ? abs_dx : -abs_dy) / 2;
    
    // 绘制起始点
    OLED_DrawPoint(x1, y1);

    // Bresenham算法进行斜线绘制
    while (x1 != x2 || y1 != y2) {
        int e2 = err;
        if (e2 > -abs_dx) { // 在x方向上移动
            err -= abs_dy;
            x1 += (x2 > x1) ? 1 : -1;
        }
        if (e2 < abs_dy) { // 在y方向上移动
            err += abs_dx;
            y1 += (y2 > y1) ? 1 : -1;
        }
        OLED_DrawPoint(x1, y1);  // 绘制当前点
    }
}


void OLED_DrawTriangle(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2, uint8_t x3, uint8_t y3)
{
    OLED_DrawLine(x1, y1, x2, y2);  // 绘制第一条边
    OLED_DrawLine(x2, y2, x3, y3);  // 绘制第二条边
    OLED_DrawLine(x3, y3, x1, y1);  // 绘制第三条边
}

//void OLED_Printf(uint8_t x,uint8_t y,char *fmt,...)
//{
//	if(x>=OLED.len)return ;
//	if(y>=OLED.wight)return ;
//	uint8_t tempbuff[256];
//    va_list vp;
//    va_start(vp,fmt);
//    vsprintf((char *)tempbuff,fmt,vp);
//    va_end(vp);
//	OLED_ShowString(x,y,(uint8_t *)tempbuff);
//}

void OLED_Printf(uint8_t x, uint8_t y, char *fmt, ...)
{
    if (x >= OLED.len) return;
    if (y >= OLED.wight) return;

    uint8_t tempbuff[256];   // 用于存储格式化后的字符串
    va_list vp;
    va_start(vp, fmt);
    vsprintf((char *)tempbuff, fmt, vp);  // 格式化字符串
    va_end(vp);

    uint8_t i = 0;
    while (tempbuff[i] != '\0') {
        if ((tempbuff[i] & 0x80) != 0) {  // 如果是高位为1,则是汉字的开头
            // 假设中文是由3个字节表示
            char chinese[4] = {0};
            uint8_t chinese_len = 0;

            // 收集UTF-8编码的汉字(3个字节)
            while ((tempbuff[i] & 0x80) != 0 && chinese_len < 3) {
                chinese[chinese_len++] = tempbuff[i++];
            }

            // 确保我们收集到了完整的中文字符
            if (chinese_len == 3) {
                // 显示汉字
                OLED_Show_Chinese(x, y, chinese);
                x += 16;  // 每个汉字占用 16 个像素宽度
            } 
        } else {
            // 普通字符
            OLED_ShowChar(x, y, tempbuff[i]);
            x += 8;  // 每个普通字符占用 8 个像素宽度
			i++;
        }
    }
}



<think>我们正在解决OLED初始化失败的问题,特别是地址配置问题。根据引用[2],我们知道OLED的I2C地址通常是0x3C或0x3D(7位地址),但在HAL库中,我们通常使用8位地址(即左移一位,因为最低位表示读写)。因此,常见的8位写地址为0x78(0x3C<<1)或0x7A(0x3D<<1)。 可能的问题: 1. 使用的地址不正确。 2. 地址扫描未正确执行,导致使用了错误的地址。 3. 初始化顺序问题(如引用[1]所述,初始化顺序可能影响外设工作)。 解决方案: 1. 确认OLED模块的I2C地址:可以通过引用[2]中的代码扫描I2C地址,或者查阅OLED的数据手册。 2. 在代码中使用正确的地址:在HAL库的I2C写入函数中,使用8位地址(即7位地址左移一位,并设置最低位为0表示写)。 3. 检查初始化顺序:确保I2C外设初始化在OLED初始化之前完成(如引用[1]所述,初始化顺序很重要)。 步骤: 1. 使用I2C扫描代码确认地址。 2. 调整初始化顺序,确保I2C初始化先于OLED初始化。 3. 在OLED初始化代码中,使用正确的地址进行通信。 示例代码(I2C扫描): 我们可以将引用[2]中的Arduino扫描代码移植到STM32 HAL库中,用于扫描I2C设备地址。 以下是一个简单的扫描函数(基于HAL库): ```c void I2C_Scan(I2C_HandleTypeDef *hi2c) { uint8_t error, address; int devices = 0; for(address = 1; address < 127; address++) { HAL_StatusTypeDef status = HAL_I2C_IsDeviceReady(hi2c, address << 1, 2, 2); // 2次尝试,2ms超时 if (status == HAL_OK) { printf("I2C device found at address 0x%02X\n", address); devices++; } } if (devices == 0) printf("No I2C devices found\n"); } ``` 在main函数中,在I2C初始化后调用此函数,以确认OLED地址。 然后,在OLED初始化函数中,使用扫描到的地址(左移一位)进行通信。 另外,注意引用[1]中提到的问题:初始化顺序。确保在调用OLED初始化函数之前,已经完成了I2C的初始化。 如果扫描不到设备,则可能是硬件连接问题,需要检查接线、上拉电阻等。 总结: 1. 使用扫描函数确认OLED的I2C地址。 2. 确保初始化顺序正确(先I2C初始化,再OLED初始化)。 3. 在OLED初始化代码中使用正确的地址(8位写地址)。 如果问题仍然存在,请检查I2C的配置(速度、时钟等)以及OLED的初始化序列是否正确。 引用[3]中提到了OLED初始化的几个关键点,包括电源、复位信号、初始化代码和延时等,这些也需要检查。 最后,生成相关问题:</think>### OLED初始化失败:地址配置问题分析与解决方案 在STM32F103上使用HAL库初始化OLED时,地址配置错误是导致无显示的常见原因。根据引用内容[2],OLED的I2C地址通常为0x3C或0x3D,但实际使用中需注意以下关键点: #### 1. **地址扫描与确认** - **问题本质**:OLED模块的I2C地址可能因厂商不同而变化(常见0x3C或0x3D) - **解决方案**: - 在代码中添加I2C扫描函数(参考引用[2]),自动检测有效地址: ```c void I2C_Scan(I2C_HandleTypeDef *hi2c) { for(uint8_t addr = 1; addr < 127; addr++) { HAL_StatusTypeDef status = HAL_I2C_IsDeviceReady(hi2c, addr << 1, 2, 10); if(status == HAL_OK) { printf("Found device at 0x%02X\n", addr); } } } ``` - 在`main()`中初始化I2C后调用扫描: ```c MX_I2C1_Init(); I2C_Scan(&hi2c1); // 打印所有检测到的设备地址 ``` #### 2. **地址格式转换** - **关键细节**:HAL库要求使用**左移后的8位地址** - 原始7位地址:0x3C → 转换后地址:`0x3C << 1 = 0x78` - 原始7位地址:0x3D → 转换后地址:`0x3D << 1 = 0x7A` - **错误示例**: ```c // 错误!直接使用7位地址 HAL_I2C_Mem_Write(&hi2c1, 0x3C, ...); ``` - **正确写法**: ```c // 正确:使用左移后的地址 #define OLED_ADDR (0x3C << 1) // 或根据扫描结果动态设置 HAL_I2C_Mem_Write(&hi2c1, OLED_ADDR, ...); ``` #### 3. **初始化顺序问题** - **引用[1]经验**:外设初始化顺序可能影响通信 - 现象:中断初始化在OLED初始化前会导致失败 - **解决方案**: 1. 确保I2C初始化在OLED初始化之前 2. 避免在中断服务程序中调用OLED函数 ```c int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); // GPIO先初始化 MX_I2C1_Init(); // I2C必须在OLED前初始化 MX_USART1_UART_Init(); OLED_Init(); // 最后初始化OLED // ... } ``` #### 4. **地址冲突排查** - **潜在问题**:I2C总线地址冲突 - 其他设备占用相同地址 - 总线未正确释放(如上次通信未完成) - **调试方法**: - 断开OLED外其他I2C设备,单独测试 - 在每次通信前重置I2C总线: ```c HAL_I2C_DeInit(&hi2c1); HAL_Delay(10); MX_I2C1_Init(); // 重新初始化 ``` #### 5. 完整初始化代码示例 ```c // 根据扫描结果设置地址 uint8_t OLED_Addr = 0x78; // 默认0x3C<<1 void OLED_Init(void) { // 发送复位序列 HAL_GPIO_WritePin(OLED_RES_GPIO_Port, OLED_RES_Pin, GPIO_PIN_RESET); HAL_Delay(50); HAL_GPIO_WritePin(OLED_RES_GPIO_Port, OLED_RES_Pin, GPIO_PIN_SET); HAL_Delay(100); // 初始化命令序列 uint8_t init_cmds[] = {0xAE, 0x20, 0x10, 0xB0, 0xC8, 0x00, 0x10, 0x40, 0x81, 0xFF, 0xA1, 0xA6, 0xA8, 0x3F, 0xA4, 0xD3, 0x00, 0xD5, 0xF0, 0xD9, 0x22, 0xDA, 0x12, 0xDB, 0x20, 0x8D, 0x14, 0xAF}; for(int i=0; i<sizeof(init_cmds); i++) { HAL_I2C_Mem_Write(&hi2c1, OLED_Addr, 0x00, I2C_MEMADD_SIZE_8BIT, &init_cmds[i], 1, 100); HAL_Delay(5); // 关键延迟 } } ``` > **关键提示**:不同OLED模块(SSD1306/SSH1106)命令序列可能不同,请查阅具体数据手册[^2]。 ### 验证流程 1. 使用I2C扫描确认OLED地址 2. 检查地址转换是否正确(7位→8位) 3. 确保初始化顺序:GPIO → I2C → OLED 4. 添加复位信号和命令间延迟 5. 检查HAL函数返回值: ```c HAL_StatusTypeDef status = HAL_I2C_Mem_Write(...); if(status != HAL_OK) { Error_Handler(); // 进入错误处理 } ``` 通过以上步骤,90%的地址配置问题可被解决。若仍无显示,建议使用逻辑分析仪检查I2C波形或更换OLED模块测试硬件完好性[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值