如何做 Perl 状态机(FSM)来解析比特流(字节序列)?

Posted

技术标签:

【中文标题】如何做 Perl 状态机(FSM)来解析比特流(字节序列)?【英文标题】:How to do Perl state machine (FSM) to parse bitstream (byte sequence)? 【发布时间】:2021-01-24 05:15:42 【问题描述】:

我目前正在使用 Perl 来解析来自 RS232 串行端口的传入命令序列。 我尝试使用状态机,它的预期行为是: (1) 从串口接收一系列字节; (2) 状态机将字节作为输入,并跳转到相应的状态。

我想出了一个简化的演示 Perl 代码(贴在下面),但遇到了一个问题: 代码进入“while(1)”时,卡在这里,出不来。 因此,$din 字节序列分配被“while(1)”阻止,并且对状态机不可见。 因此,FSM 卡在“INIT”状态,根本不跳转。

我认为这应该是 Perl 编码中非常简单或入门级的练习, 但通过谷歌搜索对我没有太大帮助。 谁能帮我这个? 先谢谢了~

...
my %next_state = (
        "INIT" => sub
                $din eq "AA" and return "HEADER0" ;
                return "INIT"                     ;
                ,
        "HEADER0" => sub
                $din eq "99" and return "HEADER1" ;
                return "INIT"                     ;
                ,
        ...
        );

# Set state machine's initial state.
my $cur_state = "INIT"   ;

# Integer for debugging purpose.
my $itgi = 0;

# Run the state machine.
while(1)
        $cur_state = $next_state$cur_state();
        print "$itgi, will jump to: $cur_state\n\n";
        $itgi++;
        

# Send in input byte sequence, which simulates
# incoming bytes from RS-232 COM port:
$din = "AA"     ;
sleep(1)        ;
...

========== 2020.10.09 22:10 更新 ==========

感谢@ikegami 经过一番努力和帮助 调试工作,现在我可以得到我可爱的 ​​Perl 状态了 机器启动并运行,代码如下所示。

但是还是有一个问题,就是:

输入字节序列(即@seq)必须是非0x00值; 如果我将 0x00 放入命令序列中,则 FSM 遇到 0x00 时会退出。

这是为什么?该代码使用“$cur_byte >= 0”,在我看来 应该能够像处理非零值一样处理 0x00。

为什么 0x00 会使状态机退出运行?

use strict      ;
use warnings    ;

# input to the state machine
my $din ;

#---------------------------------------------------------------------
# FSM's state table.
#---------------------------------------------------------------------
# Expected input sequence is:
#   AA 99 00 01 ....
# In which:
#   (1) Fixed pattern "AA" and "99" are two bytes of header,
#   (2) Following bytes are uart ID, etc.
my %next_state = (
        "INIT" => sub
                # If receives "AA" from input,
                # then jumpt to "HEADER0" state:
                $din eq "AA" and return "HEADER0" ; 
                # Otherwise just stay here:
                return "INIT"                     ; 
                ,
        "HEADER0" => sub
                # If receives "99" from input,
                # then proceed to "HEADER1" state:
                $din eq "99" and return "HEADER1" ; 
                # Otherwise, return to initial state:
                return "INIT"                     ; 
                ,
        "HEADER1" => sub
                # Capture first byte of uart ID:
                return "UARTID0";
                ,
        "UARTID0" => sub
                # Capture second byte of uart ID:
                return "UARTID1";
                ,
        "UARTID1" => sub
                # Capture second byte of uart ID:
                return "FINISHED";
                ,
        "FINISHED" => sub
                return "INIT";
                ,
        );

#---------------------------------------------------------------------
# Set state machine's initial state.
#---------------------------------------------------------------------
my $cur_state = "INIT"   ;

#---------------------------------------------------------------------
# Send in command sequence
#---------------------------------------------------------------------
my @seq = (-1, 0xAA, -1, 0x99, -1, 0x06, -1, 0x07,
           -1, 0x08, -1, 0x09, -1, 0x0a, -1, 0x0b,
           -1, 0x0c, -1, 0x0d
          );

sub get_next_byte 
        while (@seq)  #(A)
                my $cur_byte = shift(@seq);
                return $cur_byte if $cur_byte >= 0;
                #
                sleep(-$cur_byte);
                
        return (); #(B)
        

#---------------------------------------------------------------------
# Run the state machine.
#---------------------------------------------------------------------
# Integer for debugging purpose.
my $itgi = 0;

while( $din = get_next_byte() ) #(C)
        $din = sprintf("%02X",$din);
        $cur_state = $next_state$cur_state();
        print "-- Iteration $itgi, will jump to: $cur_state\n";
        $itgi++;
        

print "-- Program finish.\n";

【问题讨论】:

所以...代码在设置$din 变量之前进入while(1),因此它被锁定在INIT 状态。首先“Send in input byte sequence...”然后然后进入while...? 然而,这还不够,因为代码会反弹回INIT(因为$din99)。需要在 while 循环内,在 %next_state subs 的调用之间“喂”它。 为什么不输入while(<>) 并输入?或者在文件中有所需的输入,并使用该文件运行,prog filename。 “神奇的”<> 操作符从命令行中给出的所有文件中读取行,如果没有文件,则使用STDIN(这样您就可以输入您想要的内容并按回车键) 提示:没有理由使用字节的十六进制表示而不是字节本身。这是无用的额外工作。代替$din eq "AA",使用$din == 0xAA 我只想指出,我喜欢我们在这里遇到的人和问题的多样性。对于大多数人(包括我,而且我已经这样做了很长时间)来说,状态机绝不是入门级的。但是如果你做CS,它可能是。我们在 Perl 标记中得到的大多数问题是“我如何解析这行文本”或类似的问题。你的很有趣! :) 【参考方案1】:

您无需更改 $din 即可进入循环。你需要类似的东西

# Run the state machine.
while ( my ($din) = get_next_byte() ) 
   $din = sprintf("%02X", $din);
   $cur_state = $next_state$cur_state();
   print "$itgi, will jump to: $cur_state\n\n";
   $itgi++;

出于测试目的,您可以使用

my @seq = (-1, 0xAA, -1, 0x99);

sub get_next_byte 
   while (@seq) 
      my $next = shift(@seq);
      return $next if $next >= 0;
      sleep(-$next);
   

   return ();

【讨论】:

谢谢哥们,这似乎比我试图放入我的代码中的繁琐的 fork()、管道、AnyEvent 东西要整洁得多。我会看看我是否可以按照你的代码 sn-p 并通过它。 感谢好友@ikegami,我将您的代码合并到我的代码中,现在状态机开始运行,但传入的数据序列必须是非 0x00 值。如果我将 0x00 放入“@seq”,那么 FSM 将从这一点退出。我调试并重新检查了代码,但没有任何线索。对此有何提示? 你改变了while ( my ($din) = get_next_byte() )。好的,删除my 是有道理的,但您也删除了关键的括号。见Scalar vs List Assignment Operator 谢谢哥们,我去看看。没想到 Perl 有这么多细微差别,让人抓狂。 目前调试后的结论是:while( ($din) = get_next_byte() ) ...确实可以正常工作,而while( my ($din) = get_next_byte() )...会报错信息Use of uninitialized value $din in string eq at ... 指向$din eq "AA" 行。【参考方案2】:

感谢@zdim 和@ikegami 的帮助,现在我终于完成了这个程序。我将发布我的工作代码如下,以防有人可能有同样的问题。

以下代码受 zdim 启发:

use strict      ;
use warnings    ;

# input to the state machine
my $din ;

# FSM's state table.
# Expected input sequence is:
#   AA 99 00 01 ....
# In which:
#   "AA" and "99" are two bytes of header,
#   "00" and "01" are two bytes of uart ID.
my %next_state = (
        "INIT" => sub
                # If receives "AA" from input,
                # then jumpt to "HEADER0" state:
                $din eq "AA" and return "HEADER0" ;
                # Otherwise just stay here:
                return "INIT"                     ;
                ,
        "HEADER0" => sub
                # If receives "99" from input,
                # then proceed to "HEADER1" state:
                $din eq "99" and return "HEADER1" ;
                # Otherwise, return to initial state:
                return "INIT"                     ;
                ,
        "HEADER1" => sub
                # Capture first byte of uart ID:
                return "UARTID0";
                ,
        "UARTID0" => sub
                # Capture second byte of uart ID:
                return "UARTID1";
                ,
#        "UARTID1" => sub
#                return "FINISHED";
#                ,
        "FINISHED" => sub
                return "INIT";
                ,
        );

# Set state machine's initial state.
my $cur_state = "INIT"   ;

# Integer for debugging purpose.
my $itgi = 0;

# Run the state machine.
while($din = <>)
        chomp $din ;
        $cur_state = $next_state$cur_state();
        print "$itgi, will jump to: $cur_state\n\n";
        $itgi++;
        

# Send in input bytes:
$din = "AA"     ;
sleep(1)        ;
$din = "99"     ;
sleep(1)        ;

下面的 dode 是受 ikegami 启发的,注意($din) 和不带括号的简单$din 之间的区别:带括号,我们得到一个TRUE 或FALSE 的结果;没有括号,我们得到@seq的实际元素值,如果这个值为0x00,那么while将变为while(0)并退出

use strict      ;
use warnings    ;

# input to the state machine
my $din ;

#---------------------------------------------------------------------
# FSM's state table.
#---------------------------------------------------------------------
# Expected input sequence is:
#   AA 99 00 01 ....
# In which:
#   (1) Fixed pattern "AA" and "99" are two bytes of header,
#   (2) Following bytes are uart ID, etc.
my %next_state = (
        "INIT" => sub
                # If receives "AA" from input,
                # then jumpt to "HEADER0" state:
                $din eq "AA" and return "HEADER0" ; #(D)
                # Otherwise just stay here:
                return "INIT"                     ; 
                ,
        "HEADER0" => sub
                # If receives "99" from input,
                # then proceed to "HEADER1" state:
                $din eq "99" and return "HEADER1" ; 
                # Otherwise, return to initial state:
                return "INIT"                     ; 
                ,
        "HEADER1" => sub
                # Capture first byte of uart ID:
                return "UARTID0";
                ,
        "UARTID0" => sub
                # Capture second byte of uart ID:
                return "UARTID1";
                ,
        "UARTID1" => sub
                # Capture second byte of uart ID:
                return "FINISHED";
                ,
        "FINISHED" => sub
                return "INIT";
                ,
        );

#---------------------------------------------------------------------
# Set state machine's initial state.
#---------------------------------------------------------------------
my $cur_state = "INIT"   ;

#---------------------------------------------------------------------
# Send in command sequence
#---------------------------------------------------------------------
my @seq = (-1, 0xAA, -1, 0x99, -1, 0x00, -1, 0x00,
           -1, 0x00, -1, 0x00, -1, 0x0a, -1, 0x0b,
           -1, 0x0c, -1, 0x0d
           );

sub get_next_byte 
        while (@seq)  #(A)
                my $cur_byte = shift(@seq);
                return $cur_byte if $cur_byte >= 0;
                #
                sleep(-$cur_byte);
                
        return (); #(B)
        

#---------------------------------------------------------------------
# Run the state machine.
#---------------------------------------------------------------------
# Integer for debugging purpose.
my $itgi = 0;

##--while( my ($din) = get_next_byte() ) #(C)
    while(    ($din) = get_next_byte() ) #(C)
        $din = sprintf("%02X",$din);
        $cur_state = $next_state$cur_state();
        print "-- Iteration $itgi, will jump to: $cur_state\n";
        $itgi++;
        

print "-- Program finish.\n";

【讨论】:

以上是关于如何做 Perl 状态机(FSM)来解析比特流(字节序列)?的主要内容,如果未能解决你的问题,请参考以下文章

有限状态机FSM详解及其实现

Swift之深入解析如何在Swift中实现状态机

有限状态机FSM和层次状态机HSM

FSM(状态机)HFSM(分层状态机)BT(行为树)的区别

有限状态机FSM(Finite State Machine)及实现方式介绍

有限状态机FSM(Finite State Machine)及实现方式介绍