有限状态菜单
使用 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(¤tM); //Check if there is a next node and then go there
printf("%s\n",currentM->name);
break;
case 'a':
system("cls");
Prev(¤tM);
printf("%s\n",currentM->name);
break;
case 's':
system("cls");
Down(¤tM);
printf("%s\n",currentM->name);
break;
case 'w':
system("cls");
Up(¤tM);
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 实现相同。
关注点
这是我的第一篇文章,所以请温柔对待。