久しぶりの投稿になりましたが、鉄道模型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\~\ |
それを実行すると下記の画面が表示されます。
M5Atom Liteの COMポート番号を選択して「Burn」ボタンをクリックするだけでファームウェアを書き込むことができます。
以上です。