[MSX0] MSX0からUART接続のデバイスに通信する #MSX0

DFPlayer MiniをMSX0から制御したい

最近、DFPlayer MiniというMP3再生用のモジュールがあるのを知り、年末年始でいじってました。 このモジュール、何が良いかというと、操作コマンドをUARTで行えるので、制御のハードルが比較的低いことがあります。

これを、ポケコンとかMSX0とかのデバイスから制御できないか・・・?ということで、本記事はMSX0での接続についてです。

接続の仕組み

御存知の通り?MSX0は2024年1月現在、IoT BASIC/BIOSではUARTをサポートしていません。 外部との通信はWiFiかI2Cになります。

シリアルコンソールもあるといえばあるのですが、こちらはMSX0をリモートで操作するための仕組みなので、MSX0へのインプットとして使う手はあるものの、MSX0から外部へのデータ送信に使うには中の人にはちょっとハードルが高めではあります。

もちろん、WiFiという手もあるのですが、今回はとりあえずMSX0らしく?ということで、I2Cで送ったデータをUARTに流してもらうところを実装してみました。 変換エンジンには、Raspberry Pi Picoを利用しています。

仕掛けはとても単純で、

  • まず、MSX0から「データ長(送信データのバイト数)」と「返却を受け取るかどうか(1=受け取る、それ以外=受け取らない)」をCALL IOTPUTで2バイト送信。
  • その後、「データ本体」をまとめてCALL IOTPUTで送信。
  • ラズパイpico側はそれらの指示に従い、所定のバイト数を受け取ったら、「データ本体」をUARTに送信。
  • 「返却を受け取るかどうか」に1を指定したときは、続けてUARTからレスポンスを受け取り、I2C側に返却する。
  • MSX0ではCALL IOTGETでレスポンスを受信。

ざっくりいうと上記のような感じです。

下記ファイルはGitHubでも公開しています。 https://github.com/MobileFF/msx0_sample/tree/main/I2C_to_UART

MSX0側の実装

1 'SAVE"I2C_UART.BAS"
1000 'Init
1010   D$="3C"
1020   _IOTFIND("device/i2c_a",C)
1030   PRINT "IOTFIND:";C
1040   IF C=0 THEN 1200:'Not Found
1050   _IOTFIND("device/i2c_a",A$(0),C)
1060   PRINT "Slave Address : ";
1070   FOR I=0 TO C-1
1080     PRINT A$(I);" ";
1090     IF A$(I)=D$ THEN 1300:'Create Device Path
1100   NEXT I
1200 'Not Found
1210   PRINT "Device not found."
1220   END
1300 'Create Device Path
1310   N$="device/i2c_a/"+A$(I)

2000 'Main
2010   PRINT
2020   PRINT "---- DFPlayer Mini ----"

3000 'INIT
3010 PRINT "---- RESET ----"
3100 RESTORE 40000:'RESET
3120 GOSUB 20000:'RESET
3125 FOR I=0 TO 1000:NEXT I
3130 GOSUB 20000:'SET VOLUME
3140 GOSUB 20000:'REPEAT TRACK 1
3150 RESTORE 40300:'QUERY STATUS
3160 GOSUB 20000:'QUERY STATUS
3170 FOR I=0 TO 100:NEXT I
3180 GOTO 3150
9999 END

20000 'Write Multi Bytes
20100 READ LN
20110 READ NR
20120 PRINT "[";LN;",";NR;"]"
20200 T1$=CHR$(LN)+CHR$(NR)
20210 _IOTPUT(N$,T1$)
20300 T2$=""
20310 PRINT "[";
20320 FOR I=1 TO LN
20330   READ V$
20340   PRINT V$;",";
20350   T2$=T2$+CHR$(VAL("&H"+V$))
20360 NEXT I
20370 PRINT "]"
20380 _IOTPUT(N$,T2$)
20400 IF NR=0 THEN RETURN
20500 _IOTGET(N$,RP$)
20600 PRINT ">> ";
20610 IF LEN(RP$) = 0 THEN GOTO 20500
20620 FOR I=1 TO LEN(RP$)
20630 PRINT HEX$(ASC(MID$(RP$,I,1)));",";
20640 NEXT I
20650 PRINT
20999 RETURN

40000 'RESET
40010 DATA 8,0
40020 DATA 7E,FF,06,0C,00,00,00,EF
40100 'SET VOLUME
40110 DATA 8,0
40120 DATA 7E,FF,06,06,00,00,10,EF
40200 'REPEAT TRACK 1
40210 DATA 8,0
40220 DATA 7E,FF,06,08,00,00,01,EF
40300 'QUERY STATUS
40310 DATA 8,1
40320 DATA 7E,FF,06,42,00,00,00,EF

上記の仕掛けに基づき、データ送信サブルーチン(20000〜20999行)では、以下のようにDATA文から2つのデータを読み込み、_IOTPUTで送信しています。 ここでは、LN=8およびNR=0なので、[8,0]という2バイトを送信しています。

20100 READ LN
20110 READ NR
20200 T1$=CHR$(LN)+CHR$(NR)
20210 _IOTPUT(N$,T1$)

その後、さらにDATA文から以下のように指定バイト数分(変数LNで決定。ここでは8バイト)データを読み込んで変数T2$に追加していき、ループ終了後に_IOTPUTT2$の内容(8バイト)を送信しています。

20300 T2$=""
20320 FOR I=1 TO LN
20330   READ V$
20350   T2$=T2$+CHR$(VAL("&H"+V$))
20360 NEXT I
20380 _IOTPUT(N$,T2$)

ただ一方的にデータを送り続けるだけで良ければ、先頭2バイト([8,0])は不要で、データそのものをひたすら送るでも大丈夫なのですが、応答を取得できるようにするため、いったん送信がどこで終わるかを知る必要があるため、最初にバイト数を指定しています。

DATA文に記載の8バイトのデータはDFPLayer Miniを想定したものですが、制御コマンドの仕様等については、こちらにあるデータシートPDFを参考にしています。

Raspberry Pi pico側の実装

Raspberry Pi picoの環境はMicroPythonを想定しています。ファイルはmain.pyi2c_slave.pyになります。

I2Cは0番、UARTは1番を使いますので、

  • 0番ピン=I2C(0) SDA
  • 1番ピン=I2C(0) SCL
  • 6番ピン=UART(1) TX
  • 7番ピン=UART(1) RX

につなぎます。

ラズパイpicoとDFPlayerMiniの間は、

ラズパイpico ←→ DFPlayer Mini
6番ピン(TX) ←→ 2番ピン(RX)
7番ピン(RX) ←→ 3番ピン(TX)

となります。

[main.py]

from i2c_slave import I2C_slave
import time
from machine import UART,Pin

# I2C設定 (I2C識別ID 0or1, SDA, SCL)
i2c = I2C_slave(0,sda=0,scl=1,slaveAddress=0x3C)

# UART初期設定(UART番号,クロックレート,TXピン:GP4/6Pin,RXピン:GP5/7Pin)
uart = UART(1,baudrate=9600,tx = Pin(4),rx = Pin(5))

while True:
    length = i2c.get()
    need_resp = i2c.get()
    print("length {0}/ need_resp {1}".format(length,need_resp))
    payload = bytearray(length)
    for i in range(length):
        payload[i] = i2c.get()
        print("{0}:{1}".format(i,hex(payload[i])))
    print(payload)
    uart.write(payload)
    uart.flush()
    while (uart.txdone()==False):
        pass
    time.sleep(0.01)
    if need_resp == 1:
        response = bytearray()
        waitCount = 0
        while True:
            resp = uart.read(1)
            if resp == None:
                waitCount += 1
                if waitCount>10000:
                    break
            else:
                response.append(resp[0])
        print(response)
        while i2c.anyRead()==False:
            pass
        for d in response:
            i2c.put(d)

以下のI2Cスレーブ動作用のクラスは、

Use Pico as an I2C slave? - Raspberry Pi Forums

に掲載されていたコードを拝借しています。作者の方に深く感謝いたします。

[i2c_slave.py]

from machine import mem32,Pin

class I2C_slave:
    I2C0_BASE = 0x40044000
    I2C1_BASE = 0x40048000
    IO_BANK0_BASE = 0x40014000
    
    mem_rw =  0x0000
    mem_xor = 0x1000
    mem_set = 0x2000
    mem_clr = 0x3000
    
    IC_CON = 0
    IC_TAR = 4
    IC_SAR = 8
    IC_DATA_CMD = 0x10
    IC_RAW_INTR_STAT = 0x34
    IC_RX_TL = 0x38
    IC_TX_TL = 0x3C
    IC_CLR_INTR = 0x40
    IC_CLR_RD_REQ = 0x50
    IC_CLR_TX_ABRT = 0x54
    IC_ENABLE = 0x6c
    IC_STATUS = 0x70
    
    def write_reg(self, reg, data, method=0):
        mem32[ self.i2c_base | method | reg] = data
        
    def set_reg(self, reg, data):
        self.write_reg(reg, data, method=self.mem_set)
        
    def clr_reg(self, reg, data):
        self.write_reg(reg, data, method=self.mem_clr)
                
    def __init__(self, i2cID = 0, sda=0,  scl=1, slaveAddress=0x41):
        self.scl = scl
        self.sda = sda
        self.slaveAddress = slaveAddress
        self.i2c_ID = i2cID
        if self.i2c_ID == 0:
            self.i2c_base = self.I2C0_BASE
        else:
            self.i2c_base = self.I2C1_BASE
        
        # 1 Disable DW_apb_i2c
        self.clr_reg(self.IC_ENABLE, 1)
        # 2 set slave address
        # clr bit 0 to 9
        # set slave address
        self.clr_reg(self.IC_SAR, 0x1ff)
        self.set_reg(self.IC_SAR, self.slaveAddress &0x1ff)
        # 3 write IC_CON  7 bit, enable in slave-only
        self.clr_reg(self.IC_CON, 0b01001001)
        # set SDA PIN
        mem32[ self.IO_BANK0_BASE | self.mem_clr |  ( 4 + 8 * self.sda) ] = 0x1f
        mem32[ self.IO_BANK0_BASE | self.mem_set |  ( 4 + 8 * self.sda) ] = 3
        # set SLA PIN
        mem32[ self.IO_BANK0_BASE | self.mem_clr |  ( 4 + 8 * self.scl) ] = 0x1f
        mem32[ self.IO_BANK0_BASE | self.mem_set |  ( 4 + 8 * self.scl) ] = 3
        # 4 enable i2c 
        self.set_reg(self.IC_ENABLE, 1)


    def anyRead(self):
        status = mem32[ self.i2c_base | self.IC_RAW_INTR_STAT] & 0x20
        if status :
            return True
        return False

    def put(self, data):
        # reset flag       
        self.clr_reg(self.IC_CLR_TX_ABRT,1)
        status = mem32[ self.i2c_base | self.IC_CLR_RD_REQ]
        mem32[ self.i2c_base | self.IC_DATA_CMD] = data  & 0xff

    def any(self):
        # get IC_STATUS
        status = mem32[ self.i2c_base | self.IC_STATUS]
        # check RFNE receive fifio not empty
        if (status &  8) :
            return True
        return False
    
    def get(self):
        while not self.any():
            pass
        return mem32[ self.i2c_base | self.IC_DATA_CMD] & 0xff
    
    if __name__ == "__main__":
        import utime
        from machine import mem32
        from i2cSlave import i2c_slave
        
        s_i2c = i2c_slave(0,sda=0,scl=1,slaveAddress=0x41)
        counter =1
        try:
            while True:
                if s_i2c.any():
                    print(s_i2c.get())
                if s_i2c.anyRead():
                    counter = counter + 1
                    s_i2c.put(counter & 0xff)
            
        except KeyboardInterrupt:
            pass

おことわり

本記事で使用しているDFPlayer Miniは正規品ではなく互換品のため、プログラムで想定している挙動が、正規品とは異なっている可能性があります。あしからずご了承ください。 ちなみに中の人が入手した互換品はAmazonで5個セット1,399円のものです。

コメント