Files
DTU-HMI/README.md

28 KiB
Raw Blame History

DTU-HMI

PC 端 HMI 菜单逻辑模拟程序,纯 C 实现,用于在电脑上模拟现场 DTU 设备的人机界面HMI行为。

  • 菜单树:按真实装置菜单还原,支持多级嵌套、首尾成环遍历
  • LCD 显示:模拟 128×64 单色 LCD点阵缓冲区 + 绘制函数)
  • 键盘输入:用 PC 键盘按键映射嵌入式按键(上/下/左/右/确认/退出)
  • TCP 远程显示:实现 RemoDispBus 协议,可把 LCD 显示通过网络推送到上位机
  • UTF8 汉字库:内置 12×12 点阵汉字库,可扩展

1. 快速开始(构建 & 运行)

1.1 构建可执行程序

mkdir build
cd build
cmake ..
cmake --build .

生成可执行文件:

  • Windowsbuild/DTU-HMI.exe
  • Linux/macOSbuild/DTU-HMI

1.2 直接在本机运行

build 目录中执行:

./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 默认 MSVCLinux 需 GCC/Clang
  • 编码:源文件 UTF-8CMake 已配置 MSVC /utf-8

5. 构建步骤

5.1 Windows

mkdir build
cd build
cmake ..
cmake --build .

5.2 Linux / macOS

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 的显示图像:

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

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

/* 每个菜单包含:一、上下前后等级关系;二、属性与内容;三、显示坐标 */
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

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 解析后的菜单树为:

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} 为例,构建后该菜单项的指针与属性为:

{
    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 级子菜单:

2.虚拟遥信
2.交流虚遥测
2.直流虚遥测
2.电度虚遥测
2.动作虚事件
2.告警虚事件
2.动作出口
2.信号出口

其中:

  • 2.虚拟遥信ptBefore 指向 2.信号出口(首的前一个是尾)
  • 2.信号出口ptBehind 指向 2.虚拟遥信(尾的后一个是首)

7.4 构建流程(Menu_Main_Creat_01

g_tMenuModelTab 的定义顺序逐项处理,通过比较 byCurClassbyNextClass 设置每个菜单的层级指针。

7.4.1 情况 1byCurClass < byNextClass(下一项更深,进入子菜单)

    ptCurrent                    ptNextNode
    (当前)                        (下一项,更深一级)
        │                              │
        │  ptLower ──────────────────►│
        │◄───────────────── ptHigher   │
        │                              │
    该级尾不变                    新一级的 首=尾=ptNextNode

7.4.2 情况 2byCurClass == byNextClass(同级,并列)

    ptCurrent ─── ptBehind ──► ptNextNode
        │                          │
        │     ptBefore ◄───────────┤
        │                          │
        └──── ptHigher (同) ────────┘
    该级 尾 更新为 ptNextNode

7.4.3 情况 3byCurClass > byNextClass(下一项更浅,回到上层)

    ... byCurClass 级 ...     byNextClass 级 ...
         ptLast[byNextClass] 已存在
                    │
                    │ ptBehind ──► ptNextNode
                    │                 │
                    │◄── ptBefore ────┤
                    │                 ptHigher = 该级尾的 ptHigher
    同时:从 byCurClass 到 byNextClass+1 各级首尾成环
          ptLast[级]──►ptFirst[级]ptFirst[级]──►ptLast[级]

7.4.4 最后:各级首尾成环

表遍历完后,从 0 级到当前结点所在级,把该级首尾连成环:

    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 函数

可以把主循环理解为下面这个“数据流”:

键盘按键 (key.c) ──► 菜单路由 (Menu_Route)
                         │
                         ▼
                    当前菜单指针 g_tMenuCtrl.ptCurrent
                         │
                         ▼
         坐标计算 (Menu_Sub1PosCal / Menu_PosCal_0Level ...)
                         │
                         ▼
               显示绘制 (Menu_Show_Proc + display.c/lcd.c)

7.5.1 主循环 & 菜单刷新时序

main.c 的主循环中,大致时序可抽象成:

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,或在父/子指针之间跳转。

简化后的逻辑可以画成下面这样的状态机图

                ┌─────────────┐
                │  当前菜单项  │  (g_tMenuCtrl.ptCurrent)
                └─────┬───────┘
        ▲ 上键/W       │        下键/S ▼
        │              │
        │              │
   ptBefore           ptBehind
   (同级上一个)       (同级下一个)


 左键/A 或 退出键/Esc   右键/D 或 Enter
           │                    │
           ▼                    ▼
      ptHigher(父菜单)      ptLower(子菜单)

更具体地:

  • 上键 / W
    • ptCurrent = ptCurrent->ptBefore;
    • 由于同级首尾成环,从首再往上就会回到尾
  • 下键 / S
    • ptCurrent = ptCurrent->ptBehind;
    • 从尾再往下会回到首,实现循环菜单
  • 右键 / DEnter
    • 若当前有子菜单:ptCurrent = ptCurrent->ptLower;
    • 同时更新 g_tMenuCtrl.ptRoute[层级],记录层级路径
  • 左键 / AEsc
    • 若存在上级:ptCurrent = ptCurrent->ptHigher;
    • 层级回退,路径栈 ptRoute[] 随之更新

伪代码示意(省略防抖、长按等细节):

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/YwEPosX/Y

tagMenuItem 中与坐标相关的字段:

  • wSPosX, wSPosY本菜单的下一级子菜单框的左上角坐标
  • wEPosX, wEPosY本菜单的下一级子菜单框的右下角坐标

可以类比成:

┌──────────────── LCD 屏幕 ────────────────┐
│                                          │
│  ┌───── 本级菜单框 (父) ─────┐           │
│  │                           │           │
│  │   ● 当前高亮菜单项        │           │
│  │                           │           │
│  └───────────────────────────┘           │
│             ▲ wSPosY                     │
│             │                            │
│             ▼ wEPosY                     │
│  ┌───── 子菜单框 (ptLower) ─────┐        │
│  │  ← wSPosX       → wEPosX     │        │
│  │  子菜单项1                   │        │
│  │  子菜单项2                   │        │
│  │  ...                         │        │
│  └──────────────────────────────┘        │
└──────────────────────────────────────────┘

这样设计的好处是:
每个菜单项都“携带”了自己的子菜单应该出现在哪一块区域的信息,绘制时只需要看当前指针和这些坐标就能把所有框画出来。

7.5.4 菜单位置如何计算(以 Menu_Sub1PosCal 为例)

用于计算二级菜单(以及更深层子菜单)矩形框位置的典型函数是 Menu_Sub1PosCal,高度概括为:

  1. 统计当前菜单的同级菜单数量下一级菜单的最大标题长度
  2. 根据父菜单的框位置 + 当前菜单在本层中的序号,确定 Y 起点
  3. 根据子菜单项数和行高 LINE_HEIGHT,确定 Y 终点,并做“是否超屏”判断
  4. 若超出底部,则整体向上平移一段,避免越界

简化后的“计算流程图”:

输入:
  - 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 多级菜单同时显示时的布局

当当前指针在三级菜单时,屏幕上通常会同时显示:

┌──────── 0 级菜单框 ────────┐
│  ...                      │
└───────────────────────────┘
          │
          ▼
┌──────── 1 级菜单框 ────────┐
│  ... 当前所在的一级菜单   │
└───────────────────────────┘
                     │
                     ▼
           ┌── 2 级菜单框 ──┐
           │  子项1        │
           │  子项2(高亮)  │ ← g_tMenuCtrl.ptCurrent
           │  子项3        │
           └──────────────┘

Menu_Show_Proc 会依次遍历 g_tMenuCtrl.ptRoute[] 中记录的每一级菜单指针:

  1. 对每一级调用对应的“位置计算函数”0 级用 Menu_0LevelPosCal1/2/3 级用 Menu_Sub1PosCal 等)
  2. 根据计算出的矩形框坐标,在 display.c 中画出边框和文字内容
  3. 对当前高亮项增加反显/反色效果

这样即可实现类似真实 HMI 上的“多级弹出菜单”效果。

9. UTF8 汉字库12×12 点阵)

项目中通过 utf8_hz12_data.c/h 存储 12×12 点阵的汉字库,供 LCD 绘制函数使用。

  • 当需要增加新的汉字时,可以:
    1. 修改或扩展 gen_utf8_hz12.py 中的字符集/输入文本
    2. 运行脚本,重新生成 utf8_hz12_data.c/h
    3. 重新编译项目
  • LCD 显示模块会在绘制字符串时,根据 UTF8 编码在该表中查找对应点阵。

具体使用方式可以参考:

  • src/Drv/Ascii.cASCII/字母数字字符绘制
  • src/Drv/lcd.cLCD 基础绘制接口

10. 调试与日志

  • CMake 中提供了一个 ENABLE_DEBUG 选项,用于开启调试输出:

    cmake -DENABLE_DEBUG=ON ..
    cmake --build .
    
  • 打开后会自动定义 DEBUG 宏,部分代码(例如 Menu_Sub1PosCal 等)会通过 printf 输出调试信息,方便观察菜单坐标计算、页滚动等细节。

如果你希望在调试时看到更多状态信息,可以:

  • 在关键流程(如 Menu_RouteMenu_Show_ProcremoteDisplay 收发函数)增加 #ifdef DEBUG 包裹的日志
  • 使用 GDB / VS 调试器在 main.c/menu.c 等处打断点,单步查看菜单树和坐标计算过程