65.9K
CodeProject 正在变化。 阅读更多。
Home

有限状态菜单

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.04/5 (12投票s)

2014年7月7日

CPOL

5分钟阅读

viewsIcon

31075

downloadIcon

801

使用 C 语言中的多个链表和函数指针实现的一个有限状态菜单,用于嵌入式编程。

引言

本文着重于使用四重链表创建一个简单的菜单,该菜单可用于嵌入式系统。这可以看作是避免使用多个 switch case 语句的替代方法,因为后者可能会变得非常混乱。该系统设计用于 PIC 18f4550,配合 16x2 LCD 显示屏和由 16 个按键组成的键盘,并使用 CCS C 编译器进行编译。为了测试方便,我最初在普通的 C 语言中设计了菜单部分,本文将演示其工作原理。

背景

嵌入式系统的用户输入通常只有几个按键,与计算机的键盘和鼠标输入相比灵活性较低。为了模拟这种情况,我们将只使用按键:'a'、'w'、's' 和 'd'。其中 'a' 表示向左,'w' 表示向上,'d' 表示向右,'s' 表示向下,同时也用于选择菜单项。

在一个之前的项目中,我需要一个必须由 4 个按钮操作的菜单,它实现了以下功能:

  • 显示温度和时间
  • 设置安全温度区域的最小和最大值
  • 设置灯光开关的特定时间
  • 设置执行器开启的时间,并指定它工作多少次后关闭

以指定灯光开启时间为例,这意味着必须使用 switch case 语句设置以下结构:主菜单 -> 灯光控制 -> 开启时间 -> [执行功能]。

您已经有了 3 个嵌套的 switch case 语句,每个语句都必须评估按下的是哪个按键并据此作出反应。这使得代码维护变得困难,并且很快就变得“混乱”(杂乱)。我继续采用这种方法,因为截止日期太近了,无法试验和寻找其他方法。

现在我们将研究使用链表和函数指针在 C 语言中实现此菜单结构的“另一种方法”。

Using the Code

为了保持模块化,创建了一个名为“menu.h”的单独文件来存储菜单的层/节点以及遍历该结构的函数。(在本文中,层和节点是互换使用的)

每个菜单项都将表示为一个层结构。每个结构都有一个指向其上方、下方、前一个和下一个层结构的指针。如果您不希望该路径被遍历,则提供 0;否则,提供一个有效的层指针。DoWork 函数指针存储了一个 void 函数的指针,当调用时该函数将执行操作。

struct level {
   char name[16];
   struct level *next;
   struct level *prev;
   struct level *down;
   struct level *up;
   void (*DoWork)(void);
};

菜单层创建如下:

struct level normalM, fanM, fanSpeed, fanRpm, tempM, *currentM;

其中 currentM 是层结构的一个指针,将是唯一用于遍历结构的指针。Next 是一个用于构建菜单层的函数。

void BuildMenu(struct level *currentNode, char name[16], void (*DoWork)(void) , struct level *prevNode, struct level *nextNode,struct level *upNode,struct level *downNode)
{
    strcpy(currentNode->name, name);
    currentNode->prev = prevNode;
    currentNode->next = nextNode;
    currentNode->up = upNode;
    currentNode->down = downNode;
    currentNode->DoWork = DoWork;
}

第一个参数通过引用传递正在构建的节点。然后,您传递该节点的名称。第三个参数需要一个 void 函数,当用户选择此菜单项时,该函数将执行。参数 4 到 7 指定当前层左右两边的层,它们都必须通过引用传递以获取它们的地址。

注意:如果当前节点没有该选项,则必须指定 0,例如:

  BuildMenu(&normalM,"Normal", 0, 0,&fanM, 0, 0);

在这里,我们通过给它一个名为“Normal”的名称来构建 normalM 层,指定它在调用时没有要执行的函数,没有前一个节点,下一个节点是 fanM ,并且它没有向上或向下的节点。然后设置整个菜单。

    BuildMenu(&normalM,"Normal", 0, 0,&fanM, 0, 0);

    BuildMenu(&fanM,"Fan Control", 0, &normalM,&tempM, 0, &fanSpeed);

          BuildMenu(&fanSpeed,"Fan Speed", DoWork_FanSpeed, 0, &fanRpm, &fanM, 0);

          BuildMenu(&fanRpm,"Fan RPM", DoWork_FanRpm, &fanSpeed, 0, &fanM , 0);

    BuildMenu(&tempM,"Temperature", DoWork_Temp, &fanM,0, 0, 0);

以上菜单的可视化效果。

深黑色的圆圈表示 0,即没有定义路径。我喜欢将它们想象成 ROM 的概念,其中黑色的圆圈是已固化的链接。

然后,遍历菜单的函数会变得有点有趣。

void Next(struct level **currentNode) //Correct
{
  if( (*currentNode) ->next != 0)
  (*currentNode) = (*currentNode)->next;
}

该函数的参数接受一个指向层结构指针的指针。这是必需的,因为如果我们想让当前层成为下一层(如果它不是 0)。因此,我们不能按值传递,必须按引用传递。按值传递的函数看起来会像这样,但这是不正确的,因为变量 currentNode 只有函数作用域,无法持久化指向结构值的指针。

void Next(struct level *currentNode) //Incorrect
{
  if(currentNode->next != 0)
    currentNode = currentNode->next;
}

Previous Up 函数也像正确的 Next 函数一样定义。然而,Down 函数考虑了 DoWork 函数指针。它首先检查是否有要执行的工作,如果有,则执行它;如果没有,则检查下方是否有节点,如果有,则当前节点向下移动一层。

就这样,下面展示了如何实现 menu.h 头文件的函数和结构。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "menu.h"

//void functions for menu level actions

void DoWork_FanSpeed(void);
void DoWork_FanRpm();
void DoWork_Temp();

//declare all levels/nodes of menu

struct level normalM,fanM,fanSpeed,fanRpm,tempM, *currentM;

int main()
{
    //Build the menu in a hierarchical structure
    BuildMenu(&normalM,"Normal", 0, 0,&fanM, 0, 0);
    BuildMenu(&fanM,"Fan Control", 0, &normalM,&tempM, 0, &fanSpeed);
        BuildMenu(&fanSpeed,"Fan Speed", DoWork_FanSpeed, 0, &fanRpm, &fanM, 0);
        BuildMenu(&fanRpm,"Fan RPM", DoWork_FanRpm, &fanSpeed, 0, &fanM , 0);
    BuildMenu(&tempM,"Temperature", DoWork_Temp, &fanM,0, 0, 0);

    //Assign the current menu item the first item in the menu
    currentM = &normalM;
    printf("%s\n",currentM->name);

  char cKey;

  do
    {
        scanf("%c",&cKey);
        switch(cKey)
        {
            case 'd':
                system("cls"); //Clear screen
                Next(&currentM);  //Check if there is a next node and then go there
                printf("%s\n",currentM->name);
            break;

            case 'a':
                system("cls");
                Prev(&currentM);
                printf("%s\n",currentM->name);
            break;

            case 's':
                system("cls");
                Down(&currentM);
                printf("%s\n",currentM->name);
            break;

             case 'w':
                 system("cls");
                 Up(&currentM);
                 printf("%s\n",currentM->name);
            break;
        }
    }while(cKey != '~');

    return 0;
}

void DoWork_FanSpeed()
{
    printf("Adjusting Fan speed\n");
}

void DoWork_FanRpm()
{
    printf("Changing Fan Rpm\n");
}

void DoWork_Temp()
{
    printf("Temperature display\n");
}

实际上,这在 C 标准下工作得非常好,但许多嵌入式编译器会尽力复制 C 语言,所以我很快发现 CSS C 编译器不支持函数指针,真是倒霉。

因此,替代解决方案是创建一个 enum 来描述每个菜单项需要执行的功能,并修改层结构以存储该 enum 值。然后,当执行 Down 操作时,它会检查该层 enum 中的值,然后使用 switch case 执行相应的函数。此外,您不能将常量 string 传递给函数,它必须首先存储。Strcpy 会在内部执行此操作,因此对于每个层,都必须在函数外部单独调用它。

因此,对于 CCS C,实际实现是:

typedef enum task {None,Normal,Fanspeed,FanRpm,Temp}; // None = 0

struct level {
   char name[16];
   struct level *next;
   struct level *prev;
   struct level *down;
   struct level *up;
   task DoTask; //Changed function pointer to hold enum value
} normalM,fanM,fanSpeedM,fanRpmM,tempM, *currentM;

//Removed char name[16] and the function pointer
void BuildMenu(struct level *currentNode, task DoTask, struct level *prevNode, struct level *nextNode,struct level *upNode,struct level *downNode)
{   
    currentNode->prev = prevNode;
    currentNode->next = nextNode;
    currentNode->up = upNode;
    currentNode->down = downNode;
    currentNode->DoTask = DoTask;
}

void ExecuteTask(task taskToDo)
{
    switch(taskToDo)
    {
        case Fanspeed:
             DoWork_FanSpeed();
            break;

        case FanRpm:
            DoWork_FanRpm();
            break;

        case Temp:
            DoWork_Temp();
            break;
    }

    delay_ms(1000);
}

void Down(struct level **currentNode)
{
    if((*currentNode)->DoTask != None)
        ExecuteTask((*currentNode)->DoTask);
    else if((*currentNode)->down != 0)
        (*currentNode) = (*currentNode)->down;
}

  BuildMenu(&normalM, None, 0,&fanM, 0, 0);
     strcpy(normalM.name, "Normal");  
     
  BuildMenu(&fanM, None, &normalM,&tempM, 0, &fanSpeedM);
     strcpy(fanM.name, "Fan Control");

        BuildMenu(&fanSpeedM, Fanspeed , 0, &fanRpmM, &fanM, 0);
           strcpy(fanSpeedM.name, "Fan Speed");

        BuildMenu(&fanRpmM, FanRpm, &fanSpeedM, 0, &fanM , 0);
           strcpy(fanRpmM.name, "Fan RPM");

    BuildMenu(&tempM, Temp, &fanM,0, 0, 0);
       strcpy(tempM.name, "Temperature");

其余部分与普通 C 实现相同。

关注点

这是我的第一篇文章,所以请温柔对待。

© . All rights reserved.