STM32 移植 U8G2
u8g2 (Universal 8bit Graphics Library version2 的縮寫)是用于嵌入式設備的單色圖形庫,可以在單色屏幕中繪制 GUI。u8g2 內部附帶了例如 SSD13xx,ST7xx 等很多 OLED,LCD 驅動。內置多種不同大小和風格的字體,可以顯示中文,其次就是圖形程序實現線,框,圓等圖形繪制。
1. 建立裸機工程
在移植 u8g2 之前先創建 STM32F103 的 Keil5 工程模板(如何搭建 Keil 模板這里不多介紹),用來編譯 STM32 驅動源碼和 u8g2 源碼。這也是支持 u8g2 開發的一個單片機運行環境,并且調試好屏幕驅動,確保屏幕初始化成功。
屏幕驅動只是為了驗證屏幕可行性,移植后方便排除因屏幕不顯示的問題,u8g2 附帶屏幕驅動程序,受支持的屏幕可以使用里面的驅動程序 。整體移植操作和芯片無關,所以無論什么芯片移植方法都相同。
2. 下載源碼
在 Github 官網搜索 u8g2 進行下載,或通過 https://github.com/olikraus/u8g2 鏈接下載,u8g2 沒有發行版,所以直接下載 master 分支源碼,下載后可以得到一個 u8g2-master.zip 這樣的源碼壓縮包,如下圖。
3. u8g2 文件概覽
u8g2 源碼主要需要使用 cppsrc/
,csrc/
這兩個文件夾。分別為 u8g2 對 C++ 兼容支持庫,u8g2 源碼文件夾。
cppsrc
對 C++ 兼容支持,方便把 u8g2 移植到 C++ 應用。cppsrc
用 C++ 類簡單封裝了 u8g2 普通操作函數(實際還是依賴 csrc
),如下所示,把 u8g2 移植到 C++ 應用才需要這部分代碼。
/*u8g2_line.c */
void U8G2::drawLine(u8g2_uint_t x1, u8g2_uint_t y1, u8g2_uint_t x2, u8g2_uint_t y2)
{u8g2_DrawLine(&u8g2, x1, y1, x2, y2);
}
其他就是一些說明文檔,輔助工具程序,版本變更信息,LICENSE 等等,這部分保留 LICENSE(開源許可) 其余可以刪除精簡工程。
4. 移植 u8g2
在 Keil 項目管理器新建一個 u8g2 文件夾,添加 u8g2 目錄 u8g2/csrc/
下所有的.c
文件。即使 u8g2 移植到 C++ 應用,這部分也是需要的(u8g2 C++ 部分實際還是依賴 csrc 的源碼)。
添加 u8g2 的頭文件目錄 u8g2/csrc/
,即 u8g2 源碼和頭文件在同一個目錄。
5. 驅動函數選擇
在 csrc
目錄下屏幕驅動文件以 u8x8_d_xx_yy_zz.c 格式命名,其中 xx
芯片型號,yy
屏幕分辨率,zz
識別名稱,zz
不是一定的,可以為空或 noname。
例如:SSD1312 芯片驅動,128x64 分辨率的 OLED,使用的驅動文件為 u8x8_d_ssd1312_128x64_noname.c
驅動配置函數通常和文件同名,在驅動文件中找到同名函數即可,比如這里就是:
uint8_t u8x8_d_ssd1312_128x64_noname(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
注意:驅動文件和驅動函數也可能不對應,例如 ssd1312
的驅動函數可能定義在 ssd1306
的驅動文件,這是 u8g2 為了把相近屏幕的驅動函數統一到一個文件。
如果要減小代碼量,其它型號屏幕驅動和分辨率對應的 .c
驅動文件可以刪除。
6. u8g2_d_setup.c
在 csrc
目錄下找到這個文件,文件包含各種屏幕的配置函數,實上選擇的屏幕驅動函數就會被這些配置函數調用。
屏幕配置函數以 u8g2_Setup_xx_yy_zz_gg_[1/2/f].c 命名,具體字段解釋如下:
xx
芯片型號,ssd1306,sh1106 等等。
yy
通信方式,識不是一定的,i2c 表示為 i2c 接口,如果沒有則表示 spi 通信。
zz
屏幕分辨率,128x64,128x80 等等。
gg
識別名稱,gg
不是一定的,可以為 noname
也可以為空。
[1/2/f]
顯存 BUF 大小,1
:128字節,2
:256 字節,f
:1024 字節。
例如:SSD1312 芯片驅動,128x64 分辨率 SPI 通信的 OLED,希望使用 1024 字節顯存,使用的配置函數為 u8g2_Setup_ssd1312_128x64_noname_f
。
/*ssd1312 f*/
void u8g2_Setup_ssd1312_128x64_noname_f(u8g2_t *u8g2, const u8g2_cb_t *rotation, u8x8_msg_cb byte_cb, u8x8_msg_cb gpio_and_delay_cb)
{uint8_t tile_buf_height;uint8_t *buf;u8g2_SetupDisplay(u8g2, u8x8_d_ssd1312_128x64_noname, u8x8_cad_001, byte_cb, gpio_and_delay_cb);buf = u8g2_m_16_8_f(&tile_buf_height);u8g2_SetupBuffer(u8g2, buf, tile_buf_height, u8g2_ll_hvline_vertical_top_lsb, rotation);
}
可以看到上方函數調用了 u8x8_d_ssd1312_128x64_noname(),如果要減小代碼量,其它的配置函數可以刪除或注釋,只留下選擇的即可。
7. u8g2_d_memory.c
在 csrc
目錄下找到 u8g2_d_memory.c,文件里面是 u8g2 對顯存的定義。在屏幕配置函數中,只調用了 u8g2_m_16_8_f(),所以如果編譯 u8g2 時如果提示內存不足,除此之外其它顯存函數可以刪除或注釋。
uint8_t *u8g2_m_16_8_f(uint8_t *page_cnt)
{#ifdef U8G2_USE_DYNAMIC_ALLOC*page_cnt = 8;return 0;#elsestatic uint8_t buf[1024];*page_cnt = 8;return buf;#endif
}
8. 對接屏幕驅動
把屏幕相關的硬件控制對接給 u8g2,需要我們實現 GPIO 控制和數據發送這 2 個統一驅動函數,函數原型如下,函數名稱可以自定義:
uint8_t (*u8x8_msg_cb)(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr);
最終通過注冊的的方式提供給 u8g2。
8.1 對接 GPIO
把屏幕的 GPIO 控制函數按照用途對接到指定位置,比如 SPI 屏幕要對接 CS
,DC
,RESET
對應的 GPIO。
uint8_t u8x8_stm32_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{/**STM32 supports HW SPI, Remove unused cases like U8X8_MSG_DELAY_XXX & U8X8_MSG_GPIO_XXX */switch (msg) {case U8X8_MSG_GPIO_AND_DELAY_INIT:/*Insert codes for initialization*/break;case U8X8_MSG_DELAY_MILLI:/* ms Delay */sleep_ms(arg_int);break;#ifdef _USE_SPI/*SPI Interface*/case U8X8_MSG_GPIO_CS:/*Insert codes for SS pin control */HAL_GPIO_WritePin(GPIOA, GPIO_PIN_15, arg_int);break;case U8X8_MSG_GPIO_DC:/*Insert codes for DC pin control */HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, arg_int);break;case U8X8_MSG_GPIO_RESET:/*Insert codes for RST pin control*/HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, arg_int);break;case U8X8_MSG_GPIO_SPI_CLOCK:/*Insert codes for CLOCK pin control */break;case U8X8_MSG_GPIO_SPI_DATA:/*Insert codes for DATA pin control */break;#endif /*_USE_SPI*/#ifdef _USE_I2Ccase U8X8_MSG_GPIO_I2C_CLOCK:/*Insert codes for CLOCK pin control */HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, arg_int);break;case U8X8_MSG_GPIO_I2C_DATA:/*Insert codes for CLOCK pin control */HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, arg_int);break;
#endif /*_USE_I2C*/}return 1;
}
如果使用 GPIO 模擬時序通信還要對接模擬通信 GPIO,比如軟件模擬 I2C 還要對接 SCL
,SDA
對應 GPIO,同時還要對接延時函數,u8g2 才能模擬時序。
8.2 對接數據
把屏幕的數據發送函數對接到指定位置,如果屏幕使用 SPI 接口,U8X8_MSG_BYTE_SEND 對應的是 SPI 數據發送函數,可以參考如下寫法:
uint8_t u8x8_byte_stm32_hw_spi(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{HAL_StatusTypeDef _res = HAL_ERROR;switch (msg) {case U8X8_MSG_BYTE_SEND:/*Insert codes to transmit data*/_res = HAL_SPI_Transmit(&hspi3, arg_ptr, arg_int, TX_TIMEOUT);if (_res != HAL_OK) return 0;break;case U8X8_MSG_BYTE_INIT:/*Insert codes to begin SPI transmission*/break;case U8X8_MSG_BYTE_SET_DC:/*Control DC pin, U8X8_MSG_GPIO_DC will be called*/u8x8_gpio_SetDC(u8x8, arg_int);break;case U8X8_MSG_BYTE_START_TRANSFER:/* Select slave, U8X8_MSG_GPIO_CS will be called */u8x8_gpio_SetCS(u8x8, u8x8->display_info->chip_enable_level);sleep_ms(2);break;case U8X8_MSG_BYTE_END_TRANSFER:sleep_ms(2);/* Insert codes to end SPI transmission */u8x8_gpio_SetCS(u8x8, u8x8->display_info->chip_disable_level);break;}return 1;
}
如果屏幕使用 I2C 接口,U8X8_MSG_BYTE_SEND 對應的是 I2C 數據發送函數,可以參考如下寫法:
uint8_t u8x8_byte_stm32_hw_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{/**u8g2/u8x8 will never send more than 32 bytes between START_TRANSFER and END_TRANSFER */static uint8_t buffer[32] = {0};static uint8_t buf_idx = 0;uint8_t * data = NULL;switch (msg) {case U8X8_MSG_BYTE_SEND:data = (uint8_t *) arg_ptr;while (arg_int > 0) {buffer[buf_idx++] = *data;data++;arg_int--;}break;case U8X8_MSG_BYTE_INIT:/*add your custom code to init i2c subsystem*/break;case U8X8_MSG_BYTE_SET_DC:break;case U8X8_MSG_BYTE_START_TRANSFER:buf_idx = 0;break;case U8X8_MSG_BYTE_END_TRANSFER:HAL_I2C_Master_Transmit(_I2C_DEV, (DEV_ADDR << 1), buffer, buf_idx, TX_TIMEOUT);break;}return 1;
}
8.3 初始化外設
如果數據發送是通過單片機外設硬件實現實現的,初始化 u8g2 前先初始化外設和對應的 GPIO,例如這里初始化 SPI 接口,還有相應 GPIO。
void oled_spi3_init(void)
{/* USER CODE BEGIN SPI3_Init 0 *//* USER CODE END SPI3_Init 0 *//* USER CODE BEGIN SPI3_Init 1 *//* USER CODE END SPI3_Init 1 */hspi3.Instance = SPI3;hspi3.Init.Mode = SPI_MODE_MASTER;hspi3.Init.Direction = SPI_DIRECTION_2LINES;hspi3.Init.DataSize = SPI_DATASIZE_8BIT;hspi3.Init.CLKPolarity = SPI_POLARITY_LOW;hspi3.Init.CLKPhase = SPI_PHASE_1EDGE;hspi3.Init.NSS = SPI_NSS_SOFT;hspi3.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;hspi3.Init.FirstBit = SPI_FIRSTBIT_MSB;hspi3.Init.TIMode = SPI_TIMODE_DISABLE;hspi3.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;hspi3.Init.CRCPolynomial = 10;if (HAL_SPI_Init(&hspi3) != HAL_OK){Error_Handler();}/* USER CODE BEGIN SPI3_Init 2 *//* USER CODE END SPI3_Init 2 */
}
8.4 初始化 u8g2
u8g2 沒有對象初始化函數,自定義 u8g2 對象初始化函數,函數名稱可以自定義,在初始化函數調用顯示初始化及對應的屏幕配置函數,如下。
void u8g2_init(u8g2_t *u8g2)
{u8g2_Setup_ssd1312_128x64_noname_f(u8g2, U8G2_R2, u8x8_byte_stm32_hw_spi, u8x8_stm32_gpio_and_delay);u8g2_SetPowerSave(u8g2, 1);u8g2_InitDisplay(u8g2);u8g2_ClearDisplay(u8g2);u8g2_ClearBuffer(u8g2);u8g2_SetPowerSave(u8g2, 0);
}
可以看到這個就是前面講解屏幕的配置函數,如下。
void u8g2_Setup_ssd1312_128x64_noname_f(u8g2_t *u8g2, const u8g2_cb_t *rotation, u8x8_msg_cb byte_cb, u8x8_msg_cb gpio_and_delay_cb);
該函數的 4 個參數含義:
(1) u8g2
,指定待初始化的 u8g2 對象,即 u8g2 結構體。
(2) rotation
,配置屏幕旋轉方向,支持 U8G2_R0,U8G2_R1,U8G2_R2,U8G2_R3 四種旋轉方向。
(3) byte_cb
,注冊數據通信發送函數。
(4) gpio_and_delay_cb
:注冊屏幕 GPIO 控制函數。
8.5 總結
把對接屏幕驅動用到的這些自定義函數統一編寫到一個文件中,文件可自行命名,例如命名為 u8g2_stm32.c,并添加到 u8g2 之外的用戶目錄。
9. 測試函數
移植完成后編寫應用程序測試 u8g2 是否正常,調用 u8g2 繪圖函數在顯存繪制內容,在 main 函數調用初始化 SPI 和 u8g2,注意在主循環中調用 u8g2 顯存發送函數 u8g2_SendBuffer 將顯存內容更新至屏幕,在此附上 u8g2 測試程序。
#include "u8g2.h"static u8g2_t u8g2 = {0};void draw(u8g2_t * u8g2_p)
{u8g2_SetFontMode(u8g2_p, 1);u8g2_SetFontDirection(u8g2_p, 0);u8g2_SetFont(u8g2_p, u8g2_font_inb24_mf);u8g2_DrawStr(u8g2_p, 0, 20, "U");u8g2_SetFontDirection(u8g2_p, 1);u8g2_SetFont(u8g2_p, u8g2_font_inb30_mn);u8g2_DrawStr(u8g2_p, 21, 8, "8");u8g2_SetFontDirection(u8g2_p, 0);u8g2_SetFont(u8g2_p, u8g2_font_inb24_mf);u8g2_DrawStr(u8g2_p, 51, 30, "g");u8g2_DrawStr(u8g2_p, 67, 30,"\xb2");u8g2_DrawHLine(u8g2_p, 2, 35, 47);u8g2_DrawHLine(u8g2_p, 3, 36, 47);u8g2_DrawVLine(u8g2_p, 45, 32, 12);u8g2_DrawVLine(u8g2_p, 46, 33, 12);u8g2_SetFont(u8g2_p, u8g2_font_4x6_tr);u8g2_DrawStr(u8g2_p, 1, 54, "github.com/olikraus/u8g2");
}void main()
{oled_spi_init();u8g2_init(&u8g2);for (;;) {u8g2_ClearBuffer(&u8g2);draw(&u8g2);u8g2_SendBuffer(&u8g2);}
}
10. 移植 C++ 版本
移植 C++ 版本基礎步驟和前面小節介紹的步驟相同,除此之外現在繼續在 Keil 項目管理器再新建一個 u8g2cpp 文件夾,然后添加 u8g2 目錄 u8g2/cppsrc/
下所有的 .cpp
文件。
10.1 u8g2 繼承類
自定義 u8g2 對象初始化類,繼承 U8G2 不是必須的。可以把類定義在 U8g2lib.h 文件中,或其他文件,類名稱可以自定義。在類的構造函數調用對應的屏幕配置函數,例如調用 ssd1312 屏幕配置。
void u8g2_Setup_ssd1312_128x64_noname_f(u8g2_t *u8g2, const u8g2_cb_t *rotation, u8x8_msg_cb byte_cb, u8x8_msg_cb gpio_and_delay_cb);
再自定義初始化函數 SSD1312::Init(),調用 u8g2 顯示初始化來初始化 u8g2。
extern "C" uint8_t u8x8_stm32_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr);
extern "C" uint8_t u8x8_byte_stm32_hw_spi(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr);class SSD1312 : public U8G2 {
private:public: SSD1312(const u8g2_cb_t *rotation = U8G2_R2) : U8G2() {u8g2_Setup_ssd1312_128x64_noname_f(&u8g2, rotation, u8x8_byte_stm32_hw_spi, u8x8_stm32_gpio_and_delay);}bool Init() {/**note: call to u8x8_utf8_init is not required here, this is done in the setup procedures before*/setPowerSave(1); initDisplay(); clearDisplay(); clearBuffer(); setPowerSave(0);return 1;}
};
10.2 測試程序
調用 SSD1312 類對象方法,編寫應用程序測試 u8g2 是否正常,調用 u8g2 繪圖函數在顯存繪制內容,初始化 u8g2 對象,在主循環中調用 sendBuffer 將顯存內容更新至屏幕。
#include "U8g2lib.h"SSD1312 oled;void draw(SSD1312 * oled_p)
{oled_p->setFontMode(1);oled_p->setFontDirection(0);oled_p->setFont(u8g2_font_inb24_mf);oled_p->drawStr(0, 20, "U");oled_p->setFontDirection(1);oled_p->setFont(u8g2_font_inb30_mn);oled_p->drawStr(21, 8, "8");oled_p->setFontDirection(0);oled_p->setFont(u8g2_font_inb24_mf);oled_p->drawStr(51, 30, "g");oled_p->drawStr(67, 30,"\xb2");oled_p->drawHLine(2, 35, 47);oled_p->drawHLine(3, 36, 47);oled_p->drawVLine(45, 32, 12);oled_p->drawVLine(46, 33, 12);oled_p->setFont(u8g2_font_4x6_tr);oled_p->drawStr(1, 54, "github.com/olikraus/u8g2");
}void main()
{oled.Init();for (;;) {oled.clearBuffer();draw(&oled);oled.sendBuffer();}
}
詳細查看:
https://blog.csdn.net/qq_51183186/article/details/120348070