从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 绘制一个点 反色 区域化操作 区域置位 区域反色 区域更新 区域清空 测试我们的抽象 整理一下,我们应…...
大模型能力评估数据集都有哪些?
大模型能力的评估数据集种类繁多,涵盖了语言理解、推理、生成、代码能力、安全性和鲁棒性等多个方面。以下是一些主要的评估数据集及其特点: 通用能力评估数据集: MMLU:多模态大规模多语言任务理解数据集,覆盖从基础教育到高级专业水平的57个科目,用于评估模型的知识储备…...
论文阅读(二):理解概率图模型的两个要点:关于推理和学习的知识
1.论文链接:Essentials to Understand Probabilistic Graphical Models: A Tutorial about Inference and Learning 摘要: 本章的目的是为没有概率图形模型背景或没有深入背景的科学家提供一个高级教程。对于更熟悉这些模型的读者,本章将作为…...
《OpenCV》——图像透视转换
图像透视转换简介 在 OpenCV 里,图像透视转换属于重要的几何变换,也被叫做投影变换。下面从原理、实现步骤、相关函数和应用场景几个方面为你详细介绍。 原理 实现步骤 选取对应点:要在源图像和目标图像上分别找出至少四个对应的点。这些对…...
【16届蓝桥杯寒假刷题营】第2期DAY4
【16届蓝桥杯寒假刷题营】第2期DAY4 - 蓝桥云课 问题描述 幼儿园小班的浩楠同学有一个序列 a。 他想知道有多少个整数三元组 (i,j,k) 满足 1≤i,j,k≤n 且 aiajak。 输入格式 共2行,第一行一个整数 n,表示序列的长度。 第二行 n 个整数&#x…...
用 HTML、CSS 和 JavaScript 实现抽奖转盘效果
顺序抽奖 前言 这段代码实现了一个简单的抽奖转盘效果。页面上有一个九宫格布局的抽奖区域,周围八个格子分别放置了不同的奖品名称,中间是一个 “开始抽奖” 的按钮。点击按钮后,抽奖区域的格子会快速滚动,颜色不断变化…...
【人工智能学习笔记 一】 AI分层架构、基本概念分类与产品技术架构
新的一年2025要对AI以及LLM有个强化的学习,所以第一篇先对整体有个大概的认知,一直分不清LLM和AI的关系,在整个体系里的位置,以及AIGC是什么东西,AI AGENT类似豆包等和大语言模型的具体关系是什么,整个AI的…...
windows10 配置使用json server作为图片服务器
步骤1:在vs code中安装json server, npm i -g json-server 注意:需要安装对应版本的json server,不然可能会报错,比如: npm i -g json-server 0.16.3 步骤2:出现如下报错: json-server 不是…...
【Elasticsearch 基础入门】Centos7下Elasticsearch 7.x安装与配置(单机)
Elasticsearch系列文章目录 【Elasticsearch 基础入门】一文带你了解Elasticsearch!!!【Elasticsearch 基础入门】Centos7下Elasticsearch 7.x安装与配置(单机) 目录 Elasticsearch系列文章目录前言单机模式1. 安装 J…...
【MySQL】语言连接
语言连接 一、下载二、mysql_get_client_info1、函数2、介绍3、示例 三、其他函数1、mysql_init2、mysql_real_connect3、mysql_query4、mysql_store_result5、mysql_free_result6、mysql_num_fields7、mysql_num_rows8、mysql_fetch_fields9、mysql_fetch_row10、mysql_close …...
【零拷贝】
目录 一:了解IO基础概念 二:数据流动的层次结构 三:零拷贝 1.传统IO文件读写 2.mmap 零拷贝技术 3.sendFile 零拷贝技术 一:了解IO基础概念 理解CPU拷贝和DMA拷贝 我们知道,操作系统对于内存空间&…...
四、GPIO中断实现按键功能
4.1 GPIO简介 输入输出(I/O)是一个非常重要的概念。I/O泛指所有类型的输入输出端口,包括单向的端口如逻辑门电路的输入输出管脚和双向的GPIO端口。而GPIO(General-Purpose Input/Output)则是一个常见的术语,…...
qt-Quick3D笔记之官方例程Runtimeloader Example运行笔记
qt-Quick3D笔记之官方例程Runtimeloader Example运行笔记 文章目录 qt-Quick3D笔记之官方例程Runtimeloader Example运行笔记1.例程运行效果2.例程缩略图3.项目文件列表4.main.qml5.main.cpp6.CMakeLists.txt 1.例程运行效果 运行该项目需要自己准备一个模型文件 2.例程缩略图…...
IM 即时通讯系统-01-概览
前言 有时候希望有一个 IM 工具,比如日常聊天,或者接受报警信息。 其实主要是工作使用,如果是接收报警等场景,其实DD这种比较符合场景。 那么有没有必要再创造一个DD呢? 答案是如果处于个人的私有化使用࿰…...
二叉树——429,515,116
今天继续做关于二叉树层序遍历的相关题目,一共有三道题,思路都借鉴于最基础的二叉树的层序遍历。 LeetCode429.N叉树的层序遍历 这道题不再是二叉树了,变成了N叉树,也就是该树每一个节点的子节点数量不确定,可能为2&a…...
Baklib构建高效协同的基于云的内容中台解决方案
内容概要 随着云计算技术的飞速发展,内容管理的方式也在不断演变。企业面临着如何在数字化转型过程中高效管理和协同处理内容的新挑战。为应对这些挑战,引入基于云的内容中台解决方案显得尤为重要。 Baklib作为创新型解决方案提供商,致力于…...
MP4基础
一、什么是MP4? MP4是一套用于音频、视频信息的压缩编码标准,由国际标准化组织(ISO)和国际电工委员会(IEC)下属的“动态图像专家组”(Moving Picture Experts Group,即MPEGÿ…...
年化18%-39.3%的策略集 | backtrader通过xtquant连接qmt实战
原创内容第785篇,专注量化投资、个人成长与财富自由。 大年初五,年很快就过完了。 其实就是本身也只是休假一周,但是我们赋予了它太多意义。 周五咱们发布发aitrader v4.1,带了backtraderctp期货的实盘接口: aitra…...
通过Redisson构建延时队列并实现注解式消费
目录 一、序言二、延迟队列实现1、Redisson延时消息监听注解和消息体2、Redisson延时消息发布器3、Redisson延时消息监听处理器 三、测试用例四、结语 一、序言 两个月前接了一个4万的私活,做一个线上商城小程序,在交易过程中不可避免的一个问题就是用户…...
RAG是否被取代(缓存增强生成-CAG)吗?
引言: 本文深入研究一种名为缓存增强生成(CAG)的新技术如何工作并减少/消除检索增强生成(RAG)弱点和瓶颈。 LLMs 可以根据输入给他的信息给出对应的输出,但是这样的工作方式很快就不能满足应用的需要: 因…...
MiniMax:人工智能领域的创新先锋
MiniMax:人工智能领域的创新先锋 在人工智能领域,MiniMax正以其强大的技术实力和创新的模型架构,成为全球关注的焦点。作为一家成立于2021年12月的通用人工智能科技公司,MiniMax专注于开发多模态、万亿参数的MoE(Mixt…...
pytorch基于GloVe实现的词嵌入
PyTorch 实现 GloVe(Global Vectors for Word Representation) 的完整代码,使用 中文语料 进行训练,包括 共现矩阵构建、模型定义、训练和测试。 1. GloVe 介绍 基于词的共现信息(不像 Word2Vec 使用滑动窗口预测&…...
Unity实现按键设置功能代码
一、前言 最近在学习unity2D,想做一个横版过关游戏,需要按键设置功能,让用户可以自定义方向键与攻击键等。 自己写了一个,总结如下。 二、界面效果图 这个是一个csv文件,准备第一列是中文按键说明,第二列…...
C++ 入门速通-第3章【黑马】
内容来源于:黑马 集成开发环境:CLion 先前学习完了C第1章的内容: C 入门速通-第1章【黑马】-CSDN博客 C 入门速通-第2章【黑马】-CSDN博客 下面继续学习第3章: 数组: 字符数组: 多维数组: …...
JavaScript 中的 CSS 与页面响应式设计
JavaScript 中的 CSS 与页面响应式设计 JavaScript 中的 CSS 与页面响应式设计1. 引言2. JavaScript 与 CSS 的基本概念2.1 CSS 的作用2.2 JavaScript 的作用3. 动态控制样式:JavaScript 修改 CSS 的方法3.1 使用 `document.styleSheets` API3.2 使用 `classList` 修改类3.3 使…...
100.3 AI量化面试题:解释配对交易(Pairs Trading)的原理,并说明如何选择配对股票以及设计交易信号
目录 0. 承前1. 配对交易基本原理1.1 什么是配对交易1.2 基本假设 2. 配对选择方法2.1 相关性分析2.2 协整性检验 3. 价差计算方法3.1 简单价格比率3.2 回归系数法 4. 交易信号设计4.1 标准差方法4.2 动态阈值方法 5. 风险管理5.1 止损设计5.2 仓位管理 6. 策略评估6.1 回测框架…...
[SAP ABAP] Debug Skill
SAP ABAP Debug相关资料 [SAP ABAP] DEBUG ABAP程序中的循环语句 [SAP ABAP] 静态断点的使用 [SAP ABAP] 在ABAP Debugger调试器中设置断点 [SAP ABAP] SE11 / SE16N 修改标准表(慎用)...
WSL2中安装的ubuntu开启与关闭探讨
1. PC开机后,查询wsl状态 在cmd或者powersell中输入 wsl -l -vNAME STATE VERSION * Ubuntu Stopped 22. 从windows访问WSL2 wsl -l -vNAME STATE VERSION * Ubuntu Stopped 23. 在ubuntu中打开一个工作区后…...
走向基于大语言模型的新一代推荐系统:综述与展望
HightLight 论文题目:Towards Next-Generation LLM-based Recommender Systems: A Survey and Beyond作者机构:吉林大学、香港理工大学、悉尼科技大学、Meta AI论文地址: https://arxiv.org/abs/2410.1974 基于大语言模型的下一代推荐系统&…...
【深度分析】DeepSeek 遭暴力破解,攻击 IP 均来自美国,造成影响有多大?有哪些好的防御措施?
技术铁幕下的暗战:当算力博弈演变为代码战争 一场针对中国AI独角兽的全球首例国家级密码爆破,揭开了数字时代技术博弈的残酷真相。DeepSeek服务器日志中持续跳动的美国IP地址,不仅是网络攻击的地理坐标,更是技术霸权对新兴挑战者的…...
双指针算法思想——OJ例题扩展算法解析思路
大家好!上一期我发布了关于双指针的OJ平台上的典型例题思路解析,基于上一期的内容,我们这一期从其中内容扩展出来相似例题进行剖析和运用,一起来试一下吧! 目录 一、 基于移动零的举一反三 题一:27. 移除…...
初始Linux(7):认识进程(下)
1. 进程优先级 cpu 资源分配的先后顺序,就是指进程的优先权( priority )。 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的 linux 很有用,可以改善系统性能。 还可以把进程运行到指定的CPU 上,这样一来…...
人工智能第2章-知识点与学习笔记
结合教材2.1节,阐述什么是知识、知识的特性,以及知识的表示。人工智能最早应用的两种逻辑是什么?阐述你对这两种逻辑表示的内涵理解。什么谓词,什么是谓词逻辑,什么是谓词公式。谈谈你对谓词逻辑中的量词的理解。阐述谓词公式的解…...
Kotlin 协程 与 Java 虚拟线程对比测试(娱乐性质,请勿严谨看待本次测试)
起因 昨天在群里聊到虚拟线程的执行效率问题的时候虽然最后的结论是虚拟线程在针对IO密集型任务时具有很大的优势。但是讨论到虚拟线程和Kotlin 的协程的优势对比的话,这时候所有人都沉默了。所以有了本次的测试 提前声明:本次测试是不严谨的࿰…...
C++中的拷贝构造器(Copy Constructor)
在C中,拷贝构造器(Copy Constructor)是一种特殊的构造函数,用于创建一个新对象,该对象是另一个同类型对象的副本。当使用一个已存在的对象来初始化一个新对象时,拷贝构造器会被调用。 拷贝构造器的定义 拷…...
Spring Boot项目如何使用MyBatis实现分页查询
写在前面:大家好!我是晴空๓。如果博客中有不足或者的错误的地方欢迎在评论区或者私信我指正,感谢大家的不吝赐教。我的唯一博客更新地址是:https://ac-fun.blog.csdn.net/。非常感谢大家的支持。一起加油,冲鸭&#x…...
独立开发经验谈:如何借助 AI 辅助产品 UI 设计
我在业余时间开发了一款自己的独立产品:升讯威在线客服与营销系统。陆陆续续开发了几年,从一开始的偶有用户尝试,到如今线上环境和私有化部署均有了越来越多的稳定用户,在这个过程中,我也积累了不少如何开发运营一款独…...
笔灵ai写作技术浅析(三):深度学习
笔灵AI写作的深度学习技术主要基于Transformer架构,尤其是GPT(Generative Pre-trained Transformer)系列模型。 1. Transformer架构 Transformer架构由Vaswani等人在2017年提出,是GPT系列模型的基础。它摒弃了传统的循环神经网络(RNN)和卷积神经网络(CNN),完全依赖自…...
https数字签名手动验签
以bing.com 为例 1. CA 层级的基本概念 CA 层级是一种树状结构,由多个层级的 CA 组成。每个 CA 负责为其下一层级的实体(如子 CA 或终端实体)颁发证书。层级结构的顶端是 根 CA(Root CA),它是整个 PKI 体…...
为什么LabVIEW适合软硬件结合的项目?
LabVIEW是一种基于图形化编程的开发平台,广泛应用于软硬件结合的项目中。其强大的硬件接口支持、实时数据采集能力、并行处理能力和直观的用户界面,使得它成为工业控制、仪器仪表、自动化测试等领域中软硬件系统集成的理想选择。LabVIEW的设计哲学强调模…...
C# 操作符重载对象详解
.NET学习资料 .NET学习资料 .NET学习资料 一、操作符重载的概念 在 C# 中,操作符重载允许我们为自定义的类或结构体定义操作符的行为。通常,我们熟悉的操作符,如加法()、减法(-)、乘法&#…...
git:恢复纯版本库
初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github:codetoys,所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的,可以在任何平台上使用。 源码指引:github源…...
java异常处理——try catch finally
单个异常处理 1.当try里的代码发生了catch里指定类型的异常之后,才会执行catch里的代码,程序正常执行到结尾 2.如果try里的代码发生了非catch指定类型的异常,则会强制停止程序,报错 3.finally修饰的代码一定会执行,除…...
【架构面试】二、消息队列和MySQL和Redis
MQ MQ消息中间件 问题引出与MQ作用 常见面试问题:面试官常针对项目中使用MQ技术的候选人提问,如如何确保消息不丢失,该问题可考察候选人技术能力。MQ应用场景及作用:以京东系统下单扣减京豆为例,MQ用于交易服和京豆服…...
A4988一款常用的步进电机驱动芯片
A4988 是一款常用的步进电机驱动芯片,广泛应用于 3D 打印机、CNC 机床和小型自动化设备中。它可以驱动多种类型的步进电机,但需要根据电机的参数(如电压、电流、相数等)进行合理配置。 一、A4988 的主要特性 驱动能力:…...
TypeScript语言的语法糖
TypeScript语言的语法糖 TypeScript作为一种由微软开发的开源编程语言,它在JavaScript的基础上添加了一些强类型的特性,使得开发者能够更好地进行大型应用程序的构建和维护。在TypeScript中,不仅包含了静态类型、接口、枚举等强大的特性&…...
A星算法两元障碍物矩阵转化为rrt算法四元障碍物矩阵
对于a星算法obstacle所表示的障碍物障碍物信息,每行表示一个障碍物的坐标,例如2 , 3; % 第一个障碍物在第二行第三列,也就是边长为1的正方形障碍物右上角横坐标是2,纵坐标为3,障碍物的宽度和高度始终为1.在rrt路径规划…...
什么情况下,C#需要手动进行资源分配和释放?什么又是非托管资源?
扩展:如何使用C#的using语句释放资源?什么是IDisposable接口?与垃圾回收有什么关系?-CSDN博客 托管资源的回收有GC自动触发,而非托管资源需要手动释放。 在 C# 中,非托管资源是指那些不由 CLR(…...
【最长上升子序列Ⅱ——树状数组,二分+DP,纯DP】
题目 代码(只给出树状数组的) #include <bits/stdc.h> using namespace std; const int N 1e510; int n, m; int a[N], b[N], f[N], tr[N]; //f[i]表示以a[i]为尾的LIS的最大长度 void init() {sort(b1, bn1);m unique(b1, bn1) - b - 1;for(in…...
day37|完全背包基础+leetcode 518.零钱兑换II ,377.组合总和II
完全背包理论基础 完全背包与01背包的不同在于01背包的不同物品每个都只可以使用一次,但是完全背包的不同物品可以使用无数次 在01背包理论基础中,为了使得物品只被使用一次,我们采取倒序遍历来控制 回顾:>> for(int j …...