U8g2 是一個用於嵌入式裝置的簡易圖形庫,可以在多種 OLED 和 LCD 螢幕上,支援包括 SSD1306 等多種型別的底層驅動,並可以很方便地移植到 Arduino 、樹莓派、NodeMCU 和 ARM 上。
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.c
和 u8g2_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.c
和 u8g2_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 的初始化可以參考如下步驟:
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 參考可以參見官方檔案 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
) 座標與寬 w
高 h
繪製空心與實心矩形。
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
) 根據寬 w
高 h
繪製 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° 。
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 效率上比軟體 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);
硬體移植過程完畢。