こちらの記事「OWON VDS1022IオシロスコープのUSB切断問題(解決?)」も参照お願いします。

 

私、製作した基板の信号波形を測定するのに OWON VDS1022I という、

パソコンのUSBポートに接続するタイプのオシロスコープを使っているのですが、

測定中にUSBの切断音と接続音が鳴って測定が一時中断する現象が

頻繁に繰り返し発生していて困っていました。

本家サイトにある PC software for VDS1022(I) (v1.0.33)を使ってもダメでした。

 

それで色々検索したところ、

EEVblog: OWON VDS1022I Quick Teardown というトピックが見つかりまして、

これによると v1.0.29で修正されているらしいのですが、

私の環境では発生していて解決しませんでした。

 

ただ大収穫として、有志が開発している非公式のPCオシロソフト

GitHub: florentbr/OWON-VDS1022 (v1.0.33-cf12) があることが分かり、

これを使ったところUSB切断現象が発生しなくなりました。

これでストレスなく測定ができるようになりました。

 

OEM版 PeakTech 1290 のページで公開されているPCオシロソフト

(PeakTech 1290_Software_VO_C2_1.0.24.rar) の中に

Javaのソースコードが含まれているという…、笑えない話ですね。

 

■ OWON オリジナル版の画面

 

■ Florentbr 非公式版の画面

CH1のデフォルト色が緑色に変わっていましたので、赤色に戻しました。

 

以下、使い方です。

 

(1) OWON オリジナル版のPCオシロソフトをインストールして使えるようにしておく。

(2) GitHub: florentbr/OWON-VDS1022 から「Code」→「Download ZIP」をクリックして、

     ダウンロードした「OWON-VDS1022-master.zip」ファイルを任意のフォルダに展開する。

(3) テキストエディタで下記内容のバッチファイル「launch.bat」を作成して、

     展開したフォルダ(「README.md」があるフォルダ)に置く。

java.exe -Djava.library.path="%cd%\lib\win\amd64" -Duser.dir="%cd%" -Dsub.java2d.dpiaware=false -cp "%cd%\jar\*" com.owon.vds.tiny.Main

(4) バッチファイルを実行すると、Florentbr 非公式版のPCオシロソフトが起動します。

    (「install-win.cmd」を実行する必要はありません)

 

実行中は、PCオシロソフトの画面とは別にコマンドプロンプトが表示されたままに

なるのですが、そこにデバッグログが出力されます。

時々下記の例外が発生していて潜在的な不具合は残ったままのような感じですが、

いずれにしてもUSBの切断&再接続の音が鳴らなくなり、測定が一時中断することが

なくなりよかったです。

ch.ntb.usb.USBException: LibusbJava.usb_bulk_read: libusb0-dll:err [_usb_reap_async] timeout error

 

以上です。

ATtiny416 Xplained Nanoボードを使ってファームを開発するのにあたって、

やはり Arduinoライブラリを使いたいので、

Atmel Studioではなく Arduino IDEや VSCode+PlatformIOを使ったのですが、

ファーム書込時にエラーが出て苦労しましたのでメモを残しておきたいと思います。

 

Arduino IDEの場合

avrdude: Version 6.3-20201216

         Using Port                    : usb
         Using Programmer              : xplainedmini_updi
avrdude: Found CMSIS-DAP compliant device, using EDBG protocol
avrdude: jtag3_edbg_recv(): Inconsistent fragment number; expect 1, got 0

VSCode+PlatformIOの場合

CURRENT: upload_protocol = xplainedmini_updi
Looking for upload port...
Uploading .pio\build\ATtiny416\firmware.hex
avrdude: jtag3_edbg_recv(): Inconsistent fragment number; expect 1, got 0

 

どちらも avrdudeを使っているので同じですね。

エラーメッセージで Google検索すると、

どうも mEDBG UPDI (ATtiny416 Xplained Nanoボードの中にある

ATmega32U4マイコン側)のファームを v1.12以上に更新する必要があるとのこと。

 

現状は Version: 1.d (1.0d)で古いようでした。

> cd "C:\Program Files (x86)\Atmel\Studio\7.0\atbackend"

> atfw.exe -t medbg -r
Found medbg:ATML~
Master MCU Version: 1.d

ただ、Atmel Studioに入っている mEDBG UPDIのファームも 1.0dでした。

C:\Program Files (x86)\Atmel\Studio\7.0\tools\mEDBG\medbg_fw.zip

-> avrtools_fw.xml

-> <image id="0xFC" filename="medbgupdi.bin" major="0x01" minor="0x0D" build="43" type="MCU" pagesize="0x000" base_address="0x000000" description="MCU firmware" md5="FFFF"/>

 

それで更に検索したところ、

GitHubに v1.13がアップされていましたのでこれを使いたいと思います。

GitHub - MCUdude/microUPDIcore

/microUPDIcode/avr/firmwares/mEDBG_UPDI_1.13_AREF_fix.hex

 

上の HEXファイル(Intel HEXフォーマット)をダウンロードして、

Binexというツールを使って下記コマンドで BINフォーマットに変換します。

その際、7000h-7FFFhのエリアは DFU (Device Firmware Upgrade)という領域

(=USB経由でファームを更新するブートローダ領域)のようなので削除します。

(実際には 6FFFhも削除しないとダメでした)

> BINEX.EXE /BV /::0-6FFE /Omedbgupdi.bin mEDBG_UPDI_1.13_AREF_fix.hex

 

そうして作成した「medbgupdi.bin」ファイルと、

元々 Atmel Studioに入っていた mEDBGの「avrtools_fw.xml」ファイルの2つを、

zipファイル(medbg_fw_updi_v1.13.zip)にパックします。

 

(avrtools_fw.xmlファイルの中の minor属性などは修正した方がよいかもしれませんが、

そのままでも大丈夫でした。ただ Atmel Studioから誤って参照されないように、

zipファイルは C:\Program Files (x86)\Atmel\Studio\7.0以外のフォルダに

置いておいた方がよいかもしれません)

 

それで、atfwコマンドを実行すると、

mEDBGのファームを v1.13に更新することができました。

> cd "C:\Program Files (x86)\Atmel\Studio\7.0\atbackend"
> atfw.exe -t medbg -a "~\medbg_fw_updi_v1.13.zip"
Found medbg:ATML~
Upgrading medbg:ATML~
Upgrading MCU firmware: [==========]
Successful upgrade
> atfw.exe -t medbg -r
Found medbg:ATML~
Master MCU Version: 1.13

 

それで改めて Arduino IDEや VSCode+PlatformIOで試すと、

無事、自分のファームを書き込むことができるようになりました。

ATtiny416 Xplained Nano (Evaluation Kit)Digi-Keyで買いまして、

2ピン・デジタル出力の温度センサLMT01(TI秋月電子)を使って温度を測定しました。

 

■ 温度センサLMT01について

 

この温度センサは、Low時はTyp.34uA、High時はTyp.125uA (Max 143uA)の電流が

流れるようになっているため、10kΩの抵抗をつけたときのポート入力電圧は

それぞれ 0.34V、1.25V (1.43V) になります。

VP-VN間は2V以上が必要なため、電源電圧は3.5V程度以上が必要になります。

(電源電圧を3.3Vで使いたいときは抵抗値を低くする必要があります)

 

それで、Power Onから Typ.50msの変換時間が経過すると、温度に応じて

88kHzのパルスが出力されるため、このパルスの数をカウントして温度に換算します。

換算式は、温度[℃]=(パルス数-808)/16、となります。

 

 

■ ATtiny416(tinyAVR1)のポート割当

 

温度センサからの入力電圧を ATtiny416の AC0 (Analog Comparator)で受けて、

そのパルス数を TCA0 (16-bit Timer/Counter Type A)でカウントするのですが、

EVSYS (Event System)の仕様を見ると AC0から TCA0へつながる内部パスがありません。

そのため、AC0の結果を一旦ポート出力して、それを TCA0に入力する形にしました。

 

そのため、まず、

・PA7: AC0.AINP0 (LMT01 VN)

・PA5: AC0.OUT

が決まります。

 

あとは、ブレッドボードの作りやすさから、

・PA6: PORT.OUT (LMT01 VP)

・PA4: PORT.OUT (近くに GNDがないので Low出力して GNDの代用)

が決まりました。

 

以下、ブレッドボードの写真です。

 

 

■ ATtiny416(tinyAVR1)の機能設定

 

AC0 (Analog Comparator)の閾値電圧は、0.34Vと 1.25Vの中間値とするために

DAC0 (Digital to Analog Converter)を使用します。

 

・VREF (Voltage Reference): 2.5V

・DAC0: DATA=0x50

と設定することで、閾値電圧(=DAC0出力電圧)は 0.78V (=2.5V*0x50/0xFF)になります。

 

それで以下の波形になり、うまく閾値判定できました。

・CH1(赤色) PA7: AC0.AINP0 (LMT01 VN)

・CH2(黄色) PA5: AC0.OUT

 

 

EVSYS (Event System)は、

・SYNCCH0: PORTA_PIN5

・SYNCUSER0: TCA0

と設定することで、PA5(AC0.OUT)から TCA0につながります。

 

(最初は ASYNCCH0: AC0_OUTを使おうとしたのですが、

こちらはカウンタの中では TCB0又は TCD0にしかつながらず、

外部トリガのカウントに使える TCA0にはつながりませんでした)

 

最後に、

・TCA0: CNTEI=1 (イベント入力によるカウントを有効)

・RTC (Real Time Counter): 1kHz (= 32kHz / 32)

を設定します。

 

■ 温度測定処理の流れ

 

(1) TCA0と RTCのカウンタ値をゼロクリア

(2) PA6 (LMT01 VP)を High出力 (温度センサ Power On)

(3) RTCのカウンタ値が 104以上(変換時間&転送時間が経過)になるまで待機

(4) TCA0のカウンタ値を取得

(5) PA6 (LMT01 VP)を入力に変更 (温度センサ Power Off)

(6) TCA0のカウンタ値と換算した温度をシリアル(USART0)で出力

(7) (1)へ戻って繰り返し

 

シリアル出力をモニタすると以下のようになります。

 

■ ソースコード

 

Atmel Studio 7 (7.0.2397)と ATtiny_DFP (1.8.332)でビルドして動作確認しました。

 

/*
 * TemperatureController.cpp
 *
 * Microchip ATtiny416 Xplained Nano (Evaluation Kit)
 *   PA1: USART0.TXD (mEDBG CDC TX)
 *   PA2: USART0.RXD (mEDBG CDC RX)
 *   PA4: PORT.OUT   (GND)
 *   PA5: AC0.OUT
 *   PA6: PORT.OUT   (LMT01 VP)
 *   PA7: AC0.AINP0  (LMT01 VN)
 *   PB5: PORT.OUT   (User LED)
 *
 * Temperature Sensor TI LMT01
 *   IOH:   125uA(typ)
 *   IOL:    34uA(typ)
 *   tCONV:  54ms(max)
 *   tDATA:  50ms(max)
 */

#define F_CPU (20000000UL / 6)  // 20MHz / 6

#include <avr/io.h>
#include <stdio.h>
#include <stdarg.h>

//-----------------------------------------------------------------------------
// Sub Routine

class CQUEUE {
private:
    static const uint8_t MAX = (1 << 5);
    uint8_t m_data[MAX];
    uint8_t m_head;
    uint8_t m_tail;

public:
    CQUEUE() {
        init();
    }

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

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

    bool full(void) {
        return (m_head == ((m_tail + 1) & (MAX - 1)));
    }

    void push(uint8_t data) {
        if (full()) {
            return;
        }
        m_data[m_tail] = data;
        m_tail = (m_tail + 1) & (MAX - 1);
    }

    uint8_t pop(void) {
        if (empty()) {
            return 0;
        }
        uint8_t data = m_data[m_head];
        m_head = (m_head + 1) & (MAX - 1);
        return data;
    }
};

//-----------------------------------------------------------------------------
// AVR USART Class

class CUSART {
private:
    uint8_t m_state;
    uint8_t m_rdata;
    CQUEUE m_tdata;

public:
    static const uint8_t S_READ = 0x01;
    static const uint8_t S_BUSY = 0x02;

    void init(uint32_t speed) {
        m_state = 0;
        m_rdata = 0;
        m_tdata.init();

        // PA1: TXD, PA2: RXD
        PORTA.OUTSET = PIN1_bm;
        PORTA.DIRSET = PIN1_bm;
        PORTA.DIRCLR = PIN2_bm;
        PORTMUX.CTRLB |= PORTMUX_USART0_bm;

        // Speed,8,N,1
        uint32_t baud = (64 * F_CPU) / (16 * speed);
        USART0.BAUD = (uint16_t)(baud * (1024 + SIGROW.OSC20ERR5V) / 1024);
        USART0.CTRLA = 0;
        USART0.CTRLB = USART_RXEN_bm | USART_TXEN_bm;
        USART0.CTRLC = USART_SBMODE_1BIT_gc | USART_CHSIZE_8BIT_gc;
    }

    void write(const uint8_t data) {
        while (m_tdata.full()) {
            update();
        }
        m_tdata.push(data);
    }

    void write(const uint8_t *data, int size) {
        for (; size > 0; size--, data++) {
            write(*data);
        }
    }

    void write(const char *format, ...) {
        char data[32];
        va_list args;
        va_start(args, format);
        int size = vsnprintf(data, sizeof(data), format, args);
        va_end(args);
        write((const uint8_t *)data, size);
    }

    uint8_t update(void) {
        if (!m_tdata.empty()) {
            if (USART0.STATUS & USART_DREIF_bm) {
                m_state |= S_BUSY;
                USART0.TXDATAL = m_tdata.pop();
            }
        }
        if (USART0.STATUS & USART_TXCIF_bm) {
            USART0.STATUS = USART_TXCIF_bm;
            m_state &= ~S_BUSY;
        }
        if (USART0.STATUS & USART_RXCIF_bm) {
            m_rdata = USART0.RXDATAL;
            m_state |= S_READ;
        }
        return m_state;
    }

    uint8_t read(void) {
        m_state &= ~S_READ;
        return m_rdata;
    }
};

//-----------------------------------------------------------------------------
// AVR PORT Class

class CPORTOUT {
private:
    PORT_t *m_port;
    uint8_t m_bit;

public:
    void init(PORT_t *port, uint8_t bit) {
        m_port = port;
        m_bit = 1 << bit;

        m_port->OUTCLR = m_bit;
        m_port->DIRSET = m_bit;
    }

    void set(uint8_t value) {
        if (value) {
            m_port->OUTSET = m_bit;
        } else {
            m_port->OUTCLR = m_bit;
        }
    }

    uint8_t get(void) {
        return ((m_port->OUT & m_bit) != 0);
    }
};

//-----------------------------------------------------------------------------
// Temperature Sensor Class

class CTEMPSENS {
private:
    uint8_t m_state;
    uint16_t m_count;
    PORT_t *m_port;
    uint8_t m_bit;

public:
    static const uint8_t S_BUSY = 0x01;
    static const uint8_t S_DONE = 0x02;
    static const uint16_t S_TIME = 104;

    void init(void) {
        m_state = 0;
        m_count = 0;

        // VREF: 2.5V
        VREF.CTRLA = VREF_DAC0REFSEL_2V5_gc;
        VREF.CTRLB = VREF_DAC0REFEN_bm;

        // DAC0: 0.78V (= VREF * DATA / 0xFF)
        DAC0.DATA = 0x50;
        DAC0.CTRLA = DAC_ENABLE_bm;

        // AC0: POS: PA7, NEG: DAC0, OUT: PA5
        AC0.MUXCTRLA = AC_MUXPOS_PIN0_gc | AC_MUXNEG_DAC_gc;
        AC0.CTRLA = AC_ENABLE_bm | AC_HYSMODE_50mV_gc | AC_OUTEN_bm;

        // EVSYS: SYNCCH0: PA5(AC0.OUT) -> TCA0
        EVSYS.SYNCCH0 = EVSYS_SYNCCH0_PORTA_PIN5_gc;
        EVSYS.SYNCUSER0 = EVSYS_SYNCUSER0_SYNCCH0_gc;

        // TCA0: EVSYS
        TCA0.SINGLE.CNT = 0;
        TCA0.SINGLE.EVCTRL = TCA_SINGLE_CNTEI_bm | TCA_SINGLE_EVACT_POSEDGE_gc;
        TCA0.SINGLE.CTRLA = TCA_SINGLE_ENABLE_bm;

        // RTC: 1kHz (= 32kHz / 32)
        while (RTC.STATUS);
        RTC.CNT = 0;
        RTC.CLKSEL = RTC_CLKSEL_INT32K_gc;
        RTC.CTRLA = RTC_RTCEN_bm | RTC_PRESCALER_DIV32_gc;
    }

    void start(PORT_t *port, uint8_t bit) {
        if (m_state & S_BUSY) {
            return;
        }
        m_state |= S_BUSY;
        m_count = 0;
        m_port = port;
        m_bit = 1 << bit;

        while (RTC.STATUS);
        RTC.CNT = 0;
        TCA0.SINGLE.CNT = 0;
        m_port->OUTSET = m_bit;
        m_port->DIRSET = m_bit;
    }

    uint8_t update(void) {
        if (m_state & S_BUSY) {
            if (RTC.CNT > S_TIME) {
                m_state &= ~S_BUSY;
                m_count = TCA0.SINGLE.CNT;
                m_port->DIRCLR = m_bit;
                m_state |= S_DONE;
            }
        }
        return m_state;
    }

    uint16_t get(void) {
        m_state &= ~S_DONE;
        return m_count;
    }
};

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

int main(void)
{
    CUSART usart;
    CTEMPSENS tempsens;
    CPORTOUT led, gnd;
    uint8_t state;

    led.init(&PORTB, 5);
    led.set(0);
    gnd.init(&PORTA, 4);
    gnd.set(0);

    usart.init(9600);
    tempsens.init();

    usart.write("Temperature Controller\r\n");

    while (1)
    {
        state = tempsens.update();
        if (state == 0) {
            tempsens.start(&PORTA, 6);
        } else
        if (state & CTEMPSENS::S_DONE) {
            uint16_t cnt = tempsens.get();
            uint16_t deg = (cnt - 808) * 10 / 16;
            usart.write("0x%04X (%3d.%d)\r\n", cnt, deg / 10, deg % 10);
        }

        state = usart.update();
        if (state & CUSART::S_READ) {
            uint8_t data = usart.read();
            if (data == '1') {
                led.set(0);
            } else
            if (data == '0') {
                led.set(1);
            }
        }
    }
}

以上です。

前回製作したZVSドライバ基板を使用して、

ワイヤレス給電(WPT)でダイソーのプチ電車を走らせてみました。

基本は「IHモジュールを転用した WPTプラレール走行実験」という記事のままで、

オリジナリティはありません。


■ プチ電車の改造

 

プチ電車は中間車が駆動車となっており、

車体を利用して手持ちのポリウレタン銅線(0.5mm)を10回巻いて受電コイルを作り、

シャーシにホットメルトで固定しました。

 

受電コイルはコンデンサ(0.22uF)を通してブリッジダイオード(SDI2100)のAC側に接続し、

DC側はモーターに直接接続しました。

コンデンサは、当初 0.33uFでは電車の動きが悪かったので、0.22uFにしました。

受電コイルのインダクタンスは測定器がないので不明です…。

また一応インジケーター用に緑色LED+120Ωもつけました。

 

それで製作した電車の写真が以下になります。

ICソケットをブレッドボード代わりにして裏で配線しています。

 

 

■ レールの配線

 

レールはプチ電車シリーズの曲線線路(4本入)を2セット使って円形にし、

送電コイルはレールの裏側にポリウレタン銅線を2回巻いてホットメルトで固定しました。

配線の引き出し部分のループは写真ほど重ねない方がよかったかもしれません。

 

 

 

■ 走行動画

 

直流電源装置をZVSドライバ基板に接続して電源を入れます。

10Vから開始して3周ごとに1Vずつ上げていき、

15Vからは1周ごとに1Vずつ下げていったところ、6Vで停止しました。

電源電流は1~1.5A程度でした。

 

動画の後半では、先頭車と後尾車も連結して走らせています。

 

 

■ 部品表

 

記号 員数 品名 品番 コード 価格
D 1 ショットキーバリアダイオードブリッジ (100V 2A) SDI2100 SDI2100 I-06320 ¥30
C 1 メタライズドポリエステルフィルムコンデンサ 0.22uF 100V MEM224J101RB-5 P-14599 ¥25
- 適量 ポリウレタン銅線 (0.5mm 10m) KP10-05SP-a KYOWA ¥250
合計 ¥305

 

以上です。

最近、「RFワールドNo.43 はじめてのワイヤレス電力伝送」という本を買って

IHモジュールを転用した WPTプラレール走行実験」という記事を見ました。

 

正直なところ、技術的な内容はあまり理解できませんでしたが、

比較的簡単な回路で、ワイヤレス給電でおもちゃ電車を動かせることに驚き、

自分でも試してみたいと思い、まずはZVSドライバを製作しました。

 

「ZVS WPT」や「ZVS IH」でネット検索すると、色々と参考記事が見つかりました。


■ 基板写真

 

それで製作した基板の写真が以下になります。

基板裏側の配線は当初はポリウレタン線を使おうとしたのですが、

半田ごてを当てても被覆が全く溶けなかったので、すずめっき線を使いました。

ダイオードD1,D2は縦に並べました。

 

 

 

 

 

 

■ 配線図(部品面視)

 

 

■ 部品表

 

部品は、入手が容易な秋月電子で集めました。

 

記号 員数 品名 品番 コード 価格
PCB 1 両面スルーホールユニバーサル基板 3*7 3cm*7cm P-12978 ¥35
C1,C2 2 メタライズドポリプロピレンフィルム
コンデンサー 0.33uF 450V
450MPS334J P-05986 ¥40
L1,L2 2 トロイダルコイル 100uH 9A TCV-101M-9A-8026 P-06691 ¥100
D1,D2 2 高耐圧高速整流用ダイオード UF2010 1000V 2A UF2010 I-00124 ¥20
Q1,Q2 2 Nch MOSFET IRFSL3607PBF (75V 80A) IRFSL3607PBF I-06272 ¥90
ZD1,ZD2 2 12V 1W ツェナーダイオード BZX85C12 (20本入) BZX85C12 I-01003 ¥150
R1,R2 2 カーボン抵抗(炭素皮膜抵抗) 1W 470Ω (100本入) CFS100J470RB R-07978 ¥200
R3,R4 2 カーボン抵抗(炭素皮膜抵抗) 1/2W 10kΩ (100本入) CFS50J10KB R-07838 ¥100
JK1 1 ユーロブロック レセプタクル 緑2P ETB43024G000Z P-11599 ¥20
- 1 ユーロブロック ターミナルブロック 緑2P ETB41020G000Z P-11596 ¥40
JK2 1 ユーロブロック レセプタクル 青2P ETB43024B000Z P-11589 ¥20
- 1 ユーロブロック ターミナルブロック 青2P ETB41020B000Z P-11586 ¥50
- 適量 スズメッキ線 (0.6mm 10m) TCW 0.6mm 10m P-02220 -
合計 ¥1,115

 

■ 動作確認

 

直流電源装置から基板に給電するのですが、

念のため上限電流を3Aに設定して確認しました。

電圧が5.6V以下では発振せず、上限電流が流れ続ける状態になりました。

電圧を5.7V以上にすると発振しました。

 

下の波形は電圧8.0Vのときで、電流は0.81A流れました。

・CH1(赤色): Q1-Gate

・CH2(黄色): Q1-Drain

 

 

今回は以上です。

いろいろな人がM5StickCでJJY Simulatorを作っていましたので私も作ってみました。

 

まずは、その動画です。

 

ネットでいろいろ検索したところ、

(1) スイッチサイエンス M5StickC用JJYアンテナ基板

    → やはり外付け回路が必要なのかな…?大変そうだな。

(2) Qiita 標準電波 JJY もどきを M5StickC の Ticker で生成する

    → 適当な配線と抵抗1個でいいのか!これなら簡単そう。

(3) ESP32 (M5Stick-C) で電波時計を合わせよう

    → なんと!外付け全くなしでいいのか!びっくり!

という順番で見つかった感じです。

 

ソースコードはこちらです。

今までArduino IDEで作っていたのですがビルドが遅すぎてストレスだったので、

Visual Studio Code + PlatformIOにしたら快適になりました。

ソースコードの途中にある"<ssid>"と"<password>"は

実際に接続するWLANアクセスポイントのものに書き換えが必要です。

Aボタンを押すとNTPで時刻を再取得し、Bボタンを押すと画面が回転します。


※ 2022/06/29 こちらの関連記事もあります。

 

※ 2022/02/12 時刻取得に失敗することがあるため、

  WiFi.disconnect()を getLocalTime()の後に移動しました。

//------------------------------------------------------------------------------
// JJY Simulator for 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
// GPIO10: Internal LED
// GPIO26: Output for JJY
//
//------------------------------------------------------------------------------
// NICT JJY
//   http://jjy.nict.go.jp/jjy/trans/index-e.html
//
// (1) Time code system
//   :00 :01 :02 :03 :04 :05 :06 :07 :08 :09 :10 :11 :12 :13 :14
//    M  40m 20m 10m  0   8m  4m  2m  1m  P1  0   0  20h 10h  0
//   :15 :16 :17 :18 :19 :20 :21 :22 :23 :24 :25 :26 :27 :28 :29
//    8h  4h  2h  1h  P2  0   0 200d 100d 0  80d 40d 20d 10d  P3
//   :30 :31 :32 :33 :34 :35 :36 :37 :38 :39 :40 :41 :42 :43 :44
//    8d  4d  2d  1d  0   0  PA1 PA2 SU1  P4 SU2 80y 40y 20y 10y
//   :45 :46 :47 :48 :49 :50 :51 :52 :53 :54 :55 :56 :57 :58 :59
//    8y  4y  2y  1y  P5  4w  2w  1w LS1 LS2  0   0   0   0   P0
//
// (2) Pulse Width
//   Marker(M) and position markers(P0~P5): Pulse width 0.2s +-5ms
//   Binary 0: Pulse width 0.8s +-5ms
//   Binary 1: Pulse width 0.5s +-5ms
//
// (3) Marker (M) Position
//   The marker (M) corresponds to the exact minute
//   (the zero second of each minute).
//
// (4) Positions of the Position Markers (P0-P5)
//   The position marker P0 normally corresponds to the start of
//   the 59th second (for non-leap seconds).
//   However, for a positive leap second (insertion of a second),
//   P0 corresponds to the start of the 60th second (in this case,
//   the 59th second is represented by a binary 0).
//   For a negative leap second (removal of a second),
//   P0 corresponds to the start of the 58th second.
//   Position markers P1-P5 correspond to the start of the 9th,
//   19th, 29th, 39th, and 49th seconds, respectively.
//
// (5) Representation of Information
//   (a) Hour (6 bits: 20h,10h,8h,4h,2h,1h)
//     The hour in Japan Standard Time (JST) in 24-hour representation
//     20h,10: The value in BCD at 10 o'clock
//     8h,4h,2h,1h: The value in BCD at 1 o'clock
//   (b) Minute (7 bits: 40m,20m,10m,8m,4m,2m,1m)
//     The JST minute
//     40m,20m,10m: The value in BCD of 10 minutes
//     8m,4m,2m,1m: The value in BCD of 1 minute
//   (c) Annual date (10 bits: 200d,100d,80d,40d,20d,10d,8d,4d,2d,1d)
//     The annual date, counting January 1 as day 1.
//     Thus, Dec. 31 is day 365 in a non-leap year and day 366 in a leap year.
//     200d,100d: The value in BCD of 100 days
//     80d,40d,20d,10d: The value in BCD of 10 days
//     8d,4d,2d,1d: The value in BCD of 1 day
//   (d) Year (8 bits: 80y,40y,20y,10y,8y,4y,2y,1y)
//     The last 2 digits of the dominical year.
//     80y,40y,20y,10y: The value in BCD of 10 years
//     8y,4y,2y,1y: The value in BCD of 1 year
//   (e) Day of the week (3 bits: 4w,2w,1w)
//     The values 0-6 are allocated to Sunday-Saturday.
//   (f) Leap second information (2 bits: LS1,LS2)
//   (g) Parity (2 bits: PA1,PA2)
//     Parity bits are signals to determine whether the hour and minute signals
//     were correctly read. PA1 and PA2 correspond to the hour and minute,
//     respectively, and each is represented by an even parity of 1 bit.
//     PA1 = (20h+10h+8h+4h+2h+1h) mod 2
//     PA2 = (40m+20m+10m+8m+4m+2m+1m) mod 2
//     (mod 2 represents the remainder after division by 2)
//   (h) Spare bits (2 bits: SU1,SU2)
//
//------------------------------------------------------------------------------

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

//------------------------------------------------------------------------------
// Display Function

const char *m_name[] = {
    "M", "40m", "20m", "10m", "0", "8m", "4m", "2m", "1m", "P1",
    "0", "0", "20h", "10h", "0", "8h", "4h", "2h", "1h", "P2",
    "0", "0", "200d", "100d", "0", "80d", "40d", "20d", "10d", "P3",
    "8d", "4d", "2d", "1d", "0", "0", "PA1", "PA2", "SU1", "P4",
    "SU2", "80y", "40y", "20y", "10y", "8y", "4y", "2y", "1y", "P5",
    "4w", "2w", "1w", "LS1", "LS2", "0", "0", "0", "0", "P0"
};

int m_rotation = 1;     // 0,2: Portrait, 1,3: Landscape

void drawTitle() {
  const int px[2] = { 1, 4 };
  Serial.println("* JJY Simulator");
  M5.Lcd.setRotation(m_rotation);
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextSize(1);
  M5.Lcd.setTextColor(WHITE, BLACK);
  M5.Lcd.drawString("JJY Simulator", px[m_rotation & 1], 6);
}

void drawSsid(const char *ssid) {
  const int px[2] = { 5, 10 };
  Serial.printf("SSID: %s\n", ssid);
  M5.Lcd.setCursor(px[m_rotation & 1], 18);
  M5.Lcd.printf("SSID: %s\n", ssid);
}

void drawNtps(const char *ntps) {
  const int px[2] = { 5, 10 };
  Serial.printf("\nNTP Server: %s\n", ntps);
  M5.Lcd.printf("\n");
  M5.Lcd.setCursor(px[m_rotation & 1], M5.Lcd.getCursorY() + 2);
  M5.Lcd.printf("NTP Server: %s\n", ntps);
}

void drawTime(struct tm timeInfo) {
  char buf[32];
  strftime(buf, sizeof(buf), "%Y/%m/%d(%a) %H:%M:%S", &timeInfo);
  Serial.println(buf);
  if (m_rotation & 1) { // Landscape
    M5.Lcd.setCursor(10, 18);
    M5.Lcd.printf(buf);
    M5.Lcd.setCursor(130, 6);
    M5.Lcd.printf("%4s", m_name[timeInfo.tm_sec]);
  } else {              // Portrait
    strftime(buf, sizeof(buf), "%y/%m/%d(%a", &timeInfo);
    M5.Lcd.setCursor(5, 18);
    M5.Lcd.printf(buf);
    strftime(buf, sizeof(buf), "%H:%M:%S", &timeInfo);
    M5.Lcd.setCursor(5, 28);
    M5.Lcd.printf(buf);
    M5.Lcd.setCursor(53, 38);
    M5.Lcd.printf("%4s", m_name[timeInfo.tm_sec]);
  }
}

void drawCode(int second, int bit, int spot = false) {
  const int px[2] = { 7, 12 };
  const int py[2] = { 54, 30 };
  const int md[2] = { 10, 20 };
  int dx = 7;
  int dy = 16;
  int sx = px[m_rotation & 1] + (second % md[m_rotation & 1]) * dx;
  int sy = py[m_rotation & 1] + (second / md[m_rotation & 1]) * dy;
  int wx[] = { 5, 3, 2 };
  int wy = 14;
  int cl[][2] = { { DARKGREEN, GREEN}, { OLIVE, YELLOW }, { MAROON, RED } };
  M5.Lcd.fillRect(sx, sy, wx[bit], wy, cl[bit][spot]);
  M5.Lcd.fillRect(sx + wx[bit], sy, dx - wx[bit], wy, BLACK);
}

//------------------------------------------------------------------------------
// Clock Function

int getDays(int y, int m, int d) {
  if (m <= 2) {
    y -= 1;
    m += 12;
  }
  int dy = 365 * (y - 1);
  int dl = (y / 4) - (y / 100) + (y / 400);
  int dm = 306 * (m + 1) / 10 - 122;
  return (59 + dy + dl + dm + d);
}

void setupTime() {
  const char *ssid = "<ssid>";
  const char *password = "<password>";
  const char *ntpServer =  "ntp.nict.jp";

  drawSsid(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    M5.Lcd.print(".");
    delay(500);
  }
  drawNtps(ntpServer);
  configTime(9 * 3600, 0, ntpServer);
  //WiFi.disconnect(true);
  //WiFi.mode(WIFI_OFF);
  Serial.print(".\n");
  M5.Lcd.print(".\n");

  struct tm timeInfo;
  if (getLocalTime(&timeInfo)) {
    RTC_DateTypeDef t_date;
    RTC_TimeTypeDef t_time;
    t_time.Seconds = timeInfo.tm_sec;
    t_time.Minutes = timeInfo.tm_min;
    t_time.Hours = timeInfo.tm_hour;
    t_date.Date = timeInfo.tm_mday;
    t_date.Month = timeInfo.tm_mon + 1;
    t_date.Year = timeInfo.tm_year + 1900;
    t_date.WeekDay = timeInfo.tm_wday;
    M5.Rtc.SetData(&t_date);
    M5.Rtc.SetTime(&t_time);
  }
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF); 
}

void getTime(struct tm &timeInfo) {
  if (false) {
    getLocalTime(&timeInfo);
  } else {
    RTC_DateTypeDef t_date;
    RTC_TimeTypeDef t_time;
    M5.Rtc.GetData(&t_date);
    M5.Rtc.GetTime(&t_time);
    timeInfo.tm_sec = t_time.Seconds;
    timeInfo.tm_min = t_time.Minutes;
    timeInfo.tm_hour = t_time.Hours;
    timeInfo.tm_mday = t_date.Date;
    timeInfo.tm_mon = t_date.Month - 1;
    timeInfo.tm_year = t_date.Year - 1900;
    timeInfo.tm_wday = t_date.WeekDay;
    timeInfo.tm_yday = getDays(t_date.Year, t_date.Month, t_date.Date)
      - getDays(t_date.Year, 1, 1);
    timeInfo.tm_isdst = 0;
  }
}

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

#define PWM_PIN_LED M5_LED  // GPIO10
#define PWM_PIN_GPO 26      // GPIO26
#define PWM_CH_LED  0
#define PWM_CH_GPO  1
#define PWM_BITS    4
#define PWM_FREQ    40000   // 40 kHz, MAX: 80MHz / (1 << BITS)
#define PWM_DMAX (1 << PWM_BITS)

const int m_wait[] = { 800, 500, 200 };

int m_code[60];             // 0: Binary 0, 1: Binary 1, 2: Marker

void setup() {
  // put your setup code here, to run once:

  M5.begin();
  drawTitle();

  ledcSetup(PWM_CH_LED, PWM_FREQ, PWM_BITS);
  ledcAttachPin(PWM_PIN_LED, PWM_CH_LED);
  ledcWrite(PWM_CH_LED, PWM_DMAX);      // LED OFF

  ledcSetup(PWM_CH_GPO, PWM_FREQ, PWM_BITS);
  ledcAttachPin(PWM_PIN_GPO, PWM_CH_GPO);
  ledcWrite(PWM_CH_GPO, 0);             // GPO OFF

  setupTime();
  drawTitle();
}

void loop() {
  // put your main code here, to run repeatedly:

  static int s_tm_sec = 0;
  static int s_force = true;

  M5.update();
  if (M5.BtnA.wasPressed()) {
    drawTitle();
    setupTime();
    drawTitle();
    s_force = true;
  }
  if (M5.BtnB.wasPressed()) {
    m_rotation = (m_rotation + 1) % 4;
    drawTitle();
    s_force = true;
  }

  // Get Time
  struct tm timeInfo;
  getTime(timeInfo);
  if ((timeInfo.tm_sec == s_tm_sec) && !s_force) {
    delay(1);
    return;
  }
  drawTime(timeInfo);

  // Update Time Code
  if ((timeInfo.tm_sec == 0) || s_force) {
    s_force = false;

    // Convert to BCD
    struct tm ti = timeInfo;
    ti.tm_yday++;
    int bcd_year = (ti.tm_year % 10) + (((ti.tm_year / 10) % 10) << 4);
    int bcd_yday = (ti.tm_yday % 10) + (((ti.tm_yday / 10) % 10) << 5)
      + ((ti.tm_yday / 100) << 10);
    int bcd_wday = ti.tm_wday;
    int bcd_hour = (ti.tm_hour % 10) + ((ti.tm_hour / 10) << 5);
    int bcd_minute = (ti.tm_min % 10) + ((ti.tm_min / 10) << 5);

    for (int i = 0; i < 60; i++) {
      if ((i == 0) || (i % 10 == 9)) {
        // Marker or Position Marker
        m_code[i] = 2;
      } else if ((i >= 1) && (i <= 8)) {
        // Minute
        m_code[i] = (bcd_minute >> (8 - i)) & 1;
      } else if ((i >= 12) && (i <= 18)) {
        // Hour
        m_code[i] = (bcd_hour >> (18 - i)) & 1;
      } else if ((i >= 22) && (i <= 33)) {
        // Annual date
        m_code[i] = (bcd_yday >> (33 - i)) & 1;
      } else if (i == 36) {
        // Parity 1
        int parity = bcd_hour;
        parity ^= parity >> 4;
        parity ^= parity >> 2;
        parity ^= parity >> 1;
        m_code[i] = parity & 1;
      } else if (i == 37) {
        // Parity 2
        int parity = bcd_minute;
        parity ^= parity >> 4;
        parity ^= parity >> 2;
        parity ^= parity >> 1;
        m_code[i] = parity & 1;
      } else if ((i >= 41) && (i <= 48)) {
        // Year
        m_code[i] = (bcd_year >> (48 - i)) & 1;
      } else if ((i >= 50) && (i <= 52)) {
        // Day of the Week
        m_code[i] = (bcd_wday >> (52 - i)) & 1;
      }
      drawCode(i, m_code[i], i == timeInfo.tm_sec);
      Serial.printf("%d", m_code[i]);
    }
    Serial.println("");
  } else {
    drawCode(s_tm_sec, m_code[s_tm_sec], false);
    drawCode(timeInfo.tm_sec, m_code[timeInfo.tm_sec], true);
  }

  // Output Time Code
  ledcWrite(PWM_CH_LED, PWM_DMAX / 2);  // LED ON
  ledcWrite(PWM_CH_GPO, PWM_DMAX / 2);  // GPO ON
  delay(m_wait[m_code[timeInfo.tm_sec]]);
  ledcWrite(PWM_CH_LED, PWM_DMAX);      // LED OFF
  ledcWrite(PWM_CH_GPO, 0);             // GPO OFF

  s_tm_sec = timeInfo.tm_sec;
}

先日作った鉄道模型用のM5Stack DCCコマンドステーションに

nanoKONTROL2(MIDIコントローラ)を接続して操作できるようにしましたので、

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

 

M5Stackの3つのボタンでも、車両1台とポイント1個の制御や、

CVの書込みや読出しが行えるようになっていますが、

nanoKONTROL2をつなぐことで、車両8台(現状はアドレス1~8固定)と

ポイント8個(現状はアドレス1~8固定)の制御が同時にできるようになります。

ただ、一人の手で制御するのは結構忙しくて厳しいですね…。

 

現状は下記の機能のみですが、

どんどん機能を実装して操作や制御をしやすくしようと思っています。

nanoKONTROL2操作 DCC制御
□ボタン (STOP) POWER OFF
△ボタン (PLAY) ALL INIT
○ボタン (REC) ALL STOP
KNOB (回転つまみ) ポイント切替
SLIDER (上下スライダー) 車両の速度制御
Rボタン 車両の方向切替

 

■ デモ動画 (M5Stack DCC Command Station with nanoKONTROL2)

 

 

■ 車両

 

ロクハンのZショーティー動力シャーシ(SA001-1)に

DCCデコーダー小型汎用タイプ(A053)を取り付けて、

ヘッドライト(緑色LED)とテールライト(赤色LED)を取り付けました。

 

ただ、レールとの接触が不安定でしたので、更に

電解コンデンサ(1000uF)と釣り用オモリを取り付けました。

 

 

■ ポイントレール

 

ロクハンの55mm電動ポイントレール(R022/R023)に

DCCアクセサリーデコーダー(A060)を取り付けました。

 

デコーダーの固定台は3Dプリンタで作りました。

 

 

以上です。

最近 Seeeduino XIAOを買いました。

CircuitPythonが超簡単でしたので、まずはLチカ(LED Blinking)を作ってみました。

 

CircuitPythonを動作させる手順は公式ページに書かれていますが、

要約すると以下の手順になります。

Arduino IDEのようなボードやライブラリの設定が不要で、

ビルドの遅さにイライラすることもなく、簡単でいいですね。

 

■ CircuitPythonの動作手順

 

(1) CircuitPythonのイメージファイル(uf2ファイル)をパソコンにダウンロードする

(2) USBケーブルでSeeeduino XIAOとパソコンと接続する

(3) 金属のピンセットでRST-GNDパッド間を素早く2回ショートする

     → ブートローダモードで起動して、パソコンにArduinoドライブが表示される

(4) ダウンロードしたuf2ファイルをArduinoドライブにコピーする

     ※ 公式ページにはその後USBケーブルを抜き挿しすると書かれていますが、

          コピーしたら自動的にリブートしたような気がします…

     → パソコンにCIRCUITPYドライブが表示される

(5) 作成したmain.pyファイルをCIRCUITPYドライブにコピーする

     → 自動的にスクリプトが実行される

 

■ Lチカ動画

 

 

■ Lチカスクリプトの作成

 

橙色のLED(D13)のLチカは公式ページに書かれているのでよいのですが、

最初、青色LEDの点灯のさせ方が分かりませんでした。

 

公式ページのスクリプトでは橙色LEDのピンが「board.D13」と指定されていて、

論理ポート名と物理ポート名の対応はこれのようなのですが、

コンソールで「dir(board)」を実行すると、青色LEDのPA19,PA18に対応する

board.D11やboard.D12が定義されていません…。

>>> import board
>>> dir(board)
['A0', 'A1', 'A10', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'A9', 'BLUE_LED',
 'D0', 'D1', 'D10', 'D13', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7', 'D8', 'D9',
 'I2C', 'LED', 'MISO', 'MOSI', 'RX', 'SCK', 'SCL', 'SDA', 'SPI', 'TX', 'UART']

それで、「board.D13」を実行すると「microcontroller.pin」があることが分かり、

>>> board.D13
microcontroller.pin.D13

更に「dir(microcontroller.pin)」を実行すると物理ポート名が定義されていました!

>>> import microcontroller
>>> dir(microcontroller.pin)
['PA00', 'PA01', 'PA02', 'PA03', 'PA04', 'PA05', 'PA06', 'PA07', 'PA08', 'PA09',
 'PA10', 'PA11', 'PA12', 'PA13', 'PA14', 'PA15', 'PA16', 'PA17', 'PA18', 'PA19',
 'PA20', 'PA21', 'PA22', 'PA23', 'PA27', 'PA28', 'PA30', 'PA31', 'PB02', 'PB03',
 'PB08', 'PB09', 'PB10', 'PB11', 'PB22', 'PB23']

それで試してみたところ、ポート名の指定として、

board.D**の代わりにmicrocontroller.pin.P**も使えることが分かりました。

 

■ Lチカスクリプト

 

それでできたのが以下のスクリプトファイルになります。

main.py

# CircuitPython for Seeeduino XIAO

import board
import microcontroller
import digitalio
import pulseio
import time

print("# Blink LED with PWM")

PWM_FREQ = 5000
PWM_HIGH = 65535

led11 = pulseio.PWMOut(microcontroller.pin.PA19, frequency = PWM_FREQ, duty_cycle = PWM_HIGH)
led12 = pulseio.PWMOut(microcontroller.pin.PA18, frequency = PWM_FREQ, duty_cycle = PWM_HIGH)
led13 = pulseio.PWMOut(board.D13, frequency = PWM_FREQ, duty_cycle = PWM_HIGH)

COUNT = 50

def get_duty(count):
    if i < COUNT:
        duty = PWM_HIGH - int(i * PWM_HIGH / (COUNT - 1))
    else:
        duty = int((i - COUNT) * PWM_HIGH / (COUNT - 1))
    return duty

while True:
    for i in range(COUNT * 2):
        led11.duty_cycle = get_duty(i)
        time.sleep(0.01)

    for i in range(COUNT * 2):
        led12.duty_cycle = get_duty(i)
        time.sleep(0.01)

    for i in range(COUNT * 2):
        led13.duty_cycle = get_duty(i)
        time.sleep(0.01)

以上です。

M5Stackを使って鉄道模型用DCCコマンドステーションを作りました。

ネットで色々検索した結果、少し古い定番の組み合わせのようですが、

必要最小限の部品点数で実現できそうなTB6643KQ+ACS712を使いました。

 

■ 部品表

品名 品番 員数 備考 価格
M5Stack Basic M5Stack K001 1 スイッチサイエンス SKU#3647 \3,575
Module USB M5Stack M020 1 スイッチサイエンス SKU#6061 \1,408
Base26 M5Stack K026 1 スイッチサイエンス SKU#6355 \1,144
モータードライバーIC TOSHIBA TB6643KQ 1 秋月電子 I-07688 \280
電流センサーIC Allegro ACS712ELCTR-05B-T 1 秋月電子 I-11940 \350
1.27/8P ピッチ変換基板 aitendo 127S7248D8A 1 aitendo 127S7248D8A \110
積層セラミックコンデンサ
 10uF±10% 50V
muRata RDEC71H106K3K1H03B 1 秋月電子 P-08155 \50
 0.1uF±10% 50V Supertech RD15W104K1HL2L 2 秋月電子 P-04064 \100
 0.01uF±10% 50V Supertech RD15W103K1HL2L 1 秋月電子 P-04063 \100
金属皮膜抵抗
 1/4W 10kΩ±1%
FAITHFUL MFS25F10KB 1 秋月電子 R-08550 \200
すずめっき軟銅線 0.6mm KYOWA TCW 0.6mm 10m 適量 秋月電子 P-02220 -
ワイヤー AWG24 SUMITOMO IRRAX A 適量 秋月電子 P-10672 -
合計 \7,317

 

SOP8-DIP変換基板は、秋月電子のP-05154だとパターンが細すぎて心配でしたので、

aitendoのものを使いました。

抵抗や配線は手元にあったものを使ったものでして、これでなくても構いません。

 

(2020/08/27) 現時点の参考価格を追記しました。必要員数以上のパック品は

単価割りせずそのまま載せています。配線や半田などの副資材は含めていません。

 

■ 配線図

 
紫色で示したSPI(G23/G19/G18)とGPIO(G35/G5)はModule USBで使用しますので、
モーター制御用にはG16/G17を、電流センサー用にはG36(ADC)、を割り当てました。
 
■ 写真
実装前
 
実装後(部品面)
 
実装後(半田面)
 
ドッキング状態
 
M5Stackは何と言っても、
基板が剥き出しならずにケースに入れられるのがよいですね。
USB TypeAポートには、nanoKONTROL2を接続する予定です。