田舎の組み込みプログラマーがわざわざ趣味でも色々開発してみようとあがく様を綴るブログです。
DMM.makeのクリエイターズマーケットに出品しています。

Lチカ遷移2017年06月04日

またLチカかとお思いかもしれませんが、これで案外奥が深いのでちょっと掘り下げてみようと思います。

先日ご紹介したLチカの状態遷移ですが、実は点滅の切り替わりタイミングまでは考慮していませんでした。
が、考慮はしていなくとも遷移図を書いてしまうとそこにはタイミング的な仕様まで内包されてしまうのが恐ろしくもあります。

というわけで、以下の図をご覧ください。

第一形態のタイミング

LEDのコンディションには「消えている(C_LED_OFF)」と「点いている(C_LED_ON)」があり、点滅アクション指令(_UNIQUE)を受け取ると逆のコンディションの点滅状態に移行するようになっています。

LED_BLINK_OFFは点滅状態ではありますが、C_LED_OFFに属しているので点滅アクション指令を受け付けてLED_BLINK_ONに遷移します。
つまり、所定の時間今の状態をキープしてからLED_BLINK_ONに移ろうとしているのに、その前に遷移させられてしまうわけですね。

そんなわけで、点滅中に点滅指令を受けると点滅周期が乱れてしまうという事案が発生してしまいます。

このくらいなら

たいした問題じゃないと思われるかもしれませんが、深く考えていなかったことは事実ですし他人から指摘されると多分すごく悔しいと思いますので、そうなる前に修正しておきます。

そこで第二形態です。

Lチカ第二形態

第一形態では点滅中に点滅指令を受け付けてしまうことが事案の発生を招いていましたので、点滅中には点滅指令を受け付けないようにしただけの簡単なお仕事です。

そして、ソースはこうなります。

;==========================================================
PART$ LED_CONTROL
;----------------------------------------------------------
blink_timer:    .blkw    1
blink_limit:    .blkw    1
;==========================================================
    ; コンディション定義
    CONDITION$_DEFINE C_LED_OFF   ;[消えている]
    CONDITION$_DEFINE C_LED_ON    ;[点いている]
    ; ステータス定義
    STATE$_DEFINE LED_STATIC_OFF  ; 消灯<[消えている]
    STATE$_DEFINE LED_STATIC_ON   ; 点灯<[消えている]
    STATE$_DEFINE LED_BLINK_OFF   ; 点滅(OFF)<[点いている]
    STATE$_DEFINE LED_BLINK_ON    ; 点滅(ON)<[点いている]

    ;=======================================
    ;[消えている]
    ;=======================================
    CONDITION$ C_LED_OFF
    _ON
        TRANS LED_STATIC_ON
    _END

    ;=======================================
    ;[点いている]
    ;=======================================
    CONDITION$ C_LED_ON
    _OFF
        TRANS LED_STATIC_OFF
    _END

    ;---------------------------------------
    ; 消灯<[消えている]
    ;---------------------------------------
    STATE$ LED_STATIC_OFF, C_LED_OFF
        [ LED_PORT_BIT ] = OFF
    _UNIQUE
        TRANS LED_BLINK_ON
    _END

    ;---------------------------------------
    ; 点灯<[点いている]
    ;---------------------------------------
    STATE$ LED_STATIC_ON, C_LED_ON
        [ LED_PORT_BIT ] = ON
    _UNIQUE
        TRANS LED_BLINK_OFF
    _END

    ;---------------------------------------
    ; 点滅(OFF)<[消えている]
    ;---------------------------------------
    STATE$ LED_BLINK_OFF, C_LED_OFF
        [ LED_PORT_BIT ] = OFF
        [ blink_timer ].w = 0
    _OFF
        TRANS LED_STATIC_OFF
    _TIMER
        [ blink_timer ].w = [ blink_timer ].w + [ PART_PARAM ].w
        if [ blink_timer ].w >= [ blink_limit ].w
            TRANS LED_BLINK_ON
        endif
    _END

    ;---------------------------------------
    ; 点滅(ON)<[点いている]
    ;---------------------------------------
    STATE$ LED_BLINK_ON, C_LED_ON
        [ LED_PORT_BIT ] = ON
        [ blink_timer ].w = 0
    _ON
        TRANS LED_STATIC_ON
    _TIMER
        [ blink_timer ].w = [ blink_timer ].w + [ PART_PARAM ].w
        if [ blink_timer ].w >= [ blink_limit ].w
            TRANS LED_BLINK_OFF
        endif
    _END

;==========================================================
_INIT
    [ blink_limit ].w = 500
_SET
    [ blink_limit ].w = [ PART_PARAM ].w
;----------------------------------------------------------
_END
;==========================================================

状態遷移指向プログラミングだと、こんな風に状態遷移図の変更を簡単にコードに反映することができます。

さらに

点滅中に消灯指令と点灯指令を受けた場合についても考えてみます。

点滅からのオンオフタイミング

1は点滅でLEDが消えた直後に点灯指令を受けた場合ですが、これはこのままでよさそうですね。

問題は2ですが、点滅でLEDが点いた直後に消灯指令が来た場合、その回の点灯時間が終わるまで待ってあげた方がいいように思えますね。
点滅サイクルが異様に長かったりすると不都合に感じるかもしれませんが、そこまで長い周期では使わないという前提で設計変更をしてみます。

Lチカ第三形態

まずは単純に点灯サイクルの終了まで消灯に移行しないようにしてみました。
アクションの処理はステートがコンディションよりも先に行いますので、LED_BLINK_ONではLED_BLINK_AFTERに向かう_OFFアクションが有効となります。

LED_BLINK_AFTER(消灯遅延状態)には入場処理がなく、点滅タイマーの残り時間をそのまま受け継ぎ、カウントしていきます。
そしてカウントが終了したら消灯状態に遷移するのですが、その間に点灯または点滅指令を受け付けて遷移するようにしてやらないと仕様が破綻してしまいますので、遷移処理を入れてやります。

これでうまく動作するはずなんですが、よく見ると消灯遅延状態だけコンディションではなくステートで処理しているアクションが多くなっています。
それは、消灯遅延という状態は見かけ上はLEDが点灯しているものの、本質的なコンディションは消灯中だからなのです。

そこでもう一度本質的な部分まで立ち戻って考えてみると・・・。

Lチカ第四形態・・・

どうしてこうなった・・・ってくらいの変化かもしれません。

まぁ、着想が悪いと紆余曲折してしまうという典型ですね。
しかしながら、考え抜いた形がスッキリとした物になると何だか気分が良かったりもします。

そんなわけで

状態遷移分析では、タイミングも含めて要求される動作の本質が見えたり設計出来たりする一例をお目にかけられたと思うのですが、実は状態遷移に現れていないタイミング要件が残っていることにお気づきでしょうか?

それは、点滅中に_SETアクションで点滅時間が変更されてしまった場合です。
点滅時間は点滅中かどうかに関わらず常に変更可能で、そして時間のカウントとチェックは以下のようにしています。

        [ blink_timer ].w = [ blink_timer ].w + [ PART_PARAM ].w
        if [ blink_timer ].w >= [ blink_limit ].w
            TRANS LED_BLINK_OFF
        endif

ということは、新しく設定された[ blink_limit ]がその時の[ blink_timer ]よりも大きければすんなりと新しい周期に移行しますが、逆だと一度も指定していない周期で点滅の「点」か「滅」が行われてしまう可能性があるわけですね。

それを防ぐのは簡単で、要は「点」と「滅」のエントリー処理で[ blink_limit ]を取り込んでやればいいだけです。
そのためにもう一つ変数を用意する手もありますが、もっと簡単なのはダウンカウンターに変更することです。実例を書きましょう。

    ;---------------------------------------
    ; 点滅(ON)<[点灯中]
    ;---------------------------------------
    STATE$ LED_BLINK_ON, C_LED_ON
        [ LED_PORT_BIT ] = ON
        [ blink_timer ].w = [ blink_limit ].w
    _TIMER
        [ blink_timer ].w = [ blink_timer ].w - [ PART_PARAM ].w
        if Z || C == OFF
            TRANS LED_BLINK_OFF
        endif
    _END

こんな風に if文の中にフラグを書けるのも構造化アセンブラのお気に入りの点なのですが、引き算ではボロー(桁下がり)が発生した時に C が OFF になるということをど忘れしているとデバッグで悩むことになってしまいます。

カウント値を15bitに制限しておけば符号フラグでも判断できますが、引き算をする前に判断する方法もあります。

        if [ blink_timer ].w > [ PART_PARAM ].w
            [ blink_timer ].w = [ blink_timer ].w - [ PART_PARAM ].w
        else
            TRANS LED_BLINK_OFF
        endif

この方がぱっと見安心できるコードになりますが、ちょっとコスト高になったりもします。

というとで

今回はもっとアッサリした感じになるかと思っていたんですが、予想外の力作になりました。
ちょっと長くなってしまいましたので最終版のコードは割愛します。

では今回はこの辺で。

コメント

コメントをどうぞ

※メールアドレスとURLの入力は必須ではありません。 入力されたメールアドレスは記事に反映されず、ブログの管理者のみが参照できます。

※投稿には管理者が設定した質問に答える必要があります。

名前:
メールアドレス:
URL:
次の質問に答えてください:
日本標準時子午線の都市名をかたかなで

コメント:

トラックバック

このエントリのトラックバックURL: http://trident.asablo.jp/blog/2017/06/04/8585801/tb