通过前面的介绍,我们知道,嵌入式软件基本上是一个由外部激励驱动的系统。因此,在实现上有大量需要根据激励(包括收到的数据、用户输入的命令、有限状态机迁移等)的不同类型进行分门别类处理的情况。C语言提供了两种直接的机制可以用于处理这种情况:if/else和switch/case。一般对于分支较少的情况(一般3、4个以下),可以选用if/else;对于稍微多一些的情况(一般5、6个以下),可以选用switch/case。但是对于更多分支的情况,用这两种方式都不直观,影响可读性和可扩展性。本章主要介绍处理这种情况的一种有效方式—函数指针数组。
函数指针数组是元素为函数指针的一类数组。其形式一般为:
typedef void (*Function_Pointer_t)(int iParameter);
void Function1(int iParameter);
void Function2(int iParameter);
void Function3(int iParameter);
Function_Pointer_t funcPointerArray[] = {
? Function1,
? Function2,
? Function3,
};
|
首先定义了一个函数指针类型Function_Pointer_t,其形参为一个正数iParameter,返回值类型为void。同时有三个函数Function1、Function2、Function3,与上述函数指针类型Function_Pointer_t,具有相同的形参类型和返回值类型。然后定义了一个函数指针数组funcPointerArray,其类型为Function_Pointer_t,有三个元素,分别为:Function1、Function2、Function3。然后可以数组元素的形式调用相应的函数,比如:
(funcPointerArray[0])(iPara);
调用的就是Function1(iPara)。
下面通过实际的例子来说明函数指针数组的应用。本篇介绍一个嵌入式系统中经常需要用到的LOG输出系统的实现,下一篇介绍一个Modbus的协议处理的实现。
LOG命令处理系统
在一般的嵌入式软件中,为了调试和问题定位的方便,都实现了相应的LOG输出或保存功能。但如果LOG系统保持常时运行,会在Flash使用寿命、耗电、运行性能等方面带来不利影响,因此,在实际运行中LOG系统经常是关闭的。如果在现场发现了问题,往往希望随时激活LOG系统,取得分析问题所必要的信息。激活LOG的方式一般有两种:一种方法是采用编译选项,只在调试版中包含LOG相关的代码,而在实际运行版本(发布版)中则不包含。这样虽然可以使得发布版尽量精简,但需要采集LOG信息时,只能重新下载调试版程序。这增加了操作的难度(比如有些设备在现场难以下载程序),而且有时重新下载程序也会导致重现问题的环境丢失。第二种是采用在线命令的方式激活,也即在发布版中也包含LOG相关的代码,但平时运行时,不保存或输出LOG;只有在需要采取LOG时,才通过命令的形式激活LOG功能。虽然这使得发布版的代码增大,但可以随时激活/关闭LOG功能,大大提高了现场问题解决的便利性。
LOG命令一般通过UART与用户交互,在PC侧利用通用的串口工具(如Windows自带的超级终端)等,以命令行的形式输入命令。利用该接口,除了可以开关LOG,也可以设定工作模式、串口波特率等。
另外,作为调试工具,往往先实现个别命令,然后在使用中逐渐添加,因此要求比较好的可扩展性。
在实现这样一个系统时,主要是对不同的命令进行处理:包括识别命令、执行相应的处理。假如有以下几条命令:
- logon??????????????????????? ??—打开LOG输出
- logoff??????????????????????????—关闭LOG输出
- setlogmode b/B/t/T? ? ?—设置LOG输出模式,指定b、B为二进制输出,t、T为文本输出。
- setrate baudrate? ? ? ? ?—设置波特率
因为需要对不同的命令分别处理,比较直观的实现方式是采用if/else或者switch/case,以下是用if/else实现时的代码:
void handleLogCommands(char *pcACommand) {
? //Check the parameters.
? if(NULL == pcACommand)
??? return; //Nothing to do.
? if(0 == memcmp(pcACommand, "logon", 5)) {
??? handleLogOn();
? }
? else if(0 == memcmp(pcACommand, "logoff", 6)) {
??? handleLogOff();
? }
? else if(0 == memcmp(pcACommand, "setlogmode", 10)) {
??? handleLogModeSetting(pcACommand + 10);
? }
? else if(0 == memcmp(pcACommand, "setrate", 7)) {
??? handleBaudrateSetting(pcACommand);
? }
? else {
??? //Illegal command.
??? return;
? }
}
|
通过判定输入命令行前若干个字符是否为支持的命令,调用相应的命令处理函数。在该例子中,处理各个命令的函数handleLogOn(),handleLogOff(),handleLogModeSetting(),handleBaudrateSetting()的具体实现没有给出。这种实现方式在命令数少的时候没有问题,但随着命令数量的增加,这个函数的可读性下降。而且handleLogCommands ()作为LOG命令处理的控制函数,与各个命令的处理耦合:需要增加/删除命令时,就需要修改该控制函数。
通过引入函数指针数组,就可以解耦控制函数和各个命令的处理,使命令的增减不影响控制函数。
char *pcCommands = {
? "logon",
? "logoff",
? "setlogmode",
? "setrate",
};
#define COMMANDS_NUM?? (sizeof(pcCommands) / sizeof(pcCommands[0]))
typedef void (*Command_Handler_t)(char *pcCommand);
Command_Handler_t commandHandlers[] = {
? handleLogOn,
? handleLogOff,
? handleLogModeSetting,
? handleBaudrateSetting,
};
void handleLogCommands(char *pcACommand) {
? uint8_t ucStep;
? //Check the parameters.
? if(NULL == pcACommand)
??? return; //Nothing to do.
? for(ucStep = 0; ucStep < COMMANDS_NUM; ucStep++) {
??? if(0 == strcmp(pcCommands[ucStep], pcCommand, strlen(pcCommands[ucStep]))) {
????? commandHandlers[ucStep](pcACommand);
????? break;
??? }
? }
}
|
在以上的实现中,命令定义数组和命令处理的函数指针数组是分开的。在命令增加/删除的时候,需要修改这两个数组,并实现相应的命令处理函数即可,不需要修改控制函数handleLogCommands()。但在命令增删的时候,需要同时修改两个数组,容易造成两个数组不一致。比如从命令数组中已经删除了某条命令,但处理函数被保留了,导致程序执行错误。为了避免这种问题,我们可以将命令及其处理放在一个数组中,如以下代码所示:
typedef void (*Command_Handler_t)(char *pcCommand);
typedef struct {
? char?????????????????????????????? *pcCommand;
? Command_Handler_t? aHandler;
} Command_List_t;
Command_List_t? pCommandsList = {
? {"logon", handleLogOn },
? {"logoff", handleLogOff },
? {" setlogmode ", handleLogModeSetting },
? {" setrate ", handleBaudrateSetting },
};?
#define COMMANDS_NUM???? (sizeof(pCommandsList) / sizeof(pCommandsList[0]))
void handleLogCommands(char *pcACommand) {
? uint8_t ucStep;
? //Check the parameters.
? if(NULL == pcACommand)
??? return; //Nothing to do.
? for(ucStep = 0; ucStep < COMMANDS_NUM; ucStep++) {
??? if(0 == strcmp(pCommandsList [ucStep]. pcCommand, pcCommand, strlen(pCommandsList [ucStep]. pcCommand))) {
????? pCommandsList [ucStep]. aHandler(pcACommand);
????? break;
??? }
? }
}
|
通过定义一个由指向命令的指针与其处理函数构成结构,并用其定义一个结构数组,从而将命令和其处理函数捆绑在了一起。这样在增删命令的时候只需要改动一处,就不易出错。
作为一般的原则,互相关联、存在内在联系的元素(变量、函数等)应该通过定义结构,尽量放在一起。比如在上面的例子中那样,将其定义在一个结构中。这就可以保证命令和命令处理函数同时存在,而且保持一致。
通过以上的说明可以看到,通过引入函数指针数组,解除了控制函数与具体命令处理函数的耦合。不但提高了程序的可读性,也使系统的扩展非常方便。
|