目錄
協議層設計,以IIC為例子
關于軟硬件IIC
設計的一些原則
完成協議層的抽象
刨析我們的原理
如何完成我們的抽象
插入幾個C語言小技巧
完成軟件IIC通信
開始我們的IIC通信
結束我們的IIC通信
發送一個字節
(重要)完成命令傳遞和數據傳遞
最終一擊,完成我們的IIC通信
硬件IIC
關于架構設計概述等內容,筆者放到了:從0開始使用面對對象C語言搭建一個基于OLED的圖形顯示框架-CSDN博客,任何疑問可以到這里看看,這是一個總覽。
協議層設計,以IIC為例子
我們先按照最經典的軟硬件IIC為例子!筆者大部分接觸到的都是4針腳的使用IIC協議通信的OLED片子。所以,筆者打算優先的搭建起來IIC部分的代碼。所有完整的代碼放到了:MCU_Libs/OLED/library/OLED/Driver at main · Charliechen114514/MCU_Libs (github.com),這個文件夾內部都是協議層的代碼。
關于軟硬件IIC
軟硬件IIC都是完成IIC通信協議的東西。但區別在于,我們到底是使用自己手動模擬的IIC還是使用專門硬件特化的IIC。
關于IIC,看到這里的朋友都很熟悉了:IIC(Inter-Integrated Circuit)是一種常用的串行通信總線協議,用于微控制器與傳感器、顯示模塊等外設之間的通信。而我們的軟件IIC就是使用GPIO來模擬IIC時序。
優點:
靈活性強,可以使用任意引腳進行通信,不受特定硬件限制。
適用于不具備硬件IIC模塊的微控制器。
可以方便地調節時序,兼容性較好。
缺點:
通信效率較低,占用CPU資源較多。
對實時性要求高的應用不太適合。
穩定性較差,容易受程序時序影響。
硬件IIC則是將IIC應答處理委托給了專門的硬件。
優點
通信速度快,效率高,因為由專用硬件處理時序。
占用CPU資源少,適合需要高實時性的場合。
通信穩定可靠,不易受到程序時序干擾。
缺點:
只能使用特定的IIC引腳,不夠靈活。
不同微控制器之間的硬件IIC兼容性可能存在差異。
部分微控制器可能沒有硬件IIC模塊,導致無法使用硬IIC。
我們大概清楚了。代碼上的實現就不會復雜。下面我們就可以開始聊一聊設計了。
設計的一些原則
你認為這樣的代碼好看嗎?
void OLED_ShowImage(int16_t X, int16_t Y, uint8_t Width, uint8_t Height, const uint8_t *Image) {uint8_t i = 0, j = 0;int16_t Page, Shift;/*將圖像所在區域清空*/OLED_ClearArea(X, Y, Width, Height);/*遍歷指定圖像涉及的相關頁*//*(Height - 1) / 8 + 1的目的是Height / 8并向上取整*/for (j = 0; j < (Height - 1) / 8 + 1; j ++){/*遍歷指定圖像涉及的相關列*/for (i = 0; i < Width; i ++){if (X + i >= 0 && X + i <= 127) ? ? ? //超出屏幕的內容不顯示{/*負數坐標在計算頁地址和移位時需要加一個偏移*/Page = Y / 8;Shift = Y % 8;if (Y < 0){Page -= 1;Shift += 8;}if (Page + j >= 0 && Page + j <= 7) ? ? ? //超出屏幕的內容不顯示{/*顯示圖像在當前頁的內容*/OLED_DisplayBuf[Page + j][X + i] |= Image[j * Width + i] << (Shift);}if (Page + j + 1 >= 0 && Page + j + 1 <= 7) ? ? ? //超出屏幕的內容不顯示{ ? ? ? ? ? ? ? ? ? /*顯示圖像在下一頁的內容*/OLED_DisplayBuf[Page + j + 1][X + i] |= Image[j * Width + i] >> (8 - Shift);}}}} }
好吧,好像大部分人的代碼都是這樣的。
那這樣呢?
void CCGraphicWidget_draw_image(CCDeviceHandler* ? handler,CCGraphic_Image* ? image) {if(!image->sources_register) return;handler->operations.draw_area_device_function(handler, image->point.x, image->point.y,image->image_size.width, image->image_size.height, image->sources_register); }
你需要在乎image是如何實現的嗎?你需要知道如何完成OLED圖像的顯示是如何做的嗎?
你不需要!
這段代碼無非就是告訴了你一件事情:提供一個設備句柄作為“告知一個設備,在上面繪制”,告知一個“圖像”你需要繪制,直接提供進來,由設備自己約定的方法繪制即可。怎么繪制的?你需要關心嗎?你不需要。
直到你需要考慮設備是如何工作的時候,你會看一眼內部的設備
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; ?// clear the area before being set// 先清理一下這個區域,不要干擾賦值oled_helper_clear_area(handle, x, y , width, height); ?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;} ?OLED_GRAM[y / 8 + j][x + i] |= sources[j * width + i] << (y % 8); ?if(y / 8 + j + 1 > OLED_HEIGHT - 1){continue;} ?OLED_GRAM[y / 8 + j + 1][x + i] |= sources[j * width + i] >> (8 - y % 8);}} }
原來如此,是通過寫OLED緩存賦值就可以把這個事情給搞明白的——但是當你不關心如何實現的時候,你并不需要付出心血代價把代碼看懂然后——哦我的天,這個我壓根不關心!當然,代價就是多支付若干次的函數調用(笑)
這就是架構抽象帶來的對開發的好處,但是只有這個不足以讓我們使用復雜的抽象,我們一定還有別的好處,不是嗎?讓我們慢慢看吧!
完成協議層的抽象
我已經說過,我們的OLED框架是由協議層(使用何種協議進行通信?),設備層(這個設備可以做什么?),圖像層(可以使用設備繪制哪一些圖像?),組件層(可以使用圖像繪制哪一些組件?),層層遞進,保證互相之間互不干擾。我們下面就著重的關心協議層。協議層需要完成的就是將委托的命令(OLED命令)和委托的數據(OLED數據)發送到設備上即可。
刨析我們的原理
協議需要進行初始化,對于硬件,特別是HAL庫,只需要咔咔調API就能把事情做完了。但是對于軟件IIC,事情就需要麻煩一些,我們需要自己完成IIC時序的通信。
讓我們看看IIC的基本原理,基本上看,就是:通知起始通信,通知數據(他是命令還是數據并不關心)和通知停止。
-
起始條件(Start Condition) 主設備將SDA從高電平拉低,同時保持SCL為高電平。當SDA從高到低時形成起始條件(START),通知從設備通信即將開始。
-
地址傳輸(Address Transmission) 主設備發送一個7位或10位的從設備地址,緊接著是1位的讀寫方向標志位(R/W位)。
-
R/W為0表示寫操作,主設備發送數據
-
R/W為1表示讀操作,主設備接收數據 每發送一位數據時,SCL產生一個時鐘脈沖(SCL上升沿鎖存數據)。
-
-
應答信號(ACK/NACK) 從設備在收到地址和R/W位后,如果能夠正常接收數據,會在下一個時鐘周期內將SDA拉低產生應答信號ACK(Acknowledge)。如果不響應,則保持SDA為高電平,產生非應答信號NACK(Not Acknowledge)。
-
數據傳輸(Data Transmission) 主設備根據讀寫操作繼續發送或接收數據,每次傳輸8位數據。
-
寫操作:主設備發送數據,從設備應答ACK
-
讀操作:從設備發送數據,主設備應答ACK 每個字節傳輸完成后,從設備需發送ACK信號以確認接收正常。
-
-
停止條件(Stop Condition) 通信結束時,主設備將SDA從低電平拉高,同時保持SCL為高電平。當SDA從低到高時形成停止條件(STOP),表示通信結束。
說了一大堆,其實就是:
-
起始條件:SDA高變低,SCL保持高
-
數據傳輸:SDA根據數據位變化,SCL上升沿鎖存數據
-
應答信號:從設備將SDA拉低產生ACK,高電平為NACK
-
停止條件:SDA低變高,SCL保持高
所以這樣看來,無非就是使用兩個引腳,按照上述規則進行高低電平的按照時序的拉高拉低。
話里有話,我的意思就是:軟件IIC需要知道你使用哪兩個引腳進行通信,需要你來告知如何完成上面的協議約定控制設備。最終我們提供的,是像我們跟人聊天一般的:
嘿!我用軟件IIC發送了一個Byte的命令/數據!
這是重點!也是我們協議層抽象的終點:完成委托給我們的數據傳輸的任務,其他的任何事情都與我們無關,也不在乎這個數據到底是啥!
如何完成我們的抽象
軟件IIC需要知道你使用哪兩個引腳進行通信,需要你來告知如何完成上面的協議約定控制設備!我再強調的一次!
所以,我們給一個被抽象為軟件IIC的實體,提供一個配置,這個配置委婉的提醒了我們的IIC使用哪兩個引腳進行通信。最終這個軟件IIC實體將會提供可以完成“委托給我們的數據傳輸的任務”這個任務,需要注意的是,OLED發送數據需要區分他是命令還是數據。這樣來看,我們最終就是提供兩套方法:
/* command send fucntion */ typedef void(*SendCommand)(void*, uint8_t); /* data send fucntion */ typedef void(*SendData)(void*, uint8_t*, uint16_t); ? /* driver level oled driver's functionalities */ typedef struct __OLED_Operations{SendCommand command_sender;SendData ? data_sender; }OLED_Operations;
好像很是罕見!這是一個包裝了函數指針的結構體。說的拗口,讓我們引入面對對象的設計邏輯來再闡述上面的句子。
這是一個可以保證完成數據傳輸的OLED方法。調用這個方法,就可以保證我們完成了一個字節傳遞的命令,或者是完成一系列字節的數據傳輸
又問我咋做的?先別管,你現在需要知道的是——我一調用!他就能干好這個事情!實現是下面的事情!它隸屬于我們的協議實體的結構體,如下所示
/* this will make the gpio used for iic */ typedef struct __OLED_SOFT_IIC_Private_Config {/* soft gpio handling */ OLED_IICGPIOPack ? ? ? sda;OLED_IICGPIOPack ? ? ? scl;uint32_t ? ? ? ? ? accepted_time_delay;uint16_t ? ? ? ? ? device_address;OLED_Operations ? ? operation; }OLED_SOFT_IIC_Private_Config;
OLED_IICGPIOPack sda
表示用于IIC的SDA(數據線)引腳配置。
OLED_IICGPIOPack
應該是一個結構體或類型,定義了與GPIO相關的參數,比如引腳號、端口等。該成員用來指定IIC通信中用作SDA的具體引腳。
OLED_IICGPIOPack scl
表示用于IIC的SCL(時鐘線)引腳配置。
同樣是
OLED_IICGPIOPack
類型,用來配置時鐘信號線(SCL)的具體引腳。這個成員和
sda
一起決定了軟IIC使用的GPIO引腳。
uint32_t accepted_time_delay
用于設置IIC時序中的時間延遲。
因為軟IIC需要軟件控制時序,這個值可能表示每個時鐘周期的延遲時間(以微秒或納秒為單位)。
調節這個值可以改變IIC的通信速度,從而適配不同的外設設備。
uint16_t device_address
IIC從設備的地址。
IIC通信中,每個從設備都有唯一的地址,用于主設備區分不同的從設備。
這個值通常是7位或10位地址,需要根據設備規格書配置。
OLED_Operations operation
表示IIC通信的操作類型。
OLED_Operations
定義了常見的IIC操作,比如READ
(讀操作)、WRITE
(寫操作)等。
初始化的辦法,這里就只需要按部就班的賦值。
void oled_bind_softiic_handle(OLED_SOFT_IIC_Private_Config* ? config,OLED_IICGPIOPack* ? ? ? ? ? ? ? ? sda, ?OLED_IICGPIOPack* ? ? ? ? ? ? ? ? scl,uint16_t ? ? ? ? ? ? ? ? ? ? ? device_address,uint32_t ? ? ? ? ? ? ? ? ? ? ? accepted_time_delay ) {config->accepted_time_delay = accepted_time_delay;config->device_address = device_address;config->sda = *sda;config->scl = *scl;config->operation.command_sender ? = ?config->operation.data_sender ? ? ? = ?/* we need to init the gpio type for communications */ }
我們的函數寫到下面就頓住了。對啊,咋發送啊?咋操作啊?這才是這個時候我們思考的問題:如何實現軟件IIC呢?
我們首先需要完成的是:初始化我們的引腳,讓他們可以完成傳遞電平的任務。
static void __pvt_on_init_iic_gpio(OLED_SOFT_IIC_Private_Config* config) {/* Enable the GPIOB clock *//* 這就是把時鐘打開了而已,是__HAL_RCC_GPIOB_CLK_ENABLE的一個等價替換 *//* #define OLED_ENABLE_GPIO_SCL_CLK() __HAL_RCC_GPIOB_CLK_ENABLE()#define OLED_ENABLE_GPIO_SDA_CLK() __HAL_RCC_GPIOB_CLK_ENABLE()*/// 為什么這樣做。。。你換引腳了直接改上面的#define不香嗎?集中起來處理一坨屎而不是讓你的史滿天飛到處改OLED_ENABLE_GPIO_SCL_CLK();OLED_ENABLE_GPIO_SDA_CLK(); ?GPIO_InitTypeDef GPIO_InitStructure = {0};/* configuration */GPIO_InitStructure.Pin = config->sda.pin | config->scl.pin;GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD; // 開漏模式GPIO_InitStructure.Pull = GPIO_NOPULL; // 不上拉也不下拉GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);// 這個是一個非常方便的宏,是筆者自己封裝的:/*#define SET_SCL(config, pinstate) \do{\HAL_GPIO_WritePin(config->scl.port, config->scl.pin, pinstate);\}while(0) ?#define SET_SDA(config, pinstate) \do{\HAL_GPIO_WritePin(config->sda.port, config->sda.pin, pinstate);\}while(0)*/SET_SCL(config, 1);SET_SDA(config, 1); }
插入幾個C語言小技巧
結構體的使用更加像是對一個物理實體的抽象,比如說我們的軟件IIC實體由兩個GPIO引腳,提供一個OLED地址和延遲時間組成,他可以發送命令和數據
/* this will make the gpio used for iic */ typedef struct __OLED_SOFT_IIC_Private_Config {/* soft gpio handling */ OLED_IICGPIOPack ? ? ? sda;OLED_IICGPIOPack ? ? ? scl;uint32_t ? ? ? ? ? accepted_time_delay;uint16_t ? ? ? ? ? device_address;OLED_Operations ? ? operation; }OLED_SOFT_IIC_Private_Config;這樣的抽象也就呼之欲出了
為什么使用do while呢?答案是:符合大部分人的使用習慣。
避免宏定義中的語法問題 在宏中使用
do { } while(0);
可以確保宏內容被當作一個獨立的語句塊執行。 例如:#define MY_MACRO(x) do { if (x) func(); } while (0) ?這樣,即使在使用時加上分號也不會引發編譯錯誤:
if (condition) ?MY_MACRO(1); // 正確處理,避免語法歧義 ? else ?other_func(); ?如果直接使用
{}
而不加do-while(0)
,編譯器可能會報錯或者導致意外的邏輯問題。提升代碼的可讀性與可維護性
do { } while(0);
語法塊明確限制了語句作用范圍,避免宏或語句中的變量污染外部作用域,從而增強代碼的封裝性。兼容語法規則,減少隱患
do { } while(0);
總能確保語法結構合法,即使宏中包含復雜的控制語句也不會影響邏輯。#define SAFE_BLOCK do { statement1; statement2; } while(0) ?這樣即便加了分號也能正常執行,符合常規語句格式。
避免空語句問題 使用
do-while(0)
可以有效避免空語句可能帶來的邏輯漏洞。你問我擔心開銷?拜托!編譯器會自動優化!全給你消的一點不剩了,完全就是正常的調用,為啥不用?
為什么在函數的起頭帶上static?
保證我們的函數在文件作用域是私有的,不會跟其他函數起沖突的。說白了,就是我說的:你需要在干別的事情還要擔心一下自己的軟件IIC是咋工作的嗎?你不需要!擔心是一個有病的行為。所以,他保證了接口是簡潔的。
完成軟件IIC通信
開始我們的IIC通信
軟件IIC通信開始,需要先拉高SDA和SCL保證處于高電平,然后拽低SDA和SCL的電平
static void __pvt_on_start_iic(OLED_SOFT_IIC_Private_Config* config) {SET_SDA(config, 1);SET_SCL(config, 1);SET_SDA(config, 0);SET_SCL(config, 0); ? ? }
結束我們的IIC通信
設置我們的SDA先低,之后讓SDA和SCL都處于高電平結束戰斗
static void __pvt_on_stop_iic(OLED_SOFT_IIC_Private_Config* handle) {SET_SDA(handle, 0); SET_SCL(handle, 1); SET_SDA(handle, 1); }
發送一個字節
發送一個目標字節給我們的設備,你不需要關心這個字節是什么,你不需要現在關心它!
static void __pvt_iic_send_bytes(OLED_SOFT_IIC_Private_Config* handle, uint8_t data) { for (uint8_t i = 0; i < 8; i++){ SET_SDA(handle,!!(data & (0x80 >> i)));SET_SCL(handle,1); SET_SCL(handle,0); }SET_SCL(handle,1); SET_SCL(handle,0); }
!!
的作用是將任意數值轉換為布爾值,保證我們發的就是0和1,(0x80 >> i)
萃取了從高向低數的第I位數字發送,也就是往SDA電平上傳遞我們的data上的第I位。之后拉起釋放SCL告知完成傳遞。
(重要)完成命令傳遞和數據傳遞
我們現在開始想起來,我們最終的目的是:完成一個字節命令的傳遞或者是傳遞一系列的數據比特。結合手冊,我們來看看實際上怎么做。
按照順序,依次傳遞
-
開啟IIC通信
-
設備的地址
-
數據類型(是命令還是數據)
-
數據本身。
-
結束IIC通信
/*#define DATA_PREFIX ? ? (0x40)#define CMD_PREFIX ? ? (0x00) */ static void __pvt_iic_send_command(void* pvt_handle, uint8_t cmd) {OLED_SOFT_IIC_Private_Config* config = (OLED_SOFT_IIC_Private_Config*)pvt_handle; ?__pvt_on_start_iic(config);__pvt_iic_send_bytes(config, config->device_address);__pvt_iic_send_bytes(config, CMD_PREFIX);__pvt_iic_send_bytes(config, cmd);__pvt_on_stop_iic(config); } ? static void __pvt_iic_send_data(void* pvt_handle, uint8_t* data, uint16_t size) {OLED_SOFT_IIC_Private_Config* config = (OLED_SOFT_IIC_Private_Config*)pvt_handle;__pvt_on_start_iic(config);__pvt_iic_send_bytes(config, config->device_address);__pvt_iic_send_bytes(config, DATA_PREFIX);for(uint16_t i = 0; i < size; i++)__pvt_iic_send_bytes(config, data[i]);__pvt_on_stop_iic(config); }
最終一擊,完成我們的IIC通信
/*config: Pointer to an OLED_SOFT_IIC_Private_Config structure that contains the configuration settings for the software I2C communication,such as timing, pins, and other relevant parameters.config should be blank or uninitialized.sda: Pointer to an OLED_GPIOPack structure that represents the GPIO configuration for the Serial Data (SDA) line of the software I2C interface. ?scl: Pointer to an OLED_GPIOPack structure that represents the GPIO configuration for the Serial Clock (SCL) line of the software I2C interface. ?device_address: The 7-bit I2C address of the device that the software I2C communication is targeting, typically used to identify the device on the I2C bus. ?accepted_time_delay: A timeout value in milliseconds, specifying the maximum allowed delay for the software I2C communication process. */ void oled_bind_softiic_handle(OLED_SOFT_IIC_Private_Config* ? config,OLED_IICGPIOPack* ? ? ? ? ? ? ? ? sda, ?OLED_IICGPIOPack* ? ? ? ? ? ? ? ? scl,uint16_t ? ? ? ? ? ? ? ? ? ? ? device_address,uint32_t ? ? ? ? ? ? ? ? ? ? ? accepted_time_delay ){config->accepted_time_delay = accepted_time_delay;config->device_address = device_address;config->sda = *sda;config->scl = *scl;config->operation.command_sender ? = __pvt_iic_send_command;config->operation.data_sender ? ? ? = __pvt_iic_send_data;__pvt_on_init_iic_gpio(config); }
我們把方法和數據都傳遞給這個軟件iic實體,現在,他就能完成一次軟件IIC通信了。給各位看看如何使用
config->operation.command_sender(config, oled_spi_init_command[i]);
可以看到,我們的結構體函數指針就是這樣使用的。
硬件IIC
硬件IIC事情就會簡單特別多,原因在于,我們有專門的硬件幫助我們完成IIC通信
#ifndef OLED_HARD_IIC_H #define OLED_HARD_IIC_H #include "OLED/Driver/oled_config.h" #include "stm32f1xx_hal.h" #include "stm32f1xx_hal_i2c.h" ? typedef struct __OLED_HARD_IIC_Private_Config{I2C_HandleTypeDef* pvt_handle;uint32_t ? ? ? ? ? accepted_time_delay;uint16_t ? ? ? ? ? device_address;OLED_Operations ? ? operation; }OLED_HARD_IIC_Private_Config; ? /* handle binder, bind the raw data to the oled driverblank_config: Pointer to an OLED_HARD_IIC_Private_Config structure that holds the configuration settings for the I2C communication, typically initializing the OLED hardware interface.raw_handle: Pointer to an I2C_HandleTypeDef structure, representing the raw I2C peripheral handle used to configure and manage I2C communication for the device. ?device_address: The 7-bit I2C address of the device to which the communication is being established, typically used for identifying the target device on the I2C bus. ?accepted_time_delay: A timeout value in milliseconds that specifies the maximum allowable delay for the I2C communication process. */ void bind_hardiic_handle(OLED_HARD_IIC_Private_Config* blank_config,I2C_HandleTypeDef* raw_handle,uint16_t ? device_address,uint32_t ? accepted_time_delay ); ? #endif
現在我們可以不需要兩個引腳了,只需要客戶端提供一個硬件IIC句柄就好。
#include "OLED/Driver/hard_iic/hard_iic.h" ? static void __pvt_hardiic_send_data(void* pvt_handle, uint8_t* data, uint16_t size) {OLED_HARD_IIC_Private_Config* config = (OLED_HARD_IIC_Private_Config*)pvt_handle;for (uint8_t i = 0; i < size; i ++){HAL_I2C_Mem_Write(config->pvt_handle,config->device_address,DATA_PREFIX,I2C_MEMADD_SIZE_8BIT,&data[i], 1, config->accepted_time_delay); //依次發送Data的每一個數據} } ? static void __pvt_hardiic_send_command(void* pvt_handle, uint8_t cmd) {OLED_HARD_IIC_Private_Config* config = (OLED_HARD_IIC_Private_Config*)pvt_handle;HAL_I2C_Mem_Write(config->pvt_handle, config->device_address,CMD_PREFIX,I2C_MEMADD_SIZE_8BIT,&cmd,1,config->accepted_time_delay); } ? void bind_hardiic_handle(OLED_HARD_IIC_Private_Config* blank_config,I2C_HandleTypeDef* raw_handle,uint16_t ? device_address,uint32_t ? accepted_time_delay ) {blank_config->accepted_time_delay = accepted_time_delay;blank_config->device_address = device_address;blank_config->pvt_handle = raw_handle;blank_config->operation.command_sender = __pvt_hardiic_send_command;blank_config->operation.data_sender ? ? = __pvt_hardiic_send_data; }
HAL_I2C_Mem_Write函數直接完成了我們的委托,注意的是,我們每一次的調用這個函數,內部都是重新開始一次IIC通信的,所以,發送數據的時候,只能一個字節一個字節的發送(因為每一次都要指定這個是數據還是命令)。這一點,SPI協議的OLED就要好很多!(內部的引腳高低就直接決定了整個是命令還是數據,不需要通過解析傳遞的數據本身!)
這樣,一個典型的基于軟硬件IIC的協議層抽象就完成了。如果你著急測試的話,可以自己替換原本OLED的操作。
我們下一篇,就是開始抽象OLED的設備層。
目錄導覽
總覽
協議層封裝
OLED設備封裝
繪圖設備抽象
基礎圖形庫封裝
基礎組件實現
動態菜單組件實現