在C语言中设计大量离散函数的方法

4

您好,

我正在寻找有关在C99中处理大量函数的设计模式的信息。

背景:
我正在为我的桌面数控铣床的宠物项目开发完整的G代码解释器。目前,命令通过串行接口发送到AVR微控制器。然后解析并执行这些命令以使铣头移动。一条典型的行示例如下:

N01 F5.0 G90 M48 G1 X1 Y2 Z3

其中G90、M48和G1是“操作”代码,F5.0、X1、Y2、Z3是参数(N01是可选的行号并被忽略)。目前解析工作正在顺利进行中,但现在是让机器实际移动的时候了。

对于每个G和M代码,需要执行特定的操作。这包括控制运动、冷却剂的激活/去活以及执行预设循环。为此,我的当前设计采用了一个函数,该函数使用开关来选择正确的函数并返回指向该函数的指针,然后可以在适当的时间调用各个代码的函数。

问题:
1)是否有比switch语句更好的方法将任意代码解析成其相应的函数?请注意,这是在微控制器上实现的,内存非常紧张(总共2K)。我考虑过查找表,但不幸的是,代码分布稀疏,导致大量浪费空间。大约有100个不同的代码和子代码。

2)当名称(可能还有签名)可能会更改时,如何在C中处理函数指针?如果函数签名不同,这是否可能?

3)假设函数具有相同的签名(这就是我的倾向所在),是否有一种方式可以定义通用类型的该签名,以便传递并从中调用?

对于问题的分散提问,我深表歉意。感谢您的帮助。


对于问题2,是的,签名需要相同。我会让更聪明的人回答其他问题。我不明白为什么你要返回一个函数指针而不是直接从开关中调用函数。 - prelic
函数指针是必需的,因为在解析整行之前我无法采取任何动作。更糟糕的是,我使用的特定G-Code标准规定命令和参数可以以任何顺序出现(除了行号)。因此,我必须按照标准规定的特定顺序解析整行,然后执行找到的内容。 - MysteryMoose
1
顺便提一下,你的小语言很像惠普使用的钢笔绘图仪(HP GL?)。它可能在ROM大小方面有相同的限制。 - wildplasser
为什么不使用内存更大的控制器?(我假设2KB是闪存。) - starblue
2K DRAM,32K Flash。微控制器是AVR Mega32,对于那些好奇的人来说。我试图避免在这个项目中只是扔更多的硬件,因为Mega64或Mega128会过度设计。 - MysteryMoose
3个回答

1

有没有比switch语句更好的方法?

列出所有有效的操作代码(作为程序内存中的常量,因此不会使用任何稀缺的RAM),并逐个将每个代码与接收到的代码进行比较。也许可以将索引“0”保留为“未知操作代码”。

例如:

// Warning: untested code.
typedef int (*ActionFunctionPointer)( int, int, char * );
struct parse_item{
    const char action_letter;
    const int action_number; // you might be able to get away with a single byte here, if none of your actions are above 255.
    // alas, http://reprap.org/wiki/G-code mentions a "M501" code.
    const ActionFunctionPointer action_function_pointer;
};
int m0_handler( int speed, int extrude_rate, char * message ){ // M0: Stop
    speed_x = 0; speed_y = 0; speed_z = 0; speed_e = 0;
}
int g4_handler ( int dwell_time, int extrude_rate, char * message ){ // G4: Dwell
    delay(dwell_time);
}
const struct parse_item parse_table[] = {
    { '\0', 0, unrecognized_action }  // special error-handler 
    { 'M', 0, m0_handler }, // M0: Stop
    // ...
    { 'G', 4, g4_handler }, // G4: Dwell
    { '\0', 0, unrecognized_action }  // special error-handler 
}
ActionFunctionPointer get_action_function_pointer( char * buffer ){
    char letter = get_letter( buffer );
    int action_number = get_number( buffer );
    int index = 0;
    ActionFunctionPointer f = 0;
    do{
        index++;
        if( (letter == parse_table[index].action_letter ) and
            (action_number == parse_table[index].action_number) ){
            f = parse_table[index].action_function_pointer;
        };
        if('\0' == parse_table[index].action_letter ){
            index = 0;
            f = unrecognized_action;
        };
    }while(0 == f);
    return f;
}

如何在C语言中使用函数指针,当名称(和可能的签名)可能会改变时?如果函数签名不同,这是否可行?
可以使用varargs在C语言中创建一个函数指针,该函数指针(在不同时间)指向具有更多或更少参数(不同签名)的函数。
或者,您可以通过向需要比其他函数少参数的函数添加“虚拟”参数来强制所有可能由该函数指针指向的函数都具有完全相同的参数和返回值(相同的签名)。
根据我的经验,“虚拟参数”方法似乎更容易理解,并且使用的内存比varargs方法少。
是否有一种方式将通用类型的typedef传递并从中调用该签名?
是的。我几乎见过所有使用函数指针的代码都创建了一个typedef来引用该特定类型的函数。(当然,除了{{link1:混淆竞赛条目}})。
请参考上面的示例和Wikibooks:C编程:函数指针以获取详细信息。

p.s.:你重复发明轮子的原因是什么?也许,以下现有的AVR G-code解释器之一可以对你有所帮助,也许需要稍作调整: FiveD, Sprinter, Marlin, Teacup Firmware, sjfw, Makerbot, 或者 Grbl? (请参见http://reprap.org/wiki/Comparison_of_RepRap_Firmwares)。


非常感谢您提供如此详细的答案。我重新发明轮子的原因有几个:1)为了学习;用一个非常常见的引语来概括:“了解一个人就是要走一英里路穿他的鞋”。2)作为对加法和减法过程差异的实验。3)为了证明自己是个书呆子。=P - MysteryMoose

1

我不是嵌入式系统的专家,但我有VLSI的经验。如果我说了显而易见的话,那我很抱歉。

函数指针方法可能是最好的方法。但你需要做以下两件事之一:

  1. 将所有操作代码按地址连续排列。
  2. 实现类似于普通处理器中的操作码解码器的操作码解码器。

第一种选项可能是更好的方法(简单且占用内存小)。但如果你无法控制你的操作代码,你将需要通过另一个查找表来实现解码器。

我不完全确定你所说的“函数签名”是什么意思。函数指针应该只是一个数字-编译器会解析它。

编辑: 无论如何,我认为两个查找表(一个用于函数指针,一个用于解码器)仍然比一个大型开关语句要小得多。对于不同的参数,使用“虚拟”参数使它们保持一致。我不确定将所有内容强制转换为void指针到结构体在嵌入式处理器上的后果会是什么。

编辑2: 实际上,如果操作码空间太大,就不能仅使用查找表来实现解码器。我在那里犯了一个错误。因此,选项1确实是唯一可行的选项。


顺便说一句,你的小语言类似于惠普使用钢笔绘图仪所做的事情。 (HP GL?)它可能在ROM大小方面有相同的限制。--糟糕,我在错误的地方发表了评论。抱歉! - wildplasser
请您能详细说明第一点吗?我不确定您所说的特定顺序排列函数是指函数地址查找表吗?至于函数签名的部分:如果您声明了一个指向特定函数的指针,那么如果您试图使用它来调用带有不同参数的函数,编译器会报错吗? - MysteryMoose
好的。如果你有100个动作代码,让这些代码的值为0-99。顺序无关紧要。这样,你就可以直接使用动作代码作为函数指针表的索引。 - Mysticial

1

1) 完美哈希可以用于将关键字映射到令牌号(操作码),这些令牌号可以用于索引函数指针表。所需参数的数量也可以放在此表中。

2) 您不希望使用重载/异构函数。可选参数可能是可能的。

3) 在我看来,您唯一的选择是使用可变参数。


如果词法分析器/标记生成器能够区分关键字和参数,那么你会更容易。例如:你的参数F5.0看起来像一个关键字(它以字母开头)。也许你应该将其拆分为两个单独的标记'F' + '5.0',其中'F'表示:将下一个标记视为'F'。顺便说一句:你考虑过基于堆栈的语言,比如RPN、Forth、Postscript吗? - wildplasser
目前,我的解析器区分动作代码和参数。语言中还有一些细节没有在最初的帖子中提到,例如 F(和其他一些参数)的行为类似于动作代码,但实际上并不是。在我的示例中,F参数将设置全局进给速度,这是持久性的,并且它发生在执行序列的非常特定的点上。但是,无论如何,它基本上归结为根据关键字采取行动,哈希表应该很好地完成这项工作。 - MysteryMoose

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接