移植 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);
最终效果如下图所示
