前回の記事「ロクハン パワーパックで DCCスロットルの製作1」の続きです。

 

ロクハン製パワーパック「トレインコントローラー RC-03」に接続するケーブルのコネクタは下記の品番になります。

 

コネクタ ケーブル側プラグハウジング コンタクト
ACアダプタ用 汎用DCプラグ φ5.5x2.1mm -
フィーダー用
及びアクセサリー用
JST ELP-02V
(ELコネクタ)
JST SLF-01T-P1.3E
(AWG #26-#20)
ポイント用 TE 172336-1 or 172165-1
(Mini-Universal MATE-N-LOK Connectors)
TE 170365-1
(AWG #26-#22)

 

電子部品屋の店舗で、赤黒ケーブル付JST製ELコネクタと、TE製ミニユニバーサル・メイテンロックコネクタのハウジングとコンタクトのバラ品が偶然売っていましたので、ケーブルを半分に切ってメイテンロックコネクタのコンタクトに半田付けしてポイント用のケーブル付コネクタを作りました。

(なので必要以上に太い線になってしまいました)

 

 

マイコンボードは「Raspberry Pi Pico W」を使用しました。

パワーパックからのアクセサリ出力は 5V LDOと逆流防止用ダイオードを通してラズパイピコの電源入力に接続し、アウトプット出力は分圧兼 RCフィルタを通して A/D入力ポートに接続し、ポイント切替出力は抵抗内蔵トランジスタを通してデジタル入力ポートに接続しました。

また、線路電源ON/OFF用のタクトスイッチ1個と、DCC車両のファンクション制御用のタクトスイッチ4個も接続しました。

 

 

配線図

 

部品表

記号 部品名 部品品番 備考
MCU マイコンボード Raspberry Pi Pico W      
SW タクトスイッチ SKRGAQD010  
Q1,Q2 抵抗内蔵型トランジスタ DTC124ESA  
VD1,VD2 定電圧ダイオード 1N5231B Vz=5.1V
LDO 定電圧レギュレータ UPC78N05H-AZ Vin=Max35V , Vout=5V/300mA
D1 ダイオード 1N4148 Io=200mA
C1,C2,C3,C4 積層セラミックコンデンサ 1uF 50V  
R1,R2 抵抗器 1 kohm 1%  
R3,R4 抵抗器 220 ohm 1%  

 

他の角度から撮った写真

 

ファームウェアは「CircuitPython」を使い、スクリプトを作りました。

このようなマイコンボードのファーム開発では、車両やアクセサリの操作であったり Wi-Fi制御などの主目的の部分はやる気が出るのですが、車両やアクセサリの DCCアドレスを指定するなどの付随的なユーザーインターフェイスの部分はなかなか作り込む気が出ません。

 

その点 CircuitPythonでは、メモ帳でスクリプトファイルを修正して CIRCUITPYドライブ(USBストレージ)にファイルコピーするだけで更新ができますので、スクリプトファイルに直接 DCCアドレスを記入しても手間なく修正と更新ができ、付随的なユーザーインターフェイスを作り込む必要がありません。

 

以下、作成したスクリプトファイルです。

ライブラリは asyncio、adafruit_ticks、adafruit_requests を使用しますが、adafruit_requests については async/await 対応するために一部修正しています。

 

[\code.py]

# --------------------------------------------------------------------------------------
# DCC WiFi Throttle Adapter for DSair Wi-Fi Specification with Raspberry Pi Pico W
#
# Raspberry Pi Pico W
#   https://www.raspberrypi.com/documentation/microcontrollers/raspberry-pi-pico.html
#
# CircuitPython
#   https://circuitpython.org/board/raspberry_pi_pico_w/
#     adafruit-circuitpython-raspberry_pi_pico_w-en_US-8.*.*.uf2
#
# CircuitPython Libraries
#   https://circuitpython.org/libraries
#     adafruit-circuitpython-bundle-8.x-mpy-********.zip
#       lib/asyncio
#       lib/adafruit_ticks.mpy
#     adafruit-circuitpython-bundle-py-********.zip
#       lib/adafruit_requests.py (mod)
#
# DSair Wi-Fi Specification
#   https://desktopstation.net/wiki/doku.php/dsair_wifi_specification
#
#   Send Command
#   http://flashair/command.cgi?op=131&ADDR=0&LEN=64&DATA=DSairCommand
#     Function  Command                      Parameters
#     Power     PW(PowerFlag)                PowerFlag OFF=0, ON=1
#     Direction DI(LocoAddr,Direction)       LocoAddr 0-9999=49152(0xC000)-59151(0xE70F)
#                                            Direction FWD=1, REV=2
#     Function  FN(LocoAddr,FuncNo,FuncVal)  FuncNo F0-F28=0-28 / FuncVal OFF=0, ON=1
#     LocSpeed  SP(LocoAddr,Speed,Speedstep) Speed 0%-100%=0-1023, Speedstep 128Step=2
#     Turnout   TO(AccAddr,AccDirection)     AccAddr 1-2044=14336(0x3800)-16380(0x3FFC)
#                                            AccDirection DIV=0, STR=1
#   Read Status
#   http://flashair/command.cgi?op=130&ADDR=128&LEN=264
#     IndexByte Size Parameter               Definition
#     0         1    Rail Power              ON="Y", OFF="N"
#     8-10      3    Rail Voltage            120=12.0V
#     12-13     2    Output Current          10=1.0A
#
# --------------------------------------------------------------------------------------

import time
import board
import digitalio
import analogio
import wifi
import socketpool
import asyncio
import adafruit_requests


WIFI_SSID = "FlashAir"
WIFI_PASSWORD = "12345678"

LOC_ADDRESS = 3
LOC_FUNCTION = [0, 1, 2, 3]
ACC_ADDRESS = [3, 4]

PIN_LOW = [board.GP9, board.GP10]
PIN_POWER = board.GP0
PIN_FUNCTION = [board.GP4, board.GP7, board.GP12, board.GP15]
PIN_TURNOUT = [board.GP17, board.GP21]

URL_SEND_COMMAND = "http://flashair/command.cgi?op=131&ADDR=0&LEN=64&DATA="
URL_READ_STATUS = "http://flashair/command.cgi?op=130&ADDR=128&LEN=264"

# --------------------------------------------------------------------------------------

print()
print("DCC WiFi Throttle Adapter for DSair")

air_command = []
air_status = ""


class PortInput:
    pin = None
    value = False

    def __init__(self, pin):
        self.pin = digitalio.DigitalInOut(pin)
        self.pin.direction = digitalio.Direction.INPUT
        self.pin.pull = digitalio.Pull.UP

    def state(self):
        value = self.pin.value
        state = 0
        if value != self.value:
            state = 1 if not value else -1
            self.value = value
        return state


async def led_task():
    led = digitalio.DigitalInOut(board.LED)
    led.direction = digitalio.Direction.OUTPUT
    while True:
        led.value = not led.value
        await asyncio.sleep(0.1)


async def wifi_task():
    global air_status
    pool = socketpool.SocketPool(wifi.radio)
    request = adafruit_requests.Session(pool)

    last = 0
    while True:
        await asyncio.sleep(0.1)
        if not wifi.radio.connected:
            if (time.monotonic() - last) >= 5:
                try:
                    print("Connecting to WiFi AP:", WIFI_SSID)
                    wifi.radio.connect(WIFI_SSID, WIFI_PASSWORD)
                    print("  Connected. My IP address:", wifi.radio.ipv4_address)
                except Exception as e:
                    print(f"! {type(e).__name__}: {e}")
                last = time.monotonic()
        else:
            if (time.monotonic() - last) >= 5:
                try:
                    url = URL_READ_STATUS
                    with await request.get(url, timeout=3) as response:
                        if response.status_code == 200:
                            air_status = response.text
                            print("DSair Status:", air_status)
                except Exception as e:
                    print(f"! {type(e).__name__}: {e}")
                last = time.monotonic()
            if len(air_command) > 0:
                try:
                    url = URL_SEND_COMMAND + (command := air_command.pop(0))
                    with await request.get(url, timeout=3) as response:
                        if response.status_code == 200:
                            command = command + " -> " + response.text
                    print("DSair Command:", command)
                except Exception as e:
                    print(f"! {type(e).__name__}: {e}")


async def operation_task():
    for pin in PIN_LOW:
        low = digitalio.DigitalInOut(pin)
        low.direction = digitalio.Direction.OUTPUT
        low.value = False
    adc1 = analogio.AnalogIn(board.A1)
    adc2 = analogio.AnalogIn(board.A2)
    st_power = PortInput(PIN_POWER)
    st_function = [[PortInput(pin), False] for pin in PIN_FUNCTION]
    st_turnout = [PortInput(pin) for pin in PIN_TURNOUT]

    last = 0
    while True:
        await asyncio.sleep(0)
        if (st_power.state() == 1) and (len(air_status) > 0):
            if air_status[0] != "Y":
                air_command.append("PW(1)")
            else:
                air_command.append("PW(0)")
        for i, st in enumerate(st_function):
            if st[0].state() == 1:
                st[1] = not st[1]
                LocoAddr = 0xC000 + LOC_ADDRESS
                FuncNo = LOC_FUNCTION[i]
                FuncVal = int(st[1])
                air_command.append(f"FN({LocoAddr},{FuncNo},{FuncVal})")
        for i, st in enumerate(st_turnout):
            if state := st.state():
                AccAddr = 0x3800 + ACC_ADDRESS[i] - 1
                AccDirection = 1 if state == 1 else 0
                air_command.append(f"TO({AccAddr},{AccDirection})")
        if (time.monotonic() - last) >= 1:
            print(
                "{:.3f}".format(adc1.value * 3.3 / 65536),
                "{:.3f}".format(adc2.value * 3.3 / 65536),
            )
            last = time.monotonic()


async def main():
    await asyncio.gather(led_task(), wifi_task(), operation_task())


asyncio.run(main())

 

[\lib\adafruit_requests.py]

左側がオリジナルで、右側は変更後です。

 

このスクリプトで、パワーパックのポイント切替スイッチによるポイント制御、及びラズパイピコのタクトスイッチによる線路電源のON/OFF制御や車両のファンクション制御(4つ) については対応できました。

 

その後、車両のスピードコントロール機能について作ろうとしたところ、パワーパックのつまみを回してもアウトプット出力から波形が現れなくなってしまい、壊してしまったかもしれない状態になってしまいました。

 

ということで、これ以上 作ることができなくなってしまいました。

 

残念ですが、以上です。

前回の記事「M5Atomで DSair代替品の製作2」で DSair代替品を製作してタブレット端末から操作したのですが、個人的にはやはり物理スイッチで操作できた方がいいなと思いました。

 

それで、以前 Zゲージ用に購入してからほとんど使っていなかったロクハン製パワーパック「トレインコントローラー RC-03」を使って、DSair Wi-Fi Specificationに一部準拠した Wi-Fi DCCスロットルアダプタを製作することにしました。

 

パワーパック自体は一切改造せずに、SW #1 (ポイント1出力)、SW #2 (ポイント2出力)、ACCESSORY (Wi-Fi DCCスロットル用の電源出力)、OUTPUT (フィーダー出力)の各コネクタから別体の Wi-Fi DCCスロットルアダプタに接続して使用します。パワーパックは単三電池8本で動作します。

 

 

まずポイント出力の波形を測定しました。

ACCESSORYコネクタのマイナス端子を基準電位として、SW #1コネクタのプラス端子にオシロスコープのCH1(赤色)を接続し、マイナス端子にCH2(黄色)を接続し、ポイント切替スイッチを上下に動かしたところ、下図の波形になりました。緑色は CH1-CH2の演算波形です。

 

Wi-Fi DCCスロットルアダプタ側でポイント切替の判定をするためにはプラス端子のみをモニタすればよさそうです。波形の立上り時・立下り時は実際には細かいチャタリングが発生していますので配慮が必要です。

 

 

次にフィーダー出力の波形を測定しました。

ACCESSORYコネクタのマイナス端子を基準電位として、OUTPUTコネクタのプラス端子にオシロスコープのCH1(赤色)を接続し、マイナス端子にCH2(黄色)を接続したところ、下図の波形になりました。
 

上側は方向切替スイッチが FWDで、下側が REVです。左側から右側に向かってスピードダイヤルを MINから MAXまで段階的に回しています。

 

 

 

なお Amebloブログの画像は、Edgeブラウザの場合は画像を右クリックして「新しいタブで画像を開く」をクリックし、アドレスバーの画像リンク末尾の「?caw=800」を削除するとオリジナルサイズで表示できます。

 

以上です。

続いて、実際に鉄道模型の車両を制御した動画です。

 

画面の右上が手前で、左側が奥になっているので見づらいかもしれません。また、撮影時にカメラの邪魔にならないようにと思って静電ペンで操作したのですが、操作が下手になってしまいイマイチでした。

 

0:03 M5Atom Liteに給電開始(安定化電源装置を出力をON)

0:20 タブレットのWi-FiがM5Atom Liteに自動接続

0:30 ブラウザでWebアプリ画面が表示完了

 

 

  動作環境の説明

 

安定化電源装置から DC13Vを出力し、整流ダイオードを通して M5Atom Liteに給電しています。

デバッグ時に M5Atom Liteの USBポートをパソコンに接続した状態で安定化電源装置の電源出力を OFFすると、整流ダイオードを入れていないと USBポートの VBUS電源が安定化電源装置に逆流してしまうので、それを防ぐために整流ダイオード(Vf=約1V)を入れています。

 

そして、H-Driverのモータ制御出力を DCC電源出力として線路に接続しています。

 

KATOのNゲージ複線両渡りポイント(WX310)は、以前の記事「DCCポイントデコーダーの製作(その3)」で自作DCCデコーダーを組み込んだものを使っています。

 

車両には「ESU 58731 LokSound 5 micro DCC Kato」という DCCサウンドデコーダーを搭載しており、ヘッド/テールライトや室内灯については Kato FL12/FR11を使わず LokSoundから制御しています。

ただ、LokProgrammerを持っていないため、サウンドは購入時のデフォルトのままです。

 

タブレット端末は「HUAWEI BG2-W09 MediaPad T3 7 8GB」(Android 6.0)でして、ブックオフで約3千円で売っているのを偶然見つけて買いました。

 

  Wi-Fi通信ログ

 

M5Atom Liteの USBポートをパソコンに接続した状態で、タブレットとの Wi-Fi通信ログ(といっても httpリクエストURLと Webアプリへのステータス応答のみ)をシリアル出力してモニタしました。

(なぜか上の動画より早く接続できていますね)

 

0:03 M5Atom Liteの USBポートをパソコンに接続

0:14 タブレットのWi-FiがM5Atom Liteに自動接続

0:18 ブラウザでWebアプリ画面が表示完了

 

Webアプリは DSair1のものでして、最初にページ全体を読み込むのに約3秒ほどかかりますが、それ以降の通信は、車両やポイントを制御した時やステータスの定期取得のみのようですので、Wi-Fiの通信速度はそこまでボトルネックにはならないような感じがします。

 

どちらかというと、JavaScriptを実行するタブレット端末自身の処理速度の方がボトルネックになっている感じがします。

とはいえ、ページ全体の読み込み時に JavaScriptファイルの転送がそれなりにあるので、Content-Encoding: gzip ヘッダをつけて httpコンテンツ圧縮を行えば多少は改善するかもしれません。

 

 

あとは、最初のページ全体の読み込み時だけ、画面の縮尺が合わずに横幅がはみ出してしまうのが何とかしたいですね。

 

あと、タブレット端末でのタッチパネル操作って車両を運転している実感がわきにくいので、個人的には物理的なボタンやスイッチやボリュームなどで操作できた方が好きですね。

 

以上です。

久しぶりの投稿になりましたが、鉄道模型DCCコマンドステーションの記事です。

 

  はじめに

 

以前の記事「M5Atomで DCCコマンドステーションの製作」では、M5Stack社製 ATOM H-DRIVER (SKU:K050) という、M5Atom Lite (SKU:C008) (ESP32) マイコンモジュールと DRV8876 モータードライバーを組み合わせたキットを使って DSshield2 のパチモン品を製作しました。

 

今回は、ソフトをアップデートして、Wi-Fi対応DCCコマンドステーションである DSair1 (初代DSair) のパチモン品を製作しました。

 

M5Atom Liteのフラッシュメモリサイズは4MBですので DSair2 のFlashAir用Webアプリ(約13MB)は搭載できません。そのため、マイコンのソースコードは DSair2 及び DSshield2 (RP2040版) のものをベースにしつつ、Webアプリは DSair1 のもの(約1MB)を搭載しました。

 

なお、ハードはそのままですので S88インターフェイスは非対応です。また、追加改造した、電流モニタ出力をアナログ入力している G25ポート(ADC2)は、ESP32の制約で Wi-Fi機能使用時には使えませんので、CV値の読出しにも非対応です。

 

  ソースコードの構成と開発環境

 

ソースコードの構成をまとめると下表になります。

「◎」のファイルはベースファイルを修正なしでそのまま使用して、「○」のファイルはベースファイルを一部修正し、「□」のファイルは新規に作成しました。

 

ファイル DSair2
R3.4b
DSair
R2d
DSshield2
RP2040.002
新規 コメント
SD_WLAN/*       事前にServerFile.cppに変換
(ビルド時は不要)
hardware/adc.h       サイズゼロの空ファイル
DSairFirmware.ino       ハード依存部分を修正
DSCoreM.cpp        
DSCoreM.h        
DSCoreM_Common.cpp        
DSCoreM_Common.h        
DSCoreM_DCC.cpp        
DSCoreM_DCC.h        
DSCoreM_List.cpp        
DSCoreM_List.h        
DSCoreM_MM2.cpp        
DSCoreM_MM2.h        
DSCoreM_Type.h       ハード依存部分を修正
FlashAirSharedMem.cpp       FlashAirの制御処理を
Webサーバ機能に置換え
FlashAirSharedMem.h        
Functions.cpp        
Functions.h        
ServerFile.cpp       SD_WLANフォルダの
ファイルをバイナリ配列化
ServerFile.ps1       ServerFile.cppを自動生成
するPowerShellスクリプト
TrackReporterS88_DS.cpp       S88非対応のためスタブ化
TrackReporterS88_DS.h        

 

開発環境は Visual Studio Code (VSCode) + Arduino Extension を使っており、Arduino Board Configurationは下表になります。

 

パーティション設定を No OTA に変更することでプログラム領域(app0)を約1.2MBから 2MBに増やしています。

なお、partitions.csvファイルを直接編集して未使用のファイルシステムストレージ領域(spiffs)の約1.8MBを削減すれば、プログラム領域を最大約3.8MBまで増やすことができるはずです。

 

項目 設定 コメント
Selected Board M5Stack-ATOM (M5Stack)  
Partition Scheme No OTA (Large APP) 変更
Upload Speed 1500000 デフォルト
Core Debug Level None デフォルト
Erase All Flash Before Sketch Upload Disabled デフォルト

 

  ソースコードの説明

 

■ hardware/adc.h

「DSCoreM.cpp」ファイルの中で「adc.h」ファイルがインクルードされているため、サイズゼロの空ファイルを用意します。普通はインクルード文をコメントアウトすればよいのですが、ベースファイルは極力修正したくないのです。

 

■ DSairFirmware.ino

DSair2で使われている Arduino Nanoボードのハード依存処理は「#ifdef ARDUINO_ARCH_AVR」ブロックでコメントアウトして、代わりに「#ifdef ARDUINO_M5Stack_ATOM」ブロックに M5Atomモジュールのハード依存処理を追加しています。

digitalWrite()関数や digitalRead()関数などにおいて、元の処理と制御を変えたい GPIOピンについては、ピン番号の #define値を「0,**」のように定義することで関数の引数を一つ増やし、コール元のソースコードを修正することなくオーバーロードした関数の方がコールされるようにして制御を変えています。

#ifdef ARDUINO_M5Stack_ATOM
#include <M5Atom.h>
#define PIN_PWMA 19     // G19 (IN1)
#define PIN_PWMB 23     // G23 (IN2)
#define PIN_EDC 0,33    // G33 (VIN/10)
#define PIN_RUNLED 0,0  // dummy
#define PIN_ALERT 0,22  // G22 (nFAULT)
#define PIN_S88CHK 0,1  // dummy
#define PIN_SPICS 0     // dummy
#define PIN_CURRENT 25  // G25 (IPROPI)
#define SENSOR_CURRENT PIN_CURRENT
//#define THRESHOLD_EDC_DSA 1117  //  9V/10/3.3V*4096
//#define THRESHOLD_OV_DSA  2978  // 24V/10/3.3V*4096
void pinMode(uint8_t flag, uint8_t pin, uint8_t mode) { switch (pin) {
    case 22: pinMode(pin, mode); break;
    case 33: pinMode(pin, mode); break;
}}
void digitalWrite(uint8_t flag, uint8_t pin, uint8_t val) { switch (pin) {
    case 0: M5.dis.drawpix(0, (val ? 0x00FF00 : 0)); break;
}}
int digitalRead(uint8_t flag, uint8_t pin) { switch (pin) {
    case 22: return ((digitalRead(pin) == LOW) ? HIGH : LOW);
    default: return 0;
}}
void analogWrite(uint8_t pin, int value) {}
uint16_t analogRead(uint8_t flag, uint8_t pin) { switch (pin) {
    case 33: return (analogRead(pin) * 11 / 32);
    default: return 0;
}}
void gpio_put(unsigned char gpio, bool value) { switch (gpio) {
    case 21: digitalWrite(PIN_PWMA, value); break;
    case 20: digitalWrite(PIN_PWMB, value); break;
}}
unsigned char g_adc_input = 0;
void adc_init() {
    M5.begin(false, false, true);
    pinMode(PIN_PWMA, OUTPUT);
    pinMode(PIN_PWMB, OUTPUT);
}
void adc_gpio_init(unsigned char gpio) {}
void adc_select_input(unsigned char input) {
    g_adc_input = input;
}
unsigned short adc_read() { switch (g_adc_input) {
    case 2: return analogRead(PIN_CURRENT) >> 3;
    default: return 0;
}}
void wdt_enable(unsigned char value) {}
void wdt_reset() {}
unsigned char TCCR1B = 0;
int8_t SharedMemRead2(uint16_t adr, uint16_t len, uint8_t buf[]) {
    return SharedMemRead(adr, len, buf); }
int8_t SharedMemWrite2(uint16_t adr, uint16_t len, uint8_t buf[]) {
    return SharedMemWrite(adr, len, buf); }
#endif

 

■ DSCoreM_Type.h

RP2040マイコン固有の関数は、「DSairFirmware.ino」ファイルで定義した関数をコールするように extern宣言をしています。

#ifdef ARDUINO_M5Stack_ATOM
extern void gpio_put(unsigned char gpio, bool value);
extern void adc_init();
extern void adc_gpio_init(unsigned char gpio);
extern void adc_select_input(unsigned char input);
extern unsigned short adc_read();
#endif

 

■ FlashAirSharedMem.cpp

元々は SDカードインターフェイスを通して FlashAirと通信する処理が書かれたファイルですが、代わりに、ESP32のデュアルコアのうち、メインルーチンとは別の CPUコアを使って Webサーバー機能が動作するように実装しました。

Wi-Fi APの SSIDは「FlashAir」で、パスワードは「12345678」で一旦固定です。DNSのドメイン名は「flashair」にして互換性を持たせています。

Webサーバー機能としては下表のように実装しています。もし Webアプリがこれ以外の機能を使用していたら非対応です。

URL 機能
http://flashair/ http://flashair/SD_WLAN/List.htm にリダイレクトします。
http://flashair/command.cgi FlashAirの固有コマンドをエミュレーションします。
(下記コマンドのみ対応)
op=130 … 共有メモリからのデータの取得
op=131 … 共有メモリへのデータの書き込み
http://flashair/SD_WLAN/* ServerFile.cppファイルで事前に準備したファイルを返します。

 

#ifdef ARDUINO_ARCH_AVR
~
#elif defined(ARDUINO_M5Stack_ATOM)
/*
  DSair Wi-Fi Specification
  Send Command: http://flashair/command.cgi?op=131&ADDR=0&LEN=64&DATA=DSairCommand
  Get Status  : http://flashair/command.cgi?op=130&ADDR=128&LEN=264
    Power     : PW(PowerFlag)                 PowerFlag 0:OFF, 1:ON
    Direction : DI(LocoAddr,Direction)        Direction 1:FWD, 2:REV
    Function  : FN(LocoAddr,FuncNo,FuncVal)   FuncNo 0-28:F0-F28 / FuncVal 0:OFF, 1:ON
    LocSpeed  : SP(LocoAddr,Speed,Speedstep)  Speed 0-1023, Speedstep 2:128Step
    Turnout   : TO(AccAddr,AccDirection)      AccDirection 0:DIV, 1:STR
                                              LocoAddr 0:49152(0xC000) - 9999:59151(0xE70F)
                                              AccAddr 1:14336(0x3800 - 2044:16380(0x3FFC)
*/

#include <M5Atom.h>
#include <WiFi.h>
#include <DNSServer.h>
#include <WebServer.h>
#include <detail/mimetable.h>
#include "ServerFile.cpp"

const char* wlan_ssid = "FlashAir";
const char* wlan_pass = "12345678";

DNSServer dns;
WebServer server(80);

volatile uint8_t shared_memory[512] = {};
portMUX_TYPE shared_mutex = portMUX_INITIALIZER_UNLOCKED;

static void server_task(void *pvParameters) {
    WiFi.softAP(wlan_ssid, wlan_pass);
    dns.start(53, "flashair", WiFi.softAPIP());
    server.on("/", []() {
        server.sendHeader("Location", "/SD_WLAN/List.htm");
        server.send(302); // 302 Found
    });
    server.on("/command.cgi", []() {
        String arg_msg = server.uri() + " ";
        for (int i = 0; i < server.args(); i++) {
            arg_msg += "&" + server.argName(i) + "=" + server.arg(i);
        }
        Serial.println(arg_msg);
        bool sent = false;
        int power = ((shared_memory[0x80] == 'Y') ? 0x008000 : 0); // Rail Power
        M5.dis.drawpix(0, power + 0xFF00C0);
        uint16_t arg_addr = server.arg("ADDR").toInt();
        uint16_t arg_len = server.arg("LEN").toInt();
        switch (server.arg("op").toInt()) {
        case 130: {
            // READ_SHARED_MEMORY
            uint8_t buffer[sizeof(shared_memory) + 1] = {};
            if (SharedMemRead(arg_addr, arg_len, buffer) == 0) {
                server.setContentLength(arg_len);
                server.send(200, "text/plain", ""); // 200 OK
                server.sendContent((char *)buffer, arg_len);
                Serial.println((char *)buffer);
                sent = true;
            }
            break;
        }
        case 131: {
            // WRITE_SHARED_MEMORY
            String arg_data = server.arg("DATA");
            if (SharedMemWrite(arg_addr, arg_data.length(), (uint8_t *)arg_data.c_str()) == 0) {
                server.send(200, "text/plain", "SUCCESS"); // 200 OK
                sent = true;
            }
            break;
        }
        }
        if (!sent) {
            server.send(400); // 400 Bad Request
        }
        M5.dis.drawpix(0, power);
    });
    server.onNotFound([]() {
        if (server.method() != HTTP_GET) {
            server.send(405); // 405 Method Not Allowed
            return;
        }
        for (const struct st_server_file *pf = SERVER_FILE; pf->data; pf++) {
            if (server.uri().equals(pf->url)) {
                const mime::Entry *pt = mime::mimeTable;
                while (!server.uri().endsWith(pt->endsWith)) { pt++; };
                int power = ((shared_memory[0x80] == 'Y') ? 0x008000 : 0); // Rail Power
                M5.dis.drawpix(0, power + 0xFF0000);
                Serial.println(server.uri() + " (" + pt->mimeType + ")");
                server.setContentLength(pf->size);
                server.send(200, pt->mimeType, "");
                server.sendContent(pf->data, pf->size);
                M5.dis.drawpix(0, power);
                return;
            }
        }
        server.send(404); // 404 Not Found
    });
    server.begin();
    while (1) {
        dns.processNextRequest();
        server.handleClient();
        delay(1);
    }
}

int8_t SharedMemInit(int _cs) {
    portENTER_CRITICAL(&shared_mutex);
    memset((void *)shared_memory, 0, sizeof(shared_memory));
    portEXIT_CRITICAL(&shared_mutex);
    xTaskCreateUniversal(server_task, "task", 8192, NULL, 3, NULL, PRO_CPU_NUM);
    return 0;
}

int8_t SharedMemWrite(uint16_t adr, uint16_t len, uint8_t buf[]) {
    if ((adr + len) > sizeof(shared_memory)) {
        return -1;
    }
    portENTER_CRITICAL(&shared_mutex);
    memcpy((void *)shared_memory + adr, buf, len);
    portEXIT_CRITICAL(&shared_mutex);
    return 0;
}

int8_t SharedMemRead(uint16_t adr, uint16_t len, uint8_t buf[]) {
    if ((adr + len) > sizeof(shared_memory)) {
        return -1;
    }
    portENTER_CRITICAL(&shared_mutex);
    memcpy(buf, (void *)shared_memory + adr, len);
    portEXIT_CRITICAL(&shared_mutex);
    return 0;
}
#else
int8_t SharedMemInit(int _cs) { return -1; }
int8_t SharedMemWrite(uint16_t adr, uint16_t len, uint8_t buf[]) { return -1; }
int8_t SharedMemRead(uint16_t adr, uint16_t len, uint8_t buf[]) { return -1; }
#endif

 

■ TrackReporterS88_DS.cpp

S88インターフェイスは非対応のため、関数をスタブ化しています。

#ifdef ARDUINO_ARCH_AVR
~
#else
TrackReporterS88_DS::TrackReporterS88_DS(int modules) {}
void TrackReporterS88_DS::refresh() {}
void TrackReporterS88_DS::refresh(int inMaxSize) {}
boolean TrackReporterS88_DS::getValue(int index) { return 0; }
byte TrackReporterS88_DS::getByte(int index) { return 0; }
#endif

 

■ ServerFile.cpp

Webサーバーで返すファイルをバイト配列の形式で定義しています。PowerShellスクリプトを使って自動生成しています。

一般的には SPIFFS (SPIフラッシュメモリ用のファイルシステム) を使用した方がよさそうですが、VSCode + Arduino Extensionの開発環境で SPIFFSにファイルを書き込む方法が分かりませんでした。

const char file0001[] = {
    0x**,0x**, …
};
~
const struct st_server_file {
    const char *data;
    const char *url;
    int size;
} SERVER_FILE[] = {
    { file0001, "/SD_WLAN/c/acc/DBLSLIPSWITCH_1.png", 743 },
    ~
    { file0059, "/SD_WLAN/List.htm", 71005 },
    { 0, 0, 0 }
};

 

■ ServerFile.ps1

PowerShellスクリプトファイルで、ビルドする前に実行します。
「SD_WLAN」フォルダにあるファイルをスキャンして、「ServerFile.cpp」ファイルを自動生成します。

# ServerFile.ps1
Set-Location $PSScriptRoot
$list = New-Object System.Text.StringBuilder
$code = New-Object System.Text.StringBuilder
$count = 1
Get-ChildItem -LiteralPath "SD_WLAN" -Recurse -File | Sort-Object FullName |
ForEach-Object {
    $url = (Resolve-Path -LiteralPath $_.FullName -Relative).Replace("\", "/").TrimStart(".")
    $var = "file{0:D4}" -f $count
    $data = [System.IO.File]::ReadAllBytes($_.FullName)
    [void]$list.Append("    { $var, ""$url"", $($data.Length) },`n")
    $hex = "0x" + ([System.BitConverter]::ToString($data) -replace "-", ",0x")
    [void]$code.Append("const char $var[] = {`n    $($hex -replace ".{160}", "`$0`n    ")`n};`n")
    $count++
}
[void]$code.Append(@"
const struct st_server_file {
    const char *data;
    const char *url;
    int size;
} SERVER_FILE[] = {
$($list.ToString())    { 0, 0, 0 }
};
"@)
$code.ToString() | Out-File -FilePath "ServerFile.cpp" -Encoding ascii

 

  ビルドと EasyLoader実行ファイルの作成

 

VSCode + Arduino Extensionの開発環境を使って、ビルドして M5Atom Liteへのファームウェアの書き込みまで問題なく行えますが、書き込みのみをしたい人にとっては開発環境を準備するのは面倒です。

 

そのようなニーズのために、M5Stack社が EasyLoader Packer というワンクリック書き込みツールの作成サイトを準備してくれています。

 

ビルドしてできる 3つの binファイルと boot_app0.binファイルをサイトにアップロードしてオフセットアドレスを設定して「Make」ボタンをクリックすると、ワンクリック書き込みツールの実行ファイル「DSairFirmwareMod.exe」が作成されてダウンロードできます。

 

Offset Filename パス
0x1000 DSairFirmware.ino.bootloader.bin C:\Users\~\AppData\Local\Temp\arduino
  \sketches\~\
0x8000 DSairFirmware.ino.partitions.bin C:\Users\~\AppData\Local\Temp\arduino
  \sketches\~\
0xe000 boot_app0.bin C:\Users\~\AppData\Local\Arduino15
  \packages\m5stack\hardware
  \esp32\2.0.7\tools\partitions
0x10000 DSairFirmware.ino.bin C:\Users\~\AppData\Local\Temp\arduino
  \sketches\~\

 

EasyLoader Packer

 

それを実行すると下記の画面が表示されます。

M5Atom Liteの COMポート番号を選択して「Burn」ボタンをクリックするだけでファームウェアを書き込むことができます。

 

 

以上です。

桃井望さんの事件は、個人的にずっと記憶に残っています。

 

あれから20年が経ち、先日、久しぶりに長野県塩尻市広丘郷原の奈良井川河川敷にある「発見現場」を訪れました。

そこは奈良井川漁業協同組合 鮎オトリセンターのすぐ北側にあります。

 

 

■ 2004年10月18日(月)

 

 

 

■ 2009年11月3日(火)

 

 

 

 

■ 2022年10月29日(土)

 

 

 

 

また訪れたいと思います。

Desktop Stationのサイトで、DSshield2(DSシールド2)のスケッチが公開されていまして、以前の記事「XIAO RP2040で DCCコマンドステーションの製作」では、Seeed Studio XIAO RP2040 マイコンモジュールと TB6643KQ モータードライバーを組み合わせて製作した DCCコマンドステーションを紹介しました。

 

その後、M5Stack社製 ATOM H-DRIVER (SKU:K050) という、M5Atom Lite (ESP32) マイコンモジュールと DRV8876 モータードライバーを組み合わせたセットが販売されていることを知りまして、今回こちらで DCCコマンドステーションを製作しました。

 

まずは、ATOM H-DRIVER そのままの写真です。

 

 

ケースを外した状態です。

 

 

DRV8876 モータードライバーは IPROPIピン(6ピン)に電流モニタ出力(1V/1A アナログ出力)がありますので、DCCデコーダの CV値の読出しに対応するために、これを M5Atom Liteの G25ポート(ADC2)に接続します。

CV値の読出しに非対応でよければ改造は不要です。

 

基板の表側にある DRV8876の 6ピン端子から裏側の G25ピンヘッダ端子に配線するためにはケースとの隙間を通る極細線を使う必要があるため、今回は基板の裏側にあるパターンのレジストを削って裏側のみで配線しました。

 

 

 

マイコンのスケッチは、現在公開されている「rev.RP2040.002」をベースにして、以下の修正を行いました。

 

項目 変更前 (RP2040) 変更後 (M5Atom/ESP32)
DCC制御出力 GPIO21
GPIO20
G19 (IN1)
G23 (IN2)
アナログ電圧入力 GPIO27 (ADC1) G33 (VIN/10)、スレッショルドの変更
アナログ電流入力 GPIO28 (ADC2) G25 (IPROPI)
RUN LED GPIO02 RGB-LED (WS2812B)

 

以下、ソースコードの差分です。今回は修正箇所が少なくなるようにしました。

Arduino IDEの Board設定は「M5Stack-ATOM」を選択してビルドします。

なお、hardwareフォルダの下には adc.hファイル(サイズゼロのダミーファイル)を置いています。

(Amebloブログの画像は、画像リンク末尾の「?caw=800」を削除するとオリジナルサイズで表示できます)

 

* フォルダ比較

 

* DSshield2040.ino

#ifdef ARDUINO_M5Stack_ATOM
#include <M5Atom.h>
#define A1 33               // G33 (VIN/10)
#define A2 25               // G25 (IPROPI)
#define PIN_PWMA 19         // G19 (IN1)
#define PIN_PWMB 23         // G23 (IN2)
#define PIN_RUNLED 18       // G18 (dummy)
#define THRESHOLD_EDC 1117  //  9V/10/3.3V*4096
#define THRESHOLD_OV  2482  // 20V/10/3.3V*4096
void gpio_put(unsigned char gpio, bool value) { switch (gpio) {
  case 21: digitalWrite(PIN_PWMA, value); break;
  case 20: digitalWrite(PIN_PWMB, value); break;
  case PIN_RUNLED: M5.dis.drawpix(0, (value ? 0x00FF00 : 0)); break;
}}
void adc_init() { M5.begin(false, false, true); pinMode(A2, INPUT); }
void adc_gpio_init(unsigned char gpio) {}
void adc_select_input(unsigned char input) {}
unsigned short adc_read() { return analogRead(A2) >> 3; }
void analogWrite(uint8_t pin, int value) {}
#endif

 

* DSCoreM_Type.h

#ifdef ARDUINO_M5Stack_ATOM
extern void gpio_put(unsigned char gpio, bool value);
extern void adc_init();
extern void adc_gpio_init(unsigned char gpio);
extern void adc_select_input(unsigned char input);
extern unsigned short adc_read();
#endif

 

デスクトップステーションソフトウェアを使って、車両の速度制御やファンクション制御、CV値の読出しが問題なく行えました。

ただし、オプションのシリアルポートの設定では、「DTR有効(自動リセット)」のチェックを外す必要があります。(チェックすると ATOMがファーム更新モードに入ってしまいます)

 

 

以上です。

DesktopStation社製の鉄道模型用 DCCポケットモニタ「DSwatch」の製造・販売が終了になると、Twitterや「電機屋の毎日」のブログ「DSwatchの製造・販売終了予定通知」でアナウンスがありました。

そのため、M5StickCで代替品を製作しました。

 

部品点数を極力少なくするため、整流にはブリッジダイオードを使い、降圧にはコンデンサ内蔵 DC-DCコンバータモジュールを使い、DCC信号の入力には抵抗内蔵デジタルトランジスタを使いました。部品は全て秋月電子で集めました。

 

■ 実体配線図

 

 

■ 部品表

 

記号 品名 品番 コード
PCB 両面スルーホールガラスコンポジット
ユニバーサル基板 Fタイプ
AE-F-TH P-12731
D1 ショットキーバリアダイオードブリッジ(100V 2A)
※ 実際には 60V 2A品を使用
SDI2100
SDI260
I-06320
I-06667
U1 スーパー三端子レギュレーター 5V 500mA R-78E5.0-0.5 M-06353
Q1 抵抗入トランジスタ DTC143EL-T92-K I-12469
R1 小型 金属皮膜抵抗 1/4W 10kΩ (100本入) MFS25F10KB R-08550
R2 小型 金属皮膜抵抗 1/4W 1kΩ (100本入) MFS25F1KB R-08535
CN1 ピンヘッダ (L型) 1×8 (8P) PH-1X8RG(2) C-12985
CN2 ターミナルブロック 2.54mm 2P 緑 縦 WJEK254-2.54-02P-140-00A P-14217

 

■ 製作した基板の写真

 

 

■ 動作写真

 

以前のブログ 「XIAO RP2040で DCCブースターの製作」で製作しました DCCブースターをデスクトップステーションソフトウェアで制御して動作確認しました。

 

 

 

M5ボタンを押すたびに、車両情報の表示 → DCCパケット表示 → ポイント状態の表示 → 線路電圧表示 → パルス幅の表示 → (再び車両情報の表示から繰り返し)、と画面が遷移します。

 

 

パソコン用オシロスコープ(OWON VDS1022I)で波形を測定しました。

CH1(赤色)は DCCブースターのロジック出力(TB6643KQの IN1)で、CH2(黄色)は M5StickCの DCC信号ポート入力(G26)です。

デジタルトランジスタを使っているためか、若干遅延がありますね。これだとパルス幅を正確に測定できないため、素直に抵抗分圧にした方がよかったです。

 

 

■ M5StickC ファームウェア

 

DesktopStationのページ「DSwatch」で公開されているオリジナルのファームウェアR5をベースにして、ハードウェア依存処理を M5StickCに合うように修正しました。

 

オリジナルファイル
R5 (2020/8/7)
修正ファイル
(2022/11/21)
修正内容
I2CLCDLib.cpp
I2CLCDLib.h
削除 LCD表示は M5StickCライブラリを使用するため削除して、
ラッパークラスを DSwatch.ino に実装。
MrWatch.ino DSwatch.ino ハードウェア依存処理を M5StickCに合うように修正。
合わせてバグ(バッファオーバーフロー)も修正。
NmraDcc_v141.cpp
NmraDcc_v141.h
NmraDcc.cpp
NmraDcc.h
M5StickCに搭載されている ESP32マイコンに対応した
最新バージョン(NmraDcc-2.0.13)に差し替えて、
DCCパルス幅測定の拡張処理コール部分のみ修正。

 

* NmraDcc.h (NmraDcc-2.0.13からの差分)

 

* NmraDcc.cpp (NmraDcc-2.0.13からの差分)

 

* DSwatch.ino (DSwatch R5からの差分)

 

以上です。

 

今年の6月頃に、秋月電子で Laird Connectivity社製 BL652 Bluetoothモジュールが約半額の特価(\450)で売っていましたので、鉄道模型の車両に搭載して無線で制御することを目指して10個購入しました。

 

それと合わせて BL652ブレークアウトボードも買っていたのですが、一緒に使う USBシリアル変換モジュールの在庫が当時は無くて買えなかったため、結局使えずにいました。

それが最近、ようやく在庫が復活して買うことができて使えるようになりました。

 

購入物

・[M-14448][BL652-SA-01-T/R] Bluetoothモジュール BL652-SA-01

・[K-15567][AE-BL652-BO] BL652ブレークアウトボードキット

・[K-09951][AE-TTL-232R] FT232RQ USBシリアル変換モジュールキット

 

偉大な先人がまとめられたページが大変参考になりましたが、秋月電子の USBシリアル変換モジュールを使うとジャンパ配線は不要になり、ピンヘッダを接続するだけで済むので簡単です。電源も USBポートから USBシリアル変換モジュール経由で供給されます。

 

下記、その接続写真です。

 

 

■ 使い方 

BL652モジュールとのシリアル通信確認とファームウェア更新の手順です。

 

(1) GitHub LairdCP ページから UwTerminalX アプリのパッケージファイルをダウンロードします。

現時点での最新版は「UwTerminalX_v1.13a_Windows_SSL.zip」でした。

 

(2) 展開したフォルダにある「UwTerminalX.exe」ファイルを実行します。

 

(3) UwTerminalX アプリウィンドウの「About」タブが表示されますので、「Accept」ボタンをクリックします。

 

 

(4) 「Config」タブが表示されますので、BL652モジュールの COMポート番号(私の環境ではCOM8)を選択して「OK」ボタンをクリックします。

 

 

(5) 「Terminal」タブが表示されますので、キーボードで ATコマンドを入力して送信するとレスポンスが返ります。

ATコマンド仕様は、BL652 ページ から 「Documentation」→「User Guide - BL65x AT Interface Application」をクリックすると表示されます。ただし、ライセンスコードの取得コマンドについては記載がなく、先人のページで知りました。

 

コマンド レスポンス 説明
ATI 0 10  0   BL652 Get Device Name
ATI 3 10  3   28.6.1.2 Get Firmware Version
ATI 4 10  4   01 F14D8A9AE32F Get Bluetooth MAC Address
ATI 10 10  10  Laird Technologies, (c)2016 Get Copyright Notice
ATI 49406 10  49406   C3343C41CD82FFFFF79D Get License Code

 

 

購入直後のファームウェアバージョンは「v28.6.1.2」で古いため、最新版に更新するために一旦 UwTerminalX アプリを終了します。

 

(6)  BL652 ページ から 「Software」→「ITSE01052 ** BL652 Firmware For Upgrade v**」をクリックしてファームウェアのパッケージファイルをダウンロードします。

現時点での最新版は「ITSE01052_12_BL652_Firmware_For_Upgrade_v28_11_8_0_r1.zip」でした。

 

(7) 展開したフォルダにある「Firmware\BL65xUartFwUpgrade.exe」ファイルを実行します。

 

(8) Firmware Upgrade アプリウィンドウが表示されますので、「OK」ボタンをクリックします。

 

 

(9) BL652モジュールの COMポート番号(私の環境では COM8)を選択して「OK」ボタンをクリックします。

 

 

(10) 「Proceed」ボタンをクリックすると、ファームウェアの更新が開始します。

 

 

(11) ファームウェアの更新が正常に完了すると、「UPGRADE SUCCESS」と表示されますので、「Quit」ボタンをクリックしてアプリを終了します。ファームウェアの更新には 1分15秒程度かかりました。

 

 

(12) 改めて UwTerminalX アプリを実行します。

 

(13) 手順(3)→(4)→(5)を行って ATコマンドを送信します。

ファームウェアバージョンが「v28.6.1.2」から「v28.11.8.0」に変わり、社名も変わりました。ライセンスコードについては、今回は消去されることはなく残りました。

 

コマンド レスポンス 説明
ATI 0 10  0   BL652 Get Device Name
ATI 3 10  3   28.11.8.0 Get Firmware Version
ATI 4 10  4   01 F14D8A9AE32F Get Bluetooth MAC Address
ATI 10 10  10  Laird Connectivity, (c) 2021 Get Copyright Notice
ATI 49406 10  49406   C3343C41CD82FFFFF79D Get License Code

 

 

以上です。

前回の記事 「XIAO RP2040で PIC12F1822ライタの製作」の続きですが、

下記のページを見まして、PIC12LF1840 マイコンも試してみました。

Web Nucky Blog |入手可能な PIC12LF1840 で ワンコインデコーダ を作る


XIAO RP2040の GPIOは 3.3Vですので、

そのまま接続して特に問題なく書き込むことができました。

 

 

PIC12LF1840 マイコンのデータシートを見ると Device IDが 0x1BC0ですので、

DEVICE_LISTに定義を追加しています。

 

DEVICE_LIST = {
    0x2700: {  # Device ID
        "P": [0x0000, 0x0800, 0x3FFF],  # Address, Size, Value
        "C": [0x8007, 0x0002, 0x3FFF],  # Address, Size, Value
        "D": [0xF000, 0x0100, 0x00FF],  # Address, Size, Value
        "N": "PIC12F1822",  # Device Name
    },
    0x1BC0: {  # Device ID
        "P": [0x0000, 0x1000, 0x3FFF],  # Address, Size, Value
        "C": [0x8007, 0x0002, 0x3FFF],  # Address, Size, Value
        "D": [0xF000, 0x0100, 0x00FF],  # Address, Size, Value
        "N": "PIC12LF1840",  # Device Name
    },
}

 

ところで、前回の記事で 「Seeeduino XIAO ~」と書いてしまいましたが、

正しくは 「Seeed Studio XIAO ~」に変わっているのですね。失礼しました。

 

以上です。