2025年12月5日金曜日

超音波距離計(その5)

 基本的な機能は実現できたので、次に実際の利用に合うようプログラミングを行う。基本的なロジックは、

  1. センサー部から200ms毎に送られてくる測距データを表示する。 
  2. 7セグメントLEDは測距データを3桁の数字で表示する。但し超音波センサーの最大探知距離が 400cmなのでそれより大きな値が来たときはエラーとみなし3桁ともマイナス(-)を表示する。またセンサーがエコーを捉えられなかった場合は下線(_)を表示する。
  3. RGB-LEDは距離に応じた色を表示する。また距離が短い場合は点滅表示を行う。これは7セグメントLEDを瞬間的に見た場合に読み間違いの可能性がある為だ。
  4. センサー部からのデータが途切れた場合は7セグメントLEDはドットを表示し、RGB-LEDは消灯する。

RGB-LEDは各色8ビットの諧調つまり24ビットの表示能力を持っている。 3,ではこれについて最初に距離に応じた連続的な色変化を試したが、そうすると逆に変化が認識しずらい。また輝度が高い場合の明るさの変化も認識しずらい。そこで距離に従って色を決める事にした。5つのLEDは同じ色を表示させる。具体的には100cm以上は青、100cm~40cmは緑、それより近い場合は赤を表示。各色の諧調は8程度で、距離が短いほど指数関数的に明るくなるようにした。さらに70cm以内では距離応じて点滅、距離が近いほど早く点滅するようにした。また緑は輝度が高いので値を約半分に抑えるようにしている。

4,ではタイマーを使って監視し、センサーからの信号が2秒以上来なかった場合に途切れたと判断している。





とりあえず当初の機能目標は達成したので、これらを格納する箱探しを行った。

一応百均で大きさの合いそうな物を探した。表示部はディスプレイケースを加工したが下部の長さが不十分でコネクタを挿すとはみ出すため更なる工夫が必要だ。センサー部はスリムボックスを切断、底に21mmの穴を開けてセンサー用の窓とし倒して使うようにした。横にマグネットが付いているので場所を移動し、そのまま金属に付けられるようにしている。一方こういった箱に入れると若干感度が低下するようで(近距離の感度には問題ない)、回り込みを防ぐために吸音材を入れたりしたが効果はイマイチだった。

 


(終り) 


 



2025年12月2日火曜日

超音波距離計(その4)

 

 RGB-LED(PL-9823)は次のような特殊なPWMパルスを作って駆動する必要がある。このデータは信号が0→1になる所からスタートし、全てのビットを送り終える最後のビットの最後は0→1で終了する。であるなら全てのデータを送った後のアイドルの状態は0なのか1なのか? スタートの条件を考えれば0であるが図からは最後は1で終わるように見える。たぶん最後は0を延長したRETコードをアイドル状態として使うのだろうと推測する。

このパルスの厳密なタイミングには幾つかの情報があり曖昧であるが次の情報が確からしい。実際には動かして確かめる必要があるがHとLが概ね1:3あるいは3:1の比率であれば成立しそうだ。

またビットの並びはMSBファーストで、次の様にRGBの順だという資料もあるがGRBという資料もあり、これも実機で確かめる必要がある。

RGB-LEDはカスケード接続で複数を制御できる優れものである。今回はこれを5個を使う。

データの転送時間は上のタイミングから1ビット当たり1.25μsかかり(転送レート800Kbps相当)、1つのLEDに30us、5個のLEDを合わせると150μsかかる。

この信号をどうやって生成するかであるが、NOP命令を挿入してタイミングを取るソフト的な方法と、ハードロジックで信号を作成する方法が考えられる。MicrochipのアプリケーションノートAN1606にはPIC16F1509のCCPを使ってWS2811を駆動する例が示されており、これが参考になりそうである。しかし何れも時間的にかなりシビアで先人の例でも涙ぐましい苦労の跡が見える。特に回路の遅延などに伴うヒゲの発生には十分注意する必要がある。開発にはロジックアナライザを活用する必要があろう。

 ソフト的にやるのは後回しとして、とりあえずハードロジックを検討してみる。PL-9823はAN1606で示されたWS2811とは必要とする波形が違うので、それに合わせる必要がある。

信号の基本はSPIの出力である。PL-9823ではSPIの1つの出力ビットを4つに分けて1:3(0 code)又は3:1(1 code)の比率のPWMパルスを生成すれば仕様に合う。SPI出力が800Kbpsなら800KHzと1.6MHzの方形波があれば合成できる。SPIを駆動するクロックはFosc/4を分周したものであるため、3.2MHzの2^n倍のFoscが必要であるが一方PIC単体で発生できるクロックは限られており最も近い値として12MHzを4分周してFosc=3MHzとして使うのが適当だろう。ということで転送レート 750Kbps(=3MHz/4)で検討したい。

 AN1606に準じて考えると、SPIの出力(SDO)、SPIのクロック(SCK)、それにSCKの元となるクロックFosc/2から合成できそうだ。Fosc/2はCLKRを使ってFoscを2分周して生成した。

0 code = /SDO & SCK & Fosc/2

1 code = /(/SDO + /SCK + /Fosc/2)  

 PWMパルス = 0 code + 1 code

 この式で & は論理積、+ は論理和、語頭の/ は否定を表す。

この式ならCLKRとCLC各1個で実現できそうである。ということでこれを実際やってみたところ、SPIで送信するバイトの最後のビット(lsb)がSDOではアイドル状態まで引きの伸ばされるためlsbが1だとアイドル状態でもFosc/2がPWMパルスとして出力されAN1606のように上手くは行かない。強制的にlsbを0として7ビットのデータとするなら上手く行きそうだが。そこで悩んだ末に何とか窓信号を作り論理積をとる事とした。

具体的にはもう一個のCLCを使ってSCKをFosc/2の立下りでサンプリングすることで25%遅らせたSCK2を作り、SCKとSCK2の論理和を窓信号とする。つまり、 

     1 code = SDO & ( SCK + SCK2 ) 

 とする。これで上手く行きそうだ。そして、次の通り上手く光ってくれた。

実際に出来た波形を観測すると、 SPIの出力は1バイト(8ビット)送出した後次のバイトを送出までに隙間が出来る。つまりSPIではバイトを跨ぐ連続送信は出来ずバイト間では T0L や T1L が長く引き延ばされている状態になる。この隙間の大きさの規程は無いがRETコード、つまり(確かめた訳ではないが)50usを超えなければ問題ないようである。

 ここまでで基本的な機能は実現できたので、あとは受信した測距データをもとに表示内容を決めていきたい。

参考までにPWMパルスを作成するためのCLKR、SPI1、CLC1、CLC2の初期設定部分は次の様にした。

    //
    // CLKR Fosc(3MHz) to 1.5MHz
    //
    CLKRCONbits.CLKRDC  = 2;        // Duty is 50%
    CLKRCONbits.CLKRDIV = 1;        // divide by 2
    CLKRCONbits.CLKREN  = 1;
    //
    // SPI1
    //
    SSP1CON1            = 0x0;      // clear SSPEN
    SSP1CON1bits.CKP    = 0;        // IDLE Clock is Low
    SSP1CON1bits.SSPM   = 0x0;      // SPI with Clock = Fosc/4
    SSP1STATbits.CKE    = 0;        // trans. at clk idl->actv 
    PIR1bits.SSP1IF     = 0;        //        
    SSP1CON1bits.SSPEN  = 1;
    //
    // CLC1
    //
    CLC1CONbits.LC1MODE = 0;        // AND-OR
    CLC1SEL0            = 0x13;     //1 SPI1 SDO
    CLC1SEL1            = 0x12;     //2 SPI1 SCK
    CLC1SEL2            = 0x0B;     //3 CLKR = Fosc/2
    CLC1SEL3            = 0x05;     //4 CLC2 = SCK2
    CLC1GLS0            = 0x16;     // 0 code = /SDO & Fosc/2
    CLC1GLS1            = 0;        // T
    CLC1GLS2            = 0x02;     // 1 code = SDO
    CLC1GLS3            = 0x88;     //          &  /(/CSK & /SCK2 )  
    CLC1POL             = 0x03;
    CLC1CONbits.EN      = 1;
    //
    // CLC2 - MAKE DELAYED SCK
    //
    CLC2CONbits.LC2MODE = 4;        // 1 input D-ff with S & R
    CLC2SEL0            = 0x12;     //1 SPI1 SCK
    CLC2SEL1            = 0x0B;     //2 CLKR = Fosc/2
    CLC2SEL2            = 0;
    CLC2SEL3            = 0;
    CLC2GLS0            = 0x4;      // CK: /SEL1 = /Fosc/2
    CLC2GLS1            = 0x2;      // D:  SEL0 = SCK
    CLC2GLS2            = 0;        // R: F
    CLC2GLS3            = 0;        // S: F
    CLC2POL             = 0x00;
    CLC2CONbits.EN      = 1;
 

SPIは同時双方向通信という特徴があり、送信のみや受信のみという動作は無い。通常のシリアルインターフェイスの送信だと送信バッファーが空になるのを待って送信バッファーにデータを書き込むが、SPIでは送受のバッファーが共通になっており、送受信が完了したというフラグしかない。従ってSPI(マスターモード)でデータを送信する場には先ずバッファーに送信データを書き込み、送受信完了のフラグを待って次のデータを書き込むというプログラミングをする必要がある(受信の動作はダミーとなる)。このとき送受信完了のフラグは自動的にクリヤーされないのでソフトでクリヤーする必要がある。具体的に5個のLEDにデータを送るのは次の様にした。なお割込みが起きる事でタイミングが狂わないようこの部分は割り込み禁止としている。

void rgbSetLeds(byte *clr_r, byte *clr_g, byte *clr_b){
    byte GIEBitValue = INTCONbits.GIE;
    INTCONbits.GIE   = OFF;          // di()-----------------              for(byte i=0; i<5; i++){
        PIR1bits.SSP1IF = 0;
        SSP1BUF     = *clr_r++;     
        while(PIR1bits.SSP1IF==0){} PIR1bits.SSP1IF = 0;
        SSP1BUF     = *clr_g++;     
        while(PIR1bits.SSP1IF==0){} PIR1bits.SSP1IF = 0;
        SSP1BUF     = *clr_b++;     
        while(PIR1bits.SSP1IF==0){}
    }
    INTCONbits.GIE = GIEBitValue;    // ei()-----------------

}

これを誤って最初を、UARTの送信のように

        while(PIR1bits.SSP1IF==0){} 
        SSP1BUF     = *clr_r++;     
 

とするとSPIでは永久の待ちとなりデータは送信できない。

(続く)