ラベル 16F15325 の投稿を表示しています。 すべての投稿を表示
ラベル 16F15325 の投稿を表示しています。 すべての投稿を表示

2026年3月28日土曜日

LiDARを使った距離計(その3)

 実際にソフトを組むにあたって最初に問題となったのは通信速度だ。当初データシートの記述を頼りに 460800bps と考えていたが、本当にそうなのか?Copilotは違う値(115200bps)を第一候補として答えてくるが・・・?

TSD-10に電源を入れると基板面で微小なLEDが点滅を繰り返していた。どうも電源ONと同時に連続測定をスタートしているようだ。こういったセンサーはスタートコマンドを送るまで測定を始めないかと思っていたが違ったようだ。それなら測定データをUARTで連続送信しているだろうから、その信号で通信速度を確認すればよい。

オシロスコープでTSD-10のUARTの波形を観測すると1ビットが約2μsに相当するようで、デフォルトの通信速度は 460800bps で間違いないようだ(計算上は1ビットが2.17μsになる)。

そこでPIC16F15325の内蔵UARTを使って近い速度の信号を作ってみたが上手く行かない。内蔵の16MHzや32MHzのクロックを分周して合わせてみても速度誤差が8.5%出てしまう。UARTは多少の誤差は許容できるはずだが、ここまで違うと無理のようだ。そこで内蔵UARTを使わず、ソフトで直接ポートを駆動してUART信号を作る事にした。送信するデータの1ビットずつを取出し、それに従ってポートをON/OFFする。この方法で作った実際の波形を観測すると、オーバーヘッドが大きく1ビットに最低3μsほどかかり2.17μs迄は下がらなかった。そこで最後の手段として、プログラム中にビット出力命令を直接並べて(ハンドエンコードして)実行する事とした。試しに1/0を交互に出力させて観測するとビット出力命令の実行時間は0.124μs程度(Fosc/4に相当)なので2μs程度の遅延を挟めば丁度良い信号を生成できる。そうやって出来上がったプログラムの1バイト送信分が次のようなものである。

     // 1 - 0x5A
    LATAbits.LATA4  = 0;   _delay3(5);  // Start bit 
        LATAbits.LATA4  = 0;   _delay3(5);  // b0        
        LATAbits.LATA4  = 1;   _delay3(5);  // b1      
        LATAbits.LATA4  = 0;   _delay3(5);  // b2      
        LATAbits.LATA4  = 1;   _delay3(5);  // b3      
        LATAbits.LATA4  = 1;   _delay3(5);  // b4      
        LATAbits.LATA4  = 0;   _delay3(5);  // b5      
        LATAbits.LATA4  = 1;   _delay3(5);  // b6      
        LATAbits.LATA4  = 0;   _delay3(5);  // b7      
    LATAbits.LATA4  = 1;   _delay3(10);  // Stop bit     
 

Stop bitを長めにとっているのは次のバイトのStart bitまでの時間に余裕を持たせ多少の同期ずれを回復できるようにするためである。 このプログラムは上手く機能した。これで測定の停止(Stop)とボーレートの変更は出来るようになった。ビットレートを一旦9600bpsまで落とせば後はPIC内蔵UARTでも余裕をもってコマンドを送信できる。

もう1つ問題になったのはTSD10からのデータが全く受信出来なかった事である。色々テストした結果判明した原因は、受信に使ったPICのA5ポートが全く機能しなかった事である。以前同様なトラブルが別のPICであり、解決策も以前書いたが今回はそれでも上手く行かなかった。色々試したが結局諦めて代わりに基板を改造してC5ポートを受信に使う事で解決した。 

TSD10の仕様によると測定データは毎秒50回(20ms毎に)送られてきて、精度は±5cm、ブラインドゾーン(無感距離)が5cmある。また分解能(測定結果)がmm単位なので使いづらい。そこで複数回(とりあえず10回)の測定の平均をとり、最下桁を丸めてcm単位を結果とするようにした。

実際に周囲の対象物の距離を測定してみるとブラインドゾーンはほとんど無く、測定値には10cm位のオフセットがあるがほぼ直線に乗っている(ただ30cmところだけ原因不明のずれがある)。

オフセットについてはTDS10の個体毎に異なると推察されるのでコマンドで設定できるようにした。またデータシートによると反射の無い状態(測定範囲外)では65535mmが送られてくるよう記載されているが、実際にTSD10を空に向けてみると最大探知距離である10000mmが送られてきてデータシートと異なる。

超音波センサーとLiDARを比べてみると、 超音波センサーは測定値に少しのふらつきが見られることもあるがLiDARは比較的安定しているようだ。これは統計処理の違いに起因するかも知れなく、これが問題になる場合は更なる検証が必要だろう。

TSD10は指向性が鋭いうえに赤外線は目に見えないので、どこの距離を測定しているか分かり辛い。レーザーポインタを併用すれば分かり易くなるが、逆にレーザーポインタが明るすぎて目に辛い。幸いレーザポインタの消費電流は多くないのでPICで直接駆動できる。レーザーポインタをソフト的にON/OFF出来るようにすればもっと使い易くなるだろう。

さて、これを何に使おうか? 

(終り) 

 

2026年3月8日日曜日

LiDARを使った距離計(その2)

 

 修正した基板は次の通り。パスコンはSMD(表面実装部品)として基板の裏面へ移した。LiDARの赤外ペンシルビームはどこを指しているか分かりにくいので、J3としてレーザポインタを追加するための+5Vの分岐を追加した。

発注して1週間も経たずに修正版の基板が届いた。昨年まではSHENZHEN(深圳)から関空へ送られ国内配送されていたが、今年になって(理由は不明だが)SHENZHEN⇒香港⇒羽田という配送ルートに変わったようだ。到着が少し早くなったような気もする。

これを組み立てて、次の写真の様になった。左がLiDARインターフェイス、右が超音波測距センサーインターフェイスだ。超音波測距センサーの動作も確認できた。


ここからLiDARインターフェイスのソフトの制作について検討する。TSD-10のインターフェイスはRS-232Cだが交換するコードは文字ベースではなくバイナリーでNULL(0x00)を含む。転送速度がデフォルトの460,800bpsだと毎秒46,080バイト、22μs毎に1バイト転送され、これを処理する必要がある。結構シビアだ。転送速度の変更はコマンドで可能なので、初期化の段階で9600bpsまで速度を落としてみる事とした。

 ソフトの製作には時間がかかりそうなので、今回はとりあえずここまでで、次回へ続く。

(続く) 

 

2026年2月18日水曜日

LiDARを使った距離計(その1)

 超音波距離計は超音波センサーの前に膜など何らかの空気以外の物体があるとそこで音が反射されたり減衰したりして上手く計れない。そこでLiDAR距離計を試してみる事にした。LiDARは赤外線を使って反射物までの光の往復時間を測ることで距離を求める。1nsに光が進む距離は30cmだから往復で見ると15cm。1cmの分解能で距離を測るためには1/15nsの時間分解能が必要である。

秋月で売っているLiDAR距離計を見てみると、水平スキャンが出来るタイプと出来ないタイプの2種類あり、前者はロボット掃除機に使われているようなもので探知距離が1m程度と短い割に試すには高価である。そこで水平スキャン機能の無い単純なTSD-10というものを買った。これは最大10mまで計れるもので5m以内の精度は±5cm、それより以遠は1%である。大雑把に言うと1/3ns単位で時間を測定しているという事か。

 
指向性については超音波距離計の半値幅が30度あるのに対しTSD-10は3度しかないので ペンシルビームみたいな特性で、ターゲットの方向に正確に向けないと正しく計れない。

 TSD-10を使う上でもう1つ検討を要するのはUARTインターフェイスの通信速度である。デフォルトで460,800bpsと高速だ(これは9600bpsの48倍)。UARTを動作をさせるためには通信速度の16倍の周期のクロックが必要になる。460,800bpsの場合を計算してみると、14.7MHzのクロックが必要だ。PICの内蔵発信器で得られる周波数で最も近い周波数は16MHzなので8%の誤差がある。秋月では外付けの水晶で14.31818MHzの物を売っていて、これを使うと誤差は2.6%となる。内蔵発信器で上手く行かない場合は外付け水晶を使ってみるか。

使うPICの種別であるが、TDS-10とHOSTの両方ともUARTインターフェイスを使うので2つのUARTを内蔵して手頃なPIC16F15325を使うこととし、回路を設計すると共に基板を発注した。

なお、回路・基板の設計に当たっては、この基板を超音波距離計にもそのまま流用できるようピン配置の共通化を図っている。つまり14ピンのPIC16F15325を8ピンのPIC16F18313に挿し替えることでそのまま超音波距離計にも使えるようにした。

発注後、2日で基板は出来上がったが、その後春節と重なったため配送業者のピックアップが1週間ほど遅れている。以前と同じだ。結局、発注から半月ほどかかって届いた。 

届いた基板を組み立てると、とんでもない間違いに気づいた。センサーと接続するコネクタを逆向きに設計していた。これでは超音波センサーが後向きになってしまう。 そこで修正して再発注した。送料込みで3.3ドル、この安さがチェックを疎かにしてミスを誘発する原因かもしれない。なお上の回路図は修正版に挿し替えている。

(続く) 

2023年5月9日火曜日

赤外線で通信する 出入り検知器(まとめ)


 これまで出入り検知器を作ってきて、3作品目で一応納得できるものに仕上がった。

【送信部】

 送信部の回路には特に変更はない。

 【受信部】

受信部にはフォトリレーを追加して発声時以外はオーディオアンプの電源を切るようにした。その結果10mAあった通常時の消費電流を3.0~3.5mAまで低下できた。さらにフォトリレーを追加した結果、発声に伴って外部の回路を制御できるようになった。

 

【その後】

これまでの回路に気圧計とRTC(リアルタイムクロック)を追加した。

しかしそのままではPICの足が足りないので、代償としてLEDを廃止した。追加回路は2段目の基板に組み込んだ。

 気圧計のプログラムを書いているうちにメモリーをほぼ使い切ったのでRTCの処理まで進めなかった。PICをもう少し足の数とメモリー容量の大きいものに変える必要がある。またLEDを廃止した事で内部の状態が分かり辛くなったので、LEDも復活したい所である。


2023年4月29日土曜日

赤外線で通信する 出入り検知器(その3)

 【受信部】 

今回PICとボイスシンセサイザとの間をI2Cインターフェイスで繋いだ。I2Cインターフェイスをデフォルトのピン割り当てで使う場合は素直に使えるが、一方他のピンに割り当てる場合は少しややこしい。PIC16F15325のマニュアル15.3項のNoteによると特定のピン(RC3,RC4)だけがI2Cの入力レベル(スレッショルド)に対応しており、他のピンを使う場合はその設定の追加が必要。I2Cはデータとクロックの2つの信号を使うがPICでは2つの信号とも入力に初期設定するだけでなくスレッショルドの変更が必要であり、さらにPPSでは入力だけでなく(当然であるが)出力としても設定する必要がある。今回は以下のように記述した。

     // 12:RA1 - I2C CLK - Bidirectional
    TRISAbits.TRISA1 = 1;   // set Abit1 to input
    ANSELAbits.ANSA1 = 0;   // set Abit1 to digital
    INLVLAbits.INLVLA1 = 1; // ST level instead of TTL
    WPUAbits.WPUA1   = ON;  // weak pull-up
    RA1PPS          = 0x15; // SCK1/SCL1
    SSP1CLKPPS      = 0x01;
    
    // 13:RA0 - I2C DAT - Bidirectional
    TRISAbits.TRISA0 = 1;   // set Abit0 to intput
    ANSELAbits.ANSA0 = 0;   // set Abit0 to digital
    INLVLAbits.INLVLA0 = 1; // ST level instead of TTL
    WPUAbits.WPUA0   = ON;  // weak pull-up
    RA0PPS          = 0x16; // SD1/SDA1
    SSP1DATPPS      = 0x00;
 

このI2Cインターフェイスを使ったボイスシンセサイザとの通信は、一見上手く行ったように見えていたが暫く使っているとI2CのSCK(クロック)がLowになったままハングアップした。電源を入れなおしても同じだ。調べると同じような事例もあり、I2Cはソフト的には結構ややこしくロジックアナライザ無しではデバッグも容易でないようだ。よって、I2Cに代えてSPIインターフェイスを使うように変更した。SPIインターフェイスは構造が簡単で送受を同時に行うのが特徴。ソフト的な扱いも単純だが4本も信号線を使うので(最初は/SSをL固定し、それ以外の3本で通信を試みたが上手く機能しなかった)I2Cの2本と比べてPICの足数から自由度が減る事が残念だ。

 受信部のプログラムは1000行にもなるのでここでこれ以上掲載するのは止めて、そこで使っているロジックについて概要を解説する。

受信部の最初のポイントは、2つのIRセンサーで赤外線信号を定常的に受信出来ている状態は無情報であり信号欠落した事が情報(イベント)となる事である。これは5ms毎の割込みとソフト的なカウンタで処理することができる。

2つ目のポイントは 2つのIRセンサーの前をどう横切った時どう判断してどんなメッセージを流すか、またノイズをどう排除するかである。

このプログラミングには条件を細かく設定して分岐で処理しても良いが、そうするとプログラムがスパゲティ状態になり天才でもない限り行き詰まるか保守できなく事は目に見えている。こういう場合はステートマシンを使うのが早道である。ステートマシンでは「状態」と「イベント」を定義し、イベントの発生に伴う「アクション」と「状態の遷移」を表形式でプログラミングすることでロジックの「穴」を防ぐことができる。ステートマシンは有向グラフや状態遷移表で表す事が出来る。

 今回使うイベントは、
    C0)2つのセンサーが信号を受けている
    C1)センサー1が遮られた
    C2)センサー2が遮られた
    CB)センサー1及びセンサー2が遮られた
    TO)タイマーが設定した時間を経過した
の5つとした。また状態としてはシンプルに、
    IDL) 初期状態
    SYN) 人がいない(2つのセンサーでデータを受信)
    EN1)人がセンサー1の前に入った
    OV1)人が両方のセンサーの前まで進んだ
    EX1)人がセンサー1を通り過ぎた
    EN2)人がセンサー2の前に入った
    OV2)人が両方のセンサーの前まで進んだ
    EX2)人がセンサー2を通り過ぎた

とした。これを使って人一人の基本的な動きをトレースするためのステートマシンは次の状態遷移表になろう。この遷移表で実線で囲まれた各升目には3つの欄があり、それぞれある状態にあるときに、あるイベントを受けたときの メッセージタイマー操作次の状態への遷移を表している。 

このステートマシンはTimer0で5ms毎に実行されるようにしている。

IDLが初期状態で、2つの赤外センサー両方で赤外線を感じる(C0)とSYN状態に移行する。SYN状態が赤外線が遮られていない定常状態である。遷移表で灰色の網掛けは同じ状態に留まることを表している。

人が入店する場合は、
SYN→(C1)→EN1→(C3)→OV1→(C2)→EX1→(C0)→SYN
と移行するはずである。逆に人が退店する場合は、
SYN→(C2)→EN2→(C3)→OV2→(C1)→EX2→(C0)→SYN
と移行するはずである。単純に考えればEX1→(C0)の時に「いらっしゃいませ(WELCOM)」、逆に
EX2→(C0)の時に「ありがとうございました(THANK)」と発声すればよい。

しかし、事はそう単純ではなくこの表には空欄が多い。つまりまだ振る舞いが定義されてない条件がある事が分かる。空欄を全て埋めてステートマシンは完成する。例えばEN1-(C0)は人がセンサー1まで来て引き返した場合である。この時どう反応すべきか。またSYN-(CB)等のようにオレンジで網掛けしている欄は人一人の移動としてはあり得ない状態であり、たぶん二人の人がすれ違った場合、或いはセンサーが赤外リモコン等ノイズを拾った場合か、または送信部がいきなり停止した場合などが考えられる。このような場合にどう振舞えばよいか、このシステムへの要請に応じて欄を埋める必要がある。

これを実際に埋めてみたのが次の遷移表である。INITは電源を入れた時一度だけ通る状態であり、2つの赤外線を最初に感知したときチャイムを鳴らすために設けた。なお、このステートマシンは人が一人の場合は正しく判断できるが二人が絡むと結果は予測できない。

ここでT+2は2秒のタイマーを設定、Toffはタイマーを止める事を表す。赤色で示した欄はこの状態になることは無く何らかの内部エラーが発生した事を表すので動作としてはエラーと叫んでIDL状態に戻す事とした。またオレンジの欄も一人の移動では発生しない状態なので、「今日も良い天気」「大谷がんばれ」「大吉」など幾つか用意したメッセージをランダムに選んで発声するとともにIDLやSYN状態に戻す事とした。なお実際のメッセージは次の様になっている。
    HELLO        こんにちわ
    WELCOM    いらっしゃいませ
    THANK        ありがとうございました
    CHM            チャイム音
    ERR            エラー
    RDM         (ランダムなメッセージ)

このほか、「送信機の電圧が低下しました」、「受信機の電圧が低下しました」なども必要に応じて喋るようにしている。 

このステートマシンをプログラミングし実際に動かしてみたら上手く動いているようだ。センサーの前を移動したとき簡略化した状態遷移は次のようになった。

ID-S1-ET
ID-S2-e0-S2-e0-S2-e0-S2-e3-o2-eT
ID-S2-e0-S2-e0-S2-e0-S2-e0-S1-ET
ID-S1-E0-S2-e0-S1-E0-S1-E0-S2-e0-S1-E0-S2-e0-S1-E3-OT-O1-E0-S1-E3-O2-X0-S2-e3-o1-x0-S1-E3-O2-X0-S2-e3-o1-x0-S1-E3-OT-O1-E0-S1-E3-O2-X0-S2-e3-o1-x0-S1-E3-O2-X3-O2-X0-S2-e3-oT
ID-S2-e3-o1-x0-S1-E3-O2-X0-S3

ここでIDがIDL状態、S1がSYN状態で1番のイベントが発生した事を表す、EOXの各文字はそれぞれEN1,OV1,EX1、eoxの各文字はそれぞれEN2,OV2,EX2、また0123Tの各文字はそれぞれC1,C2,C3,TOのイベントを表す。

このシーケンスを見てセンサーの前で何が起きており、どういうメッセージが発声されているか分かるだろうか。例えば最初の ID-S1-ET は人が入口からセンサー1まで移動し、その状態で2秒経過したので「こんにちわ」と発声した事を表す。

なおステートマシンが5ms毎に動くのに比べ発声には時間がかかるので発声要求はキュー(FIFO)に入れて順次喋るようにしている。

  このようなプログラミングの答えは1つとは限らない。もっと複雑な動作をさせたければセンサーを増やしたり状態の数を増やすのも良いだろう。目的に合ったステートマシンを構築すれば今回のように一人ではなく二人の動作を追跡できるかもしれない。昨今流行のAIに作らせる/DEEP LEARNINGで学習させることも出来るかもしれない。

話は変わるが、最近PICが品薄のようである。 今回使った16F15325は秋月で在庫なし。そのほか多くのPICが在庫なしで入荷予定も立たないという事であった。EUSARTを2個内蔵したPICで手頃なものはこれしかないので困ったものである。


2023年4月8日土曜日

赤外線で通信する 出入り検知器(その1)

 PICを使った 赤外線の送受信及びタイマーと割込みの扱いが理解出来たので、これを応用して人の移動を検知してアクションを起こす簡易なセンサー(入退出検出器)を作ってみる。

【概要】

 人の出入りを検知するには焦電素子を使った人感センサーを先ず思いつくが、これは指向性や反応速度が緩いうえに焦電センサーは赤外線輻射量の変化を検知するので前を通る車や風にも反応し条件が整わないと上手く反応できない。超音波やマイクロ波を使ったレーダー式は周囲に物を置いた状態では検知しずらい。カメラの画像解析は良さそうだが高価になりそうという事で、光電管式ネズミ捕りと同様な仕組みをこれまで試した赤外線を使って作ってみる事とした。

 基本的なアイデアは次の通り。


 送信部では赤外線をある短い間隔で水平に発信する。受信部では水平に離れた2点で赤外線を受信し、人が赤外線を横切った場合の赤外線の断を検知して人の移動を判断し何らかのアクションを行う。

【検討】

 赤外線の遮蔽を的確に検知するには短いパルス状の信号を連続的に送る事が好ましいがリモコン受光部の特性から100msの周期中に25msの休止も必要。ノイズの影響を排除するためには偽信号をチェックするカラクリも欲しい。一般にUARTの信号にはパリティを付加できるがPICのEUSARTにパリティの機能は無い。但しデータに9ビット目を付加できるのでソフト的にパリティビットを実現することは可能であるが面倒、という事でビット反転させた2つのデータを連続して送る事で到達チェックすることにした。これで最短34ms毎の検出が可能となる(実際には約40msに設定した)。ちなみに本装置の受光部はTV等のリモコンの赤外線にも反応するが、このやり方で誤検知を防ぐことができる。

 人の移動速度はせいぜい時速4Km(秒速1.1m)とし、人の前後の厚みを25cmと仮定すると1つのセンサーの前を人が通過するのに必要な時間は227msとなるので送信部で40ms毎に信号を出すと受光部で最低5回のパルスの欠落が発生し探知が可能である。

 では2つのセンサーの間隔はどれくらい必要だろうか。出来れば1回でも人が両方のセンサーを同時に遮蔽することが望ましいので20cmでトライしてみよう。20cmの間隔を秒速1.1mで進むと1つ目のセンサーを通過した後2つ目のセンサーに到達するのに180msかかり、その間40ms毎のパルスを4回は遮る事になる。これで検知区間の単独の通過とその進行方向を検知できる。

 さて2つのセンサーで人の出入りを監視するロジックであるが、単に出入りのチェックだけやるのであれば簡単であるが、途中で引き返したり、真ん中に長時間留まって多少左右に動いて作業したり、TV等のリモコンの赤外線に干渉されたり、実用に使うには様々な問題がある。それらを含めてどれだけだけ正確に検知できるか、がソフト作りの腕の見せ所である。

 とりあえずこれまでの経験をもとに次のようなハードウェアを作成した。

【送信部】 

 送信部は次の回路とした。回路で新しく追加したのはA/D変換器を使ってバッテリー電圧を監視できるようにしたところ。バッテリー電圧が概ね2.7Vまで下がると電圧低下の警告が出るようにし、また送信信号にもその情報を入れるようにして受信側で警告が出せるようにした。PICを使うとバラで組むより配線をかなり簡略化できるのが嬉しい。

 次の写真が実際に秋月の汎用基板に組み立てたところ。黄色LEDは割込み処理中に点灯し、赤色LEDはバッテリー電圧低下(2.7V位)で点灯させる。LEDごとにシリーズ抵抗が違うが、黄色はデューティ比が低い一方赤色は連続点灯なので、LEDの明るさを見て暗めの適当な明るさに調整した結果である。この回路全体の消費電流は2.5mA。単2電池二本で数ヶ月持つ事を想定してる。電池3本(4.5Vでも問題なく使える)。

 その後PICを同じペリフェラルを持つ16F18313(8ピン)に替えたがピンの接続変更、及びソフトでのピンマッピング(PPS)の変更以外は何もせずそのまま動いた。

【受信部】

 受信部は次の回路とし、新しく音声合成ICとスピーカを鳴らすためのオーディオアンプを追加した。PICと音声合成ICのインターフェイスはUARTも使えるがPICの2組のEUSARTを赤外線入力に使って余裕が無かったためI2Cを使った。音声合成ICのUARTインターフェイスはPCと繋ぎPCから音声データの発声テストを出来るようにしている。JP3は人の入退室方向を反転させるためのジャンパー、JP4はPICのUSART2をデバッグ用インターフェイスとして使う場合にショートする予定だったがこれまでの所出番はなかった。

  この回路の消費電流は定常時で10mA程度であるが音声発声時に大きく増えるためUSBで電源供給するようにした。また電池駆動も考え、電源電圧が約4.5Vまで低下すると警告を出すようにした。

 次の写真が実際にこの回路を秋月の汎用基板に組み立てたところ。28ピンの音声合成ICが大きな面積を占有している。

なお、PICから制御信号を出す事で音声合成ICやオーディオアンプをスタンバイ状態において更に消費電力を減らす事も可能なようなので後で検証したい。

 
(その2)へ続く

 

2023年3月22日水曜日

赤外線で通信する(その5)眠りながら仕事をさせる

 その3,その4の処理ではメインプログラムの主要部分は、

     while(TRUE){
        if((r1state==RxSTOP)&&(nbuf < NBUF)){
            recvUART1(buf[rbufn],BUFLEN);
        }
        
    //途中省略
   
        if(t2state==TxCMPLT){
            tbufn++; tbufn %= NBUF;
            nbuf--;
            t2state = TxSTOP;
        }
    }

等のようにひたすらぐるぐる走り回って無駄に計算資源を消費する構造になっている。 しかし仕事が無いときはここでSLEEP()を実行してCPUを眠らせ省エネに努めたい。つまりOSのカーネルのアイドルプセスみたいに、

    while(TRUE){
       SLEEP();
    }

というメインプログラムとして、全ての処理を割込みの中で行いたい。 

 そう思ってメインループでいきなりSLEEP()を実行したがSleepモードに入るとPICは止まってしまって動かない。マニュアルを読むと、SleepモードにはSLEEPとIDLEの2種類があることが分かる。SLEEPではCPUのほか周辺装置(ペリフェラル)も止まり、IDLEではCPUは止まるがペリフェラルは動く(いずれの場合も割込みが発生するとSleepは解除される)。つまり何か入出力を行いながらCPUを眠らせるにはIDLEにする必要があるのだ。これを制御するのがCPUDOZEのIDLENビットだ。

 今回はEUSARTを使うのでSLEEP()を実行したときモードをIDLEにする必要があり、そのためには予め、

 CPUDOZEbits.IDLEN   = ON;     //IDLE mode when SLEEP()

としておく必要がある。ここで注意が必要なのはSleepモードに入りCPUが止まるときCPUクロック(Fosc)も止まる事である(実際にはCPUクロックを止める事でCPUを止めているのであろう)。ペリフェラルの中にはFosc或いはそれを四分周したFosc/4を同期をとるために使っているものがあり、その場合たとえSleepモードでIDLEになってもペリフェラルは止まってしまう。これを纏めると次表になる。

 EUSARTもFosc/4を使うペリフェラルの1つであり、これを止めないためにはEUSARTはFoscに依存しない非同期モードで動かす必要がある。

 今回のプログラムは、その4で作ったプログラムの一部を次の様に改修したものになる。先ずメインのループは上で述べた通り、

     while(TRUE){
       SLEEP();
    }

とする。そうするとCPUは割り込みが発生するまで何もせず眠る。割り込みが発生して一旦目覚めてもまたすぐSLEEP()を実行するので再び眠りにつく。つまり初期設定とペリフェラルの起動を終えたら何もしないメインプログラムである。

 次に元々メインループ内でやっていた部分をどう実行するかであるが、今回はタイマーで一定周期で割込みをかけてその処理の中で行う。2400bpsのシリアル通信を使うから1バイト送受するのに4ms必要である。ただし1バイト毎にメインルーチンで処理する必要はないのでとりあえず5ms毎にタイマー割込みをかけて、その割込み処理の中でメインループでやっていた処理を実行することとする(もっと短い時間間隔でも可能)。

PIC16F15325はTimer0~2の3つのタイマーを内蔵しており、各タイマーは各々性格が少し異なっている。ここではシンプルなTimer0を8ビット非同期モードで使って約5ms毎に定期的に割り込みを発生させる


タイマーの入力には31KHz(T=32.26μs)のLFINTOSC発振器を使い、156分周して5ms毎の割込みを発生させる。 ソフト的には次のように記述する。

    T0CON0          = 0x80;     //Enable, 8bit, postscaler=1:1
    T0CON1          = 0x90;     //LFINTOSC, Async, Prescaler=1:1
    TMR0H           = 156;      // tick for every 5ms
    PIR0bits.TMR0IF = OFF;
    PIE0bits.TMR0IE = ON;

割込み処理ルーチンにはタイマー割込みチェックを追加する。タイマーの割込みフラグは自動的にクリヤーされないので処理が終了した後ソフト的にクリヤーする必要がある。

 void __interrupt() isr(){
    if(PIR3bits.RC1IF && PIE3bits.RC1IE) recvISR1();
    if(PIR3bits.TX2IF && PIE3bits.TX2IE) sendISR2();
    if(PIE0bits.TMR0IE && PIR0bits.TMR0IF){
        tickISR();    PIR0bits.TMR0IF = 0;
    }
}

最後にメインループに相当する処理を5ms毎の割込みの中で処理するプログラムを作る(メインループ内から移植する)。

 void tickISR(){
    if((r1state==RxSTOP)&&(nbuf < NBUF)){
        recvUART1(buf[rbufn],BUFLEN);
    }

    if(r1state==RxCMPLT){
        nbuf++;
        rbufn++;  rbufn %= NBUF;    // next buffer
        r1state = RxSTOP;
    }

    if((t2state==TxSTOP) && (nbuf>0)){
        sendUART2(buf[tbufn]);
    }

    if(t2state==TxCMPLT){
        tbufn++; tbufn %= NBUF;
        nbuf--;
        t2state = TxSTOP;
    }

これでプログラムは完全割込みで動き始める。つまりCPUは殆ど眠っておりタイマーの5ms毎、及びEUSARTの割込みが発生したときだけ動くようになる。送信部のPIC16F18326も同じように使えるTimer0を内蔵しており、同じやり方に変更できる。

これを発展させるとOSのカーネルを作る事が出来るが、小さいPICでそこまでやるメリットも無いのでここまでに留めておく。

 

【参考】タイマーについて
 今回タイマーを扱った。PIC16F15325は(
ウオッチドッグタイマーなど特殊なものを除き)3つのタイマーを持っているが、各々性格が少し異なっている。いずれのタイマーも8or16ビットカウンタの前後にプエリスケーラとポストスケーラを持っている。プリスケーラは1/2^n形式の分周を行い、ポストスケーラは1/n形式の分周を行う。

Timer0は最も基本的なタイマーで全てのPICマイコンに実装され、8ビットモード時は設定した周期でフリーランのタイマーとして動作する。16ビットモードの時はタイムアップ毎にカウント値を設定しなおす必要がある。

Timer1は16ビットのタイマー/カウンタでクロックに対するゲート機能を備えておりゲート幅を調整することで複雑な動きが出来るようである。

Timer2は 8ビットのカウンタで、何らかの信号とトリガーとしてワンショット、或いはモノステーブルのタイマーとしても動作できるし、単なるフリーランのタイマーとしても使える

PIC16F18326はもっと多くのタイマーを内蔵しており、Timer0はPIC16F15325のTimer0と同じ、奇数番のタイマーはPIC16F15325のTimer1と、偶数番のタイマーはPIC16F15325のTimer2と似ているが機能が制約されている。例えば偶数番のタイマーはクロックソースがFosc/4に固定されているのでSleep状態では作動しないし、一方奇数番のタイマーはクロックソースやゲート信号の選択肢が4つしかない。

 


2023年3月7日火曜日

赤外線で通信する(その4)ソフトウェア(受信処理)編

受信部は、赤外線で送られてきたデータを赤外受信モジュール(OSRB38C9AA)で電気信号に変換しPIC16F15325のEUSART1で受信する。受信するだけでは何にもならないので受信したデータはEUSART2を使って送信し、これをPCで受けてTeraTermで内容を確認する。

メインプログラムは先ずEUSART1の受信割込みを起動する。EUSART1がデータを1バイト受信し割込みが発生すると受信レジスタから受信データを取出し、受信バッファに格納する。受信バッファが満杯になるか改行コードを受信すると受信を停止し受信完了をメインプログラムに通知する。

メインプログラムは受信状況を監視しており、受信が完了すると受信バッファを送信処理に渡し(これを送信バッファとする)EUSART2の送信割込みを起動する。USART2の送信割込みが発生すると送信バッファからデータを1バイト取出し送信レジスタに書き込む。このとき最後のデータを書き込んだ場合は送信割込みを停止し送信処理完了をメインプログラムに通知する。しかし実際にはこのとき最大2バイトが物理的に未送信で送信完了迄には更に最大8msが必要である事に注意が必要である。

 メインプログラムは3つのバッファを用意し、それらを順に使って処理を行う。つまり最初のバッファの受信が完了すると直ぐ送信に回し、同時に2つ目のバッファで受信処理を起動する。これを全3つのバッファを使って繰り返すことで疑似マルチタスク処理を行う。

送信スピードより受信スピードが速ければいずれバッファが不足し受信の取りこぼしが発生するが、受信赤外線回線は赤外線受信モジュールの特性で休止時間があるため最大効率75%だから今回は送受同速の通信でもデータが溢れることは無い筈である。

次図はPIC16F18326の割り込みロジックで、送信部で使ったPIC16F18326と同じと考えてよい。

受信部では2つのEUSARTを(同時に)使うため複数の要因で割込みが起こるが、割込みが発生した場合、1つのエントリーポイントが入口となるため、割込み処理ではどの要因で割込みが発生した識別してそれぞれ対応する処理を呼び出す必要がある。そのため割込みは次の様にする。

void __interrupt() isr(){    //割込み処理の入り口
    if(PIR1bits.RX1IF && PIE3bits.RC1IE)
                recvISR();  //UART1が受信済で割込み可なら受信処理

    if(PIR1bits.TX2IF && PIE3bits.TX2IE)
                sendISR();  //UART2が送信可
で割込み可なら送信処理
}

今回の様なフロー制御の効かない通信方式の場合、送信より受信を優先して処理するのは通信の基本である。また割込み処理中の割込み(多重割込み)は禁止されるが、送信処理の割込みでも受信データの有無が先にチェックされ、もし受信データが存在すると望まない受信処理が実行される事になるので割込み可かどうかのチェックは必須である(最初はこれが無くて上手く行かなかった)。

割り込み処理では次のような状態(state)を導入する。RxSTOPは待機状態、RxBUSYは受信中、RxCMPLTは受信完了を表す。

 typedef enum{ RxSTOP, RxBUSY, RxCMPLT }    rxstate;
 

作業領域は次の様にする。

rxstate     r1state = RxSTOP;   // RX1 state
char        *r1bufp;            // RX Buffer
int         r1buf_siz;          // remaining RX buffer size

上位プログラムは受信部がRxIDLEにある時受信を起動する事が出来る。この時パラメータとして受信データを格納するバッファの場所とその大きさを指定する。そして状態を受信中(RxBUSY)とし、割込みを許可する。

 void recvUART1(char *buf, int siz){
    r1bufp      = buf;
    r1buf_siz   = siz;
    r1state     = RxBUSY;
    PIE3bits.RC1IE  = ON;
}

データが受信され割込みが起こると次の受信処理が実行される。

 void recvISR1(void){
    byte st = RC1STA;            // read status 1st
    char c  = RC1REG;            // read data
    if(st & 0x02){               // if overrun error
        RC1STAbits.CREN = OFF;     // clear CREN bit once
        RC1STAbits.CREN = ON;
    }
    *r1bufp++ = (st & 0x06) ? '?' : c; //if any error chg char to '?'
    r1buf_siz--;
    if((r1buf_siz<=0)||(c=='\n')){  // completion
        PIE3bits.RC1IE  = OFF;
        *r1bufp = 0;
        r1state = RxCMPLT;
        LED1(OFF);
    }
}
先ず受信状態(RxBUSY)である事を確認した後、受信データと受信ステータスを取り込み、エラーがあれば然るべく回復処理を行い、正常であればバッファにデータを格納する(エラーの場合は'?'を入れる)。もし改行を受信するかバッファが満杯になれば割り込みを停止しバッファに文字列の終端(0)を書き込み、状態を受信完了(RxCMPLT)として上位プログラムに知らせる。

プログラムを頻繁にいじってPICに書き込むが、外から中身が分からないのでスタート時にバージョン等を出力するようにした。次は先に受信部を起動し、次に送信部を起動したときのTeraTermの表示である。


次の写真は赤外通信状態にある送信部(左)、受信部(右)である。受信部では4つのLEDで内部状態を示すようにしてデバッグを行った。

    LED1 - 受信がBUSY状態にある
   
LED2 - 割り込み処理を実施中
   
LED3 - 2つ以上のバッファに受信済データが入っている
   
LED4 - 送信がBUSY状態にある


  参考までに3月7日時点のソースコード(約300行)を以下に示す。

 /*
 * File:   main.c
 * Author: MYcrosLip
 * Created on 2023/02/24
 *
 * Recieve Message on IR using interrupt
 *
 * * Programmed for PIC16F15325
 */
/*
 * PIC16F15325 pin configuration
 * PIN & PORT Allocation
 *  5:RC5 - RX1 in
 *  6:RC4 - Tx1 oout
 *  2:RA5 - Rx2 in
 *  3:RA4 - Tx2 out
 * 10:RC0 - LED1
 *  9:RC1 - LED2
 *  8:RC2 - LED3
 *  7:RC3 - LED4
 */

#include <stdio.h>
#include <stdlib.h>
#include <xc.h>

#define _XTAL_FREQ 32000000

// config word 1
#pragma config RSTOSC   = HFINT32   //** Power-up Default Value for COSC
#pragma config FEXTOSC  = OFF       // FEXTOSC External Oscillator Mode Selection
#pragma config FCMEN    = OFF       // Fail-Safe Clock Monitor Enable
#pragma config CSWEN    = ON        // Clock Switch Enable
#pragma config CLKOUTEN = OFF       // Clock Out Enable
#pragma config PWRTE = OFF  // Power-up Timer Enable
#pragma config WDTE  = OFF  //** Watchdog Timer Enable
#pragma config LVP = OFF    //** Low Voltage Programming
#pragma config CP  = OFF    // Program Memory Code Protection

#define MYNAME          "IR-Rx-Intrpt-Basic"
#define VERSION         " 0.03"
#define DATE            " 2023/03/07"
#define COPYRIGHT1      " Copyright(C)2023"
#define COPYRIGHT2      " by MYcrosLip"
#define NL              "\n"

#define NBLINK          2       // startup blink sign

#define BUFLEN          30      // Buffer Length in byte
#define NBUF            3       // Number of Buffer

#define     ON      1
#define     OFF     0

typedef unsigned char         byte;
typedef unsigned int          uint;
typedef enum{TRUE=1, FALSE=0} boolean;


// Syntax Sugar; Dialect Adaptation Codes
#define RX1PPS          RX1DTPPS
#define RX2PPS          RX2DTPPS

// LED Macros
#define LED4(x)         LATCbits.LATC3 = x
#define LED3(x)         LATCbits.LATC2 = x
#define LED2(x)         LATCbits.LATC1 = x
#define LED1(x)         LATCbits.LATC0 = x

typedef enum{ RxSTOP, RxBUSY, RxCMPLT }    rxstate;
typedef enum{ TxSTOP, TxBUSY, TxCMPLT }    txstate;

// PROTOTYPES
void Init_Clk(void);
void Initialize(void);
void blink(int);
void LEDs(byte);

void __interrupt() isr(void);
void resetUART1(void);
void recvUART1(char*, int);
void recvISR1(void);
void resetUART2(void);
void sendUART2(char*);
void sendISR2(void);

//========================================================
// Interrupt Dispatcher
//========================================================
void __interrupt() isr(){
    LED2(ON);
    if(PIR3bits.RC1IF && PIE3bits.RC1IE) recvISR1();
    if(PIR3bits.TX2IF && PIE3bits.TX2IE) sendISR2();
    LED2(OFF);
}

//========================================================
//      UART1 Handler
//========================================================
rxstate     r1state = RxSTOP;   // RX1 state
char        *r1bufp;            // RX Buffer
int         r1buf_siz;          // remaining RX buffer size
txstate     t1state = TxSTOP;   // TX1 state
char        *t1bufp;            // TX Buffer

void resetUART1(){
    byte    b;
    while(PIR3bits.RC1IF) b = RC1REG;
    RC1STAbits.CREN = OFF;     // clear CREN bit once
    RC1STAbits.CREN = ON;
    r1state = RxSTOP;
}
//========================================================
void recvUART1(char *buf, int siz){
    r1bufp      = buf;
    r1buf_siz   = siz;
    r1state     = RxBUSY;
    LED1(ON);
    PIE3bits.RC1IE  = ON;
}
//========================================================
void recvISR1(void){
    byte st = RC1STA;            // read status 1st
    char c  = RC1REG;            // read data
    if(st & 0x02){               // if overrun error
        RC1STAbits.CREN = OFF;     // clear CREN bit once
        RC1STAbits.CREN = ON;
    }
    *r1bufp++ = (st & 0x06) ? '?' : c ; //if any error chg char to '?'
    r1buf_siz--;
    if((r1buf_siz<=0)||(c=='\n')){  // completion
        PIE3bits.RC1IE  = OFF;
        *r1bufp = 0;
        r1state = RxCMPLT;
        LED1(OFF);
    }
}

//========================================================
//      UART2 Handler
//========================================================
rxstate     r2state = RxSTOP;   // RX2 state
char        *r2bufp;            // Recieve Buffer
int         r2buf_siz;          // remaining buffer size
txstate     t2state = TxSTOP;   // TX2 state
char        *t2bufp;            // Send Buffer

void resetUART2(){
    byte    b;
    while(PIR3bits.RC2IF) b = RC2REG;
    RC2STAbits.CREN = OFF;     // clear CREN bit once
    RC2STAbits.CREN = ON;
    t2state = TxSTOP;
}
//========================================================
// Initiator
void sendUART2(char *bufr){
    t2bufp            = bufr;
    t2state           = TxBUSY;
    LED4(ON);
    PIE3bits.TX2IE   = ON;
}
//========================================================
// Interrupt Service
void sendISR2(void){
    TX2REG = *t2bufp++;
    if(*t2bufp == 0){
        LED4(OFF);      
        PIE3bits.TX2IE = OFF;   // Dsiable Interrupt
        t2state = TxCMPLT;      // Mission Completed
    }
}

//=======================================================
// CONSTANTS
static char id[]   = NL NL MYNAME VERSION DATE COPYRIGHT1 COPYRIGHT2 NL;

// VARIABLES
char    buf[NBUF][BUFLEN+1];
int     rbufn = 0, tbufn = 0;   // recieve buffer & send buffer
int     nbuf = 0;               // number of filled buffers

//========================================================
// MAIN
//
int main() {
    __delay_ms(1000);   // 1 sec delay
    Init_Clk();
#undef  _XTAL_FREQ  
#define _XTAL_FREQ   8000000     //chg Fosc to 8MHz dor __delay_ms()

    Initialize();
    blink(NBLINK);      // Startup Salute
    
    // Reset serial ports
    resetUART1();
    resetUART2();
    
    rbufn = 0; tbufn = 0; nbuf = 0;

    PIE3            = 0;
    INTCONbits.PEIE = ON;
    INTCONbits.GIE  = ON;
    
    sendUART2(id);
    while(t2state!=TxCMPLT){};
    t2state = TxSTOP;
    
//MAIN LOOP
    while(TRUE){
        if((r1state==RxSTOP)&&(nbuf < NBUF)){
            recvUART1(buf[rbufn],BUFLEN);
        }
        
        if(r1state==RxCMPLT){
            nbuf++;
            rbufn++;  rbufn %= NBUF;    // next buffer
            r1state = RxSTOP;
        }
        
        LED3( nbuf>1 );
        
        if((t2state==TxSTOP) && (nbuf>0)){
            sendUART2(buf[tbufn]);
        }
        
        if(t2state==TxCMPLT){
            tbufn++; tbufn %= NBUF;
            nbuf--;
            t2state = TxSTOP;
        }
    }
    return (EXIT_SUCCESS);
}

void Init_Clk(){
// OSC: HFINTOSC=16MHz, Fosc=8MHz
    OSCCON1     = 0x61;     // New ClockSource is HIFNTOSC, Div by 2
    OSCEN       = 0x40;     // HFINTOSC OSC Manual Req Enable
    OSCFRQ      = 0x05;     // HFINTOSC is set to 16MHz
}

void Initialize(void){
// PIN-Periphral Selection
    // 5:RC5 - RX1 in
    TRISCbits.TRISC5 = 1;   // set Cbit5 to intput
    ANSELCbits.ANSC5 = 0;   // set Cbit5 to digital
    RX1PPS          = 0x15; // RC5 EUSART1
    // 6:RC4 - Tx1 oout
    TRISCbits.TRISC4 = 0;   // set Cbit4 to output
    ANSELCbits.ANSC4 = 0;   // set Cbit4 to digital
    RC4PPS          = 0x0F;  // TX1 EUSART

    // 2:RA5 - Rx2 in
    TRISAbits.TRISA5 = 1;   // set Abit5 to intput
    ANSELAbits.ANSA5 = 0;   // set Abit5 to digital
    RX2PPS          = 0x05; // RA5 EUSART2
    // 3:RA4 - Tx2 out
    TRISAbits.TRISA4 = 0;   // set Abit4 to output
    ANSELAbits.ANSA4 = 0;   // set Abit4 to digital
    RA4PPS          = 0x11;  // TX2 UASRT2

    //10:RC0 - LED1
    TRISCbits.TRISC0 = 0;   // set Cbit0 to output
    ANSELCbits.ANSC0 = 0;   // set Cbit0 to digital
    // 9:RC1 - LED2
    TRISCbits.TRISC1 = 0;   // set Cbit1 to output
    ANSELCbits.ANSC1 = 0;   // set Cbit1 to digital
    // 9:RC1 - LED3
    TRISCbits.TRISC2 = 0;   // set Cbit2 to output
    ANSELCbits.ANSC2 = 0;   // set Cbit2 to digital
    // 7:RC3 - LED4
    TRISCbits.TRISC3 = 0;   // set Cbit3 to output
    ANSELCbits.ANSC3 = 0;   // set Cbit3 to digital
    //
//EUART1 Tx : Fosc=8MHz
    TX1STA      = 0x24;     // Async mode, Tx enable, high speed
    RC1STA      = 0x90;     //  
    // Baud Rate Generator
    BAUD1CON    = 0x08;     // BRG16
    SP1BRGH     = 0x03;     // 1200: 1666(682) 2400:832(340) on SYNC=0,BRGH=1,BRG16=1
    SP1BRGL     = 0x40;
    
//EUART2 Tx : Fosc=8MHz
    TX2STA      = 0x24;     // Async mode, Tx enable, high speed
    RC2STA      = 0x90;     //  
    // Baud Rate Generator
    BAUD2CON    = 0x08;     // BRG16
    SP2BRGH     = 0x03;     // 1200: 1666(682) 2400:832(340) on SYNC=0,BRGH=1,BRG16=1
    SP2BRGL     = 0x40;
}

void blink(int n){
    for(int i=n; i>0; i--){
        LEDs(ON);   __delay_ms(300);
        LEDs(OFF);  __delay_ms(300);
    }
    __delay_ms(300);
}

void LEDs(byte f){
    LED1(f);    LED2(f);   LED3(f);    LED4(f);
}


 


 

 

 

 

2023年2月19日日曜日

赤外線で通信する(その2)受信部

 【受信部】

受信部は赤外線を受信モジュール(OSRB38C9AA)で37.9KHzの赤外線キャリヤを受信しPICのEUSARTの受信部に渡す。今回は次のステップを考えてPICにはEUSARTを2セット内蔵するPIC16F15325を選んだ。基本的な回路は次のようにしたがLEDを光らせる条件は後で決める。

ここで私が使った基板にはLEDを正論理で光らせるように予めパターンが作ってあったのでそれに従った回路とした(R1の接地側パターンが接地せず浮いていたのがワゴンセールの理由かな)。なお電源電圧が3Vしかないので青色LEDなど順方向電圧降下(Vf)が大きいLEDはこの回路では光らない。