この文書は、Arduino を使った YM2151 の VGM Player ハードを mame でエミュレーションしてみようとドライバー(mame用語・基板固有のエミュモジュール)を追加している時に作成した雑多なメモです。
2021/03 注) MAME のエミュレーションフレームワークやサウンドシステムが刷新されたため、記事中のソースコードは現在バージョンの MAME API になっていません。またドライバーのディレクトリ構成も変わっているのでご注意ください。
この文書でサンプルとしているハードは、Arduino マイコンから FM 音源を制御し VGM 形式となっているパッヘルベルのカノンを奏でる機械なので、mame 上では vgmduino
の kanon
というドライバー名称にしました。
ちなみに本気でドライバーを書く場合は、 Arduino + FM音源シールドのエミュレーターということになりますので、本来的にはシリアルUSB変換の ATmega16U2 なども接続することになると思います。
オペコードは正しく動作し YM2151 が発声しているものの、TIMER0 が未実装のため正しく楽曲になっていません。 TIMER0 を hack で実装してみたところ、正しく micros()
関数が動作するようになり発声の音長が反映され、正確ではないものの楽曲として再生できるようになりました。
/Library/Frameworks/SDL2.framework/
git clone https://github.com/mamedev/mame.git
cd mame
vgmduino.bin
だった例)# sha1 は Ubuntu に入ってる
sha1sum vgmduino.bin
4a74f844114cc5dd5722f98669542a9d53a91c2d vgmduino.bin
# crc32 は標準でないため apt で Perl のライブラリーのを拝借
apt-get install libarchive-zip-perl
crc32 vgmduino.bin
9fe6d5d0 vgmduino.bin
roms
ディレクトリにドライバー名.zip(ここでは kanon.zip
) で ROM バイナリーファイルを圧縮して mame ソースコードの roms
ディレクトリーに格納しておく。src/mame/drivers
に .cpp ファイルを追加(ここでは vgmduino.cpp
の例) - ソースの全体像は後述。vgmduino.cpp
で ROM_START を設定(ここでは kanon
ドライバー)ROM_REGION
で容量(0x8000
)と名前("maincpu"
) をかいて開始のサイン。ROM_START( kanon )
ROM_REGION( 0x8000, "maincpu", 0 )
/* Arduino UNO user program */
ROM_LOAD("vgmduino.bin", 0x0000, 0x7e00, CRC(9fe6d5d0) SHA1(4a74f844114cc5dd5722f98669542a9d53a91c2d) )
/* Arduino UNO bootloader 0x7e00 */
ROM_LOAD( "optiboot_atmega328.bin", 0x7e00, 0x200, CRC(388b1a0e) SHA1(529a4a966913261f0bc467ef80424bb74bd2cc03) )
/* on-die 1kbyte eeprom */
ROM_REGION( 0x400, "eeprom", ROMREGION_ERASEFF )
ROM_END
src/mame/mame.lst
に vgmduino.cpp
を追加
@source:vgmduino.cpp
kanon // vgmplayer
arcade.lua
にソースを追加(mics
のところでよい?) TODO: Makefile を調査
--------------------------------------------------
-- remaining drivers
--------------------------------------------------
createMAMEProjects(_target, _subtarget, "misc")
files {
MAME_DIR .. "src/mame/drivers/vgmduino.cpp",
}
arcade.flt
にソースを追加(必要?)TODO: Makefile を調査。
vgmduino.cpp
# j の値は CPU のコア数 + 1 が最速コンパイル。しかし全部もってかれるのでこの機械(コア4)では 3 に設定。
# SUBTARGET を指定するとそのドライバーだけビルドできる。
make -j3 SUBTARGET=kanon SOURCES=src/mame/drivers/vgmduino.cpp
ドライバーにデバイス(YM2151)などを追加した場合、いったんクリーンしないとリンクでエラーとなることがある。
make clean
# サブターゲット付きでビルドした場合、ドライバー名 + プラットフォーム名(64bit)の実行ファイルがつくられる。
./kanon64
# ウインドウ起動
./kanon64 -window -resolution 1024x768
# デバッガー付き起動(エミュレーション CPU の最初のステップで停止する)
./kanon64 -debug -window -resolution 1024x768
# ランチャーが不要の場合はドライバー名を引数に指定すると一気に起動
./kanon64 kanon -window -resolution 1024x768 -debug
ドライバー(mame)自体を gdb ステップ実行する場合はSYMBOLS
オプションでソースマップを付与してビルド。なお、処理速度がかなり遅くなるのでリアルタイム処理の場合は注意。
# mame 全体にシンボルつけるためいったん clean
make clean
# OPTIMIZE=0 SYMBOLS=1 つけてコンパイル
make -j3 OPTIMIZE=0 SYMBOLS=1 SUBTARGET=kanon SOURCES=src/mame/drivers/vgmduino.cpp
make -j6 SUBTARGET=vgmplay SOURCES=src/mame/drivers/vgmplay.cpp OVERRIDE_CC=clang OVERRIDE_CXX=clang++
.vscode/c_cpp_properties.json
includePath
を ${workspaceFolder}/src/**
に設定。またビルド時にジェネレートされるソースコードも ${workspaceFolder}/build/generated/**
として追加。defines
でプラットフォームに合わせた SDLMAME_UNIX=1
などに設定。{
"configurations": [
{
"name": "Linux",
"intelliSenseMode": "clang-x64",
"cStandard": "c11",
"cppStandard": "c++17",
"includePath": [
"${workspaceFolder}/src/**",
"${workspaceFolder}/3rdparty/**",
"${workspaceFolder}/build/generated/**"
],
"defines": [
"SDLMAME_UNIX=1"
],
"compilerPath": "/usr/bin/clang"
}
],
"version": 4
}
.vscode/launch.json
program
と args
セクションに対象の実行ファイル名と引数を設定。
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "mame 起動 (GDB debug)",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/kanon64",
"args": [
"kanon",
"-window",
"-resolution",
"1280x768"
],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "gdb の再フォーマットを有効にする",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
},
{
"name": "mame 起動 (LLDB debug)",
"type": "lldb",
"request": "launch",
"program": "${workspaceFolder}/kanon64",
"args": [
"kanon",
"-window",
"-resolution",
"1280x768"
],
"cwd": "${workspaceFolder}",
"stopOnEntry": false
}
]
}
required_device
で使うデバイスを指定する。#include "includes/vgmduino.h"
として配置。ATMEGA328(
を ATMEGA644(
に修正すればコンパイル自体は通るはず。#include "emu.h"
#include "cpu/avr8/avr8.h"
#include "sound/ym2151.h"
#include "speaker.h"
class vgmduino_state : public driver_device
{
public:
vgmduino_state(const machine_config &mconfig, device_type type, const char *tag)
: driver_device(mconfig, type, tag)
, m_maincpu(*this, "maincpu")
, m_lspeaker(*this, "lspeaker")
, m_rspeaker(*this, "rspeaker")
, m_ym2151(*this, "ym2151")
{
}
void vgmduino(machine_config &config);
void init_vgmduino();
private:
required_device<avr8_device> m_maincpu;
required_device<speaker_device> m_lspeaker;
required_device<speaker_device> m_rspeaker;
required_device<ym2151_device> m_ym2151;
virtual void machine_start() override;
virtual void machine_reset() override;
void vgmduino_data_map(address_map &map);
void vgmduino_io_map(address_map &map);
void vgmduino_prg_map(address_map &map);
};
void vgmduino_state::machine_start()
{
}
uint8_t vgmduino_state::port_r(offs_t offset)
{
}
void vgmduino_state::port_w(offs_t offset, uint8_t data)
{
}
void vgmduino_state::vgmduino_prg_map(address_map &map)
{
/* ATmega328 32KB(0x0 - 0x7fff) internal flash */
map(0x0000, 0x7fff).rom();
}
void vgmduino_state::vgmduino_data_map(address_map &map)
{
/* ATmega328 2KB SRAM */
map(0x0100, 0x08ff).ram();
}
void vgmduino_state::vgmduino_io_map(address_map &map)
{
/* ATmega328 PORTA-PORTD (PORTA not exist) */
map(AVR8_IO_PORTA, AVR8_IO_PORTD).rw(FUNC(vgmduino_state::port_r), FUNC(vgmduino_state::port_w));
}
void vgmduino_state::init_vgmduino()
{
}
void vgmduino_state::machine_reset()
{
}
void vgmduino_state::vgmduino(machine_config &config)
{
/* ATmega328 16MHz clock */
ATMEGA328(config, m_maincpu, XTAL(16'000'000));
m_maincpu->set_addrmap(AS_PROGRAM, &vgmduino_state::vgmduino_prg_map);
m_maincpu->set_addrmap(AS_DATA, &vgmduino_state::vgmduino_data_map);
m_maincpu->set_addrmap(AS_IO, &vgmduino_state::vgmduino_io_map);
/* ATmega328 EEPROM */
m_maincpu->set_eeprom_tag("eeprom");
/* Arduino UNO setting */
m_maincpu->set_low_fuses(0xff);
m_maincpu->set_high_fuses(0xde);
m_maincpu->set_extended_fuses(0xfd);
m_maincpu->set_lock_bits(0x0f);
/* speaker */
SPEAKER(config, m_lspeaker).front_left();
SPEAKER(config, m_rspeaker).front_right();
/* YM2151 3.579545MHz clock */
YM2151(config, m_ym2151, XTAL(3'579'545));
m_ym2151->add_route(0, m_lspeaker, 1);
m_ym2151->add_route(1, m_rspeaker, 1);
}
ROM_START( kanon )
ROM_REGION( 0x8000, "maincpu", 0 )
/* Arduino UNO user program */
ROM_LOAD("vgmduino.bin", 0x0000, 0x7e00, CRC(9fe6d5d0) SHA1(4a74f844114cc5dd5722f98669542a9d53a91c2d) )
/* Arduino UNO bootloader 0x7e00 */
ROM_LOAD( "optiboot_atmega328.bin", 0x7e00, 0x200, CRC(388b1a0e) SHA1(529a4a966913261f0bc467ef80424bb74bd2cc03) )
/* on-die 1kbyte eeprom */
ROM_REGION( 0x400, "eeprom", ROMREGION_ERASEFF )
ROM_END
// YEAR NAME PARENT COMPAT MACHINE INPUT CLASS INIT COMPANY FULLNAME FLAGS
COMP(2020, kanon, 0, 0, vgmduino, 0, vgmduino_state, init_vgmduino, "vgmduino", "vgmduino", MACHINE_NOT_WORKING | MACHINE_NO_SOUND)
void vgmduino_state::vgmduino(machine_config &config)
{
...
m_maincpu->set_addrmap(AS_IO, &vgmduino_state::vgmduino_io_map);
...
}
void vgmduino_state::vgmduino_io_map(address_map &map)
{
/* ATmega328 PORTA-PORTD (PORTA not exist) */
map(AVR8_IO_PORTA, AVR8_IO_PORTD).rw(FUNC(vgmduino_state::port_r), FUNC(vgmduino_state::port_w));
}
save_item(NAME(m_bits));
のような形で実装する。save_item
に対応しているのでこれを使うのが適切そう(TODO:)class vgmduino_state : public driver_device
{
private:
/* ATmega328 PORTA-PORTD (PORTA not exist) */
uint8_t m_port_a;
uint8_t m_port_b;
uint8_t m_port_c;
uint8_t m_port_d;
...
}
vgmduino_state::machine_reset()
で初期化
void vgmduino_state::machine_reset()
{
m_port_a = 0;
m_port_b = 0;
m_port_c = 0;
m_port_d = 0;
}
I/O レジスターリードで呼ばれる READ8_MEMBER(vgmduino_state::port_r)
ではその時点の state を単純に返却。
uint8_t vgmduino_state::port_r(offs_t offset)
{
switch( offset )
{
case AVR8_IO_PORTA:
{
return m_port_a;
}
case AVR8_IO_PORTB:
{
return m_port_b;
}
case AVR8_IO_PORTC:
{
return m_port_c;
}
case AVR8_IO_PORTD:
{
return m_port_d;
}
default:
break;
}
return 0;
}
I/O レジスターライトで呼ばれる WRITE8_MEMBER(vgmduino_state::port_w)
で、YM2151 に接続されている IC や RD/WR 足の変化を検知して、YM2151 にコマンドを送信。
void vgmduino_state::port_w(offs_t offset, uint8_t data)
{
/* YM2151 ATMEGA328 PORT_REG
D0 2 AVR8_IO_PORTD 2
D1 3 AVR8_IO_PORTD 3
D2 4 AVR8_IO_PORTD 4
D3 5 AVR8_IO_PORTD 5
D4 6 AVR8_IO_PORTD 6
D5 7 AVR8_IO_PORTD 7
D6 8 AVR8_IO_PORTB 0
D7 9 AVR8_IO_PORTB 1
RD 10 AVR8_IO_PORTB 2
WR 11 AVR8_IO_PORTB 3
A0 12 AVR8_IO_PORTB 4
IC 13 AVR8_IO_PORTB 5
*/
switch( offset )
{
case AVR8_IO_PORTA:
{
if (data == m_port_a) break;
m_port_a = data;
break;
}
case AVR8_IO_PORTB:
{
if (data == m_port_b) break;
/* YM2151 IC 1->0 */
if (BIT(m_port_b, 5) && !BIT(data, 5))
{
m_port_b = data;
/* YM2151 RESET */
m_ym2151->reset();
printf("VGMDUINO: m_ym2151->reset()\n");
break;
}
/* YM2151 WR 1->0 */
if (BIT(m_port_b, 3) && !BIT(data, 3))
{
m_port_b = data;
/* YM2151 A0, D0-D7 */
uint8_t adr = BIT(m_port_b, 4);
// D6 8 AVR8_IO_PORTB 0
// D7 9 AVR8_IO_PORTB 1
uint8_t dat = (0b11111100 & m_port_d) >> 2 | BIT(m_port_b, 0) << 6 | BIT(m_port_b, 1) << 7;
m_ym2151->write(adr, dat);
// printf("VGMDUINO: m_ym2151->write(0x%02X, 0x%02X)\n", adr, dat);
break;
}
/* YM2151 RD 1->0 */
if (BIT(m_port_b, 2) && !BIT(data, 2))
{
m_port_b = data;
/* YM2151 A0 */
uint8_t adr = BIT(m_port_b, 4);
uint8_t state = m_ym2151->read(adr);
// printf("VGMDUINO: m_ym2151->read(0x%02X) = 0x%02X\n", adr, state);
/* YM2151 D0-D7 */
m_port_d |= (state & 0b0011111) << 2;
m_port_b |= (state & 0b1100000) >> 5;
break;
}
m_port_b = data;
break;
}
case AVR8_IO_PORTC:
{
if (data == m_port_c) break;
m_port_c = data;
break;
}
case AVR8_IO_PORTD:
{
if (data == m_port_d) break;
m_port_d = data;
break;
}
default:
break;
}
}
micros()
関数が正しい時間を返していない --optimize-for-debug=true
で引数サポートしてように見えるものの「not for release」になっている(arduino-cli 0.9)avr8.cpp
に ATMEGA328 の定義を追加しているので資料化、ソースコードを整理してどこかにコミット。→ mame にマージ済みavr8.cpp
でブートストラップにまつわる不具合がいくつかあるようなので Testers に報告? → mame にマージ済みavr8.cpp
修正箇所maicrs()
は TIMER0 を利用READ8_MEMBER( avr8_device::regs_r )
{
// printf("--- READ offset %04x ---\n", offset);
switch( offset )
{
// TODO: hack
case AVR8_REGIDX_MCUSR:
case AVR8_REGIDX_MCUCR:
case AVR8_REGIDX_TCCR0A:
case AVR8_REGIDX_TCCR0B:
case AVR8_REGIDX_ADCSRA:
case AVR8_REGIDX_ADCSRB:
case AVR8_REGIDX_TCCR1A:
case AVR8_REGIDX_TCCR1B:
case AVR8_REGIDX_TCCR1C:
case AVR8_REGIDX_TCNT0:
case AVR8_REGIDX_OCR0A:
case AVR8_REGIDX_OCR0B:
case AVR8_REGIDX_UCSR0B:
case AVR8_REGIDX_UCSR0C:
case AVR8_REGIDX_TCCR2A:
case AVR8_REGIDX_TCCR2B:
return m_r[offset];
case AVR8_REGIDX_TIFR0:
case AVR8_REGIDX_TIFR1:
case AVR8_REGIDX_TIFR2:
case AVR8_REGIDX_TIFR3:
case AVR8_REGIDX_TIFR4:
case AVR8_REGIDX_TIFR5:
return 0b00000111;
default:
// printf("[%08X] AVR8: Unknown Register Read: 0x%03X\n", m_shifted_pc, offset);
// machine().debug_break();
return 0;
}
}
TIMER0 hack
case WGM02_FAST_PWM:
// TODO: hack
if(count == m_r[AVR8_REGIDX_OCR0A]) {
m_r[AVR8_REGIDX_TIFR0] |= AVR8_TIFR0_TOV0_MASK;
update_interrupt(AVR8_INTIDX_TOV0);
}
break;
avr8.cpp
修正箇所(mame マージ済み)DEFINE_DEVICE_TYPE(ATMEGA328, atmega328_device, "atmega328", "Atmel ATmega328")
void atmega328_device::atmega328_internal_map(address_map &map)
{
map(0x0000, 0x00ff).rw(FUNC(atmega328_device::regs_r), FUNC(atmega328_device::regs_w));
}
//-------------------------------------------------
// atmega328_device - constructor
//-------------------------------------------------
atmega328_device::atmega328_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock)
: avr8_device(mconfig, tag, owner, clock, ATMEGA328, 0x7fff, address_map_constructor(FUNC(atmega328_device::atmega328_internal_map), this), CPU_TYPE_ATMEGA328)
{
}
//TODO: review this!
void atmega328_device::update_interrupt(int source)
{
const CInterruptCondition &condition = s_int_conditions[source];
int intstate = 0;
if (m_r[condition.m_intreg] & condition.m_intmask)
intstate = (m_r[condition.m_regindex] & condition.m_regmask) ? 1 : 0;
set_irq_line(condition.m_intindex << 1, intstate);
if (intstate)
{
m_r[condition.m_regindex] &= ~condition.m_regmask;
}
}
ブートローダーの大きさを修正
void avr8_device::device_reset()
{
// switch ((m_hfuses & (BOOTSZ1|BOOTSZ0)) >> 1){
// case 0: m_boot_size = 4096; break;
// case 1: m_boot_size = 2048; break;
// case 2: m_boot_size = 1024; break;
// case 3: m_boot_size = 512; break;
// default: break;
// }
// TODO:
switch ((m_hfuses & (BOOTSZ1|BOOTSZ0)) >> 1){
case 0: m_boot_size = 2048; break;
case 1: m_boot_size = 1024; break;
case 2: m_boot_size = 512; break;
case 3: m_boot_size = 256; break;
default: break;
}
HIGH FUSE でブートローダー指定起動時に m_pc の値が正しく初期化されてないのを修正。
void avr8_device::device_reset()
{
if (m_hfuses & BOOTRST){
m_shifted_pc = 0x0000;
logerror("Booting AVR core from address 0x0000\n");
} else {
m_shifted_pc = (m_addr_mask + 1) - 2*m_boot_size;
// ADD:
m_pc = m_shifted_pc >> 1;
logerror("AVR Boot loader section size: %d words\n", m_boot_size);
}
DTR
信号受付から ATmega328 をリセットできるように結線されている。ATmega16U PD7
-> ATmega328 RESET
データーシートより