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$
に追加していき、ループ終了後に_IOTPUT
でT2$
の内容(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.py
とi2c_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円のものです。
コメント
コメントを投稿