如何在8051汇编中编写高效的开关()?

SF.*_*SF. 7 embedded assembly 8051

如果我想在8051汇编中创建一个有限状态机,我需要一个有效的C switch()表达式.

[对于这个问题,让我们忽视掉落行为,保留和放弃都是可以接受的].

在8051程序集中有几种方法可以实现这一点,但每种方法都有其缺点.用于5-10个案例的短开关的那些很简单,并且足够清晰,但如果我想要一个> 128,甚至> 256个案例的开关,事情会变得复杂.

第一个是简单的,一个CJNE比较操作数和值的链,如果不相等则跳转到下一个案例; 相当于if(){...}else if(){....} else if(){...}.优点是简单,缺点很明显:如果是长开关,这将产生很长的选择.这可以通过构建二叉树来减少,使用JB控制变量的连续位.这仍然不是很有效,很难维护并且难以实现稀疏键设置(案例1:......;案例5:......;案例23:......;案例118:......)

接下来的方法是乘以,然后跳跃偏移.将控制变量乘以2,将结果加载到DPTR中,使用偏移量加载累加器,然后执行JMP @A+DPTR预加载多个的区域AJMP.(或乘以3并跳入预先加载了多个区域LJMP.).

我做过一次.调整跳转指令到字节的位置是一个我真的不想重复的难题,加上跳转表不必要地大(重复ajmp每个其他字节).也许我不知道一些让它变得容易的技巧......

还有从专用表中提取地址,预加载堆栈并执行的方法RET.听起来非常整洁,除了从专用表中拉出一个地址需要用可怕的MOVC A, @A+DPTR或者 MOV A, @A+PC- 这些寻址模式让我畏缩,我从未尝试过实施它.如果您知道这样做的简洁方法,请将其作为答案发布.

一般来说,我想知道是否有更优雅,更有效的方式来执行switch()样式跳转 - 一个不会产生太多开销,不会浪费太多内存并且可以自由跳跃至少AJMP距离,数量为case进入数百人.

Roc*_*net 6

我的第二个答案是你可能不喜欢的另一个答案.

不要在汇编程序中写这种东西!你有什么希望实现的?老年?

状态机(可能用于词法分析?)正是生成代码的那种东西.生成的代码实际上没有错误,并且容易维护.有很好的免费工具.代码生成器的输出通常很好,方便C.您将C提供给编译器.猜猜看,编译器知道你问题的答案.一个好的编译器将知道如何制作最有效的switch语句,并且它将继续使用它,而不会浪费你几周的生命调试它.

另一件好事是,有一天,当你决定8051不够强大时,你可以轻松地转向更强大的架构,而无需从头开始重新编写和调试整个状态机!此外,你将不会被困在8051s的余生中.

添加:

由于这不是词法分析,我建议您使用状态机编译器,如Ragel.您只需向其提供状态机的描述,它就会生成您的状态机C代码.

或者,尝试逻辑网格方法.

  • @SteveBarnes:'51没有gcc.:)但是,SDCC会起作用. (2认同)

Roc*_*net 4

我通常不喜欢这种类型的答案,但我觉得它在这里是合适的。

不要这样做!非常大的 switch 语句是一种代码味道。它表明您的代码中的某个地方发生了一些糟糕的规划,或者随着项目范围的扩大,一些原本良好的设计已经失去了控制。

当您可以选择几个真正不同的选项时,您应该使用 switch 语句。像这样:

void HandleCommand(unsigned char commadndByte)
{
switch (commandByte)
{
        case COMMAND_RESET:
            Reset();
            break;

        case COMMAND_SET_PARAMETERS:
            SetPID(commandValue[0], commandValue[1], commandValue[2]);
            ResetController();
            SendReply(PID_PARAMETERS_SET);
            break;

        default:
            SendReply(COMMAND_HAD_ERROR);
            break;
    }
Run Code Online (Sandbox Code Playgroud)

您的 switch 语句是否真的将程序流转移到数百个真正不同的选项?还是更像这样?

void HandleCharacter(unsigned char c)
{
switch (c)
    {
        case 'a':    c='A';    break;
        case 'b':    c='B';    break;
        case 'c':    c='C';    break;
        case 'd':    c='D';    break;
        ...
        case 'w':    c='W';    break;
        case 'x':    c='X';    break;
        case 'y':    c='Y';    break;
        case 'z':    c='Z';    break;
    }
}
Run Code Online (Sandbox Code Playgroud)

无论你在做什么,使用某种数组可能会做得更好。为了节省用汇编程序编写答案的时间,我将用 C 语言编写它,但概念是相同的。

如果切换的每种情况都涉及调用不同的函数,则:

const void (*p[256]) (int x, int y) = {myFunction0, myFunction1, ... myFunction255};

void HandleCharacter(unsigned char c)
{
    (*p[c])();
}
Run Code Online (Sandbox Code Playgroud)

您可能会说函数指针数组占用了大量内存。如果它是const,那么它应该只占用FLASH,而不占用RAM,并且应该比等效的switch语句占用更少的FLASH;

或者,类似的事情可能更相关。如果开关的每种情况都涉及分配不同的值,则:

char validResponses[256] = {INVALID, OK, OK, OK, PENDING, OK, INVALID, .... };

void HandleCharacter(unsigned char c)
{
    sendResponse(validResponses[c]);
}
Run Code Online (Sandbox Code Playgroud)

总之,尝试在 switch 语句中找到一种模式;什么变化了,什么保持不变?将变化的内容放入数组中,将保持不变的内容放入代码中。

  • 具有真正不同选项的大型开关是每个有限状态机的肉体,并且在协作多任务处理中,或者在没有实际操作系统的情况下编写的等效 RTOS 中,每个任务都是一个有限状态机。我知道如何查找表或参数执行;当您编写需要多个不同等待状态(或重量级循环)的大型代码并且禁止创建任何实际等待状态或长时间占用资源时,高效的切换就变得至关重要。 (3认同)