我如何在8051汇编中编写高效的switch()语句?

7
如果我想在8051汇编语言中创建有限状态机,我需要一个有效的等效C switch()表达式。
[对于这个问题,让我们忽略落空行为,保留和删除都是可以接受的]
有几种方法可以在8051汇编中实现这一点,但它们各自有其缺点。对于5-10个情况的短开关,它们足够简单、清晰,但如果我需要一个>128甚至>256个情况的开关,事情就会变得复杂。
第一种方法很简单,一系列用CJNE比较操作数的链,如果不相等,则跳转到下一个情况;相当于if(){...}else if(){....} else if(){...}。优点是简单,缺点是显而易见的:在长开关的情况下,这将创建一个非常长的选择字符串。通过构建二叉树,使用JB来连续比特位的控制变量可以减少这种情况。这仍然不太有效,非常难以维护,并且使得实现稀疏键集变得困难(例如case 1:...; case 5:...; case 23:...; case 118:...)
接下来的方法是乘法跳转偏移量。将控制变量乘以2,将结果加载到DPTR中,使用偏移量将累加器加载到先前预加载了大量AJMP的区域中,然后执行JMP @A+DPTR。 (或者将其乘以3并跳转到预先加载了大量LJMP的区域)。我曾经这样做过。调整跳转指令的位置到字节是一个难题,我不想重复它,而且跳转表过于庞大(每个其他字节都会重复ajmp)。也许我不知道一些可以使其更容易的技巧...
还有一种方法是从专用表中提取地址,预加载堆栈并执行RET。听起来非常整洁,除了需要使用可怕的MOVC A, @A+DPTRMOV A, @A+PC这些寻址模式,这些模式让我感到非常难过,我从未尝试过实现它们。如果您知道一种好的方法,请把它作为答案发布。
总的来说,我想知道是否有更优雅、更有效的方法来执行switch()风格的跳转——一种不会创建太多开销、不浪费太多内存并至少具有AJMP距离跳转自由度的方法,其中case的数量达到了数百个。

2
看看编译器是如何做的!如果你选择一个严肃的优化编译器,你会发现它非常复杂。你可以拥有高效或优雅,但不能两者兼备。为什么不在代码的这部分使用C语言呢? - Gilles 'SO- stop being evil'
2个回答

6

我的第二个答案可能不会让你喜欢。

不要用汇编语言写这种东西!你希望实现什么?老年?

状态机(也许是为了词法分析?)正是生成代码的应用场景。生成的代码几乎没有错误,而且更加易于维护。有很多好用的免费工具可用。代码生成器的输出通常是漂亮、方便的C代码。将C代码交给编译器。然后,编译器知道如何回答你的问题。一个好的编译器会知道如何生成最有效的switch语句,并且会自动进行调试,而不需要你浪费数周时间来进行调试。

另一个很好的事情是,有一天当您认为8051不够强大时,您可以轻松地转移到更强大的架构,而无需从头重新编写和调试整个状态机! 此外,您将不会终身困在使用8051中。
补充:
由于这不是词法分析,我建议您使用状态机编译器,例如Ragel。 您只需提供状态机描述,它即可生成状态机C代码。
或者,尝试逻辑网格方法。

1
词法分析?不是。状态机对于任何控制系统都是必不可少的。状态:开启阀门。可能的转换:阀门打开 - 充填;阀门超时 - 故障恢复;用户中止 - 将系统恢复到“安全”状态。同时,其他任务也在运行,因此我们不能只是等待其中一个结果状态而冻结。我可能会用梯形图语言来编写它,这些语言与汇编语言一样无法转移。 - SF.
通常我会用C或C++(针对“更大”的板子)来编写。但是我想要学习如何使用'51汇编语言编写代码,或者至少了解编译器是如何完成这个过程的。我不认为我会在'51上编写一个超过10个状态的有限状态机,但我希望这是因为我不需要而不是因为我做不到。 - SF.
@SF。- 很好。我很高兴这是情况。但我认为你刚刚回答了自己的问题... - Rocketmagnet
不,我想看到整洁的代码片段来实现我提出的第二和第三个选项(而不是我的屠杀),或者如果有更聪明的方法的话。 - SF.
2
@SteveBarnes:'51没有gcc编译器。但是,SDCC可以使用。 - SF.
显示剩余3条评论

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;
    }

您的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;
    }
}

无论你在做什么,使用某种类型的数组可能会使其更加高效。为了避免使用汇编语言编写答案,我将使用C语言编写,但概念是相同的。
如果每个switch的情况都涉及调用不同的函数,则:
const void (*p[256]) (int x, int y) = {myFunction0, myFunction1, ... myFunction255};

void HandleCharacter(unsigned char c)
{
    (*p[c])();
}

你可能会认为函数指针数组占用了大量内存。如果它是常量(const),那么它只应该占用FLASH而不是RAM,且应该比等价的switch语句占用更少的FLASH;

或者,类似以下内容可能更为相关。如果switch的每个case都涉及分配不同的值,则:

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

void HandleCharacter(unsigned char c)
{
    sendResponse(validResponses[c]);
}

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


3
每个有限状态机的核心是具备不同选项的大开关,而在协作多任务或无实际操作系统编写的RTOS中,每个任务都是一个有限状态机。我知道如何编写查找表或参数执行;但当你需要编写大量代码并且需要多个不同的等待状态(或重型循环),又禁止创建任何实际的等待状态或长时间占用资源时,高效的开关变得至关重要。 - SF.
另一方面,您可以通过将“状态”变量加载为实际状态过程的地址而不仅仅是其标识符来创建有限状态机。 这种方法有自己的问题范围,但至少可以跳过地址查找。 - SF.
@SF。- 请查看我的第二个答案。 - Rocketmagnet

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