NES Emulator in Rust : 4 Pad Input

e-tipsmemo.hatenablog.com

続き

スプライト実装をする前に、Nestestを通すために、PAD入力をサクッと実装する。
NES on FPGA PAD

キー入力

Piston windowでキー入力をとるのはEvent::Inputが来たとき。
本当はInputでパターンマッチした引数でうまくやれるかもしれんが、逆に長くなったので、やめた。

while let Some(event) = window.next() {
        match event {
            Event::Loop(Loop::Render(_)) => {
//略
            }
            Event::Input(_, _) => {
                if let Some(Button::Keyboard(key)) = event.press_args() {
                    match key {
                        Key::J => nes.pad.press(pad::PadButton::A),
                        Key::G => {
                            let tmp = !nes.ppu.grid_on.get();
                            nes.ppu.grid_on.set(tmp);
                        }
                        _ => {}
                    }
                }
                if let Some(Button::Keyboard(key)) = event.release_args() {
                    match key {
                        Key::J => nes.pad.release(pad::PadButton::A),
                        _ => {}
                    }
                }
            }
            _ => {}
        }

press/releaseで一旦NES内のPADステータス値を更新する。

PAD読み込み

NESプログラムがPADの状態を得るためには1Pなら0x4016、2Pなら0x4017に一定の操作を行う。

NES on FPGA PAD


0x01書き込み→0x00書き込みで読み込み可能状態になるのでPADか、NESの内部に状態(strobe)を持っておく。

pub fn write8(&self, addr: u16, data: u8) {
	match addr {
...
		0x4016 => {
			let strobe = (data & 0x01) != 0; //data == 1 -> true

			if strobe {
				self.pad.strobe_enable.set(true);
			} else {
				self.pad.shift_index.set(0);
				self.pad.strobe_enable.set(false);
			}
		}
....
	}
}


その後、0x4016(0x4017)をリードするたびに下から順にビットを判定し、1か0を返す。

bitflags! {
    #[derive(Default)]
    pub struct PadButton:u8{
        const A = 1 << 0;
        const B = 1 << 1;
        const START = 1 << 2;
        const SELECT = 1 << 3;
        const UP = 1 << 4;
        const DOWN = 1 << 5;
        const LEFT = 1 << 6;
        const RIGHT = 1 << 7;
    }
}
pub fn read8(&self, addr: u16) -> u8 {
	match addr {
....
		0x4016 => {
			let s = self.pad.shift_index.get();
			if s > 7 {
				return 1;
			}

			self.pad.shift_index.set(s + 1);

			let btn = if !self.pad.strobe_enable.get() {
				let scanbtn = pad::PadButton::from_bits_truncate(0x01 << s);
				self.pad.status.get().contains(scanbtn)
			} else {
				true
			};

			btn as u8
		}
		0x4017 => 0,
....
	}
}

2Pコントローラー(0x4017)を実装しないときは、しっかり0を返すべきで、ここで0以外の値を返していると、ずっと2P入力されていることになってスーパーマリオブラザーズでタイトル画面を操作できない。

nestest

前回の背景描写と合わせて、PAD操作ができれば、nestestを実行することができる。

f:id:katakanan:20210907224448p:plain

感想

操作ができるとちょっとうれしい。
まだスプライトを実装していないので、ギコ猫は動かせない。
事前調査で、piston_windowがController(Game pad?)も使えそうだったので、これで実装しているが、
Switch Pro ConをBluetooth接続したところ、なんの入力もされない!?
nestopiaでは使えるのに....
仕方ないので寄り道して、RustでSwitch Pro Controllerの入力をとる実装を入れるかもしれない。
(Switch持ってないのにPro conだけ買った。)

NES Emulator in Rust : 3 PPU Background

e-tipsmemo.hatenablog.com
続き

PPUを始めるにあたってnestestの表示または、HelloWorldの表示が最初の目標となる。
ファミコンエミュレータの創り方 - Hello, World!編 - - Qiita
スプライト表示なし。(スクロールなしの)バックグラウンドを描写さえすれば動く。

PPU

まずCPUと同じようにPPUのレジスタを定義しておく。
内部にVRAMを持っていたりするのでよしなに定義しておく。

backgroundに最低限必要なのは、chrom, vram, palette, addr, ctrl, latch, imgあたり?とそれらを読み書きするメモリアクセスの関数。

pub struct Ppu {
    pub chrrom: [u8; 0x2000], //readonly
    pub vram: Cell<[u8; 0x1000]>,
    pub oam: Cell<[u8; 0x0100]>,
    pub palette: Cell<[u8; 0x0020]>,
    pub addr: Cell<u16>,
    pub ctrl: Cell<PpuCtrl>,
    pub mask: Cell<PpuMask>,
    pub status: Cell<PpuStatus>,
    pub oamaddr: Cell<u8>,
    pub scroll: Cell<u16>,
    pub latch: Cell<bool>,
    pub img: RefCell<image::ImageBuffer<image::Rgba<u8>, Vec<u8>>>,

    pub dly_data: Cell<u8>,
    secondary_oam: Cell<[u8; 32]>, //4byte * 8 sprites
    sprite_zero_flag: Cell<[bool; 8]>,

    //for debue
    pub grid_on: Cell<bool>,
}

chromeは起動時にromからキャラクターROMをコピーしてくる。
imgはpistonの描写ループでborrowしたいので、RefCellになっている。
(Cellだとgetしかできなかった気がする。Copy Traitが必要となるがImageBufferには実装されていない)
VBlankが立つときにのみ、PPUの描写関数が持つ画面バッファをcloneして代入する。

タイミング

参考サイトを元に、
以前書いた
NES Emulator in Rust : 1 Operation Cycle - e-tipsmemo
の、CPU 1サイクル後に、実行されるPPU 3サイクルで呼ばれるGeneratorの関数内部で描写を行う。
(参考サイトはここら辺から何も書いていない上に、githubのPPUは不完全な実装となっている)
ここの内部で、スキャンラインとx方向をカウントして、描写エリア内で、VRAMを読んでPPUの1サイクルごとにピクセルを置いていく。

pub fn run<'a>(nes: &'a Nes) -> impl Generator<Yield = PpuStep, Return = !> + 'a {
	let mut sprite_evaluation = Ppu::sprite_evaluation(nes);
	let mut run_renderer = Ppu::renderer(nes);

	move || loop {
		loop {
			match Pin::new(&mut sprite_evaluation).resume(()) {
				GeneratorState::Yielded(PpuStep::Cycle) => {
					break;
				}
				GeneratorState::Yielded(step) => {
					yield step;
				}
			}
		}
		loop {
			match Pin::new(&mut run_renderer).resume(()) {
				GeneratorState::Yielded(PpuStep::Cycle) => {
					break;
				}
				GeneratorState::Yielded(step) => {
					yield step;
				}
			}
		}
		yield PpuStep::Cycle;
	}
}

今回はbackgroundだけなので、sprite_evalutateはいらない。
rendererの中で、scanlineをカウントアップし、そのscanline loopの中で、341個のyieldを入れることで、x方向のサイクルを消費する。
https://wiki.nesdev.com/w/images/d/d1/Ntsc_timing.png
をもとに、scanline = 240, 261, x = 0の時の処理として、
最低限背景描写に必要ないくつかのフラグの処理をしておく。(VBlank)
(厳密にはx=1サイクル目だけどnestestやhelloworldには影響がない)

    fn renderer<'a>(nes: &'a Nes) -> impl Generator<Yield = PpuStep, Return = !> + 'a {
        move || loop {
            let mut buf =
                image::ImageBuffer::new(cparams::VFRAME_W as u32, cparams::VFRAME_H as u32);
            for frame in 0.. {
                for scanline in 0..cparams::FRAME_H {

                    //略

                    if scanline == 240 {
                        nes.ppu.status.update(|mut status| {
                            status.set(PpuStatus::VBLANK_STARTED, true);
                            status
                        });

                        let ctrl = nes.ppu.ctrl.get();
                        if ctrl.contains(PpuCtrl::VBLANK_INTERRUPT) {
                            nes.cpu.nmi.set(true);
                        }

                        *nes.ppu.img.borrow_mut() = buf.clone();
                        yield PpuStep::Vblank;
                    } else if y == 261 {
                        //Pre-render line
                        nes.ppu.status.update(|mut status| {
                            status.set(PpuStatus::VBLANK_STARTED, false);
                            status.set(PpuStatus::ZERO_HIT, false);
                            status
                        });
                        buf = image::ImageBuffer::new(
                            cparams::VFRAME_W as u32,
                            cparams::VFRAME_H as u32,
                        );
                    }

                    //略

この後に 341回分のyieldがある。

描写

x が 8の倍数の時(0, 8, 16, ...)、そこから8ピクセル分の色を一気に計算して、
そのあと、yield を8回loopさせて、1ピクセルごとに置くことにした。

for dx in 0..8 {
	let x = tile_x * 8 + dx;
	let y = scanline;

	//Name tableとAttr Tableからbgのカラーインデックを計算
	//略

	bg_color_index_array[dx as usize] = color_index;
}

for dx in 0usize..8 {
	let x = (tile_x * 8) as u32 + dx as u32;
	let bg_color_index = bg_color_index_array[dx];

	
	let color = cparams::COLORS[bg_color_index];

	let color = if nes.ppu.grid_on.get() {
		if (x + 1) % 8 == 0 || (y + 1) % 8 == 0 {
			[0, 0, 255]
		} else {
			color
		}
	} else {
		color
	};

	let r = color[0];
	let g = color[1];
	let b = color[2];
	let a = 255;
	let pixel = image::Rgba([r, g, b, a]);
	buf.put_pixel(x, y as u32, pixel);
	yield PpuStep::Cycle;
}

上と下のループを一緒にしてもよかったかもしれない。(なぜこうしたのかは忘れた。)
やる気が出ればそうする。

Name TableとAttribute Tables

Name Tableからの情報が、Sprite indexを示して、
Attribute Tableからの情報が、Palette indexを示している。
読みだすNTアドレスとATTRアドレスはスクリーン上の(x, y)座標から計算する。(とりあえずスクロール値無視)

f:id:katakanan:20210829163310p:plainf:id:katakanan:20210829161723p:plain

NTは8pixel単位、ATTRは32pixel単位で管理されているので
アドレスは割り算をしなくてもxとyのいくつかのビットを判定して
ベースアドレスにオフセットを足すだけで計算できる。(はず)

NT byte

0x48なので「H」のスプライト。
f:id:katakanan:20210829163341p:plain
Charactor romを参照して、座標に対応するNT byteを得る。
f:id:katakanan:20210829180401p:plain

AT byte

f:id:katakanan:20210829180606p:plain

PPUが内部で持つ色テーブルは以下で出てきたのをそのまま使っている。
ファミコンエミュレータの創り方 - Hello, World!編 - - Qiita

描写

f:id:katakanan:20210829180705p:plain
NT byteからの値と、AT byteからの値をセレクタとして、パレットから色番号を得る。
さらに色番号から固定の色テーブルを参照して、やっと実際のRGB値を得て色を描写できる。

「x」はいつも0x00で背景色を出力することになる。
背景色は0x3F00の色番号。

HelloWorldとNestest

そしてようやく

f:id:katakanan:20210722220850p:plain:w300f:id:katakanan:20210722201831p:plain
まだ入力を実装していない(ことになっている)ので、nestestを実行できない。(実は実装してある)

スクロールはアドレスを求めるxとyにscroll_xとscroo_yを足して、NTとATのアドレスを計算するときに

  • PPUCTRLレジスタの下位2ビット
  • 画面をまたぐ
  • 垂直・水平ミラー

あたりを考慮することになる。がまた別で記事を書く気がする。

感想

ここら辺からRustのCargo.tomlで、opt-level = 2 を指定しないと60fpsが維持できなくなってきた。

実際のPPUは、この画像だと2タイル先読み(16pixel分)して、(例:タイル0とタイル1)
16pixelの情報を1 PPU Cycleずつシフトしながら出力していき、8pixel(タイル0)描写中に、タイル2のNTとATをfetch/decodeしていき、
タイル0の描写分が終わったタイミングではタイル2の8pixelが準備完了する。というようにパイプライン化されている。
(このエミュレータ今のところはそうなってはいない。)
ATやNTを読むタイミングが16 PPU Cycleだけ遅いので、もし描写中にVRAMを書き換えるようなゲームがあると正しく動かない可能性がある。
(やる気がでたらNTSC_Timingを実現したい。)

NES Emulator in Rust : 2 CPU

e-tipsmemo.hatenablog.com
続き

CPU

#[derive(Default, Clone)]
pub struct Cpu {
    pub pc: Cell<u16>,
    pub a: Cell<u8>,
    pub x: Cell<u8>,
    pub y: Cell<u8>,
    pub s: Cell<u8>,
    pub p: Cell<Status>,
    pub nmi: Cell<bool>,
}

レジスタを用意する。Statusはbitflags crateでflag管理する。
メインメモリはNESが持っているので、メモリアクセスはNESメンバ関数にしてある。
ここら辺は参考サイトとほぼ同様

Fetch / Decode / Exec

//Not consume Cpu Cycle
let pc = nes.cpu.pc.get();
let bin = nes.read8(pc);
let opcode = num_traits::FromPrimitive::from_u8(bin)
	.unwrap_or_else(|| panic!("Unknown Opcode 0x{:02X} @0x{:04X}", bin, pc));

let oparg = match opcode {
	////////////////////
	/// LDA Operations
	////////////////////
	Opcode::LdaXind => {
		yield_all! { Xind_load(nes, RegA) }
	}
	Opcode::LdaZpg => {
		yield_all! { Zpg_load(nes, RegA) }
	}
....
	////////////////////
	/// LDY Operations
	////////////////////
	Opcode::LdyImm => {
		yield_all! { Imm_load(nes, RegY) }
	}
	Opcode::LdyZpg => {
		yield_all! { Zpg_load(nes, RegY) }
	}
....

公式命令の動作の説明は、いろいろなサイトで解説されており、基本的な動作をするものばかりであった。
アドレッシングモードのタイミングさえ合わせれば難しいことはない。

いくつかのアドレッシングモードでのアドレス計算ではオーバーフローを無視したりする計算があり、注意。

フラグを更新するときに使いまくっているstd::cell::Cellのupdateメソッドは
feature cell_updateが必要。

    #[inline]
    #[unstable(feature = "cell_update", issue = "50186")]
    pub fn update<F>(&self, f: F) -> T
    where
        F: FnOnce(T) -> T,
    {
        let old = self.get();
        let new = f(old);
        self.set(new);
        new
    }

f:id:katakanan:20210616211408p:plain:w400
実装抜けがないようにチェック(nestestドリブンで実装したほうがよかった気がする)

nestest.nes

命令実装で役に立つのが、nestest.nes

    if cfg!(feature = "nestestlog") {
        nes = nes::Nes::start("../nes-roms/nestest.nes");
        nes.cpu.pc.set(0xC000);
        nes.cpu.p.set(cpu::Status::from_bits_truncate(0x24));
    } else {
        nes = nes::Nes::start("../nes-roms/sample1.nes");
    }

0xC000にジャンプし、フラグを0x24、スタックアドレスは0xFD(デフォルト)とするとPPUなしでCPUの動作テストができる。
その動作結果を既存のlogと比較することで、ミスを発見し、直す。
https://www.qmtpro.com/%7Enes/misc/nestest.log

このlogですこし面倒だったのは、
左側にはあるアドレスで実行されるに命令とオペランドの組み合わせがかかれているが、
右側にあるのはその命令を実行する前のCPUの状態とPPU、CPUサイクルということだった。

C8D1  B8        CLV                             A:00 X:00 Y:00 P:6F SP:FB PPU:  4, 19 CYC:461
C8D2  A9 F8     LDA #$F8                        A:00 X:00 Y:00 P:2F SP:FB PPU:  4, 25 CYC:463
C8D4  29 EF     AND #$EF                        A:F8 X:00 Y:00 P:AD SP:FB PPU:  4, 31 CYC:465

2行目のLDX #$FBが実行された結果、3行目の右側の状態となる。

このエミュレータがGeneratorを使用しているので、
左側の命令の情報がすべて得られている時にはすでに、fetch/decode/execが実行された後となり、(とくにCPUサイクルは進んでいる)
エミュレータ内部で持っているCPUの状態はすでに変わっているので
前の状態をわざわざ保存しておかなければならなかった。

loop {
	match Pin::new(&mut nes_cpu).resume(()) {
		GeneratorState::Yielded(cpu_step @ cpu::CpuStep::Cycle) => {
			cpu_cycle = cpu_cycle + 1;
			yield NesStep::Cpu(cpu_step);
			break;
		}
		GeneratorState::Yielded(operation) => {
			if cfg!(feature = "nestestlog") {
				println!(
					"{:<48}{} PPU:{:>3},{:>3} CYC:{}",
					cpu::debug_op(&operation),
					cpu::debug_reg(&regs_before_exec),
					ppu_line_before_exec,
					ppu_cycle_before_exec,
					cpu_cycle_before_exec
				);
				cpu_cycle_before_exec = cpu_cycle;
				ppu_cycle_before_exec = ppu_cycle;
				ppu_line_before_exec = ppu_line;
				regs_before_exec = self.cpu.clone();
			}

			yield NesStep::Cpu(operation);
		}
	}
}

汚い。

nestestを通しつつある。(WinMergeUを使用)
f:id:katakanan:20210716213858p:plain
f:id:katakanan:20210719075353p:plain
0xC6BCまでが公開されている命令セットのテストとなる。ここまで通っていれば大体のゲームは動くらしい。

Undocumented Instruction

せっかくなのでnestest.logを通すためだけのUndocumented Instructionも実装した。
いくつかのUndocumented ISAは、既存のOfficial ISAが組み合わさったような動作を行う。
https://www.nesdev.com/undocumented_opcodes.txt
例えば、

ISC (ISB) [INS]
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
Increase memory by one, then subtract memory from accu-mulator (with
borrow). Status flags: N,V,Z,C

なので INCとSUBが組み合わさっている。

pub struct ISB;
impl MemCalcOperation for ISB {
    #[inline(always)]
    fn calc(&self, nes: &Nes, value: u8) -> u8 {
        let tmp = (value as u16 + 1) as u8; //Increase memory by one
        let c = nes.cpu.p.get().contains(Status::C) as u16;
        let a = nes.cpu.a.get();
        let not_value = !tmp;
        let res = (a as u16) + (not_value as u16) + c; //then subtract memory from accu-mulator
        //A - b- !c = A + (not b) + c
        let mut p = nes.cpu.p.get();
        p.set(Status::Z, res & 0xFF == 0);
        p.set(Status::C, res & 0x100 != 0);
        p.set(Status::N, res & 0x80 != 0);
        p.set(
            Status::V,
            (a ^ res as u8) & (not_value ^ res as u8) & 0x80 != 0,
        );

        nes.cpu.p.set(p);
        nes.cpu.a.set(res as u8);
        tmp
    }
}

アドレス0xC68B以降は入力やAPUなのでとりあえず無視。0xFFを返すようにしておけば問題ない。

この記事を書きながら、nestestにないUndocumented ISAもあることに気づいたが。
それらはやる気が出たら実装する。
HLTとかASR (ASR) [ALR]....

こうして命令実装はかなり順調に進む。
SUBのオーバーフローなどはわかりにくいが検索するとすぐ出てくる。
Determining carry and overflow flag in 6502 emulation in Java? - Stack Overflow


大半のアドレッシングモードを計算してから、オーバーフローを無視するアドレス計算では、wrapping_addといった関数を使えば、かなり楽できたということを知った。
(知らなかったのでがんばって符号拡張したりしている。しかし符号拡張が間違っているところでは実行中にoverflow panicを起こしたりするかもしれない。)
f:id:katakanan:20210821180758p:plain
こういう基礎的なところを知るためにも、NESエミュレータ作成はプログラミング言語入門には最適だと改めて感じた。

またnestestを通せば最低限CPUは間違っていないという安心感があるので、心おきなくPPU実装に移ることができる。
実際、background機能だけのPPUを実装後、nestsetを実行すると一発で通った。

f:id:katakanan:20210804080502p:plain
PPUはバグっているが、nestestは通っていることがわかる。

NES Emulator in Rust : 1 Operation Cycle

e-tipsmemo.hatenablog.com

まずはどうCPU & PPU Cycleを合わせていくかというところを見る。
最初はほぼ参考サイトと同じように実装していく。

Operation Cycle

Generatorを使うことで、NESの1画面更新が、以下の1ループと同じになる。
yieldでVBlankで停止した時だけ、ループを抜け画面描写などをする

let mut nes_run = nes.run();
while true {
	loop {
		match Pin::new(&mut nes_run).resume(()) {
			GeneratorState::Yielded(NesStep::Ppu(ppu::PpuStep::Vblank)) => {
				// VBlank;
				break;
			}
			GeneratorState::Yielded(_) => {}
		}
	}

  //画面描写など
}


このNES Runを呼び出し側から 4回 resumeすることによって上から下まで実行することはCPUの1Cycleと同じ、かつPPUの3Cycleとなる。
必ずCPUとPPUのサイクルは1:3となり面倒なサイクル計算が必要なくなる。

pub fn run<'a>(&'a self) -> impl Generator<Yield = NesStep, Return = !> + 'a {
	let mut nes_cpu = Cpu::run(self);
	let mut nes_ppu = Ppu::run(self);

	move || loop {
		loop {
			match Pin::new(&mut nes_cpu).resume(()) {
				GeneratorState::Yielded(cpu_step @ cpu::CpuStep::Cycle) => {
					yield NesStep::Cpu(cpu_step);
					break;
				}
				GeneratorState::Yielded(operation) => {
					yield NesStep::Cpu(operation); //for nestest log
				}
			}
		}

		for _ in 0..3 {
			loop {
				match Pin::new(&mut nes_ppu).resume(()) {
					GeneratorState::Yielded(ppu_step @ ppu::PpuStep::Cycle) => {
						yield NesStep::Ppu(ppu_step);
						break;
					}
					GeneratorState::Yielded(ppu_step) => {
						//VBlank
						yield NesStep::Ppu(ppu_step);
					}
				}
			}
		}
	}
}

また、こういう実装方法をしていくと、borrow checkでいろいろとエラーでるのでCPUやPPUの構造体はCellで値を持つ。(マルチスレッドにはしないと思うので)

Addressing Mode & Cycle

どこのサイトにもあるが、対象とするレジスタとメモリのアドレッシングモードの組み合わせがあり、
アドレッシングモードによってCPU Cycle数が違う。
それらレジスタの種類 * アドレッシングモードの数 の組み合わせ全部を個別に実装するは大変となってくる。
これをTraitによって、実装を楽することができる。(参考サイトもそうなっている)

メモリ op レジスタ --> レジスタな計算に必要なアドレッシングモードと
BinaryOperation Traitを実装した計算(ORA、EOR、ADC、SUB、etc.)を引数にとる。

pub fn Imm_calc<'a>(
    nes: &'a Nes,
    op: impl BinaryOperation + 'a,
) -> impl Generator<Yield = CpuStep, Return = OpArg> + 'a {
    move || {
        Cpu::fetch_byte_inc_pc(&nes);
        yield CpuStep::Cycle;

        let value = Cpu::fetch_byte_inc_pc(&nes);
        op.calc_and_set(nes, value);
        yield CpuStep::Cycle;

        OpArg {
            addressing: Addressing::Implied,
            args: vec![value],
            value: AddressingResult::RWValue(value),
        }
    }
}

pub fn Xind_calc<'a>(
    nes: &'a Nes,
    op: impl BinaryOperation + 'a,
) -> impl Generator<Yield = CpuStep, Return = OpArg> + 'a {
   ////略
}
pub trait BinaryOperation {
    fn calc_and_set(&self, nes: &Nes, value: u8);
}

pub struct ORA;
impl BinaryOperation for ORA {
    #[inline(always)]
    fn calc_and_set(&self, nes: &Nes, value: u8) {
        let res = nes.cpu.a.get() | value;

        let mut p = nes.cpu.p.get();
        p.set(Status::Z, res == 0);
        p.set(Status::N, res & 0x80 != 0);

        nes.cpu.p.set(p);
        nes.cpu.a.set(res);
    }
}
////////////////////
/// ORA Operations
////////////////////
Opcode::OraXind => {
	yield_all! { Xind_calc(nes, ORA) }
}
Opcode::OraZpg => {
	yield_all! { Zpg_calc(nes, ORA) }
}
Opcode::OraImm => {
	yield_all! { Imm_calc(nes, ORA) }
}
.....

アドレッシングモードが同じでも、例えばSTAのIndirect.YとADCのIndirect.Yでサイクル数が違う気がしたので、
レジスタに対する操作別で分けたが不要だったかもしれない。
f:id:katakanan:20210815183519p:plain
opcodes and addressing modes – the 6502 – [ emudev ]

NES on FPGA CPUではIndirect,Yは全て 5 + 1サイクルで、同じだった。
(NESと6502で違うのか?)
また、もっとTraitを使いこなせばアドレッシングモードの実装をまとめることができる気がする。

その他

スプライトのbg透過を実装した。
ピーチは欠けたまま。
f:id:katakanan:20210815094940p:plain:w400

NES Emulator in Rust : 0

以前ファミコンのエミュを書きたいといってROMを読んだ記事を書いた。
e-tipsmemo.hatenablog.com

コミットログを見ると始めてから2か月というところで、
giko00x.nesが動かないが、Donky kongがバグりつつ動いてくれたりと躓いているので。
かわりに雑記を書いていく。
ピーチが欠けている。スプライトが透過していない。など。
f:id:katakanan:20210809220204p:plain:w400

方針

ファミコンエミュレータを製作するとはいっても、すでに多くの人たちが、多くの言語で実装を行っており、
NES emulator [言語]で調べると、日本語・英語で豊富な情報が得られる。
それらの実装ではほとんどが、クロックサイクルを内部でカウントし、実機のものと同じサイクルになったら処理を実行するといったものが多い。

しかし今回は、あるサイトを参考に別の方針でいくことにした。
I made a NES emulator in Rust using generators | ~kylewlacy
ファミコンのRust実装もいくつか見つけたが、
その中でも、このサイトではGeneratorというnightly機能を使用することによって、
タイミングを合わせることが非常に簡単である。(と書いてある)
generators - The Rust Unstable Book

そして、どのNESエミュレータRom読み込み、CPU実装から始まり、
nestestのCPUテストをクリアしてppuへ。。といった手順を踏んでいる。

Home AssistantとESPHome導入とESP8266でLED点灯

Getting Started with ESPHome and Home Assistant — ESPHome
ここをやったときのメモ

Home AssitantのSupervisorになるものを用意

候補
Installation - Home Assistant
Dockerが動けば大体いけるので、最近のSynologyやQNAPのハイスペックNASでも行ける気がするが
無難にRaspberry piでいく。
公式ではRaspberry pi 3以上を推奨している。
Raspberry Pi - Home Assistant
手元にあったのはRaspberry pi 3B+でRAMが1GBであった。

Raspebian + Home Assistant Container(Docker)でいこうとするとRaspberry pi 4の8GBのほうがよいと思う。

面倒を避けるため最初から全部入りのHome Assistant Operating Systemでいく。
f:id:katakanan:20210808120329p:plain

OSのインストール

大体ここに沿って行けばよい
balenaEtcher - Flash OS images to SD cards & USB drives
Raspberry piのバージョンに合わせてOSを選択
f:id:katakanan:20210808120507p:plain

8分ぐらいかかる。

起動

LANケーブルをいれてから
電源をいれる。

初回起動はかなり遅いので、10分ぐらいまつ。
そうすると
http://homeassistant.local:8123
でHome Assistantの管理画面が初回起動画面が見れた。
f:id:katakanan:20210807075601p:plain
ここで20分程度待つ。

納豆が糸引いてるみたい。
f:id:katakanan:20210807095032p:plain:w150

その後画面が勝手に切り替わってアカウント作成しろとなる。
Onboarding Home Assistant - Home Assistant

Add-onでESPHomeを入れる

f:id:katakanan:20210808121227p:plain
SupervisorをクリックしてアドオンストアからESPHomeをクリックし、インストールする。
f:id:katakanan:20210808121349p:plain
インストールにも20分ぐらいかかった。

アドオンがインストール完了するので開始を押す
f:id:katakanan:20210808121500p:plain
開始するのに60秒ぐらいはかかる。
f:id:katakanan:20210808121554p:plain
WebUIでようやくターゲットデバイスのコンフィグを行うyamlを書く画面となる。

バイスの作成(ノードの追加)

LAN上にある管理されるデバイスのことをノードと呼ぶ?
右下の緑の+から。
f:id:katakanan:20210808121708p:plain
ノードの名前とそのノードが接続するWifiSSIDとPasswordをいれる。(あとから編集できる)

f:id:katakanan:20210808121922p:plain
今回はESP8266( https://akizukidenshi.com/catalog/g/gK-12236/ )
f:id:katakanan:20210808122004p:plain
EDITからデバイスの構成を書ける。

デフォルトで以下のようになっている。

esphome:
  name: test
  platform: ESP8266
  board: esp01_1m

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:
  password: "********************"

wifi:
  ssid: "**************"
  password: "****************"

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Test Fallback Hotspot"
    password: "***************"

captive_portal:

適当なところに以下をついか

switch:
  - platform: gpio
    name: "LED"
    pin: 14

f:id:katakanan:20210808122745p:plain:w400

SAVEする。

Firmwareを書く

f:id:katakanan:20210808123102p:plain:w400
INSTALLボタンからManual Downloadを選ぶ。
Wirelesslyは、OFFLINEだと使えない。
yamlで指定した構成をもとに、ソースコードが生成されて、ビルドが始まる。
f:id:katakanan:20210808123404p:plain:w400
終わるとbinがダウンロードされ始める。
f:id:katakanan:20210808123712p:plain:w400

ESP flasherなどでbinを書き込む。
f:id:katakanan:20210808123854p:plain:w400

ダッシュボードに置く。

書き込んだESPを起動して、Wifiが接続されて、Home Assistantがそれを発見すると、ONLINEになる。
f:id:katakanan:20210808124110p:plain:w400
f:id:katakanan:20210808124152p:plain:w400
設定のインテグレーションの中に先ほど作ったノードが”発見”されているので「設定」する。

Statusにデバイスが設定される。
f:id:katakanan:20210808124357p:plain:w400
Toggle Buttonをスライドさせると、Pin14につながったLEDが点灯した。

感想

インストールで待つのがいやだったら、Raspberry pi 4を使ったほうがいい。

しかし、そうするとかなり熱くなる。
3B+ではヒートシンクでもなんとかなるかもしれないが、
4ならアルミケースなど全体で冷やさないともたなさそう。

さらに長時間使うとSDカードがダメになるという報告も見たので、(書き込み上限などで)
そういう場合はSSDなどからブートできたらと思う。(USB3.0じゃないと起動が遅いと思われる)
これらを満たす以下のケースを見つけた。

しかし値段的には
Raspberry pi 4 8GB + 上のアルミケース(M.2可能)とCeleronIntel NUCが対応するかもしれない。(どちらもストレージは別)

以前つかったTasmotaによってコンフィグされたデバイスもおそらくこのHomeassistantで管理できるのだと思う。
しかしTasmotaはデバイス側の自由度が低く、自分で作ったESPデバイスにあった構成をコンフィグできるとは限らないと思った。
その点、こちらのESPHomeはプログラミングまでではないが、yamlによって構成を自由に記述できるので、自分でESPが乗ったデバイスを作ったりする場合はこちらのほうが良いと思った。


バイスからのイベントをつなげてある自動化をおこなうTutorialもあったが、これはまた別記事に書こうと思う。
Automating Home Assistant - Home Assistant

久しぶりにPCBGOGOに発注

2.5年ぶりにPCBGOGOに基板製造を注文した。
PCBGOGOがどれくらい変化してるのか気づいたところを書く。

製造能力

デザインルールを見ていたが、かなり製造能力が上がっている。
標準FR-4基板- PCBGOGO.JP
例えば、
BGAパッド径は0.2mm以上、(以前は0.3mm以上)
BGAピッチは0.12mm以上になっている。
f:id:katakanan:20210626115708p:plain

また、配線幅とクリアランスが3mil以上となっていた。(以前は確か4mil以上)
f:id:katakanan:20210626120035p:plain

以前の記録。
KiCAD BGA基板設計①回路編 - e-tipsmemo

更に10層までの多層基板が作れるので、
0.8mmの大きめのBGAなども普通に実装できるはず。

やろうと思えばDDR3メモリなども実装できると思われる。(やろうと思わないが)
f:id:katakanan:20210626120546p:plain

Garbar Viewer

ブラウザに直接ガーバーデータをアップロードして見れる。
KiCADのよりサクサク動く気がする。こっちのほうが使いやすいまである。
f:id:katakanan:20210729072551p:plain

この時のビューをそのままSVGファイルとして、ダウンロードできる。
PNGにして、Fusiuon360のSTEPモデルに貼ったりするのも面白いかもしれない。

データ確認サービス

アップロードして支払う前に、データ確認サービスは健在
注文時の見積もり選択とデータチェックの結果が食い違うと、人の確認が入って製造開始が遅くなる。

注文確認画面

f:id:katakanan:20210729071009p:plain
注文確認画面で、基板の画像をクリックするとガーバービュワーが起動するようになっていた

製造進捗

f:id:katakanan:20210630195748p:plain
なんかおしゃれになっていた

基板を間違えたので90mm X 90mmの基板を2回注文した。
1回目は両面基板3mil/3milと端面スルーホールをつけてしまったので6000円ぐらいだったが、
2回目は両面基板6mil/6milに変更したら、550円だった。
さらにそこに送料2000円がかかる。