hi3861移植u8g2库并显示中文

移植 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 的接口将字体读出.

因此添加中文显示支持需要适配两处:

  1. 将中文字体数据数组存放在 Flash 中
  2. 适配读 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 placeXIP)是指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, &param);
            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);

最终效果如下图所示

标题: hi3861移植u8g2库并显示中文
作者: QingChenW
链接: https://dawncraft.cc/2024/03/549/
本文遵循 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 许可
禁止商用, 非商业转载请注明作者及来源!
上一篇
隐藏