从0开始使用面对对象C语言搭建一个基于OLED的图形显示框架(OLED设备层封装)

目录

OLED设备层驱动开发

如何抽象一个OLED

完成OLED的功能

初始化OLED

清空屏幕

刷新屏幕与光标设置1

刷新屏幕与光标设置2

刷新屏幕与光标设置3

绘制一个点

反色

区域化操作

区域置位

区域反色

区域更新

区域清空

测试我们的抽象

整理一下,我们应该如何使用?


在上一篇博客:从0开始使用面对对象C语言搭建一个基于OLED的图形显示框架2-CSDN博客中,我们完成了协议层的抽象,现在让我们更近一步,完成对设备层的抽象。

OLED设备层驱动开发

现在,我们终于来到了最难的设备层驱动开发。在这里,我们抽象出来了一个叫做OLED_Device的东西,我们终于可以关心的是一块OLED,他可以被打开,被设置,被关闭,可以绘制点,可以绘制面,可以清空,可以反色等等。(画画不是这个层次该干的事情,要知道,绘制一个图形需要从这个设备可以被绘制开始,也就是他可以画点,画面开始!)

所以,离我在这篇总览中从0开始使用面对对象C语言搭建一个基于OLED的图形显示框架-CSDN博客提到的绘制一个多级菜单还是有一些遥远的。饭一口口吃,事情一步步做,这急不得,一着急反而会把我们精心维护的抽象破坏掉。

代码在MCU_Libs/OLED/library/OLED at main · Charliechen114514/MCU_Libs (github.com),两个文件夹都有所涉及,所以本篇的代码量会非常巨大。请各位看官合理安排。

如何抽象一个OLED

协议层上,我们抽象了一个IIC协议。现在在设备层上,我们将进一步抽象一个OLED。上面笔者提到了,一个OLED可以被开启,关闭,画点画面,反色等等操作,他能干!他如何干是我们马上要做的事情。现在,我们需要一个OLED句柄。这个OLED句柄代表了背后使用的通信协议和它自身相关的属性信息,而不必要外泄到其他模块上去。所以,封装一个这样的抽象变得很有必要。

OLED的品种很多,分法也很多,笔者顺其自然,打算封装一个这样的结构体

typedef struct __OLED_Handle_Type{/* driver types announced the way we explain the handle */OLED_Driver_Type        stored_handle_type;/* handle data types here */OLED_Handle_Private     private_handle;
}OLED_Handle;

让我来解释一下:首先,我们的OLED品种很多,程序如何知道你的OLED如何被解释呢?stored_handle_type标识的类型来决定采取何种行动解释。。。什么呢?解释我们的private_handle。

typedef enum {OLED_SOFT_IIC_DRIVER_TYPE,OLED_HARD_IIC_DRIVER_TYPE,OLED_SOFT_SPI_DRIVER_TYPE,OLED_HARD_SPI_DRIVER_TYPE
}OLED_Driver_Type;
​
/*  to abstract the private handle base this is to isolate the dependencies ofthe real implementations
*/
typedef void* OLED_Handle_Private;

也就是说,笔者按照采取的协议进行抽象,将OLED本身的信息属性差异封装到文件内部去,作为使用不同的片子,只需要使用编译宏编译不同的文件就好了。现在,OLED_Handle就是我们的OLED,拿到这个结构体,我们就掌握了整个OLED。所以,整个OLED结构体必然可以做到如下的事情

#ifndef OLED_BASE_DRIVER_H
#define OLED_BASE_DRIVER_H
​
#include "oled_config.h"
​
typedef struct __OLED_Handle_Type{/* driver types announced the way we explain the handle */OLED_Driver_Type        stored_handle_type;/* handle data types here */OLED_Handle_Private     private_handle;
}OLED_Handle;
​
/*oled_init_hardiic_handle registers the hardiic commnications
handle: Pointer to an OLED_Handle structure that represents the handle for the OLED display, used for managing and controlling the OLED device.programmers should pass a blank one!
​
config: Pointer to an OLED_HARD_IIC_Private_Config structure that contains the configuration settings for initializing the hardware interface, typically related to the I2C communication parameters for the OLED display.
*/
// 按照硬件IIC进行初始化
void oled_init_hardiic_handle(OLED_Handle* handle, OLED_HARD_IIC_Private_Config* config);
​
/*oled_init_hardiic_handle registers the hardiic commnications
handle: Pointer to an OLED_Handle structure that represents the handle for the OLED display, used for managing and controlling the OLED device.programmers should pass a blank one!
​
config: Pointer to an OLED_SOFT_IIC_Private_Config structure that contains the configuration settings for initializing the hardware interface, typically related to the I2C communication parameters for the OLED display.
*/
// 按照软件IIC进行初始化
void oled_init_softiic_handle(OLED_Handle* handle,OLED_SOFT_IIC_Private_Config* config
);
​
/* 可以清空 */
void oled_helper_clear_frame(OLED_Handle* handle);
void oled_helper_clear_area(OLED_Handle* handle, uint16_t x, uint16_t y, uint16_t width, uint16_t height);
​
/* 需要刷新,这里采用了缓存机制 */
void oled_helper_update(OLED_Handle* handle);
void oled_helper_update_area(OLED_Handle* handle, uint16_t x, uint16_t y, uint16_t width, uint16_t height);
​
/* 可以反色 */
void oled_helper_reverse(OLED_Handle* handle);
void oled_helper_reversearea(OLED_Handle* handle, uint16_t x, uint16_t y, uint16_t width, uint16_t height);
​
/* 可以绘制 */
void oled_helper_setpixel(OLED_Handle* handle, uint16_t x, uint16_t y);
void oled_helper_draw_area(OLED_Handle* handle, uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t* sources);
​
/* 自身的属性接口,是我们之后要用的 */
uint8_t     oled_support_rgb(OLED_Handle* handle);
uint16_t    oled_width(OLED_Handle* handle);
uint16_t    oled_height(OLED_Handle* handle);
​
#endif

说完了接口,下面就是实现了。

完成OLED的功能

初始化OLED

整个事情我们终于开始翻开我们的OLED手册了。我们的OLED需要一定的初始化。让我们看看江科大代码是如何进行OLED的初始化。

void OLED_Init(void)
{uint32_t i, j;for (i = 0; i < 1000; i++)          //上电延时{for (j = 0; j < 1000; j++);}OLED_I2C_Init();            //端口初始化OLED_WriteCommand(0xAE);    //关闭显示OLED_WriteCommand(0xD5);    //设置显示时钟分频比/振荡器频率OLED_WriteCommand(0x80);OLED_WriteCommand(0xA8);    //设置多路复用率OLED_WriteCommand(0x3F);OLED_WriteCommand(0xD3);    //设置显示偏移OLED_WriteCommand(0x00);OLED_WriteCommand(0x40);    //设置显示开始行OLED_WriteCommand(0xA1);    //设置左右方向,0xA1正常 0xA0左右反置OLED_WriteCommand(0xC8);    //设置上下方向,0xC8正常 0xC0上下反置OLED_WriteCommand(0xDA);    //设置COM引脚硬件配置OLED_WriteCommand(0x12);OLED_WriteCommand(0x81);    //设置对比度控制OLED_WriteCommand(0xCF);OLED_WriteCommand(0xD9);    //设置预充电周期OLED_WriteCommand(0xF1);OLED_WriteCommand(0xDB);    //设置VCOMH取消选择级别OLED_WriteCommand(0x30);OLED_WriteCommand(0xA4);    //设置整个显示打开/关闭OLED_WriteCommand(0xA6);    //设置正常/倒转显示OLED_WriteCommand(0x8D);    //设置充电泵OLED_WriteCommand(0x14);OLED_WriteCommand(0xAF);    //开启显示OLED_Clear();               //OLED清屏
}

好长一大串,麻了,代码真的不好看。我们为什么不使用数组进行初始化呢?

uint8_t oled_init_commands[] = {0xAE,  // Turn off OLED panel0xFD, 0x12,  // Set display clock divide ratio/oscillator frequency0xD5,  // Set display clock divide ratio0xA0,  // Set multiplex ratio0xA8,  // Set multiplex ratio (1 to 64)0x3F,  // 1/64 duty0xD3,  // Set display offset0x00,  // No offset0x40,  // Set start line address0xA1,  // Set SEG/Column mapping (0xA0 for reverse, 0xA1 for normal)0xC8,  // Set COM/Row scan direction (0xC0 for reverse, 0xC8 for normal)0xDA,  // Set COM pins hardware configuration0x12,  // COM pins configuration0x81,  // Set contrast control register0xBF,  // Set SEG output current brightness0xD9,  // Set pre-charge period0x25,  // Set pre-charge as 15 clocks & discharge as 1 clock0xDB,  // Set VCOMH0x34,  // Set VCOM deselect level0xA4,  // Disable entire display on0xA6,  // Disable inverse display on0xAF   // Turn on the display
};
#define CMD_TABLE_SZ ( (sizeof(oled_init_commands)) / sizeof(oled_init_commands[0]) )

现在,我们只需要按部就班的按照顺序发送我们的指令。以hardiic的初始化为例子

void oled_init_hardiic_handle(OLED_Handle* handle, OLED_HARD_IIC_Private_Config* config)
{// 传递使用的协议句柄, 以及告知我们的句柄类型 handle->private_handle = config;handle->stored_handle_type = OLED_HARD_IIC_DRIVER_TYPE;// 按部就班的发送命令表for(uint8_t i = 0; i < CMD_TABLE_SZ; i++)// 这里我们协议的send_command就发力了, 现在我们完全不关心他是如何发送命令的config->operation.command_sender(config, oled_init_commands[i]);// 把frame清空掉oled_helper_clear_frame(handle);// 把我们的frame commit上去oled_helper_update(handle);
}

这里我们还剩下最后两行代码没解释,为什么是oled_helper_clear_frame和update要分离开来呢?我们知道,频繁的刷新OLED屏幕非常占用我们的单片机内核,也不利于我们合并绘制操作。比如说,我想绘制两个圆,为什么不画完一起更新上去呢?比起来画一个点更新一下,这个操作显然更合理。所以,为了完成这样的技术,我们需要一个Buffer缓冲区。

uint8_t OLED_GRAM[OLED_HEIGHT][OLED_WIDTH];

他就承担了我们的缓存区。多大呢?这个事情跟OLED的种类有关系,一些OLED的大小是128 x 64,另一些是144 x 64,无论如何,我们需要根据chip的种类,来选择我们的OLED的大小,更加严肃的说,是OLED的属性和它的功能。

所以,这就是为什么笔者在MCU_Libs/OLED/library/OLED/Driver/oled_config.h at main · Charliechen114514/MCU_Libs (github.com)文件中,引入了这样的控制宏

#ifndef SSD1306_H
#define SSD1306_H
​
/* hardware level defines */
#define PORT_SCL    GPIOB
#define PORT_SDA    GPIOB
#define PIN_SCL     GPIO_PIN_8
#define PIN_SDA     GPIO_PIN_9
​
#define OLED_ENABLE_GPIO_SCL_CLK() __HAL_RCC_GPIOB_CLK_ENABLE()
#define OLED_ENABLE_GPIO_SDA_CLK() __HAL_RCC_GPIOB_CLK_ENABLE()
​
#define OLED_WIDTH  (128)
#define OLED_HEIGHT (8)
​
#define POINT_X_MAX     (OLED_WIDTH)
#define POINT_Y_MAX     (OLED_HEIGHT * 8)
​
#endif

这个文件是ssd1306.h,这个文件专门承载了关于SSD1306配置的一切。现在,我们将OLED的配置系统建立起来了,当我们的chip是SSD1306的时候,只需要定义SSD1306的宏

#ifndef OLED_CONFIG_H
#define OLED_CONFIG_H
​
...
​
/* oled chips selections */
​
#ifdef SSD1306
​
#include "configs/ssd1306.h"
​
#elif SSD1309
#include "configs/ssd1309.h"
#else
#error "Unknown chips, please select in compile time using define!"
#endif
​
#endif

现在,我们的configure就完整了,我们只需要依赖config文件就能知道OLED自身的全部信息。如果你有IDE,现在就可以看到,当我们定义了SSD1306的时候,我们的OLED_GRAM自动调整为OLED_GRAM[8][128]的数组,另一放面,如果我们使用了SSD1309,我们自动会更新为OLED_GRAM[8][144],此事在ssd1309.h中亦有记载

清空屏幕

显然,我们有一些人对C库并不太了解,memset函数负责将一块内存设置为给定的值。一般而言,编译器实现将会使用独有的硬件加速优化,使用上,绝对比手动设置值只快不慢。

软件工程的一大原则:复用!能不自己手搓就不自己手搓,编译器提供了就优先使用编译器提供的

void oled_helper_clear_frame(OLED_Handle* handle)
{memset(OLED_GRAM, 0, sizeof(OLED_GRAM));
}
刷新屏幕与光标设置1

设置涂写光标,就像我们使用Windows的绘图软件一样,鼠标在哪里,左键嗯下就从那里开始绘制,我们的set_cursor函数就是干设置鼠标在哪里的工作。查询手册,我们可以这样书写(笔者是直接参考了江科大的实现)

/*set operating cursor
*/
void __pvt_oled_set_cursor(OLED_Handle* handle, const uint8_t y,const uint8_t x)
{   // 笔者提示:下面这一行是修正ssd1309的,ssd1306并不需要 + 2!// 也就是说,SSD1306的OLED不需要下面这一行,但是SSD1309需要,这一点可以去我的github仓库上看的// 更加的明白  const uint8_t new_x = x + 2;OLED_Operations op_table;__on_fetch_oled_table(handle, &op_table);op_table.command_sender(handle->private_handle, 0xB0 | y);op_table.command_sender(handle->private_handle,0x10 | ((new_x & 0xF0) >> 4));   //设置X位置高4位op_table.command_sender(handle->private_handle,0x00 | (new_x & 0x0F));          //设置X位置低4位
}
刷新屏幕与光标设置2

不对,这个代码没有看懂!其一原因是我没有给出__on_fetch_oled_table是什么。

static void __on_fetch_oled_table(const OLED_Handle* handle, OLED_Operations* blank_operations)
{switch (handle->stored_handle_type){case OLED_HARD_IIC_DRIVER_TYPE:{OLED_HARD_IIC_Private_Config* config = (OLED_HARD_IIC_Private_Config*)(handle->private_handle);blank_operations->command_sender = config->operation.command_sender;blank_operations->data_sender = config->operation.data_sender;}break;case OLED_SOFT_IIC_DRIVER_TYPE:{OLED_SOFT_IIC_Private_Config* config = (OLED_SOFT_IIC_Private_Config*)(handle->private_handle);blank_operations->command_sender = config->operation.command_sender;blank_operations->data_sender = config->operation.data_sender;}break;... // ommited spi seletctions}break;default:break;}
}

这是干什么呢?答案是:根据OLED的类型,选择我们的操作句柄。这是因为C语言没法自动识别void*的原貌是如何的,我们必须将C++中的虚表选择手动的完成

题外话:接触过C++的朋友都知道继承这个操作,实际上,这里就是一种继承。无论是何种IIC操作,都是IIC操作。他都必须遵守可以发送字节的接口操作,现在的问题是:他到底是哪样的IIC?需要执行的是哪样IIC的操作呢?所以,__on_fetch_oled_table就是把正确的操作函数根据OLED的类型给筛选出来。也就是C++中的虚表选择操作

/*set operating cursor
*/
void __pvt_oled_set_cursor(OLED_Handle* handle, const uint8_t y,const uint8_t x)
{   const uint8_t new_x = x + 2;OLED_Operations op_table;__on_fetch_oled_table(handle, &op_table);op_table.command_sender(handle->private_handle, 0xB0 | y);op_table.command_sender(handle->private_handle,0x10 | ((new_x & 0xF0) >> 4));   //设置X位置高4位op_table.command_sender(handle->private_handle,0x00 | (new_x & 0x0F));          //设置X位置低4位
}

现在回到上面的代码,我们将正确的操作句柄选择出来之后,可以发送设置“鼠标”的指令了。

复习一下位操作的基本组成

  • &是一种萃取操作,任何数&0就是0,&1则是本身,说明可以通过对应&1保留对应位,&0抹除对应位

  • |是一种赋值操作,任何数&1就是1,|0是本身,所以|可以起到对应位置1的操作。

所以,保留高4位只需要 & 0xF0(0b11110000),保留低四位只需要&0x0F就好了(0b00001111)

刷新屏幕与光标设置3

现在让我们看看刷新屏幕是怎么做的

void oled_helper_update(OLED_Handle* handle)
{OLED_Operations op_table;__on_fetch_oled_table(handle, &op_table);for (uint8_t j = 0; j < OLED_HEIGHT; j ++){/*设置光标位置为每一页的第一列*/__pvt_oled_set_cursor(handle, j, 0);/*连续写入128个数据,将显存数组的数据写入到OLED硬件*/// 有趣的是,这里笔者埋下了一个伏笔,我为什么没写OLED_WIDTH呢?尽管在SSD1306这样做是正确的// 但那也是偶然,笔者在移植SSD1309的时候就发现了这样的不一致性,导致OLED死机.// 笔者提示: OLED长宽和可绘制区域的大小不一致性op_table.data_sender(handle->private_handle, OLED_GRAM[j], 128);}
}

刷新整个屏幕就是将鼠标设置到开头,然后直接向后面写入128个数据结束我们的事情,这比一个个写要快得多!

绘制一个点

实际上,就是将对应的数组的位置放上1就好了,这需要牵扯到的是OLED独特的显示方式。

OLED自身分有页这个概念,一个页8个像素,由传递的比特控制。举个例子,我想显示的是第一个像素亮起来,就需要在一个字节的第一个比特置1余下置0,这就是为什么OLED_HEIGHT的大小不是64而是8,也就意味着setpixel函数不是简单的

OLED[height][width] = val

而实需要进行一个复杂的计算。我们分析一下,给定一个Y的值。它落在的页就是 Y / 8。比如说,Y为5的时候落在第0页的第六个比特上,Y为9的时候落在第一个页的第一个第二个比特上(注意我们的Y从0开始计算),我们设置的位置也就是:OLED_GRAM[y / 8][x],设置的值就是Y给定的比特是0x01 << (y % 8)

void oled_helper_setpixel(OLED_Handle* handle, uint16_t x, uint16_t y)
{// current unused(void)handle;if( 0 <= x && x <= POINT_X_MAX &&0 <= y && y <= POINT_Y_MAX)OLED_GRAM[y / 8][x] |= 0x01 << (y % 8);
}

(void)T是一种常见的放置maybe_unused的写法,现代编译器支持[[maybe_unused]]的指示符,表达的是这个参数可能不被用到,编译器不需要为此警告我,这在复用中很常见,一些接口的参数可能不被使用,这样的可读性会比传递空更加的好读,为了遵循ISO C,笔者没有采取,保证任何编译器都可以正确的理解我们的意图。

反色

反色就很简单了。只需要异或即可,首先,当给定的比特是0的时候,我们异或1,得到的就是相异的比较,所以结果是1:即0变成了1。我们给定的比特是1的时候,我们还是异或1,得到了相同的结果,所以结果是0,即1变成了0,这样不就实现了一个像素的反转吗!

void oled_helper_reverse(OLED_Handle* handle)
{for(uint8_t i = 0; i < OLED_HEIGHT; i++){for(uint8_t j = 0; j < OLED_WIDTH; j++){OLED_GRAM[i][j] ^= 0xFF;}}
}

能使用memset吗?为什么?所以memset是在什么情况下能使用呢?

我都这样问了,那显然不能,因为设置的值跟每一个字节的内存强相关,memset的值必须跟内存的值没有关系。

区域化操作

我们还有区域化操作没有实现。基本的步骤是

思考需要的参数:需要知道对

  • 哪个OLED:OLED_Handle* handle,

  • 起头在哪里:uint16_t x, uint16_t y,

  • 长宽如何:uint16_t width, uint16_t height

  • 对于置位,则需要一个连续的数组进行置位,它的大小就是描述了区域矩形的大小

我们先来看置位函数

区域置位
void oled_helper_draw_area(OLED_Handle* handle, uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t* sources)
{// 确保绘制区域的起点坐标在有效范围内,如果超出最大显示坐标则直接返回if(x > POINT_X_MAX)  return;if(y > POINT_Y_MAX)  return;
​// 在设置图像前,先清空绘制区域oled_helper_clear_area(handle, x, y, width, height); 
​// 遍历绘制区域的高度,以8像素为单位划分区域for(uint16_t j = 0; j < (height - 1) / 8 + 1; j++){for(uint16_t i = 0; i < width; i++){// 如果绘制超出屏幕宽度,则跳出循环if(x + i > OLED_WIDTH) { break; }// 如果绘制超出屏幕高度,则直接返回if(y / 8 + j > OLED_HEIGHT - 1) { return; }
​// 将sources中的数据按位移方式写入OLED显存GRAM// 当前行显示,低8位数据左移与显存当前内容进行按位或OLED_GRAM[y / 8 + j][x + i] |= sources[j * width + i] << (y % 8);
​// 如果绘制数据跨页(8像素一页),处理下一页的数据写入if(y / 8 + j + 1 > OLED_HEIGHT - 1) { continue; }
​// 将高8位数据右移后写入下一页显存OLED_GRAM[y / 8 + j + 1][x + i] |= sources[j * width + i] >> (8 - y % 8);}}
}
我们正
常来讲,传递的会是一个二维数组,C语言对于二维数组的处理是连续的。也就是说。对于一个被声明为OLED[WIDTH][HEIGHT]的数组,访问OLED[i][j]本质上等价于OLED + i * WIDTH + j,这个事情如果还是不能理解可以查照专门的博客进行学习。笔者默认在这里看我写的东西已经不会被这样基础的知识所困扰了。所以,我们的所作的就是将出于低页的内容拷贝到底页上

OLED_GRAM[y / 8 + j][x + i]:这是显存二维数组的索引访问。

  • y / 8 + j 计算出当前数据位于哪个页(OLED通常按8个像素一页分块存储),通过整除将 y 坐标映射到显存页。

  • x + i 表示横向的列位置。

sources[j * width + i]:这是源图像数据数组的索引访问。

  • j * width + i 计算当前像素在 sources 数据中的位置偏移。

<< (y % 8):将当前像素数据向左移动 (y % 8) 位,以确保源数据对齐到目标位置。

  • y % 8 获取绘制的起点在当前页中的垂直偏移。

|=:按位或运算符,将偏移后的数据合并到 OLED_GRAM 中现有内容。

如果 y = 5,那么 y % 8 = 5,表示当前像素从第5位开始绘制。例如:

  • 如果 sources[j * width + i] 的值是 0b11000000,经过 << 5 位移后变为 0b00000110,再与 OLED_GRAM 的原有数据合并,从而只影响目标位置上的两个像素。

先试一下分析OLED_GRAM[y / 8 + j + 1][x + i] |= sources[j * width + i] >> (8 - y % 8);,笔者的分析如下

  1. OLED_GRAM[y / 8 + j + 1][x + i]

    • 这是下一页显存中的对应位置。

    • y / 8 + j + 1 表示当前绘制位置的下一页。

    • x + i 仍为当前列位置。

  2. sources[j * width + i]

    • 源图像数据中当前像素的数据。

    • j * width + i 计算出当前像素在源数据中的位置。

  3. >> (8 - y % 8)

    • 将数据右移 (8 - y % 8) 位,将超出当前页的高位部分对齐到下一页。

    • 8 - y % 8 计算需要移入下一页的位数。

  4. |=

    • 按位或,将偏移后的数据合并到下一页显存中,以保留已有内容。

假设 y = 5,那么 8 - y % 8 = 3。如果 sources[j * width + i]0b10110000,右移 3 位得到 0b00010110,这部分数据写入下一页显存。

区域反色
void oled_helper_reversearea(OLED_Handle* handle, uint16_t x, uint16_t y, uint16_t width, uint16_t height)
{// 确认起点坐标是否超出有效范围if(x > POINT_X_MAX)  return;if(y > POINT_Y_MAX)  return;
​// 确保绘制区域不会超出最大范围,如果超出则调整宽度和高度if(x + width > POINT_X_MAX)     width = POINT_X_MAX - x;if(y + height > POINT_Y_MAX)    height = POINT_Y_MAX - y;
​// 遍历高度范围中的每个像素行for(uint8_t i = y; i < y + height; i++){for(uint8_t j = x; j < x + width; j++){// 反转显存GRAM中的指定像素位(按位异或)OLED_GRAM[i / 8][j] ^= (0x01 << (i % 8));}}
}
区域更新
void oled_helper_update_area(OLED_Handle* handle, uint16_t x, uint16_t y, uint16_t width, uint16_t height)
{// 检查起点坐标是否超出有效范围if(x > POINT_X_MAX)  return;if(y > POINT_Y_MAX)  return;
​// 确认绘制区域不超出最大范围if(x + width > POINT_X_MAX)     width = POINT_X_MAX - x;if(y + height > POINT_Y_MAX)    height = POINT_Y_MAX - y;
​// 定义OLED操作表变量OLED_Operations op_table;// 获取对应的操作函数表__on_fetch_oled_table(handle, &op_table);
​// 遍历绘制区域中的每个页(8像素一页)for(uint8_t i = y / 8; i < (y + height - 1) / 8 + 1; i++){// 设置光标到指定页及列的位置__pvt_oled_set_cursor(handle, i, x);// 从显存中读取指定页和列的数据,通过data_sender发送到OLED硬件op_table.data_sender(handle, &OLED_GRAM[i][x], width);        }
}

也就是将光标对应到位置上刷新width个数据,完事!

区域清空
void oled_helper_clear_area(OLED_Handle* handle, uint16_t x, uint16_t y, uint16_t width, uint16_t height)
{// 检查起点坐标是否超出有效范围if(x > POINT_X_MAX)  return;if(y > POINT_Y_MAX)  return;
​// 确保绘制区域不超出最大范围if(x + width > POINT_X_MAX)     width = POINT_X_MAX - x;if(y + height > POINT_Y_MAX)    height = POINT_Y_MAX - y;
​// 遍历高度范围内的所有像素for(uint8_t i = y; i < y + height; i++){for(uint8_t j = x; j < x + width; j++){// 清除显存中的指定像素位(按位与非操作)OLED_GRAM[i / 8][j] &= ~(0x01 << (i % 8));}}
}
 
  1. OLED_GRAM[i / 8][j]

    • 访问显存缓冲区中指定位置的字节。

    • i / 8 确定当前像素所在的页,因为 OLED 每页存储 8 个垂直像素。

    • j 为水平方向的列位置。

  2. 0x01 << (i % 8)

    • 生成一个掩码,将 0x01 左移 (i % 8) 位。

    • i % 8 计算出在当前页中的垂直位偏移。

  3. ~(0x01 << (i % 8))

    • 对掩码取反,生成一个用于清零的掩码。例如,如果 i % 8 == 2,则 0x01 << 20b00000100,取反后得到 0b11111011

  4. &=

    • 按位与运算,将显存当前位置对应的像素清零,而其他位保持不变。

假设 i = 10j = 5

  • i / 8 = 1 表示访问第 2 页(页索引为 1);

  • i % 8 = 2 表示需要清除该页第 3 位的像素;

  • 0x01 << 2 = 0b00000100,取反得到 0b11111011

  • OLED_GRAM[1][5] &= 0b11111011 会将第 3 位清零,其余位保持不变。

测试我们的抽象

现在,我们终于可以开始测试我们的抽象了。完成了既可以使用软件IIC,又可以使用硬件IIC进行通信的OLED抽象,我们当然迫不及待的想要测试一下我们的功能是否完善。笔者这里刹住车,耐下性子听几句话。

首先,测试不是一番风顺的,我们按照我们的期望对着接口写出了功能代码,基本上不会一番风顺的得到自己想要的结果,往往需要我们进行调试,找到其中的问题,修正然后继续测试。

整理一下,我们应该如何使用?

首先回顾接口。我们需要指定一个协议按照我们期望的方式进行通信。在上一篇博客中,我们做完了协议层次的抽象,在这里,我们只需要老老实实的注册接口就好了。

指引:如果你忘记了我们上一篇博客在做什么的话,请参考从0开始使用面对对象C语言搭建一个基于OLED的图形显示框架2-CSDN博客!

笔者建议,新建一个Test文件夹,书写一个文件叫:oled_test_hard_iic.coled_test_soft_iic.c测试我们的设备层和协议层是正确工作的。笔者这里以测试硬件IIC的代码为例子。

新建一个CubeMX工程,只需要简单的配置一下IIC就好了(笔者选择的是Fast Mode,为了方便以后测试我们的组件刷新),之后,只需要

#include "OLED/Driver/hard_iic/hard_iic.h"
#include "Test/OLED_TEST/oled_test.h"
#include "i2c.h"
/* configs should be in persist way */
OLED_HARD_IIC_Private_Config config;
​
void user_init_hard_iic_oled_handle(OLED_Handle* handle)
{bind_hardiic_handle(&config, &hi2c1, 0x78, HAL_MAX_DELAY);oled_init_hardiic_handle(handle, &config);
}

bind_hardiic_handle注册了使用硬件IIC通信的协议实体,我们将一个空白的config,注册了配置好的iic的HAL库句柄,提供了IIC地址和最大可接受的延迟时间

oled_init_hardiic_handle则是进一步的从协议层飞跃到设备层,完成一个OLED设备的注册,即,我们注册了一个使用硬件IIC通信的OLED。现在,我们就可以直接拿这个OLED进行绘点了。

void test_set_pixel_line(OLED_Handle* handle, uint8_t xoffset, uint8_t y_offset)
{for(uint8_t i = 0; i < 20; i++)oled_helper_setpixel(handle,xoffset * i, y_offset * i);oled_helper_update(handle);
}
​
void test_oled_iic_functionalities()
{OLED_Handle handle;// 注册了一个使用硬件IIC通信的OLEDuser_init_hard_iic_oled_handle(&handle);// 绘制一个test_set_pixel_line(&handle, 1, 2);HAL_Delay(1000);test_clear(&handle);test_set_pixel_line(&handle, 2, 1);HAL_Delay(1000);test_clear(&handle);
}

这个测试并不全面,自己可以做修改。效果就是在导言当中的视频开始的两条直线所示。

笔者的OLED设备层的代码已经全部开源到MCU_Libs/OLED/library/OLED at main · Charliechen114514/MCU_Libs (github.com),感兴趣的朋友可以进一步研究。

目录导览

总览

协议层封装

OLED设备封装

绘图设备抽象

基础图形库封装

基础组件实现

动态菜单组件实现

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/11645.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL(高级特性篇) 13 章——事务基础知识

一、数据库事务概述 事务是数据库区别于文件系统的重要特性之一 &#xff08;1&#xff09;存储引擎支持情况 SHOW ENGINES命令来查看当前MySQL支持的存储引擎都有哪些&#xff0c;以及这些存储引擎是否支持事务能看出在MySQL中&#xff0c;只有InnoDB是支持事务的 &#x…

影视文件大数据高速分发方案

在当今的数字时代&#xff0c;影视行业的内容创作和传播方式经历了翻天覆地的变化。随着4K、8K高清视频的普及&#xff0c;以及虚拟现实(VR)和增强现实(AR)技术的发展&#xff0c;影视文件的数据量正以前所未有的速度增长。这就要求行业内的参与者必须拥有高效的大数据传输解决…

C语言教程——文件处理(2)

目录 前言 一、顺序读写函数&#xff08;续&#xff09; 1.1fprintf 1.2fscanf 1.3fwrite 1.4fread 二、流和标准流 2.1流 2.2标准流 2.3示例 三、sscanf和sprintf 3.1sprintf 3.2sscanf 四、文件的随机读写 4.1fseek 4.2ftell 4.3rewind 五、文件读取结束的…

建表注意事项(2):表约束,主键自增,序列[oracle]

没有明确写明数据库时,默认基于oracle 约束的分类 用于确保数据的完整性和一致性。约束可以分为 表级约束 和 列级约束&#xff0c;区别在于定义的位置和作用范围 复合主键约束: 主键约束中有2个或以上的字段 复合主键的列顺序会影响索引的使用&#xff0c;需谨慎设计 添加…

线性回归的损失和优化02

线性回归的损失和优化 学习目标 知道线性回归中损失函数知道使用正规方程对损失函数优化的过程知道使用梯度下降法对损失函数优化的过程 假设刚才的房子例子&#xff0c;真实的数据之间存在这样的关系&#xff1a; 真实关系&#xff1a; 真实房子价格 0.02中心区域的距离 0.…

年化18%-39.3%的策略集 | backtrader通过xtquant连接qmt实战

原创内容第785篇&#xff0c;专注量化投资、个人成长与财富自由。 大年初五&#xff0c;年很快就过完了。 其实就是本身也只是休假一周&#xff0c;但是我们赋予了它太多意义。 周五咱们发布发aitrader v4.1&#xff0c;带了backtraderctp期货的实盘接口&#xff1a; aitra…

【数据结构】_链表经典算法OJ(力扣/牛客第二弹)

目录 1. 题目1&#xff1a;返回倒数第k个节点 1.1 题目链接及描述 1.2 解题思路 1.3 程序 2. 题目2&#xff1a;链表的回文结构 2.1 题目链接及描述 2.2 解题思路 2.3 程序 1. 题目1&#xff1a;返回倒数第k个节点 1.1 题目链接及描述 题目链接&#xff1a; 面试题 …

成绩案例demo

本案例较为简单&#xff0c;用到的知识有 v-model、v-if、v-else、指令修饰符.prevent .number .trim等、computed计算属性、toFixed方法、reduce数组方法。 涉及的功能需求有&#xff1a;渲染、添加、删除、修改、统计总分&#xff0c;求平均分等。 需求效果如下&#xff1a…

git基础使用--4---git分支和使用

文章目录 git基础使用--4---git分支和使用1. 按顺序看2. 什么是分支3. 分支的基本操作4. 分支的基本操作4.1 查看分支4.2 创建分支4.3 切换分支4.4 合并冲突 git基础使用–4—git分支和使用 1. 按顺序看 -git基础使用–1–版本控制的基本概念 -git基础使用–2–gti的基本概念…

Kafka下载

一、Kafka下载 下载地址&#xff1a;https://kafka.apache.org/downloads 二、Kafka安装 因为选择下载的是 .zip 文件&#xff0c;直接跳过安装&#xff0c;一步到位。 选择在任一磁盘创建空文件夹&#xff08;不要使用中文路径&#xff09;&#xff0c;解压之后把文件夹内容…

nodejs:express + js-mdict 网页查询英汉词典,能播放声音

向 DeepSeek R1 提问&#xff1a; 我想写一个Web 前端网页&#xff0c;后台用 nodejs js-mdict, 实现在线查询英语单词 1. 项目结构 首先&#xff0c;创建一个项目目录&#xff0c;结构如下&#xff1a; mydict-app/ ├── public/ │ ├── index.html │ ├── st…

【自开发工具介绍】SQLSERVER的ImpDp和ExpDp工具01

1、开发背景 大家都很熟悉&#xff0c;Oracle提供了Impdp和ExpDp工具&#xff0c;功能很强大&#xff0c;可以进行db的导入导出的处理。但是对于Sqlserver数据库只是提供了简单的图形化的导出导入工具&#xff0c;在实际的开发和生产环境不太可能让用户在图形化的界面选择移行…

【Block总结】完全注意力Fully Attentional,同时捕捉空间和通道的注意力|即插即用

论文信息 标题: Fully Attentional Network for Semantic Segmentation论文链接: https://arxiv.org/pdf/2112.04108GitHub链接: https://github.com/maggiesong7/FullyAttentional 创新点 全注意力模块&#xff08;FLA&#xff09;: 该模块能够在一个相似性图中同时捕捉空…

强化学习、深度学习、深度强化学习的区别是什么?

前言 深度强化学习就是 深度学习 和 强化学习 的结合体。它让计算机程序&#xff08;也就是智能体&#xff09;在特定环境中不断尝试&#xff0c;从错误中学习&#xff0c;最终找到最优的行动策略。 深度学习是AlphaGo从棋谱里学习&#xff0c;强化学些Alphazero 学习规则&am…

99.20 金融难点通俗解释:中药配方比喻马科维茨资产组合模型(MPT)

目录 0. 承前1. 核心知识点拆解2. 中药搭配比喻方案分析2.1 比喻的合理性 3. 通俗易懂的解释3.1 以中药房为例3.2 配方原理 4. 实际应用举例4.1 基础配方示例4.2 效果说明 5. 注意事项5.1 个性化配置5.2 定期调整 6. 总结7. 代码实现 0. 承前 本文主旨&#xff1a; 本文通过中…

笔灵ai写作技术浅析(四):知识图谱

知识图谱(Knowledge Graph)是一种结构化的知识表示方式,通过将知识以图的形式进行组织,帮助AI系统更好地理解和利用信息。在笔灵AI写作中,知识图谱技术被广泛应用于结构化组织各种领域的知识,使AI能够根据写作主题快速获取相关的背景知识、概念关系等,从而为生成内容提供…

基于python的Kimi AI 聊天应用

因为这几天deepseek有点状况&#xff0c;导致apikey一直生成不了&#xff0c;用kimi练练手。这是一个基于 Moonshot AI 的 Kimi 接口开发的聊天应用程序&#xff0c;使用 Python Tkinter 构建图形界面。 项目结构 项目由三个主要Python文件组成&#xff1a; 1. main_kimi.py…

python算法和数据结构刷题[5]:动态规划

动态规划&#xff08;Dynamic Programming, DP&#xff09;是一种算法思想&#xff0c;用于解决具有最优子结构的问题。它通过将大问题分解为小问题&#xff0c;并找到这些小问题的最优解&#xff0c;从而得到整个问题的最优解。动态规划与分治法相似&#xff0c;但区别在于动态…

JavaFX - 3D 形状

在前面的章节中&#xff0c;我们已经了解了如何在 JavaFX 应用程序中的 XY 平面上绘制 2D 形状。除了这些 2D 形状之外&#xff0c;我们还可以使用 JavaFX 绘制其他几个 3D 形状。 通常&#xff0c;3D 形状是可以在 XYZ 平面上绘制的几何图形。它们由两个或多个维度定义&#…

wax到底是什么意思

在很久很久以前&#xff0c;人类还没有诞生文字之前&#xff0c;人类就产生了语言&#xff1b;在诞生文字之前&#xff0c;人类就已经使用了语言很久很久。 没有文字之前&#xff0c;人们的语言其实是相对比较简单的&#xff0c;因为人类的生产和生活水平非常低下&#xff0c;…