M5Stack:MZ-80C / MZ-700 エミュレータ 移植メモ

エミュレータ移植時に詰まったことで、(自分のためにも)メモとして残しておきたいことをいくつか。


元ソースとの差分を取ればわかりますが、入力/出力以外のロジック大半は、ほぼ元コードのままです。というか、僕自身は、MZ-80Cのハードウェアや内部動作、エミュレータの仕組みについて全然詳しくない状態で、移植だけした感じです。スイマセン。

Arduinoでも、C / C++ のソースがここまでそのまま使えるもんなんですね。


移植にあたり、作業したことはこういう感じです。

M5Stack.h をinclude するために、Mzmain.c / MZhw.c / Mzscrn.c を *.cpp に。(とはいえ中身の記述は C のままです)
z80.h / Z80Code.h は extern “C” で include。

Webサーバでのコントロール関連はいったん削除。その後、M5Stackに合わせて省機能のWebサーバ機能を新規作成。
画面描画はM5Stackにあわせて実装。テレビくんで使った TFT_eSprite が活躍しました。

File読み込みはSDファイルからの読み込みに変更。

File dataFile = SD.open(romPath, FILE_READ);
while (dataFile.available()) {
    data = dataFile.read();
   (処理)
}
dataFile.close();

z80.h と Arduino.h の間で、word のtypedef の違いがありました。

z80.h: typedef unsigned short word;
Arduino.h: typedef unsigned int word;

z80側で使っている箇所そんなに多くなかったので、z80ソース内の word を

typedef unsigned short WORD;

に書き換えて対応してます。


メモリ確保は ps_malloc で。PSRAMから確保するように変更。4MBの広大なメモリが使えます!

mz_font = (uint8_t*)ps_malloc(ROMFONT_SIZE);

「アプリ終了時にはいったん本体リセットするでしょ」って考えのもとに、free処理はひとつもしてません!(良い子はマネしちゃダメ)

pthread 処理はそのまま使えました。

int st;
st = pthread_create(&scrn_thread_id, NULL, scrn_thread, NULL);
if(st != 0)
{
   perror("update_scrn_thread");
   return;
}
pthread_detach(scrn_thread_id);

Arduino 標準の処理、loop() には入らずに、setup() の中で自分でループをさせています。(Arduinoの作り的にはよろしくないのかな?)

本スレッドでZ80の処理を行い、画面描画、キー入力、Web処理はスレッド作成して処理してます。


エミュレータと関係ないとこでも結構はまりました。

SDからの読み込み、Serial出力/入力で止まってしまうことがありました。前後に短時間delayを入れるとなぜか安定する様子。
→ M5.begin() の中で Serial.begin() されていたのに、その後で Serial.begin() を再度呼んでしまっていた。Serial.begin() を2回やってしまうと、変な不具合が発生するみたい。Serial.begin() を呼ばないようにしたら問題解決。

M5.begin();
Serial.begin();

これはダメ。

M5.begin();

これだけでOK。


エミュレータ開始後に、SDカードにアクセスしようとすると、プログラムが落ちてしまう。
SDカードと液晶ディスプレイを同時に使うと、同じ信号線を使っているとかで不具合が起きるのかも?
→SDアクセス時(ROMファイル、テープファイル読み込み)に画面の更新を停止することで改善しました。


処理を追加していると、全体的に不安定になってしまった。SDカードアクセスしようとしただけで再起動したりする。
コンソールログを確認すると、

Task watchdog got triggered. The following tasks did not reset the watchdog in time:
03:08:42.929 -> - IDLE (CPU 0)
03:08:42.929 -> Tasks currently running:
03:08:42.929 -> CPU 0: pthread
03:08:42.929 -> CPU 1: IDLE

というのが定期的に出ていた。

調べてみたら、ループ内でdelay()が全く呼ばれない時に発生するとのこと。そんなわけないのになー、と見直してみたら

if(synctime - (syncTmp - timeTmp) > 0){
   delay(synctime - (syncTmp - timeTmp));
}

処理時間(syncTmp – timeTmp)が、待ちの時間(synctime)より長い場合、delay呼ばれてなかった…。

if(synctime - (syncTmp - timeTmp) > 0){
  delay(synctime - (syncTmp - timeTmp));
}else{
  delay(1);
}

delay(1);を入れることで動作が超安定!


Wi-Fi アクセスポイントは、簡単でした。

const IPAddress apIP(192, 168, 4, 1);
int randNumber = random(99999);
String adSSIDString = "M5Z700_" + String(randNumber);
const char* apSSID = adSSIDString.c_str();

WiFi.mode(WIFI_MODE_STA);
WiFi.disconnect(true);
delay(1000);
WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));
WiFi.softAP(apSSID);
WiFi.mode(WIFI_MODE_AP);
message = "[SSID:" + String(apSSID) + "] http://" + apIP.toString();

SSIDはランダム数字付にしてます。起動する前に、いったん disconnect(true) して、ちょっと待つのが安定起動のコツらしい。
softAPConfig のパラメータは、IPアドレス, ゲートウェイ, サブネットマスク。
IPアドレスとゲートウェイは同じで、サブネットマスクは IPAddress(255, 255, 255, 0)で問題ないと思います。


Wi-Fi 接続するならこう。「WiFi.begin()」よりも「WiFiMulti」使った方がよいそうです。

WiFi.disconnect(true);
delay(1000);
WiFi.mode(WIFI_STA);
wifiMulti.addAP(ssid, pass);
Serial.println("Connecting Wifi...");

if(wifiMulti.run() == WL_CONNECTED) {
   //接続成功
   Serial.println("IP address: ");
   Serial.println(WiFi.localIP());
}else{
  //接続エラー
  return false;
}

こちらも、いったんdisconnect() してちょっと待つのが安定起動のポイント。SSID と PASS 指定して接続します。


Webサーバもらくらくでした。

WebServer webServer(80);

ポート指定して、Webサーバ変数を定義。

webServer.on("/selectRom", selectRom);

こんな感じで、URLごとの処理を定義できます。上の例だと、Webブラウザから「http://[サーバIPアドレス]/selectRom 」に接続時に、selectRom 関数が呼び出されるようになります。

関数の中で、Webブラウザから送られたパラメータを取得することができます。形式は、POSTでもGETでもOK。助かるー。

String romFile = webServer.arg("romFile");

値は、URL Encode されているので、必要なら URL Decode してください。

関数終了時に、WebブラウザにHTMLを返す場合は、これでいけます。

webServer.send(200, "text/html", HTMLString ));

このあたりを定義した後で、Webサーバの起動。

webServer.begin();

スレッドループの中で

webServer.handleClient();

を呼ぶと、Webへの接続が処理されます。

webServer.stop();

で、Webサーバ停止します。webServer.handleClient(); は、起動時のみ呼び出されるようにフラグ制御しています。


音は ledcWriteTone で出しました。

こちらを参考にしました。

ESP32-WROOM-32
https://ht-deko.com/arduino/esp-wroom-32.html

setup() で

ledcSetup(LEDC_CHANNEL_0, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT);
ledcAttachPin(SPEAKER_PIN, LEDC_CHANNEL_0);

としておいて、

ledcWriteTone(LEDC_CHANNEL_0, 1000000 / freqtmp);

で、音が出ます。

単に周波数の音を出しているだけで、音色の計算しているわけではないので、音を出していても、動作が重くなっている感じはあまりないですね。音量調整ができなくて、ちょっと(かなり)うるさいです。深夜にはキビシイ。