高通字库
版本 V2.0 · 更新于 2026-05-25

12. GT-HMI Engine 移植操作流程

12.1 GT-HMI Engine 移植平台流程

  移植 GT_GUI 下位机流程导图如下所示(本手册以正点原子 STM32F429 阿波罗开发板示例):

图7-1 移植流程导图

图7-1 移植流程导图

12.1.1 准备工作

图7-2 准备工作示意图

图7-2 准备工作示意图

  首先需要一份已经实现对应外设驱动(显示屏驱动、触摸驱动、定时器驱动、SDRAM 或 SRAM 驱动,如果需要使用到字库内容或 flash 文件系统,则还需要实现对应的 SPI flash 驱动)的工程文件。

图7-3 外设驱动示例

图7-3 外设驱动示例

  实现对应驱动的示例。

  然后还需要准备 GUI 下位机的 GT-HMI-Engine 平台引擎包(官网下载或仓库克隆,这里将文件包改名为 gui):

图7-4 GT-HMI-Engine 引擎包下载

图7-4 GT-HMI-Engine 引擎包下载

图7-5 GT-HMI-Engine 引擎包目录结构

图7-5 GT-HMI-Engine 引擎包目录结构

  GT-HMI-Engine 引擎包的目录结构。直接将整个文件夹放入 STM32F429 工程的根目录。

图7-6 放入工程根目录

图7-6 放入工程根目录

12.1.2 移植编译

  在 keil 工程中新建 gui 的 groups 文件夹,并将 driver 和 src 文件中的 C 文件添加进去,example 文件夹为控件示例可以添加也可以不添加。

图7-7 新建 gui groups 文件夹

图7-7 新建 gui groups 文件夹

图7-8 添加 driver 中的 C 文件

图7-8 添加 driver 中的 C 文件

图7-9 添加 src 中的 C 文件

图7-9 添加 src 中的 C 文件

图7-10 添加 example 中的 C 文件

图7-10 添加 example 中的 C 文件

  添加 C 文件后,还需要在工程配置中将对应的头文件路径一一添加进来。

图7-11 添加头文件路径(1)

图7-11 添加头文件路径(1)

图7-12 添加头文件路径(2)

图7-12 添加头文件路径(2)

图7-13 添加头文件路径(3)

图7-13 添加头文件路径(3)

  添加完成如上图头文件路径后还需要打开 C99 和 GUN 模式编译选项,以防编译报错。

图7-14 开启 C99 和 GUN 模式

图7-14 开启 C99 和 GUN 模式

  设置完成后编译,会提示四个错误。

图7-15 编译错误提示

图7-15 编译错误提示

  原因是还没有实现对应的刷屏、触摸、按键和 SPI 读写驱动接口。

  在 main.c 文件中分别实现这些接口函数(注意函数名与各自外部声明的位置一致):

  _flush_cb 刷屏函数(声明在 gt_port_disp.c 中)

图7-16 _flush_cb 刷屏函数实现

图7-16 _flush_cb 刷屏函数实现

  read_cb 触摸函数(声明在 gt_port_indev.c 中)

图7-17 read_cb 触摸函数实现

图7-17 read_cb 触摸函数实现

  read_cb_btn 按键函数(声明在 gt_port_indev.c 中)

图7-18 read_cb_btn 按键函数实现

图7-18 read_cb_btn 按键函数实现

  spi_wr 函数(声明在 gt_port_vf.c 中,这里没使用字库,可以为空不实现功能,查看 6.2 实现。)

图7-19 spi_wr 函数实现

图7-19 spi_wr 函数实现

  再次编译,会提示成功未出现报错。

图7-20 编译成功

图7-20 编译成功

  在 main.c 文件中 include 包含 gt.h,然后调用里面声明的 gt_init 函数,再次编译,可能会提示 No space xxx 的错误。

图7-21 包含 gt.h 并调用 gt_init

图7-21 包含 gt.h 并调用 gt_init

图7-22 No space 错误提示

图7-22 No space 错误提示

  原因是 gt_port_disp.c 中定义的刷屏数组太大,超过片内的 RAM 空间而报错,将其定义在片外的 SRAM 中即可,其定义的地址需要自己参考对应主控芯片的情况以及 SRAM 的地址范围来修改。

图7-23 重定义刷屏数组到片外 SRAM

图7-23 重定义刷屏数组到片外 SRAM

  重定义 gt_port_disp.c 中的刷屏数组。再次编译即可通过。

图7-24 编译通过

图7-24 编译通过

  之后修改 gt_conf.h 中的屏幕尺寸和内存空间参数,以适配开发板环境。

图7-25 修改屏幕尺寸参数

图7-25 修改屏幕尺寸参数

图7-26 修改内存空间参数

图7-26 修改内存空间参数

  如果使用控件很多,可适当加大 GT_MEM_SIZE 的大小。

图7-27 GT_MEM_SIZE 配置

图7-27 GT_MEM_SIZE 配置

  需要根据实际使用的字库来修改 GT_FONT_FAMILY_OLD_ENABLE,为 1 时使用旧版字库体系,为 0 时使用新版字库体系,不然在编译时会有报错。

图7-28 GT_FONT_FAMILY_OLD_ENABLE 配置

图7-28 GT_FONT_FAMILY_OLD_ENABLE 配置

  在开发板的定时器驱动中加入 1ms 的心跳函数 gt_tick_inc(1),保证 GUI 持续运行。

图7-29 加入心跳函数

图7-29 加入心跳函数

  在 main 函数中设置定时器 1ms 触发一次中断并在 while 循环中加入 GUI 的事务处理函数 gt_task_handler 使其每 1ms 循环执行一次。

图7-30 main 函数中的 GUI 初始化

图7-30 main 函数中的 GUI 初始化

  导入 example 文件夹中的例程,调用控件的例程进行测试。

图7-31 导入控件例程(1)

图7-31 导入控件例程(1)

图7-32 导入控件例程(2)

图7-32 导入控件例程(2)

  编译并烧录进开发板,可以看到 btn 控件的生成。

图7-33 btn 控件显示效果

图7-33 btn 控件显示效果

  点击按钮会有对应事件出现:

图7-34 按钮点击事件效果

图7-34 按钮点击事件效果

  至此移植完成。

12.2 素材导入流程(bin 文件方式)

  在 GUI 平台上如果使用自定义的图片或字库等素材,采用 Bin 文件形式将素材导入到下位机板子中。

  GT-HMI Designer 工程文件编译成功后,会在工程目录下生成 out 文件夹(使用 GT-HMI 的模块时请使用 board 文件夹中的 resource.bin 文件),其中包含 resource.bin 文件。

图7-35 out 文件夹中的 resource.bin

图7-35 out 文件夹中的 resource.bin

  将 resource.bin 文件烧录到您所使用的我司提供的 flash 芯片中。然后复制生成的 gt_port_vf.c 文件,假如使用字库则会额外生成 gt_gui_driver.h 和 gt_gui_driver.c 文件,将这些文件复制到工程中。

图7-36 复制 gt_port_vf.c 文件

图7-36 复制 gt_port_vf.c 文件

图7-37 复制字库驱动文件

图7-37 复制字库驱动文件

图7-38 添加文件到工程

图7-38 添加文件到工程

  实现 gt_gui_driver.h 中外部声明的 r_dat_bat 函数和之前移植中未实现的 spi_wr() 函数。

图7-39 r_dat_bat 函数实现

图7-39 r_dat_bat 函数实现

图7-40 spi_wr 函数实现

图7-40 spi_wr 函数实现

图7-41 函数实现完成

图7-41 函数实现完成

  实现上述两个函数之后,调用文字或者 img 图片控件,设置图片路径,可以看到文字和图片显示情况,根据需求做排版布局调整。

图7-42 调用控件设置图片路径

图7-42 调用控件设置图片路径

图7-43 文字和图片显示效果

图7-43 文字和图片显示效果

12.3 素材导入流程(C 文件数组方式)

  在 GUI 平台上如果使用自定义的图片或字库等素材,采用 C 文件数组形式将素材导入到下位机板子中。

  GT-HMI Designer 工程文件编译后,图片素材同样会以数组形式导出来,存放路径为"工程存放路径/sources/imgsCode"。Image1 是图片 1 转换后的 C 文件,Image2 是图片 2 转换后的 C 文件,以此类推。

图7-44 imgsCode 目录中的图片 C 文件

图7-44 imgsCode 目录中的图片 C 文件

  将各自图片 C 文件中的数组直接复制到工程的 gt_port_src.c 文件中:

图7-45 复制数组到 gt_port_src.c

图7-45 复制数组到 gt_port_src.c

  使用 imgsCode/gt_src.c 文件中数组 user_src_list 列举出的文件路径数据替换掉 gt_port_src.c 中的 _src_icon_sys 结构体数组:

图7-46 替换结构体数组(图1)

图7-46 替换结构体数组(图1)

图7-47 替换结构体数组(图2)

图7-47 替换结构体数组(图2)

  调用 img 控件并赋予文件路径即可显示:

图7-48 调用 img 控件

图7-48 调用 img 控件

图7-49 图片显示效果

图7-49 图片显示效果

12.4 需要实现的硬件适配代码

  移植 GUI 所需适配的底层硬件函数及示例。

刷屏函数

  函数原型:

void _flush_cb(struct _gt_disp_drv_s * drv,
               gt_area_st * area, gt_color_t * color)

  参数说明:_gt_disp_drv_s drv 此参数不需用户适配;gt_area_st 结构体中 area->x 为绘图起始 x 坐标,area->y 为绘图起始 y 坐标,area->w 为绘图区域宽度,area->h 为绘图区域高度;color 为图片数据数组。

  例程中调用的为正点原子的区域填充代码,其余方式的实现函数只需要能够在指定区域绘制指定大小的图片均可。

void _flush_cb(struct _gt_disp_drv_s * drv,
               gt_area_st * area, gt_color_t * color)
{
    LCD_Color_Fill(area->x, area->y,
        area->w + area->x - 1, area->h + area->y - 1,
        (uint16_t *)color);
}

  正点原子 LCD_Color_Fill 函数实现示例(非 RGB 屏):

// 在指定区域内填充指定颜色块
// (sx,sy),(ex,ey): 填充矩形对角坐标
// color: 要填充的颜色
void LCD_Color_Fill(u16 sx, u16 sy, u16 ex, u16 ey, u16 *color)
{
    u16 height, width;
    u16 i, j;
    if (lcdltdc.pwidth != 0) { // 如果是 RGB 屏
        LTDC_Color_Fill(sx, sy, ex, ey, color);
    } else {
        width = ex - sx + 1;   // 得到填充的宽度
        height = ey - sy + 1;  // 高度
        for (i = 0; i < height; i++) {
            LCD_SetCursor(sx, sy + i);   // 设置光标位置
            LCD_WriteRAM_Prepare();      // 开始写入 GRAM
            for (j = 0; j < width; j++)
                LCD->LCD_RAM = color[i * width + j]; // 写入数据
        }
    }
}

  RGB 屏使用 DMA2D 方式传输的 LTDC_Color_Fill 实现示例:

// 在指定区域内填充指定颜色块,DMA2D 填充
// 仅支持 u16/RGB565 格式颜色数组
void LTDC_Color_Fill(u16 sx, u16 sy, u16 ex, u16 ey, u16 *color)
{
    u32 psx, psy, pex, pey;
    u32 timeout = 0;
    u16 offline;
    u32 addr;
    // 坐标系转换
    if (lcdltdc.dir) { // 横屏
        psx = sx; psy = sy;
        pex = ex; pey = ey;
    } else { // 竖屏
        psx = sy; psy = lcdltdc.pheight - ex - 1;
        pex = ey; pey = lcdltdc.pheight - sx - 1;
    }
    offline = lcdltdc.pwidth - (pex - psx + 1);
    addr = ((u32)ltdc_framebuf[lcdltdc.activelayer]
            + lcdltdc.pixsize * (lcdltdc.pwidth * psy + psx));
    RCC->AHB1ENR |= 1 << 23;   // 使能 DMA2D 时钟
    DMA2D->CR &= ~(1 << 0);    // 先停止 DMA2D
    DMA2D->CR = 0 << 16;       // 存储器到存储器模式
    DMA2D->FGPFCCR = LCD_PIXFORMAT; // 设置颜色格式
    DMA2D->FGOR = 0;            // 前景层行偏移为 0
    DMA2D->OOR = offline;       // 设置行偏移
    DMA2D->FGMAR = (u32)color;  // 源地址
    DMA2D->OMAR = addr;         // 输出存储器地址
    DMA2D->NLR = (pey - psy + 1) | ((pex - psx + 1) << 16); // 设定行数寄存器
    DMA2D->CR |= 1 << 0;        // 启动 DMA2D
    while ((DMA2D->ISR & (1 << 1)) == 0) { // 等待传输完成
        timeout++;
        if (timeout > 0X1FFFFF) break; // 超时退出
    }
    DMA2D->IFCR |= 1 << 1; // 清除传输完成标志
}

读取触摸函数

  函数原型:

void read_cb(struct _gt_indev_drv_s * indev_drv,
        gt_indev_data_st * data)

  在此函数中,判断有无触摸事件发生,如果没有发生则返回,如果发生触摸则将触摸点的 x 坐标和 y 坐标传入 data 结构体,并将 state 设置为 GT_INDEV_STATE_PRESSED。

void read_cb(struct _gt_indev_drv_s * indev_drv, 
        gt_indev_data_st * data)
{
    if (!tp_dev.scan(1)) {
        data->state = GT_INDEV_STATE_RELEASED;
        return;
    }
    data->point.x = tp_dev.x[0];
    data->point.y = tp_dev.y[0];
    data->state = GT_INDEV_STATE_PRESSED;
}

  主要需要实现 scan 函数,正点原子例程中针对不同触摸设备设计了不同的 scan 函数,以下为 GT9147 触摸芯片的示例实现:

// 扫描触摸屏(采用查询方式)
// mode: 0,正常扫描
// 返回值: 0 无触摸;1 有触摸
u8 GT9147_Scan(u8 mode)
{
  u8 buf[4];
  u8 i = 0;
  u8 res = 0;
  u8 temp;
  u8 tempsta;
  static u8 t = 0; // 控制查询间隔,降低 CPU 占用率
  t++;
  if ((t % 10) == 0 || t < 10) {
    GT9147_RD_Reg(GT_GSTID_REG, &mode, 1); // 读取触摸点状态
    if (mode & 0X80 && ((mode & 0XF) < 6)) {
      temp = 0;
      GT9147_WR_Reg(GT_GSTID_REG, &temp, 1); // 清标志
    }
    if ((mode & 0XF) && ((mode & 0XF) < 6)) {
      temp = 0XFF << (mode & 0XF);
      tempsta = tp_dev.sta;
      tp_dev.sta = (~temp) | TP_PRES_DOWN | TP_CATH_PRES;
      tp_dev.x[4] = tp_dev.x[0];
      tp_dev.y[4] = tp_dev.y[0];
      for (i = 0; i < 5; i++) {
        if (tp_dev.sta & (1 << i)) {
          GT9147_RD_Reg(GT9147_TPX_TBL[i], buf, 4);
          if (lcddev.id == 0X5510) { // 4.3 寸 800*480 MCU 屏
            if (tp_dev.touchtype & 0X01) { // 横屏
              tp_dev.y[i] = ((u16)buf[1] << 8) + buf[0];
              tp_dev.x[i] = 800 - (((u16)buf[3] << 8) + buf[2]);
            } else {
              tp_dev.x[i] = ((u16)buf[1] << 8) + buf[0];
              tp_dev.y[i] = ((u16)buf[3] << 8) + buf[2];
            }
          } else if (lcddev.id == 0X4342) { // 4.3 寸 480*272 RGB 屏
            if (tp_dev.touchtype & 0X01) {
              tp_dev.x[i] = (((u16)buf[1] << 8) + buf[0]);
              tp_dev.y[i] = (((u16)buf[3] << 8) + buf[2]);
            } else {
              tp_dev.y[i] = ((u16)buf[1] << 8) + buf[0];
              tp_dev.x[i] = 272 - (((u16)buf[3] << 8) + buf[2]);
            }
          } else if (lcddev.id == 0X4384) { // 4.3 寸 800*480 RGB 屏
            if (tp_dev.touchtype & 0X01) {
              tp_dev.x[i] = (((u16)buf[1] << 8) + buf[0]);
              tp_dev.y[i] = (((u16)buf[3] << 8) + buf[2]);
            } else {
              tp_dev.y[i] = ((u16)buf[1] << 8) + buf[0];
              tp_dev.x[i] = 480 - (((u16)buf[3] << 8) + buf[2]);
            }
          }
        }
      }
      res = 1;
      if (tp_dev.x[0] > lcddev.width || tp_dev.y[0] > lcddev.height) {
        if ((mode & 0XF) > 1) {
          tp_dev.x[0] = tp_dev.x[1];
          tp_dev.y[0] = tp_dev.y[1];
          t = 0;
        } else {
          tp_dev.x[0] = tp_dev.x[4];
          tp_dev.y[0] = tp_dev.y[4];
          mode = 0X80;
          tp_dev.sta = tempsta;
        }
      } else {
        t = 0;
      }
    }
  }
  if ((mode & 0X8F) == 0X80) { // 无触摸点按下
    if (tp_dev.sta & TP_PRES_DOWN) {
      tp_dev.sta &= ~(1 << 7); // 标记按键松开
    } else {
      tp_dev.x[0] = 0xffff;
      tp_dev.y[0] = 0xffff;
      tp_dev.sta &= 0XE0; // 清除点有效标记
    }
  }
  if (t > 240) t = 10;
  return res;
}

读取按键函数

void read_cb_btn(struct _gt_indev_drv_s * indev_drv,
                 gt_indev_data_st * data)
{
    uint8_t status = KEY_Scan(1);
    if (status) {
        data->btn_id = status;
        data->state = GT_INDEV_STATE_PRESSED;
    } else {
        data->state = GT_INDEV_STATE_RELEASED;
    }
}

SPI 读写函数

  函数原型:

uint32_t spi_wr(uint8_t * data_write, uint32_t len_write,
                uint8_t * data_read, uint32_t len_read)

  参数说明:data_write 存放 24 位地址值(高/中/低各 8 位),data_read 为存放读取数据的指针,len_read 为读取数据的长度。

uint32_t spi_wr(uint8_t * data_write, uint32_t len_write,
                uint8_t * data_read, uint32_t len_read)
{
    unsigned long ReadAddr;
    ReadAddr = *(data_write + 1) << 16;
    ReadAddr += *(data_write + 2) << 8;
    ReadAddr += *(data_write + 3);
    r_dat_bat(ReadAddr, len_read, data_read);
}

定时器配置

  GUI 需要一个 1ms 一次的心跳脉冲,通过定时中断来实现:

void TIM3_IRQHandler(void)
{
    if (TIM3->SR & 0X0001) {
        gt_tick_inc(1);
    }
    TIM3->SR &= ~(1 << 0);
}

  在初始化硬件时,将 TIM3 的定时时间设置为 1ms:

TIM3_Int_Init(10 - 1, 9000 - 1); // 1ms 定时中断

  TIM3_Int_Init 函数实现示例(时钟为 APB1 的 2 倍,APB1 为 48M):

// arr: 自动重装值;psc: 时钟预分频数
// 溢出时间 Tout = ((arr+1)*(psc+1)) / Ft us,Ft 单位 MHz
void TIM3_Int_Init(u16 arr, u16 psc)
{
    RCC->APB1ENR |= 1 << 1;  // TIM3 时钟使能
    TIM3->ARR = arr;          // 设定计数器自动重装值
    TIM3->PSC = psc;          // 预分频器
    TIM3->DIER |= 1 << 0;    // 允许更新中断
    TIM3->CR1 |= 0x01;       // 使能定时器 3
    MY_NVIC_Init(1, 3, TIM3_IRQn, 2); // 抢占 1,子优先级 3,组 2
}

  定时器的自动重装值和时钟预分频数需要根据各自定时器配置,程序中仅供参考。

SDRAM 配置

  如果 MCU 内部 RAM 无法满足 GUI 刷屏数组的定义,则需要将 gt_port_disp.c 中的刷屏数组定义到 SRAM 或者 SDRAM 中,地址需要根据项目对 SDRAM 的配置来修改。

图7-50 SDRAM 配置示例

图7-50 SDRAM 配置示例

  使用 __attribute__ 关键字修饰该数组。

  其余配置,如果需要使用 GUI 的 log 打印功能,则需要用户自己实现 printf 函数,使其能在串口格式化打印内容。