移植 u8g2 库
首先从 https://github.com/olikraus/u8g2 将 u8g2 的代码下载至 third_party/u8g2 文件夹中.
然后新建 BUILD.gn
文件, 编写将 u8g2 的源码构建为静态库的构建脚本, 内容如下所示:
static_library("u8g2") { sources = [ "csrc/u8x8_setup.c", "csrc/u8x8_u8toa.c", "csrc/u8g2_d_memory.c", "csrc/u8g2_selection_list.c", "csrc/u8g2_hvline.c", "csrc/u8x8_message.c", "csrc/u8g2_kerning.c", "csrc/u8x8_input_value.c", "csrc/u8g2_polygon.c", "csrc/u8g2_input_value.c", "csrc/u8x8_8x8.c", "csrc/u8x8_debounce.c", "csrc/u8g2_setup.c", "csrc/u8x8_display.c", "csrc/u8g2_box.c", "csrc/u8x8_selection_list.c", "csrc/u8x8_byte.c", "csrc/u8log.c", "csrc/u8log_u8x8.c", "csrc/u8g2_message.c", "csrc/u8g2_line.c", "csrc/u8g2_intersection.c", "csrc/u8g2_fonts.c", "csrc/u8g2_cleardisplay.c", "csrc/u8g2_font.c", "csrc/u8x8_cad.c", "csrc/u8x8_string.c", "csrc/u8g2_ll_hvline.c", "csrc/u8x8_fonts.c", "csrc/u8log_u8g2.c", "csrc/u8g2_circle.c", "csrc/u8g2_buffer.c", "csrc/u8g2_bitmap.c", "csrc/u8x8_gpio.c", "csrc/u8g2_d_setup.c", "csrc/u8x8_u16toa.c", "csrc/u8x8_capture.c", "csrc/u8g2_button.c", "csrc/u8x8_d_ssd1306_128x64_noname.c" ] include_dirs = [ "csrc" ] }
添加中文显示支持
由于中文字体体积较大, 一般来说难以存储在单片机的 SRAM 中, 而是需要存储到 Flash 中, 再使用读 Flash 的接口将字体读出.
因此添加中文显示支持需要适配两处:
- 将中文字体数据数组存放在 Flash 中
- 适配读 Flash 接口
首先研究如何将变量存在 Flash 中, 这需要对单片机的 ld 链接脚本进行分析, 看哪个段的变量存放在 Flash 里.
这里我分析的是 OpenHarmony 3.1 中的 Hi3861 SDK 代码, 但考虑到 3861 已进入量产维护阶段, 其余版本的 SDK 应该相差不大.
首先完整编译一遍系统, 然后在 device\hisilicon\hispark_pegasus\sdk_liteos\build\build_tmp\scripts 目录下可找到 link.lds
链接脚本, 其 95 行起的代码如下所示:
.text_non_rom : AT ((((4248512 - (278K - 32) - 32) + (278K - 32)) + 32)) { . = ALIGN(0x20); __text_cache_start2_ = .; KEEP(*(.entry.text)) . = ALIGN(0x20); SORT(*)(.init*) SORT(*)(.rom.text.patch) SORT(*)(EXCLUDE_FILE(*libasm_flash.o) .text*) SORT(*)(EXCLUDE_FILE(*libasm_flash.o) .got*) . = ALIGN(0x20); __text_cache_end2_ = .; SORT(*)(.rodata*) . = ALIGN(0x20); __text_rodata_end_ = .; . = ALIGN(0x20); } > FLASH
可以看到 rodata
段的变量都存储在 Flash 中, 所以只要将字体变量存储的段设置为 .rodata.xxx
即可.
接下来是适配读 Flash 接口, 一般来说需要自己实现 u8x8_pgm_read
函数, 传入地址并返回一个字节的数据.
但目前大部分单片机都支持 xip 技术, 因此只需要像访问内存上的变量一样直接访问数组就可以了.
在计算机科学中,就地执行,或称片内执行(英语:Execute in place,XIP)是指CPU直接从存储器中读取程序代码执行,而不用再读到内存中。好处即是程序代码无需占用内存,减少内存的要求。
—— 引用自维基百科
例如 u8g2 库中自带的 esp8266 实现:
u8g2/csrc/u8x8.h#L160: #if defined(ESP8266) uint8_t u8x8_pgm_read_esp(const uint8_t * addr); /* u8x8_8x8.c */ # define U8X8_FONT_SECTION(name) __attribute__((section(".text." name))) # define u8x8_pgm_read(adr) u8x8_pgm_read_esp(adr) # define U8X8_PROGMEM #endif u8g2/csrc/u8x8_8x8.c#L40 #if defined(ESP8266) uint8_t u8x8_pgm_read_esp(const uint8_t * addr) { uint32_t bytes; bytes = *(uint32_t*)((uint32_t)addr & ~3); return ((uint8_t*)&bytes)[(uint32_t)addr & 3]; } #endif
可以看到在 u8x8_pgm_read_esp
函数按 4 字节对齐从传入的地址处读取了一个双字, 然后再返回地址对应的字节, 这是因为 esp8266 的 Flash 只支持 4 字节对齐的读写操作. 但 Hi3861 没有这个限制, 所以直接返回地址对应的字节即可, 而默认实现就是直接对地址解引用, 也就是说在 Hi3861 上无需实现 u8x8_pgm_read
.
最后再打开使用大字体的宏就 ok 啦, 下面给出我的适配代码:
third_party/u8g2/csrc/u8x8.h 的 166 行下面插入以下代码:
#if defined(CHIP_VER_Hi3861) # define U8X8_FONT_SECTION(name) __attribute__((section(".rodata.font." name))) // # define u8x8_pgm_read(adr) // Hi3861 支持 xip 和非对齐读写, 使用默认实现即可 # define U8X8_PROGMEM #endif
third_party/u8g2/csrc/u8g2.h 的 193 行改为以下代码 (添加 defined(CHIP_VER_Hi3861)
):
#if defined(unix) || defined(__unix__) || defined(__arm__) || defined(__arc__) || defined(ESP8266) || defined(ESP_PLATFORM) || defined(__LUATOS__) || defined(CHIP_VER_Hi3861) #ifndef U8G2_USE_LARGE_FONTS #define U8G2_USE_LARGE_FONTS #endif #endif
*这里的 CHIP_VER_Hi3861
也可以改为其他 Hi3861 平台特有的宏
创建示例项目
创建项目的流程可以参考其他 OpenHarmony 教程, 这里就只贴一下我的 BUILD.gn
脚本.
创建 applications\sample\wifi-iot\app\oled_demo 目录, 在该目录下创建 BUILD.gn
脚本, 内容如下:
static_library("oled_demo") { sources = [ "//vendor/bearpi/bearpi_hm_nano/common/iot_hardware_hals/src/hal_iot_adc.c", "//vendor/bearpi/bearpi_hm_nano/common/iot_hardware_hals/src/hal_iot_gpio_ex.c", "//vendor/bearpi/bearpi_hm_nano/common/iot_hardware_hals/src/hal_iot_i2c_ex.c", "oled_demo.c" ] include_dirs = [ "//utils/native/lite/include", "//kernel/liteos_m/kal/cmsis", "//base/iot_hardware/peripheral/interfaces/kits", "//vendor/bearpi/bearpi_hm_nano/common/iot_hardware_hals/include", "//device/hisilicon/hispark_pegasus/sdk_liteos/include", "//third_party/u8g2/csrc" ] }
这里我使用了小熊派的 iot 扩展函数, 如果你用的是润和的不要忘记改一下下面代码中函数的名字. OpenHarmony 3.2 及以上版本上述头文件目录也有差异, 需要自行修改一下.
然后新建 oled_demo.c, 用于编写代码.
最后修改 applications\sample\wifi-iot\app\BUILD.gn, 在 features 数组中加入 "//third_party/u8g2:u8g2"
和 "oled_demo:oled_demo"
.
lite_component("app") { features = [ "startup", "//third_party/u8g2:u8g2", "oled_demo:oled_demo" ] }
适配硬件 SPI
这里使用的是 Hi3861 的 SPI0
, 代码如下:
#include <stdio.h> #include <string.h> #include <unistd.h> #include "ohos_init.h" #include "cmsis_os2.h" #include "iot_errno.h" #include "iot_gpio.h" #include "iot_gpio_ex.h" #include "hi_io.h" #include "hi_spi.h" #include "u8g2.h" #define IOT_SPI 0 #define OLED_RST 13 #define OLED_DC 14 static u8g2_t u8g2; uint8_t u8x8_byte_hw_spi(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) { switch (msg) { case U8X8_MSG_BYTE_SEND: hi_spi_host_write(IOT_SPI, (uint8_t*) arg_ptr, arg_int); break; case U8X8_MSG_BYTE_INIT: { hi_spi_cfg_init_param init_param = { .is_slave = false }; hi_spi_cfg_basic_info param = { .cpol = HI_SPI_CFG_CLOCK_CPOL_0, .cpha = HI_SPI_CFG_CLOCK_CPHA_0, .fram_mode = HI_SPI_CFG_FRAM_MODE_MOTOROLA, .data_width = HI_SPI_CFG_DATA_WIDTH_E_8BIT, .endian = HI_SPI_CFG_ENDIAN_LITTLE, .freq = 8000000, }; // 初始化 GPIO13 作为 OLED RST IoTGpioInit(OLED_RST); IoTGpioSetFunc(OLED_RST, IOT_GPIO_FUNC_GPIO_13_GPIO); IoTGpioSetDir(OLED_RST, IOT_GPIO_DIR_OUT); // 初始化 GPIO14 作为 OLED DC IoTGpioInit(OLED_DC); IoTGpioSetFunc(OLED_DC, IOT_GPIO_FUNC_GPIO_14_GPIO); IoTGpioSetDir(OLED_DC, IOT_GPIO_DIR_OUT); // GPIO9 复用为 SPI0 TXD IoTGpioInit(9); IoTGpioSetFunc(9, IOT_GPIO_FUNC_GPIO_9_SPI0_TXD); // GPIO10 复用为 SPI0 CLK IoTGpioInit(10); IoTGpioSetFunc(10, IOT_GPIO_FUNC_GPIO_10_SPI0_CK); hi_io_set_driver_strength(10, HI_IO_DRIVER_STRENGTH_2); // GPIO11 复用为 SPI0 RXD IoTGpioInit(11); IoTGpioSetFunc(11, IOT_GPIO_FUNC_GPIO_11_SPI0_RXD); // GPIO12 复用为 SPI0 CS IoTGpioInit(12); IoTGpioSetFunc(12, IOT_GPIO_FUNC_GPIO_12_SPI0_CSN); // 初始化 SPI0, 参数如上方结构体所示 uint8_t ret = hi_spi_init(IOT_SPI, init_param, ¶m); if (ret != IOT_SUCCESS) { printf("Failed to init SPI! Error: %d\n", ret); return 0; } break; } case U8X8_MSG_BYTE_SET_DC: IoTGpioSetOutputVal(OLED_DC, arg_int); break; case U8X8_MSG_BYTE_START_TRANSFER: break; case U8X8_MSG_BYTE_END_TRANSFER: break; default: return 0; } return 1; } uint8_t u8x8_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) { uint8_t i; switch (msg) { case U8X8_MSG_GPIO_AND_DELAY_INIT: break; case U8X8_MSG_DELAY_NANO: usleep(1); break; case U8X8_MSG_DELAY_MILLI: usleep(arg_int * 1000); break; case U8X8_MSG_GPIO_DC: IoTGpioSetOutputVal(OLED_DC, arg_int); break; case U8X8_MSG_GPIO_RESET: IoTGpioSetOutputVal(OLED_RST, arg_int); break; default: return 0; } return 1; }
绘制中文文本
写了一个假的进度条以及“加载中…”文字作为演示, 代码如下:
// 居中绘制文本 u8g2_uint_t u8g2_DrawCenterUTF8(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, const char *str) { u8g2_DrawUTF8(u8g2, x - u8g2_GetUTF8Width(u8g2, str) / 2, y, str); } static void OLEDTask(void) { // 初始化屏幕 u8g2_Setup_ssd1306_128x64_noname_f(&u8g2, U8G2_R0, u8x8_byte_hw_spi, u8x8_gpio_and_delay); u8g2_InitDisplay(&u8g2); u8g2_SetPowerSave(&u8g2, 0); u8g2_SetFont(&u8g2, u8g2_font_wqy16_t_gb2312); int progress = 0; while (1) { u8g2_ClearBuffer(&u8g2); u8g2_DrawFrame(&u8g2, 16, 16, 96, 16); u8g2_DrawBox(&u8g2, 18, 18, progress * (96 - 4) / 100, 12); u8g2_DrawCenterUTF8(&u8g2, 64, 56, "加载中..."); u8g2_SendBuffer(&u8g2); if (progress++ == 100) { progress = 0; sleep(3); } usleep(1000); } } static void OLEDDemo(void) { osThreadAttr_t attr; attr.name = "OLEDTask"; attr.attr_bits = 0U; attr.cb_mem = NULL; attr.cb_size = 0U; attr.stack_mem = NULL; attr.stack_size = 0x4000; attr.priority = osPriorityNormal; if (osThreadNew(OLEDTask, NULL, &attr) == NULL) { printf("Failed to create OLEDTask!\n"); } } SYS_RUN(OLEDDemo);
最终效果如下图所示