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

Assembly 如何在8051汇编中编写有效的开关()?,assembly,embedded,8051,Assembly,Embedded,8051,如果我想在8051汇编中创建一个有限状态机,我需要一个高效的Cswitch()expression等价物 [对于这个问题,让我们忽略掉失败行为,保留和放弃都是可以接受的] 在8051汇编中有几种方法可以实现这一点,但每种方法都有其缺点。5-10箱的短开关足够简单,足够清晰,但如果我想要>128箱甚至>256箱的开关,事情就会变得复杂 第一种方法很简单,一系列的CJNE将操作数与值进行比较,如果不相等,则跳到下一种情况;相当于if(){…}else if(){…}else if(){…}。优点是简

如果我想在8051汇编中创建一个有限状态机,我需要一个高效的C
switch()
expression等价物

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

在8051汇编中有几种方法可以实现这一点,但每种方法都有其缺点。5-10箱的短开关足够简单,足够清晰,但如果我想要>128箱甚至>256箱的开关,事情就会变得复杂

第一种方法很简单,一系列的
CJNE
将操作数与值进行比较,如果不相等,则跳到下一种情况;相当于
if(){…}else if(){…}else if(){…}
。优点是简单,缺点是显而易见的:在长切换的情况下,这将创建一个非常长的选择字符串。 这可以通过构建二叉树来减少,对控制变量的连续位使用
JB
。这仍然不是很有效,很难维护,并且使得稀疏密钥集难以实现(`case1:…;case5:…;case23:…;case118:…)

下一种方法是相乘,然后跳转偏移。将控制变量乘以2,将结果加载到DPTR中,用偏移量加载累加器,然后在预加载了大量AJMP的区域执行
JMP@a+DPTR
。(或者乘以3,跳入预加载了大量
LJMP
的区域)

我做过一次。将跳转指令的位置调整到字节是一个我不想重复的难题,加上跳转表太大了(重复
ajmp
每个字节)。也许我不知道什么把戏可以让它变得简单

还有一种方法可以从专用表中提取地址,预加载堆栈并执行
RET
。它听起来非常整洁,除了从专用表中提取地址需要使用可怕的
MOVC a、@a+DPTR
mova、@a+PC
——这些寻址模式让我畏缩不前,我从未尝试过实现它。如果你知道一个简洁的方法,请把它作为一个答案


总的来说,我想知道是否有一种更优雅、更有效的方法来执行switch()式跳转—一种不产生太多开销、不浪费太多内存、至少不允许跳转距离的跳转方式,随着
案例的数量增加到数百个。

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

别这样!非常大的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编写,但概念是一样的

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

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

void HandleCharacter(unsigned char c)
{
    (*p[c])();
}
您可能会认为函数指针数组占用大量内存。如果它是const,那么它应该只占用FLASH,也不占用RAM,并且占用的FLASH应该比等效的switch语句少

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

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

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

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

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

不要用汇编语言写这种东西!你希望达到什么目标?年老

状态机(也许是for?)正是这种类型的东西。生成的代码实际上是无bug的,并且更易于维护。有好的免费的。代码生成器的输出通常是漂亮、方便的C。您可以将C提供给编译器。猜猜看,编译器知道你问题的答案。一个好的编译器将知道如何生成最有效的switch语句,它将继续进行下去,而不会浪费数周的时间调试它

另一件伟大的事情是,有一天,当您决定8051对您来说不够强大时,您可以轻松地转向更强大的体系结构,而不必从头开始完全重新编写和调试整个状态机!另外,你的余生都不会这样

增加:

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


或者,尝试一种方法。

看看编译器是如何做到这一点的!如果你选择一个认真的优化编译器,你会发现它非常复杂。你可以选择高效或优雅,但不能两者兼而有之。为什么不在这部分代码中使用C?具有真正不同选项的大型交换机是每个有限状态机的核心,在协作多任务处理中,或在没有实际操作系统的情况下编写的RTO的等效程序中,每个任务都是有限状态机。我知道如何查找表或参数执行;当您编写需要多个不同等待状态(或重量级循环)的大型代码时,禁止创建任何实际等待状态,或长时间占用资源,高效切换就变得至关重要。(OTOH,您可以通过加载带有实际状态过程地址的“state”变量来创建有限状态机,而不仅仅是它的标识符。这种方法有其自身的问题范围,但至少是地址