从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);
,笔者的分析如下
OLED_GRAM[y / 8 + j + 1][x + i]
:
这是下一页显存中的对应位置。
y / 8 + j + 1
表示当前绘制位置的下一页。
x + i
仍为当前列位置。
sources[j * width + i]
:
源图像数据中当前像素的数据。
j * width + i
计算出当前像素在源数据中的位置。
>> (8 - y % 8)
:
将数据右移
(8 - y % 8)
位,将超出当前页的高位部分对齐到下一页。
8 - y % 8
计算需要移入下一页的位数。
|=
:
按位或,将偏移后的数据合并到下一页显存中,以保留已有内容。
假设
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));}}
}
OLED_GRAM[i / 8][j]
:
访问显存缓冲区中指定位置的字节。
i / 8
确定当前像素所在的页,因为 OLED 每页存储 8 个垂直像素。
j
为水平方向的列位置。
0x01 << (i % 8)
:
生成一个掩码,将
0x01
左移(i % 8)
位。
i % 8
计算出在当前页中的垂直位偏移。
~(0x01 << (i % 8))
:
对掩码取反,生成一个用于清零的掩码。例如,如果
i % 8 == 2
,则0x01 << 2
为0b00000100
,取反后得到0b11111011
。
&=
:
按位与运算,将显存当前位置对应的像素清零,而其他位保持不变。
假设
i = 10
,j = 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.c
和oled_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设备封装
绘图设备抽象
基础图形库封装
基础组件实现
动态菜单组件实现
相关文章:

从0开始使用面对对象C语言搭建一个基于OLED的图形显示框架(OLED设备层封装)
目录 OLED设备层驱动开发 如何抽象一个OLED 完成OLED的功能 初始化OLED 清空屏幕 刷新屏幕与光标设置1 刷新屏幕与光标设置2 刷新屏幕与光标设置3 绘制一个点 反色 区域化操作 区域置位 区域反色 区域更新 区域清空 测试我们的抽象 整理一下,我们应…...

【Redis】Redis 经典面试题解析:深入理解 Redis 的核心概念与应用
Redis 是一个高性能的键值存储系统,广泛应用于缓存、消息队列、排行榜等场景。在面试中,Redis 是一个高频话题,尤其是其核心概念、数据结构、持久化机制和高可用性方案。 1. Redis 是什么?它的主要特点是什么? 答案&a…...

TensorFlow 示例摄氏度到华氏度的转换(一)
TensorFlow 实现神经网络模型来进行摄氏度到华氏度的转换,可以将其作为一个回归问题来处理。我们可以通过神经网络来拟合这个简单的转换公式。 1. 数据准备与预处理 2. 构建模型 3. 编译模型 4. 训练模型 5. 评估模型 6. 模型应用与预测 7. 保存与加载模型 …...

7.DP算法
DP 在C中,动态规划(Dynamic Programming,DP)是一种通过将复杂问题分解为重叠子问题来高效求解的算法设计范式。以下是DP算法的核心要点和实现方法: 一、动态规划的核心思想 重叠子问题:问题可分解为多个重…...

Baklib构建高效协同的基于云的内容中台解决方案
内容概要 随着云计算技术的飞速发展,内容管理的方式也在不断演变。企业面临着如何在数字化转型过程中高效管理和协同处理内容的新挑战。为应对这些挑战,引入基于云的内容中台解决方案显得尤为重要。 Baklib作为创新型解决方案提供商,致力于…...

在C语言多线程环境中使用互斥量
如果有十个银行账号通过不同的十条线程同时向同一个账号转账时,如果没有很好的机制保证十个账号依次存入,那么这些转账可能出问题。我们可以通过互斥量来解决。 C标准库提供了这个互斥量,只需要引入threads.头文件。 互斥量就像是一把锁&am…...

项目练习:重写若依后端报错cannot be cast to com.xxx.model.LoginUser
文章目录 一、情景说明二、解决办法 一、情景说明 在重写若依后端服务的过程中 使用了Redis存放LoginUser对象数据 那么,有存就有取 在取值的时候,报错 二、解决办法 方法1、在TokenService中修改如下 getLoginUser 方法中:LoginUser u…...

代码随想录刷题笔记
数组 二分查找 ● 704.二分查找 tips:两种方法,左闭右开和左闭右闭,要注意区间不变性,在判断mid的值时要看mid当前是否使用过 ● 35.搜索插入位置 ● 34.在排序数组中查找元素的第一个和最后一个位置 tips:寻找左右边…...

AI智慧社区--人脸识别
前端 人脸的采集按钮: 首先对于选中未认证的居民记录,进行人脸采集 前端的按钮 <el-form-item><el-button v-has"sys:person:info" type"info" icon"el-icon-camera" :disabled"ids.length < 0" …...

对象的实例化、内存布局与访问定位
一、创建对象的方式 二、创建对象的步骤: 一、判断对象对应的类是否加载、链接、初始化: 虚拟机遇到一条new指令,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化…...

React基础知识回顾详解
以下是React从前端面试基础到进阶的系统性学习内容,包含核心知识点和常见面试题解析: 一、React基础核心 JSX原理与本质 JSX编译过程(Babel转换)虚拟DOM工作原理面试题:React为何使用className而不是class?…...

开发第一个安卓页面
一:在java.com.example.myapplication下创建MainActivity的JAVA类 里面的代码要把xml的页面名字引入 二:如果没有这两个,可以手动创建layout文件夹和activity_main.xml activity_main.xml使用来做页面的。 三、找到这个文件 把你的JAVA类引入…...

物联网 STM32【源代码形式-ESP8266透传】连接OneNet IOT从云产品开发到底层MQTT实现,APP控制 【保姆级零基础搭建】
一、MQTT介绍 MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议)是一种基于发布/订阅模式的轻量级通讯协议,构建于TCP/IP协议之上。它最初由IBM在1999年发布,主要用于在硬件性能受限和网络状况不佳的情…...

微服务-配置管理
配置管理 到目前为止我们已经解决了微服务相关的几个问题: 微服务远程调用微服务注册、发现微服务请求路由、负载均衡微服务登录用户信息传递 不过,现在依然还有几个问题需要解决: 网关路由在配置文件中写死了,如果变更必须重…...

基于SpringBoot的智慧康老疗养院管理系统的设计与实现(源码+SQL脚本+LW+部署讲解等)
专注于大学生项目实战开发,讲解,毕业答疑辅导,欢迎高校老师/同行前辈交流合作✌。 技术范围:SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:…...

100.1 AI量化面试题:解释夏普比率(Sharpe Ratio)的计算方法及其在投资组合管理中的应用,并说明其局限性
目录 0. 承前1. 夏普比率的基本概念1.1 定义与计算方法1.2 实际计算示例 2. 在投资组合管理中的应用2.1 投资组合选择2.2 投资组合优化 3. 夏普比率的局限性3.1 统计假设的限制3.2 实践中的问题 4. 改进方案4.1 替代指标4.2 实践建议 5. 回答话术 0. 承前 如果想更加全面清晰地…...

LLMs之OpenAI o系列:OpenAI o3-mini的简介、安装和使用方法、案例应用之详细攻略
LLMs之OpenAI o系列:OpenAI o3-mini的简介、安装和使用方法、案例应用之详细攻略 目录 相关文章 LLMs之o3:《Deliberative Alignment: Reasoning Enables Safer Language Models》翻译与解读 LLMs之OpenAI o系列:OpenAI o3-mini的简介、安…...

深度解析:网站快速收录与网站安全性的关系
本文转自:百万收录网 原文链接:https://www.baiwanshoulu.com/58.html 网站快速收录与网站安全性之间存在着密切的关系。以下是对这一关系的深度解析: 一、网站安全性对收录的影响 搜索引擎惩罚: 如果一个网站存在安全隐患&am…...

【Rust自学】16.2. 使用消息传递来跨线程传递数据
喜欢的话别忘了点赞、收藏加关注哦,对接下来的教程有兴趣的可以关注专栏。谢谢喵!(・ω・) 16.2.1. 消息传递 有一种很流行而且能保证安全并发的技术(或者叫机制)叫做消息传递。在这种机制里,线…...

如何实现滑动网格的功能
文章目录 1 概念介绍2 使用方法3 示例代码 我们在上一章回中介绍了SliverList组件相关的内容,本章回中将介绍SliverGrid组件.闲话休提,让我们一起Talk Flutter吧。 1 概念介绍 我们在本章回中介绍的SliverGrid组件是一种网格类组件,主要用来…...

使用C# 如何获取本机连接的WIFI名称[C# ---1]
前言 楼主最近在写一个WLAN上位机,遇到了使用C#查询SSID 的问题。CSDN上很多文章都比较老了,而且代码过于复杂。楼主自己想了一个使用CMD来获得SSID的方法 C#本身是没有获得WINDOWS网路信息的能力,必须要用系统API,WMI什么的&…...

【Docker】快速部署 Nacos 注册中心
【Docker】快速部署 Nacos 注册中心 引言 Nacos 注册中心是一个用于服务发现和配置管理的开源项目。提供了动态服务发现、服务健康检查、动态配置管理和服务管理等功能,帮助开发者更轻松地构建微服务架构。 仓库地址 https://github.com/alibaba/nacos 步骤 拉取…...

OpenCV:闭运算
目录 1. 简述 2. 用膨胀和腐蚀实现闭运算 2.1 代码示例 2.2 运行结果 3. 闭运算接口 3.1 参数详解 3.2 代码示例 3.3 运行结果 4. 闭运算的应用场景 5. 注意事项 相关阅读 OpenCV:图像的腐蚀与膨胀-CSDN博客 OpenCV:开运算-CSDN博客 1. 简述…...

Python | Pytorch | Tensor知识点总结
如是我闻: Tensor 是我们接触Pytorch了解到的第一个概念,这里是一个关于 PyTorch Tensor 主题的知识点总结,涵盖了 Tensor 的基本概念、创建方式、运算操作、梯度计算和 GPU 加速等内容。 1. Tensor 基本概念 Tensor 是 PyTorch 的核心数据结…...

aws(学习笔记第二十六课) 使用AWS Elastic Beanstalk
aws(学习笔记第二十六课) 使用aws Elastic Beanstalk 学习内容: AWS Elastic Beanstalk整体架构AWS Elastic Beanstalk的hands onAWS Elastic Beanstalk部署node.js程序包练习使用AWS Elastic Beanstalk的ebcli 1. AWS Elastic Beanstalk整体架构 官方的guide AWS…...

《OpenCV》——图像透视转换
图像透视转换简介 在 OpenCV 里,图像透视转换属于重要的几何变换,也被叫做投影变换。下面从原理、实现步骤、相关函数和应用场景几个方面为你详细介绍。 原理 实现步骤 选取对应点:要在源图像和目标图像上分别找出至少四个对应的点。这些对…...

9 点结构模块(point.rs)
一、point.rs源码 use super::UnknownUnit; use crate::approxeq::ApproxEq; use crate::approxord::{max, min}; use crate::length::Length; use crate::num::*; use crate::scale::Scale; use crate::size::{Size2D, Size3D}; use crate::vector::{vec2, vec3, Vector2D, V…...

Java线程认识和Object的一些方法ObjectMonitor
专栏系列文章地址:https://blog.csdn.net/qq_26437925/article/details/145290162 本文目标: 要对Java线程有整体了解,深入认识到里面的一些方法和Object对象方法的区别。认识到Java对象的ObjectMonitor,这有助于后面的Synchron…...

【高等数学】贝塞尔函数
贝塞尔函数(Bessel functions)是数学中一类重要的特殊函数,通常用于解决涉及圆对称或球对称的微分方程。它们在物理学、工程学、天文学等多个领域都有广泛的应用,例如在波动方程、热传导方程、电磁波传播等问题中。 贝塞尔函数的…...

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. 承前 本文主旨: 本文通过中…...