網(wǎng)站解析什么意思建網(wǎng)站需要多少錢和什么條件
目錄
OLED設(shè)備層驅(qū)動(dòng)開發(fā)
如何抽象一個(gè)OLED
完成OLED的功能
初始化OLED
清空屏幕
刷新屏幕與光標(biāo)設(shè)置1
刷新屏幕與光標(biāo)設(shè)置2
刷新屏幕與光標(biāo)設(shè)置3
繪制一個(gè)點(diǎn)
反色
區(qū)域化操作
區(qū)域置位
區(qū)域反色
區(qū)域更新
區(qū)域清空
測試我們的抽象
整理一下,我們應(yīng)該如何使用?
在上一篇博客:從0開始使用面對對象C語言搭建一個(gè)基于OLED的圖形顯示框架2-CSDN博客中,我們完成了協(xié)議層的抽象,現(xiàn)在讓我們更近一步,完成對設(shè)備層的抽象。
OLED設(shè)備層驅(qū)動(dòng)開發(fā)
現(xiàn)在,我們終于來到了最難的設(shè)備層驅(qū)動(dòng)開發(fā)。在這里,我們抽象出來了一個(gè)叫做OLED_Device的東西,我們終于可以關(guān)心的是一塊OLED,他可以被打開,被設(shè)置,被關(guān)閉,可以繪制點(diǎn),可以繪制面,可以清空,可以反色等等。(畫畫不是這個(gè)層次該干的事情,要知道,繪制一個(gè)圖形需要從這個(gè)設(shè)備可以被繪制開始,也就是他可以畫點(diǎn),畫面開始!)
所以,離我在這篇總覽中從0開始使用面對對象C語言搭建一個(gè)基于OLED的圖形顯示框架-CSDN博客提到的繪制一個(gè)多級菜單還是有一些遙遠(yuǎn)的。飯一口口吃,事情一步步做,這急不得,一著急反而會(huì)把我們精心維護(hù)的抽象破壞掉。
代碼在MCU_Libs/OLED/library/OLED at main · Charliechen114514/MCU_Libs (github.com),兩個(gè)文件夾都有所涉及,所以本篇的代碼量會(huì)非常巨大。請各位看官合理安排。
如何抽象一個(gè)OLED
協(xié)議層上,我們抽象了一個(gè)IIC協(xié)議?,F(xiàn)在在設(shè)備層上,我們將進(jìn)一步抽象一個(gè)OLED。上面筆者提到了,一個(gè)OLED可以被開啟,關(guān)閉,畫點(diǎn)畫面,反色等等操作,他能干!他如何干是我們馬上要做的事情?,F(xiàn)在,我們需要一個(gè)OLED句柄。這個(gè)OLED句柄代表了背后使用的通信協(xié)議和它自身相關(guān)的屬性信息,而不必要外泄到其他模塊上去。所以,封裝一個(gè)這樣的抽象變得很有必要。
OLED的品種很多,分法也很多,筆者順其自然,打算封裝一個(gè)這樣的結(jié)構(gòu)體
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標(biāo)識的類型來決定采取何種行動(dòng)解釋。。。什么呢?解釋我們的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;
也就是說,筆者按照采取的協(xié)議進(jìn)行抽象,將OLED本身的信息屬性差異封裝到文件內(nèi)部去,作為使用不同的片子,只需要使用編譯宏編譯不同的文件就好了?,F(xiàn)在,OLED_Handle就是我們的OLED,拿到這個(gè)結(jié)構(gòu)體,我們就掌握了整個(gè)OLED。所以,整個(gè)OLED結(jié)構(gòu)體必然可以做到如下的事情
#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進(jìn)行初始化
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進(jìn)行初始化
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);
?
/* 需要刷新,這里采用了緩存機(jī)制 */
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
說完了接口,下面就是實(shí)現(xiàn)了。
完成OLED的功能
初始化OLED
整個(gè)事情我們終于開始翻開我們的OLED手冊了。我們的OLED需要一定的初始化。讓我們看看江科大代碼是如何進(jìn)行OLED的初始化。
void OLED_Init(void)
{uint32_t i, j;for (i = 0; i < 1000; i++) //上電延時(shí){for (j = 0; j < 1000; j++);}OLED_I2C_Init(); //端口初始化OLED_WriteCommand(0xAE); //關(guān)閉顯示OLED_WriteCommand(0xD5); //設(shè)置顯示時(shí)鐘分頻比/振蕩器頻率OLED_WriteCommand(0x80);OLED_WriteCommand(0xA8); //設(shè)置多路復(fù)用率OLED_WriteCommand(0x3F);OLED_WriteCommand(0xD3); //設(shè)置顯示偏移OLED_WriteCommand(0x00);OLED_WriteCommand(0x40); //設(shè)置顯示開始行OLED_WriteCommand(0xA1); //設(shè)置左右方向,0xA1正常 0xA0左右反置OLED_WriteCommand(0xC8); //設(shè)置上下方向,0xC8正常 0xC0上下反置OLED_WriteCommand(0xDA); //設(shè)置COM引腳硬件配置OLED_WriteCommand(0x12);OLED_WriteCommand(0x81); //設(shè)置對比度控制OLED_WriteCommand(0xCF);OLED_WriteCommand(0xD9); //設(shè)置預(yù)充電周期OLED_WriteCommand(0xF1);OLED_WriteCommand(0xDB); //設(shè)置VCOMH取消選擇級別OLED_WriteCommand(0x30);OLED_WriteCommand(0xA4); //設(shè)置整個(gè)顯示打開/關(guān)閉OLED_WriteCommand(0xA6); //設(shè)置正常/倒轉(zhuǎn)顯示OLED_WriteCommand(0x8D); //設(shè)置充電泵OLED_WriteCommand(0x14);OLED_WriteCommand(0xAF); //開啟顯示OLED_Clear(); //OLED清屏
}
好長一大串,麻了,代碼真的不好看。我們?yōu)槭裁床皇褂脭?shù)組進(jìn)行初始化呢?
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]) )
現(xiàn)在,我們只需要按部就班的按照順序發(fā)送我們的指令。以hardiic的初始化為例子
void oled_init_hardiic_handle(OLED_Handle* handle, OLED_HARD_IIC_Private_Config* config)
{// 傳遞使用的協(xié)議句柄, 以及告知我們的句柄類型 handle->private_handle = config;handle->stored_handle_type = OLED_HARD_IIC_DRIVER_TYPE;// 按部就班的發(fā)送命令表for(uint8_t i = 0; i < CMD_TABLE_SZ; i++)// 這里我們協(xié)議的send_command就發(fā)力了, 現(xiàn)在我們完全不關(guān)心他是如何發(fā)送命令的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屏幕非常占用我們的單片機(jī)內(nèi)核,也不利于我們合并繪制操作。比如說,我想繪制兩個(gè)圓,為什么不畫完一起更新上去呢?比起來畫一個(gè)點(diǎn)更新一下,這個(gè)操作顯然更合理。所以,為了完成這樣的技術(shù),我們需要一個(gè)Buffer緩沖區(qū)。
uint8_t OLED_GRAM[OLED_HEIGHT][OLED_WIDTH];
他就承擔(dān)了我們的緩存區(qū)。多大呢?這個(gè)事情跟OLED的種類有關(guān)系,一些OLED的大小是128 x 64,另一些是144 x 64,無論如何,我們需要根據(jù)chip的種類,來選擇我們的OLED的大小,更加嚴(yán)肅的說,是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
這個(gè)文件是ssd1306.h,這個(gè)文件專門承載了關(guān)于SSD1306配置的一切?,F(xiàn)在,我們將OLED的配置系統(tǒng)建立起來了,當(dāng)我們的chip是SSD1306的時(shí)候,只需要定義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
現(xiàn)在,我們的configure就完整了,我們只需要依賴config文件就能知道OLED自身的全部信息。如果你有IDE,現(xiàn)在就可以看到,當(dāng)我們定義了SSD1306的時(shí)候,我們的OLED_GRAM自動(dòng)調(diào)整為OLED_GRAM[8][128]
的數(shù)組,另一放面,如果我們使用了SSD1309,我們自動(dòng)會(huì)更新為OLED_GRAM[8][144]
,此事在ssd1309.h中亦有記載
清空屏幕
顯然,我們有一些人對C庫并不太了解,memset函數(shù)負(fù)責(zé)將一塊內(nèi)存設(shè)置為給定的值。一般而言,編譯器實(shí)現(xiàn)將會(huì)使用獨(dú)有的硬件加速優(yōu)化,使用上,絕對比手動(dòng)設(shè)置值只快不慢。
軟件工程的一大原則:復(fù)用!能不自己手搓就不自己手搓,編譯器提供了就優(yōu)先使用編譯器提供的
void oled_helper_clear_frame(OLED_Handle* handle)
{memset(OLED_GRAM, 0, sizeof(OLED_GRAM));
}
刷新屏幕與光標(biāo)設(shè)置1
設(shè)置涂寫光標(biāo),就像我們使用Windows的繪圖軟件一樣,鼠標(biāo)在哪里,左鍵嗯下就從那里開始繪制,我們的set_cursor函數(shù)就是干設(shè)置鼠標(biāo)在哪里的工作。查詢手冊,我們可以這樣書寫(筆者是直接參考了江科大的實(shí)現(xiàn))
/*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需要,這一點(diǎn)可以去我的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)); //設(shè)置X位置高4位op_table.command_sender(handle->private_handle,0x00 | (new_x & 0x0F)); //設(shè)置X位置低4位
}
刷新屏幕與光標(biāo)設(shè)置2
不對,這個(gè)代碼沒有看懂!其一原因是我沒有給出__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;}
}
這是干什么呢?答案是:根據(jù)OLED的類型,選擇我們的操作句柄。這是因?yàn)镃語言沒法自動(dòng)識別void*的原貌是如何的,我們必須將C++
中的虛表選擇手動(dòng)的完成
題外話:接觸過C++的朋友都知道繼承這個(gè)操作,實(shí)際上,這里就是一種繼承。無論是何種IIC操作,都是IIC操作。他都必須遵守可以發(fā)送字節(jié)的接口操作,現(xiàn)在的問題是:他到底是哪樣的IIC?需要執(zhí)行的是哪樣IIC的操作呢?所以,__on_fetch_oled_table就是把正確的操作函數(shù)根據(jù)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)); //設(shè)置X位置高4位op_table.command_sender(handle->private_handle,0x00 | (new_x & 0x0F)); //設(shè)置X位置低4位
}
現(xiàn)在回到上面的代碼,我們將正確的操作句柄選擇出來之后,可以發(fā)送設(shè)置“鼠標(biāo)”的指令了。
復(fù)習(xí)一下位操作的基本組成
&是一種萃取操作,任何數(shù)&0就是0,&1則是本身,說明可以通過對應(yīng)&1保留對應(yīng)位,&0抹除對應(yīng)位
|是一種賦值操作,任何數(shù)&1就是1,|0是本身,所以|可以起到對應(yīng)位置1的操作。
所以,保留高4位只需要 & 0xF0(0b11110000),保留低四位只需要&0x0F就好了(0b00001111)
刷新屏幕與光標(biāo)設(shè)置3
現(xiàn)在讓我們看看刷新屏幕是怎么做的
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 ++){/*設(shè)置光標(biāo)位置為每一頁的第一列*/__pvt_oled_set_cursor(handle, j, 0);/*連續(xù)寫入128個(gè)數(shù)據(jù),將顯存數(shù)組的數(shù)據(jù)寫入到OLED硬件*/// 有趣的是,這里筆者埋下了一個(gè)伏筆,我為什么沒寫OLED_WIDTH呢?盡管在SSD1306這樣做是正確的// 但那也是偶然,筆者在移植SSD1309的時(shí)候就發(fā)現(xiàn)了這樣的不一致性,導(dǎo)致OLED死機(jī).// 筆者提示: OLED長寬和可繪制區(qū)域的大小不一致性op_table.data_sender(handle->private_handle, OLED_GRAM[j], 128);}
}
刷新整個(gè)屏幕就是將鼠標(biāo)設(shè)置到開頭,然后直接向后面寫入128個(gè)數(shù)據(jù)結(jié)束我們的事情,這比一個(gè)個(gè)寫要快得多!
繪制一個(gè)點(diǎn)
實(shí)際上,就是將對應(yīng)的數(shù)組的位置放上1就好了,這需要牽扯到的是OLED獨(dú)特的顯示方式。
OLED自身分有頁這個(gè)概念,一個(gè)頁8個(gè)像素,由傳遞的比特控制。舉個(gè)例子,我想顯示的是第一個(gè)像素亮起來,就需要在一個(gè)字節(jié)的第一個(gè)比特置1余下置0,這就是為什么OLED_HEIGHT的大小不是64而是8,也就意味著setpixel函數(shù)不是簡單的
OLED[height][width] = val
而實(shí)需要進(jìn)行一個(gè)復(fù)雜的計(jì)算。我們分析一下,給定一個(gè)Y的值。它落在的頁就是 Y / 8。比如說,Y為5的時(shí)候落在第0頁的第六個(gè)比特上,Y為9的時(shí)候落在第一個(gè)頁的第一個(gè)第二個(gè)比特上(注意我們的Y從0開始計(jì)算),我們設(shè)置的位置也就是:OLED_GRAM[y / 8][x]
,設(shè)置的值就是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的寫法,現(xiàn)代編譯器支持
[[maybe_unused]]
的指示符,表達(dá)的是這個(gè)參數(shù)可能不被用到,編譯器不需要為此警告我,這在復(fù)用中很常見,一些接口的參數(shù)可能不被使用,這樣的可讀性會(huì)比傳遞空更加的好讀,為了遵循ISO C,筆者沒有采取,保證任何編譯器都可以正確的理解我們的意圖。
反色
反色就很簡單了。只需要異或即可,首先,當(dāng)給定的比特是0的時(shí)候,我們異或1,得到的就是相異的比較,所以結(jié)果是1:即0變成了1。我們給定的比特是1的時(shí)候,我們還是異或1,得到了相同的結(jié)果,所以結(jié)果是0,即1變成了0,這樣不就實(shí)現(xiàn)了一個(gè)像素的反轉(zhuǎn)嗎!
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是在什么情況下能使用呢?
我都這樣問了,那顯然不能,因?yàn)樵O(shè)置的值跟每一個(gè)字節(jié)的內(nèi)存強(qiáng)相關(guān),memset的值必須跟內(nèi)存的值沒有關(guān)系。
區(qū)域化操作
我們還有區(qū)域化操作沒有實(shí)現(xiàn)。基本的步驟是
思考需要的參數(shù):需要知道對
哪個(gè)OLED:OLED_Handle* handle,
起頭在哪里:uint16_t x, uint16_t y,
長寬如何:uint16_t width, uint16_t height
對于置位,則需要一個(gè)連續(xù)的數(shù)組進(jìn)行置位,它的大小就是描述了區(qū)域矩形的大小
我們先來看置位函數(shù)
區(qū)域置位
void oled_helper_draw_area(OLED_Handle* handle, uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t* sources)
{// 確保繪制區(qū)域的起點(diǎn)坐標(biāo)在有效范圍內(nèi),如果超出最大顯示坐標(biāo)則直接返回if(x > POINT_X_MAX) return;if(y > POINT_Y_MAX) return;
?// 在設(shè)置圖像前,先清空繪制區(qū)域oled_helper_clear_area(handle, x, y, width, height);
?// 遍歷繪制區(qū)域的高度,以8像素為單位劃分區(qū)域for(uint16_t j = 0; j < (height - 1) / 8 + 1; j++){for(uint16_t i = 0; i < width; i++){// 如果繪制超出屏幕寬度,則跳出循環(huán)if(x + i > OLED_WIDTH) { break; }// 如果繪制超出屏幕高度,則直接返回if(y / 8 + j > OLED_HEIGHT - 1) { return; }
?// 將sources中的數(shù)據(jù)按位移方式寫入OLED顯存GRAM// 當(dāng)前行顯示,低8位數(shù)據(jù)左移與顯存當(dāng)前內(nèi)容進(jìn)行按位或OLED_GRAM[y / 8 + j][x + i] |= sources[j * width + i] << (y % 8);
?// 如果繪制數(shù)據(jù)跨頁(8像素一頁),處理下一頁的數(shù)據(jù)寫入if(y / 8 + j + 1 > OLED_HEIGHT - 1) { continue; }
?// 將高8位數(shù)據(jù)右移后寫入下一頁顯存OLED_GRAM[y / 8 + j + 1][x + i] |= sources[j * width + i] >> (8 - y % 8);}}
}
我們正
常來講,傳遞的會(huì)是一個(gè)二維數(shù)組,C語言對于二維數(shù)組的處理是連續(xù)的。也就是說。對于一個(gè)被聲明為OLED[WIDTH][HEIGHT]
的數(shù)組,訪問OLED[i][j]
本質(zhì)上等價(jià)于OLED + i * WIDTH + j
,這個(gè)事情如果還是不能理解可以查照專門的博客進(jìn)行學(xué)習(xí)。筆者默認(rèn)在這里看我寫的東西已經(jīng)不會(huì)被這樣基礎(chǔ)的知識所困擾了。所以,我們的所作的就是將出于低頁的內(nèi)容拷貝到底頁上
OLED_GRAM[y / 8 + j][x + i]
:這是顯存二維數(shù)組的索引訪問。
y / 8 + j
計(jì)算出當(dāng)前數(shù)據(jù)位于哪個(gè)頁(OLED通常按8個(gè)像素一頁分塊存儲),通過整除將y
坐標(biāo)映射到顯存頁。
x + i
表示橫向的列位置。
sources[j * width + i]
:這是源圖像數(shù)據(jù)數(shù)組的索引訪問。
j * width + i
計(jì)算當(dāng)前像素在sources
數(shù)據(jù)中的位置偏移。
<< (y % 8)
:將當(dāng)前像素?cái)?shù)據(jù)向左移動(dòng)(y % 8)
位,以確保源數(shù)據(jù)對齊到目標(biāo)位置。
y % 8
獲取繪制的起點(diǎn)在當(dāng)前頁中的垂直偏移。
|=
:按位或運(yùn)算符,將偏移后的數(shù)據(jù)合并到OLED_GRAM
中現(xiàn)有內(nèi)容。如果
y = 5
,那么y % 8 = 5
,表示當(dāng)前像素從第5位開始繪制。例如:
如果
sources[j * width + i]
的值是0b11000000
,經(jīng)過<< 5
位移后變?yōu)?0b00000110
,再與OLED_GRAM
的原有數(shù)據(jù)合并,從而只影響目標(biāo)位置上的兩個(gè)像素。
先試一下分析OLED_GRAM[y / 8 + j + 1][x + i] |= sources[j * width + i] >> (8 - y % 8);
,筆者的分析如下
OLED_GRAM[y / 8 + j + 1][x + i]
:
這是下一頁顯存中的對應(yīng)位置。
y / 8 + j + 1
表示當(dāng)前繪制位置的下一頁。
x + i
仍為當(dāng)前列位置。
sources[j * width + i]
:
源圖像數(shù)據(jù)中當(dāng)前像素的數(shù)據(jù)。
j * width + i
計(jì)算出當(dāng)前像素在源數(shù)據(jù)中的位置。
>> (8 - y % 8)
:
將數(shù)據(jù)右移
(8 - y % 8)
位,將超出當(dāng)前頁的高位部分對齊到下一頁。
8 - y % 8
計(jì)算需要移入下一頁的位數(shù)。
|=
:
按位或,將偏移后的數(shù)據(jù)合并到下一頁顯存中,以保留已有內(nèi)容。
假設(shè)
y = 5
,那么8 - y % 8 = 3
。如果sources[j * width + i]
為0b10110000
,右移 3 位得到0b00010110
,這部分?jǐn)?shù)據(jù)寫入下一頁顯存。
區(qū)域反色
void oled_helper_reversearea(OLED_Handle* handle, uint16_t x, uint16_t y, uint16_t width, uint16_t height)
{// 確認(rèn)起點(diǎn)坐標(biāo)是否超出有效范圍if(x > POINT_X_MAX) return;if(y > POINT_Y_MAX) return;
?// 確保繪制區(qū)域不會(huì)超出最大范圍,如果超出則調(diào)整寬度和高度if(x + width > POINT_X_MAX) ? ? width = POINT_X_MAX - x;if(y + height > POINT_Y_MAX) ? height = POINT_Y_MAX - y;
?// 遍歷高度范圍中的每個(gè)像素行for(uint8_t i = y; i < y + height; i++){for(uint8_t j = x; j < x + width; j++){// 反轉(zhuǎn)顯存GRAM中的指定像素位(按位異或)OLED_GRAM[i / 8][j] ^= (0x01 << (i % 8));}}
}
區(qū)域更新
void oled_helper_update_area(OLED_Handle* handle, uint16_t x, uint16_t y, uint16_t width, uint16_t height)
{// 檢查起點(diǎn)坐標(biāo)是否超出有效范圍if(x > POINT_X_MAX) return;if(y > POINT_Y_MAX) return;
?// 確認(rèn)繪制區(qū)域不超出最大范圍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;// 獲取對應(yīng)的操作函數(shù)表__on_fetch_oled_table(handle, &op_table);
?// 遍歷繪制區(qū)域中的每個(gè)頁(8像素一頁)for(uint8_t i = y / 8; i < (y + height - 1) / 8 + 1; i++){// 設(shè)置光標(biāo)到指定頁及列的位置__pvt_oled_set_cursor(handle, i, x);// 從顯存中讀取指定頁和列的數(shù)據(jù),通過data_sender發(fā)送到OLED硬件op_table.data_sender(handle, &OLED_GRAM[i][x], width); ? ? ? ?}
}
也就是將光標(biāo)對應(yīng)到位置上刷新width個(gè)數(shù)據(jù),完事!
區(qū)域清空
void oled_helper_clear_area(OLED_Handle* handle, uint16_t x, uint16_t y, uint16_t width, uint16_t height)
{// 檢查起點(diǎn)坐標(biāo)是否超出有效范圍if(x > POINT_X_MAX) return;if(y > POINT_Y_MAX) return;
?// 確保繪制區(qū)域不超出最大范圍if(x + width > POINT_X_MAX) ? ? width = POINT_X_MAX - x;if(y + height > POINT_Y_MAX) ? height = POINT_Y_MAX - y;
?// 遍歷高度范圍內(nèi)的所有像素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]
:
訪問顯存緩沖區(qū)中指定位置的字節(jié)。
i / 8
確定當(dāng)前像素所在的頁,因?yàn)?OLED 每頁存儲 8 個(gè)垂直像素。
j
為水平方向的列位置。
0x01 << (i % 8)
:
生成一個(gè)掩碼,將
0x01
左移(i % 8)
位。
i % 8
計(jì)算出在當(dāng)前頁中的垂直位偏移。
~(0x01 << (i % 8))
:
對掩碼取反,生成一個(gè)用于清零的掩碼。例如,如果
i % 8 == 2
,則0x01 << 2
為0b00000100
,取反后得到0b11111011
。
&=
:
按位與運(yùn)算,將顯存當(dāng)前位置對應(yīng)的像素清零,而其他位保持不變。
假設(shè)
i = 10
,j = 5
:
i / 8 = 1
表示訪問第 2 頁(頁索引為 1);
i % 8 = 2
表示需要清除該頁第 3 位的像素;
0x01 << 2 = 0b00000100
,取反得到0b11111011
;
OLED_GRAM[1][5] &= 0b11111011
會(huì)將第 3 位清零,其余位保持不變。
測試我們的抽象
現(xiàn)在,我們終于可以開始測試我們的抽象了。完成了既可以使用軟件IIC,又可以使用硬件IIC進(jìn)行通信的OLED抽象,我們當(dāng)然迫不及待的想要測試一下我們的功能是否完善。筆者這里剎住車,耐下性子聽幾句話。
首先,測試不是一番風(fēng)順的,我們按照我們的期望對著接口寫出了功能代碼,基本上不會(huì)一番風(fēng)順的得到自己想要的結(jié)果,往往需要我們進(jìn)行調(diào)試,找到其中的問題,修正然后繼續(xù)測試。
整理一下,我們應(yīng)該如何使用?
首先回顧接口。我們需要指定一個(gè)協(xié)議按照我們期望的方式進(jìn)行通信。在上一篇博客中,我們做完了協(xié)議層次的抽象,在這里,我們只需要老老實(shí)實(shí)的注冊接口就好了。
指引:如果你忘記了我們上一篇博客在做什么的話,請參考從0開始使用面對對象C語言搭建一個(gè)基于OLED的圖形顯示框架2-CSDN博客!
筆者建議,新建一個(gè)Test文件夾,書寫一個(gè)文件叫:oled_test_hard_iic.c
和oled_test_soft_iic.c
測試我們的設(shè)備層和協(xié)議層是正確工作的。筆者這里以測試硬件IIC的代碼為例子。
新建一個(gè)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通信的協(xié)議實(shí)體,我們將一個(gè)空白的config,注冊了配置好的iic的HAL庫句柄,提供了IIC地址和最大可接受的延遲時(shí)間
oled_init_hardiic_handle
則是進(jìn)一步的從協(xié)議層飛躍到設(shè)備層,完成一個(gè)OLED設(shè)備的注冊,即,我們注冊了一個(gè)使用硬件IIC通信的OLED?,F(xiàn)在,我們就可以直接拿這個(gè)OLED進(jìn)行繪點(diǎn)了。
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;// 注冊了一個(gè)使用硬件IIC通信的OLEDuser_init_hard_iic_oled_handle(&handle);// 繪制一個(gè)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);
}
這個(gè)測試并不全面,自己可以做修改。效果就是在導(dǎo)言當(dāng)中的視頻開始的兩條直線所示。
筆者的OLED設(shè)備層的代碼已經(jīng)全部開源到MCU_Libs/OLED/library/OLED at main · Charliechen114514/MCU_Libs (github.com),感興趣的朋友可以進(jìn)一步研究。
目錄導(dǎo)覽
總覽
協(xié)議層封裝
OLED設(shè)備封裝
繪圖設(shè)備抽象
基礎(chǔ)圖形庫封裝
基礎(chǔ)組件實(shí)現(xiàn)
動(dòng)態(tài)菜單組件實(shí)現(xiàn)