MicroPython的午睡(37) ラズパイPicoとDMA3重塔でZ80の制御を奪う

Joseph Halfmoon

ノスタルジックな理由からZ80の機械語を走らせることができる実機を入手いたしました。しかしデバイス的な理由からそのままでは自分でコードを書いて走らせることができませぬ。そこでラズパイPicoで、ターゲットのメモリを乗っ取って書き換えてしまおう、と計画中。今回はラズパイPicoからターゲットメモリを読み取る(まだバグありそうだけれど)ところまで。

※「MicroPython的午睡」投稿順 Indexはこちら

(末尾に今回のテストで使ったMicroPythonスクリプト全文を掲げました。)

Z80ベースのボードを手に入れたときのノスタルジーな投稿は以下です。

冥界のLSI(9) TMPZ84C015BF、Z80複合チップ、1993年7月時点国内最速

こちらから購入させていただき、組み立ててテスト用のROMを使って動作確認まで行いました。問題は、プログラムを書き込むメモリです。時代も時代です。プログラムは紫外線消去型のEPROMに書き込む必要がありました。流石にEPROMライタ(今時の不揮発メモリと違って当時は書き込み時に高電圧必要だったので専用ツールがありました)とか紫外線消去器(記憶をクリアするときはシールを剝がして紫外線をかけるのです)の持ち合わせなどありません。

折角の「当時最速」12MHz動作ボードですが、4MHzに勝手にクロックダウンいたしました。RAMからでもプログラムを実行できるように、との改造であります。意図としては

    • Z80のバスをBUSREQ(DMA要求)使って乗っ取る
    • EPROMをSRAMで置き換える(書き込みは直接制御)

という感じです。0番地からのSRAMに自分のプログラムを書き込んでおいてから、Z80のRESETを放してやれば起動できる筈。

ラズパイPico+IOExpanderでソフトウエア制御「DMAコントローラ」

既に以下の投稿で、ラズパイPicoにMicrochip社のI2C制御IOExpander MCP23017を2個、合計32ビットのIOとして接続してあります。

MicroPython的午睡(35) ラズパイPico、MCP23017でIO端子数拡張

2個のExpanderを「0番」と「1番」と勝手に呼んでおり、それぞれ8ビットのGPIOポートA, Bを持つので、以下のように使用することにいたしました。

    • Expander0番、ポートA Z80データバス D7-D0の8本
    • Expander0番、ポートB Z80バスを乗っ取るための制御信号類8本
    • Expander1番、ポートA Z80アドレスバス A15-A8の8本
    • Expander1番、ポートB Z80アドレスバス A7-A0の8本

これをMicroPythonから制御することで、ソフトウエア制御の一種のDMAコントローラといたします。バスを乗っ取って動作させることで、Z80システム側の主記憶メモリを自在に操作できるようにしようというわけです。

原理は簡単、でも今時、パラレルバスのこの本数、工作するのが辛いです。とりあえず意図している回路を以下に掲げます。

PiPicoZ80DMAschematic

「DMAコントローラ」本体は3重の塔

ターゲットのTMPZ84C015BFボード(Z80と周辺回路を一体化した「複合チップ」。このころはまだSoCという言い方はなかった記憶)は、名刺サイズの小型ボードですが、4辺に2列のピンヘッダが並んでおり、Z80本体と周辺チップのほとんどの信号を取り出すことができます。Z80のアドレス、データバスについてはCN4というピンヘッダにまとめて出ています。Z80の制御信号類はCN1ピンヘッダです。

そこで、上記の回路を以下の3枚のボードに分割して実装することにいたしました。

第1のボード。CN4ピンヘッダに刺さるピンソケットを下側に持ち、第2のボードを刺すことができるピンソケットを上側に持つ、メスーメスタイプの変換ボード。ついでに順番にでていないアドレスバスの配置なども入れ替える。

第2のボード。第1のボードに刺すことができるピンヘッダを下に持ち、第3のボードを刺すことができるピンソケットを上側に持つ。また、ラズパイPicoからのI2C信号端子、および5V、3.3V、GND端子を側面に持つ。このボード上でI2C信号の3.3V-5Vレベル変換を行う。

第3のボード。第2のボードに刺すことができるピンヘッダを下に持つ。MCP23017を2個搭載し、第2のボードからくる5V電源で動作し、5V化されたI2C信号で制御される。アドレス、データの合計16本は第2のボードから縦に上がってくるので、それらに接続。制御信号の8本は側面から出力。制御信号はメスメスのジャンパ線でZ80システム側のCN1ピンヘッダのしかるべきところに接続する。

3重の塔、下のZ80ボードを入れたら4重、配線が見苦しいスタックの様子がこちら。

BoardTower一応ね、簡単な導通試験などして、ショートなどはないことは確認したのですが、配線間違い、いまいち不安な一品であります。

ラズパイPicoによるZ80の制御実験(第1回)

末尾に掲げたMicroPythonコードによる、第1回の実験はこんな状況です。

ラズパイPicoからZ80側にRESETかけることはできる。RESETかけるとZ80側で動作しているモニタ用のLチカが停止する。RESETをネゲートするとZ80側は再び実行を開始し、Lチカを続ける

ラズパイPicoからZ80側にBUSREQ(DMAリクエスト)をかけることができる。BUSREQかけた後、Z80からのBUSACKがアサートされていることを確認できる。BUSREQをネゲートすれば、BUSACKもネゲートされる。BUSACK期間中はZ80が実行しているLチカは中断している。

Z80側がBUSACKを返してきている状態(Z80側のアドレス、データバス、制御信号類はすべてHiZになっている)を検出したら、こちらのMCP23017のアドレスバスを出力、データバスを入力とした上で、MEMREQをアサートし、その状態のままRD信号をアサートするとこちらから出力しているアドレスのメモリの内容が読み出せる筈。一応できている「感じ」がします。同じアドレスに対して必ず同じ値が返ります。こんな感じ。

negate RESET w/BUSREQ
BUSACK= 0
ADR=0x0000 DAT=0xcd
ADR=0x0001 DAT=0x09
ADR=0x0002 DAT=0x11
ADR=0x0003 DAT=0xcd
ADR=0x0004 DAT=0xe2
ADR=0x0005 DAT=0x13
ADR=0x0006 DAT=0xd5
ADR=0x0007 DAT=0xcd
ADR=0x0008 DAT=0x73
ADR=0x0009 DAT=0x0e
ADR=0x000a DAT=0xc2
ADR=0x000b DAT=0x56
ADR=0x000c DAT=0x17
ADR=0x000d DAT=0x2a
ADR=0x000e DAT=0x28
ADR=0x000f DAT=0x80
BUSACK= 1

ただこれが本当に正しいメモリ内容であるのか不明(Z80のRESET後のスタートアドレスである0番地に0xcdとか書いてある時点でちょっと不穏な感じがする)メモリ操作そのものは安定してできている感じがするので、

    • アドレスバスの順番を3重の塔のどこかで取り違えている(0番地は間違いようがないが。。。)
    • データバスの順番を3重の塔のどこかで取り違えている
    • タイミングや極性など、どこかで取り違えている

要調査です。ここが固まったアカツキには、次のROMをSRAMでエミュレーションのステップに進みとうございます。いつになるのか?それにしても3重の塔が4重になるのか?もやはバベルの塔というべきか。

MicroPython的午睡(36) ThonnyでATOM Liteにインストール へ戻る

MicroPython的午睡(38) ラズパイPicoとDMA3重塔でZ80つづき へ進む

今回のテストに使用したMicroPythonコード(暫定版)
import time, usys
from machine import Pin, I2C

IOEX0 = 0x20
IOEX1 = 0x21
IOCONA = 0x0A
IOCONB = 0x0B
IODIRA = 0x00
IODIRB = 0x01
GPIOA = 0x12
GPIOB = 0x13
OLATA = 0x14
OLATB = 0x15

i2c = I2C(0, scl=Pin(17), sda=Pin(16), freq=100000)

def isIOEXavailable():
    lis = i2c.scan()
    if (IOEX0 in lis) and (IOEX1 in lis):
        return True
    return False

def readByte(i2cADR, regADR):
    bufW = bytes([regADR])
    ackW = i2c.writeto(i2cADR, bufW)
    bufR = bytearray(1)
    ackR = i2c.readfrom_into(i2cADR, bufR)
    return int.from_bytes(bufR, 'little')  

def writeByte(i2cADR, regADR, dat):
    bufW = bytes([regADR, dat])
    ackW = i2c.writeto(i2cADR, bufW)
    return ackW  

def letPortsHz():
    writeByte(IOEX0, IODIRA, 0xFF)
    writeByte(IOEX0, IODIRB, 0xFF)
    writeByte(IOEX1, IODIRA, 0xFF)
    writeByte(IOEX1, IODIRB, 0xFF)

def letADRbusOutput():
    writeByte(IOEX1, OLATA, 0xFF) # initial value, A15-A8
    writeByte(IOEX1, OLATB, 0xFF) # initial value, A7-A0
    writeByte(IOEX1, IODIRA, 0x00) # DIR=ouput A15-A8
    writeByte(IOEX1, IODIRB, 0x00) # DIR=ouput A7-A0

def letADRbusInput():
    writeByte(IOEX1, IODIRA, 0xFF) # DIR=input A15-A8
    writeByte(IOEX1, IODIRB, 0xFF) # DIR=input A7-A0

def outputADRbus(adr):
    adrH = (adr >> 8) & 0xFF
    adrL = adr & 0xFF
    writeByte(IOEX1, OLATA, adrH) # output A15-A8
    writeByte(IOEX1, OLATB, adrL) # output A7-A0
    
def letDATAbusInput():
    writeByte(IOEX0, IODIRA, 0xFF) # D7-D0 input

def letDATAbusOutput():
    writeByte(IOEX0, OLATA, 0xFF) # initial value, D7-D0
    writeByte(IOEX0, IODIRA, 0x00) # DIR=ouput D7-D0

def outputDATAbus(dat):
    dat &= 0xFF
    writeByte(IOEX0, OLATA, dat) # output D7-D0

def inputDATAbus():
    return readByte(IOEX0, GPIOA) # input D7-D0

# B0: MREQ, B1: IORQ, B2:RD, B3:WR
# B4: RST, B5:BUSREQ, B6:BUSACK, B7:ROMWR
def enableRSTBUSREQ():
    writeByte(IOEX0, OLATB, 0xFF) # initial value, Control signals
    writeByte(IOEX0, IODIRB, 0x4F) # RST, BUSREQ, ROMWR: output, BUSACK: input

def assertRST(breq):
    if breq:
        temp = 0xCF # RST and BUSREQ
    else:
        temp = 0xEF # RST only
    writeByte(IOEX0, OLATB, temp)

def negateRST(breq):
    if breq:
        temp = 0xDF # BUSREQ active
    else:
        temp = 0xFF # RST only   
    writeByte(IOEX0, OLATB, temp)

def isBUSACK():
    temp = readByte(IOEX0, GPIOB)
    return (temp >> 6) & 1

# B0: MREQ, B1: IORQ, B2:RD, B3:WR
# B4: RST, B5:BUSREQ, B6:BUSACK, B7:ROMWR
def enableControl():
    writeByte(IOEX0, OLATB, 0xDF) # RST=1, BREQ=0, Control signals = 1
    writeByte(IOEX0, IODIRB, 0x40) # RST, BUSREQ, ROMWR, Control signals: output

def disableControl():
    writeByte(IOEX0, OLATB, 0xDF) # RST=1, BREQ=0, Control signals = 1
    writeByte(IOEX0, IODIRB, 0x4F) # RST, BUSREQ, ROMWR, Control signals: input

# B0: MREQ, B1: IORQ, B2:RD, B3:WR
# B4: RST, B5:BUSREQ, B6:BUSACK, B7:ROMWR
def memoryWR():
    writeByte(IOEX0, OLATB, 0xDE) # RST=1, BREQ=0, MREQ=0
    writeByte(IOEX0, OLATB, 0xD6) # RST=1, BREQ=0, MREQ=0, WR=0
    writeByte(IOEX0, OLATB, 0xDE) # RST=1, BREQ=0, MREQ=0
    writeByte(IOEX0, OLATB, 0xDF) # RST=1, BREQ=0, Control signals = 1    

# B0: MREQ, B1: IORQ, B2:RD, B3:WR
# B4: RST, B5:BUSREQ, B6:BUSACK, B7:ROMWR
def memoryRD():
    writeByte(IOEX0, OLATB, 0xDE) # RST=1, BREQ=0, MREQ=0
    writeByte(IOEX0, OLATB, 0xDA) # RST=1, BREQ=0, MREQ=0, RD=0
    temp = inputDATAbus()
    writeByte(IOEX0, OLATB, 0xDE) # RST=1, BREQ=0, MREQ=0
    writeByte(IOEX0, OLATB, 0xDF) # RST=1, BREQ=0, Control signals = 1
    return temp

def testMemoryRead(adr):
    letADRbusOutput()
    letDATAbusInput()
    enableControl()
    for _ofs in range(16):
        outputADRbus(adr)
        print("ADR=0x{0:04x} DAT=0x{1:02x}".format(adr, memoryRD()))
        adr += 1
    letADRbusInput()
    disableControl()

def main():
    print("IO Expander is ", end="")
    if not isIOEXavailable():
        print("not available.")
        usys.exit(1)
    else:
        print("available.")
    letPortsHz()
    print("BUSACK=", isBUSACK())
    time.sleep(10)   

    enableRSTBUSREQ()
    for _i in range(2):
        print("assert RESET")
        assertRST(0)
        time.sleep(10)   
        print("BUSACK=", isBUSACK())
        print("negate RESET")
        negateRST(0)
        time.sleep(10)   
        print("BUSACK=", isBUSACK())
    for _i in range(2):
        print("assert RESET w/BUSREQ")
        assertRST(1)
        time.sleep(10)   
        print("BUSACK=", isBUSACK())
        print("negate RESET w/BUSREQ")
        negateRST(1)
        time.sleep(10)   
        print("BUSACK=", isBUSACK())
        testMemoryRead(0x00)

    letPortsHz()
    print("BUSACK=", isBUSACK())
    
if __name__ == "__main__":
    main()