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.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簡單使用
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) 坐標與寬 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° ,
其它繪圖相關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 = https://www.cnblogs.com/frozencandles/p/(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.uj5u.com/caozuo/492403.html
標籤:嵌入式
