2002/01/29 【ソフトウエア編TOPに戻る】
PICは、その名の由来の通り周辺装置とのインターフェイスに威力を発揮します。周辺装置や周辺デバイスの制御を行なう場合、シビアなタイミング調整や、こまめな待機時間などといったリアルタイム性が必要になることがありますが、PICをアセンブラでプログラミングすると、比較的簡単にその実行速度が計算できます。つまり、処理に何秒かかるのか、が簡単に予測できるのです。なので、特定の短い期間にどれくらいの処理を詰め込むことが出来るのかといった解析を行なったり、タイマー機能を使わずに正確な待機処理を簡単に実現するといったことが出来ます。
312−1.PICの命令実行プロセス
PICデバイスのデータシートには、「2ステージ命令のパイプラインによって、2サイクルを必要とするプログラム分岐を除き、全ての命令が1サイクルで実行可能です。」と書いてあります。はて、何のことやら?という感じですね。
PICの機械語命令は、全て14ビットを1ワードとする固定長になっています。そして、それがプログラムメモリ上のひとつの番地に格納される構造になっています。ひとつの命令が実行される場合、必ず以下の2サイクル分の処理が必要になります。
1.プログラムメモリからの命令の読み出し(フェッチサイクル)
2.読み出した命令に基づいた処理の実行(実行サイクル)
ここで1サイクルとは、PICに与えているクロックの4周期分の期間です。
ん?さきほど「...全ての命令が1サイクルで実行可能...」と言っていたのに、話が違う! と思われるかもしれませんが、ここで言う1サイクルとは「見かけ上」での早さのことなのです。
PICでは、ひとつの命令を実行している時に、裏で次の命令の読み出しを行なっています。つまり、命令を先読みすることによって、順次連続したサイクルで命令を続けて実行できるので、見かけ上は1サイクルの周期で1命令を実行するパフォーマンスが発揮できるのです。このような処理を「パイプライン処理」と呼びます。
ただし、ジャンプ命令や条件による分岐命令の場合は例外です。先読みは、プログラムカウンタで示されるアドレスの命令を次の命令とみなして行ないますが、ジャンプ命令や分岐命令の場合には、命令の実行結果で次のプログラムアドレスが決定されるため、せっかく先読みした命令が無効となってしまいます。なので、再度分岐先の命令を読み直す手間が必要になり、実行までに2サイクルの期間を要することになります。
どの命令が何サイクル必要なのかは、「302.PICの命令とレジスタ一覧」のページでご紹介していますので、そちらを参照して下さい。
312−2.実行速度の求め方
命令を実行する時間の単位として「サイクル」という言葉が出ました。では、1サイクルとは何秒なのでしょう?
PICのクロック4周期分が1サイクルになります。例えばクロックが10MHzの場合、以下のように求めます。
■1クロックの周期 = 1/クロック周波数 = 1/10MHz = 0.1μS
■1サイクルの時間 = 1クロックの周期 x 4 = 0.1μS x 4 = 0.4μS
なので、PICのクロック周波数によって1サイクルの実行に要する時間は変わります。例えばクロックを20MHzとすると、1サイクルは0.2μSになります。
では、プログラム上でのサイクルの数え方はどうなるのでしょう。下のプログラム例は、「310.PIOで入出力する」のページでご紹介したpiotes1A.asm のイニシャル処理部分です。
それぞれの命令の実行サイクルをコメント部分に書き込みました。
MAIN BSF
STATUS,RP0 ;1cycle |
上のプログラム部分では、全ての命令が1サイクルで実行されるので、合計で7サイクルとなります。つまり、クロック10MHzで動作させている場合、2.8μSで上の処理が終了することになります。
では、同じくpiotes1A.asmの続きの以下の部分はどうでしょう。
|
LOOP |
MOVF文とMOVWF文は1サイクルで実行されますが、GOTO文はジャンプ命令となるため実行に2サイクルが必要となります。なのでこの部分は合計で4サイクルの実行時間でループ処理されます。つまり、クロック10MHzで動作させる場合、1.6μSの周期で実行されることが分かります。
具体的には、PORTAのデータを読み取り、PORTBに出力するという動作を、1.6μSの周期で繰り返し行なっているということになります。
■クロック10MHzの1周期は0.1μSでした。余談ですが、この世で一番早い「光」は1秒間に地球を7周半すると言われています。では、0.1μSの時間で光はどれくらいの距離を進むことが出来るのでしょうか?答えは単純計算で30mです。上のループ処理1.6μSの時間でも480mしか進みません。μS(マイクロ秒)といってしまうとピンとこないかもしれませんが、実際にマイコンで実行できる処理速度というのは、それほど桁外れに高速なのです。
312−3.正確な時間稼ぎ処理
アセンブラブログラムの命令の種類と数が分かれば、その処理時間が計算できることをご紹介しました。では、今度はそれを利用して、必要な時間だけ待機する時間稼ぎプログラムをご紹介します。なお、これは後閑哲也氏著「電子工作のためのPIC活用ガイドブック」技術評論社のコラムで紹介されているサブルーチンプログラムを参考にしています。
正確な時間稼ぎをするためには、それに相当する時間を消費する命令を実行させれば良いということになります。つまり、サイクルの総数を必要な時間分に調整してあげれば良いのです。
以下のプログラムは、時間稼ぎ処理をWAITというサブルーチンにして独立させ、メインプログラムからCALL文で呼び出すようにしてあります。ここをクリックするとダウンロードできます。waittest.asm
|
;********************************************************************** BSF
STATUS,RP0 ;メモリーバンクを1にセット |
312−4.プログラム waittest.asmの説明
簡単に上記プログラムの説明をします。
■PIC16F84Aの設定
list p=16F84A
#include <p16F84A.inc>
__CONFIG _CP_OFF & _WDT_OFF & _PWRTE_ON & _HS_OSC
ここでは、list文でのプロセッサ指定と、include文でのレジスタ設定ファイルの読み込み、そしてPICライタの設定などをPIC16F84A用に設定しています。詳しくは前回までのページを参照して下さい。
■汎用レジスタの確保
TCNT EQU 0CH
ここでは、「TCNTという文字列は、以後0CHという数字として扱うこと」という宣言をしています。それをEQUという擬似命令で行なっています。擬似命令なので、具体的にPICに対して命令を出しているわけではなく、アセンブラが機械語に翻訳する際に、TCNTという文字列を0CHに置き換えるというだけのことです。
イメージとしては、TCNTという変数を宣言し、そのメモリ番地を0CHに確保したというような意味合いになります。
PICのデータメモリ領域には、汎用的にデータを一時保管することのできるレジスタ領域があります。PIC16F84Aの場合には0CHから4FHまでがそれにあたります。今回はその先頭アドレスである0CHを、TCNTという一時保管場所として使うということです。
■I/Oのイニシャル設定
ORG
00H ; processor reset vector
GOTO
MAIN ; go to beginning of program
MAIN
BSF STATUS,RP0
;メモリーバンクを1にセット
CLRF
TRISB
;TRISBをクリア PORT-Bを出力にセット
MOVLW
080H
;Wレジスタに80Hをセット
MOVWF
OPTION_REG ;OPTION_REGに80Hをセット PORT-B PULL UPなし
BCF
STATUS,RP0 ;メモリーバンクを0にセット
ここでは、リセットベクターとPIOポートのイニシャル設定を行なっています。ポートBを出力設定にして、プルアップ抵抗無しにしているだけです。詳しくは前回までのページを参照して下さい。
■メイン部分
LOOP
MOVLW
0AAH
;WレジスタにAAHをセット
MOVWF
PORTB
;PORTBにAAHを出力
CALL
WAIT
;サブルーチンWAITの呼び出し
MOVLW
055H
;Wレジスタに55Hをセット
MOVWF
PORTB
;PORTBに55Hを出力
CALL
WAIT
;サブルーチンWAITの呼び出し
GOTO
LOOP
;ラベルLOOPにジャンプ
ここが、PICの主な処理部分になります。やっていることは簡単で、ポートBにAAH(1010 1010)を出力してからCALL WAIT を実行し、次に55H(0101 0101)を出力してから再びCALL WAIT を実行する。という処理を永遠に繰り返しているだけです。
ここで、CALL とはサブルーチンプロブラムを呼び出すための命令です。ここではWAITというサブルーチンを呼び出しています。
■サブルーチンプログラム WAIT
WAIT
MOVLW
020H ;1
Wレジスタに20H(32)をセット
MOVWF
TCNT ;1
TCNT変数に20Hをセット
NOP
;1
TLOOP
DECFSZ
TCNT ;1x31+2
TCNT-1を行い0になったら次の命令スキップ
GOTO
TLOOP ;2x31
ラベルTLOOPに戻る
RETURN
;2 戻る
サブルーチンとはなんでしょう?聞きなれないと非常に変な響きで、20年近く前にはじめて聞いた時などは「中国語?」などと思ったりしたことも有りますが(力弥だけ?)、聞きなれてくると、サブ+ルーチンであることが分かります。(当然ですが...) つまり、メインの処理の流れとは別に、サブ的な独立した処理のことを言います。
あちこちで共通して使える汎用的な処理をサブルーチンとして独立させておくと、効率がよく、分かりやすいプログラムが組めます。N88-BASICなどの経験がある方にはお馴染みの概念です。C言語の関数という概念にも似ていますが、変数名などが完全に独立して扱えない点などが決定的に違うところです。
さて、サブルーチン WAIT の説明をしましょう。
変数TCNTに20Hという数字を書き込み、その後にGOTO TLOOPをTCNTで指定した回数繰り返して時間を潰し、RETURN命令でサブルーチンを抜けて、CALL命令で呼び出された元の場所に戻ります。
TCNTで指定された回数繰り返すという処理は、DECFSZ 命令で行なっています。ここではTCNTに格納されている数値から1を引き、0(ゼロ)でなかったら次の命令(ここではGOTO文)を実行し、0(ゼロ)だったら次の命令をNOP(何もしないという命令)に置き換えて実行します。つまり、TCNTが0になるまでGOTO TLOOPを実行し、Oになったら素通りしてRETURN文を実行することになります。
ここで行なっている処理は、時間潰し以外の何者でもありません。しかし、注目してもらいたいのは、このサブルーチンの総サイクル数です。MOVLW 0FFHから始まって、RETURN文が実行されるまで、ちょうど100サイクルに調整してあるのです。つまり、このサブルーチンの実行時間は、以下の通りになります。
クロック10MHzの場合 1サイクル0.4μS x 100 = 40μS
クロック20MHzの場合 1サイクル0.2μS x 100 = 20μS
なので、TCNTに書き込む値を加減することによって、待機時間を調整できるということになります。
312−5.TCNTの決め方
上の事例では、TCNTを20H(十進数で32)に設定して、ちょうど100サイクルの時間潰しに合わせました。では、このTCNTの値はどのようにして決めたら良いのでしょう。
サブルーチンWAITの基本形は以下のとおりです。上でご紹介しているWAITと比べると分かりますが、TLOOP の前のNOP命令がありません。これは後でご説明します。
WAIT
MOVLW
X
;1
WレジスタにXをセット
MOVWF
TCNT ;1
TCNT変数に20Hをセット
TLOOP
DECFSZ
TCNT ;1x(X-1)+2
TCNT-1を行い0になったら次の命令スキップ
GOTO
TLOOP ;2x(X-1)
ラベルTLOOPに戻る
RETURN
;2 戻る
これらの総サイクル数をYとすると、上の各命令のサイクル数の足し算から、以下の式が導けます。
Y = 1 + 1 + [1 x (X - 1) + 2] + [2 x (X - 1)] + 2
これを整理すると、
Y = 3X + 3
これをXについて解くと、
X = (Y - 3)/3
となります。
総サイクル数Yを100としたければ、X = (100 - 3)/3 = 97/3 = 32.333 となって割り切れません。また、X = 32 とした時のYは、Y = 3 x 32 + 3 = 99 となって、100サイクルまで1サイクルだけ足りなくなります。そこで、TLOOPラベルの前にNOP命令をひとつ追加して、ちょうど100サイクルにしているのです。
■もう少し詳しく...
DECFSZ
TCNT ;1x(X-1)+2
TCNT-1を行い0になったら次の命令スキップ
なぜこの命令部分のサイクル数が1 x (X - 1) + 2という回数になるでしょう?多分、一番分かりづらいところではないかと思います。
DECFSZ命令が必要とするサイクル数は1です。ただし、今回はGOTO TLOOPによって繰り返し実行されます。何回実行されるかというと、X - 1回です。X回目にはTCNTの値は0(ゼロ)となり、次のGOTO TLOOPをNOP命令に置き換えて実行します。つまり、X回目の0(ゼロ)の判定に1サイクル、NOP命令の実行に1サイクルの、合計2サイクルが消費されます。なので、1 x (X - 1) + 2です。分かりますか?
ですから、次の
GOTO
TLOOP ;2x(X-1)
ラベルTLOOPに戻る
も、X - 1回だけ実行され、各サイクル数が2なので、2x(X-1) ということになります。
今回の要点はサイクル数の数え方と、サブルーチンという考え方でした。サブルーチンは比較的単純な同じ処理を汎用的に使いたい場合に有効です。
WAITというサブルーチン名は、単純にサブルーチン部分の先頭アドレスにつけたラベル名のことです。サブルーチンを実行したいときに、その先頭アドレスにジャンプすれば良いわけですから、GOTO WAIT でもいいじゃん!と思われるかもしれませんが、それではいけません。CALL文で呼ばれたサブルーチンは、処理が終わったら元の場所に戻らせるためにRETURN文で締めくくります。つまり、GOTO文でジャンプしても元の場所に戻ることができないのです。メインとなる処理の合間にサブルーチンを織り交ぜるのですから、元のメインの処理に戻れないと意味がありませんよね。
今回ご紹介した時間稼ぎの方法は、人間の目で確認できるほどの長時間には不向きですが、0.1mS以下程度のタイトで正確な時間調整を手軽に行なうには向いています。周辺デバイスなどとのインターフェイスには便利だと思います。