踏切の警報音について、前回はスピーカを机にテープで固定していましたが、

3Dプリンタでスピーカのエンクロージャーを作成しましたので、

それによる音の違いを確認しました。

 

まずは、その動画です。全く音が変わりますね。

 

 

エンクロージャーは FreeCADで設計しました。

スピーカを表と裏から挟む構造にして、配線を通す溝を設けています。

 

 

それを、光造形式の 3Dプリンタで印刷しました。

しばらくトレイにレジンを入れっぱなしにしていて、FEPフィルムも掃除しておらず、

洗浄用のイソプロピルアルコール(IPA)も使い回しているので、

白い変な塊が付着しています。取ろうとしてもうまく取れませんでした。

 

 

噛み合わせ部分の壁が少し厚かったので少しナイフで削って、

スピーカを挟みました。

 

 

以上です。

DCCポイントデコーダー基板を使ってスピーカから踏切の警報音を鳴らしてみました。

 

 

回路のブロック図を示します。

 


DCCポイントデコーダー基板のモータードライバの二つの入力ピンに

700Hzと750HzのPWM波形を別々に入力してスピーカを駆動するということは、

700Hzと750Hzの波形を引き算して合成することと同じになります。

 

引き算するということは、片方の位相を180°ずらして足し算することと同じですので、

結局 700Hzと750Hzを普通に足し算して合成したのと同じになります。

 

 

下のブログではPICマイコンが使われていますが、

私はAVRマイコンしか使ったことがありませんので、

AVRマイコン(ATtiny412、tinyAVR1)で実現方法を検討しました。

 


最初は、16ビットタイマーカウンターTCAを周波数(FRQ)波形生成モードで使用して

700Hz(TCA.WO1)と750Hz(TCA.WO0)の波形を生成し、

別の16ビットタイマーカウンターTCBで音量調整用のPWM波形(TCB.WO)を生成し、

それらをカスタムロジックCCLでANDすると実現できるかなと思いました。

・ LUT1-OUT = TCA.WO1 and TCB.WO

・ LUT0-OUT = TCA.WO0 and TCB.WO

でも、ATtiny412ではピン割当の制約で実現できません。

 

そこで DCCポイントデコーダー基板の回路では、

・ スピーカ+側: TCA.WO1 (PA1)

・ スピーカ-側: TCA.WO0 (PA7)

の接続になっているため、TCAで音量調整用のPWM波形を生成し、

ソフトウェアで 700Hz又は750Hzの周波数でそのPWM波形をON/OFFすることで

合成波形を生成することにしました。

 

あとは、RTC (Real Time Counter)の 32.768kHzを基準クロックとして、

それに同期させて 700Hzと750Hzを制御すれば実現できます。

 

音量調整用のPWM波形の周波数は、モータードライバーの仕様が

最大 400kHzですのでマージンをもって 100kHzとして、

デューティー比については、12V駆動の場合に 100%では大きすぎるため、

0~約50%で駆動しましたが、これでもスピーカの定格を超えているかもしれません。

音量は毎分130回の周期で、単純下降(逆のごぎり波)にしました。

 

それでプログラムを作成してビルドしたところ、

今回は、どちらもほぼ同じコードサイズになりました。

 

◆ VSCode+PlatformIO

RAM:   [          ]   0.0% (used 0 bytes from 256 bytes)
Flash: [=         ]   9.3% (used 382 bytes from 4096 bytes)
 

◆ Atmel Studio 7.0 (AVR/GNU C++ Compiler flags: -std=gnu++11)

Program Memory Usage    :   384 bytes   9.4 % Full
Data Memory Usage       :   0 bytes   0.0 % Full
 

スピーカは、下のブログで紹介されていた、

「PUI Audio AS01808AO-3-R」を使用しました。

 

 

■ ソースコード 2021/06/18(金) 08:36版

 

 

//------------------------------------------------------------------------------
// Model Railway Crossing Sound Generator
//   Copyright (C) 2021 lonetrip
//   https://ameblo.jp/lonetrip
//
// PlatformIO Project Configuration (platformio.ini)
//   [env:ATtiny412]
//   platform = atmelmegaavr
//   board = ATtiny412
//   framework = arduino
//   upload_protocol = xplainedmini_updi
//
// Microchip ATtiny412-SSN (8-Pin SOIC)
//      Pin  Signal   Function           |     Pin  Signal   Function
//   1: VDD  +5V                         |  8: GND  GND
//   2: PA6  DAC      DAC0.OUT           |  7: PA3  DCC_CMD  PORT.IN
//   3: PA7  OUT_REV  PORT.OUT/TCA0.WO0  |  6: PA0  UPDI     UPDI
//   4: PA1  OUT_FWD  PORT.OUT/TCA0.WO1  |  5: PA2  DCC_ACK  PORT.OUT
//
// TOSHIBA TB67H450FNG (8-Pin SOIC)
//      Pin  Signal   |     Pin  Signal
//   1: GND  GND      |  8: OUT2 MOT_FWD
//   2: IN2  OUT_FWD  |  7: RS   RS
//   3: IN1  OUT_REV  |  6: OUT1 MOT_REV
//   4: VREF DAC      |  5: VM   +12V
//
//------------------------------------------------------------------------------

#define F_CPU (20000000UL)                              // fCLK_CPU=20MHz

#include <avr/io.h>

int main() {

  static const uint16_t RK = 2;                         // 16bit/32768Hz
  static const uint16_t PWM_PER = 199;                  // f=100kHz=20MHz/1/(PER+1)
  static const uint16_t WO1_FRQ = 700 * 2 * RK;         // f=700Hz*2
  static const uint16_t WO0_FRQ = 750 * 2 * RK;         // f=750Hz*2
  static const uint16_t VOL_PER = 100;
  static const uint16_t VOL_FRQ = VOL_PER * RK * 130 / 60;  // f=PER*(130/60)Hz

  uint16_t rtc_cnt = 0;
  uint16_t wo1_cnt = 0;
  uint16_t wo0_cnt = 0;
  uint16_t vol_cnt = 0;
  uint16_t vol_cmp = 0;

  // CLKCTRL (Clock Controller)
  _PROTECTED_WRITE(CLKCTRL.MCLKCTRLB, (0 << CLKCTRL_PEN_bp));

  // RTC (Real Time Counter)
  while (RTC.STATUS != 0);
  RTC.CTRLA = RTC_PRESCALER_DIV1_gc | RTC_RTCEN_bm;     // f=32768Hz

  // PORT
  PORTA.OUTCLR = PIN1_bm | PIN7_bm;                     // PA1 (OUT_FWD)
  PORTA.DIRSET = PIN1_bm | PIN7_bm;                     // PA7 (OUT_REV)

  // DAC (Digital-to-Analog Converter)
  VREF.CTRLA = VREF_DAC0REFSEL_4V34_gc;
  DAC0.CTRLA |= DAC_OUTEN_bm;                           // PA6 (DAC)
  DAC0.DATA = 0xFFUL * 1000 / 4340;                     // 0xFF*1V/4.34V
  DAC0.CTRLA |= DAC_ENABLE_bm;

  // TCA (16-bit Timer/Counter Type A)
  PORTMUX.CTRLC |= PORTMUX_TCA00_ALTERNATE_gc;
  TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc;
  TCA0.SINGLE.CTRLB = TCA_SINGLE_WGMODE_SINGLESLOPE_gc;
  TCA0.SINGLE.CTRLC = 0;
  TCA0.SINGLE.PER = PWM_PER;
  TCA0.SINGLE.CMP1 = 0;
  TCA0.SINGLE.CMP0 = 0;
  TCA0.SINGLE.CTRLA |= TCA_SINGLE_ENABLE_bm;

  // Main Loop
  while (true) {

    uint16_t cnt;
    do {
      while (RTC.STATUS & RTC_CNTBUSY_bm);
      cnt = RTC.CNT;
    } while (rtc_cnt == cnt);
    rtc_cnt = cnt;

    // PA1 (OUT_FWD): Software Oscillator by switching between LOW and TCA0.WO1
    wo1_cnt = wo1_cnt + WO1_FRQ;
    if (wo1_cnt < WO1_FRQ) {                            // Check Overflow
      TCA0.SINGLE.CTRLB ^= TCA_SINGLE_CMP1EN_bm;
    }

    // PA7 (OUT_REV): Software Oscillator by switching between LOW and TCA0.WO0
    wo0_cnt = wo0_cnt + WO0_FRQ;
    if (wo0_cnt < WO0_FRQ) {                            // Check Overflow
      TCA0.SINGLE.CTRLB ^= TCA_SINGLE_CMP0EN_bm;
    }

    // Volume Control
    vol_cnt = vol_cnt + VOL_FRQ;
    if (vol_cnt < VOL_FRQ) {                            // Check Overflow
      vol_cmp = (vol_cmp + 1) % VOL_PER;
      uint16_t cmp = VOL_PER - vol_cmp;                 // PWM Duty
      TCA0.SINGLE.CMP1BUF = cmp;
      TCA0.SINGLE.CMP0BUF = cmp;
    }
  }
  return 0;
}

以上です。

こちらのブログを見て、

踏切の警報音が結構簡単に作れるものなんだなと驚きましたので

Audacityというフリーのオーディオ編集ソフトで作ってみました。

 

 

まずは作った警報音の動画です。似ていますかね?

 

 

 

作り方は簡単で、まずAudacityを起動して、

ツールバーから「道具箱」→「Nyquist プロンプト...」をクリックします。

 

 

プロンプトウィンドウが表示されるので、コマンド

「(stretch-abs 0.46 (mult (sum (osc-pulse 700 0) (osc-pulse 750 0)) (pwl 0 0.5 1 0)))」を

入力して「OK」ボタンをクリックします。

 

コマンドの意味は下記になります。

・ (osc-pulse 700 0) … 700Hzの矩形波を生成

・ (osc-pulse 750 0) … 750Hzの矩形波を生成

・ (sum wave1 wave2) … wave1(700Hz矩形波)とwave2(750Hz矩形波)を加算

・ (pwl time1 level1 time2 level2) … フェードアウト波形を生成 (0,0.5)→(1,0)

・ (mult wave1 wave2) … wave1(合成波)とwave2(フェードアウト波形)を乗算

・ (stretch-abs time wave) … 時間を0.46秒に設定 (=60秒/130回)

 

 

波形が生成されるので、
Shiftキーを押しながら再生ボタンをクリックするとループ再生されます。

 

 

以上です。

ATtiny412を使った DCCポイントデコーダーを更に発展させて、

車両にも使えるように取り組んでいるのですが、

まだまだかかりそうなので一旦途中のソースコードを公開したいと思います。

無保証です。

 

VSCode+PlatformIOでプロジェクトを作成して、

platformio.iniファイルが下のコメントの内容になるように設定して、

main.cppファイルにコピペすればビルドできると思います。

(私は極力一つのファイルにまとめたいタイプなのです)

 

Atmel Studioでビルドすると余計なライブラリが含まれてしまうのか、

コードサイズが増えてしまいます。減らす方法を知りたい…。

 

◆ VSCode+PlatformIO

RAM:   [======    ]  62.9% (used 161 bytes from 256 bytes)
Flash: [========  ]  76.6% (used 3139 bytes from 4096 bytes)
 

◆ Atmel Studio 7.0 (AVR/GNU C++ Compiler flags: -std=gnu++11)

Program Memory Usage    :   3846 bytes   93.9 % Full
Data Memory Usage       :   161 bytes   62.9 % Full
 

■ 2021/06/07(月) 19:45版

 

 

サービスモードをプリアンブルのビット数のみで判定していたりと、

手抜きな実装になっています。

DCCパケットパーサーの関数はもう少しスマートにしたかったのですが中々難しいですね。

あと、通知関数はNmraDccライブラリに似せて作っています(互換性はありません)。

 

//------------------------------------------------------------------------------
// Model Railway DCC Decoder
//   Copyright (C) 2021 lonetrip
//   https://ameblo.jp/lonetrip
//
// PlatformIO Project Configuration (platformio.ini)
//   [env:ATtiny412]
//   platform = atmelmegaavr
//   board = ATtiny412
//   framework = arduino
//   upload_protocol = xplainedmini_updi
//   monitor_speed = 115200
//
// Microchip ATtiny412-SSN (8-Pin SOIC)
//      Pin  Signal   Function           |     Pin  Signal   Function
//   1: VDD  +5V                         |  8: GND  GND
//   2: PA6  DAC      DAC0.OUT           |  7: PA3  DCC_CMD  PORT.IN
//   3: PA7  OUT_REV  PORT.OUT/TCA0.WO0  |  6: PA0  UPDI     UPDI
//   4: PA1  OUT_FWD  PORT.OUT/TCA0.WO1  |  5: PA2  DCC_ACK  PORT.OUT
//
// TOSHIBA TB67H450FNG (8-Pin SOIC)
//      Pin  Signal   |     Pin  Signal
//   1: GND  GND      |  8: OUT2 MOT_FWD
//   2: IN2  OUT_FWD  |  7: RS   RS
//   3: IN1  OUT_REV  |  6: OUT1 MOT_REV
//   4: VREF DAC      |  5: VM   +12V
//
//------------------------------------------------------------------------------

#define F_CPU (20000000UL)                              // CLK_CPU=20MHz

//#include <Arduino.h>
#ifdef Arduino_h
#include <EEPROM.h>
#endif
#include <avr/io.h>
#include <avr/eeprom.h>
#include <util/atomic.h>
#include <util/delay.h>

#define PIN_DCC_CMD_bm PIN3_bm                          // PORTA_PIN3
#define PIN_DCC_ACK_bm PIN2_bm                          // PORTA_PIN2

#define PIN_OUT_FWD_bm PIN1_bm                          // PORTA_PIN1
#define PIN_OUT_REV_bm PIN7_bm                          // PORTA_PIN7
#define PIN_OUT_DAC_bm PIN6_bm                          // PORTA_PIN6

//#define DEBUG_PORT
#ifdef DEBUG_PORT
#define DEBUG_PORT_LOOP
#define DEBUG_PORT_BIT
#define DEBUG_PORT_INTERRUPT
#endif

//#define DEBUG_UART
#ifdef DEBUG_UART
#define DEBUG_UART_PACKET
#endif

//-----------------------------------------------------------------------------
// Alternative Routine to Arduino 

#ifdef Arduino_h
#define MILLIS_ADJUST(ms) (ms)
#else
#define MILLIS_ADJUST(ms) ((ms) * 4 + ((ms) + 9) / 10)  // k=1/(4096Hz/1000Hz-4)

void setup();
void loop();

static uint8_t EEMEM EEPROM_DATA[EEPROM_SIZE];

class EEPROMClass {
public:
  static const int16_t length() {
    return EEPROM_SIZE;
  }

  uint8_t read(uint8_t address) {
    eeprom_busy_wait();
    return eeprom_read_byte(&EEPROM_DATA[address]);
  }

  void update(uint8_t address, uint8_t data) {
    eeprom_busy_wait();
    eeprom_update_byte(&EEPROM_DATA[address], data);
  }

} EEPROM;

uint16_t millis() {
  while (RTC.STATUS & RTC_CNTBUSY_bm);
  return RTC.CNT;
}

void delay(uint16_t ms) {
  uint16_t time = millis();
  uint16_t target = MILLIS_ADJUST(ms);
  while (((uint16_t)millis() - time) < target);
}

int main() {

  // CLKCTRL (Clock Controller)
  _PROTECTED_WRITE(CLKCTRL.MCLKCTRLB, (0 << CLKCTRL_PEN_bp));

  // RTC (Real Time Counter)
  while (RTC.STATUS != 0);
  RTC.CTRLA = RTC_PRESCALER_DIV8_gc | RTC_RTCEN_bm;     // f=4096Hz, T=16s

  setup();
  sei();
  while (true) {
    loop();
  }
  return 0;
}
#endif

//-----------------------------------------------------------------------------
// Debug Output Class

class DebugOutput {
  static const uint8_t PIN_DBG_bm = PIN_DCC_ACK_bm;

public:
  inline void forLoop() {
    #ifdef DEBUG_PORT_LOOP
    PORTA.OUTTGL = PIN_DBG_bm;
    #endif
  }

  inline void forBit(bool bit) {
    #ifdef DEBUG_PORT_BIT
    if (bit) {
      PORTA.OUTSET = PIN_DBG_bm;
    } else {
      PORTA.OUTCLR = PIN_DBG_bm;
    }
    #endif
  }

  inline void forInterrupt(bool bit) {
    #ifdef DEBUG_PORT_INTERRUPT
    if (bit) {
      PORTA.OUTSET = PIN_DBG_bm;
    } else {
      PORTA.OUTCLR = PIN_DBG_bm;
    }
    #endif
  }

} m_debug;

//-----------------------------------------------------------------------------
// Serial Communication Class

class SerialCommunication {
private:

  #ifdef DEBUG_UART
  class ByteQueue {
  private:
    static const uint8_t SIZE = 8;
    uint8_t m_data[SIZE];
    uint8_t m_head;
    uint8_t m_tail;

  public:
    void init() {
      m_head = 0;
      m_tail = 0;
    }

    bool empty() {
      return (m_head == m_tail);
    }

    bool full() {
      return (m_head == ((m_tail + 1) % SIZE));
    }

    void push(uint8_t data) {
      if (full()) {
        return;
      }
      m_data[m_tail] = data;
      m_tail = (m_tail + 1) % SIZE;
    }

    uint8_t pop() {
      if (empty()) {
        return 0;
      }
      uint8_t data = m_data[m_head];
      m_head = (m_head + 1) % SIZE;
      return data;
    }

  } m_txd;
  #endif

public:
  void setup(uint32_t speed = 0) {
    #ifdef DEBUG_UART
    m_txd.init();

    // PORT
    PORTA.OUTSET = PIN1_bm;
    PORTA.DIRSET = PIN1_bm;
    PORTMUX.CTRLB |= PORTMUX_USART0_bm;                 // USART0.TXD=PA1

    // USART (Universal Synchronous and Asynchronous Receiver and Transmitter)
    if (speed) {                                        // Speed,8,N,1
      uint32_t baud = (64 * F_CPU) / (16 * speed);
      USART0.BAUD = (uint16_t)(baud * (1024 + SIGROW.OSC20ERR5V) / 1024);
    } else {
      USART0.BAUD = 1;
    }
    USART0.CTRLA = 0;
    USART0.CTRLB = USART_TXEN_bm;
    USART0.CTRLC = USART_SBMODE_1BIT_gc | USART_CHSIZE_8BIT_gc;
    #endif
  }

  void process() {
    #ifdef DEBUG_UART
    if (!m_txd.empty()) {
      if (USART0.STATUS & USART_DREIF_bm) {
        USART0.TXDATAL = m_txd.pop();
      }
    }
    #endif
  }

  void write(const uint8_t data) {
    #ifdef DEBUG_UART
    while (m_txd.full()) {
      process();
    }
    m_txd.push(data);
    #endif
  }

} m_serial;

//------------------------------------------------------------------------------
// General Timer Class

class GeneralTimer {
private:
  bool m_run;
  uint16_t m_time;
  uint16_t m_target;

public:
  GeneralTimer() {
    stop();
  }

  void start(uint16_t ms = 0) {
    m_run = true;
    m_time = millis();
    m_target = MILLIS_ADJUST(ms);
  }

  void stop() {
    m_run = false;
  }

  bool process() {
    if (m_run) {
      return (((uint16_t)millis() - m_time) >= m_target);
    }
    return false;
  }

};

//------------------------------------------------------------------------------
// DCC Decoder Class

class DccDecoder {
private:
  static const uint8_t HALFBIT_INVALID = -1;

  enum State : uint8_t {
    STATE_PREAMBLE,
    STATE_START_BIT,
    STATE_DATA_BYTE,
    STATE_END_BIT,
  } m_state;

  struct Packet {
    static const uint8_t SIZE = 6;
    uint8_t preamble;
    uint8_t data[SIZE];
    uint8_t size;
  } m_packet;

  uint8_t m_halfbit;
  uint8_t m_byte;
  uint8_t m_bit;
  uint8_t m_xor;

  class PacketQueue {
  private:
    static const uint8_t SIZE = 8;
    Packet m_data[SIZE];
    uint8_t m_head;
    uint8_t m_tail;

  public:
    void init() {
      m_head = 0;
      m_tail = 0;
    }

    bool empty() {
      return (m_head == m_tail);
    }

    bool full() {
      return (m_head == ((m_tail + 1) % SIZE));
    }

    void push(Packet &packet) {
      ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
        if (full()) {
            return;
        }
        m_data[m_tail] = packet;
        m_tail = (m_tail + 1) % SIZE;
      }
    }

    void pop(Packet &packet) {
      ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
        if (empty()) {
          packet = {};
          return;
        }
        packet = m_data[m_head];
        m_head = (m_head + 1) % SIZE;
      }
    }

  } m_queue;

  struct Configuration {
    uint16_t addressMultiFunction;
    uint16_t addressAccessory;
    bool cv29_addressing_method;
    bool cv29_fl_location;
  } m_config;

  enum DCC_ACK_MODE : uint8_t {
    DCC_ACK_BASIC,
    DCC_ACK_ADVANCED,
  };

protected:
  enum DCC_ADDR_TYPE : uint8_t {
    DCC_ADDR_SHORT,
    DCC_ADDR_LONG,
  };

  enum DCC_DIRECTION : uint8_t {
    DCC_DIR_REV,
    DCC_DIR_FWD,
  };

  enum DCC_SPEED_STEPS : uint8_t {
    SPEED_STEP_14 = 14,
    SPEED_STEP_28 = 28,
    SPEED_STEP_128 = 126,
  };

  enum FN_GROUP : uint8_t {
    FN_0_4,
    FN_5_8,
    FN_9_12,
    FN_13_20,
    FN_21_28,
    FN_0,
  };

private:
  void reset() {
    m_state = STATE_PREAMBLE;
    m_packet = {};
    m_halfbit = HALFBIT_INVALID;
    m_byte = 0;
    m_bit = 0;
    m_xor = 0;
  }

  void processPulse(uint16_t width) {
    uint8_t halfbit = HALFBIT_INVALID;

    // NMRA S-9.1 Electrical Standards for Digital Command Control
    // A: Technique for Encoding Bits

    // "1" Half Bit: Nominal 58us (52-64us)
    if (((52 - 6) <= width) && (width <= (64 + 6))) {
      halfbit = 1;
    } else

    // "0" Half Bit: Nominal 116us (90-10000us)
    if ((90 <= width) && (width <= 10000)) {
      halfbit = 0;
    } else

    // Timeout
    if (12000 <= width) {
      reset();
      return;
    }

    // Check Valid Bit
    if ((halfbit == m_halfbit) && (halfbit != HALFBIT_INVALID)) {
      m_debug.forBit(halfbit);
      processBit(halfbit);
      halfbit = HALFBIT_INVALID;
    }
    m_halfbit = halfbit;
  }

  void processBit(uint8_t bit) {

    // NMRA S-9.2 Communications Standards for Digital Command Control
    // A: General Packet Format
    switch (m_state) {

    // Preamble
    case STATE_PREAMBLE:
      if (bit) {
        m_packet.preamble++;
        if (m_packet.preamble >= 12) {
          m_state = STATE_START_BIT;
        }
      } else {
        m_packet.preamble = 0;
      }
      break;

    // Packet Start Bit
    case STATE_START_BIT:
      if (bit) {
        m_packet.preamble++;
      } else {
        m_state = STATE_DATA_BYTE;
        m_packet.size = 0;
        m_bit = 0;
        m_xor = 0;
      }
      break;

    // Data Byte
    case STATE_DATA_BYTE:
      m_byte = (m_byte << 1) | bit;
      m_bit++;
      if (m_bit == 8) {
        m_state = STATE_END_BIT;
        if (m_packet.size < Packet::SIZE) {
          m_packet.data[m_packet.size] = m_byte;
        }
        m_packet.size++;
        m_bit = 0;
        m_xor ^= m_byte;
      }
      break;

    // Data Byte Start Bit & Packet End Bit
    case STATE_END_BIT:
      if (bit) {
        // Check Error Detection
        if (m_xor == 0) {
          // Check Packet Size
          if ((3 <= m_packet.size) && (m_packet.size <= Packet::SIZE)) {
            m_queue.push(m_packet);
          }
        }
        m_state = STATE_PREAMBLE;
        m_packet.preamble = 0;
      } else {
        m_state = STATE_DATA_BYTE;
      }
      break;
    }
  }

  void parsePacket(Packet &packet) {

    // Check Mode
    if (packet.preamble < 20) {
      parseOperationModePacket(packet);
    } else {
      parseServiceModePacket(packet);
    }
  }

  void parseOperationModePacket(Packet &packet) {
    uint8_t address = packet.data[0];
    uint8_t command = packet.data[1];
    bool hasNoData = (packet.size == 3);

    // NMRA S-9.2.1 Extended Packet Formats for Digital Command Control
    // A: Address Partitions

    // Address 0: Broadcast address
    // Address 1-127: Multi-Function decoders with 7 bit addresses
    if ((0 <= address) && (address <= 127)) {
      parseMultiFunctionDecoderPacket(packet, DCC_ADDR_SHORT);
    } else

    // Address 128-191: Basic Accessory Decoders with 9 bit addresses
    //   and Extended Accessory Decoders with 11-bit addresses
    if ((128 <= address) && (address <= 191)) {
      parseAccessoryDecoderPacket(packet);
    } else

    // Address 192-231: Multi-Function Decoders with 14 bit addresses
    if ((192 <= address) && (address <= 231)) {
      parseMultiFunctionDecoderPacket(packet, DCC_ADDR_LONG);
    } else

    // Address 232-254: Reserved for Future Use
    if ((232 <= address) && (address <= 254)) {
      // No implement
    } else

    // Address 255: Idle Packet
    if (address == 255) {
      if (hasNoData) {
        if (command == 0x00) {

          // NMRA S-9.2 Communications Standards for Digital Command Control
          // B: Baseline Packets
          // Digital Decoder Idle Packet For All Decoders
          //   {Preamble} 0 11111111 0 00000000 0 11111111 1
          notifyDccIdle();
        }
      }
    }
  }

  void parseMultiFunctionDecoderPacket(Packet &packet, DCC_ADDR_TYPE type) {
    uint16_t address = ((type == DCC_ADDR_SHORT)
      ? packet.data[0] : (((packet.data[0] & 0x3F) << 8) | packet.data[1]));
    uint8_t offset = ((type == DCC_ADDR_SHORT) ? 0 : 1);
    uint8_t *data = &packet.data[offset];
    uint8_t command = data[1];
    uint8_t data1 = data[2];
    uint8_t data2 = data[3];
    bool hasNoData = (packet.size == (3 + offset));
    bool hasData1 = (packet.size == (4 + offset));
    bool hasData2 = (packet.size == (5 + offset));

    // Check Address
    if ((address != m_config.addressMultiFunction) && (address != 0)) {
      return;
    }

    // NMRA S-9.2.1 Extended Packet Formats for Digital Command Control
    // B: Broadcast Command for Multi Function Digital Decoders
    //   {Preamble} 0 00000000 0 {instruction-bytes} 0 EEEEEEEE 1
    // C: Instruction Packets for Multi Function Digital Decoders
    //   {Preamble} 0 0AAAAAAA            0 CCCDDDDD (0 DDDDDDDD (0 DDDDDDDD)) 0 EEEEEEEE 1
    //   {Preamble} 0 11AAAAAA 0 AAAAAAAA 0 CCCDDDDD (0 DDDDDDDD (0 DDDDDDDD)) 0 EEEEEEEE 1
    switch ((command & 0xE0) >> 5) {

    // CCC=000: Decoder and Consist Control Instruction
    case 0b000:
      switch ((command & 0x10) >> 4) {

      // 0: Decoder Control
      //   {instruction byte} = 0000CCCF (DDDDDDDD)
      case 0b0:
        switch ((command & 0x0E) >> 1) {

        // 000: Reset
        case 0b000:
          if (hasNoData) {

            // NMRA S-9.2 Communications Standards for Digital Command Control
            // B: Baseline Packets
            // Digital Decoder Reset Packet For All Decoders
            //   {Preamble} 0 00000000 0 00000000 0 00000000 1

            // D=0: Digital Decoder Reset
            // D=1: Hard Reset
            notifyDccReset(command & 0x01);
          }
          break;

        // 001: Factory Test Instruction
        // 011: Set Decoder Flags
        // 101: Set Advanced Addressing
        // 111: Decoder Acknowledgment Request (D=1)
        // Else: Reserved for future use
        }
        break;

      // 1: Consist Control
      //   {instruction byte} = 0001CCCC 0AAAAAAA
      case 0b1:
        break;
      }
      break;

    // CCC=001: Advanced Operation Instructions
    //   {instruction byte} = 001CCCCC DDDDDDDD
    case 0b001:
      switch (command & 0x1F) {

      // 11111: 128 Speed Step Control
      //   {instruction byte} = 00111111 DSSSSSSS
      // D: Direction (1:Forward, 0:Reverse)
      // S: Speed (0:Stop, 1:Emergency stop, 2-127:Speed)
      case 0b11111:
        if (hasData1) {
          uint8_t speed = (data1 & 0x7F);
          if (speed < 2) {
            // Swap 0:Stop and 1:Emergency stop
            speed ^= 1;
          }
          DCC_DIRECTION dir = ((data1 & 0x80) ? DCC_DIR_FWD : DCC_DIR_REV);

          // speed (0:Emergency stop, 1:Stop, 2-127:Speed)
          notifyDccSpeed(address, type, speed, dir, SPEED_STEP_128);
        }
        break;

      // 11110: Restricted Speed Step Instruction
      //   {instruction byte} = 00111110 F0DCSSSS
      case 0b11110:
        break;

      // 11101: Analog Function Group
      //   {instruction byte} = 00111101 VVVVVVVV DDDDDDDD
      case 0b11101:
        break;
      }
      break;

    // CCC=010: Speed and Direction Instruction for reverse operation
    // CCC=011: Speed and Direction Instruction for forward operation
    //   {instruction byte} = 01DCSSSS
    case 0b010:                                         // FALLTHROUGH
    case 0b011:
      if (hasNoData) {
        uint8_t speed = (command & 0x0F);
        uint8_t fl = ((command & 0x10) >> 4);
        if (speed < 2) {
          // Swap 0:Stop and 1:Emergency stop
          speed ^= 1;
        }
        DCC_DIRECTION dir = ((command & 0x20) ? DCC_DIR_FWD : DCC_DIR_REV);

        // NMRA S-9.2 Communications Standards for Digital Command Control
        // B: Baseline Packets
        // Speed and Direction Packet For Locomotive Decoders
        //   {Preamble} 0 0AAAAAAA 0 01DCSSSS 0 EEEEEEEE 1
        // Digital Decoder Broadcast Stop Packets For All Decoders
        //   {Preamble} 0 00000000 0 01DC000S 0 EEEEEEEE 1

        // for CV29[1]=1: 28 Speed Step
        // speed (0:Emergency stop, 1:Stop, 2-29:Speed)
        if (m_config.cv29_fl_location) {
          if (speed >= 2) {
            speed = ((speed << 1) | fl) - 2;
          }
          notifyDccSpeed(address, type, speed, dir, SPEED_STEP_28);
        } else

        // for CV29[1]=0: 14 Speed Step
        // speed (0:Emergency stop, 1:Stop, 2-15:Speed)
        {
          notifyDccSpeed(address, type, speed, dir, SPEED_STEP_14);
          notifyDccFunc(address, type, FN_0, fl);
        }
      }
      break;

    // CCC=100: Function Group One Instruction
    //   {instruction byte} = 100DDDDD
    case 0b100:
      if (hasNoData) {
        notifyDccFunc(address, type, FN_0_4, (command & 0x1F));
      }
      break;

    // CCC=101: Function Group Two Instruction
    //   {instruction byte} = 101SDDDD
    case 0b101:
      if (hasNoData) {
        FN_GROUP fng = ((command & 0x10) ? FN_5_8 : FN_9_12);
        notifyDccFunc(address, type, fng, (command & 0x0F));
      }
      break;

    // CCC=110: Future Expansion
    //   {instruction byte} = 110CCCCC DDDDDDDD (DDDDDDDD)
    case 0b110:
      switch (command & 0x1F) {

      // 00000: Binary State Control Instruction long form
      case 0b00000:
        break;

      // 11101: Binary State Control Instruction short form
      case 0b11101:
        break;

      // 11110: F13-F20 Function Control
      case 0b11110:
        if (hasData1) {
          notifyDccFunc(address, type, FN_13_20, data1);
        }
        break;

      // 11111: F21-F28 Function Control
      case 0b11111:
        if (hasData1) {
          notifyDccFunc(address, type, FN_21_28, data1);
        }
        break;
      }
      break;

    // CCC=111: Configuration Variable Access Instruction
    case 0b111:
      switch ((command & 0x10) >> 4) {

      // 1: Configuration Variable Access Instruction - Short Form
      //   {instruction byte} = 1111CCCC DDDDDDDD
      case 0b1:
        break;

      // 0: Configuration Variable Access Instruction - Long Form
      //   {instruction byte} = 1110CCVV VVVVVVVV DDDDDDDD
      //   {instruction byte} = 1110CCVV VVVVVVVV 111CDBBB
      case 0b0:
        if (hasData2) {
          command = ((command & 0x0C) >> 2);
          uint16_t cva = (((command & 0x03) << 8) | data1) + 1;
          uint8_t cvd = data2;
          parseCVInstruction(command, cva, cvd, DCC_ACK_ADVANCED);
        }
        break;
      }
      break;
    }
  }

  void parseAccessoryDecoderPacket(Packet &packet) {
    uint8_t command = packet.data[1];
    uint16_t accDecAddress = (packet.data[0] & 0x3F)
      | ((uint16_t)(~packet.data[1] & 0x70) << 2);
    uint8_t power = ((command & 0x08) >> 3);
    uint8_t index = ((command & 0x06) >> 1);
    uint8_t direction = (command & 0x01);
    uint16_t accOutAddress = ((((accDecAddress - 1) << 2) | index) + 1);

    // Check Address
    if (m_config.cv29_addressing_method) {
      if (accOutAddress != m_config.addressAccessory) {
        return;
      }
    } else {
      if ((accDecAddress != m_config.addressAccessory) && (accDecAddress != 511)) {
        return;
      }
    }

    // NMRA S-9.2.1 Extended Packet Formats for Digital Command Control
    // D: Accessory Digital Decoder Packet Formats

    // Basic Accessory Decoder Packet Format (9-bit address)
    //   {Preamble} 0 10AAAAAA 0 1aaaCDDd 0 EEEEEEEE 1
    // Broadcast Command for Basic Accessory Decoders
    //   {Preamble} 0 10111111 0 1000CDDd 0 EEEEEEEE 1
    // A: Address bit
    // a: Address bit (ones complement)
    // C: Power (0:Deactivate, 1:Activate)
    // D: Index (0-3:Turnout Number)
    // d: Direction (0:Straight, 1:Diverging)
    if (packet.size == 3) {
      if ((command & 0x80) == 0x80) {

        // for CV29[6]=1: Output Address method
        // DecAddress   1, Index 0 1 2 3 -> OutAddress    1    2    3    4
        // DecAddress   2, Index 0 1 2 3 -> OutAddress    5    6    7    8
        // DecAddress 511, Index 0 1 2 3 -> OutAddress 2041 2042 2043 2044
        if (m_config.cv29_addressing_method) {
          notifyDccAccTurnoutOutput(accOutAddress, direction, power);
        } else

        // for CV29[6]=0: Decoder Address method
        {
          notifyDccAccTurnoutBoard(accDecAddress, index, direction, power);
        }
      }
    } else

    // Extended Accessory Decoder Control Packet Format (11-bit address)
    //   {Preamble} 0 10AAAAAA 0 0aaa0AA1 0 000XXXXX 0 EEEEEEEE 1
    // Broadcast Command for Extended Accessory Decoders
    //   {Preamble} 0 10111111 0 00000111 0 000XXXXX 0 EEEEEEEE 1
    if (packet.size == 4) {
      if ((command & 0x89) == 0x01) {
        notifyDccSigOutputState(accOutAddress, packet.data[2]);
      }
    } else

    // Accessory Decoder Configuration Variable Access Instruction
    // Basic Accessory Decoder Packet address for operations mode programming
    //   {Preamble} 0 10AAAAAA 0 1aaaCDDd 0 1110CCVV 0 VVVVVVVV 0 DDDDDDDD 0 EEEEEEEE 1
    // Extended Decoder Control Packet address for operations mode programming
    //   {Preamble} 0 10AAAAAA 0 0aaa0AA1 0 1110CCVV 0 VVVVVVVV 0 DDDDDDDD 0 EEEEEEEE 1
    if (packet.size == 6) {
      if (((command & 0x80) == 0x80) || ((command & 0x89) == 0x01)) {
        if ((packet.data[2] & 0xF0) == 0xE0) {
          command = ((packet.data[2] & 0x0C) >> 2);
          uint16_t cva = (((packet.data[2] & 0x03) << 8) | packet.data[3]) + 1;
          uint8_t cvd = packet.data[4];
          parseCVInstruction(command, cva, cvd, DCC_ACK_ADVANCED);
        }
      }
    }
  }

  void parseServiceModePacket(Packet &packet) {

    // NMRA S-9.2.3 Service Mode for Digital Command Control
    // E: Service Mode Instruction Packets
    switch ((packet.data[0] & 0xF0) >> 4) {
    case 0b0111:

      // Service Mode Instruction Packets for Direct Mode
      //   {Long-preamble} 0 0111CCAA 0 AAAAAAAA 0 DDDDDDDD 0 EEEEEEEE 1
      //   {Long-preamble} 0 011110AA 0 AAAAAAAA 0 111KDBBB 0 EEEEEEEE 1
      if (packet.size == 4) {
        uint8_t command = ((packet.data[0] & 0x0C) >> 2);
        uint16_t cva = (((packet.data[0] & 0x03) << 8) | packet.data[1]) + 1;
        uint8_t cvd = packet.data[2];
        parseCVInstruction(command, cva, cvd, DCC_ACK_BASIC);
      }
      break;
    }
  }

  void parseCVInstruction(uint8_t command, uint16_t cva, uint8_t cvd, DCC_ACK_MODE ack) {

    // NMRA S-9.2.1 Extended Packet Formats for Digital Command Control
    //   Configuration Variable Access Instruction - Long Form
    // NMRA S-9.2.3 Service Mode for Digital Command Control
    //   Instructions packets using Direct CV Addressing
    switch (command) {

    // CC=11: Write Byte
    //   {instruction byte} = xxxxCCAA AAAAAAAA DDDDDDDD
    case 0b11:
      if (notifyCVWrite(cva, cvd) == cvd) {
        performAck(ack);
      }
      break;

    // CC=01: Verify Byte
    //   {instruction byte} = xxxxCCAA AAAAAAAA DDDDDDDD
    case 0b01:
      if (notifyCVRead(cva) == cvd) {
        performAck(ack);
      }
      break;

    // CC=10: Bit Manipulation
    //   {instruction byte} = xxxxCCAA AAAAAAAA 111KDBBB
    case 0b10:
      if ((cvd & 0xE0) == 0xE0) {
        uint8_t mask = (1 << (cvd & 0x07));
        uint8_t bit = (((cvd & 0x08) >> 3) * mask);
        switch ((cvd & 0x10) >> 4) {

          // K=1: Write Bit
          case 0b1:
            cvd = (notifyCVRead(cva) & ~mask) | bit;
            if ((notifyCVWrite(cva, cvd) & mask) == bit) {
              performAck(ack);
            }
            break;

          // K=0: Verify Bit
          case 0b0:
            if ((notifyCVRead(cva) & mask) == bit) {
              performAck(ack);
            }
            break;
        } 
      }
      break;
    }
  }

  void performAck(DCC_ACK_MODE ack) {
    switch (ack) {
    case DCC_ACK_BASIC:
      notifyCVAck();
      break;
    case DCC_ACK_ADVANCED:
      notifyAdvancedCVAck();
      break;
    }
  }

protected:

  // NMRA S-9.2.2 Configuration Variables for Digital Command Control
  enum CV_INDEX {
    // L:Multi-Function(Locomotive), A:Accessory, C:Common
    // M:Mandatory, R:Recommended, O:Optional

    // Common
    CV07_CM_VERSION           = 7,      // Manufacturer Version Number
    CV08_CM_MANUFACTURER_ID   = 8,      // Manufacturer ID
    CV29_CM_CONFIG_DATA       = 29,     // Configuration Data

    // Multi-Function Decoder
    CV01_LM_PRIMARY_ADDRESS   = 1,      // Primary Address (1-127)
    CV02_LR_VSTART            = 2,      // Vstart
    CV03_LR_ACCEL_RATE        = 3,      // Acceleration Rate
    CV04_LR_DECEL_RATE        = 4,      // Deceleration Rate
    CV05_LO_VHIGH             = 5,      // Vhigh
    CV06_LO_VMID              = 6,      // Vmid
    CV11_LR_PACKET_TIMEOUT    = 11,     // Packet Time-Out Value
    CV17_LO_EXT_ADDRESS_MSB   = 17,     // Extended Address (MSB) (192-231)
    CV18_LO_EXT_ADDRESS_LSB   = 18,     // Extended Address (LSB) (0-255)

    // Accessory Decoder
    CV01_AM_ADDRESS_LSB       = 1,      // Decoder Address (LSB)
    CV02_AO_AUX_ACTIVATION    = 2,      // Auxiliary Activation
    CV03_AO_TIME_ON_F1        = 3,      // Time On for Functions F1
    CV04_AO_TIME_ON_F2        = 4,      // Time On for Functions F2
    CV05_AO_TIME_ON_F3        = 5,      // Time On for Functions F3
    CV06_AO_TIME_ON_F4        = 6,      // Time On for Functions F4
    CV09_AM_ADDRESS_MSB       = 9,      // Decoder Address (MSB)
  };

  enum CV08_MANUFACTURER_ID {
    CV08_C_PUBLIC_DOMAIN      = 0x0D,   // Public Domain & Do-It-Yourself Decoders
  };

  enum CV29_CONFIG_DATA {
    CV29_L_LOCO_DIRECTION     = 0b00000001, // [0] Locomotive Direction (0:Normal, 1:Reversed)
    CV29_L_FL_LOCATION        = 0b00000010, // [1] FL location (0:14 Speed Step, 1:28 Speed Step)
    CV29_L_APS                = 0b00000100, // [2] Power Source Conversion (0:NMRA, 1:CV#12)
    CV29_C_BIDI_ENABLE        = 0b00001000, // [3] Bi-Directional Communications (0:Disabled, 1:Enabled)
    CV29_L_SPEED_TABLE        = 0b00010000, // [4] Speed Table (0:CV#2,5,6, 1:CV#66-95)
    CV29_L_EXT_ADDRESSING     = 0b00100000, // [5] Addressing Type (0:One byte, 1:Two byte)
    CV29_A_DECODER_TYPE       = 0b00100000, // [5] Decoder Type (0:Basic, 1:Extended Accessory)
    CV29_A_ADDRESSING_METHOD  = 0b01000000, // [6] Addressing Method (0:Decoder Address, 1:Output Address)
    CV29_C_ACCESSORY_DECODER  = 0b10000000, // [7] Accessory Decoder (0:Multi-Function, 1:Accessory)
    CV29_L_MFUNCTION_DECODER  = 0b00000000, // [7]
  };

  // Declaration for Notification Functions
  virtual void notifyDccReset(uint8_t hardReset) {}
  virtual void notifyDccIdle() {}
  virtual void notifyDccSpeed(uint16_t Addr, DCC_ADDR_TYPE AddrType, uint8_t Speed, DCC_DIRECTION Dir, DCC_SPEED_STEPS SpeedSteps) {}
  virtual void notifyDccFunc(uint16_t Addr, DCC_ADDR_TYPE AddrType, FN_GROUP FuncGrp, uint8_t FuncState) {}
  virtual void notifyDccAccTurnoutBoard(uint16_t BoardAddr, uint8_t OutputPair, uint8_t Direction, uint8_t OutputPower) {}
  virtual void notifyDccAccTurnoutOutput(uint16_t Addr, uint8_t Direction, uint8_t OutputPower) {}
  virtual void notifyDccSigOutputState(uint16_t Addr, uint8_t State) {}
  virtual uint8_t notifyCVRead(uint16_t CV) { return 0; }
  virtual uint8_t notifyCVWrite(uint16_t CV, uint8_t Value) { return 0; }
  virtual void notifyCVResetFactoryDefault(uint8_t value = 0x08) {}
  virtual void notifyCVAck() {}
  virtual void notifyAdvancedCVAck() {}

  void setConfiguration() {
    uint8_t cv29 = notifyCVRead(CV29_CM_CONFIG_DATA);
    m_config.addressMultiFunction = -1;
    m_config.addressAccessory = -1;
    m_config.cv29_addressing_method = (cv29 & CV29_A_ADDRESSING_METHOD);
    m_config.cv29_fl_location = (cv29 & CV29_L_FL_LOCATION);

    // Accessory Decoder
    if (cv29 & CV29_C_ACCESSORY_DECODER) {

      // Output Address method
      // CV01,CV09: 76543210 .....A98 (CV01=0-255, CV09=0-7)
      // Packet:    ..765432 .A98.10. (ADRS=1-2044)
      if (cv29 & CV29_A_ADDRESSING_METHOD) {
        m_config.addressAccessory =
          ((notifyCVRead(CV09_AM_ADDRESS_MSB) & 0x07) << 8)
          | notifyCVRead(CV01_AM_ADDRESS_LSB);
      } else

      // Decoder Address method
      // CV01,CV09: ..543210 .....876 (CV01=0-63, CV09=0-7)
      // Packet:    ..543210 .876.... (ADRS=0-511)
      {
        m_config.addressAccessory =
          ((notifyCVRead(CV09_AM_ADDRESS_MSB) & 0x07) << 6)
          | (notifyCVRead(CV01_AM_ADDRESS_LSB) & 0x3F);
      }
    } else

    // Multi-Function Decoder
    {
      // Two byte addressing (Extended addressing)
      // CV17,CV18: ..DCBA98 76543210 (CV17=0-39/192-231, CV18=0-255)
      // Packet:    ..DCBA98 76543210 (ADRS=128-9999)
      if (cv29 & CV29_L_EXT_ADDRESSING) {
        m_config.addressMultiFunction =
          ((notifyCVRead(CV17_LO_EXT_ADDRESS_MSB) & 0x3F) << 8)
          | notifyCVRead(CV18_LO_EXT_ADDRESS_LSB);
      } else

      // One byte addressing
      // CV01:      .6543210 (CV01=1-127)
      // Packet:    .6543210 (ADRS=1-127)
      {
        m_config.addressMultiFunction =
          (notifyCVRead(CV01_LM_PRIMARY_ADDRESS) & 0x7F);
      }
    }
  }

public:
  void setup() {
    reset();
    m_queue.init();

    // PORT
    PORTA.DIRCLR = PIN_DCC_CMD_bm;
    PORTA.OUTCLR = PIN_DCC_ACK_bm;
    PORTA.DIRSET = PIN_DCC_ACK_bm;

    // EVSYS (Event System)
    EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH0_gc;    // TCB0
    EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_PORTA_PIN3_gc;      // PA3 (PIN_DCC_CMD)

    // TCB (16-bit Timer/Counter Type B)
    TCB0.CTRLB = TCB_CNTMODE_PW_gc;                     // Pulse Width Measurement Mode
    TCB0.EVCTRL = TCB_CAPTEI_bm | TCB_FILTER_bm;
    TCB0.INTCTRL = TCB_CAPT_bm;
    TCB0.CTRLA = TCB_ENABLE_bm | TCB_CLKSEL_CLKDIV2_gc; // f=fCLK_PER/2

    // Configuration
    if (notifyCVRead(CV08_CM_MANUFACTURER_ID) != CV08_C_PUBLIC_DOMAIN) {
      notifyCVResetFactoryDefault();
    }
    setConfiguration();
  }

  void interrupt() {
    uint16_t width = TCB0.CCMP;
    PORTA.PIN3CTRL ^= PORT_INVEN_bm;                    // PA3 (PIN_DCC_CMD)

    m_debug.forInterrupt(1);
    processPulse(width / (F_CPU / 1000000UL / 2));      // T=6553us
    m_debug.forInterrupt(0);
  }

  void process() {
    Packet packet;
    m_queue.pop(packet);

    if (packet.size > 0) {
      #ifdef DEBUG_UART_PACKET
      m_serial.write(packet.preamble);
      m_serial.write(packet.size);
      for (int i = 0; i < packet.size; i++) {
        m_serial.write(packet.data[i]);
      }
      #endif
      parsePacket(packet);
    }
  }

};

//------------------------------------------------------------------------------
// Motor Driver Class

class MotorDriver {
private:
  static const bool MOTOR_DRIVER_USE_PWM = true;

  static const uint16_t PWM_PER = 199;                  // 100kHz=20MHz/1/(PER+1)
  static const uint16_t PWM_LOW = 0;
  static const uint16_t PWM_HIGH = PWM_PER + 1;

public:
  enum Mode {
    MODE_STOP,
    MODE_FORWARD,
    MODE_REVERSE,
    MODE_BREAK,
  };

  enum Limit {
    LIMIT_OFF = 0,
    LIMIT_ACK = 1000,
    LIMIT_LOCO = 1500,
    LIMIT_TURNOUT = 1500,
  };

  void setup() {

    // PORT
    PORTA.OUTCLR = PIN_OUT_FWD_bm | PIN_OUT_REV_bm;
    PORTA.DIRSET = PIN_OUT_FWD_bm | PIN_OUT_REV_bm;

    // DAC (Digital-to-Analog Converter)
    VREF.CTRLA = VREF_DAC0REFSEL_4V34_gc;
    DAC0.CTRLA |= DAC_OUTEN_bm;
    DAC0.DATA = 0;
    DAC0.CTRLA |= DAC_ENABLE_bm;

    // TCA (16-bit Timer/Counter Type A)
    if (MOTOR_DRIVER_USE_PWM) {
      PORTMUX.CTRLC |= PORTMUX_TCA00_ALTERNATE_gc;
      TCA0.SINGLE.CTRLA &= ~TCA_SINGLE_ENABLE_bm;
      TCA0.SINGLE.CTRLD &= ~TCA_SINGLE_SPLITM_bm;
      TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc;
      TCA0.SINGLE.CTRLB = TCA_SINGLE_WGMODE_SINGLESLOPE_gc |
        TCA_SINGLE_CMP1EN_bm |                          // TCA0.WO1=PA1 (PIN_OUT_FWD)
        TCA_SINGLE_CMP0EN_bm;                           // TCA0.WO0=PA7 (PIN_OUT_REV)
      TCA0.SINGLE.CTRLC = 0;
      TCA0.SINGLE.PER = PWM_PER;                        // f=fCLK_PER/N/(PER+1)
      TCA0.SINGLE.CMP1 = PWM_LOW;
      TCA0.SINGLE.CMP0 = PWM_LOW;
      TCA0.SINGLE.CTRLA |= TCA_SINGLE_ENABLE_bm;
    }
  }

  void setLimit(uint16_t millivolt) {
    uint16_t data = (millivolt + 8) / 17;               // volt=4.34V*data/0xFF
    DAC0.DATA = ((data <= 0xFF) ? data : 0xFF);
  }

  void setMode(Mode mode, uint8_t duty = 1, uint8_t period = 1) {
    switch (mode) {
    case MODE_STOP:
      if (MOTOR_DRIVER_USE_PWM) {
        TCA0.SINGLE.CMP1 = PWM_LOW;
        TCA0.SINGLE.CMP0 = PWM_LOW;
      } else {
        PORTA.OUTCLR = PIN_OUT_FWD_bm | PIN_OUT_REV_bm;
      }
      setLimit(LIMIT_OFF);
      break;
    case MODE_FORWARD:
      if (MOTOR_DRIVER_USE_PWM) {
        TCA0.SINGLE.CMP1 = PWM_HIGH;
        TCA0.SINGLE.CMP0 = PWM_HIGH * (period - duty) / period;
      } else {
        PORTA.OUTSET = PIN_OUT_FWD_bm;
        PORTA.OUTCLR = PIN_OUT_REV_bm;
      }
      break;
    case MODE_REVERSE:
      if (MOTOR_DRIVER_USE_PWM) {
        TCA0.SINGLE.CMP0 = PWM_HIGH;
        TCA0.SINGLE.CMP1 = PWM_HIGH * (period - duty) / period;
      } else {
        PORTA.OUTSET = PIN_OUT_REV_bm;
        PORTA.OUTCLR = PIN_OUT_FWD_bm;
      }
      break;
    case MODE_BREAK:
      if (MOTOR_DRIVER_USE_PWM) {
        TCA0.SINGLE.CMP1 = PWM_HIGH;
        TCA0.SINGLE.CMP0 = PWM_HIGH;
      } else {
        PORTA.OUTSET = PIN_OUT_FWD_bm | PIN_OUT_REV_bm;
      }
      break;
    }
  }

} m_driver;

//------------------------------------------------------------------------------
// My DCC Decoder Class

class MyDccDecoder : public DccDecoder {
private:

  struct CVSet {
    uint16_t index;
    uint8_t data;
  };
  
  enum CV_INDEX_OPTION {
    CV33_AO_MU_MODE           = 33,     // Manufacturer Unique: Decoder Mode
    CV34_AO_MU_DELAY          = 34,     // Manufacturer Unique: Trigger Delay
  };

  enum CV33_MU_MODE {
    CV33_L_MAGNETIC_TURNOUT   = 0,      // Magnetic force drive Turnout mode
    CV33_L_MAGNETIC_TURNOUT_E = 1,      // Magnetic force drive Turnout mode
    CV33_L_MOTOR_LIGHT_ACC    = 255,    // Motor rotating force drive Turnout mode, Lighting accessory mode
  };

  // Factory Default for Multi-Function Decoder
  // (referred to Rokuhan A053 DCC Decoder General Purpose type)
  const CVSet m_cvMultiFunctionDefault[12] = {
    { CV01_LM_PRIMARY_ADDRESS, 3 },     // Primary address (2 digit address)
    { CV02_LR_VSTART,          0 },     // Starting voltage
    { CV03_LR_ACCEL_RATE,      0 },     // Acceleration Rate
    { CV04_LR_DECEL_RATE,      0 },     // Deceleration Rate
    { CV05_LO_VHIGH,           160 },   // Highest voltage
    { CV06_LO_VMID,            80 },    // Middle voltage
    { CV07_CM_VERSION,         1 },     // Decoder version
    { CV08_CM_MANUFACTURER_ID, CV08_C_PUBLIC_DOMAIN },
    { CV11_LR_PACKET_TIMEOUT,  0 },     // Packet time-out Time
    { CV17_LO_EXT_ADDRESS_MSB, 192 },   // Extended address (Upper) (4 digit address)
    { CV18_LO_EXT_ADDRESS_LSB, 128 },   // Extended address (Lower) (4 digit address)
    { CV29_CM_CONFIG_DATA,     CV29_L_MFUNCTION_DECODER | CV29_L_FL_LOCATION },
  };

  // Factory Default for Accessory Decoder
  // (referred to Rokuhan A060 DCC Accessory Decoder (Turnout & Lighting))
  const CVSet m_cvAccessoryDefault[9] = {
    { CV01_AM_ADDRESS_LSB,     1 },     // Decoder address (Lower)
    { CV02_AO_AUX_ACTIVATION,  0 },     // Auxiliary input
    { CV03_AO_TIME_ON_F1,      30 },    // Running time
    { CV07_CM_VERSION,         1 },     // Decoder version
    { CV08_CM_MANUFACTURER_ID, CV08_C_PUBLIC_DOMAIN },
    { CV09_AM_ADDRESS_MSB,     0 },     // Decoder address (Upper)
    { CV29_CM_CONFIG_DATA,     CV29_C_ACCESSORY_DECODER | CV29_A_ADDRESSING_METHOD },
    { CV33_AO_MU_MODE,         0 },     // Decoder mode
    { CV34_AO_MU_DELAY,        0 },     // Function delay
  };

  GeneralTimer m_turnout_delay;
  GeneralTimer m_turnout_width;
  uint8_t m_turnout_dir;

protected:
  void notifyDccReset(uint8_t hardReset) override {
    m_driver.setMode(MotorDriver::MODE_STOP);
  }

  void notifyDccSpeed(uint16_t Addr, DCC_ADDR_TYPE AddrType, uint8_t Speed, DCC_DIRECTION Dir, DCC_SPEED_STEPS SpeedSteps) override {
    if (Speed < 2) {
      m_driver.setMode(MotorDriver::MODE_STOP);
      return;
    }
    m_driver.setLimit(MotorDriver::LIMIT_LOCO);
    if (Dir == DCC_DIR_FWD) {
      m_driver.setMode(MotorDriver::MODE_FORWARD, Speed - 1, SpeedSteps);
    } else {
      m_driver.setMode(MotorDriver::MODE_REVERSE, Speed - 1, SpeedSteps);
    }
  }

  void notifyDccAccTurnoutOutput(uint16_t Addr, uint8_t Direction, uint8_t OutputPower) override {
    if (OutputPower) {
      m_turnout_dir = Direction;
      uint8_t cv34 = notifyCVRead(CV34_AO_MU_DELAY);
      m_turnout_delay.start(cv34 * 100UL);              // CV34 x 100ms
    }
  }

  uint8_t notifyCVRead(uint16_t CV) override {
    if (CV >= (uint16_t)EEPROM.length()) {
      return 0;
    }
    return EEPROM.read(CV);
  }

  uint8_t notifyCVWrite(uint16_t CV, uint8_t Value) override {
    if (CV >= (uint16_t)EEPROM.length()) {
      return 0;
    }
    bool updateCV = true;
    bool updateConfig = false;

    switch (CV) {
    case CV08_CM_MANUFACTURER_ID:
      notifyCVResetFactoryDefault(Value);
      updateCV = false;
      updateConfig = true;
      break;
    case CV07_CM_VERSION:
      updateCV = false;
      break;
    case CV29_CM_CONFIG_DATA:
      updateConfig = true;
      break;
    case CV01_LM_PRIMARY_ADDRESS | CV01_AM_ADDRESS_LSB:
    case CV17_LO_EXT_ADDRESS_MSB:
    case CV18_LO_EXT_ADDRESS_LSB:
    case CV09_AM_ADDRESS_MSB:
      updateConfig = true;
      break;
    }

    if (updateCV) {
      EEPROM.update(CV, Value);
    }
    if (updateConfig) {
      setConfiguration();
    }
    return EEPROM.read(CV);
  }

  void notifyCVResetFactoryDefault(uint8_t value = 0x08) override {
    int n = 0;
    const CVSet *cvSet = NULL;
    switch (value) {
    case 0x08:
      n = sizeof(m_cvAccessoryDefault) / sizeof(CVSet);
      cvSet = m_cvAccessoryDefault;
      break;
    case 0x09:
      n = sizeof(m_cvMultiFunctionDefault) / sizeof(CVSet);
      cvSet = m_cvMultiFunctionDefault;
      break;
    }
    for (int i = 0; i < n; i++) {
      EEPROM.update(cvSet[i].index, cvSet[i].data);
    }
  }

  void notifyCVAck() override {
    m_driver.setLimit(MotorDriver::LIMIT_ACK);
    m_driver.setMode(MotorDriver::MODE_FORWARD);
    PORTA.OUTSET = PIN_DCC_ACK_bm;
    delay(6);
    PORTA.OUTCLR = PIN_DCC_ACK_bm;
    m_driver.setMode(MotorDriver::MODE_STOP);
  }

public:
  void setup() {
    DccDecoder::setup();
    m_driver.setup();
  }

  void process() {
    DccDecoder::process();

    if (m_turnout_delay.process()) {
      m_turnout_delay.stop();

      MotorDriver::Mode mode = (m_turnout_dir
        ? (MotorDriver::MODE_FORWARD) : (MotorDriver::MODE_REVERSE));
      uint8_t cv03 = notifyCVRead(CV03_AO_TIME_ON_F1);

      switch (notifyCVRead(CV33_AO_MU_MODE)) {
      case CV33_L_MAGNETIC_TURNOUT:                     // FALLTHROUGH
      case CV33_L_MAGNETIC_TURNOUT_E:
        m_driver.setLimit(MotorDriver::LIMIT_TURNOUT);
        m_driver.setMode(mode);
        m_turnout_width.start(cv03);                    // CV03 x 1ms
        break;
      case CV33_L_MOTOR_LIGHT_ACC:
        m_driver.setLimit(MotorDriver::LIMIT_TURNOUT);
        m_driver.setMode(mode);
        if ((cv03 == 0) || (cv03 == 255)) {
          // No timer (Always ON)
        } else {
          m_turnout_width.start(cv03 * 20UL);           // CV03 x 20ms
        }
        break;
      }
    }

    if (m_turnout_width.process()) {
      m_turnout_width.stop();

      m_driver.setMode(MotorDriver::MODE_STOP);
    }
  }

} m_decoder;

//------------------------------------------------------------------------------
// Main Routine

ISR(TCB0_INT_vect) {
  m_decoder.interrupt();
}

void setup() {
  m_serial.setup();
  m_decoder.setup();
}

void loop() {
  m_serial.process();
  m_decoder.process();
  m_debug.forLoop();
}

以上です。

OWON VDS1022I オシロスコープのUSB切断問題ですが、

以前の記事でソフトを変えて直ったはずだったのですが、

それはぬか喜びでして、結局その後も時々発生して困っていました。

 

このUSBオシロは、

デバイス側も何故か USB Type-Aコネクタという怪しさなのですが、

USBコネクタの接続部分を触って力をかけたりすると切断が起きるという

わけではなく、全く触っていなくても不意に切断が起きたりするので、

元々は電気的な接触の問題ではないだろうなと思っていました。

 

ただ、どうにもならないので分解してみました。

 

実装基板を見ると、

セラミックコンデンサの上にダイオード?が重ねて実装されていたり、

チップ抵抗を横倒しにして無理やり実装されていたり、

こんな状態で量産するより基板を作り直した方がいいのでは?と思えるほどでした。

 

 

それで、基板を裏返してよく見ると、

写真の右側、USBコネクタのランドのうちの1ヶ所でスルーホールの穴が見えます。

まさかー!と思いましたが、半田が濡れていません。

 

 

なので自分で半田付けしました。

写真の上側が半田付け前、下側が半田付け後です。

 

 

その後、今のところ USB切断は起きていません。

Google検索しても日本語サイトで同じ不具合がヒットしなかったのは、

個別不良だったからですね。最初から分解して確認するべきでした。

 

以上です。

実は、タイトルは脈拍モニタとして保険をかけつつも、

将来的にホビー用のパルスオキシメーターを作りたいなと思っていました。

 

ですがこの回路では、

赤色LEDと近赤外線LEDを高速に切り替えても

フォトトランジスタの出力にフィルタがかかった後の信号を M5StickCに入力するので、

赤色/近赤外線それぞれの状態でのAD値を取得できないことが今更ながら分かりました。

 

RENESASのアプリケーションノート「ECG, PPG, SpO2 の同時測定制御例」にある

回路のようにフォトトランジスタの出力を直接入力できるマイコンを選定しないと

シンプルに実現できなさそうですね。

 

https://www.renesas.com/us/ja/document/apn/936246

 

秋月の回路でも、入力段の47uFをかなり小さくして、

オペアンプのローパスフィルターのコンデンサを外して M5StickCに入力して、

ソフトでローパスフィルタを実装すれば実現できるのかどうか分かりませんが、

自己嫌悪でもうやる気がなくなってしまいましたので中断したいと思います。

 

以下、M5StickCのAD値をグラフ表示するソースコードです。

Aボタン(M5ボタン)を押すと、LEDが消灯→赤色→近赤外線→消灯…、と切り替わり、

Bボタン(横のボタン)を押すと、近赤外線用に縦軸を拡大して表示します。

 

//------------------------------------------------------------------------------
// Pulse Oximeter with M5StickC
// 
// * PlatformIO Project Configuration (platformio.ini)
// [env:m5stick-c]
// platform = espressif32
// board = m5stick-c
// framework = arduino
// lib_deps = m5stack/M5StickC@^0.2.0
// monitor_speed = 115200
//
// * M5StickC Port Configuration
// GPIO26: Digital Output for RED LED
// GPIO0 : Digital Output for IR LED
// GPIO36: Analog Input
//
//------------------------------------------------------------------------------

#include <Arduino.h>
#include <M5StickC.h>

#define PIN_RED 26
#define PIN_IR  0
#define PIN_ADC 36

enum MODE {
  MODE_OFF,
  MODE_IR,
  MODE_RED,
};

class LCD {
private:
  volatile uint8_t m_px;
  volatile bool m_change;
  uint8_t m_scale;

  uint8_t getY(uint16_t adc, uint8_t scale = 0) {
    long min = 0;
    long max = 4095;
    switch (scale) {
    case 0:
      break;
    case 1:
      min = 1800;
      max = 2200;
      break;
    }
    adc = ((adc < min) ? min : adc);
    adc = ((adc > max) ? max : adc);
    return map(adc, min, max, M5.Lcd.height() - 6, 19);
  }

public:
  void setup() {
    M5.Lcd.setRotation(3);
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setTextSize(1);
    M5.Lcd.setTextColor(WHITE, BLACK);
    M5.Lcd.drawString("Pulse Oximeter", 6, 6);
    m_px = 0;
    m_change = true;
    m_scale = 0;
  }

  void showMode(MODE mode) {
    static const uint8_t px = 102;
    static const uint8_t py = 6;
    switch (mode) {
      case MODE_OFF:
        M5.Lcd.drawString("   ", px, py);
        break;
      case MODE_RED:
        M5.Lcd.drawString("RED", px, py);
        break;
      case MODE_IR:
        M5.Lcd.drawString("IR ", px, py);
        break;
    }
  }

  void next() {
    m_px = (m_px + 1) % M5.Lcd.width();
    m_change = true;
  }

  void changeScale() {
    m_scale = (m_scale + 1) % 2;
  }

  void drawGraph(uint16_t adc) {
    uint8_t px = m_px;

    if (m_change) {
      m_change = false;
      uint32_t color = ((m_scale == 0) ? 0x18E3 : 0x18E7);
      M5.Lcd.drawLine(px, getY(0), px, getY(4095), color);
    }
    M5.Lcd.drawPixel(px, getY(adc, m_scale), WHITE);
    M5.Lcd.setCursor(M5.Lcd.width() - 6 * 5, 6);
    M5.Lcd.printf("%4d", adc);
  }

} m_lcd;

class LED {
private:
  MODE m_mode;

public:
  void setup() {
    pinMode(PIN_RED, OUTPUT);
    pinMode(PIN_IR, OUTPUT);
    pinMode(M5_LED, OUTPUT);
    digitalWrite(M5_LED, HIGH);
    setMode(MODE_OFF);
  }

  void setMode(MODE mode) {
    m_mode = mode;
    switch (mode) {
      case MODE_OFF:
        digitalWrite(PIN_IR, LOW);
        digitalWrite(PIN_RED, LOW);
        break;
      case MODE_RED:
        digitalWrite(PIN_IR, LOW);
        digitalWrite(PIN_RED, HIGH);
        break;
      case MODE_IR:
        digitalWrite(PIN_RED, LOW);
        digitalWrite(PIN_IR, HIGH);
        break;
    }
  }

  MODE getMode() {
    return m_mode;
  }

} m_led;

void IRAM_ATTR onTimer();

class PPG {
private:
  hw_timer_t *m_timer;
  volatile uint8_t m_state;

public:
  uint16_t m_adc;

  void setup() {
    m_state = 0;

    analogSetAttenuation(ADC_11db);
    analogSetWidth(12);
    pinMode(PIN_ADC, ANALOG);

    m_timer = timerBegin(0, getApbFrequency() / 1000000UL, true);
    timerAttachInterrupt(m_timer, &onTimer, true);
    timerAlarmWrite(m_timer, 1000ULL, true);  // 1ms
    timerAlarmEnable(m_timer);
  }

  void interrupt() {
    m_adc = analogRead(PIN_ADC);
    m_state = 1;
  }

  void process() {
    switch (m_state) {
    case 1:
      m_lcd.drawGraph(m_adc);
      m_state = 0;
      break;
    }
  }

  uint8_t state() {
    return m_state;
  }

  void filter() {
    // Raw
    // LPF (Low Pass Filter)
    // HPF (High Pass Filter)
    // DF  (Derivative Filter)
    // SSF (Slope Sum Function Filter)
    // MAF (Moving Average Filter)
    // Threshold
  }

} m_ppg;

void IRAM_ATTR onTimer() {
  m_ppg.interrupt();
}

//------------------------------------------------------------------------------
// Main Routine

unsigned long m_micros;

void setup() {
  M5.begin();
  m_micros = 0;

  m_lcd.setup();
  m_led.setup();
  m_led.setMode(MODE_OFF);
  m_lcd.showMode(MODE_OFF);
  m_ppg.setup();
}

void loop() {
  M5.update();
  m_ppg.process();

  if (M5.BtnA.wasPressed()) {
    switch (m_led.getMode()) {
    case MODE_OFF:
      m_led.setMode(MODE_RED);
      m_lcd.showMode(MODE_RED);
      break;
    case MODE_RED:
      m_led.setMode(MODE_IR);
      m_lcd.showMode(MODE_IR);
      break;
    case MODE_IR:
      m_led.setMode(MODE_OFF);
      m_lcd.showMode(MODE_OFF);
      break;
    }
  }
  if (M5.BtnB.wasPressed()) {
    m_lcd.changeScale();
  }

  if ((micros() - m_micros) > 50000UL) {
    m_lcd.next();
    m_micros = micros();
  }
}

 

以上です。

秋月電子で「NJL5501R搭載 パルスオキシメータ用・反射型センサ

DIP化モジュールキット」[AE-NJL5501R][K-09433]が売っていましたので、

ホビー用の脈拍モニタを作ろうと思います。

 

回路は秋月のページにある配線図そのままですが、

LEDの制限抵抗だけ750Ω(5V用)→510Ω(3.3V用)に変更しています。

https://akizukidenshi.com/catalog/g/gK-09433

 

 

まずは、ミニブレッドボード BB-601 [P-05155]用の実体配線図を作って

組み立てたところ、オシロで脈拍パルスを観測できました。

 

 

 

その後に、ユニバーサル基板 Eタイプ(13x8穴)[P-12725]用の実体配線図を作って

半田付けしました。

 

 

M5StickCに接続して、とりあえず、

・赤色LED、近赤外線LEDの点灯制御 (ボタン押下で切替え)

・オペアンプのアナログ出力のA/D変換値の表示

ができるようになりました。

 

これから

・脈拍波形の画面表示処理

・脈拍波形のピーク検出処理

を実装していこうと思います。

 

 

以上です。

前回の続きです。自作DCCデコーダーを使って、

KATOのNゲージ複線両渡りポイント(WX310)を制御できましたので、

その動画をYouTubeにアップしました。

 

動画では、

2個のポイントのデコーダーアドレスは同じ設定(デフォルトの1)にしていますが、

手前のポイントのみ 100ms遅延の設定(CV34=1)をしています。

 

 

分解前。

 

 

底面の金属カバーを外した状態。

最初、先に金属カバーのネジを外しても金属カバーが外れなくて困りましたが、

下の分解動画のおかげで中央の樹脂部品を先に外す必要があることが分かり、

大変助かりました。

KATO 複線両渡りポイントの改造方法 / 配線方法 / Nゲージ 鉄道模型 【Michel Cleman】 - YouTube

 

 

自作DCCデコーダーを取り付けた状態。

緑色と水色がレールからDCCデコーダーへの給電線で、

赤色と白色が電磁石を駆動する出力線です。

また、非選択式になるように配線しています。

 

 

合計 2個作りました。

 

 

以上です。

DCCポイントデコーダーの作成の続きです。

 

ゴールデンウィーク中はほとんどマイコンのファーム開発をしていて、

ようやくロクハンA060相当(のやや劣化版?)まで実装できました。

もう少ししたらソースコードを公開したいと思います。

 

それで、KATOのNゲージ電動ポイント(EP150-45R/EP150-45L)を

制御できましたので、その動画をYouTubeにアップしました。

 

 

デコーダー基板は3枚追加実装しました。

この基板の場合、半田付けは、チップ抵抗 → 半導体部品

→ チップ積層セラミックコンデンサ、の順番で行うとやりやすいです。

 

 

ATtiny412マイコンにファームを書き込んでから電動ポイントの道床に組み込みました。

 

部品高さを含めた実装基板の総厚みが4.1~4.2mmくらいになってしまい、

そのままだと道床がわずかに浮いてしまうので、

基板が当たる部分(道床の裏側)は平ヤスリで少し削りました。

また灰色のカバーは、黒色の配線と干渉する部分をカットしました。

 

 

カバー取付け後。

 

 

自作DCCコマンドステーション(「M5Stack DCCコマンドステーションをnanoKONTROL2で操作する鉄道模型デモ」の記事参照)で制御しました。

 

今までアドレス指定は 1~511(Decoder-Addressモード)でしたが、

今回 1-2044 (Output-Addressモード)に変更しました。

 

下の記事が大変参考になりました。ありがとうございます。

・電機屋の毎日 激安DCCデコーダの開発状況 その3

・ゲヌマ・フジガヤ2 アクセサリデコーダのアドレスの説明

 

 

一応裏側です(カバーを外した状態です)。

 

 

以上です。

鉄道模型用のDCCポイントデコーダーを自作しましたので簡単にまとめます。

マイコンのファームはまだ開発途中です。

 

■ 経緯

 

以前、

M5Stack DCCコマンドステーションをnanoKONTROL2で操作する鉄道模型デモ」という

ブログで書いたように、ロクハン Zゲージの小型卓上レイアウトを作っていたのですが、

・ 動力シャーシにオモリを乗せないと走行が安定しないので車体が取り付けられない

・ ポイントでよく脱線する

・ DCCポイントデコーダーの存在感が大きすぎて見栄えがよくない

など、今後の拡張や展開が大変そうで気持ちが少し萎えていました。

 

そのため、

Nゲージと Bトレインショーティーに少しステップアップすることにした次第です。

 

■ 車両

 

とりあえず車両は4両買いました。

 

 

■ レールとポイント

 

レールやポイントは、KATO ユニトラック コンパクト シリーズで集めました。

 

 

CV2 ポイントセットに含まれているのは電動ポイント EP150-45L & EP150-45R で、

これのコネクタ接続部分に自作DCCポイントデコーダーを組み込めるようにします。

 

 

 

 

■ ポイントデコーダーの仕様

 

先人の素晴らしい成果を参考にさせていただきつつ、

ポイントデコーダーの仕様を次の観点で検討しました。

 

・ 道床の内部に入る大きさであること

・ 半田付けが容易であること (部品点数やピン数が少なく、狭ピッチでないこと)

・ 生基板以外の部品は秋月電子通商か Digi-Keyで入手できること

・ マイコンのファームは VSCode+PlatformIOを使って C++で開発できること

・ マイコンのファームの書込み環境(ROMライタや治具など)も安価であること

 

その結果、ポイントデコーダーの仕様は次のようになりました。

 

・ 基板サイズは 16x8mm、部品含めた厚みは 4mm程度

・ マイコンは Microchip ATtiny412-SSN (最新のtinyAVR1シリーズ)

・ モータードライバーは TOSHIBA TB67H450FNG

 

 

 

■ ポイントデコーダーの回路と基板

 

回路入力と基板パターン設計は KiCadで行いました。部品表は下表になります。

 

ATtiny412は秋月での取扱いが無いのが惜しいですね。

また R1,R2のチップ抵抗100kΩは、秋月ではこれしか見つかりませんでしたが、

精度は全く必要ないので、実際にはDigi-Keyから安価な 5%品を選んだ方がよいですね。

 

記号 員数 品名 品番 コード
C1,C2 2 チップ積層セラミックコンデンサー 10uF 35V X5R 2012 GRM21BR6YA106KE43 秋月 P-13336
D1 1 表面実装用ショットキーバリアダイオードブリッジ 60V 2A TS260S 秋月 I-15166
R1,R2 2 超精密級 金属皮膜チップ抵抗器 1608 100kΩ 0.1% RG1608N-104-B-T5 秋月 R-11792
R3 1 角形低抵抗チップ抵抗器 0.2W 0.1Ω±1% SR731JTTDR100F 秋月 R-09570
U1 1 AVRマイコン ATTINY412-SSN ATTINY412-SSN Digi-Key
U2 1 ブラシ付きDCブラシモータドライバ TB67H450FNG TB67H450FNG 秋月 I-14967
U3 1 表面実装型三端子レギュレーター 5V 150mA TA78L05F TA78L05F 秋月 I-00494

 

以下、回路の説明です。

 

・ ブリッジダイオード D1(TS260S)とコンデンサ C1(10uF)を使って

    DCCのAC電源を整流して +12V DC電源を作り、モータードライバーの電源とします。

・ 三端子レギュレータ U3(TA78L05F)とコンデンサ C2(10uF)を使って

    +12Vから +5V DC電源を作り、マイコンの電源とします。

・ レールの片方 J1(RAIL_R)から抵抗(R1,R2)で分圧してマイコンに入力し、

    DCC信号を検出します。

    個人的には、抵抗を通したとしても 12V系からマイコンのポートに接続するのは

    良くない設計と思っていた(トランジスタやFETを通したい)のですが、

    マイコンのデータシートに「If Vpin is greater than VDD+0.6V, then a current

    limiting resistor is required. The positive DC injection current limiting resistor

    is calculated as R = (Vpin-(VDD+0.6))/ICn.」と書かれていたので、

    問題ないようですね。でも一応下側の R2も追加しました。

 ・ マイコン U1(ATTINY412)のポートは、VCC電源(1ピン)、GND(8ピン)、

    ファーム書込み用の UPDIピン(6ピン)、DAC出力ピン(2ピン)がまず決まり、

    他は基板パターン配線の都合で決めました。

・ モータードライバー U2(TB67H450FNG)は、せっかくなので電流制限機能が

    使えるように、RSピンに 0.1Ω抵抗を接続し、VREFピンにはマイコンの

    DAC出力を接続しました。

    DAC出力を 0.1Vに設定すると 100mA制限、1Vに設定すると 1A制限になります。

 

 

以下、基板の説明です。

 

・ まず、KATO 電動ポイントのコネクタ接続端子を直接半田付けできるように、

    モータードライバー出力のパッド J3(+)/J4(-) を 3mmピッチで配置しました。

・ その両側にレールからの接続パッド J1(R)/J2(L) を配置しました。

    片面パッドでは配線に力がかかったときに剥がれてしまう恐れがあるので、

    スルーホールビアを設けて強化しています。

・ マイコンは、電動ポイントに取り付けた状態でもファーム書込みができるように

    表側に配置しました。

・ モータードライバーは、ICの底面に放熱用の端子があるのですが、

    ポイント動作用なので通電時間が短く発熱は少ないはず、

    小型基板で GNDベタパターンもないので放熱効果は期待できない、

    そもそも手半田付けできない、

    パターン配線の大きな制約になる、

    などの理由でパッドは設けず、絶縁のためにベタシルクにしました。

・ その他の部品は、配線がスマートになるように配置しました。

 

 

■ 基板の試作と実装

 

生基板は Seeed FusionPCBに 10シート(400枚分)発注しました。

価格は、基板 $12.90 + 送料 $17.85 = 合計 $30.75 (\3,522)、でした。

 

基板仕様

・ 材質: FR-4

・ 層数: 両面

・ 寸法: 80x64mm (個片サイズ 16x8mmを 5x8個で面付け、Vカット)

・ 板厚: 1.0mm

・ レジスト: 緑色

・ 表面処理: 鉛フリー半田レベラー

 

製造履歴

・2021/4/10(土) 夜 … ガーバーデータ送付&発注

・2021/4/12(月) 夜 … データに不備あり(このままだと追加費用が必要)との連絡

・2021/4/13(火) 夜 … ガーバーデータを修正して再送付 (追加費用なし)

・2021/4/20(火) 午後 … 出荷連絡、OCSに引渡し

・2021/4/22(木) 午後 … OCS TOKYO SKYGATE

・2021/4/23(金) 午前 … 佐川急便に引渡し

・2021/4/24(土) 午前 … 到着

 

出来上がりは概ね良いのですが、

・ C1と D1のパッド間のレジストがなくなっており、パッドがつながっている

・ 真ん中の横Vカット 1本が浅すぎてほとんど入っていない (10シート全て)

ということがあり、少し残念でした。

 

 

基板が到着ししだい、早速、部品を実装しました。

肉眼では綺麗に半田付けできたつもりでしたが、写真に撮って見ると汚いですね…。

 

 

■ マイコンのファーム書込みと動作確認

 

マイコンのファーム書込みは、ATtiny416 Xnanoボードの mEDBGを使って行います。

GND(写真の黒色の配線)と UPDI(白色の配線)の 2本を

ICクリップ(aitendo TCLIP-SOP8)に接続して、マイコンを挟むだけなので楽ですね。

デバッグ用の波形測定も ICクリップからオシロスコープに接続して行います。

ICクリップは、意外としっかり ICを保持できて、安定して接続できたのでよかったです。

 

 

マイコンのファームは、とり急ぎ、

・ DCC信号から 0/1ビットの検出処理

・ ビットデータの解析とパケットの構築処理

・ パケットのコマンド解釈と実行処理 (一部のみ)

を実装して、

コマンドステーションからモータードライバーの制御ができるようになりました。

(現在はまだ電動ポイントではなく LEDで確認しています)

 

 

デバッグ用に測定した波形です。

・ CH1(赤色): 抵抗分圧後の DCC信号入力ポート (マイコン 6ピン)

・ CH2(黄色): 0/1ビットの検出結果の出力ポート (マイコン 4ピン)

 

 

以上です。