STC32G+RA6809 HMI实战教程:从参数设置到上报主机,再到显示页面的完整流程
适用对象:在 STC32G + RA6809 平台上实现"用户触摸设置参数 → 点击确定上报主机 → 自动切换到参数显示页面"完整业务流程。
前置知识:了解 C 语言、Keil C251 编译器、基本的单片机开发概念。
配套硬件:STC32G12K128(或其他 STC32G 系列)、RA6809 液晶屏驱动芯片(LCD驱动控制芯片)、GT911 电容触摸屏、800×480 LCD 显示屏。
教程特色:从框架原理到每一行代码,手把手讲解,零基础也能跟着做出来。
目录
- 一、项目整体架构速览
- 二、UI框架核心概念:页面与响应区域
- 三、触摸事件处理流程详解
- 四、准备工作:图片资源与枚举登记
- 五、从零新建"参数设置页"
- 六、从零新建"参数显示页"
- 七、通信层:向主机上报参数
- 八、确定按钮:上报+页面切换的完整实现
- 九、在Keil中添加新页面并编译
- 十、调试与常见问题
一、项目整体架构速览
1.1 硬件架构
┌─────────────────────────────────────────────────┐
│ STC32G12K128 │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ 主循环 │ │ GT911驱动 │ │ UART1通信 │ │
│ │ main.c │ │ (触摸屏) │ │ (主机通信) │ │
│ └────┬─────┘ └────┬─────┘ └──────┬───────┘ │
│ │ │ │ │
│ │ ┌─────────┴───────────────┘ │
│ │ │ │
│ ┌────▼───────────────────────────────────────▼┐│
│ │ UI 框架层 (ui_page_*) ││
│ │ ┌─────────┐ ┌─────────┐ ┌─────────────┐ ││
│ │ │页面链表 │ │触摸分发 │ │ 响应区域表 │ ││
│ │ └─────────┘ └─────────┘ └─────────────┘ ││
│ └─────────────────────────────────────────────┘│
└───────────────────────┬─────────────────────────┘
│
▼ SPI/MCU接口
┌─────────────────────┐
│ RA6809 │
│ LCD驱动 + 显存管理 │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ 800×480 LCD屏幕 │
│ + GT911 触摸屏 │
└─────────────────────┘
数据流向总结:
- 触摸屏 → STC32G :
GT911触摸芯片通过 I2C 通信,产生中断通知 STC32G,STC32G 在主循环中读取触摸坐标(x, y)。 - STC32G → RA6809:STC32G 通过 MCU 接口向 RA6809 发送命令和数据,控制 LCD 显示内容(背景图、动态文字、PNG 数字等)。
- STC32G → 主机:通过 UART1 串口以约定的协议格式向外部主机发送参数数据。
- 主机 → STC32G:主机也可通过 UART1 下发参数,STC32G 接收后更新显示。
1.2 软件目录结构
STC32G_RA6809_HMI/
├── main.c # 主循环、触摸处理、喂狗
├── uart1.c / uart1.h # 串口通信(与主机)
├── RA6809/
│ ├── RA8889_51.c / .h # RA6809底层寄存器驱动
│ ├── RA8889_API_51.c / .h # RA6809高层API(绘图、解码等)
│ ├── RA8889_MCU_IF.c / .h # MCU与RA6809接口封装
│ ├── delay.c / delay.h # 延时函数
│ ├── MCU_Dev_Board.c / .h # 开发板外围初始化
│ ├── UserDef.h # 用户定义(颜色、地址等)
│ └── Show_UTF16.LIB # 字库(已编译好的库)
├── Touch_Demo/
│ ├── GT911.c / .h # GT911触摸芯片驱动
│ └── flash.h # 触摸校正数据
├── LCD_Menu_logic_architecture/ # ★ UI逻辑核心目录 ★
│ ├── ui_page_base.c / .h # 页面链表、切换、触摸分发
│ ├── ui_page_main.c / .h # 页面配置表、图片信息表、初始化入口
│ ├── lcdts_LCD_Pic.h # 图片ID枚举(与NOR Flash图资对应)
│ ├── ui_page_main.h # 页面定义结构
│ ├── ui_mempool.c / .h # 动态内存池(XMALLOC)
│ └── ★ 新页面 .c / .h ★ # 自己创建的参数设置页、显示页
├── ui_mempool.h # 内存池定义
└── Pic/ # 源图片文件(需烧录到NOR Flash)
核心原则 :所有 UI 页面代码放在
LCD_Menu_logic_architecture/目录下,统一通过ui_page_main.c中的配置表注册,由ui_page_base.c中的框架统一管理。
二、UI框架核心概念:页面与响应区域
2.1 什么是"页面"(ui_page_t)
本框架将每个 UI 屏幕定义为一个页面(Page),每个页面包含三要素:
┌─────────────────────────────────────────────────┐
│ ui_page_t │
├─────────────────────────────────────────────────┤
│ page_id 页面唯一ID(图片ID+参数ID)│
│ image_reference_id 背景图资源ID(枚举值) │
│ param_reference_id 参数ID(多实例页使用) │
│ page_type 页面类型(动画/单页/组页) │
├─────────────────────────────────────────────────┤
│ touch_select_judgment() ← 触摸坐标→区域ID │
│ page_display() ← 绘制整个页面 │
│ page_response_area[] ← 各响应区域配置 │
│ response_area_num ← 响应区域数量 │
└─────────────────────────────────────────────────┘
通俗理解:可以把页面想象成一张"答题卡",触摸就是用铅笔在某块区域涂答案。
touch_select_judgment()= 阅卷老师,根据触摸坐标判断"你涂了哪块"(返回区域 ID)。page_display()= 重新发一张新答题卡并填写好答案(刷新显示)。page_response_area[]= 登记每块区域的"用途说明"------这块是返回、那块是设置参数、那块是确定......
2.2 触摸一次发生了什么
用户手指按下屏幕
│
▼
GT911中断触发 ──► tp_flag = 1(主循环感知到有触摸)
│
▼
主循环检测到 tp_flag=1 ──► 调用 GT911_Scan() 读取 (x, y) 坐标
│
▼
ui_touch_select_process() 被调用
│
├── 第1步:调用当前页的 touch_select_judgment(x, y) ──► 得到 response_area_id
│
├── 第2步:在 page_response_area[] 中找到对应的区域配置
│
├── 第3步:如果有 config_function(),先执行它
│ (用途:做"点击变色"、保存参数、切换键盘编辑模式......)
│
└── 第4步:如果有 page_switch_function(),再执行它
(用途:跳转到另一个页面)
关键理解 :同一块触摸区域的 config_function 和 page_switch_function 可以组合使用:
- 先执行 config (写数据)→ 再执行 switch(跳转并刷新目标页)
- 这就实现了"点击确定 → 保存参数 → 跳到显示页"的完整流程。
2.3 页面配置表(ui_page_def_conf)
所有页面在程序启动时通过 ui_page_def_conf[] 配置表注册到框架:
c
// ui_page_main.c 中
static ui_page_def_conf_t ui_page_def_conf[] = {
// 格式:{ 图片ID, 页面类型, 是否启用, 保留, 保留, 触摸判断函数, 初始化函数, 响应区域数量 }
{IMAGE_ID_XXX, PAGE_TYPE_INTERACT_SINGLE, 1, 0, 0,
ui_tsj_XXX, ui_init_XXX, 5},
// 配置表结束标记(必须保留)
{0, 0, 0, 0, 0, NULL, NULL, 0}
};
2.4 图片资源(_Pic[] 与枚举)
图片存储在外部 NOR Flash 中,通过 lcdts_LCD_Pic.h 中的枚举与 _Pic[] 表对应:
c
// lcdts_LCD_Pic.h 中
typedef enum {
Image1 = 0,
Image2 = 1,
// ...
MySetPage, /* 参数设置页背景 */
MySetPage_backup, /* 参数设置页备份(按键按下态) */
MyDisplayPage, /* 参数显示页背景 */
MyDisplayPage_backup, /* 参数显示页备份 */
// ...
} PICTURE_NAME_Pic;
_Pic[] 表中记录每个图片的宽、高、大小(字节)、在 NOR Flash 中的起始地址:
c
// ui_page_main.c 中
static const INFO_Pic _Pic[] = {
// { 宽, 高, 大小, 起始地址 }
{ 800, 480, 123456, 0x00010000 }, /* MySetPage */
{ 800, 480, 123456, 0x00030000 }, /* MySetPage_backup */
{ 800, 480, 234567, 0x00050000 }, /* MyDisplayPage */
{ 800, 480, 234567, 0x00080000 }, /* MyDisplayPage_backup */
};
三、触摸事件处理流程详解
3.1 主循环中的触摸处理(main.c)
c
// main.c 主循环
while(1)
{
if (touch_debounce_counter > 0) touch_debounce_counter--; // 防抖递减
main_loop_touch_handler(); // 处理触摸
delay_ms(2);
}
static void main_loop_touch_handler(void)
{
if (tp_flag) // 有触摸中断
{
tp_flag = 0;
GT911_Scan(); // 读取触摸坐标 LCD_X, LCD_Y
if (touch_processed) {
// 上一次按下还没松开,等松开后再处理
if (TouchFlag == 0) touch_processed = 0;
else { LCD_X = 0; LCD_Y = 0; }
return;
}
// 防抖:counter 未归零时忽略本次触摸
if (touch_debounce_counter > 0) {
LCD_X = 0; LCD_Y = 0; return;
}
// 坐标无效或未按下:忽略
if ((LCD_X == 0 && LCD_Y == 0) || TouchFlag != 1) {
LCD_X = 0; LCD_Y = 0; return;
}
// 刚切换页面后,忽略第一次触摸(防止手指还在屏幕上被误判)
if (g_ui_just_switched_page) {
g_ui_just_switched_page = 0;
LCD_X = 0; LCD_Y = 0;
touch_processed = 1;
return;
}
// ★ 核心:调用UI框架处理触摸 ★
ui_touch_select_process();
touch_debounce_counter = 8; // 防抖:约80ms内忽略新触摸
touch_processed = 1;
}
}
3.2 ui_touch_select_process() 完整流程
c
int ui_touch_select_process(void)
{
ui_page_t *current_page;
lcd_coordinate_t coordinate;
// 1. 保存坐标(避免后续被覆盖)
coordinate.x = saved_x = LCD_X;
coordinate.y = saved_y = LCD_Y;
LCD_X = 0; LCD_Y = 0;
// 2. 查找当前页面
current_page = ui_page_lookup(g_current_page_id);
// 3. 调用触摸判断函数:坐标 → 区域ID
response_area_id = current_page->touch_select_judgment(&coordinate);
// 4. 未命中任何区域
if (response_area_id == AREA_ID_NONE_SELECT) {
return UNOS_OK;
}
// 5. 遍历响应区域表,找到匹配项
for (i = 0; i < current_page->response_area_num; i++) {
response_area = ¤t_page->page_response_area[i];
if (response_area->response_area_id == response_area_id) {
// 记录当前触摸的区域ID(键盘等需要知道"刚才按的是哪块")
g_ui_last_response_area_id = response_area_id;
// ★ 6. 先执行 config_function(如果有)★
// → 用途:按键变色、写参数、清除缓冲区......
if (response_area->config_function != NULL) {
response_area->config_function(response_area->config_param);
}
// ★ 7. 再执行 page_switch_function(如果有)★
// → 用途:跳转到其他页面(此时目标页的 page_display 会自动被调用)
if (response_area->page_switch_function != NULL) {
response_area->page_switch_function(response_area->page_id);
}
break;
}
}
return UNOS_OK;
}
3.3 响应区域结构体详解
c
typedef struct {
unos_id_t response_area_id; // ① 本区域的唯一ID(与 tsj 返回值对应)
int (* config_function)(void *param); // ② 点击时"额外"要做什么(可选)
void *config_param; // ③ 传给 config_function 的参数
int (* page_switch_function)(unos_id_2_t); // ④ 跳转目标页的函数(通常填 ui_page_switch_to)
unos_id_2_t page_id; // ⑤ 跳转到哪个页面(用 UI_PAGE_ID_BY_IMAGE_PARAM 生成)
} ui_response_area_t;
四种典型用法:
| 场景 | config_function | page_id | 效果 |
|---|---|---|---|
| 返回上一页 | 备份图矩形拷贝(变色) | 上一页 ID | 变色后跳转 |
| 点击确定(写参数+跳转) | 写参数到全局变量 + UART上报 | 显示页 ID | 先保存再跳转 |
| 同页刷新(如数字键盘) | 追加字符到缓冲区 | 本页ID | 只刷新,显示更新 |
| 切换键盘编辑模式 | 设置键盘编辑哪个参数 | 本页ID | 只刷新 |
四、准备工作:图片资源与枚举登记
4.1 准备两张背景图
使用 UI 设计工具(如 PhotoShop、UI编辑器)制作:
| 图片 | 尺寸 | 命名建议 | 说明 |
|---|---|---|---|
| 参数设置页背景 | 800×480 | SetParam_page.jpg |
包含:标题"参数设置"、输入框(电压/电流等)、确定按钮 |
| 参数显示页背景 | 800×480 | DisplayParam_page.jpg |
包含:标题"当前参数"、显示区域(只读,不含输入框)、返回按钮 |
设计要点:
- 确定按钮要足够大(建议 200×80 像素),方便触摸
- 按钮做成"正常态"和"按下态"两个版本(后者颜色略深/高亮),用于实现点击变色
- 输入框区域也建议有正常态/按下态两个版本
4.2 烧录图资到NOR Flash
使用厂商提供的烧录工具(如 RA6809 配套工具),将背景图烧录到 NOR Flash 起始地址(如 0x00100000),记录每个文件的:
- 起始地址(烧录工具会自动分配)
- 文件大小(字节)
- 宽 × 高
4.3 登记到项目中
第一步:在 lcdts_LCD_Pic.h 中增加枚举
c
// lcdts_LCD_Pic.h
typedef enum {
Main_Page = 0,
// ... 其他已有枚举 ...
SetParam_page, /* 参数设置页(新增) */
SetParam_page_backup, /* 参数设置页备份(新增) */
DisplayParam_page, /* 参数显示页(新增) */
DisplayParam_page_backup, /* 参数显示页备份(新增) */
} PICTURE_NAME_Pic;
第二步:在 ui_page_main.c 中补充 _Pic[] 表
c
// ui_page_main.c
static const INFO_Pic _Pic[] = {
// { 宽, 高, 大小, 起始地址 }
// ... 已有条目 ...
{800, 480, 198234, 0x00100000}, /* SetParam_page */
{800, 480, 198234, 0x00130000}, /* SetParam_page_backup */
{800, 480, 210456, 0x00160000}, /* DisplayParam_page */
{800, 480, 210456, 0x00190000}, /* DisplayParam_page_backup */
};
五、从零新建"参数设置页"
5.1 创建头文件:SetParam.h
在 LCD_Menu_logic_architecture/ 目录下新建 SetParam.h:
c
#ifndef __SET_PARAM_PAGE_H__
#define __SET_PARAM_PAGE_H__
#include "ui_page_base.h"
#include "lcdts_LCD_Pic.h"
/* 本页使用的图片ID */
#define IMAGE_ID_SET_PARAM_PAGE SetParam_page
/* ===== 触摸区域坐标(必须与UI图精确对应) ===== */
/* 返回上一页 */
#define SET_PARAM_BACK_X 0
#define SET_PARAM_BACK_Y 0
#define SET_PARAM_BACK_W 92
#define SET_PARAM_BACK_H 72
/* 电压输入框(点击进入键盘输入) */
#define SET_PARAM_VOLTAGE_X 100
#define SET_PARAM_VOLTAGE_Y 150
#define SET_PARAM_VOLTAGE_W 600
#define SET_PARAM_VOLTAGE_H 80
/* 电流输入框(点击进入键盘输入) */
#define SET_PARAM_CURRENT_X 100
#define SET_PARAM_CURRENT_Y 260
#define SET_PARAM_CURRENT_W 600
#define SET_PARAM_CURRENT_H 80
/* 确定按钮 */
#define SET_PARAM_CONFIRM_X 300
#define SET_PARAM_CONFIRM_Y 380
#define SET_PARAM_CONFIRM_W 200
#define SET_PARAM_CONFIRM_H 80
/* ===== 响应区域ID ===== */
#define SET_PARAM_AREA_ID_BACK 1
#define SET_PARAM_AREA_ID_VOLTAGE 2
#define SET_PARAM_AREA_ID_CURRENT 3
#define SET_PARAM_AREA_ID_CONFIRM 4
/* ===== 外部函数声明 ===== */
/* 设置/获取电压值(单位:0.1V,内部用 unsigned long 防溢出) */
void set_param_voltage_set(unsigned long val);
unsigned long set_param_voltage_get(void);
/* 设置/获取电流值(单位:0.001A,内部用 unsigned long 防溢出) */
void set_param_current_set(unsigned long val);
unsigned long set_param_current_get(void);
/* 页面入口函数 */
unos_id_t ui_tsj_SetParam(lcd_coordinate_t *pCoordinate);
int ui_init_SetParam(ui_page_t *ui_page);
#endif /* __SET_PARAM_PAGE_H__ */
5.2 创建源文件:SetParam.c
在 LCD_Menu_logic_architecture/ 目录下新建 SetParam.c:
c
#include "SetParam.h"
#include "ui_page_main.h"
#include "RA6809/RA8889_API_51.h"
#include "RA6809/UserDef.h"
/* ===== 全局参数变量 ===== */
static unsigned long g_voltage_01V = 2200; /* 默认220.0V */
static unsigned long g_current_001A = 5000; /* 默认5.000A */
/* ===== 参数读写接口(供其他模块调用) ===== */
void set_param_voltage_set(unsigned long val)
{
g_voltage_01V = val;
}
unsigned long set_param_voltage_get(void)
{
return g_voltage_01V;
}
void set_param_current_set(unsigned long val)
{
g_current_001A = val;
}
unsigned long set_param_current_get(void)
{
return g_current_001A;
}
/* ===== 辅助函数 ===== */
/* 判断坐标是否在矩形内 */
static int in_rect(unsigned int x, unsigned int y,
unsigned int rx, unsigned int ry,
unsigned int rw, unsigned int rh)
{
return (x >= rx && x < rx + rw && y >= ry && y < ry + rh);
}
/* 从备份图拷贝矩形到显示层(实现点击变色) */
static int backup_rect_to_display(void *param)
{
unsigned short rx, ry, rw, rh;
if (param == NULL) return -1;
rx = ((unsigned short *)param)[0];
ry = ((unsigned short *)param)[1];
rw = ((unsigned short *)param)[2];
rh = ((unsigned short *)param)[3];
/* 加载备份图(按下态)到图层2 */
Canvas_Image_Start_address(LAYER_ADDRESS(2));
IDEC_Destination_Start_Address(LAYER_ADDRESS(2));
ui_image_load(SetParam_page_backup);
/* 拷贝该区域到显示层0 */
BTE_Memory_Copy(LAYER_ADDRESS(2), canvas_image_width, rx, ry,
0, 0, 0, 0,
LAYER_ADDRESS(0), canvas_image_width, rx, ry,
12, rw, rh);
return UNOS_OK;
}
/* ===== 页面显示函数 ===== */
static int ui_display_SetParam_page(ui_page_t *ui_page)
{
unsigned short string_utf16[20];
unsigned short font_style = 3200;
unsigned long v, c;
char buf[20];
if (ui_page == NULL) return -1;
feed_wdt();
/* 1. 画背景到图层1,再拷贝到显示层0 */
Canvas_Image_Start_address(LAYER_ADDRESS(1));
IDEC_Destination_Start_Address(LAYER_ADDRESS(1));
ui_image_load(SetParam_page);
feed_wdt();
BTE_Memory_Copy(LAYER_ADDRESS(1), canvas_image_width,
0, 0, 0, 0, 0, 0,
LAYER_ADDRESS(0), canvas_image_width,
0, 0, 12, LCD_Width, LCD_Height);
/* 2. 初始化SPI Flash(用于字库访问) */
SPI_NOR_initial_DMA(0, 1, 1, 1, 0);
Set_Font_Style(1, 1, 1);
Foreground_color_16M(color16M_white);
Background_color_16M(color16M_blue);
/* 3. 显示当前电压值(示例:格式 220.0V) */
v = set_param_voltage_get();
sprintf(buf, "%lu.%luV", v / 10, v % 10);
/* 在输入框上方区域显示当前值(作为预览) */
UFT8toUTF16(string_utf16, (unsigned char *)buf);
Show_UTF16(FONT_LIB_ADDR, FONT_BUFFER_ADDR,
SET_PARAM_VOLTAGE_X + 10, SET_PARAM_VOLTAGE_Y + 20,
font_style, 0, 0, string_utf16);
/* 4. 显示当前电流值(示例:格式 5.000A) */
c = set_param_current_get();
sprintf(buf, "%lu.%03luA", c / 1000, c % 1000);
UFT8toUTF16(string_utf16, (unsigned char *)buf);
Show_UTF16(FONT_LIB_ADDR, FONT_BUFFER_ADDR,
SET_PARAM_CURRENT_X + 10, SET_PARAM_CURRENT_Y + 20,
font_style, 0, 0, string_utf16);
Canvas_Image_Start_address(LAYER_ADDRESS(0));
Main_Image_Start_Address(LAYER_ADDRESS(0));
return UNOS_OK;
}
/* ===== 触摸判断函数:坐标 → 区域ID ===== */
unos_id_t ui_tsj_SetParam(lcd_coordinate_t *pCoordinate)
{
unsigned int x, y;
if (pCoordinate == NULL) return AREA_ID_NONE_SELECT;
x = pCoordinate->x;
y = pCoordinate->y;
/* 判断顺序:从大区域到小区域,避免重叠误判 */
if (in_rect(x, y, SET_PARAM_BACK_X, SET_PARAM_BACK_Y,
SET_PARAM_BACK_W, SET_PARAM_BACK_H))
return SET_PARAM_AREA_ID_BACK;
if (in_rect(x, y, SET_PARAM_VOLTAGE_X, SET_PARAM_VOLTAGE_Y,
SET_PARAM_VOLTAGE_W, SET_PARAM_VOLTAGE_H))
return SET_PARAM_AREA_ID_VOLTAGE;
if (in_rect(x, y, SET_PARAM_CURRENT_X, SET_PARAM_CURRENT_Y,
SET_PARAM_CURRENT_W, SET_PARAM_CURRENT_H))
return SET_PARAM_AREA_ID_CURRENT;
if (in_rect(x, y, SET_PARAM_CONFIRM_X, SET_PARAM_CONFIRM_Y,
SET_PARAM_CONFIRM_W, SET_PARAM_CONFIRM_H))
return SET_PARAM_AREA_ID_CONFIRM;
return AREA_ID_NONE_SELECT;
}
/* ===== 页面初始化函数 ===== */
int ui_init_SetParam(ui_page_t *ui_page)
{
ui_response_area_t *r;
unsigned int i;
unos_id_2_t self_id;
/* 静态数组:存储各区域矩形参数(传递给 backup_rect_to_display) */
static const unsigned short rect_back[4] = {SET_PARAM_BACK_X, SET_PARAM_BACK_Y, SET_PARAM_BACK_W, SET_PARAM_BACK_H};
static const unsigned short rect_voltage[4] = {SET_PARAM_VOLTAGE_X, SET_PARAM_VOLTAGE_Y, SET_PARAM_VOLTAGE_W, SET_PARAM_VOLTAGE_H};
static const unsigned short rect_current[4] = {SET_PARAM_CURRENT_X, SET_PARAM_CURRENT_Y, SET_PARAM_CURRENT_W, SET_PARAM_CURRENT_H};
static const unsigned short rect_confirm[4] = {SET_PARAM_CONFIRM_X, SET_PARAM_CONFIRM_Y, SET_PARAM_CONFIRM_W, SET_PARAM_CONFIRM_H};
if (ui_page == NULL) return -1;
feed_wdt();
ui_page->page_display = ui_display_SetParam_page;
self_id = UI_PAGE_ID_BY_IMAGE_PARAM(IMAGE_ID_SET_PARAM_PAGE, 0);
/* 遍历所有响应区域,填写配置 */
for (i = 0; i < ui_page->response_area_num; i++) {
r = &ui_page->page_response_area[i];
r->response_area_id = (unos_id_t)(i + 1); /* 区域ID从1开始 */
r->page_switch_function = ui_page_switch_to;
switch (r->response_area_id) {
case SET_PARAM_AREA_ID_BACK:
r->config_function = backup_rect_to_display;
r->config_param = (void *)rect_back;
r->page_id = /* 上一页ID,此处设为自身(示例) */ self_id;
break;
case SET_PARAM_AREA_ID_VOLTAGE:
r->config_function = NULL; /* 也可设为切换到键盘编辑模式 */
r->config_param = NULL;
r->page_id = self_id; /* 点击输入框可跳转键盘页,此处示例为本页 */
break;
case SET_PARAM_AREA_ID_CURRENT:
r->config_function = NULL;
r->config_param = NULL;
r->page_id = self_id;
break;
case SET_PARAM_AREA_ID_CONFIRM:
/* ★ 确定按钮:config=备份变色,switch=跳转显示页 ★ */
r->config_function = backup_rect_to_display;
r->config_param = (void *)rect_confirm;
r->page_id = UI_PAGE_ID_BY_IMAGE_PARAM(IMAGE_ID_DISPLAY_PARAM_PAGE, 0);
break;
default:
r->config_function = NULL;
r->config_param = NULL;
r->page_id = self_id;
break;
}
}
return UNOS_OK;
}
六、从零新建"参数显示页"
6.1 创建头文件:DisplayParam.h
c
#ifndef __DISPLAY_PARAM_PAGE_H__
#define __DISPLAY_PARAM_PAGE_H__
#include "ui_page_base.h"
#include "lcdts_LCD_Pic.h"
#define IMAGE_ID_DISPLAY_PARAM_PAGE DisplayParam_page
/* 触摸区域 */
#define DISP_BACK_X 0
#define DISP_BACK_Y 0
#define DISP_BACK_W 92
#define DISP_BACK_H 72
/* 显示区域(只读,用于显示当前参数) */
#define DISP_SHOW_VOLTAGE_X 100
#define DISP_SHOW_VOLTAGE_Y 150
#define DISP_SHOW_VOLTAGE_W 600
#define DISP_SHOW_VOLTAGE_H 80
#define DISP_SHOW_CURRENT_X 100
#define DISP_SHOW_CURRENT_Y 260
#define DISP_SHOW_CURRENT_W 600
#define DISP_SHOW_CURRENT_H 80
/* 响应区域ID */
#define DISP_AREA_ID_BACK 1
#define DISP_AREA_ID_SHOW_VOLTAGE 2
#define DISP_AREA_ID_SHOW_CURRENT 3
/* 页面函数 */
unos_id_t ui_tsj_DisplayParam(lcd_coordinate_t *pCoordinate);
int ui_init_DisplayParam(ui_page_t *ui_page);
#endif
6.2 创建源文件:DisplayParam.c
c
#include "DisplayParam.h"
#include "SetParam.h" /* 引入设置页的参数读取接口 */
#include "ui_page_main.h"
#include "RA6809/RA8889_API_51.h"
#include "RA6809/UserDef.h"
/* 从设置页读取参数(通过 SetParam.h 中暴露的 get 函数) */
extern unsigned long set_param_voltage_get(void);
extern unsigned long set_param_current_get(void);
/* ===== 辅助函数 ===== */
static int in_rect(unsigned int x, unsigned int y,
unsigned int rx, unsigned int ry,
unsigned int rw, unsigned int rh)
{
return (x >= rx && x < rx + rw && y >= ry && y < ry + rh);
}
static int backup_rect_to_display(void *param)
{
unsigned short rx, ry, rw, rh;
if (param == NULL) return -1;
rx = ((unsigned short *)param)[0];
ry = ((unsigned short *)param)[1];
rw = ((unsigned short *)param)[2];
rh = ((unsigned short *)param)[3];
Canvas_Image_Start_address(LAYER_ADDRESS(2));
IDEC_Destination_Start_Address(LAYER_ADDRESS(2));
ui_image_load(DisplayParam_page_backup);
BTE_Memory_Copy(LAYER_ADDRESS(2), canvas_image_width, rx, ry,
0, 0, 0, 0,
LAYER_ADDRESS(0), canvas_image_width, rx, ry,
12, rw, rh);
return UNOS_OK;
}
/* ===== 页面显示函数 ===== */
static int ui_display_DisplayParam_page(ui_page_t *ui_page)
{
unsigned short string_utf16[20];
unsigned short font_style = 3200;
unsigned long v, c;
char buf[24];
if (ui_page == NULL) return -1;
feed_wdt();
/* 1. 画背景 */
Canvas_Image_Start_address(LAYER_ADDRESS(1));
IDEC_Destination_Start_Address(LAYER_ADDRESS(1));
ui_image_load(DisplayParam_page);
feed_wdt();
BTE_Memory_Copy(LAYER_ADDRESS(1), canvas_image_width,
0, 0, 0, 0, 0, 0,
LAYER_ADDRESS(0), canvas_image_width,
0, 0, 12, LCD_Width, LCD_Height);
/* 2. 初始化字库 */
SPI_NOR_initial_DMA(0, 1, 1, 1, 0);
Set_Font_Style(1, 1, 1);
Foreground_color_16M(color16M_green);
Background_color_16M(color16M_black);
/* ★ 3. 显示电压(从设置页读取) ★ */
v = set_param_voltage_get();
sprintf(buf, "电压: %lu.%lu V", v / 10, v % 10);
UFT8toUTF16(string_utf16, (unsigned char *)buf);
Show_UTF16(FONT_LIB_ADDR, FONT_BUFFER_ADDR,
DISP_SHOW_VOLTAGE_X + 10, DISP_SHOW_VOLTAGE_Y + 20,
font_style, 0, 0, string_utf16);
/* ★ 4. 显示电流(从设置页读取) ★ */
c = set_param_current_get();
sprintf(buf, "电流: %lu.%03lu A", c / 1000, c % 1000);
UFT8toUTF16(string_utf16, (unsigned char *)buf);
Show_UTF16(FONT_LIB_ADDR, FONT_BUFFER_ADDR,
DISP_SHOW_CURRENT_X + 10, DISP_SHOW_CURRENT_Y + 20,
font_style, 0, 0, string_utf16);
Canvas_Image_Start_address(LAYER_ADDRESS(0));
Main_Image_Start_Address(LAYER_ADDRESS(0));
return UNOS_OK;
}
/* ===== 触摸判断 ===== */
unos_id_t ui_tsj_DisplayParam(lcd_coordinate_t *pCoordinate)
{
unsigned int x, y;
if (pCoordinate == NULL) return AREA_ID_NONE_SELECT;
x = pCoordinate->x;
y = pCoordinate->y;
if (in_rect(x, y, DISP_BACK_X, DISP_BACK_Y, DISP_BACK_W, DISP_BACK_H))
return DISP_AREA_ID_BACK;
if (in_rect(x, y, DISP_SHOW_VOLTAGE_X, DISP_SHOW_VOLTAGE_Y,
DISP_SHOW_VOLTAGE_W, DISP_SHOW_VOLTAGE_H))
return DISP_AREA_ID_SHOW_VOLTAGE;
if (in_rect(x, y, DISP_SHOW_CURRENT_X, DISP_SHOW_CURRENT_Y,
DISP_SHOW_CURRENT_W, DISP_SHOW_CURRENT_H))
return DISP_AREA_ID_SHOW_CURRENT;
return AREA_ID_NONE_SELECT;
}
/* ===== 页面初始化 ===== */
int ui_init_DisplayParam(ui_page_t *ui_page)
{
ui_response_area_t *r;
unsigned int i;
static const unsigned short rect_back[4] = {
DISP_BACK_X, DISP_BACK_Y, DISP_BACK_W, DISP_BACK_H
};
if (ui_page == NULL) return -1;
feed_wdt();
ui_page->page_display = ui_display_DisplayParam_page;
for (i = 0; i < ui_page->response_area_num; i++) {
r = &ui_page->page_response_area[i];
r->response_area_id = (unos_id_t)(i + 1);
r->page_switch_function = ui_page_switch_to;
if (r->response_area_id == DISP_AREA_ID_BACK) {
r->config_function = backup_rect_to_display;
r->config_param = (void *)rect_back;
/* 返回到设置页 */
r->page_id = UI_PAGE_ID_BY_IMAGE_PARAM(IMAGE_ID_SET_PARAM_PAGE, 0);
} else {
r->config_function = NULL;
r->config_param = NULL;
r->page_id = UI_PAGE_ID_BY_IMAGE_PARAM(IMAGE_ID_DISPLAY_PARAM_PAGE, 0);
}
}
return UNOS_OK;
}
七、通信层:向主机上报参数
7.1 串口通信基础
STC32G 与外部主机通过 UART1 通信(115200 波特率,8N1)。定义统一的通信协议:
┌─────────┬─────────┬──────────────┬──────────────┬────────┬─────────┐
│ 帧头 │ 命令 │ 电压(4B) │ 电流(4B) │ 校验 │ 帧尾 │
│ 0xAA │ 0x01 │ unsigned long │ unsigned long │ XOR │ 0x55 │
└─────────┴─────────┴──────────────┴──────────────┴───────┴─────────┘
- 帧头:0xAA(固定)
- 命令:0x01 = 设置参数请求
- 电压:4字节 unsigned long,单位 0.1V(如 2200 = 220.0V)
- 电流:4字节 unsigned long,单位 0.001A(如 5000 = 5.000A)
- 校验:前面所有字节的 XOR
- 帧尾:0x55(固定)
7.2 通信模块实现
新建 host_comm.c 和 host_comm.h:
host_comm.h:
c
#ifndef __HOST_COMM_H__
#define __HOST_COMM_H__
/* 上报参数给主机 */
void host_param_upload(unsigned long voltage_01V, unsigned long current_001A);
/* 从主机接收参数(由uart1.c的接收中断调用) */
void host_on_param_from_host(unsigned long voltage_01V, unsigned long current_001A);
#endif
host_comm.c:
c
#include "host_comm.h"
#include "uart1.h"
#include "SetParam.h" /* 设置页参数写接口 */
/* UART1发送一个字节(需在 uart1.c 中实现) */
extern void UART1_SendByte(unsigned char dat);
/* 计算异或校验和 */
static unsigned char calc_xor(const unsigned char *data, unsigned char len)
{
unsigned char xor_val = 0;
unsigned char i;
for (i = 0; i < len; i++) xor_val ^= data[i];
return xor_val;
}
/*
* host_param_upload - 上报参数给主机
* voltage_01V : 电压值,单位0.1V (如 2200 = 220.0V)
* current_001A: 电流值,单位0.001A (如 5000 = 5.000A)
*/
void host_param_upload(unsigned long voltage_01V, unsigned long current_001A)
{
unsigned char frame[12];
unsigned char xor_sum;
frame[0] = 0xAA; /* 帧头 */
frame[1] = 0x01; /* 命令:设置参数 */
/* 电压:拆分为4字节(低位在前) */
frame[2] = (unsigned char)( voltage_01V & 0xFF);
frame[3] = (unsigned char)((voltage_01V >> 8) & 0xFF);
frame[4] = (unsigned char)((voltage_01V >> 16) & 0xFF);
frame[5] = (unsigned char)((voltage_01V >> 24) & 0xFF);
/* 电流:拆分为4字节(低位在前) */
frame[6] = (unsigned char)( current_001A & 0xFF);
frame[7] = (unsigned char)((current_001A >> 8) & 0xFF);
frame[8] = (unsigned char)((current_001A >> 16) & 0xFF);
frame[9] = (unsigned char)((current_001A >> 24) & 0xFF);
/* 校验和:字节0~9的异或 */
xor_sum = calc_xor(frame, 10);
frame[10] = xor_sum;
frame[11] = 0x55; /* 帧尾 */
/* 通过UART1逐字节发送 */
{
unsigned char i;
for (i = 0; i < 12; i++) {
UART1_SendByte(frame[i]);
}
}
}
/*
* host_on_param_from_host - 从主机接收参数
* 由uart1.c的接收完成中断调用
*/
void host_on_param_from_host(unsigned long voltage_01V,
unsigned long current_001A)
{
/* 写入设置页(让显示页下次刷新时能读取到最新值) */
set_param_voltage_set(voltage_01V);
set_param_current_set(current_001A);
}
7.3 串口接收中断(uart1.c)
c
/*
* UART1接收中断处理
* 简单的帧解析:寻找帧头0xAA,找到后读取11字节,验证校验和与帧尾
*/
void UART1_ISR(void) interrupt 20
{
unsigned char rb;
if (RI) { /* 接收完成中断 */
RI = 0;
rb = SBUF;
/* 帧解析状态机 */
static unsigned char state = 0;
static unsigned char frame[12];
static unsigned char frame_idx = 0;
switch (state) {
case 0: /* 等待帧头 */
if (rb == 0xAA) {
frame[0] = rb;
frame_idx = 1;
state = 1;
}
break;
case 1: /* 接收数据区 */
frame[frame_idx++] = rb;
if (frame_idx >= 12) {
/* 帧接收完毕,验证 */
if (frame[11] == 0x55) {
unsigned char xor_val = calc_xor(frame, 10);
if (xor_val == frame[10]) {
unsigned long v, c;
v = *(unsigned long *)&frame[2];
c = *(unsigned long *)&frame[6];
host_on_param_from_host(v, c);
}
}
state = 0;
frame_idx = 0;
}
break;
}
}
}
八、确定按钮:上报+页面切换的完整实现
8.1 关键:在config_function中完成"保存+上报+变色"
在 SetParam.c 中,增加一个新的 config_function,专门处理"确定"按钮:
c
/* ===== 确定按钮的处理函数 ===== */
static int set_param_confirm_action(void *param)
{
unsigned long v, c;
unsigned short rx, ry, rw, rh;
(void)param;
/* 1. 变色:从备份图拷贝确定按钮区域到显示层 */
rx = SET_PARAM_CONFIRM_X;
ry = SET_PARAM_CONFIRM_Y;
rw = SET_PARAM_CONFIRM_W;
rh = SET_PARAM_CONFIRM_H;
Canvas_Image_Start_address(LAYER_ADDRESS(2));
IDEC_Destination_Start_Address(LAYER_ADDRESS(2));
ui_image_load(SetParam_page_backup);
BTE_Memory_Copy(LAYER_ADDRESS(2), canvas_image_width, rx, ry,
0, 0, 0, 0,
LAYER_ADDRESS(0), canvas_image_width, rx, ry,
12, rw, rh);
/* ★ 2. 获取当前参数值 ★ */
v = set_param_voltage_get();
c = set_param_current_get();
/* ★ 3. 通过UART1上报给主机 ★ */
host_param_upload(v, c);
/* ★ 4. page_switch_function 会紧接着执行,
跳转到显示页(ui_display_DisplayParam_page 被自动调用) ★ */
return UNOS_OK;
}
然后在 ui_init_SetParam 的初始化中,将确定按钮的 config_function 指向它:
c
case SET_PARAM_AREA_ID_CONFIRM:
/* 确定按钮:变色 + 上报主机 + 跳转到显示页 */
r->config_function = set_param_confirm_action; /* ← 改为自定义函数 */
r->config_param = NULL;
r->page_id = UI_PAGE_ID_BY_IMAGE_PARAM(IMAGE_ID_DISPLAY_PARAM_PAGE, 0);
break;
8.2 完整数据流图解
┌──────────────────────────────────────────────────────────────────────┐
│ 用户触摸"确定"按钮 │
└───────────────────────────────┬──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ ui_tsj_SetParam(坐标) 返回 SET_PARAM_AREA_ID_CONFIRM │
└───────────────────────────────┬──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ ui_init_SetParam 中查表: │
│ config_function = set_param_confirm_action │
│ page_switch_function = ui_page_switch_to │
│ page_id = IMAGE_ID_DISPLAY_PARAM_PAGE │
└───────────────────────────────┬──────────────────────────────────────┘
│
┌───────────────────┴──────────────────┐
│ │
▼ ▼
┌───────────────────────────┐ ┌──────────────────────────────────┐
│ set_param_confirm_action()│ │ ui_page_switch_to(DISPLAY_PAGE) │
│ │ │ │
│ ① 备份图→显示层(变色) │ │ ① g_current_page_id = DISPLAY │
│ │ │ ② ui_display_DisplayParam_page() │
│ ② v = voltage_get() │ │ │
│ c = current_get() │ │ ├─ 读取 set_param_voltage_get() │
│ │ │ ├─ 读取 set_param_current_get() │
│ ③ host_param_upload │ │ └─ 绘制电压/电流值到屏幕 │
│ (v, c) │ │ │
└───────────────────────────┘ └──────────────────────────────────┘
│
▼
┌───────────────────────────────────┐
│ 主机UART接收 │
│ 0xAA 0x01 v(4B) c(4B) XOR 0x55 │
└───────────────────────────────────┘
九、在Keil中添加新页面并编译
9.1 将新文件加入Keil项目
- 在 Keil uVision 中,右键点击
LCD_Menu_logic_architecture组 → Add Existing Files... - 添加
SetParam.c和DisplayParam.c - 添加
host_comm.c(如果之前没有的话)
9.2 在ui_page_main.c中注册页面
c
/* page_worked[]:启用新页面 */
static char page_worked[] = {
1, /* Key_CN_YingDa:键盘页 */
1, /* SetParam:参数设置页(新增) */
1, /* DisplayParam:参数显示页(新增) */
};
/* ui_page_def_conf[]:配置表 */
static ui_page_def_conf_t ui_page_def_conf[] = {
// 键盘页(已有)
{IMAGE_ID_KEY_CN_YINGDA, PAGE_TYPE_INTERACT_SINGLE, 1, 0, 0,
ui_tsj_Key_CN_YingDa, ui_init_Key_CN_YingDa, 42},
// ★ 新增:参数设置页 ★
{IMAGE_ID_SET_PARAM_PAGE, PAGE_TYPE_INTERACT_SINGLE, 1, 0, 0,
ui_tsj_SetParam, ui_init_SetParam, 4},
// ★ 新增:参数显示页 ★
{IMAGE_ID_DISPLAY_PARAM_PAGE, PAGE_TYPE_INTERACT_SINGLE, 1, 0, 0,
ui_tsj_DisplayParam, ui_init_DisplayParam, 3},
/* 结束标记 */
{0, 0, 0, 0, 0, NULL, NULL, 0}
};
9.3 在main.c中引用新页面(防止链接器剔除)
c
/* main.c */
#include "LCD_Menu_logic_architecture/SetParam.h"
#include "LCD_Menu_logic_architecture/DisplayParam.h"
/* 强制链接,防止Keil L51将未调用的init/tsj函数剔除 */
static void *_set_param_init_ref = (void *)ui_init_SetParam;
static void *_set_param_tsj_ref = (void *)ui_tsj_SetParam;
static void *_disp_param_init_ref = (void *)ui_init_DisplayParam;
static void *_disp_param_tsj_ref = (void *)ui_tsj_DisplayParam;
9.4 设置开机进入设置页(调试用)
c
void main(void)
{
// ... 系统初始化 ...
/* 调试:开机进入参数设置页 */
{
unos_id_2_t page_id = UI_PAGE_ID_BY_IMAGE_PARAM(IMAGE_ID_SET_PARAM_PAGE, 0);
ui_page_switch_to(page_id);
}
while (1) { /* 主循环 */ }
}
9.5 编译与烧录
- Rebuild All(清除旧编译文件)
- 若有 W25N01 链接错误,参考前面的说明注释掉
RA8889_API_51.c中的三行 W25N01 调用 - 烧录 HEX 文件到 STC32G
十、调试与常见问题
10.1 触摸坐标偏差
症状:按在按钮上但没反应,或按A键触发了B键。
排查步骤:
-
在
ui_tsj_*函数开头加printf()打印触摸坐标:cprintf("touch: x=%u, y=%u\n", pCoordinate->x, pCoordinate->y); -
观察串口输出,对照 UI 图确认坐标是否匹配。
-
检查
in_rect()的边界条件:x >= rx && x < rx + rw(注意用<而不是<=)。 -
检查是否有多处
in_rect重叠------先写的条件会优先命中。
修正方法 :微调头文件中的 *_X、*_Y 值(每次 ±5~10 像素)。
10.2 点击后页面没切换
排查步骤:
- 确认
ui_init_*中该区域的page_switch_function不为 NULL。 - 确认
page_id是有效的页面 ID(用UI_PAGE_ID_BY_IMAGE_PARAM生成)。 - 确认目标页已注册到
ui_page_def_conf[]且page_worked[]对应值为1。
10.3 主机收不到数据
排查步骤:
- 用 USB 转 TTL 模块连接 STC32G 的 UART1(TX=P3.1, RX=P3.0)到电脑串口。
- 打开串口助手,在 STC32G 按下确定按钮后观察是否有
AA 01 ... 55帧发出。 - 检查波特率设置是否匹配(程序中
UART1_Init(115200))。 - 检查校验和计算是否正确。
10.4 显示页读不到设置值
排查步骤:
SetParam.c中的g_voltage_01V/g_current_001A必须是static(模块内全局),不能是局部变量。DisplayParam.c通过extern引用这些变量时,拼写必须完全一致。- 确认
host_on_param_from_host()中调用的是set_param_voltage_set()而不是直接操作内部变量。
10.5 点击变色没效果
排查步骤:
- 确认
backup_rect_to_display()中加载的是_backup后缀的图片。 - 确认
LAYER_ADDRESS(2)对应的显存区域已正确初始化。 - 拷贝前是否已设置
Canvas_Image_Start_address(LAYER_ADDRESS(2))和IDEC_Destination_Start_Address(LAYER_ADDRESS(2))。
10.6 Keil L51 链接器警告"UNCALLED FUNCTION"
症状:init 或 tsj 函数被链接器当作未使用而剔除,导致页面无法工作。
解决方法 :在 main.c 中强制引用(不调用,只是取地址):
c
static void *_ref_init = (void *)ui_init_SetParam;
static void *_ref_tsj = (void *)ui_tsj_SetParam;
10.7 主机下发参数后显示页未刷新
说明 :主机通过 UART1 下发参数后,参数已写入全局变量,但显示页只有在切换到它时 才会调用 page_display() 刷新。
解决方案:
- 方案A:在显示页加定时器,每秒自动刷新(参考 main.c 中 1s 刷新的模式)
- 方案B:主机下发后立即调用
ui_page_switch_to(DISPLAY_PAGE_ID)强制刷新 - 方案C:在主循环中添加定期检查机制,发现参数变化则刷新显示页
结语
本教程从框架原理到具体代码,完整覆盖了"用户在触摸屏设置参数 → 点击确定 → 上报主机 → 切换到显示页"的整个业务流程。核心要点归纳如下:
| 步骤 | 关键点 |
|---|---|
| 1. 页面结构 | 每个页面 = tsj() + display() + init() + 响应区域表 |
| 2. 触摸分发 | tsj() 返回区域ID → 查表 → config() → switch() |
| 3. 保存+变色 | 在 config_function 中从备份图拷贝矩形实现变色 |
| 4. 上报主机 | 在 config_function 中调用 host_param_upload() |
| 5. 页面切换 | page_switch_function() → ui_page_switch_to(目标页ID) |
| 6. 显示读取 | 显示页通过 extern 调用设置页的 get() 函数读取最新值 |
掌握了这套框架后,可以在此基础上扩展出任意复杂的 UI 页面,包括多级菜单、弹窗确认、数据曲线显示、设置向导等。所有页面共享同一套触摸处理机制,代码复用度高,维护成本低。
如需更细的操作步骤说明或工程代码(可与瑞佑科技分公司深圳市瑞福科技联系)。
RA6809 的 HMI(人机交互) 开发:菜单逻辑架构设计