使用链表实现 LCD 菜单是处理复杂多级菜单的理想方案,尤其适合菜单结构动态变化(如运行时新增 / 删除选项)、层级不固定的场景。与数组实现相比,链表通过指针关联节点,无需预先定义固定大小,能更灵活地管理菜单层级关系。

一、链表菜单的核心优势

  1. 动态扩展性:支持运行时动态添加 / 删除菜单项(如根据设备状态生成不同菜单);
  2. 内存高效:仅为实际使用的菜单项分配内存,适合资源有限的单片机;
  3. 层级灵活:轻松实现无限级嵌套菜单(如 “系统设置→显示设置→背光调节→亮度等级”);
  4. 结构清晰:通过指针直接关联父 / 子 / 同级节点,逻辑关系直观。

二、链表节点结构设计(核心)

链表菜单的基础是菜单节点结构体,需包含 “自身信息 + 关联指针”,通过指针串联形成树形结构。

1. 节点结构体定义

// 提前声明结构体(解决递归引用)
typedef struct MenuNode MenuNode;

// 菜单功能函数指针(指向具体业务逻辑,如参数设置、数据显示)
typedef void (*MenuFunc)(void);

// 菜单节点结构体(核心)
struct MenuNode {
    char *name;               // 菜单项名称(如"背光调节")
    MenuNode *parent;         // 父节点指针(根节点为NULL)
    MenuNode *child;          // 子菜单首节点指针(无子嗣为NULL)
    MenuNode *prev;           // 同级前一个节点指针(用于上移选单)
    MenuNode *next;           // 同级后一个节点指针(用于下移选单)
    MenuFunc func;            // 功能函数(非功能项为NULL)
};

指针关系说明:

  • parent:指向父菜单(如 “背光调节” 的父节点是 “显示设置”);
  • child:指向子菜单的第一个节点(如 “显示设置” 的 child 指向 “背光调节”);
  • prev/next:串联同级节点(如 “背光调节” 的 next 指向 “对比度调节”)。

2. 链表菜单的树形结构示例

以 “3 级菜单” 为例,节点关联关系如下:

plaintext

根菜单(main)
  ├─ 系统设置(sys)→ parent=main
  │  ├─ 显示设置(disp)→ parent=sys,prev=NULL,next=net
  │  │  ├─ 背光调节(backlight)→ parent=disp
  │  │  └─ 对比度(contrast)→ parent=disp,prev=backlight
  │  └─ 网络设置(net)→ parent=sys,prev=disp,next=NULL
  └─ 数据显示(data)→ parent=main,prev=sys

三、链表菜单的基础操作函数

需实现创建节点、添加子菜单、遍历同级节点等工具函数,用于构建和管理菜单树。

1. 创建新节点

// 创建菜单节点(参数:名称、功能函数)
MenuNode* createMenuNode(const char *name, MenuFunc func) {
    MenuNode *node = (MenuNode*)malloc(sizeof(MenuNode));
    if(node == NULL) return NULL; // 内存分配失败
    
    // 初始化节点信息
    node->name = (char*)name;     // 简化处理(实际需拷贝字符串)
    node->func = func;
    node->parent = NULL;
    node->child = NULL;
    node->prev = NULL;
    node->next = NULL;
    return node;
}

2. 向父节点添加子菜单

// 给父节点添加子菜单(子节点将追加到同级菜单末尾)
void addChildMenu(MenuNode *parent, MenuNode *child) {
    if(parent == NULL || child == NULL) return;
    
    child->parent = parent; // 设置父节点
    
    // 若父节点无子菜单,直接作为第一个子节点
    if(parent->child == NULL) {
        parent->child = child;
    } else {
        // 否则遍历到同级最后一个节点,追加新节点
        MenuNode *lastChild = parent->child;
        while(lastChild->next != NULL) {
            lastChild = lastChild->next;
        }
        lastChild->next = child;
        child->prev = lastChild; // 双向关联
    }
}

3. 遍历同级节点(获取选项总数)

// 计算当前节点所在同级菜单的选项总数
uint8_t getSiblingCount(MenuNode *node) {
    if(node == NULL) return 0;
    
    // 找到同级第一个节点
    MenuNode *first = node;
    while(first->prev != NULL) {
        first = first->prev;
    }
    
    // 从第一个节点开始计数
    uint8_t count = 0;
    while(first != NULL) {
        count++;
        first = first->next;
    }
    return count;
}

四、LCD 菜单显示与交互实现

1. 全局状态变量(记录当前菜单状态)

MenuNode *currentNode; // 当前选中的节点(初始为根菜单第一个节点)

2. 菜单界面刷新(LCD 显示逻辑)

// 刷新LCD显示(以1602为例,显示当前节点的同级菜单)
void menuRefresh() {
    if(currentNode == NULL) return;
    
    lcdClear(); // 清屏(实际可优化为局部刷新)
    
    // 1. 显示当前菜单层级路径(如"系统设置→显示设置")
    char path[16] = {0};
    MenuNode *temp = currentNode;
    while(temp->parent != NULL) {
        // 拼接父节点名称(简化处理,实际需控制长度)
        strcat(path, temp->parent->name);
        strcat(path, "→");
        temp = temp->parent;
    }
    lcdShowString(0, 0, path); // 第一行显示路径
    
    // 2. 显示当前选中项及其同级菜单(第二行)
    // 找到同级第一个节点
    MenuNode *firstSibling = currentNode;
    while(firstSibling->prev != NULL) {
        firstSibling = firstSibling->prev;
    }
    
    // 显示同级菜单(最多显示2项,超出可滚动)
    uint8_t i = 0;
    temp = firstSibling;
    while(temp != NULL && i < 2) {
        // 选中项反显(LCD1602反显指令:0x0F)
        if(temp == currentNode) {
            lcdWriteCmd(0x0F); // 反显
        } else {
            lcdWriteCmd(0x0C); // 正常显示
        }
        lcdShowString(i*8, 1, temp->name); // 每行分2列显示
        temp = temp->next;
        i++;
    }
    lcdWriteCmd(0x0C); // 恢复正常显示
}

3. 按键交互处理(核心逻辑)

// 处理按键输入(1=上移,2=下移,3=确认,4=返回)
void menuProcessKey(uint8_t key) {
    if(currentNode == NULL) return;
    
    switch(key) {
        // 上移:切换到同级前一个节点
        case 1:
            if(currentNode->prev != NULL) {
                currentNode = currentNode->prev;
                menuRefresh();
            }
            break;
        
        // 下移:切换到同级后一个节点
        case 2:
            if(currentNode->next != NULL) {
                currentNode = currentNode->next;
                menuRefresh();
            }
            break;
        
        // 确认:进入子菜单或执行功能
        case 3:
            // 若有子菜单,进入子菜单第一个节点
            if(currentNode->child != NULL) {
                currentNode = currentNode->child;
                menuRefresh();
            }
            // 若有功能函数,执行功能
            else if(currentNode->func != NULL) {
                currentNode->func(); // 调用业务函数
                menuRefresh(); // 执行完返回当前菜单
            }
            break;
        
        // 返回:回到父菜单
        case 4:
            if(currentNode->parent != NULL) {
                currentNode = currentNode->parent;
                menuRefresh();
            }
            break;
    }
}

五、完整菜单构建示例

以 “系统设置→显示设置→背光调节” 三级菜单为例,展示如何用链表构建:

// 1. 定义业务功能函数(应用层)
void funcBacklight() {
    lcdClear();
    lcdShowString(0, 0, "Backlight: 70%");
    delayMs(1000); // 模拟设置过程
}

void funcContrast() {
    lcdClear();
    lcdShowString(0, 0, "Contrast: 50%");
    delayMs(1000);
}

void funcDataShow() {
    lcdClear();
    lcdShowString(0, 0, "Temp: 25C Hum:60%");
    delayMs(1000);
}

// 2. 构建菜单树(初始化)
void menuInit() {
    // 创建节点
    MenuNode *main = createMenuNode("Main", NULL);          // 根节点(虚拟,无实际显示)
    MenuNode *sysSet = createMenuNode("System", NULL);      // 系统设置
    MenuNode *dispSet = createMenuNode("Display", NULL);    // 显示设置
    MenuNode *backlight = createMenuNode("Backlight", funcBacklight); // 背光调节
    MenuNode *contrast = createMenuNode("Contrast", funcContrast);   // 对比度
    MenuNode *dataShow = createMenuNode("Data", funcDataShow);        // 数据显示
    
    // 构建层级关系
    addChildMenu(main, sysSet);       // 主菜单添加"系统设置"
    addChildMenu(main, dataShow);     // 主菜单添加"数据显示"
    addChildMenu(sysSet, dispSet);    // 系统设置添加"显示设置"
    addChildMenu(dispSet, backlight); // 显示设置添加"背光调节"
    addChildMenu(dispSet, contrast);  // 显示设置添加"对比度"
    
    // 初始选中根菜单的第一个子节点(系统设置)
    currentNode = main->child;
    menuRefresh();
}

六、优化与注意事项

1. 内存管理(关键!)

  • 单片机通常无 OS 自动回收内存,需手动释放无用节点(如free(node));
  • 动态创建节点前检查内存是否充足(malloc返回 NULL 时的容错处理);
  • 固定菜单建议在编译时静态分配(static MenuNode node),避免动态内存碎片。

2. 菜单导航效率优化

  • 缓存当前同级菜单的首节点和总数量,避免每次刷新都遍历链表;
  • 支持 “快速跳转”(如长按返回键直接回到根菜单)。

3. 处理大型菜单(超过 LCD 显示行数)

  • 实现滚动逻辑:当同级菜单数量超过 LCD 可显示行数(如 1602 显示 2 行),通过scrollOffset控制显示范围;
uint8_t scrollOffset = 0; // 滚动偏移量(当前显示的起始索引)
// 下移时,若currentNode超出显示范围,scrollOffset++

4. 防错处理

  • 对NULL指针进行判断,避免操作空节点导致程序崩溃;
  • 功能函数执行时禁用菜单导航(如设置参数时锁定按键,仅响应确认 / 返回)。

七、链表 vs 数组实现对比

特性链表实现数组实现
动态扩展支持(运行时增删节点)不支持(编译时固定大小)
内存效率按需分配,适合复杂菜单预分配,可能浪费内存
导航复杂度稍高(需遍历指针)简单(通过索引访问)
多级菜单天然支持,层级无限需固定层级深度
适合场景动态 / 复杂 / 多级菜单静态 / 简单 / 固定层级菜单

总结

链表是实现复杂 LCD 菜单的高效方案,通过parent/child/prev/next指针构建树形结构,配合动态节点管理,可灵活应对多级、可变的菜单需求。核心步骤为:

  1. 定义包含关联指针的MenuNode结构体;
  2. 实现节点创建、添加子菜单等基础操作;
  3. 基于当前节点指针实现菜单刷新和按键导航;
  4. 注意内存管理和边界容错,确保单片机稳定运行。

对于需要动态调整(如根据设备状态生成不同选项)或层级极深的菜单,链表是优于数组的首选方案。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐