嵌入式微系统
上QQ阅读APP看书,第一时间看更新

1.3.8 界面设计

一般地讲,基于MCU51的项目,菜单界面都相对简单,所以大家本能的采用状态机的方式来开发菜单界面,简单易用,推荐使用。

界面设计属于人机接口的一部分,通常我们用MMI来描述,MMI是Man Machine Interface的缩写,有些也叫HMI,Man换成Human。完整的人机界面包括按键、鼠标、触摸与界面显示部分。一般除了指示状态之外,往往都是根据按键消息显示不同的菜单,执行不同的消息处理,MCU51下消息处理往往比较简单。所以MS中按键处理程序基本上就等价于界面设计了。

状态机界面设计往往采用switch case来实现,从按键处理这个共同的入口进,根据一个或多个菜单状态变量选择入口,再根据按键值进行相应的消息处理,退出时根据按键值或者消息处理的结果决定是否要改变状态变量。改变状态变量等价于切换菜单界面,这种利用状态字来实现界面设计方式就叫菜单状态机设计思想,建议初学者用这种方式先做几个项目。

图1-20 状态机

图1-20为一个简单的一级目录三态状态机案例。

代码清单1-24:状态机状态枚举

typedef enum
{
    WorkState = 1,                                        // 工作状态
    SetupState = 2,                                       // 设置状态
    ServiceState = 3                                      // 服务状态
}StateEnum;
static StateEnum idata State = WorkState;                 // 定义状态机实体,赋初值

代码清单1-25:按键处理入口代码

void KeyProcess(KeyEnum key)
{
    switch (State)
    {
        case WorkState:                                        // 工作状态
            WorkStateProcess(key);
            break;
        case SetupState:                                       // 设置状态
            SetupStateProcess(key);
            break;
        case ServiceState:                                     // 服务状态
            ServiceStateProcess(key);
            break;
    }
}

代码清单1-26:工作状态处理代码

static void WorkStateProcess(KeyEnum key)
{
    printf(“当前工作界面: key = %c\n, key);             // 显示当前界面
    if (key == '2')                                        // 按键2执行
    {   printf(State = 2: 切换为设置界面\n);
        State = SetupState;                                // 切换到设置状态
    }
}

代码清单1-27:设置状态处理代码

static void SetupStateProcess(KeyEnum key)
{
    printf(“当前设置界面: key = %c\n, key);              // 显示当前界面
    if (key == '3')                                         // 按键3执行
    {
        printf(State = 3: 切换为维护界面\n);
        State = ServiceState;                               // 切换到服务状态
    }
}

代码清单1-28:服务状态处理代码

static void ServiceStateProcess(KeyEnum key)
{
    printf(“当前维护界面: key = %c\n, key);             // 显示当前界面
    if (key == '1')                                        // 按键1执行
    {
        printf(State = 1: 切换为工作界面\n);
        State = WorkState;                                 // 切换到服务状态
    }
}

用Keil3的软件模拟器Debug,在Serial#1窗口下运行结果如图1-21所示。

图1-21 Debug结果

状态机是一种比较简单的界面设计技术,不复杂的项目推荐使用。但读者可以发现,若复杂的界面,需要定义比较多的状态,尤其是菜单结构比较复杂的项目,状态机里面switch case语句过多,状态过多,很繁琐。有没有更好的方式呢?我们分析发现,每一个状态其实都对应一个函数,比如WorkState对应唯一的WorkStateProcess函数,其他两个也是这么一一映射的,既然这些状态跟函数名是一一对应,是唯一的,为什么不直接用函数名来做状态呢,这个就是用函数指针来设计界面的思路来源。

如同状态机,必须要有一个状态机变量,那么函数,必须要有一个函数名变量,且这个函数名变量的专业术语就是函数指针变量,也叫函数指针。因为函数名其实就是这个函数存放的首地址,定义一个函数指针变量可以存放函数地址,于是我们用函数指针方式来改造界面设计。

代码清单1-29:函数指针变量

typedef void (*MmiFunction)(KeyEnum key);                //定义函数指针类型
MmiFunction EnterFunction = WorkPointerProcess;          //定义函数指针变量并赋值

代码清单1-30:按键消息入口代码

case MessageKey:                                         // 按键消息处理
    EnterFunction(value);                                // 函数指针处理方式
    //KeyProcess(value);                                 // 状态机处理方式
    break;

代码清单1-31:工作状态处理代码

static void WorkStateProcess(KeyEnum key)
{
    printf("当前工作界面: key = %c\n", key);                // 显示当前界面
    if (key == '2')                                         // 按键2执行
    {
        printf("State = 2: 切换为设置界面\n");
        EnterFunction = SetupPointerProcess;                // 切换到设置状态
    }
}

代码清单1-32:设置状态处理代码

static void SetupStateProcess(KeyEnum key)
{
    printf("当前设置界面: key = %c\n", key);                // 显示当前界面
    if (key == '3')                                         // 按键3执行
    {
        printf("State = 3: 切换为维护界面\n");
        EnterFunction = ServicePointerProcess;              // 切换到服务状态
    }
}

代码清单1-33:服务状态处理代码

static void ServiceStateProcess(KeyEnum key)
{
    printf("当前维护界面: key = %c\n", key);               // 显示当前界面
    if (key == '1')                                        // 按键1执行
    {
        printf("State = 1: 切换为工作界面\n");
        EnterFunction = WorkPointerProcess;                // 切换到服务状态
    }
}

以上大家注意到,仅仅只是切换语句从状态码改成了函数名,所以函数指针的方式相对状态机略微简单一些。读者自己测试一下代码,效果完全跟状态机一样。我们通过对比发现,利用函数指针,化解了状态机的复杂性,当有多级菜单的时候,这个效果更加明显,因为函数指针是,从按键入口直达想要执行的函数,而状态机的三叉口很多,很容易混淆,尤其菜单级数一多,需要大分支切换的时候,很复杂。而函数指针,直接切换即可,针对性很强。当然,好东西是要付出一定代价的,那就是需要掌握函数指针的使用方法。尤其要记住函数名,其实就是函数的地址,也就是函数指针,所以可以用函数指针变量这种方式来替代状态机。

相比状态机,函数指针在开发常规菜单界面有一定的优势,编程上更为简单,但需要对指针有比较好的掌握。考虑到MCU51的ROM和RAM不是统一编址,函数指针在编译过程中容易出现未知错误,所以慎用。稍微复杂的项目建议用状态机比较合适,函数指针适合于统一编址的ARM芯片。msOS是采用ARM的Cortex M3内核,支持内存统一编址,很适合函数指针的广泛使用。