ESP8266でBトレ(第13回 アクセサリ編その1)PICで信号機を作るよ(1)

こんにちはけろけろおじさんです。あいかわらず全然進んでいないのですが、最近、おうちで電子工作することが国をあげて推奨されている(?)ので、ちょっとだけ進めました。といっても本線とはあまり関係ない部分ですが...

そのまえにちょっと前回の補足です

前回の記事にリンクを貼ってなかったのですが、簡単な自動運転(3列車でダブルホーム運転)をやったときのビデオをYoutubeに載せてあります。ビデオにテロップを入れるなどという高度な技が使えないのでここで解説しますね(全体のシステム構成などは前回以前の記事をご覧ください)。

youtu.be

  • エンドレス10閉塞+側線1の11閉塞でダブルホーム運転しています。
  • ごくシンプルに、次のルールで制御しています。
    1. 出発番線は交互に切り替える。切り替えタイミングは先発列車が出発区間を抜けたとき。
    2. 出発信号は次の条件で進行(または注意または減速)現示にする
      • 停車してから規程秒数経過した
      • 自分の番線が開通している
      • 上記の場合に、自区間の閉塞値を表示する
    3. 到着番線は交互に切り替える。到着番線は、駅から数えて第三コーナーの入口すぐ手前の直線線路に入ったときに決め、切り替えは、場内~自列車までの区間に列車がいなくなったときにおこなう

閉塞信号は自律的に動作するので、これだけのルールで交互発着が行えます。イメージとしては「傾斜のついた溝を転がるビー玉を、溝の分岐や合流の手前につけたゲートを操作して制御する」ような感じです。

制御側のPCでは、列車ごとに電圧-速度の関係を記述したパラメータを用意し、特性の違いを吸収しようとしているのですが、京急(赤い電車)は減速が効きすぎて場内区間を抜けきれずに止まってしまうことがあり、そのときは、後続列車が駅手前で信号待ちさせられています。

PICで信号機 ~方式検討

さてここからが今回の本題です。 ゾーンコントローラ方式をやってみようと思いついたとき、「信号機もゾーンコントローラから動かしたいな」と考えてました。また、信号機を作るなら、信号機ヘッド内に周辺回路も内蔵したいなと考えていました。両方とも、配線を削減したいがためです。ただ、ぼんやり考えていただけで、方法についてはノーアイデアでした。とりあえずゾーンコントローラにIOExpanderの8bit分を用意してはありましたが...。

どうやろうかな

  • 最初に考えたのはパラレル伝送方式です。これだと、4灯式1機あたり2本の信号線が必要なので、8bitでは4機分にしかなりません。また、信号機への配線も1機あたり4本(電源/GND/信号線×2)必要となり結構大げさです。レイアウトが配線であふれそうですね。さらに、信号機側のデコーダIC(74HCシリーズ)はフラットパッケージでも信号機のヘッドに内蔵できるほどは小さくありません。ですので、デコーダから信号機までは5本の配線が必要で大量生産には向きません。というわけでこの案はボツになりました。

    パラレル伝送
    パラレル伝送だと線の数が多いですね。

  • 次に、DCCを使うことを検討してみました。DCCなら一組2本の配線を引き回せばすみます。配線は途中で分岐させることもできますので、「ゾーンコントローラで信号も制御する」という目論見からははずれますが、電線のぐちゃぐちゃからは解放されます。DCCの送信側は既製品を使い、受信側はオープンソースのものを利用させてもらえば、コストもそれほどかからなさそうです。ですが、よく考えてみると信号を点けるためだけにDCC規格のデコーダを使うのはやや大げさな気がします(そもそもやりたいことに比べてDCCの仕様は多機能すぎで、中身を理解する前に挫折してしまいます)。また、デコーダに使用するチップも、ある程度の処理能力(といってもPIC12F程度?)が必要で、信号機ヘッドへの内蔵はむつかしく、デコーダから信号機ヘッドへの配線は相変わらず残ってしまいます。

  • そこで、「電源とシリアルデータ線を兼用する」というアイデアはDCCからいただきつつ、アクセサリ用に特化した軽量なオレオレプロトコルを使うことにしました。通信手順を単純にすることで、非力ながらもパッケージが小さい(SOT-23)のPIC10Fシリーズを受信側に使うことができます。SOT-23と1608サイズのLEDの組み合わせにより、信号機ヘッドと受信機を一体化しても、なんとかサマになる大きさに収まりそうで、そうすると、信号灯と制御部を結ぶ外付け配線が不要になり、レイアウトへの設置がラクチンになります。これで、すきなだけ信号機をおったてることができますね。送信側は、ゾーンコントローラ上でシリアルポート用に確保してあるGPIOがヒマ(となりのゾーンコントローラと位相同期をするとき以外は遊んでいる)なのでこれを使います。

    シリアル伝送方式
    シリアル伝送だと配線が節約できます

    PIC初体験

    というわけで、前に買ってあったPIC10F222を引っ張り出してきました。どうです、小さいでしょ。

    PIC10F222
    PIC10F222(SOT-23パッケージ)と1円玉
    PICは名前だけは前から知っていたのですが、使うのは初めてです。以前は無償の開発環境はアセンブラだったので、おじさんは手が出せなかったのですが、今は開発環境としてMPLAB X IDEがフリーで使え、これにはCコンパイラもついています。書き込みツールもPICKit3の中華コンパチ品がお手頃価格で買えますのでプログラム開始までのハードルはESP8266よりも低いくらいですね。

あれ、タイマー割り込みがない

コードを書き始めて分かったのですが、PIC10F222はタイマーは内蔵していますが、割り込みはかけられないので、シリアル受信をするにはちょっと面倒です。10F322ならタイマー割り込みが使えるようなので、おとなしくそっちを買いなおせばいいのですが、今回のプログラムは受信+Lチカだけなので、受信に全力を投入しても問題ありません。そこで、こちらなどを参考にさせていただきつつシリアル受信コードをCで書きました。

ピン足りなくない?

PIC10F222は6ピンです。電源/GND/シリアル入力で3pin使うので、出力に使えるのは3pinだけです。「あれ?4灯式以上の信号だとデコーダが必要なんじゃない?」と思うでしょ?(話の流れ上、思ってくださいね)。ところがどっこい、デコーダなしで間に合います。どうやるかというとIOピンの3ステート目(ハイインピーダンス)を使います。LEDは電球と違って点灯最低電圧以下だと全く点灯しませんので、図のような回路にしたうえで、電源電圧をLED1とLED2の最低点灯電圧の和以下にしておきます。この状態で、GPIOをハイインピーダンスにしてやるとLED1,LED2とも消灯するというわけです。この方法だと原理的には6灯式までいけます。

LED切り替え
0,1,Highインピーダンスで切り替える
論理表
GPIOの状態によりLEDがきりかわります

プロトコルについて

PIC10Fのクロック(8MHz)で無理なく処理できるように、1200bps(8ビット、パリティなし、ストップビット1)にしました。LEDの点灯電圧は1.7V程度なのでPICの動作電圧は3~3.2V程度を想定し、信号は3V程度で送るつもりです。が、これでノイズが問題になるようでしたら、高めの電圧で送って、信号機の根元で分圧するように変えるかもしれません。アドレス4bit+データ4bitとして、アドレスの一つはブロードキャスト用としたので、ゾーンコントローラ1台あたり15機の信号機をつなぐことができます。信号機だけならデータに4bitもいらないので今後気が変わるかもしれません。消費電流は実測で1機分あたり7mA程度でしたので、送信側はESP8266のGPIOそのままでは不足で、バスバッファをつける必要があります(そもそもCPUの信号線をそのまま外に出すようなことはしないですね)。

で、動くの?

ブレッドボードに2組分組んで、PCのUSBシリアルで動かしてみた動画を載せます。

ブレッドボード
2組(左:5灯式,右:4灯式)組みたててみました


PIC10F222で鉄道模型用の信号機を作ってみる(その1)

キーボードの入力に応じて色が変化してます。この例では0-3(ASCIIコードでいうと上位4バイトが0b0011)が右の4灯式向けのデータで@~C(〃0b0100)だと左の5灯式のデータになるようにしています(いつにもまして地味な動画ですね)。なんかLEDの明るさがまちまちだったり、Offでも完全に消えていないやつがいたりしますが、試作なのでご容赦ください。ちなみに電圧レベルの1,0とも点灯に使っているのため、通常のようなPWMによる明るさの調節はできません(1/High,0/Highの比率を変えるPWMを書けばいけそうですけど...)

完成イメージ

PCBデザインソフト(今回はKiCadを使ってみました)で作ったイメージです。本物の信号機はメーカーのサイト(PDFです)によると幅60cm弱なので、スケールでは約4mmですが、さすがに厳しいので2割オーバーの4.8mmでデザインしました。1608より小さい1206サイズのLEDを使えば(正面からのサイズは)スケールにできそうですが、老眼がつらいので今回はパスしました。基板の製造はSeeedStudioに発注しました。でも、小さすぎて外形カットはオーダーできないので、間隔を大きめにとって電源/データ線を結線した状態でレイアウトしています。パーツを取り付けて動作テストした後に切り出すつもりです。 信号機ヘッドの前面は液晶3Dプリンタで出力する予定ですが、こちらは基板ができてからそれに合わせて考えます。

課題など

今後解決する必要のある点がまだいくつかあります。

  • 量産するとなると(単品でも)手はんだではむつかしいので、リフローでやる必要がありそうですが、両面なのでどうしたもんか思案中です。SeeedStudioは部品の実装を含めて注文できるそうなので、PICのコードや部品の定数が決まっているのならこっちのほうがよさそうですね。今回はLEDの明るさを決めるRの値や、使うPICの型番、コードをいろいろ試したいので、ボートとメタルマスクの発注だけにしました。
  • 現状のコードでは、表示色を切り替えるときに今の色を一旦消灯し、ワンテンポおいて新たな色を点けるようにしていますが、横着してdelay関数を使ったので、この間シリアルデータの受信処理を行っていません。通常は同じ信号機に連続して指示を送信することはないはずですが、ブロードキャスト(発報信号などを想定)を取りこぼす恐れがあります。ちゃんと書きなおせばいいのでしょうが、京成や京急フリッカー信号も表示したいななどと考えると、素直にPIC10F322を使うのが早そうです(ピン配置は同じなので簡単に試せそうです)。
  • 踏切などもこれで動かせたらいいなと思っているのですが、そうすると、踏切表示灯(線路脇にあって、踏切が閉じると点くやつ)用や、障害物検知のデータを返すための3本目の線が必要ですね。そこまでやるならポイントの制御もこの通信方式でおこなえば、配線やゾーンコントローラ側の回路が簡単にできそうです。

回路図とか(暫定)コードとか

一応回路図(5灯式)と暫定版のコード(〃)を載せておきます(コードは一番下に載せました)。データと電源は兼用なので電源用にはダイオードを挟んでコンデンサを入れています。信号は「0」のときに電圧がかっています(232Cそのまま)。LEDの抵抗をHigh側とLow側で兼用にしたり、信号電圧がVddよりダイオードの分だけ高かったりしてますが、狭いところに配置するためにパーツを減らした結果ですので見逃してください。

回路図
回路図

本線についての近況など

ここのところずっと停滞していて、いつになったら開業するのか、やってる本人にもワカラナイのですが、現況は次のような感じです。

  • ゾーンコントローラを追加で作成して全部で8台になりましたので、数の面からはPoint to Pointの小規模な鉄道路線が実現できそうです(はんだ付けが修行のようでした。これ以上必要になるようなら追加分は周辺回路をCPLD化します)。
  • コントロールボードについては今までSVG+jQuery+SignalRでやってきましたが、昨年末あたりにBlazorというのが正式リリース(ServerSideのほう)されたので試してみたところ、用途に適していそうでしたので、今後はこれで書こうと思っています。従来のWebシステムは、サーバー側と画面側を別々に作って、この間のデータの受け渡しはプログラマの責任で結構原始的な方法によっていたのですが、(Server side)Blazorで(プッシュに)Rxなどを使えば、サーバーと画面の通信が双方向とも型安全にc#で書けます。IDEでピリオドを打つのに頼り切っているおじさんは大喜びです。で、今後はそっちで進めることにしました(Blazor流行るといいな...)。
    ゾーンコントローラ8台
    修行の成果です...

コード

/*
 * File:   newmain.c
 * Author: KERO
 *
 * Created on 2020/04/26, 21:43
 */
// CONFIG

#pragma config IOSCFS = 8MHZ    // Internal Oscillator Frequency Select bit (8 MHz)
#pragma config MCPU = ON        // Master Clear Pull-up Enable bit (Pull-up enabled)
#pragma config WDTE = OFF       // Watchdog Timer Enable bit (WDT disabled)
#pragma config CP = OFF         // Code protection bit (Code protection off)
#pragma config MCLRE = OFF      // GP3/MCLR Pin Function Select bit (GP3/MCLR pin function is digital I/O, MCLR internally tied to VDD)

#define _XTAL_FREQ 8000000
#define MY_ADDR 0b0101 //0b0011 0-3  0b0100 @-D

#include <xc.h>
#include "newmain.h"
void main(void) {
    ADCON0bits.ANS0=0;//use pin5(1) as GPIO0
    ADCON0bits.ANS1=0;//use pin4(3) as GPIO1
    OSCCALbits.FOSC4=0;//use pin3(4) as GPIO2
    TMR0=0x00;
    OPTION=0b10011001;//(bit0-2 TMR0 divide 1:4 (bit5 use pin5(1) as GPIO0)
    TRISGPIO=0b1000;//bit3は入力用 bit2.H:R bit1.H:G bit0.H:Y0/L:Y1/
    TestBlink();

    unsigned char bitsRead=0;
    unsigned char buf=0;
    unsigned char status=0;//0:idle 1:prepare 2:reading 
    unsigned char cnt=0;
    unsigned char chkstart=0;
    while(1){
        //9600の8分周のとき:170
        if(TMR0>170){
            TMR0=0;
            cnt++;
            if(status==0){//待機状態
                if(GPIObits.GP3==0){//アイドル状態のとき0になれば
                    status=1;//開始準備状態へ遷移
                    cnt=0;//カウンタリセット(4サイクル待つ用)
                    chkstart=0;//スタートビット確認用カウンタをリセット
                }
            }
            else if(status==1){//開始準備状態
                    if(GPIObits.GP3==0){
                        chkstart++;
                        if(chkstart==4){//4回続けて0だったらスタートビットとみなす
                            buf=0;
                            bitsRead=0;
                            status=2;//読込中状態へ遷移
                            cnt=0;//カウンタリセット(8サイクル待つ用)
                        }
                    }
                    else{
                        status=0;//
                    }
                //}
            }
            else if(status==2){//読込中状態
                if(cnt==8){
                    if(bitsRead<8){
                        unsigned char v=GPIObits.GP3;//負論理なのであとで反転すること
                        unsigned char w=v<<bitsRead;
                        buf=buf | w;
                    }
                    else if(bitsRead==8){
                        /* 格納データチェック
                        for(unsigned char a=0;a<8 ;a++){
                            unsigned char b=0;
                            b=buf & 0b1<<(7-a);
                            GPIObits.GP2=(b>0);
                                GPIObits.GP1=(!(b>0));
                                __delay_ms(400);
                                GPIO=0b00000000;
                                __delay_ms(100);
                        }
                        */
                        SetSignal(buf);
                        buf=0;
                    }
                    else if(bitsRead>8){//Stopビット読込
                        status=0;//待機状態へ遷移
                    }
                    bitsRead++;
                    cnt=0;//カウンタリセット
                }
            }
        }
    }
    return;
}
inline void Off(){
    TRISGPIO=0b1101;//all high
    GPIO=0b0000;
}
inline void TestBlink(){
    Red();
    __delay_ms(500);
    Off();
    __delay_ms(100);
    YellowYellow();
    __delay_ms(500);
    Off();
    __delay_ms(100);    
    Yellow();
    __delay_ms(500);
    Off();
    __delay_ms(100);
    YellowGreen();
    __delay_ms(500);
    Off();
    __delay_ms(100);
    Green();
    __delay_ms(500);
    Off();
    __delay_ms(100);
    Red();
}
inline void GYR(){
        TRISGPIO=0b1000;//bit0-2を出力に
        GPIO=0b0110;//
}
inline void Red(){
        TRISGPIO=0b1101;//bit0,2をハイインピーダンスに
        GPIO=0b1111;//
}
inline void YellowYellow(){
        TRISGPIO=0b1000;//
        GPIO=0b1101;//
}
inline void Yellow(){
        TRISGPIO=0b1001;//bit0をハイインピーダンスに
        GPIO=0b1101;//
}
inline void YellowGreen(){
        TRISGPIO=0b1000;//
        GPIO=0b1000;// 
}
inline void Green(){
        TRISGPIO=0b1001;//bit0をハイインピーダンスに
        GPIO=0b1001;//bit1=Low G点灯
}
inline void SetSignal(unsigned char buf){
    unsigned b;
    b=(buf & 0b11110000)>>4;
    if(b== 0b00000000){Red();return;}//ブロードキャスト(とりあえず全部赤)
    else if(b!=MY_ADDR){return;}
    else{ //自分宛なら色を設定する
        Off();
        __delay_ms(150);//仮(ブロックしないようにループ内でチェックするように変える)
        b=buf & 0b00001111;//下位2ビットのみ使用(現示用に下位4ビット割り当てのうち)
        if(b==0b00000100){Green();}
        else if(b==0b00000011){YellowGreen();}
        else if(b==0b00000010){Yellow();}
        else if(b==0b00000001){YellowYellow();}
        else {Red();}
    }
    return;
}