9. アクセサリデコーダのアドレス(DCCモード)


信号機のアドレスはプログラム中では二つの変数My_AdrHとMy_AdrLに分割して使います。My_AdrHはMMパケットのアドレス部分に相当し、My_AdrLはMMパケットのデータ部分のうち、k83のポート部分を”00XXXX00”の形で表します。これらの変数はプログラム中で定数と定義して、異なるアドレスを持った信号機を作る際には、一個一個、PICマイコンに書き込んでも良いのですが、場合によってはレイアウトに配置した後で、アドレスを変更したい場合がないとも限りません。その都度、PICマイコンを信号機から取り外してPICKit 3で書き直すのも面倒なので、本線上に設置したままでアドレスを書き換えられる仕組みを検討します。


9.1. DIPスイッチ方式とコマンドステーションからのCV書込み方式

アクセサリデコーダk83、m83、k84、m84では、4つ(8つ)単位ではありますが、DIPスイッチでアドレスを決定できます。また、ポイント用のデコーダもDIPスイッチでアドレスを決める仕様になっています。メルクリンのアクセサリ用デコーダでは10PのDIPスイッチがデフォルトのようです。

 製作する信号機にもDIPスイッチを設置できればアドレスの変更が楽で良いと思うのですが、マイコンのピン数をもっと大きなものに変更するか、DIPスイッチ専用にマイコンを増設する必要があります。結果的に、基板が大きくなってしまいますが、これはできれば避けたいところ。表面実装にして基板の集積度を上げるなどの方法もありますが、私の技術ではなかなか難しそうです。

 そこで、機関車のCV値を書き換えるようにコマンドステーションからの命令でアドレスを書き換えられれば、ハードウエア的には最小で済みますし、コストも低く抑えられます。日本におけるメルクリンのバイブル的サイト“風のおひるね”さんは、アクセサリデコーダm84のCV値の書き換え方を紹介しています。m84をDCCモードにしたうえで、CS2側でDCCの機関車を仮に設定してCV値を書き換える方法です。たしかにCS2には、どこにもアクセサリデコーダのCV値を書き換える機能は見当たらないので、DCCのサービスモードを利用するしかありません。


9.2. DCCの通信プロトコルについて

という訳で、CS2からDCCのサービスモードによって送られてくるアドレスを受信し、EEPROMに書き込む機能を実装しようと考えました。DCCのサービスモードでは、コマンドステーションと対象とするデコーダが、1:1の接続となっており、他のデバイス(機関車やアクセサリなど)に影響を及ぼさないように接続されていることが前提条件であることにご注意ください。メルクリンにおけるプログラムトラックをちゃんと守る、ということに準ずるものです。

 アドレスさえ拾えれば良いので、必要最小限の機能で良いのですが、それにしてもDCCの通信を解析する必要があります。幸いなことにNMRAのサイトでは規格が詳細に公開されているので、かなり参考になります。言語も英語だし。(メルクリンのはドイツ語が多い…)

 DCCの通信では、58μsのパルス(一周期で116μs)が”1”を表します。デコーダ側では±6μsの誤差を許容しなければならないので52 ~ 64μsの範囲を”1”と見なします。一方、100μs以上の“長い”パルスは“0”を表します。“0”の場合は、許容度が広く、パルス幅としては10, 000μsまでの間延びした”0”も認められています。

DCCパルス

9.3. DCC通信での1bitの取得

1と0を判別するには、いろいろな方法が考えられますが、Lowレベルの間隔を測定して75μs未満の長さであれば1、それ以上であれば0というざっくりとした判定で必要十分です。もちろん、間延びした0もこの条件で判定できます。しきい値(75μs)はテキトーですが、結果的にこれでうまく判別できています。

 この測定にはMMプロトコルの解析に用いたタイマ1を使った状態変化割り込みのルーチンをそのまま使います。 “7. パケット取得開始タイミングの決定(タイマ1)”をご参照ください。MMパケットの解析では長い時間間隔が重要だったので、タイマ1の下位8bitを表すTMR1L(TML)は使いませんでしたが、今回はむしろ短い時間間隔の方が重要となります。ただし、間延びした0についても対応する必要があるので、TMR1H(TMH)、TMR1L(TML)両方を使って正確なLowレベルの長さを評価します。

 以下に1bit分を取得する関数を示します。この関数では、割り込み関数の結果を判定します。


/*1ビットを取得する(DCCモード)
*パルスの立ち下がりでタイマ1をスタート、立ち上がりでタイマ1を止めてパルスのlow状態の長さを計測する。 割り込み関数TIMER21()のIOCAF4のルーチンを利用。1は58(52-64)us, 0は100us以上と規定されているので75usを障壁として0と1を判別する*/
unsigned char Get_bit(void){

  unsigned char Pulse_width; //パルス幅

  while(Packet_N == 0); //パルスの立ち上がり検知までここで待つ(割り込み関数の結果)
    Packet_N = 0; //パルス立ち上がり検知リセット
    Pulse_width = TMH*256 + TML; //パルス幅を計算 0は相当長くなることがあり得る
    if(Pulse_width > 75) //75usより長いか短いかで1,0を判別する
      return(0); //長い方が0
    else
      return(1); //短い方が1
}

9.4. DCCの命令パケット

次に肝心の命令パケットを受信するルーチンを考えます。命令パケット本体の前に1が14個以上連続で出力されるプリアンブルが流されます。その後、スタートビット0に続いて、8bitのデータが続きます。8bitのデータの区切りは0ですが、区切りが1になるとデータ送信終了となります。流されるバイト数は3バイト以上(エラー検知1バイトを含む)から、多分6バイトぐらいまででしょうか。資料をちゃんと読んでいないのでゴメンナサイ。一般的には以下に示すパケットになります。


Preamble …111 0 AAAAAAAA 0 BB… … 0 XXXXXXXX 1

9.4.1. プリアンブルの検知

まず、プリアンブルを検知してパケット受信に備える必要があります。1が14個以上続けばプリアンブルですが、プリアンブル以外で1が長く続く可能性を考えると、命令パケットの最終バイト(通常はエラーチェック)が11111111の場合に、ストップビット1を加えた、9個の1の連続が最長と思われます。ストップビットの後なので、次はプリアンブルが来るはずです。したがって、少なくとも1が9個続いた状況では、必ずプリアンブルに入っているものと判定できます。

 以下の関数は、プリアンブルが検出できるまで関数内で待つルーチンで、検知するまでは呼び出し先の処理はそこで止まります。


/*プリアンブルを検知する(DCCモード)
*データ中に1が連続で含まれる最長は11111111にストップビット1が続いた9個の場合なので1が10個以上続いた場合はプリアンブルに入ったとみなせる。プリアンブルを検知したらなにもしないで戻る。スタートビットの検知はコール先でおこなう*/
void Detect_Preamble(void){

  unsigned char pA_Counter, a; //プリアンブルの個数をカウントする aは作業用

  pA_Counter = 0; //カウンタ初期化
  while(pA_Counter < 10){ //連続した1が10個になるまで
    a = Get_bit(); //1ビット取得
    if(a) //1ならば
      pA_Counter++; //カウンタ1増加
    else //0ならば
      pA_Counter = 0; //カウンタをリセットしてやり直し
  } //1が10個続いたらプリアンブルと見なせるので戻る
}

9.4.2. パケット中の1バイト取得

順番は前後しますが、パケット中の1バイトを取得する関数を示します。外部のルーチンで、データ区切り(またはスタートビット)を検知後、コールされることにより、1バイトを取得し戻り値として返す関数です。関数内では、1bit取得するごとにデータを変数に書込みます。DCCでは上位ビットから受信するので、左から右にデータを書き込みます。MMプロトコルの逆ですが、DCC の方が直感的でよいと思います。

以下に8bit取得用の関数を示します。


/*命令パケット中の1バイトデータを取得する(DCCモード)。外部のルーチンでスタートビット(0)を検出したらこの関数で処理する*/
unsigned char Get_8bit(void){

  unsigned char a, b; //処理用変数

  BitCounter = 0; //ビット数0~7
  BitPos = 0b10000000; //ビット位置左から一つずつ右に移動
  a = 0; //1バイトのデータ、初期化
  while(BitCounter < 8){ //1バイト分を受信する
    b = Get_bit(); //1ビット取得
    if(b) //1ならば
      a |= BitPos; //ビット位置に1を立てる
      BitPos = BitPos >> 1; //ビット位置を右に一つ移動
      BitCounter++; //ビットカウンタ +1
  }
  return(a); //戻り値 a
}

9.4.3. パケットの取得

道具立てが整ったので、DCCの命令パケットを受信するルーチンを考えます。DCCにおいても、命令パケットを二つ受信して、それらが一致したとき命令を実行するよう定められています。今回、対象とするサービスモードのダイレクトモードでは4バイトのデータを使いますので、D_DCC[0..7]という配列を定義して、D_DCC[0..3]は一番目のパケット、D_DCC[4..7]は二番目のパケットのデータとして扱うことにします。パケット1つを取得する関数上では、4バイト以上のパケットを処理することもあり、最大8バイトまでの命令パケットを受信できますが、4バイト以外はコール先で無視することにします。前掲した関数によって、データ取得の機能はほぼ網羅されているのですが、以下のルーチンではそれらを組み合わせて、データを配列DCC_D[x]に代入し、データのバイト数をコール先に戻します。


/*命令パケット1つを取得する(DCCモード)
取得したデータは1バイトごとにグローバル配列D_DCC[x]に代入される(最高8バイトまで)。関数の戻り値Byte_Countは命令パケットのバイト数を表すが、連続してふたつ目のパケットを取得する場合は単純に加される(4バイトのパケットふたつを受信するとByte_Count=8となる*/
unsigned char Acquistion_Instruction_Packet(void){

  Detect_Preamble(); //プレアンブルを認識するまでココで待つ
  Start_bit = 1; //スタートビット初期化
  while(Start_bit){ //スタートビット待ち 0で次の行へ
    Start_bit = Get_bit();
  }
  while(Start_bit == 0){ //エンドビット(1)が出るまで)
    D_DCC[Byte_Count] = Get_8bit(); //配列に1バイトのデータを入れる
    Start_bit = Get_bit(); //区切りビットを取得
    Byte_Count++; //バイト数 +1
  }
  return(Byte_Count); //1パケット中(または2パケット中)のバイト数を戻す
}

9.5. サービスモード

実際にCS2から信号機のアドレスを書き換えるためには、仮のDCC機関車を手動で登録した後、Configuration画面からCV値の書き替え画面に移ります。

DCC設定

 CV値書き換え画面下方にあるPOM、PRG、REGのうちPRG が反転していることを確認します。CV#1:Prim. Adr.のCV値を希望する値に変更した後、右側にある書き込みボタンを押します。

DCC CV書き込み

 この操作により、アドレス書き換え用のパケットは、“ダイレクトモード”と呼ばれる形式でトラックに出力されます。 (画面最下段の書き込みボタンを押すと他のモードで出力されるのかもしれませんが、未確認なので不明です。)

9.5.1. ダイレクトモード

ダイレクトモードは、ある一つのCVアドレスに対して値を設定する命令です。NMRAのサイトから引用します。

ダイレクトモードのパケット

オプション:必要に応じて電源オン-サイクル

3つ以上のリセットパケット

5つ以上のCV1-1023のどれか一つに向けての検証パケット、確認が検知されれば、続けて1つ以上のリセットパケット あるいは、5つ以上のCV1-1023のどれか一つに向けての書込みパケット
  6つ以上の同じ書き込みパケット、またはリセットパケット(デコーダの復帰時間のため)

オプション:電源オフ


 電源オン-サイクルとは、線路に電源を供給した際、サービスモードの開始前にデコーダが安定するまで待つために、コマンドステーションから少なくとも20個のパケットを送信するシーケンスです。

 リセットパケットはすべてのデコーダをリセット(揮発性メモリの値を初期化、電源投入時の状態にリセット、走行中の機関車はすぐに停止など)する命令です。パケットは次の形をとります。


Preamble 0 00000000 0 00000000 0 00000000 1

 今回は必要最小限の機能を実現することを目標としていますので、電源オン-サイクルやリセットパケットは、すべて読み飛ばします。

ダイレクトモードの命令パケットは4バイトからなり、次の形をとります。


Preamble 0 0111CCAA 0 AAAAAAAA 0 DDDDDDDD 0 EEEEEEEE 1

Aは10bitでCV値の番号を示します。番号としてはAの10bitの値に1を足したものになりますので、例えば、00 00000000はCV #1を表すことになります。製作する信号機では一般に使われているとおり、00 00000000をデコーダのアドレスに割り当てます。

 2bitのCCの値により、このパケットがどんな命令かを示します。どんな命令?… 今さら?  実はこのパラメータによって、三つの異なった命令に変化します。

CC=11: 書込み
 10bitのアドレスで指定されたCVの値に、DDDDDDDDを上書きする命令です。この命令では、PICマイコンのEEPROMにDDDDDDDDに書き換えた後、コマンドステーションに対して確認信号を送り返します。

CC=01: 検証
 10bitのアドレスで指定されたCVの現在の値が、DDDDDDDDと一致するかを検証する命令です。この命令では、PICマイコンのEEPROMに保存されているCV値をDDDDDDDDと比較して、一致していれば、コマンドステーションに対して確認信号を送り返します。プログラム中には一応、実装しましたが、この命令が実際にCS2から出ているかどうかは未確認です。

CV=10: ビット操作による書込み
 10bitのアドレスで指定されたCVの値を1bit単位で書き換えます。実装していないので、詳細は省略します。



9.5.2. プログラミング

ダイレクトモードにより、CV#1のCV値を操作するプログラムを示します。プログラム上ではmain()での処理となります。まず、命令パケットのアドレスやデータなどを収容する配列DCC_D[0..7]をすべて0xFFに初期化します。次に命令パケットを受信します。ダイレクトモードでは命令パケットが4バイトなので、バイト数が4以外は読み飛ばします。
 命令パケットが4バイトの場合(受信した変数はDCC_D[0..3]に入っている)、続く命令パケットをDCC_D[4..7]に受信します。これらの各々4バイトのうち、最初の3バイトが一致すれば受信に成功していると見なします。(4バイト目はエラーチェック用)
 ダイレクトモードの確認およびCVアドレスが#1であることを確認した後、書き込み命令(CC=11)であればアドレスを書込み、検証命令(CC=01)であればアドレスの検証をおこないます。

while(1){
  D_DCC[0]=0xFF; //パケットデータの初期化
  D_DCC[1]=0xFF; //命令パケットはダイレクトモードの4byteを仮定
  D_DCC[2]=0xFF; //同じパケットを2回受信したら正しい命令と見なす
  D_DCC[3]=0xFF;
  D_DCC[4]=0xFF;
  D_DCC[5]=0xFF;
  D_DCC[6]=0xFF;
  D_DCC[7]=0xFF;
  
  Byte_Count = 0; //命令パケット中のバイト数, 初期化
  Byte_Count = Acquistion_Instruction_Packet();
  //配列D_DCC[x]に受信データを入力、命令パケットのバイト数を値で返す
  if(Byte_Count == 4){ //たぶんDirect mode
    if(D_DCC[0] !=0b00000000){ //Reset packetでなければ 
      Byte_Count = Acquistion_Instruction_Packet();
      //2パケット目を受信 結果はD_DCC[4-7]に入る
      if((D_DCC[0]==D_DCC[4])&&(D_DCC[1]==D_DCC[5])&&(D_DCC[2] ==D_DCC[6])){
        //連続したふたつのパケットが同じであれば
        if(D_DCC[1] ==0b00000000){ //ダイレクトモードだとするとアドレスのCV=0
          if(D_DCC[0] == 0b01111100){ //ダイレクトモードを確認/write byte
            Write_Address(); //write address
          }
          else{
            if(D_DCC[0] == 0b01110100) //ダイレクトモードを確認/verify address
              Verify_Address(); //Verify address
            }
          }
        }
      }
    }
  }
}

9.6. 書込み完了信号(Acknowledgement)

プログラムのデバッグ時に、CS2からダイレクトモードの書換え命令を出力すると、デバッガが停止してしまう、という現象がおきました。CS2から書換え命令を出したときの動画を以下に示します。(路線上には対象となるデコーダが存在しない条件です。)




 分かりにくいかもしれませんが、オシログラムが一瞬平坦(0 V)になった後にDCCの命令パケットが流れます。次に、5 sほどオシログラムが平坦(0 V)になり、続いて何らかのパケットを送信しています。その後、短い給電オフをはさみ、アイドルパケットに戻り、この時点でCV値の書込みに失敗したというメッセージが出ます。

 CS2側からはCV値の書換え時に、給電を一瞬切って、デコーダを立ち上げ状態にリセットしてから書換え命令を出しているようです。命令を出した後はパケットの送出を止めてデコーダからの書込み完了信号(ACK)の受信を待っているのでしょうか?? 反応がなければ、何らかのパケットを送信して給電オフによるリセット後、メッセージを出してアイドル状態に戻っていると推察されます。

 デバッグができないのは困りますので、CS2の給電オフによるリセットを避けるため、ブレッドボードへはCS2からではなく、別電源により給電することにしました。これでデバッガ起動中も停止することなく命令パケットを受信できるようになりました。

 DCCのACK信号はデコーダ側で消費電流をパルス的に増加させることでコマンドステーションに伝える仕様となっています。(Advanced Acknowledgementというデコーダ側からパケットを送信する規格もあるようですがここでは省略します。)規定では60mA、6 ms±1 msの電流増加をデコーダ側で実施することにより、ACK信号とします。製作する信号機の場合、電流消費の大きい負荷がリレーぐらいしかないので、最大でも52 mA程度と、ちょっと足りないのですが、規定値にはかなり近い値なので、検討には値します。

 疑問なのは、CS2が命令パケットを出した後の5 sにも及ぶ電源オフの意味です。この期間にデコーダ側からのACKを待ち受けるのだと最初は思ったのですが、考えてみれば、そもそも線路からの給電を断たれたら、電源を内蔵しているデコーダ以外は、どんな形であれACKを送ることはできません。したがって、この長い給電オフの前に、CS2にACK信号を届ける必要があります。NMRAの規定によれば、ダイレクトモードの書込み命令は6つ以上を出力することになっているので、最初の二つで命令を実行できれば、給電が断たれる前にACK信号を送る時間的余裕はありそうです。

 という訳で、以下のACK送信関数をテストしました。デバッガは使わず、CS2の反応だけで挙動を評価しました。


/*コマンドステーションに対して確認信号を送る(DCCモード)
*確認信号はモーターの駆動やライトの点灯により消費電流を増加させることで信号とする仕組み。NMRAの規格では60mA 6ms+-1msの電流増加パルスと規定されているが、このデコーダではリレーのコイルに出力ポートいっぱい(25mA)流せたとしてLED点灯と合わせても52mAぐらいとなる*/

void Send_Ack(void){

/*CS2ではサービスインストラクションを送信する直前、短い間隔(〜0.1s)の電源オフがあり、サービスインストラクションに続いて長い(最大〜5s)電源オフがある。後半の長い電源オフの意味は現状不明。*/

  RA0=0; //すべての出力をオフにして…
  RA1=0;
  RA2=0;
  RA5=0;

_delay(20000); //10msの間、電流消費を最低にする

  RA0=1;       //Acknowledgment用電流消費 6ms,〜52mA
  RA1=1;
  RA2=1;
  RA5=1;
  _delay(12000);
  RA0=0; //パルスをオフにする
  RA1=0;
  RA2=0;
  RA5=0;
}


案ずるより産むが易し。無事、ACKを受け取ってもらえたようです。以下の動画に示すように、CS2の反応は前掲したデコーダが無い場合とは、明らかに異なり、エラーメッセージも出ることなく通常画面に戻り、その後すぐにアイドリングシーケンスに入っています。(動画では2回、書込みをおこなっています。生活感が漂っていてすいません)もちろん、EEPROMにも新しいアドレスが書き換えられています。




9.7. アドレスのEEPROMへの書込みと検証

DCCモードで送信される新しいアドレスは8 bitの普通の変数に入ります。信号機モードでアドレスの比較をするには、MMフォーマットの方が処理時間が短く済みますので、3進法のアドレスと2進法のk83ポートに変換します。アドレスの変換は“5.3.コマンドステーション上のアドレスとパケットの関係”で述べた変換方法を用います。


/*引数xをメルクリン用アドレス(上位4ビット(3進数)、下位4ビット中の真ん中2ビット(2進数))に変換する*/
void Decord_Address(unsigned char x){

  unsigned char a,b,c; //作業用変数

  //下位2ビット分のアドレス(0-3)を決定
  a = (x-1) / 4 + 1;
  b = (x-1) % 4;
  if(b == 0)
    Sol_AdrL=0b00000000;
  if(b == 1)
    Sol_AdrL=0b00001100;
  if(b == 2)
    Sol_AdrL=0b00110000;
  if(b == 3)
    Sol_AdrL=0b00111100;

  //上位8ビット(実際には実際には3進数の4ビット)
  BitPos=0b00000001; //ビット位置を左端にセット
  Sol_AdrH=0;//初期化
  while(a != 0){ //a=0で終了

    c = a % 3; //3で割った余り
    if(c == 2){ //3進数2に相当  10で表す
      Sol_AdrH |= BitPos; //1のみを代入
      BitPos = BitPos << 2; //ビット位置をふたつずらす
    }
    if(c == 1){ //3進数1に相当  11で表す
      Sol_AdrH |= BitPos; //1を代入
      BitPos = BitPos << 1; //ビット位置を左に一つずらす
      Sol_AdrH |= BitPos; //1を代入
      BitPos = BitPos << 1; //ビット位置を左に一つずらす
    }
    if(c == 0) //3進数0に相当  00で表す
      BitPos = BitPos << 2; //何も書き込まずビット位置を左にふたつずらす
      a = a /3; //aを一桁下げる
    }
  }
}

書込みモードの場合は、変換したアドレスをEEPROMに保存し、書込みが終了したら、ACK信号を送信して戻ります。


/*アドレスをeepromに書き込む(DCCモード)*/
void Write_Address(void){

  Decord_Address(D_DCC[2]); //アクセサリアドレスをメルクリン用アドレスに変換する
  eeprom_write(0,Sol_AdrH); //3進数アドレスをeepromに書き込む
  eeprom_write(1,Sol_AdrL); //k83アドレス(0-3)eepromに書き込む)
  Send_Ack(); //確認信号を送信
}

検証の場合も8bitのアドレスをMMフォーマットに変換し、それをEEPROMに保存されているアドレスと比較します。一致していればACK信号を送信します。


/*アドレスを検証する(DCCモード)*/
void Verify_Address(void){

unsigned char AdrH, AdrL;

  Decord_Address(D_DCC[2]); //受信したアドレスをメルクリン用アドレスに変換する
  AdrH = eeprom_read(0); //メルクリン用アドレス(3進数)を読み込む
  AdrL = eeprom_read(1); //メルクリン用アドレス(k83)を読み込む
  if((Sol_AdrH == AdrH)&&(Sol_AdrL == AdrL)) //両アドレスが一致すれば
    Send_Ack(); //確認信号を送信
  }


8.パケット取得(タイマ2) TOP pageへ 10. ブレーキモジュール回路
inserted by FC2 system