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++;
}
}
}