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

2022-06-09 15:02:00

U8g2圖形庫

簡介

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

U8g2 庫同時包含了 U8x8 繪相簿,兩者的區別為:

  • U8g2 包含各種簡單及複雜圖形的繪製,並支援各種形式的字型,但需要佔用一定微控制器的記憶體作為繪圖快取
  • U8x8 只包含簡單的顯示文字功能,且只支援簡單、定寬的字型。它直接繪製圖形,沒有快取功能

U8g2 庫的 GitHub 地址為:https://github.com/olikraus/u8g2 ,可以從中獲取到原始碼與檔案幫助。

移植

本次以將 U8g2 移植到 STM32 微控制器與 SSD1306 通過 I2C 驅動的 128x64 OLED 為例,介紹移植的方法。不同微控制器和驅動的移植可以參考這一過程,也可以參考 U8g2 的官方移植教學 https://github.com/olikraus/u8g2/wiki/Porting-to-new-MCU-platform

首先下載或克隆 U8g2 的原始碼,這裡主要是使用 C 語言編寫,所以只需要用到 csrc 目錄下的檔案。

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

刪除無用內容

接下來,需要刪除一些無用的程式碼,並新增底層驅動的程式碼。

U8g2 的原始碼為了支援多種裝置驅動,包含了許多相容性的程式碼。首先,類似 u8x8_d_xxx.c 命名的檔案中包含 U8x8 的驅動相容,檔名包括驅動的型號和螢幕解析度,因此需要刪除無用的驅動檔案,只保留當前裝置的驅動。例如,本次使用的是 128x64 的 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 ,編譯後佔據的記憶體非常大。

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

<prefix> '_' <name> '_' <purpose> <charset>

其中:

  • <prefix> 字首基本上以 u8g2 開頭;
  • <name> 字型名,其中可能包含字元大小
  • 各種 <purpose> 含義如下表所示:
名稱 描述
t 透明字型形式
h 所有字元等高
m monospace 字型(等寬字型)
8 每一個字元都是 8x8 大小的
  • <charset> 是字型支援的字元集,如下表所示:
名稱 描述
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);

硬體移植過程完畢。