U8g2圖形庫與STM32移植(I2C,軟件與硬件)

U8g2圖形庫

簡介

U8g2 是一個用於嵌入式設備的簡易圖形庫,可以在多種 OLED 和 LCD 屏幕上,支持包括 SSD1306 等多種類型的底層驅動,並可以很方便地移植到 Arduino 、樹莓派、NodeMCU 和 ARM 上。

U8g2 庫同時包含了 U8x8 繪圖庫,兩者的區別爲:

  • U8g2 包含各種簡單及複雜圖形的繪製,並支持各種形式的字體,但需要佔用一定單片機的內存作爲繪圖緩存
  • U8x8 只包含簡單地顯示文本功能,且只支持簡單、定寬的字體。它直接繪製圖形,沒有緩存功能

U8g2 庫克 GitHub 地址爲:https://github.com/olikraus/u8g2 ,可以從中獲取到源碼與文檔幫助。

移植

本次即將 U8g2 移植到 STM32 單片機與 SSD1306 通過 I2C 驅動的 128×64 OLED 爲例,介紹移植的方法。不同單片機和驅動的移植設備可以參考這一過程,也可以參考 U8g2 的官方移植教程 https://github.com/olikraus/u8g2/wiki/Porting-to-new-MCU-platform 。

首先下載或克隆 U8g2 的源碼,這裏主要是使用 C 語言編寫,所以只需要用到 csrc 目錄下的文件。

下載完成後,將 csrc 目錄拷貝或移動到工程目錄裏,並重命名爲合適的目錄名例如 u8g2lib

刪除無用內容

接下來,需要刪除一些無用的代碼,並添加底層驅動的代碼。

U8g2 源碼爲了支持多種設備驅動,包含了許多兼容性的代碼。首先,類似 u8x8_d_xxx.c 命名的文件中包含 U8x8 的驅動兼容,文件名包括驅動的型號和屏幕分辨率,因此需要刪除無用的驅動文件,只保留當前設備的驅動。例如,本次使用的是 128×64 的 SSD1306 屏幕,那麼只需要保留 u8x8_d_ssd1306_128x64_noname.c 文件,刪除其它類似的文件即可。U8g2 支持的所有屏幕驅動可以在 https://github.com/olikraus/u8g2/wiki/u8g2setupc 找到。

同時還需要精簡 u8g2_d_setup.cu8g2_d_memory.c 中 U8g2 提供的驅動兼容。

u8g2_d_setup.c 中,只需要保留 u8g2_Setup_ssd1306_i2c_128x64_noname_f() 這是一個函數即可。注意,該文件內有幾個命名類似的函數:命名中無 i2c 的是 SPI 接口驅動的函數,需要根據接口選擇;以 1 結尾的函數代表使用的緩存空間爲 128 字節,以 2 結尾的函數代表使用的緩存位置 256字節,類似於 f 結尾的函數代表使用的緩存位置 1024 字節。

u8g2_d_memory.c 文件也是同理,它需要根據 u8g2_d_setup.c 中的調用情況決定用到哪些函數。由於 u8g2_Setup_ssd1306_i2c_128x64_noname_f() 函數只用到 u8g2_m_16_8_f() 這是一個函數,因此只需要保留它,其餘函數全部刪除即可。

還有一處必要的精簡字體文件 u8x8_fonts.cu8g2_fonts.c ,尤其是 u8g2_fonts.c ,該文件提供了包括漢字在內的幾萬個文字的多種字體,僅源文件就有 30MB ,編譯後佔據的內存非常大。

字體類型的變量非常多,建議先複製一個備份後將所有變量刪除,之後視情況再添加字體。字體變量的命名大致遵循以下規則:

 '_'  '_'  

其中:

  • 前綴基本上以 u8g2 開頭;
  • 字體名,其中可能包含字符大小
  • 各種 含義如下表所示:

名稱

描述

t

透明字體形式

h

所有字符等高

m

monospace 字體(等寬字體)

8

每一個字符都是 8×8 大小的

  • 是字體支持的字符集,如下表所示:

名稱

描述

f

只包含單字節字符

r

只包含 ASCII 範圍爲 32~127 的字符

u

只包含 ASCII 範圍爲 32~95 的字符,即不包括小寫英文

n

只包含數字及一些特殊用途的字符

還包括許多自定義的字符集,例如有一些結尾帶 gb2312 或 Chinese 的字體名就包括中文

一般建議只保留需要的字體即可。

添加回調函數

U8g2 已經包含了 SSD1306 的驅動,只需要添加一個函數 u8x8_gpio_and_delay() 用於模擬時序即可。官方文件給出了一個函數的編寫模板爲:

uint8_t u8x8_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) {
    switch (msg) {
        case U8X8_MSG_GPIO_AND_DELAY_INIT:  // called once during init phase of u8g2/u8x8
            break;                          // can be used to setup pins
        case U8X8_MSG_DELAY_NANO:           // delay arg_int * 1 nano second
            break;  
        case U8X8_MSG_DELAY_100NANO:        // delay arg_int * 100 nano seconds
            break;
        /* and many other cases */
        case U8X8_MSG_GPIO_MENU_HOME:
            u8x8_SetGPIOResult(u8x8, /* get menu home pin state */ 0);
            break;
        default:
            u8x8_SetGPIOResult(u8x8, 1);     // default return value
            break;
    }
    return 1;
}

以下是一個寫法示例:

uint8_t u8x8_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) {
    switch (msg) {
        case U8X8_MSG_DELAY_100NANO: // delay arg_int * 100 nano seconds
            __NOP();
            break;
        case U8X8_MSG_DELAY_10MICRO: // delay arg_int * 10 micro seconds
            for (uint16_t n = 0; n < 320; n++)
                __NOP();
            break;
        case U8X8_MSG_DELAY_MILLI:   // delay arg_int * 1 milli second
            delay_ms(1);
            break;
        case U8X8_MSG_DELAY_I2C:     // arg_int is the I2C speed in 100KHz, e.g. 4 = 400 KHz
            delay_us(5);
            break;                    // arg_int=1: delay by 5us, arg_int = 4: delay by 1.25us
        case U8X8_MSG_GPIO_I2C_CLOCK: // arg_int=0: Output low at I2C clock pin
            arg_int ? GPIO_SetBits(GPIO_B, GPIO_Pin_6) : GPIO_ResetBits(GPIO_B, GPIO_Pin_6);  
            break;                    // arg_int=1: Input dir with pullup high for I2C clock pin
        case U8X8_MSG_GPIO_I2C_DATA:  // arg_int=0: Output low at I2C data pin
            arg_int ? GPIO_SetBits(GPIO_B, GPIO_Pin_7) : GPIO_ResetBits(GPIO_B, GPIO_Pin_7);  
            break;                    // arg_int=1: Input dir with pullup high for I2C data pin
        case U8X8_MSG_GPIO_MENU_SELECT:
            u8x8_SetGPIOResult(u8x8, /* get menu select pin state */ 0);
            break;
        case U8X8_MSG_GPIO_MENU_NEXT:
            u8x8_SetGPIOResult(u8x8, /* get menu next pin state */ 0);
            break;
        case U8X8_MSG_GPIO_MENU_PREV:
            u8x8_SetGPIOResult(u8x8, /* get menu prev pin state */ 0);
            break;
        case U8X8_MSG_GPIO_MENU_HOME:
            u8x8_SetGPIOResult(u8x8, /* get menu home pin state */ 0);
            break;
        default:
            u8x8_SetGPIOResult(u8x8, 1); // default return value
            break;
    }
    return 1;
}

如果使用的引腳不是 PB6 和 PB7 ,注意在對應的位置修改;如果是使用硬件 I2C 的方式,那麼可以不需要模擬時序,但是需要編寫硬件驅動函數。在結尾處,會給出一個基於標準庫的硬件移植方法。

最後,不要忘記了初始化 I2C 對應的 GPIO 引腳。

U8g2簡單使用

U8g2 的初始化可以參考如下步驟:

void u8g2_Init(u8g2_t *u8g2) {
    u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2, U8G2_R0, u8x8_byte_sw_i2c, u8x8_gpio_and_delay);  // 初始化 u8g2 結構體
    u8g2_InitDisplay(u8g2);      // 根據所選的芯片進行初始化工作,初始化完成後,顯示器處於關閉狀態
    u8g2_SetPowerSave(u8g2, 0);  // 打開顯示器
    u8g2_ClearBuffer(u8g2);
}

這裏需要調用之前保留的 u8g2_Setup_ssd1306_128x64_noname_f() 函數,該函數的4個參數,其含義爲:

  • u8g2 :需要配置的 U8g2 結構體
  • rotation :配置屏幕是否要旋轉,默認使用 U8G2_R0 即可
  • byte_cb :傳輸字節的方式,這裏使用軟件 I2C 驅動,因此使用 U8g2 源碼提供的 u8x8_byte_sw_i2c() 函數。如果是硬件 I2C 的話,可以參照編寫自己的函數
  • gpio_and_delay_cb :提供給軟件模擬 I2C 的 GPIO 輸出和延時,使用之前編寫的配置函數 u8x8_gpio_and_delay()

如果需要顯示字符串,需要提前調用以下函數設置字體:

void u8g2_SetFont(u8g2_t *u8g2, const uint8_t *font);

U8g2 的繪製方式有 2 種,每種都有不同的特點。

首先是全屏緩存模式(Full screen buffer mode),它的特點是繪製速度快,並且所有的繪製方法都可以使用。但是這種模式需要大量的 RAM 空間,因此使用需要用到緩存爲 1024 字節的初始化函數(函數名以 f 結尾)。

這種繪圖的方式首先需要清除緩衝區,調用繪圖 API 後繪製的內容會保留在緩存內,需要手動發送緩存的內容到屏幕上:

u8g2_t u8g2;
u8g2_ClearBuffer(&u8g2);
/* Draw Something */
u8g2_SendBuffer(&u8g2);

第二種是分頁模式(Page mode),它同樣可以使用所有的繪製方法,但繪製速度較慢,不過佔用的 RAM 空間也少,可以使用 128 或 256 字節的緩存(函數名以 1 和 2 結尾)。

這種繪圖的方式首先創建第一頁,然後在一個 do...while 循環內部繪製圖形,不斷判斷是否到達下一頁,如果到達了就自動刷新緩存:

u8g2_FirstPage(&u8g2);
do {
    /* Draw Something */
} while (u8g2_NextPage(&u8g2));

可以認爲分頁模式是一塊一塊繪製的。

還可以使用 U8x8 的繪圖模式,這種情況下需要使用 U8x8 提供的結構體以及一系列函數,這裏不再說明。

繪圖API

完整的 API 參考可以參見官方文檔 https://github.com/olikraus/u8g2/wiki/u8g2reference/ ,裏面不僅有 API 的介紹,還有繪製效果的圖片演示。

U8g2 的座標系和絕大多數 GUI 庫一樣,原點在左上角,(x, y) 往右下遞增,座標的單位爲像素。

簡單圖形繪製

void u8g2_DrawPixel(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y);
void u8g2_DrawHLine(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, u8g2_uint_t len);
void u8g2_DrawVLine(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, u8g2_uint_t len);
void u8g2_DrawLine(u8g2_t *u8g2, u8g2_uint_t x1, u8g2_uint_t y1, u8g2_uint_t x2, u8g2_uint_t y2);

分別用於繪製像素點、根據左上角頂點 (x, y) 與長度 len 繪製水平線與垂直線,以及繪製兩點之間的線段。

void u8g2_DrawFrame(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, u8g2_uint_t w, u8g2_uint_t h);
void u8g2_DrawBox(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, u8g2_uint_t w, u8g2_uint_t h);

根據左上角的 (x, y) 座標與寬 wh 繪製空心與實心矩形。

void u8g2_DrawRBox(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, u8g2_uint_t w, u8g2_uint_t h, u8g2_uint_t r);
void u8g2_DrawRFrame(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, u8g2_uint_t w, u8g2_uint_t h, u8g2_uint_t r);

繪製實行與空心圓角矩形,多了一個參數圓角半徑 r

void u8g2_DrawCircle(u8g2_t *u8g2, u8g2_uint_t x0, u8g2_uint_t y0, u8g2_uint_t rad, uint8_t option);
void u8g2_DrawDisc(u8g2_t *u8g2, u8g2_uint_t x0, u8g2_uint_t y0, u8g2_uint_t rad, uint8_t option);

根據圓心 (x0, y0) 繪製直徑爲 rad ×2+1 的空心圓和實心圓。

option 爲圓的部分選項,此參數可控制繪製圓弧:

取值

結果

U8G_DRAW_ALL

整個圓弧

U8G2_DRAW_UPPER_RIGHT

右上部分的圓弧

U8G2_DRAW_UPPER_LEFT

左上部分的圓弧

U8G2_DRAW_LOWER_LEFT

左下部分的圓弧

U8G2_DRAW_LOWER_RIGHT

右下部分的圓弧

還可以使用按位或運算符 | 連接幾個部分。

void u8g2_DrawEllipse(u8g2_t *u8g2, u8g2_uint_t x0, u8g2_uint_t y0, u8g2_uint_t rx, u8g2_uint_t ry, uint8_t option);
void u8g2_DrawFilledEllipse(u8g2_t *u8g2, u8g2_uint_t x0, u8g2_uint_t y0, u8g2_uint_t rx, u8g2_uint_t ry, uint8_t option);

根據圓心 (x0, y0) 和水平半徑 rx 、豎直半徑 ry 繪製空心和實心橢圓。

void u8g2_DrawTriangle(u8g2_t *u8g2, int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t x2, int16_t y2);

根據三個點繪製實心三角形(空心三角形可以使用直線達到類似效果)。

void u8g2_DrawXBM(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, u8g2_uint_t w, u8g2_uint_t h, const uint8_t *bitmap);

在圖形左上角 (x, y) 根據寬 wh 繪製 XBM 格式的位圖。可以使用 https://tools.clz.me/image-to-bitmap-array 工具將一般圖片轉換爲位圖代碼。

和 Bitmap 有關的函數還有一個:

void u8g2_SetBitmapMode(u8g2_t *u8g2, uint8_t is_transparent);

該函數用於設置 Bitmap 是否透明。

字符顯示

爲了顯示字符串,首先要設置字體。調用以下函數可以提前設置字體:

void u8g2_SetFont(u8g2_t *u8g2, const uint8_t *font);
void u8g2_SetFontMode(u8g2_t *u8g2, uint8_t is_transparent);

字體是一種特殊的位圖,因此也可以設置是否透明。所有的字體保存在 u8g2_fonts.c 源文件中,注意在移植 U8g2 庫時曾經裁剪過該文件。

u8g2_uint_t u8g2_DrawStr(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, const char *str);

在左下角 (x, y) 處顯示字符串。注意,這個方法只能繪製 ASCII 字符。如有需要顯示 Unicode 字符,需要使用以下函數:

u8g2_uint_t u8g2_DrawGlyph(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, uint16_t encoding);
u8g2_uint_t u8g2_DrawUTF8(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, const char *str);

繪製 Unicode 字符和字符串。U8g2 支持 16 位的 Unicode 字符集,因此 encoding 的範圍被限制在 65535 。該函數繪製 Unicode 字符串時還需要對應的字體也支持 Unicode 字符。

注意這幾個函數都有返回值,它們返回繪製成功的字符個數。

#define u8g2_GetAscent(u8g2)
#define u8g2_GetDescent(u8g2)

這兩個宏定義用於獲取字體基線以上和基線以下的高度。上文提到的顯示字符串的函數實際上參數 y 指的是基線高度。此外注意基線以下的高度返回的是負值。

u8g2_uint_t u8g2_GetStrWidth(u8g2_t *u8g2, const char *s);
u8g2_uint_t u8g2_GetUTF8Width(u8g2_t *u8g2, const char *str);

獲取當前字體下,字符串和 UTF-8 字符串的寬度,單位爲像素。

void u8g2_SetFontDirection(u8g2_t *u8g2, uint8_t dir);

設置文字朝向,根據參數不同分別設置爲正常朝向的順時針旋轉 dir ×90° 。

其它繪圖相關API

void u8g2_SetClipWindow(u8g2_t *u8g2, u8g2_uint_t clip_x0, u8g2_uint_t clip_y0, u8g2_uint_t clip_x1, u8g2_uint_t clip_y1);

設置採集窗口大小,設置後繪製的圖形只在該窗口範圍內顯示。設置後可以使用 u8g2_SetMaxClipWindow() 函數去掉該限制。

示例代碼

以下官方示例代碼可以在 OLED 上顯示該庫的 logo :

u8g2_t u8g2;
u8g2_FirstPage(&u8g2);
do {
    u8g2_SetFontMode(&u8g2, 1);
    u8g2_SetFontDirection(&u8g2, 0);
    u8g2_SetFont(&u8g2, u8g2_font_inb24_mf);
    u8g2_DrawStr(&u8g2, 0, 20, "U");
    u8g2_SetFontDirection(&u8g2, 1);
    u8g2_SetFont(&u8g2, u8g2_font_inb30_mn);
    u8g2_DrawStr(&u8g2, 21, 8, "8");
    u8g2_SetFontDirection(&u8g2, 0);
    u8g2_SetFont(&u8g2, u8g2_font_inb24_mf);
    u8g2_DrawStr(&u8g2, 51, 30, "g");
    u8g2_DrawStr(&u8g2, 67, 30, "\xb2");
    u8g2_DrawHLine(&u8g2, 2, 35, 47);
    u8g2_DrawHLine(&u8g2, 3, 36, 47);
    u8g2_DrawVLine(&u8g2, 45, 32, 12);
    u8g2_DrawVLine(&u8g2, 46, 33, 12);
    u8g2_SetFont(&u8g2, u8g2_font_4x6_tr);
    u8g2_DrawStr(&u8g2, 1, 54, "github.com/olikraus/u8g2");
} while (u8g2_NextPage(&u8g2));

首發於:http://frozencandles.fun/archives/301

附錄:使用硬件I2C移植U8g2

硬件 I2C 效率上比軟件 I2C 快了非常多,因此特別適合 U8g2 這種大型 UI 框架。下面基於標準庫介紹硬件 I2C 的移植方式。

如果使用硬件 I2C ,需要在調用該函數(或類似函數)時,使用自己的硬件讀寫函數:

void u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2_t *u8g2, const u8g2_cb_t *rotation, u8x8_msg_cb byte_cb, u8x8_msg_cb gpio_and_delay_cb);

首先還是需要編寫一個 gpio_and_delay() 回調函數。不過由於這裏是使用硬件 I2C ,因此不再需要提供 GPIO 和時序操作的支持,只需要提供一個毫秒級的延時即可:

uint8_t u8x8_gpio_and_delay_hw(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) {
    switch (msg) {
        case U8X8_MSG_DELAY_100NANO: // delay arg_int * 100 nano seconds
            break;
        case U8X8_MSG_DELAY_10MICRO: // delay arg_int * 10 micro seconds
            break;
        case U8X8_MSG_DELAY_MILLI: // delay arg_int * 1 milli second
            Delay_ms(1);
            break;
        case U8X8_MSG_DELAY_I2C: // arg_int is the I2C speed in 100KHz, e.g. 4 = 400 KHz
            break;                    // arg_int=1: delay by 5us, arg_int = 4: delay by 1.25us
        case U8X8_MSG_GPIO_I2C_CLOCK: // arg_int=0: Output low at I2C clock pin
            break;                    // arg_int=1: Input dir with pullup high for I2C clock pin
        case U8X8_MSG_GPIO_I2C_DATA:  // arg_int=0: Output low at I2C data pin
            break;                    // arg_int=1: Input dir with pullup high for I2C data pin
        case U8X8_MSG_GPIO_MENU_SELECT:
            u8x8_SetGPIOResult(u8x8, /* get menu select pin state */ 0);
            break;
        case U8X8_MSG_GPIO_MENU_NEXT:
            u8x8_SetGPIOResult(u8x8, /* get menu next pin state */ 0);
            break;
        case U8X8_MSG_GPIO_MENU_PREV:
            u8x8_SetGPIOResult(u8x8, /* get menu prev pin state */ 0);
            break;
        case U8X8_MSG_GPIO_MENU_HOME:
            u8x8_SetGPIOResult(u8x8, /* get menu home pin state */ 0);
            break;
        default:
            u8x8_SetGPIOResult(u8x8, 1); // default return value
            break;
    }
    return 1;
}

如果是使用硬件 I2C ,那麼需要自行編寫硬件驅動函數,向 OLED 寫入字節。這個函數的編寫可以參考官方提供的軟件驅動函數 u8x8_byte_sw_i2c() ,一個編寫示例爲:

uint8_t u8x8_byte_hw_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) {
    uint8_t* data = (uint8_t*) arg_ptr;
    switch(msg) {
        case U8X8_MSG_BYTE_SEND:
            while( arg_int-- > 0 ) {
                I2C_SendData(I2C1, *data++);
                while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)) 
                    continue;
            }
            break;
        case U8X8_MSG_BYTE_INIT:
        /* add your custom code to init i2c subsystem */
            RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
            I2C_InitTypeDef I2C_InitStructure = {
                .I2C_Mode = I2C_Mode_I2C,
                .I2C_DutyCycle = I2C_DutyCycle_2,
                .I2C_OwnAddress1 = 0x10,
                .I2C_Ack = I2C_Ack_Enable,
                .I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit,
                .I2C_ClockSpeed = 400000
            };
            I2C_Init(I2C1, &I2C_InitStructure);
            I2C_Cmd(I2C1, ENABLE);  
            break;
        case U8X8_MSG_BYTE_SET_DC:
        /* ignored for i2c */
            break;
        case U8X8_MSG_BYTE_START_TRANSFER:
            while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
            I2C_GenerateSTART(I2C1, ENABLE);
            while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
                continue;
            I2C_Send7bitAddress(I2C1, 0x78, I2C_Direction_Transmitter);
            while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
                continue;
            break;
        case U8X8_MSG_BYTE_END_TRANSFER:
            I2C_GenerateSTOP(I2C1, ENABLE);
            break;
        default:
            return 0;
    }
    return 1;
}

從各個 case 標籤可以很明白地看出一個 I2C 的讀寫過程:U8X8_MSG_BYTE_INIT 標籤下需要初始化 I2C 外設,U8X8_MSG_BYTE_START_TRANSFER 標籤產生起始信號併發出目標地址,U8X8_MSG_BYTE_SEND 標籤開始發送字節,並且發送的字節存儲在 *arg_ptr 參數中,arg_int 是字節的總長度( U8g2 庫似乎一次不會傳輸多餘 32 字節的信息)。最後,U8X8_MSG_BYTE_END_TRANSFER 標籤處產生停止信號。

注意在使用硬件 I2C 時,GPIO 需要設置爲複用開漏輸出模式 GPIO_Mode_AF_OD

最後一步,用以上編寫的硬件函數初始化 U8g2 驅動:

u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2, U8G2_R0, u8x8_byte_hw_i2c, u8x8_gpio_and_delay_hw);

硬件移植過程完畢。

文章來自https://www.cnblogs.com/frozencandles/p/16358483.html

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。