田舎の組み込みプログラマーがわざわざ趣味でも色々開発してみようとあがく様を綴るブログです。
DMM.makeのクリエイターズマーケットに出品しています。
hx711の仕様を読み解く ― 2018年11月30日
先日、hx711の動作にミステリーな点があると書きましたが、結論から言うと、思い込みよる勘違いでした。
今回はその辺りについて書いてみようと思います。
やっちゃだめなやつ
今回の失敗例は、4個のhx711から同時に重量値を読み出そうとして、1つのシリアルクロック(PD_SCK)で4個まとめて処理する方法です。
つまり、4個のhx711が全てreadyになったら読み出しをかけていたんですが、その結果がこちら。
readyになるタイミングがバラバラなだけでなく、数周期の間readyにならないやつまで出てくる始末です。
4個全てがreadyにならないと読み出しをかけられないため、しばらく待っていたんですが、他の3個はその間一定周期で短時間だけhighになっています。
ここでようやくhx711のアウトプットレート(10SPS or 80SPS)の意味に思い至ったわけです。
そもそも
実はこれまでA/D変換は逐次比較型しか使ったことがなくて、変換は任意のタイミングで出来るものだと思っていました。
hx711の場合、24bitということは多分ΔΣ型で、あらためて調べてみると、常時変換をしているタイプでワンショットやマルチプレックスには向かないとのことでした。
常時変換しているということは、読み出しをかけなくてもずっと動いているということなので、その辺をロジアナで確認してみました。
ご覧のように、それぞれ一定周期で一瞬だけhighになっています。
highになっている間はbusyなので、この時にA/D変換を行っているわけですね。
なので、busyになる直前に読み出しをかけると、読み出し中にA/D変換が行われてしまうことになり、その時のDOUT出力はおかしなことになってしまうわけです。
今使っている4個だけでもA/D変換周期の個体差が結構あるため、徐々に互いのタイミングがずれてきて問題が発生したというオチでした。
というわけで
hx711を複数使うシンプルな方法は、それぞれにそれぞれのシリアルクロックで読み出しをかけてやることですね。
まぁ、横着せずに最初からそうしておけば余計な苦労をせずに済んだのですが、趣味ですのでこういうのもありかなと思います。
なんとなく理解が深まった気もしますしね。
本題に進む前に
今まで使っていたモジュールは推奨回路と違っていたり出力周期が変えられなかったりと気になる点が多かったので、他のモジュールを取り寄せてみました。
右のモジュールはコンデンサの容量は分かりませんが部品構成は推奨通りで、出力周期も変えられます。
あと、コンパクトで裏面がベタなのもポイントが高いですね。
せっかくなので周期設定を80SPSにしてみました。
いよいよ本題
配線とソフトを変更して実行した結果がこちらです。
ソフトは大がかりな変更をしましたが、珍しく一発で動いてかなり感動しました。
そしてそのソフトがこちら。
メインループで直に処理していますが、今回はスマートに構造体風に処理してみました。
一回のループで1パルスだけ処理するようにし、次のhx711に切り替えるようになっています。
一見何の言語か分からないかもしれませんが、R8Cの構造化アセンブラです。
マクロ機能が便利なので調子に乗って使いまくってしまっているのが、正体不明な感じに拍車をかけていますね。
ifとか++があったりしてC言語風でありながら、アセンブラですので普通にニーモニックが使えますし、マイコンのフラグも記述できます。
R8C自体もビット処理が使いやすかったり、アドレスレジスタ以外にもSBとかFBが相対アドレッシングに使えたりと、アセンブラでプログラミングするのが楽しいマイコンですね。
もう一つの方法
今回はやっていませんが、複数のhx711を使用するもう一つの方法として、複数のhx711に同一の外部クロックを供給してやる方法があります(たぶん)。
個々の内臓オシレータで動作している状態ではタイミングが徐々にズレていくのは当然と言えば当然なわけで、ならば、同一の外部クロックで動作させれば(たぶん)同期がとれるはずなわけです。
その際、外部クロックは最大20Mhzまで使え、RATE端子がhighの場合は20MHz÷138240で約145SPS、lowの場合は20MHz÷1105920で約18SPSとなります。
つまり、外部クロックを使う事で変換周期も変更できるということですね。
あともう一つ
データシートに「セトリング時間」という項目があり、A/D変換4回分の時間になっています。
A/D変換の場合、ステップ信号入力に対してデジタル出力が対応した値に収束するまでの時間ということで、例えばロードセルに100gの重りがスパンと乗ったとして、デジタル出力値がその100g相当の値になるまでには4回のA/D変換が必要というわけです。
ということで、今回はhx711の仕様を少しばかり読み解いてみました。
ゆっくりしていってみる ― 2018年06月13日
このところ仕事が色々大変だったり、仕事以外でもアレコレあったりしたので、ここらでちょっとゆっくりしてみたいと思います。
というわけで
ゆっ くりチップで遊んでみます。
なんかごっちゃりしていますが、PCのターミナルとR8Cマイコンをつなぎ、R8Cマイコンからゆっくりチップにしゃべらせる構成になっています。
具体的にはこんな感じです。
ゆっくりチップに対してローマ字風のコマンドを送ることで発声できます。
R8Cのソフトでは、受信割り込みでキューに入ったデータをメインループで横流ししています。
構造化アセンブラと当方オリジナルのマクロライブラリで記述していますので見慣れない感じだとは思いますが、だいたい理解していただけるのではないかと。
R8Cで仲介しなくてもPCのターミナルとゆっくりチップで直にやりとりもできるのですが(USB<->UART変換は必要)、次段への布石とい うことで。
さて
ゆっくりチップとのUARTによる通信にはいくつかのお約束があり、主なところはだいたい以下の通りです。
- ボーレートを認識してもらうため、最初に"?"を送信
- コマンドの最後にはキャリッジリターンをつける
- ">"が返ってくるまで次のコマンド送信は控える
ちなみに、発声中に次のコマンドを送ると"*"が返ってきます。
なので、その辺りの処理を実装すると、後の処理が楽になります。
$LOOP
; PC→ゆっくり
PROC$_CALL S0R$_GET ; 受信データ取得(→R0L)
for S == OFF
PROC$_CALL$_B YUKKURI_UART$_PUT, R0L
PROC$_CALL S0R$_GET ; 受信データ取得(→R0L)
next
; ゆっくり→PC
PROC$_CALL S2R$_GET ; 受信データ取得(→R0L)
for S == OFF
PROC$_CALL$_B YUKKURI_UART$_GET, R0L
PROC$_CALL S2R$_GET ; 受信データ取得(→R0L)
next
$END
;==========================================================
PART$ YUKKURI_UART
;----------------------------------------------------------
; メモリ確保
QUEUE$_B$_ALLOC yukkuri_send_queue, 128 ; 送信キュー確保
send_char: .blkb 1
;==========================================================
; コンディション定義
CONDITION$_DEFINE S_ENABLE ; [送信可能]
; ステータス定義
STATE$_DEFINE YUKKURI_BUSY ; ビジー<[]
STATE$_DEFINE YUKKURI_READY ; 送信受け付け<[送信可能]
;-----------------------------------
; ビジー<[]
;-----------------------------------
STATE$ YUKKURI_BUSY, 0
_GET
if [ PARAMETER ].b == '>'
TRANS YUKKURI_READY
endif
_END
;=======================================
;[送信可能]
;=======================================
CONDITION$ S_ENABLE
_PUT
QUEUE$_GET yukkuri_send_queue, [send_char]
for C ; キューデータあり
PROC$_CALL$_B S2T$_PUT, [send_char]
if [ send_char ] == 0dh
TRANS YUKKURI_BUSY
endif
QUEUE$_GET yukkuri_send_queue, [send_char]
next
_END
;-----------------------------------
; 送信受け付け<[送信可能]
;-----------------------------------
STATE$ YUKKURI_READY, S_ENABLE
REDIRECT _PUT
_END
_PUT
; 代替えキャラを元のコードに
R0L = [ PARAMETER ].b
if R0L == '!'
R0L = 27h
elif R0L == '$'
R0L = 0dh
endif
QUEUE$_PUT yukkuri_send_queue, R0L
THROW ; 後は STATE に投げる
_GET
PROC$_CALL$_B S0T$_PUT, [PARAMETER].b
THROW ; 後は STATE に投げる
_INIT
QUEUE$_INIT yukkuri_send_queue
PROC$_CALL$_B S2T$_PUT, "'?'" ; ゆっくりチップに同期コマンドを送る
;----------------------------------------------------------
_END
;==========================================================
といった感じです。
PART$というのは状態遷移処理を内蔵したオブジェクト的な物で、指定したメソッドをアクティブなSTATE$で処理するのが基本です。
メソッドの探査順はPART$→STATE$(アクティブ)→CONDITION$(アクティブ)となっており、対応メソッド処理が見つかった時点で打ち
切りますが、THROWで次段に投げることができ、REDIRECTで次段の別のメソッド処理に投げることができるため、非常にシンプルな記述が可能に
なっています。
という、投げっぱなしな感じで今回はここまでです。
ターミナルから指示してみる ― 2018年05月15日
せっかくシリアルの送受信ができるようになったので、ターミナルからコマンドを入力して何かをやらせてみたいですね。
なので、ここまでの流れでLチカをターミナルから実行してみることにしました。
イメージとしてはこんな感じです。
文字列を扱うのは面倒なのでコマンドは1文字とし、リターンキーで実行します。
"S"コマンドは数値を入力すると点滅時間の設定となり、コマンドのみでは現在の設定値を返すようにします。
そのため、LED_CONTROLには_GETアクションを追加しました。
そんなところで
いきなりソースです。
;==========================================================
PART$ TINY_TOKENIZER
;----------------------------------------------------------
ENUM$_SET 0
ENUM TYPE_CHAR
ENUM TYPE_NUM
ENUM TYPE_SPACE
ENUM TYPE_ENTER
ENUM TYPE_BS
ENUM TYPE_ESCAPE
C_BUFF_SIZE .equ 10
c_type: .blkb 1
c_index: .blkb 1
c_buff: .blkb C_BUFF_SIZE
;==========================================================
; コンディション定義
CONDITION$_DEFINE C_NANIKA ; [何か入ってる]
; ステータス定義
STATE$_DEFINE TKN_NULL ; 何もない
STATE$_DEFINE TKN_NUM ; 数値<[何か入ってる]
STATE$_DEFINE TKN_CHAR ; 文字<[何か入ってる]
;-----------------------------------
; 何もない
;-----------------------------------
STATE$ TKN_NULL, 0
; エントリー処理
[ c_index ] = 0
_PUT
if [ c_type ] == TYPE_CHAR
TRANS TKN_CHAR
elif [ c_type ] == TYPE_NUM
TRANS TKN_NUM
endif
_GO
; ヘラルドさんに実行を通達(命令が伝わっているかどうかは知らない)
PROC$_CALL HERALD$_GO
_BACK
; ヘラルドさんに後退を伝える(後退できるかどうかは知らない)
PROC$_CALL HERALD$_BACK
_ESCAPE
; ヘラルドさんに撤退を伝える(撤退できるかどうかは知らない)
PROC$_CALL HERALD$_ESCAPE
_END
;=======================================
;[何か入ってる]
;=======================================
CONDITION$ C_NANIKA
_GO
PROC$_CALL HERALD$_VALUE ; ヘラルドさんにトークンを与える
PROC$_CALL HERALD$_GO ; ヘラルドさんに実行を通達
TRANS TKN_NULL
_BACK ; BS 入力
[ c_index ] = -- [ c_index ]
if Z ; 何もなくなった
TRANS TKN_NULL
endif
_VALUE ; トークン抽出
PROC$_CALL HERALD$_VALUE ; ヘラルドさんにトークンを与える
TRANS TKN_NULL
_ESCAPE
TRANS TKN_NULL
_END
;-----------------------------------
; 数値<[何か入ってる]
;-----------------------------------
STATE$ TKN_NUM, C_NANIKA
_PUT
if [ c_type ] == TYPE_CHAR
REDIRECT _ESCAPE ; 俺はいらない
endif
_END
;-----------------------------------
; 文字<[何か入ってる]
;-----------------------------------
STATE$ TKN_CHAR, C_NANIKA
; 俺は知らない
_END
_PUT
; キャラクタータイプ判定
[ c_type ] = TYPE_ESCAPE
R0L = [ PARAMETER ].b
if R0L <= 'z'
; コードの大きい方から判定
if R0L >= 'a' ; 小文字
R0L = R0L - 20h ; →大文字
[ c_type ] = TYPE_CHAR
elif R0L > 'Z' ; Z より後ろ
; エスケープ扱い
elif R0L >= 'A' ; 大文字
[ c_type ] = TYPE_CHAR
elif R0L > '9' ; 9 より後ろ
; エスケープ扱い
elif R0L >= '0' ; 数字
[ c_type ] = TYPE_NUM
elif R0L == ' ' || R0L == 0ah || R0L == 09h ; SPC, LF, TAB
[ c_type ] = TYPE_SPACE
elif R0L == 0dh ; CR
[ c_type ] = TYPE_ENTER
elif R0L == 08h ; BS
[ c_type ] = TYPE_BS
endif
endif
; キャラクタータイプに応じて処理を指示
if [ c_type ] == TYPE_NUM || [ c_type ] == TYPE_CHAR
if [ c_index ] < C_BUFF_SIZE
mov.b c_index, A0
[ c_buff[A0]] = R0L
[ c_index ] = ++ [ c_index ]
THROW ; _PUT
else ; バッファオーバー
REDIRECT _ESCAPE
endif
elif [ c_type ] == TYPE_ENTER
REDIRECT _GO
elif [ c_type ] == TYPE_BS
REDIRECT _BACK
elif [ c_type ] == TYPE_SPACE
REDIRECT _VALUE
else
REDIRECT _ESCAPE
endif
;----------------------------------------------------------
_END
;==========================================================
;==========================================================
PART$ HERALD
;----------------------------------------------------------
;==========================================================
; コンディション定義
CONDITION$_DEFINE C_WAIT_PARAM ;[パラメータ待ち]
CONDITION$_DEFINE C_READY ;[準備完了]
; ステータス定義
STATE$_DEFINE H_WAIT ; 待機<[]
STATE$_DEFINE H_B_CYCLE ; 点滅周期<[パラメータ待ち]
STATE$_DEFINE H_ON_READY ; 点灯準備<[準備完了]
STATE$_DEFINE H_OFF_READY ; 消灯準備<[準備完了]
STATE$_DEFINE H_BLINK_READY ; 点滅準備<[準備完了]
STATE$_DEFINE H_B_CYCLE_READY ; 点滅周期<[準備完了]
;-----------------------------------
; 待機<[]
;-----------------------------------
STATE$ H_WAIT, 0
_VALUE ; トークン
R0L = [ c_buff ]
if R0L == 'O' ; LED 点灯指令
TRANS H_ON_READY ; 点灯準備<[準備完了]
elif R0L == 'F' ; LED 消灯指令
TRANS H_OFF_READY ; 消灯準備<[準備完了]
elif R0L == 'B' ; LED 点滅指令
TRANS H_BLINK_READY ; 点滅準備<[準備完了]
elif R0L == 'S' ; LED 点滅時間 設定/返答
TRANS H_B_CYCLE ; 点滅周期<[パラメータ待ち]
else
RET$_FALSE
endif
_END
;=======================================
;[パラメータ待ち]
;=======================================
CONDITION$ C_WAIT_PARAM
_BACK ; 後退
TRANS H_WAIT
_ESCAPE ; 撤退
TRANS H_WAIT
_END
;-----------------------------------
; 点滅周期<[パラメータ待ち]]
;-----------------------------------
STATE$ H_B_CYCLE, C_WAIT_PARAM
_VALUE ; パラメータ来た
CHAR2VAL$_CALC c_buff, [c_index].b ; 数値文字列→数値
TRANS H_B_CYCLE_READY
_GO ; パラメータなし
PROC$_CALL LED_CONTROL$_GET ; 現在の設定値→R0
VAL2CHAR$_UW R0 ; 符号なしでキャラクター変換
VAL2CHAR$_GET R0L ; 変換したキャラクターを順次受け取る
for R0L != -1
PROC$_CALL$_B S0T$_PUT, R0L ; シリアルにキャラクターを送る
VAL2CHAR$_GET R0L ; 変換したキャラクターを順次受け取る
next
PROC$_CALL$_B S0T$_PUT, 0dh ; [CR]
TRANS H_WAIT
_END
;=======================================
;[準備完了]
;=======================================
CONDITION$ C_READY
_VALUE ; もうパラメーターはいらない
TRANS H_WAIT
_BACK ; 後退
TRANS H_WAIT
_ESCAPE ; 撤退
TRANS H_WAIT
_END
;-----------------------------------
; 点灯準備<[準備完了]
;-----------------------------------
STATE$ H_ON_READY, C_READY
_GO
PROC$_CALL LED_CONTROL$_ON
TRANS H_WAIT
_END
;-----------------------------------
; 消灯準備<[準備完了]
;-----------------------------------
STATE$ H_OFF_READY, C_READY
_GO
PROC$_CALL LED_CONTROL$_OFF
TRANS H_WAIT
_END
;-----------------------------------
; 点滅準備<[準備完了]
;-----------------------------------
STATE$ H_BLINK_READY, C_READY
_GO
PROC$_CALL LED_CONTROL$_UNIQUE
TRANS H_WAIT
_END
;-----------------------------------
; 点滅周期<[準備完了]
;-----------------------------------
STATE$ H_B_CYCLE_READY, C_READY
_GO
CHAR2VAL$_GET$_W R0 ; パラメータの数値取得
PROC$_CALL$_W LED_CONTROL$_SET, R0
TRANS H_WAIT
_END
;----------------------------------------------------------
_END
;==========================================================
イメージでは解析器は1つにしていましたが、実際には2段階になっていて、この辺りは字句解析→構文解析のお約束ですね、多分。
呼び出し部分のソースは掲載していませんが、シリアルで受信した文字をTINY_TOKENIZERに_PUTすることで動作します。
TINY_TOKENIZERはパート部分でキャラクタータイプの判定だけを行い、文字か数字ならバッファに入れて、あとはアクティブステートに投げます。
その際、キャラクタータイプによって後に続く処理が限定されるので、処理をリダイレクトします。
例えば[CR]が来た場合は何かを実行することになりますので、アクティブステートに対して_GOを投げるといった具合です。
区切りとなる文字が入るとトークンが確定しますので、後処理であるHERALDに投げます。
変数渡しなのがちょっとアレですが、アセンブラですのでご容赦ください。
HERALDは受け取ったトークンが有効なコマンドなら対応する状態に遷移します。
で、受け取った指示とその時の状態によってやることが変わるわけで、状態遷移処理の真骨頂と言えると思います。
そしてお約束の
デバッガ画面です。
今回はソースコード量に比べて説明がアッサリ過ぎますが、この辺で。
シリアル カンタービレ ― 2018年05月12日
とても昔からあって、今でも便利に使い続けられている技術。
それは俗にシリアルとかターミナルとか呼ばれているやつです。
以前少しだけarduinoをいじってみたんですが、標準でターミナルが実装されいて、コマンドを送ったりデータを表示させたりといったことが簡単にできるのが印象的でした。
そんなわけで
随分以前に写真だけ公開した基板をようやく動かす時が来ました。
実は通電するのも初めてだったりします。
そして、まずはシリアル送信を試してみます。
$LOOP
; タイミング処理
btstc SYSTEM_CYCLE ; 1msec毎にONされる
if C
DIVIDING$_ regular_10, 10 ; 10分周
PROC$_CALL$_B S0T$_PUT, 30h
PROC$_CALL$_B S0T$_PUT, 31h
PROC$_CALL$_B S0T$_PUT, 32h
_$
endif
$END
; シリアル送信処理
S0T$ 1
QUEUE$_B$_ALLOC send_queue, 20 ; 送信キュー確保
_ITP ; 送信終了割り込み(レジスタバンク切り替え)
QUEUE$_GET send_queue, R0L
if C
WRITE$_B R0L
endif
_PUT
if [ ti_u0c1 ] ; 送信バッファ空
WRITE$_B [PARAMETER].b
else ; 送信中
; 送信データをキューに入れる(満杯の場合は空くまで待つ)
QUEUE$_PUT$_WAIT send_queue, [PARAMETER].b
endif
_INIT
QUEUE$_INIT send_queue
_END
10msec周期で3バイトのデータを送り続けるだけの簡単なお仕事です。
簡単と言いつつ、送信バッファが空なら直に送信し、そうでなければキューに入れるという小技も使っています。
そして、キューにいれておけば、送信終了割り込みで勝手に持って行ってくれるという寸法です。
結果はこんな感じ。
狙い通り、10msec周期できっちり送信されていますね。
ちなみに、通信速度設定は9600bpsです。
気分のほうが乗ってきたので
受信もやってみます。
パソコンのターミナルソフトから送ってみて・・・・・・・・・・・・・・・・・・・・・・・・・・受信できません。
一応、受信割り込みは1回かかるのですが、2回目以降は反応なし。
よく見るとフレームエラーが出ていましたが、速度・ストップビット・パリティの設定は間違っていないし、何故?
で、信号をよく見てみると・・・
信号は問題無さそうですが、電圧が3V少々ですね。
実は、使っているUSB-シリアル変換モジュールの出力(マイコン側では入力)が3.3V系なんですが、入力は5V対応なので5V電源のマイコンともそのまま接続できるということだったので気にしていませんでした。
しかし、R8C/xMの5V電源時のH入力閾値には届かないようですね。
受信割り込みはかかったので、オーバーシュート分が微妙に届いている感じなんだと思います。
そこでレベル変換です。
変換ICを乗せる程のことでもないので、トランジスタを使いました。
改めて信号を確認してみると、ちゃんと5V付近まで上がっています。
ソフトの動作はこちらです。
これでようやく準備が整い、あとはパソコンからコマンドを送って・・・と行きたいのですが、字句解析とかやったことがないので、どうなりますやら。
投げ技炸裂 ― 2018年05月07日
昨日の状態遷移図とソースコードには、よく見るとものすごい無駄があります。
それは、遷移先が違うだけで全く同じタイマー処理を3カ所でやっていることです。
そこで、今回はタイマー処理を一つにまとめてみます。
サブルーチン呼び出しなんかを使うとこれまでの苦労が台無しですので、オブジェクト指向風にいきます。
と言うわけで
まずは状態遷移図です。
外部からの指令はまず LED_CONTROL で受けますので、ここに _TIMER の処理を書いておけばアクティブなステートに関わりなくタイマー処理を行えます。
そして、タイムアップしたらおもむろに _TIMER を投げます。
で、アクティブなステートが受け取り可能なら受け取ってくれますし、そうでなければスルーされます。
最後にアクティブなコンディションに向かうわけですが、そこでも受け取ってもらえなければ投げっぱなしになってしまうわけですね。
今回の状態遷移図では _TIMER を受け取ることができるステートが3つあり、それ以外のステートとコンディションではスルーされます。
それではコーディングいきます
今回はパート処理の部分だけ掲載します。
;==========================================================
PART$ LED_CONTROL
;----------------------------------------------------------
blink_timer: .blkw 1
blink_limit: .blkw 1
;==========================================================
; コンディション定義
CONDITION$_DEFINE C_LED_OFF ;[消灯中]
CONDITION$_DEFINE C_LED_ON ;[点灯中]
CONDITION$_DEFINE C_LED_BLINK ;[点滅中]
; ステータス定義
STATE$_DEFINE LED_STATIC_OFF ; 消灯<[消灯中]
STATE$_DEFINE LED_OFF_DELAY ; 消灯遅延<[消灯中]
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 ; → 点灯<[点灯中]
_UNIQUE
TRANS LED_BLINK_ON ; → 点滅(ON)<[点滅中]
_END
;-----------------------------------
; 消灯<[消灯中]
;-----------------------------------
STATE$ LED_STATIC_OFF, C_LED_OFF
[ LED_PORT_BIT ] = OFF
_END
;-----------------------------------
; 消灯遅延<[消灯中]
;-----------------------------------
STATE$ LED_OFF_DELAY, C_LED_OFF
_TIMER
TRANS LED_STATIC_OFF ; → 消灯<[消灯中]
_END
;=======================================
;[点灯中]
;=======================================
CONDITION$ C_LED_ON
_OFF
TRANS LED_STATIC_OFF ; → 消灯<[消灯中]
_UNIQUE
TRANS LED_BLINK_OFF ; → 点滅(OFF)<[点滅中]
_END
;-----------------------------------
; 点灯<[点灯中]
;-----------------------------------
STATE$ LED_STATIC_ON, C_LED_ON
[ LED_PORT_BIT ] = ON
_END
;=======================================
;[点滅中]
;=======================================
CONDITION$ C_LED_BLINK
_OFF
NOP
TRANS LED_OFF_DELAY ; → 消灯遅延<[消灯中]
_ON
TRANS LED_STATIC_ON ; → 点灯<[点灯中]
_END
;-----------------------------------
; 点滅(OFF)<[点滅中]
;-----------------------------------
STATE$ LED_BLINK_OFF, C_LED_BLINK
[ LED_PORT_BIT ] = OFF
[ blink_timer ].w = [ blink_limit ].w
_TIMER
TRANS LED_BLINK_ON ; → 点滅(ON)<[点滅中]
_END
;-----------------------------------
; 点滅(ON)<[点滅中]
;-----------------------------------
STATE$ LED_BLINK_ON, C_LED_BLINK
[ LED_PORT_BIT ] = ON
[ blink_timer ].w = [ blink_limit ].w
_TIMER
TRANS LED_BLINK_OFF ; → 点滅(OFF)<[点滅中]
_END
;==========================================================
_TIMER
if [ blink_timer ].w > [ PARAMETER ].w
[ blink_timer ].w = [ blink_timer ].w - [ PARAMETER ].w
else ; タイムアップ
[ blink_timer ].w = 0 ; blink_limit が割り切れないときの保険
THROW ; おもむろに _TIMER を投げる
endif
_INIT
[ blink_limit ].w = 1000
_SET
[ blink_limit ].w = [ PARAMETER ].w
;----------------------------------------------------------
_END
;==========================================================
そう、投げるコマンドは "THROW" です。
このおかげでタイマー更新処理をまとめることができ、コードがスッキリします。
ただ、哀しいことにここまでやってもコードサイズはほとんど変わらなかったりします。
アセンブラなので元々効率がいいんですよね。
そんなこんなで
ようやくミドルウェア部分が出来上がってきましたが、まだ一つ重要な仕組みができていないのと、マイコン周辺機能処理の見直し、LCDなどの周辺機器ドライバやシリアル通信機能などなど、やることが山積みで、またしばらく更新が滞ってしまうかもしれません。
では、今回はこの辺で。
シェミュレータ炸裂 ― 2018年05月06日
またもや随分と放置してしまいました。
例によって思いっきりサボってみたり、水面下で色々あがいたりしていたんですが、その結果絞り出したのがこれです。
ご覧の通り、マイコンを載せた変換基板をエミュレータに繋げた物ですが、この状態でデバッガーを走らせることができます。
つまり、シミュレータ的に使えるエミュレータということで、「シェミュレータ」というわけですね。
エミュレータからの給電だけで動きますし、シミュレータと違ってタイマーなどの周辺機能も普通に使えますので、ちょっとしたコードを確認したいときに便利です。
そんなわけで
ちょっとしたコードを書いてみました。
.list OFF
.include CORE_BASE.inc
.include sfr_r832m.inc
.include R8C_3M.inc
.list ON
PART$_DEFINE LED_CONTROL
$CPU
PORT_BIT$_PULL P42 ; Vref不使用、プルアップ
XIN$_USE ; XIN使用
CLOCK$_EXT CLOCK_F1 ; CPU クロック 外部クロック入力、分周なし
; システム周期(タイマRA)
TRA$_TIMER_MODE C_f8 ; タイマモード f8 - 0.4usec
TRA$_PRE 250 ; プリスケーラ 0.1msec
TRA$_REG 10 ; TRAレジスタ 1.0msec
TRA$_START ; 初期化時にスタートする
$END
RAM$
FLAG$_ system_flags
FLAG$_ADD SYSTEM_CYCLE
FLAG$_ADD LED_PORT_BIT
_$
DIVIDING$_ALLOC regular_10 ; 分周処理定義
stop_timer: .blkw 1
_END
$SETUP ; arduino風
PROC$_CALL LED_CONTROL$_UNIQUE ; いきなり点滅
[ stop_timer ] = 230
$END
$LOOP ; arduino風
btstc SYSTEM_CYCLE ; 1msec毎にONされる(フラグ→C、クリア)
if C ; 1msec周期
DIVIDING$_ regular_10, 10 ; 10分周
PROC$_CALL$_W LED_CONTROL$_TIMER, 10 ; 10msec を通知
_ ; 処理分割
if [ stop_timer ]
[ stop_timer ] = -- [ stop_timer ]
if Z
PROC$_CALL LED_CONTROL$_OFF ; 点滅をやめる
endif
endif
_$
endif
$END
TRA$ 1
_ITP ; 割り込み処理
btsts SYSTEM_CYCLE ; フラグをCにコピーした後セット
if C ; フラグがクリアされていない
; 処理遅れ
endif
_END
;==========================================================
PART$ LED_CONTROL
;----------------------------------------------------------
blink_timer: .blkw 1
blink_limit: .blkw 1
;==========================================================
; コンディション定義
CONDITION$_DEFINE C_LED_OFF ;[消灯中]
CONDITION$_DEFINE C_LED_ON ;[点灯中]
CONDITION$_DEFINE C_LED_BLINK ;[点滅中]
; ステータス定義
STATE$_DEFINE LED_STATIC_OFF ; 消灯<[消灯中]
STATE$_DEFINE LED_OFF_DELAY ; 消灯遅延<[消灯中]
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 ; → 点灯<[点灯中]
_UNIQUE
TRANS LED_BLINK_ON ; → 点滅(ON)<[点滅中]
_END
;-----------------------------------
; 消灯<[消灯中]
;-----------------------------------
STATE$ LED_STATIC_OFF, C_LED_OFF
[ LED_PORT_BIT ] = OFF
_END
;-----------------------------------
; 消灯遅延<[消灯中]
;-----------------------------------
STATE$ LED_OFF_DELAY, C_LED_OFF
_TIMER
; ジャンクション●的な動作
if [ blink_timer ].w > [ PARAMETER ].w
; タイマー更新
[ blink_timer ].w = [ blink_timer ].w - [ PARAMETER ].w
else
; タイムアップ
TRANS LED_STATIC_OFF ; → 消灯<[消灯中]
endif
_END
;=======================================
;[点灯中]
;=======================================
CONDITION$ C_LED_ON
_OFF
TRANS LED_STATIC_OFF ; → 消灯<[消灯中]
_UNIQUE
TRANS LED_BLINK_OFF ; → 点滅(OFF)<[点滅中]
_END
;-----------------------------------
; 点灯<[点灯中]
;-----------------------------------
STATE$ LED_STATIC_ON, C_LED_ON
[ LED_PORT_BIT ] = ON
_END
;=======================================
;[点滅中]
;=======================================
CONDITION$ C_LED_BLINK
_OFF
NOP
TRANS LED_OFF_DELAY ; → 消灯遅延<[消灯中]
_ON
TRANS LED_STATIC_ON ; → 点灯<[点灯中]
_END
;-----------------------------------
; 点滅(OFF)<[点滅中]
;-----------------------------------
STATE$ LED_BLINK_OFF, C_LED_BLINK
[ LED_PORT_BIT ] = OFF
[ blink_timer ].w = [ blink_limit ].w
_TIMER
if [ blink_timer ].w > [ PARAMETER ].w
[ blink_timer ].w = [ blink_timer ].w - [ PARAMETER ].w
else
TRANS LED_BLINK_ON ; → 点滅(ON)<[点滅中]
endif
_END
;-----------------------------------
; 点滅(ON)<[点滅中]
;-----------------------------------
STATE$ LED_BLINK_ON, C_LED_BLINK
[ LED_PORT_BIT ] = ON
[ blink_timer ].w = [ blink_limit ].w
_TIMER
if [ blink_timer ].w > [ PARAMETER ].w
[ blink_timer ].w = [ blink_timer ].w - [ PARAMETER ].w
else
TRANS LED_BLINK_OFF ; → 点滅(OFF)<[点滅中]
endif
_END
;==========================================================
_INIT
[ blink_limit ].w = 1000
_SET
[ blink_limit ].w = [ PARAMETER ].w
;----------------------------------------------------------
_END
;==========================================================
$END$
.end
前回の最終形態の状態遷移図からコーディングしたもので、なんと、これがソースファイルの全体像なのです。
つまり、このまま動いてしまうわけで、スタートアップ処理とかその他もろもろの面倒なことはマクロの中でやってしまっているわけです。
ちなみに、このコードのプログラムセクションは988バイトです。
ほぼ定型的な処理をあちこちで展開しているせいで大きくなってしまっていますが、アセンブラの癖を悪用している都合上、色々難しいところではあります。
一応、コードを far 領域にも置けるように設計していますが、ピン数の少ない品種ではそこまでの領域がないのがつらいですね。
ぼやいていてもしかたないので
状態遷移図を再掲します。
外部からの指令によってLEDを点灯/消灯/点滅させるわけですが、[点滅]から[消灯]に移行するときにLEDが唐突に消えないよう、遅延タイマーを入れているのが特徴です。
今回のコードだと、点滅は2秒周期、つまり1秒点いて1秒消えるの繰り返しになっていて、点滅を終えるときも1秒点いてから消えるようにしています。
テストコードでは、スタート直後に点滅指令を出し、2.3秒後に消灯指令を出しています。
遅延処理がなければ2回目は0.3秒で消灯してしまいますが、遅延処理のおかげでちゃんと1秒後に消灯します。
実を言えば
前回の更新から一年近く経ってしまったのは、状態遷移処理の実装方法を煮詰めるだけでなく、分周処理の仕様で悩んでいたことが大きかったりします。
以前は別オブジェクト的な実装でしたが、最終的に今回のインライン的な扱いに決めました。
以下のような感じで"_"で分割して処理を書くことで、1回通る毎に1つだけ処理を実行し、次回には次の処理を実行します。
DIVIDING$_ regular_10, 10 ; 10分周('_'で分割された処理を切替実行)
処理1
_
処理2
_
処理3
・
・
・
_$
ここでは分割数を10にしているので10個までの処理を書けますが、10個に満たない分は何もせずに通過します。
つまり、1msec毎に通過する部分にこの処理を書いておけば10msecのタイマーを10個得たことになるわけですね。
メインループの負荷分散のための優れた方法だと自画自賛しておきます。
そんなわけで
今度こそちゃんとした物を完成させようと何度目になるか分からない誓いを立てたりしていますが、どうなることやら。
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に移ろうとしているのに、その前に遷移させられてしまうわけですね。
そんなわけで、点滅中に点滅指令を受けると点滅周期が乱れてしまうという事案が発生してしまいます。
このくらいなら
たいした問題じゃないと思われるかもしれませんが、深く考えていなかったことは事実ですし他人から指摘されると多分すごく悔しいと思いますので、そうなる前に修正しておきます。
そこで第二形態です。
第一形態では点滅中に点滅指令を受け付けてしまうことが事案の発生を招いていましたので、点滅中には点滅指令を受け付けないようにしただけの簡単なお仕事です。
そして、ソースはこうなります。
;==========================================================
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が点いた直後に消灯指令が来た場合、その回の点灯時間が終わるまで待ってあげた方がいいように思えますね。
点滅サイクルが異様に長かったりすると不都合に感じるかもしれませんが、そこまで長い周期では使わないという前提で設計変更をしてみます。
まずは単純に点灯サイクルの終了まで消灯に移行しないようにしてみました。
アクションの処理はステートがコンディションよりも先に行いますので、LED_BLINK_ONではLED_BLINK_AFTERに向かう_OFFアクションが有効となります。
LED_BLINK_AFTER(消灯遅延状態)には入場処理がなく、点滅タイマーの残り時間をそのまま受け継ぎ、カウントしていきます。
そしてカウントが終了したら消灯状態に遷移するのですが、その間に点灯または点滅指令を受け付けて遷移するようにしてやらないと仕様が破綻してしまいますので、遷移処理を入れてやります。
これでうまく動作するはずなんですが、よく見ると消灯遅延状態だけコンディションではなくステートで処理しているアクションが多くなっています。
それは、消灯遅延という状態は見かけ上はLEDが点灯しているものの、本質的なコンディションは消灯中だからなのです。
そこでもう一度本質的な部分まで立ち戻って考えてみると・・・。
どうしてこうなった・・・ってくらいの変化かもしれません。
まぁ、着想が悪いと紆余曲折してしまうという典型ですね。
しかしながら、考え抜いた形がスッキリとした物になると何だか気分が良かったりもします。
そんなわけで
状態遷移分析では、タイミングも含めて要求される動作の本質が見えたり設計出来たりする一例をお目にかけられたと思うのですが、実は状態遷移に現れていないタイミング要件が残っていることにお気づきでしょうか?
それは、点滅中に_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
この方がぱっと見安心できるコードになりますが、ちょっとコスト高になったりもします。
というとで
今回はもっとアッサリした感じになるかと思っていたんですが、予想外の力作になりました。
ちょっと長くなってしまいましたので最終版のコードは割愛します。
では今回はこの辺で。
状態遷移指向プログラミング ― 2017年05月31日
突然ですが
みなさん、状態遷移図は描いていますか?
そしてどんな風に実装していますか?
状態遷移とソースコードが違いすぎて悩んでいませんか?
当方も長年あれこれ試行錯誤してきたんですが、ようやく自分の求める形が見えてきましたのでちょっとお披露目してみます。
題して、状態遷移指向アクション駆動モジュールです。
勝手に改造
状態遷移図はこんな感じにアレンジしています。

件のモジュールをパート(PART)と呼ぶことにします。
パート内部には状態遷移処理が仕込んであり、アクションを指示することで動作します。
ステート(STATE)は複数のコンディション(CONDITION)を持つことができ、パート→アクティブステート→アクティブコンディションの
順にアクションを評価します。
対応するアクションがなければ何もしません。
ステートには入退場処理機能も持たせてあり、特に退場処理ができることで色々捗ります。
こんな感じで機能毎にパートを用意し、あとはサイクリック・エグゼクティブ処理で回していくのが基本形です。
これで解決できない問題は後々作り込んでいくということで。
そして、お約束の
Lチカをやってみます。まずは状態遷移図から。
ご覧のようにLEDのオン/オフと点滅ができる仕様です。
諸事情によりアクションは固定となっていますので、_BLINKなんて限定的なアクションを入れる余裕はなく、点滅は _UNIQUE(固有機能)アクションで実行するようにしています。
_TIMERアクション処理はジャンクションで書いてありますが、事情により合流ができませんので厳密にはジャンクションとは言えません。
チョイスについてはちゃんと実装する予定です。
では、いよいよソースコードに行きます。
;==========================================================
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
_UNIQUE
TRANS LED_BLINK_ON
_END
;=======================================
;[点灯中]
;=======================================
CONDITION$ C_LED_ON
_OFF
TRANS LED_STATIC_OFF
_UNIQUE
TRANS LED_BLINK_OFF
_END
;---------------------------------------
; 消灯<[消灯中]
;---------------------------------------
STATE$ LED_STATIC_OFF, C_LED_OFF
[ LED_PORT_BIT ] = OFF
_END
;---------------------------------------
; 点灯<[点灯中]
;---------------------------------------
STATE$ LED_STATIC_ON, C_LED_ON
[ LED_PORT_BIT ] = ON
_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 = 100
_SET
[ blink_limit ].w = [ PART_PARAM ].w
;----------------------------------------------------------
_END
;==========================================================
状態遷移図からかなり忠実に落とし込めていると思うのですが、どうでしょう?
そしてこれは実際に動くコードなのです。
こういうことができるのも構造化アセンブラの素晴らしいところだと、忘れずにヨイショしておきます。
さて、このソースでは変数[blink_limit]の初期値を100としていて、単位はmsecのつもりです。
この値は_SETアクションで変更することができ、_TIMERアクションでは呼び出し周期をパラメータとします。
つまり、10msec周期で_TIMERアクションを呼び出すならパラメータを10にするということです。
そうすることで、点滅時のLEDのオン時間とオフ時間がそれぞれ100msecとなる仕掛けです。
我ながら
独自の道を突き進んでる感がハンパない気がしますが、趣味ですのでこれでいいんです。
まぁ、仕事の方でも似たようなことをしてるんですけどね。
それでは今回はこの辺で。
最近のコメント