# DTU-HMI PC 端 HMI 菜单逻辑模拟程序,纯 C 实现,用于在电脑上模拟现场 DTU 设备的人机界面(HMI)行为。 - **菜单树**:按真实装置菜单还原,支持多级嵌套、首尾成环遍历 - **LCD 显示**:模拟 128×64 单色 LCD(点阵缓冲区 + 绘制函数) - **键盘输入**:用 PC 键盘按键映射嵌入式按键(上/下/左/右/确认/退出) - **TCP 远程显示**:实现 RemoDispBus 协议,可把 LCD 显示通过网络推送到上位机 - **UTF‑8 汉字库**:内置 12×12 点阵汉字库,可扩展 --- ## 1. 快速开始(构建 & 运行) ### 1.1 构建可执行程序 ```powershell mkdir build cd build cmake .. cmake --build . ``` 生成可执行文件: - Windows:`build/DTU-HMI.exe` - Linux/macOS:`build/DTU-HMI` ### 1.2 直接在本机运行 在 `build` 目录中执行: ```powershell ./DTU-HMI.exe # Windows PowerShell ./DTU-HMI # Linux / macOS ``` 终端中会出现类似“LCD 文本界面 + 菜单内容”的输出,使用键盘控制菜单。 ## 3. 目录结构 ``` DTU-HMI/ ├── CMakeLists.txt ├── gen_utf8_hz12.py # 12×12 UTF-8 汉字库生成脚本 ├── remo_disp_server.py # 远程显示 Python 服务端 ├── include/ │ └── types.h ├── src/ │ ├── main.c │ ├── thread_utils.c/h │ ├── remoteDisplay.c/h │ ├── TCP/tcp.c, tcp.h │ └── Drv/ │ ├── menu.c/h │ ├── display.c/h │ ├── lcd.c/h │ ├── Ascii.c/h │ └── utf8_hz12_data.c/h # 由脚本生成 └── build/ ``` --- ## 4. 环境要求 - **CMake** 3.10+ - **编译器**:Windows 默认 MSVC;Linux 需 GCC/Clang - **编码**:源文件 UTF-8,CMake 已配置 MSVC `/utf-8` --- ## 5. 构建步骤 ### 5.1 Windows ```powershell mkdir build cd build cmake .. cmake --build . ``` ### 5.2 Linux / macOS ```bash mkdir build cd build cmake .. cmake --build . ``` --- ## 6. TCP 远程显示(RemoDispBus) 程序内部通过 `remoteDisplay.c` + `tcp.c` 实现 **RemoDispBus** 协议通讯,可把 LCD 显示内容实时推送给远程上位机。 - 默认监听 **端口:7003** - 协议命令: - `CMD_INIT`:初始化/握手 - `CMD_LCDMEM`:下发/上传 LCD 显存缓冲区 - `CMD_KEY`:上位机模拟按键下发给设备 - `CMD_KEEPLIVE`:心跳保持连接 ### 6.1 使用 Python 远程显示服务端 在 PC 上运行模拟程序的同时,可以用仓库中的 `remo_disp_server.py` 来查看 LCD 的显示图像: ```bash python remo_disp_server.py # 需要 Python 3 + 标准库,具体依赖参考脚本头部说明 ``` 脚本会作为 **“远程显示上位机”**: - 主动连接 DTU-HMI 程序监听的端口(默认 7003) - 接收并解析 `CMD_LCDMEM`,在一个窗口中实时绘制 LCD 内容 - 也可以把 GUI 上的按键事件转换为 `CMD_KEY` 下发给 DTU-HMI 程序 --- ## 7. 菜单系统 ### 7.1 概述 菜单由**静态表** `g_tMenuModelTab` 定义,经 `Menu_Main_Creat_01` 构建为**可遍历树** `g_tMenuItem[]`。 运行时由 `menu.c` 负责: - 把静态表解析成**树 + 循环双向链表** - 根据按键事件更新当前指针 `g_tMenuCtrl.ptCurrent` - 调用一系列“坐标计算函数”给每一级菜单计算矩形框位置 - 最终调用 `display.c` / `lcd.c` 完成绘制 ### 7.2 数据结构 #### 7.2.1 静态菜单定义(`tagMenuModel`) ```c typedef struct { uint8_t byClass; // 菜单分级标志 0/1/2/3 uint8_t byName[50]; // 菜单字符串 uint8_t byTip[50]; // 菜单提示文本 uint8_t byAttrib; // 菜单属性 uint16_t wPassword; // 访问密码,0x0000 表示无密码 uint16_t wPara; // 菜单执行函数参数 FUNCPTR pfnWinProc; // 界面执行函数指针 } tagMenuModel, *tagPMenuModel; /* 注意:表定义时顺序不能乱,需从 0 级开始,一级一级按顺序写入 */ const tagMenuModel g_tMenuModelTab[] = { ... }; ``` #### 7.2.2 菜单遍历树(`tagMenuItem`) ```c /* 每个菜单包含:一、上下前后等级关系;二、属性与内容;三、显示坐标 */ typedef struct _MENU_ITEM_ { struct _MENU_ITEM_ *ptHigher; // 上级菜单指针 struct _MENU_ITEM_ *ptLower; // 下级菜单指针 struct _MENU_ITEM_ *ptBefore; // 同级上方指针 struct _MENU_ITEM_ *ptBehind; // 同级下方指针 uint8_t byClass; uint8_t byName[50]; uint8_t byTip[50]; uint8_t byAttrib; uint16_t wPassword; uint16_t wPara; FUNCPTR pfnWinProc; uint16_t wPos, wNum; uint16_t wSPosX, wSPosY, wEPosX, wEPosY; } tagMenuItem, *tagPMenuItem; tagMenuItem g_tMenuItem[300]; /* 所有菜单存储于此数组 */ ``` ### 7.3 菜单构建示例 #### 7.3.1 静态表(`g_tMenuModelTab`) ```c const tagMenuModel g_tMenuModelTab[] = { { 0, " ", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, { 1, "装置信息", "查看装置信息", 0, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_AppInfo }, { 1, "实时数据", "装置实时数据", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, { 2, "交流量", "查看遥测交流量", EN_MEA_AC, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_YC }, { 2, "直流量", "查看遥测直流量", EN_MEA_DC, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_YC }, { 2, "遥信量", "查看遥信开入量", EN_INPUT_RLY_ALL, 0x0000, EN_INPUT_0, (FUNCPTR)MenuProc_See_Input }, { 1, "参数定值", "保护参数查看与修改", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, { 2, "整定", "整定装置保护参数", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, { 3, "参数", "查看设备参数定值", EN_FIGURE_SET, CN_USER_PWD, EN_SIDE_BASIC, (FUNCPTR)MenuProc_Set_Value }, { 3, "定值", "设置装置数值定值", EN_FIGURE_SET, CN_USER_PWD, EN_SIDE_DEF, (FUNCPTR)MenuProc_Set_Value }, { 3, "控制字", "设置装置控制字", EN_SOFT_SET, CN_USER_PWD, EN_SIDE_DEF, (FUNCPTR)MenuProc_Set_Value }, { 3, "软压板", "设置软压板", 0, CN_USER_PWD, EN_SOFT_PRO, (FUNCPTR)MenuProc_Set_Soft }, { 2, "查看", "查看装置保护参数", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, { 3, "参数", "设置设备参数定值", EN_FIGURE_SET, 0x0000, EN_SIDE_BASIC, (FUNCPTR)MenuProc_See_Set }, { 3, "定值", "查看数值型定值", EN_FIGURE_SET, 0x0000, EN_SIDE_DEF, (FUNCPTR)MenuProc_See_Set }, { 3, "控制字", "查看控制字定值", EN_SOFT_SET, 0x0000, EN_SIDE_DEF, (FUNCPTR)MenuProc_See_Set }, { 3, "软压板", "查看软压板", 0, 0x0000, EN_SOFT_PRO, (FUNCPTR)MenuProc_See_Soft }, { 1, "三遥设置", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, { 2, "遥测死区", "设置遥测量死区门槛", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YC_SetSqValue }, { 2, "遥测系数", "设置遥测量微调系数", EN_MEA_ADJ, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YC_SetAdjCoe }, { 2, "遥信类型", "设置遥信类型", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YX_SetCommType }, { 2, "遥信防抖", "设置遥信防抖时间", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YX_SetWidth }, { 2, "双点遥信", "设置双点遥信虚端子", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YX_SetTwin }, { 1, "装置维护", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, { 2, "时钟设置", "设置系统时钟", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_Time }, { 2, "强制复归", "可复归未返回事件", EN_REV_FORCE, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_RevEvent }, { 2, "手动录波", "启动手动录波", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_ManualWave }, { 2, "清除记录", "", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_ClrRec }, { 1, "通讯参数", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, { 2, "通讯设置", "外部通讯设置", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_ComPara }, { 2, "网口设置", "", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_EditIP }, { 2, "SNTP设置", "", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_EditSntp }, { 1, "记录查询", "查看各种装置记录", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, { 2, "SOE记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecSOE }, { 2, "事故记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecAct }, { 2, "操作记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecOpt }, { 2, "保护告警", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecAlm }, { 2, "保护启动", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecStart }, { 2, "遥控记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecYK }, { 2, "自检记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecChk }, { 2, "运行记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecRun }, { 2, "运行报告", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecFault }, { 0, "厂家设置", "设置装置相关参数", 0, CN_COP_PWD, 0, (FUNCPTR)Menu_NonPfunc }, { 1, "元件配置", "配置元件配置", 0, CN_COP_PWD, EN_FACTORY_PASSWORD,(FUNCPTR)MenuProc_Cfg_CellConf }, { 1, "恢复默认", "恢复默认元件定值参数", 0, CN_COP_PWD, 0, (FUNCPTR)Menu_NonPfunc }, { 2, "全部恢复", "全部参数恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_AllInf_Default }, { 2, "默认参数", "当前参数恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Para_Default }, { 2, "默认定值", "当前定值区恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Set_Default }, { 2, "软压板", "当前软压板恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Resume_Soft }, { 2, "元件配置", "元件配置恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Cfg_CellDef }, { 1, "交流显示", "交流显示方式设置", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Cfg_ShowAnaType }, { 1, "装置调试", "调试装置", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, { 2, "虚拟遥信", "设置虚拟遥信值", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_XuYX }, { 2, "交流虚遥测", "设置虚拟交流遥测值", EN_MEA_AC, CN_COP_PWD, EN_ANA_0, (FUNCPTR)MenuProc_Dbg_XuYC }, { 2, "直流虚遥测", "设置虚拟直流遥测值", EN_MEA_DC, CN_COP_PWD, EN_ANA_0, (FUNCPTR)MenuProc_Dbg_XuYC }, { 2, "电度虚遥测", "设置虚拟电度遥测值", EN_MEA_POWER, CN_COP_PWD, EN_ANA_0, (FUNCPTR)MenuProc_Dbg_XuYC }, { 2, "动作虚事件", "设置虚拟动作事件", EN_ACT_REC, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_XuEvent }, { 2, "告警虚事件", "设置虚拟告警事件", EN_ALM_REC, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_XuEvent }, { 2, "动作出口", "进入此菜单保护退出", EN_OUTPUT_TRIP, CN_COP_PWD, EN_INPUT_0, (FUNCPTR)MenuProc_Dbg_Relay }, { 2, "信号出口", "进入此菜单保护退出", EN_OUTPUT_SIGN, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_Relay }, { 1, "版本信息", "查看板件版本信息", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_See_VersionBoard }, }; ``` #### 7.3.2 解析后的树形结构 根据上表,`Menu_Main_Creat_01` 解析后的菜单树为: ```text 0 级目录 { 1.装置信息 1.实时数据 { 2.交流量 2.直流量 2.遥信量 } 1.参数定值 { 2.整定 { 3.参数 3.定值 3.控制字 3.软压板 } 2.查看 { 3.参数 3.定值 3.控制字 3.软压板 } } 1.三遥设置 { 2.遥测死区 2.遥测系数 2.遥信类型 2.遥信防抖 2.双点遥信 } 1.装置维护 { 2.时钟设置 2.强制复归 2.手动录波 2.清除记录 2.通讯参数 2.通讯设置 2.网口设置 2.SNTP设置 } 1.记录查询 { 2.SOE记录 2.事故记录 2.操作记录 2.保护告警 2.保护启动 2.遥控记录 2.自检记录 2.运行记录 2.运行报告 } } 0.厂家设置 { 1.元件配置 1.恢复默认 { 2.全部恢复 2.默认参数 2.默认定值 2.软压板 2.元件配置 } 1.交流显示 1.装置调试 { 2.虚拟遥信 2.交流虚遥测 2.直流虚遥测 2.电度虚遥测 2.动作虚事件 2.告警虚事件 2.动作出口 2.信号出口 } 1.版本信息 } ``` #### 7.3.3 单例解析示例:直流量 以 `{2,"直流量","查看遥测直流量",EN_MEA_DC,0x0000,EN_ANA_0,(FUNCPTR)MenuProc_See_YC}` 为例,构建后该菜单项的指针与属性为: ```text { ptHigher = 1.实时数据; ptLower = NULL; ptBefore = 2.交流量; ptBehind = 2.遥信量; byClass = 2; byName = 直流量; byTip = 查看遥测直流量; byAttrib = EN_MEA_DC; wPassword = 0x0000; wPara = 0; pfnWinProc = MenuProc_See_YC; } ``` #### 7.3.4 同级首尾成环 同级菜单中,首尾通过 `ptBefore/ptBehind` 相连形成环。例如 2 级子菜单: ```text 2.虚拟遥信 2.交流虚遥测 2.直流虚遥测 2.电度虚遥测 2.动作虚事件 2.告警虚事件 2.动作出口 2.信号出口 ``` 其中: - `2.虚拟遥信` 的 `ptBefore` 指向 `2.信号出口`(首的前一个是尾) - `2.信号出口` 的 `ptBehind` 指向 `2.虚拟遥信`(尾的后一个是首) ### 7.4 构建流程(`Menu_Main_Creat_01`) 按 `g_tMenuModelTab` 的定义顺序逐项处理,通过比较 `byCurClass` 与 `byNextClass` 设置每个菜单的层级指针。 #### 7.4.1 情况 1:`byCurClass < byNextClass`(下一项更深,进入子菜单) ```text ptCurrent ptNextNode (当前) (下一项,更深一级) │ │ │ ptLower ──────────────────►│ │◄───────────────── ptHigher │ │ │ 该级尾不变 新一级的 首=尾=ptNextNode ``` #### 7.4.2 情况 2:`byCurClass == byNextClass`(同级,并列) ```text ptCurrent ─── ptBehind ──► ptNextNode │ │ │ ptBefore ◄───────────┤ │ │ └──── ptHigher (同) ────────┘ 该级 尾 更新为 ptNextNode ``` #### 7.4.3 情况 3:`byCurClass > byNextClass`(下一项更浅,回到上层) ```text ... byCurClass 级 ... byNextClass 级 ... ptLast[byNextClass] 已存在 │ │ ptBehind ──► ptNextNode(新) │ │ │◄── ptBefore ────┤ │ ptHigher = 该级尾的 ptHigher 同时:从 byCurClass 到 byNextClass+1 各级首尾成环 ptLast[级]──►ptFirst[级],ptFirst[级]──►ptLast[级] ``` #### 7.4.4 最后:各级首尾成环 表遍历完后,从 **0 级到当前结点所在级**,把该级首尾连成环: ```text ptFirst[级] ◄──────────────► ptLast[级] │ │ └──── ptBehind ───────────────┘ ◄──────── ptBefore ────────────┘ ``` ### 7.5 `menu.c` 运行时整体逻辑 `menu.c` 在运行时主要做三件事: 1. **构建菜单树**:`Menu_Main_Creat` / `Menu_0LevelNumCal` 2. **根据按键路由当前菜单**:`Menu_Route` 3. **计算坐标并显示**:`Menu_Show_Proc` + 一系列 `Menu_*PosCal` 函数 可以把主循环理解为下面这个“数据流”: ```text 键盘按键 (key.c) ──► 菜单路由 (Menu_Route) │ ▼ 当前菜单指针 g_tMenuCtrl.ptCurrent │ ▼ 坐标计算 (Menu_Sub1PosCal / Menu_PosCal_0Level ...) │ ▼ 显示绘制 (Menu_Show_Proc + display.c/lcd.c) ``` #### 7.5.1 主循环 & 菜单刷新时序 在 `main.c` 的主循环中,大致时序可抽象成: ```text while (1) { // 1. 采集按键状态(key.c) Key_Scan(); // 2. 依据当前按键更新菜单当前位置(menu.c) Menu_Route(); // 3. 根据当前位置和菜单树,计算各级菜单的坐标 Menu_Show_Proc(); // 4. 刷新 LCD 显示(lcd.c / display.c) LCD_Refresh(); } ``` 你可以把 **`Menu_Route` 看成“控制层”**,把 **`Menu_Show_Proc` 看成“视图层布局计算”**。 #### 7.5.2 按键如何驱动菜单移动(`Menu_Route`) `Menu_Route` 的核心思想是: **根据按键,沿着当前层的循环链表移动 `ptCurrent`,或在父/子指针之间跳转。** 简化后的逻辑可以画成下面这样的**状态机图**: ```text ┌─────────────┐ │ 当前菜单项 │ (g_tMenuCtrl.ptCurrent) └─────┬───────┘ ▲ 上键/W │ 下键/S ▼ │ │ │ │ ptBefore ptBehind (同级上一个) (同级下一个) 左键/A 或 退出键/Esc 右键/D 或 Enter │ │ ▼ ▼ ptHigher(父菜单) ptLower(子菜单) ``` 更具体地: - **上键 / `W`**: - `ptCurrent = ptCurrent->ptBefore;` - 由于同级首尾成环,**从首再往上就会回到尾** - **下键 / `S`**: - `ptCurrent = ptCurrent->ptBehind;` - 从尾再往下会回到首,实现循环菜单 - **右键 / `D` 或 `Enter`**: - 若当前有子菜单:`ptCurrent = ptCurrent->ptLower;` - 同时更新 `g_tMenuCtrl.ptRoute[层级]`,记录层级路径 - **左键 / `A` 或 `Esc`**: - 若存在上级:`ptCurrent = ptCurrent->ptHigher;` - 层级回退,路径栈 `ptRoute[]` 随之更新 伪代码示意(省略防抖、长按等细节): ```c void Menu_Route(void) { tagPMenuItem ptCur = g_tMenuCtrl.ptCurrent; if (KEY_UP_PRESSED()) { ptCur = ptCur->ptBefore; } else if (KEY_DOWN_PRESSED()) { ptCur = ptCur->ptBehind; } else if (KEY_RIGHT_PRESSED() || KEY_ENTER_PRESSED()) { if (ptCur->ptLower != NULL) { ptCur = ptCur->ptLower; } } else if (KEY_LEFT_PRESSED() || KEY_ESC_PRESSED()) { if (ptCur->ptHigher != NULL) { ptCur = ptCur->ptHigher; } } g_tMenuCtrl.ptCurrent = ptCur; } ``` 实际代码里还会配合 `ptCurBak` 判断“是否移动了”,以决定是否需要重算坐标和刷新显示。 #### 7.5.3 坐标字段含义(`wSPosX/Y`、`wEPosX/Y`) `tagMenuItem` 中与坐标相关的字段: - `wSPosX, wSPosY`:**本菜单的下一级子菜单框的左上角坐标** - `wEPosX, wEPosY`:**本菜单的下一级子菜单框的右下角坐标** 可以类比成: ```text ┌──────────────── LCD 屏幕 ────────────────┐ │ │ │ ┌───── 本级菜单框 (父) ─────┐ │ │ │ │ │ │ │ ● 当前高亮菜单项 │ │ │ │ │ │ │ └───────────────────────────┘ │ │ ▲ wSPosY │ │ │ │ │ ▼ wEPosY │ │ ┌───── 子菜单框 (ptLower) ─────┐ │ │ │ ← wSPosX → wEPosX │ │ │ │ 子菜单项1 │ │ │ │ 子菜单项2 │ │ │ │ ... │ │ │ └──────────────────────────────┘ │ └──────────────────────────────────────────┘ ``` 这样设计的好处是: **每个菜单项都“携带”了自己的子菜单应该出现在哪一块区域的信息**,绘制时只需要看当前指针和这些坐标就能把所有框画出来。 #### 7.5.4 菜单位置如何计算(以 `Menu_Sub1PosCal` 为例) 用于计算二级菜单(以及更深层子菜单)矩形框位置的典型函数是 `Menu_Sub1PosCal`,高度概括为: 1. 统计当前菜单的**同级菜单数量**和**下一级菜单的最大标题长度** 2. 根据父菜单的框位置 + 当前菜单在本层中的序号,确定 Y 起点 3. 根据子菜单项数和行高 `LINE_HEIGHT`,确定 Y 终点,并做“是否超屏”判断 4. 若超出底部,则整体向上平移一段,避免越界 简化后的“计算流程图”: ```text 输入: - bylevel 当前层级(例如 1 表示一级菜单) - ptFirst[bylevel] 本级菜单首节点 - ptIndex[bylevel] 遍历用当前指针 步骤: 1) 统计下一级菜单: byMaxLen = Menu_charLenCal(...) → 得到最长标题字符数 byMaxLen → 得到各层级菜单项数 byMenuNum[] 2) 计算 X 方向: wSPosX = 父菜单 wEPosX wEPosX = wSPosX + byMaxLen * MENU_WITDTH + MENU_XADD 3) 计算 Y 起点: byMenuPos = 当前菜单在本级中的位置 (wPos) byItemNum = 本级菜单总项数 (byMenuNum[bylevel]) 如本级菜单超过一屏,修正 byMenuPos 做翻页映射 wSPosY = 父菜单 wSPosY + (byMenuPos - 1) * LINE_HEIGHT 4) 计算 Y 终点: byItemNum = 子菜单项数 (byMenuNum[bylevel + 1]) wEPosY = wSPosY + byItemNum * LINE_HEIGHT + MENU_YADD 5) 越界修正: 如果 wEPosY > MENU_YMAX: a) 尝试从当前项往上展开,使尾部对齐当前项 b) 若仍越界,则限制在 [0, MENU_YMAX] 范围内 6) 写回结果: ptIndex[bylevel]->wSPosX/Y = wSPosX/Y ptIndex[bylevel]->wEPosX/Y = wEPosX/Y ``` 可以把它想象成: **“拿到父菜单的框,把子菜单框贴在它右侧,再根据当前高亮位置往上或往下展开一串子项,同时保证整块框不会跑出屏幕。”** #### 7.5.5 多级菜单同时显示时的布局 当当前指针在三级菜单时,屏幕上通常会同时显示: ```text ┌──────── 0 级菜单框 ────────┐ │ ... │ └───────────────────────────┘ │ ▼ ┌──────── 1 级菜单框 ────────┐ │ ... 当前所在的一级菜单 │ └───────────────────────────┘ │ ▼ ┌── 2 级菜单框 ──┐ │ 子项1 │ │ 子项2(高亮) │ ← g_tMenuCtrl.ptCurrent │ 子项3 │ └──────────────┘ ``` `Menu_Show_Proc` 会依次遍历 `g_tMenuCtrl.ptRoute[]` 中记录的每一级菜单指针: 1. 对每一级调用对应的“位置计算函数”(0 级用 `Menu_0LevelPosCal`,1/2/3 级用 `Menu_Sub1PosCal` 等) 2. 根据计算出的矩形框坐标,在 `display.c` 中画出边框和文字内容 3. 对当前高亮项增加反显/反色效果 这样即可实现类似真实 HMI 上的“多级弹出菜单”效果。 --- ## 9. UTF‑8 汉字库(12×12 点阵) 项目中通过 `utf8_hz12_data.c/h` 存储 12×12 点阵的汉字库,供 LCD 绘制函数使用。 - 当需要**增加新的汉字**时,可以: 1. 修改或扩展 `gen_utf8_hz12.py` 中的字符集/输入文本 2. 运行脚本,重新生成 `utf8_hz12_data.c/h` 3. 重新编译项目 - LCD 显示模块会在绘制字符串时,根据 UTF‑8 编码在该表中查找对应点阵。 具体使用方式可以参考: - `src/Drv/Ascii.c`:ASCII/字母数字字符绘制 - `src/Drv/lcd.c`:LCD 基础绘制接口 --- ## 10. 调试与日志 - CMake 中提供了一个 `ENABLE_DEBUG` 选项,用于开启调试输出: ```bash cmake -DENABLE_DEBUG=ON .. cmake --build . ``` - 打开后会自动定义 `DEBUG` 宏,部分代码(例如 `Menu_Sub1PosCal` 等)会通过 `printf` 输出调试信息,方便观察菜单坐标计算、页滚动等细节。 如果你希望在调试时看到更多状态信息,可以: - 在关键流程(如 `Menu_Route`、`Menu_Show_Proc`、`remoteDisplay` 收发函数)增加 `#ifdef DEBUG` 包裹的日志 - 使用 GDB / VS 调试器在 `main.c`/`menu.c` 等处打断点,单步查看菜单树和坐标计算过程