ログファイルへの書き込みを追いかける

目標

ログファイルに処理記録が書き出される時に、追加された書き込みを見守りたい。

方法

ファイルに追加された書き込みを追いかけるには、tailコマンドに-Fオプションを付けて使用します。

$ tail -F logfile

これで追加された書き込みが標準出力に表示され続けます。

tailコマンドには似たオプションの-fがあります。これも同じく追記された部分が表示されますが、ログローテートなどでファイル名が変更された時の動作が異なります。

オプション ファイル名が変わった時の動作
-F 同名のファイルを開いて追記を追いかける。
-f そのまま追記を待ち続ける。

ログを追いかけている時にファイル名が変わるのは大抵ログローテートによるものなので、新しいログファイルを開いて追記を追いかけれれる-Fオプションを使うことになります。

lessコマンドもある

ログの追記を見守りたいが、同時に過去の書き込みも遡って読みたい時にはlessコマンドが便利です。

$ less logfile

Gキーを押すと常にファイルの最後が表示され、再度Gを押すとその時の最後に更新されます。

そしてFキーを押すとtail -fと同じくファイルに追記されるたびに更新されます。ただし追記から更新までのタイムラグがtail -fよりも少し大きくなります。また-fオプションと同じなので、ファイル名が変更されても同名の新しいファイルには自動では切り替わりません1

脚注

  1. lessのコマンド入力モードでhキーを押してヘルプを表示させれ戻ると、開いていたファイルと同名の新しいファイルの内容が表示されます。逆に元のファイルに戻りたい時にはどうしたら良いのか?

ListViewを含むDialogFragmentの表示を速くする

症状

custom viewを表示するリストをDialogFragmentで表示させようとしたところ、Dialogが表示されるまで二秒以上かかっていました。

どこで時間がかかるのか調べてみると、ListViewに設定したadapterのgetView()メソッドが繰り返し呼び出されていました。

改善方法

そこで同じような例がないかとググってみると、layout_heightをmatch_parentにすればgetView()メソッドの無駄な呼び出しを抑えられるというページ1を見つけました。

そこでDialogFragment内でAlterDialog.Builderに設定するViewを次のようにしてみましたが、最初のgetViewから最後まで2.1秒もかかって効果はありませんでした。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/node_list_view" />
</LinearLayout>
12-27 12:29:22.896 31433-31433/jp.nosuz.* D/DialogList: getView: 0
12-27 12:29:22.916 31433-31433/jp.nosuz.* D/DialogList: getView: 1
...
12-27 12:29:25.028 31433-31433/jp.nosuz.* D/DialogList: getView: 7
12-27 12:29:25.028 31433-31433/jp.nosuz.* D/DialogList: getView: 8

そこでさらに検索するとRelativeLayoutが有効というページ2を見つけました。

このページに従ってLinearLayoutを次のようにRelativeに書き換えると、getView()メソッドを呼び出す回数がぐっと減りました。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/node_list_view" />
</RelativeLayout>

最初のgetViewから最後まで2.1秒かかっていたものが0.07秒になり、表示されるまでの「あれ?」というストレスがなくなりました。

12-27 12:39:58.967 9244-9244/jp.nosuz.* D/DialogList: getView: 16
12-27 12:39:58.977 9244-9244/jp.nosuz.* D/DialogList: getView: 15
12-27 12:39:58.987 9244-9244/jp.nosuz.* D/DialogList: getView: 14
12-27 12:39:59.007 9244-9244/jp.nosuz.* D/DialogList: getView: 13
12-27 12:39:59.007 9244-9244/jp.nosuz.* D/DialogList: getView: 12
12-27 12:39:59.017 9244-9244/jp.nosuz.* D/DialogList: getView: 11
12-27 12:39:59.027 9244-9244/jp.nosuz.* D/DialogList: getView: 10
12-27 12:39:59.027 9244-9244/jp.nosuz.* D/DialogList: getView: 9
12-27 12:39:59.037 9244-9244/jp.nosuz.* D/DialogList: getView: 8

欠点

ただしlayout_heightにmatch_parentを指定しているので、表示する項目数が少ない場合にもDialogが最大の大きさで表示され無駄な空白ができてしまいます。

参照

ArrayAdapterがNullPointException

NullPointExceptionという例外でアプリが終了するのはよくあるバグです。ログを見ればどこで例外を起こしているか分かるのですが、今回は次のようにArrayAdapterの中で例外が起きていて困りました。

このNullPointExceptionの原因は、ArrayAdapterを作る時に渡したList<>の中にnullが入っていたことでした。

FATAL EXCEPTION: main
Process: jp.nosuz.secretkeyring, PID: 11911
java.lang.NullPointerException
    at android.widget.ArrayAdapter.createViewFromResource(ArrayAdapter.java:394)
    at android.widget.ArrayAdapter.getView(ArrayAdapter.java:362)
    at android.widget.AbsListView.obtainView(AbsListView.java:2263)
    at android.widget.ListView.makeAndAddView(ListView.java:1790)
    at android.widget.ListView.fillDown(ListView.java:691)
    at android.widget.ListView.fillFromTop(ListView.java:752)
    at android.widget.ListView.layoutChildren(ListView.java:1630)
    at android.widget.AbsListView.onLayout(AbsListView.java:2091)
    at android.view.View.layout(View.java:14948)
    at android.view.ViewGroup.layout(ViewGroup.java:4631)
    at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1671)
    at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1525)
    at android.widget.LinearLayout.onLayout(LinearLayout.java:1434)
    at android.view.View.layout(View.java:14948)
    at android.view.ViewGroup.layout(ViewGroup.java:4631)
    at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
    at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
    at android.view.View.layout(View.java:14948)
    at android.view.ViewGroup.layout(ViewGroup.java:4631)
    at android.support.v7.widget.ActionBarOverlayLayout.onLayout(ActionBarOverlayLayout.java:433)
    at android.view.View.layout(View.java:14948)
    at android.view.ViewGroup.layout(ViewGroup.java:4631)
    at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
    at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
    at android.view.View.layout(View.java:14948)
    at android.view.ViewGroup.layout(ViewGroup.java:4631)
    at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1671)
    at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1525)
    at android.widget.LinearLayout.onLayout(LinearLayout.java:1434)
    at android.view.View.layout(View.java:14948)
    at android.view.ViewGroup.layout(ViewGroup.java:4631)
    at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
    at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
    at android.view.View.layout(View.java:14948)
    at android.view.ViewGroup.layout(ViewGroup.java:4631)
    at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:1991)
    at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1748)
    at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1004)
    at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:5692)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:761)
    at android.view.Choreographer.doCallbacks(Choreographer.java:574)
    at android.view.Choreographer.doFrame(Choreographer.java:544)
    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:747)
    at android.os.Handler.handleCallback(Handler.java:733)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:212)
    at android.app.ActivityThread.main(ActivityThread.java:5151)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:515)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:877)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:693)
    at dalvik.system.NativeStart.main(Native Method)

参照

Spinner: NullPointerException at ArrayAdapter.createViewFromResource –stackoverflow

Androidで高速なファイルコピー

Android(Java)でファイルコピーをするのには、単純にInputから読んで、それをそのままOutputに書き出す方法が単純です。しかしFileChannelを使用するとシンプルかつ高速にファイルをコピーすることができます。

ただしFileChannelを使用したコピーでは一度にコピーできるサイズが2GBに制限されるという仕様があるので、どこまでコピーしたか確認しながらコピーするように修正しました。

public static boolean copy(File src, File dst) {
        boolean result = false;
	
        FileInputStream inStream = null;
        FileOutputStream outStream = null;
        try {
            inStream = new FileInputStream(src);
            outStream = new FileOutputStream(dst);
            FileChannel inChannel = inStream.getChannel();
            FileChannel outChannel = outStream.getChannel();
            long pos = 0;
            while (pos < inChannel.size()) {
                pos += inChannel.transferTo(pos, inChannel.size(), outChannel);
            }
            result = true;
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (inStream != null) inStream.close();
                if (outStream != null) outStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if ((! result) && dst.exists()) dst.delete();

        return result;
    }

純粋にJavaでのコピーならが、Files.copy()を使用するのが一番簡単です。

参照

How to make a copy of a file in android? – stackoverflow

AndroidのService#OnStartCommand()の返り値

Androidのサービスは、起動する時に呼ばれるメソッドOnStartCommandの返り値によって強制終了された時の振る舞いが変わります。

START_STICKY
強制終了されても自動的にサービスが再起動されます。再起動時にはOnStartIntentに渡されるintentがnullの可能性があります。
START_NOT_STICY
強制終了されても再起動はされない。
START_REDELIVER_INTENT
強制終了されても自動的にサービスが再起動されます。この時START_STICKYとは異なり最初に起動された時のintentが再起動する時にも渡されます。

START_STICKY or START_REDELIVER_INTENT

サービスが強制終了された時に自動的に再起動するための返り値が二種類あるけど、intentの有無の他に違いはないのでしょうか?

そこで簡単に実験してみたところ、START_STICKYとSTART_REDELIVER_INTENTではサービスが再起動されるまでの時間が異なるようです。具体的には、START_STICKYは強制終了してもすぐに再起動されますが、START_REDELIVER_INTENTは再起動されるまでに分単位で時間がかかりました。

返り値 再起動までの時間
START_STICKY すぐに再起動
START_REDELIVER_INTENT 再起動まで1分くらい

サービスが強制終了させられてもすぐに同じintentでサービスを開始したい場合は、intentの内容をSharedPreferenceやデータベースに書いておき、START_STICKYを指定してサービスを起動するのが良さそうです。そしてintentがnullでサービスが起動されたら保存しておいたintent内容を読んで再開するという方法を取ります。

参照

Service – Developers

UbuntuでCapsLockキーをControlキーに

LinuxでCapsLockとControlキーを入れ替えるには一般的にxmodmapコマンドを使用します。しかしUbuntuでCapsLockキーとControlを入れ替えるには、dconfコマンドで簡単に変更できます。

参照したページではUbuntu 14.04LTSが対象と書かれていましたが、Ubuntu 16.04LTSでも有効でした。

dconfでCapsLockキーもControlキーに

今回はControlキーが壊れたようで認識しなくなったので、交換ではなく次のようにしてCapsLockもControlキーとすして使えるようにしました。

$ dconf write /org/gnome/desktop/input-sources/xkb-options "['ctrl:nocaps']"

xmodmapでCapsLockキーもControlキーに

xmodmapコマンドを使用してCapsLockキーもControlキーと使用できるようにするには、例えば設定を記述するファイルとして~/.Xmodmapファイルを作成して、そこに次のように設定を書き込みます。

keycode  66 = Control_L Control_L Control_L Control_L
remove Lock = Control_L
add Control = Control_L

これをxmodmapコマンに読み込ませます。

$ xmodmap ~/.Xmodmap

この方法は、dconfと異なりログアウトすると設定を忘れてしまうので、.bash_profileにxmodmap ~/.Xmodmapを追加しておく必要があります。

参照

UbuntuTips/Desktop/HowToSetCapsLockAsCtrl – ubuntu Japanese Team Wiki

Android Studioの編集画面を二分割

別のファイルや編集中のファイルの別の箇所を参照しながら編集する時には、画面を分割して同時に見られるそうにすると便利です。

Android Studioで編集画面を分割するには、メニュータブからは、「Window→Editor Tabs→Spilit」と進みます。

標準のキーマップではこの操作にショートカットが設定されていませんが、Emacsに変更すると、次のようにEmacsでの画面分割キーシーケンスを使用できます。

縦に二分割(上下に分割)
Ctrl-x, 2
横に二分割(左右に分割)
Ctrl-x, 3
分割画面を結合
ファイル名タブの「X」をクリックするとタブが消え、全てのタブがなくなると画面が結合されます。またCtrl-x, 1では、全ての分割を一度に消して一つの画面にできます。

分割した画面の割合は、画面の境界をドラッグすることで自由に変更できます。

分割した画面で同じ操作を繰り返すと、さらに場面を分割することができます。

screenshot-from-2016-12-14-16-43-28

別のウィンドウへ

画面を分割ではなく別のウィンドウで開くことも可能です。

別のウィンドウで開くには、ファイル名のタブをつかんでAndroid Studioのウィンドウの外にドラッグします。

Android Studioでプロジェクトのパッケージ名を変更する方法

  1. package_name(app -> java -> package_name)の上で右クリックする。
  2. Refactor -> Renameを選択する。
  3. Rename packageボタンをクリックする。
  4. パッケージ名を聞かれるので、新しいパッケージ名を入力してRefactorボタンをクリックする。Search in comments and stringsとSearch for text occurencesをチェックしておく。
  5. Do Refactorボタンをクリックする。

参考にしたstackoverflowの回答では、build.grade(Module:app)のApplication IDを手で編集するようになっていましたが、新しいパッケージ名を入力するダイアログでSearch in comments and stringsとSearch for text occurencesをチェックしておいたからか、AndroidManifest.xmlとbuild.grade(Module:app)もいい具合に変更されました。

プロジェクトの名前を変更する

ちなみにプロジェクト名を変更したい時は、次のようにします。

  1. Android Studioで開いているプロジェクトを閉じる。
  2. プロジェクトのディレクトリー名を変更する。
  3. Android Studioで名前を変更したプロジェクトを開く。

参照

BluetoothモジュールRN-42をお試し

Microchip製のBluetoothモジュールRN-42を使ってみました。このモジュールは、シリアルから設定を簡単に変更でき、キーボードとしてパソコンに接続できることを確認しました。マイコンをBluetoothで通信させるのに便利そうです。

パソコンに定形文字列を入力する方法を探していました。そこでマイコンをキーボードなどのHIDとするためのライブラリーやモジュールを探していて、Microchip製のBluetoothモジュールRN-42を見つけました。このモジュールを使うとシリアル端子に送った文字があたかもBluetooth接続のキーボードから入力されたように見せかけられます。そこでどんな感じなのか試してみました。

RN-42とは

RN-42は、Microchips製1のBluetooth Ver. 2.1に対応したモジュールです。内蔵プロファイルにはシリアル接続するためのSPP(Serial Port Profile)の他にHID(Human Interface Device)プロファイルが組み込まれており、モジュールにシリアル端子からコマンドを送ることでプロファイルや設定を簡単に切り替えることができます。

海外の無線モジュールを使用する時に問題になる法律面の制限(いわゆる技適マーク)もクリアしています

RN-42の購入

RN-42はモジュールとして電子部品店で容易に購入することができます。ただ今回は簡単に使い方を試してみたかったので、USB端子に接続するだけで電源とシリアル通信回路が提供される評価キットを購入しました。

秋月電子ではMicrochip純正の評価キットマイクロチップ Bluetooth評価キット RN-42-EKと秋月電子オリジナルのRN-42使用 Bluetooth無線モジュール評価キットを扱っています。どちらの評価キットもUSB-シリアル変換チップを搭載していて、電源もUSBから供給できるようになっています。

二つに機能的な違いは無いようなので、値段の安い秋月オリジナルの評価キットを購入しました。

RN-42を設定

RN-42評価モジュールをUSB端子に接続したら、ターミナルソフトを起動します。私はUbuntu上のcuコマンドを使用しました2。WindowsならばTeraTermなどが使用できます。接続するときのパラメータは、初期設定では次のようになっています。

設定
速度 115200 bps
長さ 8 bit
パリティ なし
ストップビット 1

RN-42のコマンドは、RN41/RN42 Bluetooth Data Module Command Reference User’s Guideを参照しました。

次のはRN-42との通信例です。ここで「#」で始まる行は操作のコメントを示します。太文字の部分が実際に入力した行です。

$ cu -l /dev/serial/by-id/usb-FTDI_FT231X_USB_UART_DQ00380E-if00-port0 -s 115200
Connected.
# コマンドモードに入る。
# $$$の後には[Enter]が不要です。
# それ以外のコマンドでは、コマンドの後に[Enter]が必要です。
$$$
CMD
# コマンドモードに入ると、緑色のLEDが非常に早く点滅します。

# RN-42のfirmware versionを確認。
V
Ver 6.15 04/26/2013
(c) Roving Networks
AOK
# このfirmwareは、HIDモードではGPIO4によるFactory Reset機能が働きません。
# 新しいver. 6.30ではこの問題を解消済みです。

# 工場出荷時の設定に戻す。
SF,1
AOK

# ペアリングに必要なPINを設定する。初期設定では1234となっています。
SP,01234
AOK

# pairing modeに設定する。電源が入ると自動的にペアリングした相手と接続する。
SM,6
AOK

# HIDモードにする。
S~,6
# キーボードに設定する。工場出荷時のデフォルト。
SH,0200
AOK

# デバイスに名前を付ける。
# 名前を付けるコマンドには二種類あります。SNコマンドは、指定した名前がそのまま設定されます。一方S-コマンドは、名前の後にデバイスのMAC Adressの最後2バイト分が加えられます。
SN,RN42HID
# or
S-,RN42HID

# 設定内容は、多くの場合設定コマンドのSをGに変えたコマンドで取得できます。
GP
01234

# リブートして設定を反映させる。
R,1
Reboot!

R,1コマンドでリブートすると、緑色LEDがゆっくりと点滅するようになります。そしてRN-42と別のパソコンとのペアリングを完了すると、ゆっくりと点滅していた緑色LEDが点灯したままとなります。

言うまでもありませんが、RN-42を接続したパソコンでペアリングすると、入力した文字が無限ループしてしまいます。

RN-42のキーボードはUS配列

RN-42の設定とペアリングが完了したら、RN-42に送った文字が相手側のパソコンに入力されることを確認します。

この時、一部の文字が文字化けすることがあります。例えば「”@:」と入力すると、「*”+」となります。これは、キーボードの配列をパソコンが間違って認識しているためです。

HIDプロファイルで送られてくるデータは、「どの文字が入力されたか」ではなく「どのキーが押されたか」というデータです。RN-42は、シリアルに送られてきた「文字」をUS配列のキーボードで「どのキーが押されたか」に変換してパソコンに送ります。そのためパソコンがJIS配列のキーボードとして認識してしまうと一部の文字で文字化けが発生します。

同じ理由から「日本語」などASCII以外の文字列を送ることができません。

レイアウトをUS配列に設定

この文字化けを解消するには、パソコン側の設定を変更してRN-42のキーボードがUS配列のキーボードだと認識させる必要があります。

Windows Vistaに接続したキーボードをUS配列と認識させる方法は、WindowsでBTキーボードだけをUS配列で使う(るびゅ備忘録)を参考にさせてもらいました。

デバイスインスタンスを調べる

まず設定からデバイスマネジャを開いて、HIDキーボードのプロパティを確認します。この詳細タブを開いて「デバイスインスタンスパス」の値を調べます。

rn42-devmng

レジストリを変更する

次にコマンドプロンプトからregeditコマンドを起動して、該当するエントリーの「Device Parameters」に次のエントリーを追加します。

名前 種類 データ
KeyboardTypeOverride REG_DWORD 4
KeyboardSubtypeOverride REG_DWORD 0

rn42-regedit

設定を変更した後で、再度RN-42を起動またはリブートすると入力した文字がパソコンにも正しく入力されるようになります。

RN-42のハマりポイント

コマンドモードに入れない。
コマンドモードへ入るには、電源が入った時またはリブート後60秒以内に時間が制限されています。この時間は、STコマンドで変更できます。またペアリングが完了して接続している場合(HIDの時のみ?)もコマンドモードには入れません。
ファクトリーリセットできない。
RN-42モジュールは、GPIO4をHにして起動し、L,H,L,Hと1秒以上開けて変更することで工場出荷時の設定に戻すことができます。詳しいリセット方法は、RN41/RN42 Bluetooth Data Module Command Reference User’s Guideの3.7 DESIGN CONCERNSの3.7.1.2 FACTORY RESETを参照して下さい。
ただし、firmware ver. 6.30未満では、HIDプロファイルに設定しているとGPIO4によるファクトリーリセット機能が働きません。iPhoneにUSBキーボードを接続するための装置の作成 2:RN-42(mswinvksの忘備録)のコメント参照。この制限は、RN41/RN42 v6.30 RELEASE NOTES – MicrochipにあるようにVer. 6.30で解消されています。
文字化けする。
この問題は、RN-42のキーボードはUS配列を参照して下さい。

脚注

  1. RN-42はMicrochipの製品ですが、元々Roving Networks社が開発していました。そのため製品名にRNが入っているのだと思います。現在Roving Networks社はMicrochips社に吸収されています。

  2. Linuxでシリアルデバイスを指定する時には、/dev/serial/by-id/*を指定するとどれに接続しているか分かりやすいです。

Z80マイコンボードの製作

2015年夏に偶然Z80(正確にはTMPZ84C00)を見つけた事をきっかけに、今更ながらCP/Mマシンを製作することにしました。とりあえず試作回路で上手く行きそうな感触がつかめたので、試作回路にあった無駄な部分を省くと共にデータ通信の高速化を目指した基板を新しく作成しました。基板の設計データはGitHub上に公開しています。

CP/Mマシンに必要な仕様

CP/Mを動かすために必要な仕様は次のとおりです。

  • CPUはi8080とマシン語レベルで互換性があること。
  • メモリはO番地から最低20KiBのRAMがあること。
  • 入出力のためのコンソール端末があること。
  • 最低一台のディスクドライブがあること。
  • 割り込み機能は使用していない。

ちなみにCP/Mが全盛期だった頃のクロック周波数は数MHzと現在の1/1000という世界でした。

以下に各項目を具体的に検討していきます。

CPU

CP/Mが全盛時代にはi8080よりもZ80が一般的に使われていました。よってZ80はi8080とのマシン語レベルでの互換性に問題がないことは確かです。

今回はZ80と言っても現在手に入るZ80の後継チップであるZ84C00と通信機能などを集積したZ8S180を使用します。これらもソフトウェア的にはZ80と互換性があるのでCP/Mを動かすことに問題はありません。

Z80の時代はクロック周波数は数MHzでしたが、Z84C00とZ8S180は20MHz版1が手に入るのでより高速なCP/Mマシンを実現できそうです。

メモリ

必要なメモリー容量は今となっては誤差のような容量です。むしろ少容量すぎて入手できない可能性さえあります。しかし現在Z80の後継CPUが入手可能なように小容量なメモリーも種類が少ないながらもちゃんと販売されています。

秋月電子のページを見ると、小容量とは言っても昔とは比べられ物にならないアクセス速度のSRAMが販売されています。容量についても128KiBで十分すぎます。

HM678127UHJ-12(12ns)とM68AF127B(55ns)のどちらを使うかですが、55nsはほとんど20MHzの1サイクルの相当にするのでこれで十分かと余り考えずにM68AF127B(55ns)を選択しました。

タイミングの検討

余り考えずに選んだメモリですが、M68AF127B(55ns)で最高どのくらいの周波数まで使用できるかデータシートのタイムチャートを確認してタイミングを検討してみました。

Z80のメモリアクセスで一番厳しいのは命令を読み込む時です。通常メモリーを読み込むときはT3の立ち下がりに読み込むのですが、命令を読み込む場合は半クロック早いT3の立ち上がりで取り込まれます。

命令読み込みのタイミングチャート。Z8400/Z84C00 Z80 CPU Product Specificationより引用。
命令読み込みのタイミングチャート。 Z8400/Z84C00 Z80 CPU Product Specificationより引用。

Z84C00とZ8S180を比べるとZ84C00の方が時間がかかるようなので、Z84C00のデータを使ってタイミングを検討します。

Z84C00のデータシートを見るとタイミングは次のようになっています。

  • アドレスの値が確定するのは、T1の立ち上がりから最大57ns。
  • MREQとRDがLになるのは、T1の立ち下がりから最大40ns。
  • データは、T3の立ち上がりの最低12ns前に確定している必要がある。

またメモリのタイムチャートは次のようになっています。

  • tAVQV(アドレスの確定からデータの確定までの時間)は最大55ns。
  • tELQV(チップ選択からデータの確定までの時間)も最大55ns。
  • tGLQV(出力ゲートが開いてからデータが確定するまでの時間)は最大25ns。
メモリーのタイミングチャート。M68AF127Bのデータシートより引用。
メモリーのタイミングチャート。 M68AF127Bのデータシートより引用。

よって確定が一番遅いMREQとRDがLになってからT3が立ち上がるまでの時間は、次の式になります。

40ns(T1の立ち下がりからMREQがL)+ 55ns(EがLからデータが確定) + 12ns(T3立ち上がり前の余裕) = 1.5T(wait無し)

この等式を計算するとT = 71nsとなります。よって最大の周波数は約14MHzとなります。

また同様にZ8S180の場合を計算すると

25ns(T1の立ち下がりからMREQがL)+ 55ns(EがLからデータが確定) + 10ns(T3立ち上がり前の余裕) = 1.5T(wait無し)

となり、T = 60nsとなります。よって最大周波数は16.6MHzとなります。

リセット直後のプログラム

Z80はリセットした時に0番地からプログラムをスタートします。そのためメモリの最初の方はシステムの初期設定やCP/Mをディスクから読み込むプログラムがリセット前に書き込まれている必要があります。そのため一般的にはその領域に電源を切っても内容が消えないROMが配置され、CP/Mが起動する時にはRAMに置き換えるようになっていました。

現在も紫外線で消去できるUV-ROMは手に入ります。しかしそれを使うには、書き込みと消去の装置が必要になります。さらにアクセス速度が遅いのでwait回路が必要になり、RAMとの切り替え回路も必要となります。

そこでメモリは全てRAMにし、リセット直後に必要なプログラムはスイッチを操作してメモリにマシン語を一ステップずつ書き込むというより古い手法を使うことにしました。もっともこれは面倒なので、一ステップずつメモリに書き込む操作は現代的なマイコンに肩代わりしてもらいます。

コントローラ

リセット直後に必要なプログラムの書き込みには使い慣れているAtmelのAVRマイコンを使用します。Arduinoに使われているATmega328を考えていたのですが、これだとピン数が足りずにアドレスなどの値をラッチしておく回路が必要になります。そこで少し値段は高くなりますがアドレスとデータバスを一対一で割り当てられるATmega64aを使用することにしました。

このコントローラは、初期プログラムの書き込みだけでなく、Z80の起動後は次のように端末やディスクドライブのインタフェイスとしても機能します。

ATmega64aはクロック回路を内蔵していますが、少しでも早いほうが良いかと思いZ80に供給するクロックをATmega64でも使用できるようにしました。この場合最大周波数がATmega64aの最大周波数16MHzに制限されます。

ATmega64aのハマりポイント

ATmega64aは、買ってきたままだとATmega64aではありません。

ATmega103というMMUとの互換モードになっています。mainルーチン内だけで済むプログラムだと気が付かないのですが、別の関数を呼び出すとフリーズしてしまい「あれ?」となります。

ATmega64aを使う前には、まずfuse設定を変更してATmega103互換モードをOFF(unprogrammed)にします。

またPFを使うときにも注意が必要です。

ATmega64aはJTAGインタフェイスを持っていますが、そのインタフェイスがPFに割り当てられており、入出力端子としてPFを使用できません。

これも必要ならばfuse設定を変更してOFF(unprogrammed)にするか、プログラム内でMCUCSRのJTDをセットします。MCUCSRのJTDは、誤動作で書き換えてしまうことを防ぐために、二回連続してセットする必要があります2

入出力

Z80の入出力もATmega64aが担います。メモリ上に実行したい入出力操作を書き込んでおき、準備が整ったところでATmega64aに実行を依頼します。

ATmega64aに入出力を要求するシグナルを送る簡単な出力回路も検討しましたが、Z80のHALT信号をATmega64aへのシグナルに使う回路3が試作で上手く行ったので外付け部品が不要なこの方法を使うことにしました。

具体的にはATmega64aに実行してもらいたい入出力操作をメモリに書いておきhalt命令を実行してHALT信号をLにします。ATmega64はHALT信号がLになったことを確認したらメモリ上に書かれた入出力操作を実行して、Z80を割り込みでhalt命令の次から実行を再開させます。RESETでもHalt状態から抜けられますが、通常のRESETと区別する必要や再開後の設定など面倒なので割り込みシグナルを使用することにしました。特にNMIは間違って割り込みを無効にしてしまう心配が無く好都合です。

ただしこのNMIを使ってHalt状態から抜けだすのは、NMIの割り込みベクタがCP/Mの予約領域にあるという問題点があります。そこでhaltを実行する前に割り込みベクタである0x66番地のデータを退避しておき、NMIでHaltから抜けだした後に書き戻すことにしました。またCP/Mでは他の割り込みを使用しませんが、一応入出力操作が完了するまで待つビジーウエイトを入れておきます。

実際のルーチンは次のようになります。

viotrap0:
	ld	hl,(NMI_VECT)	; save value on NMI_VECT
	ld	bc,045edh	; set RETN code
	ld	(NMI_VECT),bc
	halt
viowait:
	ld	a,(virtio_command)
	or	a
	jr	NZ,viowait
	ld	(NMI_VECT),hl

LED

電子工作で動作確認の基本はLチカなので、Z80でLチカを出来るようにするためLEDを点灯できるようにします。

ディスクドライブ

ディスクドライブには、実物のフロッピーディスクドライブやSDメモリカードを使用する方法を考えましたが、データのやり取りが面倒そうなのでパソコン上のディスクイメージに直接アクセスすることにしました。これによりパソコン上で作成したバイナリやテキストファイルをディスクイメージにコピーするだけでZ80側から使用することができるようになります。

実際のディスクイメージへのアクセスは、Z80からATmega64aにディスクの読み書きを依頼し、ATmega64aがパソコン上のプログラムにディスクイメージのデータを読み書きする命令を送ります。

パソコンとATmega64aは、電源供給も兼ねてUSBで接続します。ただしATmega64aはUSBに直接接続できないので、FTDIのFT240XというUSB-FIFO変換チプを使用しました。FT240XはZ80のデータバスに直接接続できるので、データを直接メモリに書き込むことができ最高1Mbyte/secのデータ転送が可能です4

FT240Xは、ATmega64aから見ると8bitパラレルのFIFOデバイスですが、パソコンからはシリアル端末に接続したように見えます。そのためパソコン側のプログラムには特別なライブラリは不要です。例えばターミナルソフトを開いてAという文字を送ると、そのままTF240XのデータバスにAのASCIIコードが現れます。

コンソール端末

コンソール端末は簡略化のため自前でキーボードやディスプレイを持たずにパソコンを昔のディスプレイターミナルとして使用します。この通信にもディスクドライブと同じUSBを使用します。

コンソール端末はATmega64aの要求で動くディスクドライブと違ってパソコン側から非同期でデータが送られます。そのためディスクでのデータと混じってしまわないように非常に簡単なプロトコールでディスクのデータとコンソールのデータを多重化します。

リセット

リセットスイッチはZ80に直接接続しないで、ATmega64aに接続したスイッチが押されたらZ80のリセット端子をLにすることにしました。これによりチャタリング回避やリセット時間を確保する回路をなくすことができます。

基板の設計と組み立て

そうして完成したZ84C00とZ8S180ボードの回路図は次のようになります。

Z84C00を使ったZ80ボードの回路図。
Z84C00を使ったZ80ボードの回路図。
Z8S180を使ったZ80ボードの回路図。
Z8S180を使ったZ80ボードの回路図。

基板はKiCadを使って設計し、プリント基板の製造は格安なElecrowに注文しました5

必要な部品は、秋月電子Mouser千石電商から購入しました。

回路図や基板の設計データはGitHub上に公開しています。これをフォークしてより素晴らしい物を作る基板としてもらえると幸いです。

Z84C00用のプリント基板

Z8S180用のプリント基板

プリンプ基板は10枚単位での注文なため何枚か基板が余っています。もし欲しいという方がおりましたら送料のみでお分けいたします。ただし基板制作後にも回路図と基板設計に手を入れているので、GitHub上のデータとは多少パターンなどが異なります。

次は

Z80ボードが完成したので、次はATmega64aのプログラムとCP/MのBIOSを作成してCP/Mを動かします。

脚注

  1. Z8S180は20MHz版の他に30MHz版も存在します。ただし値段が急に高くなるので今回は20MHz版を購入しました。

  2. JTDビットは、正確には4CPUサイクル以内に2回セットする必要があります。

  3. halt命令をトラップに使用するアイデアは我ながら冴えた方法だと思ったのですが、既にネット上に公開されていました。T:(@T_colon) 部品を減らす工夫 – neko Java Home Page – GMOとくとくbb

  4. 試作段階ではパソコンとの接続に一般的なUSB-シリアル変換チップを使用しましたが、これでもデータ転送がそれほど遅くはありませんでした。

  5. 数千円と約1週間(国際宅急便使用)でプリント基板を製造してもらうことができます。精度などでは国内のしっかりした業者には及ばないのかも知れませんが、個人的に使用するには全く問題がないレベルです。手配線や自分でプリント基板を作る手間や時間を考えれば、このサービスを使わない手はありません。