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内核,支持内存统一编址,很适合函数指针的广泛使用。