들어가며
https://en.wikipedia.org/wiki/Nintendo_Entertainment_System
- NES(Nintendo Entertainment System)/ 현대 컴보이 / Famicom / 패밀리컴퓨터
- 1983년 7월 15일 출시
- 8비트 시대/ 십자키(D-Pad) 도입
구성
- RP2A03
- CPU - Central Processing Unit - W-RAM(2kb) work ram
- APU - Audio Processing Unit
- RP2C02
- PPU - Picture Processing Unit - V-RAM(2kb) video ram
- 카트리지
- 컨트롤러
구현할 만 한가?
-
화면이 보인다
- NES에는 재미있는 게임들 있다.
-
적당한 수(최대 256)의 opcode
-
2D라 디버깅 비교적 쉬움.
-
레퍼런스가 많다
-
파고들 구석이 있다
- 비공식 opcode들
- 여러가지 Mapper 구현.
- Mapper별로 가능한 게임이 다르다
- Cpu cycle과 ppu 타이밍 맞춘다던가
- 단순 화면만 아닌 디버그 뷰
-
Programming the NES - Everything you need to know
- https://github.com/InkboxSoftware/NESGuide
- NES Homebrew Game - Alter Ego
- https://shiru.untergrund.net/
- http://shiru.untergrund.net/files/nes/alter_ego.zip
- https://shiru.untergrund.net/files/src/alter_ego_src.zip
- NES Homebrew Game - Alter Ego https://www.youtube.com/watch?v=XN-Z8S_MIH4
- https://forums.nesdev.org/viewtopic.php?t=7999
https://www.nesdev.org/obelisk-6502-guide/reference.html
https://skilldrick.github.io/easy6502/ https://6502.org/tools/emu/ https://6502.org/tutorials/
cpp - https://github.com/gabriel88766/NESEmulator/ lisp - https://github.com/samanthadoran/potential-disco
test
- https://github.com/SingleStepTests/ProcessorTests/nes6502
- https://github.com/SingleStepTests/65x02
- https://www.nesdev.org/wiki/Emulator_tests
- https://github.com/christopherpow/nes-test-roms
NES Emulator From Scratch https://github.com/OneLoneCoder/olcNES
https://nesmaker.nerdboard.nl/2022/03/21/the-cpu-memory-map/
- https://llvm-mos.org/
- zig
- https://llvm-mos.org/wiki/Zig
- https://github.com/kassane/zig-mos-bootstrap
- https://kassane.github.io/blog/zig_mos_6502/
- zig
https://github.com/enusbaum/ADNES/ https://github.com/BotRandomness/NET-NES/
https://github.com/BotRandomness/CODE-DMG - gameboy
https://github.com/enusbaum/XamariNES
https://medium.com/happyprogrammer-in-jeju/rust를-업무용-언어로-쓰다-7723cd2c0a59 https://www.inflearn.com/course/%EC%B0%A8%EC%84%B8%EB%8C%80-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%96%B8%EC%96%B4-rust-%EC%8B%A4%EC%9A%A9-%EC%9E%85%EB%AC%B8?cid=331813&inst=458c9aa9&utm_campaign=inflearn_%ED%8A%B8%EB%9E%98%ED%94%BD_promotion-link&utm_medium=referral&utm_source=instructor https://github.com/hatemogi/rust-course
I Am Error: The Nintendo Family Computer / Entertainment System Platform The Elements Of Computing Systems: Building A Modern Computer From First Principles
https://nerdy-nights.nes.science/
$0000-$00FF: 제로 페이지는 다른 주소보다 더 적은 바이트와 사이클로 접근할 수 있습니다. $0100-$01FF: 스택이 포함된 페이지로, 이 영역 내 어디에든 위치할 수 있지만 일반적으로 $01FF에서 시작하여 아래쪽으로 갈수록 커집니다.
그러면 벡터는 다음과 같습니다. FFFA-FFFB - NMI(Non-Maskable Interrupt). The PPU finished drawing a frame FFFC-FFFD - 리셋 벡터 - 콘솔 시작 혹은 재시작 - 코드 실행이 여기서 시작 FFFE-FFFF - IRQ 벡터 - Software interrupt (BRK) or hardware interrupt (IRQ)
$FFFA: $00 $80 => NMI handler at $8000 $FFFC: $00 $C0 => RESET: start at $C000 $FFFE: $50 $80 => IRQ/BRK handler at $8050
https://github.com/taylus/nes-dev
에뮬레이터
-
https://forums.nesdev.org/viewtopic.php?t=25721
-
http://www.codeslinger.co.uk/
-
https://www.reddit.com/r/EmuDev/comments/6zjrqz/how_to_make_a_nes_emulator/?tl=ko
-
https://www.pagetable.com/?p=410 - Internals of BRK/IRQ/NMI/RESET on a MOS 6502
-
256x240 pixels - 60fps
-
topleft >> down right
-
64 색상 출력 가능
- 동시 표시 25개
- 배경 팔레트 (4개) × 3가지
- 스프라이트 팔레트 (4개) × 3가지
- 슬롯 1개는 ‘투명색’
- 공통 배경색 1개
카트리지ChrRom -> 패턴테이블
패턴테이블 $0000-$0FFF $1000-$1FFF
- https://www.nesdev.org/wiki/PPU_OAM
- https://www.nesdev.org/wiki/DMA
https://github.com/nesdev-org/MesenCE/ https://www.retroreversing.com/nes https://www.copetti.org/ko/writings/consoles/nes/
https://people.ece.cornell.edu/land/courses/ece4760/FinalProjects/s2009/bhp7_teg25/bhp7_teg25/index.html
https://www.zophar.net/pdroms/nes.html
https://github.com/nkane/nessy
- https://github.com/nkane/chippy
Ref
- https://www.nesdev.org/
- https://www.reddit.com/r/EmuDev/search/?q=nes
- https://hn.algolia.com/?q=nes
책
- book: Building Your First Emulator : Understanding how emulators work by building a NES from scratch by Matias Salles
- lang: crystal
- https://github.com/matiassalles99/nes-emulator-book
- https://matiassalles99.codes/posts/building-nes-emulator-crystal-book/
- apu까지 다뤄줘서 좋음
- include때문에 코드 파악하는데 힘드네
- crystal ide 한계
- Writing NES Emulator in Rust
- lang: rust
- https://github.com/bugzmanov/nes_ebook
- book: Computer Science From Scratch: Building Interpreters, Art, Emulators and ML in Python by David Kopec
- lang: python
- https://github.com/davecom/ComputerScienceFromScratch
- Classic Game Programming on the NES: Make your own retro video game
- 어셈블리로 NES용 간단한 게임 제작
- https://github.com/tony-cruise/ProgrammingGamesForTheNES
- https://tony-cruise.github.io/ProgammingForTheNES.html
기타
- https://famicom.party/book/
- Making Games for the NES (8bitworkshop)
- https://8bitworkshop.com/
- online ide
- https://andrewkelley.me/post/jamulator.html
- https://github.com/andrewrk/jamulator
https://pikuma.com/courses/nes-game-programming-tutorial
참고 소스
- https://github.com/LeeTeng2001/go-nes-emulator
- https://github.com/analog-hors/pones/blob/main/pones-6502/src/cpu.rs
- https://github.com/erspicu/AprNes
- https://github.com/jakehffn/zig-nes
- https://github.com/mikai233/nesium
https://ltriant.github.io/2019/11/22/nes-emulator.html https://github.com/ltriant/nes
https://github.com/emu-russia/breaks
-
nesicide
- https://github.com/christopherpow/nesicide
- https://www.romhacking.net/utilities/603/
-
https://nesdir.github.io/
How I Made a Handheld NES on a $1 CPU
CPU (Central Processing Unit)
모스 6502 - MOS Technology 6502
- https://en.wikipedia.org/wiki/MOS_Technology_6502
- [27C3] (en) Reverse Engineering the MOS 6502 CPU
- 27차 CCC(Chaos Computer Club)
- http://www.visual6502.org/
ADC구현하면 거의 다 한것
어디에서 사용?
- 애플 II
- 반다이 다마고치
리코 2A03/2A07 - Ricoh 2A03 or RP2A03
- https://en.wikipedia.org/wiki/Ricoh_2A03
- 게임용 CPU
- RP2A03/RP2A03 = MOS 6502(BCD기능 비활성화된 버전) + NES용 APU
| TV규격 NTSC | TV규격 PAL | |
|---|---|---|
| 지역 | 일본, 미국, 한국 대부분 | 유럽, 호주 |
| FPS | 60.1 Hz | 50 Hz |
| CPU 클럭 | 1.789773 MHz | 1.662607 MHz |
| 화면 라인 수 | 262 | 312 |
| NES CPU+APU | RP2A03 | RP2A07 |
| NES PPU | RP2C02 | RP2C07 |
- MOS 6502에서 BCD기능이 비활성화됨
- 특허 침해 소송 방지 및 생산 단가 절감
- 2KB WRAM (Working Ram)
BCD(Binary-Coded Decimal) - 10진수 모드
딱히 필요없음
10진수 45를 저장한다고 할때
일반 방식 : 0010_1101 :
(32) + (8 + 4 + 1) => 45
BCD 방식 : 0100_0101
4 5
레지스터
- 8bit cpu - 16비트 어드레스 버스( 2^16 => 64kB 메모리 )
- NES Architecture Explained
메모리는 리틀엔디언(Little Endian)
| 레지스터(6개) | Bit | ||
|---|---|---|---|
| X | 8 | Index | |
| Y | 8 | Index | |
| A | 8 | Accumulator 결과 저장 | |
| SP | 8 | Stack Pointer | $0100에서 $01FF 사이에 위치한 256바이트 스택을 지원 |
| P | 8 | Status | |
| PC | 16 | Program Counter |
| P(Status) | bit | ||
|---|---|---|---|
| N | 7 | Negative Flag | Set if bit 7 set |
| V | 6 | Overflow Flag | Set if sign bit is incorrect |
| U | 5 | Unused | Always 1, not used |
| B | 4 | Break Command | The interrupt was triggered by software (BRK) |
| D | 3 | Decimal Mode Flag | Decimal mode. The NES ignores it, but the flag exists |
| I | 2 | Interrupt Disable | IRQ interrupts are disabled |
| Z | 1 | Zero Flag | Set if A = 0 |
| C | 0 | Carry Flag | Set if overflow in bit 7 |
SP를 0xFD 로
https://www.nesdev.org/wiki/CPU_power_up_state
| 사이클 | 내부 동작 | R/W 상태 | 스택 포인터(SP) 변화 |
|---|---|---|---|
| 초기 상태 | 전원이 켜지며 SP가 0x00으로 시작 (하드웨어 기본값) | - | SP = 0x00 |
| Cycle 3 | PC High를 스택에 저장하는 시늉 (실제론 읽고 버림) | Read (가짜) | SP = 0xFF (0x00에서 감소) |
| Cycle 4 | PC Low를 스택에 저장하는 시늉 (실제론 읽고 버림) | Read (가짜) | SP = 0xFE (0xFF에서 감소) |
| Cycle 5 | Status(P)를 스택에 저장하는 시늉 (실제론 읽고 버림) | Read (가짜), | SP = 0xFD (0xFE에서 감소) |
| Cycle 6~7 | 리셋 벡터($FFFC, $FFFD)에서 시작 주소를 읽어옴 | Read | SP = 0xFD (유지) |
- SP는 Push시 감소 / Pop시 증가
- 스택은 메모리의 높은 주소에서 낮은 주소 방향으로 자라나는 하향식 스택 구조
- NES (6502): 현재 위치에 값을 먼저 저장하고, 스택 포인터를 감소 (Post-decrement)
- x86: 스택 포인터를 먼저 감소시키고, 그 바뀐 주소에 값을 저장 (Pre-decrement)
- 현대 컴퓨터 구조에서는 스택의 맨 위 데이터를 자주 들여다보고(PEEK) 읽기 성능이 중요한 경우가 많아 감소 후 저장(x86 방식)이 표준처럼 자리
- 0x00 -> 0xFD 는 하드웨어 절약의 결과
- 6502 설계 당시 트랜지스터 수를 극한으로 줄이기 위해, 리셋 전용 회로를 따로 만드는 대신 기존의 인터럽트 회로를 그대로 재활용
- 가짜 Push - 0x00이었던 스택 포인터가 언더플로우를 일으키며 0xFF -> 0x0E -> 0xFD 순으로 세 번 감소
- 회로를 적게씀
- 스택 영역(Page 1: $0100-$01FF)을 온전히 256바이트 모두 사용하고 싶은 개발자들은
- 프로그램 시작부(Reset Routine)에 직접 LDX #$FF, TXS 명령어를 넣어 스택 포인터를 0xFF로 초기화 해서 사용
- 0xFF ==> 0x01FF
- SP는 8비트라 0x00~0xFF까지만 지정가능하나, 주소를 지정할때는 16비트 0x0100과 합쳐저 $0x0100-$0x01FF 범위를 접근하게됨
메모리 맵
- CPU memory map
- https://www.nesdev.org/wiki/CPU_memory_map
| cpu addr | |
|---|---|
| $0000-$00FF | RAM - Zero Page (256 bytes - 0.25KB) |
| $0100-$01FF | RAM - Stack (256 bytes - 0.25KB) |
| $0200-$02FF | RAM - 일반적으로 Shadow OAM for Sprite (256 bytes) |
| $0300-$07FF | RAM - RAM - General-purpose |
| $0800-$1FFF | RAM - Mirror($0000-$07FF) x 3 (2KB) |
| $2000-$2007 | PPU (V-RAM) 레지스터 (8) |
| $2008-$3FFF | PPU - Mirror($2000-$2007) x 1024 |
| $4000-$4003 | APU - Pulse 1 |
| $4004-$4007 | APU - Pulse 2 |
| $4008-$400B | APU - Triangle |
| $400C-$400F | APU - Noise |
| $4010-$4013 | APU - DMC |
| $4014 | OAM DMA |
| $4015 | APU - Status |
| $4016 | APU - 조이패드 |
| $4017 | APU - 프레임 카운터 & 조이패드 2 |
| $4018-$401F | 미사용 영역 - APU and I/O functionality |
| $4020-$5FFF | Unmapped - 카트리지 |
| $6000-$7FFF | 카트리지 RAM (있는 경우) |
| $8000-$BFFF | 카트리지 ROM + mapper registers - lower |
| $C000-$FFFF | 카트리지 ROM + mapper registers - upper |
OAM
- PPU 내부 OAM이 존재.
- CPU가 직접 읽거나 쓰는 것이 느림.
- $0200-$02FF영역에 스프라이트 데이터를 먼저 작성
- V-Blank 시간에 DMA(Direct Memory Access, $4014 레지스터)기능을 사용 한번에 복사
Instruction과 opcode
8bit - 2^8 opcode가 존재하고, 56개의 instruction이 존재.
| instruction | 공식(56) |
| opcode | 256 ( 공식(151) + 비공식(105)) |
| Instruction 56 | Op 151 |
|---|---|
| NOP | x1 |
| LDA | x8 |
| LDX | x5 |
| LDY | x5 |
| STA | x7 |
| STX | x3 |
| STY | x3 |
| INX INY DEX DEY | 4 - 각각 x1 |
| INC | x4 |
| DEC | x4 |
| ADC | x8 |
| SBC | x8 |
| TAX TXA TAY TYA TSX TXS | 6 - 각각 x1 |
| AND | x8 |
| ORA | x8 |
| EOR | x8 |
| BIT | x2 |
| ASL | x5 |
| LSR | x5 |
| ROL | x5 |
| ROR | x5 |
| CMP | x8 |
| CPX | x3 |
| CPY | x3 |
| JMP | x2 |
| JSR RTS | 2 - 각각 x1 |
| BRK RTI | 2 - 각각 x1 |
| CLC SEC CLI SEI CLD SED CLV | 7 - 각각 x1 |
| PHA PHP PLA PLP | 4 - 각각 x1 |
| BCC BCS BEQ BNE BPL BMI BVC BVS | 8 - 각각 x1 |
https://www.nesdev.org/wiki/Instruction_reference
opcode https://www.pagetable.com/c64ref/6502/ instruction
- https://www.pagetable.com/c64ref/6502/?tab=2
- https://www.masswerk.at/6502/6502_instruction_set.html
INX(Increment X Register)
Instructions
- Mnemonic(연상 기호) + Operand
- Opcode + Operand opcode
- CPU가 수행해야 할 ‘구체적인 동작’ 그 자체를 나타내는 컴퓨터의 기계어(바이너리 코드
- NES(6502) Opcode는 무조건 1바이트(8비트) 고정 사양 - 2^8 == 256개
unofficial opcodes - https://www.nesdev.org/wiki/CPU_unofficial_opcodes
opcode https://www.pagetable.com/c64ref/6502/ instruction https://www.pagetable.com/c64ref/6502/?tab=2 addressing https://www.pagetable.com/c64ref/6502/?tab=3
LDX $FF, TXS
addressing 모드
- https://www.nesdev.org/obelisk-6502-guide/addressing.html
| Implicit | |
| Accumulator | |
| Immediate | |
| Zero Page | |
| Zero Page,X | |
| Zero Page,Y | |
| Relative | |
| Absolute | |
| Absolute,X | |
| Absolute,Y | |
| Indexed Indirect | |
| Indirect Indexed | |
| Indirect |
cycle 동기화 - if page crossed CPU가 메모리 주소를 계산하는 과정에서 256바이트(Page) 경계를 넘어가는 경우 0x00_FF => 0x01_00 처럼 High Byte가 변경 AbsoluteX AbsoluteY IndirectY 시 발생 읽기(Read) 명령시 발생. 쓰기(Store)에는 발생 안함.
if branch taken BCC와 같이 분기(PC가 변경)시
ABSOLUTE_X
u16 baseAddr = Absolute(cpu);
u16 nextAddr = baseAddr + cpu.Register.X;
outIsPageCrossed = (baseAddr & 0xFF00) != (nextAddr & 0xFF00);
ABSOLUTE_Y
u16 baseAddr = Absolute(cpu);
u16 nextAddr = baseAddr + cpu.Register.Y;
outIsPageCrossed = (baseAddr & 0xFF00) != (nextAddr & 0xFF00);
Indirect_Y (INDIRECT_INDEXED)
u8 data = cpu.FetchByte();
u16 lo = (u16)cpu.ReadByte((u16)data);
u16 hi = (u16)cpu.ReadByte((u16)(data + 1));
u16 baseAddr = (hi << 8) | lo;
u16 nextAddr = baseAddr + cpu.Register.Y;
outIsPageCrossed = ((lo & 0xFF) + cpu.Register.Y > 0xFF);
immediate lda $#01 - read 1byte
A: 01
zero-page lda $#01 - immediate sta $02 - zero-page - 1byte(0x00 ~ 0xFF) addr ldx $02 - zero-page (0x00 ~ 0xFF)
X: 01
zero-page index lda $#01 ldx $#02 sta $05 ldy $03, X ($05 = $03 + X($#02))
A: 01 X: 02 Y :01
absolute lda $#01 - immediate sta $0200 - absolute - 2byte(0x00_00 ~0xFF_FF) addr ldx $0200
X: 01
absolute-x
todo
Cpu cycle 처리 방식
- 완전한 타이밍을 일일이 맞추는 ‘Tick-perfect’ 방식 대신
- 구현이 간단하고 직관적인 ’Instruction by instruction
- CPU가 한 명령어를 실행하면, 그 주기(Cycle)에 맞춰 PPU가 캐치업(Catch-up) 형태
Cycle을 한번에 받아서 돌리는 일괄(Batch) 방식
Cpu.Step(out int cpuCycle);
for (int i = 0; i < cpuCycle * 3; ++i)
{
Ppu.Step(out bool isNmiTriggerRequired);
if (isNmiTriggerRequired)
{
Cpu.Nmi();
}
}
for (int i = 0; i < cpuCycle; ++i)
{
Apu.Step();
}
- 후에 Ppu.Step에서 채워진 FrameBuffer로 화면을 그리고
- Apu.Step에서 채워진 SampleBuffer로 오디오 플레이
스탭 안에서 Tick을 호출하여 정확도를 높이는 방식
Cpu.Step();
Cpu
{
Step()
{
op_Blabla();
}
op_Blabla()
{
Tick();
Tick();
Tick();
}
Tick()
{
for (int i = 0; i < 3; ++i)
{
Ppu.Step(out bool isNmiTriggerRequired);
if (isNmiTriggerRequired)
{
Cpu.Nmi();
}
}
Apu.Step();
}
}
테스트
nestest.nes / nestest.log
- https://www.nesdev.org/wiki/Emulator_tests
- https://github.com/christopherpow/nes-test-roms
- How Inaccurate are Nintendo’s Official Emulators?
- https://github.com/100thCoin/AccuracyCoin
Cartridge
카트리지 / 게임 팩
NES 게임 팩(ROM)= PRG (코드/소리)CHR (그림) CHR - Character memory, the data for graphics PRG - Program memory, the code for the game
-
PRG-ROM (Program Read-Only Memory)
-
CHR-ROM (Character Read-Only Memory)
-
MMC 1B2 (Memory Management Controller) - Bank Switching
-
emulator container format
- 실제 ROM칩 데이터 + 카트리지 정보(metadata)
- .nes 파일
- ines
- NES 2.0
iNes
- https://www.nesdev.org/wiki/INES
- FACE Marat Fayzullin
- https://fms.komkon.org/
| Header(16) | Trainer(512 if present) | PRG-ROM(N * 16kb) | CHR-ROM(N * 8kb)|
Header
| Byte | |
|---|---|
| 0-3 | “NES” followed by byte 0x1A |
| 4 | Number of PRG-ROM blocks (each block = 16KB) |
| 5 | Number of CHR-ROM blocks (each block = 8KB) |
| 6 | Flags 6 |
| 7 | Flags 7 |
| 8-15 | Padding |
Flag6 - Flag7 -
롬
- https://www.emu-land.net/en/consoles/dendy/roms
- https://www.retrostic.com/roms/nes
- https://romsfun.com/roms/nes/
controller
https://www.nesdev.org/wiki/Controller_reading_code
| bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| button | A | B | Select | Start | Up | Down | Left | Right |
PPU (Picture Processing Unit)
이제 화면을 그려볼 차례
- vram(Video RAM, Console-Internal Ram, CIRAM)
- 해상도 256x240(RP2A03기준)
- V-Blank
렌더링은 어떻게?
- NES Graphics Explained
- NES Attribute Tables Explained
- https://nerdy-nights.nes.science/#main_tutorial-2
- https://nerdy-nights.nes.science/#main_tutorial-4
- everything2 - NES PPU
- https://nesmaker.nerdboard.nl/2024/10/14/nes-architecture-ppu-and-cpu-timing/
- https://blogs.oregonstate.edu/ericmorgan/2022/02/18/nes-emulation-ppu-quick-start/
- https://github.com/emu-russia/breaks/releases/tag/ppu-book-revB-english
- https://famicom.party/book/09-theppu/
- https://nesrocks.com/blog/nes-graphics/
- https://austinmorlan.com/posts/nes_rendering_overview/
일단 용어부터
- https://www.nesdev.org/wiki/PPU_pattern_tables
- https://www.nesdev.org/wiki/PPU_nametables
- https://www.nesdev.org/wiki/PPU_OAM
| 팔레트 | 사용가능한 64(0x3F)가지 색상 코드(Color Code)표(고유색은 56) |
| 서브 팔레트 | 타일마다 사용할 4가지 색상코드 (x0x1x2x3) |
| 배경 팔레트 | 4개의 서브 팔레트로 구성.(a0a1a2a3_b0b1b2b3_c0c1c2c3_d0d1d2d3) |
| vram $3F00-$3F0F 16byte 영역에 위치. | |
| 스프라이트 팔레트 | 4개의 서브 팔레트로 구성. |
| vram $3F10-$3F1F 16byte 영역에 위치. | |
| 패턴(Pattern) 테이블 | 서브 팔레트의 내부 인덱스(0,1,2,3) 모음. (16byte(타일) * 256개 = 4KB) |
| vram $0000-$0FFF 4KB 배경 / $1000-$1FFF 4KB 스프라이트 영역에 위치 | |
| 네임(Name) 테이블 | 패턴 테이블의 인덱스 모음 - 화면의 타일 배치 정보 (타일 32x30 = 960바이트) |
| 속성(Attribute) 테이블 | 몇번째 서브 팔레트 사용할지 고르는 인덱스(a,b,c,d). 배경 only |
| OAM(Object Attribute Memory) | vram과는 별도의 다른 메모리. 개당 4바이트, 최대 64개의 스프라이트 정보 (256바이트) 스프라이트 only |
배경은
화면:
256 * 240 = 61440 픽셀로 구성됨
32 * 30 = 960 타일로 구성됨 (256/8 * 240/8)
타일:
타일당: 8 * 8 = 64 픽셀
픽셀당 2비트로 서브 팔레트내의 몇번째 색상코드를 사용할지 저장
타일당: 64 * 2bit = 128bit = 16 byte (패턴테이블에 저장시)
패턴 테이블: 00 => (a,b,c,d)
vram $0000-$0FFF (4KB): 배경
4KB = 4096 byte = 16 byte * 256
네임 테이블:
vram $2000-$23BF (960byte): Name Table 0
1byte(8bit)로 표현할 수 있는 것은 2^8 = 256 개
타일 32 * 30 = 960개 => 패턴테이블 인덱스로 960바이트가 필요
메타(meta) 타일:
2 * 2 타일
속성 테이블: 00 => (0,1,2,3)
vram $23C0-$23FF (64byte): Attribute Table 0
화면에 있는 타일을 메타타일 단위로 나누면 (32/2) * (30/2) = 16 * 15 = 60
A | B
--+--
C | D
타일당 2비트씩 1byte: 0bDDCC_BBAA
배경 팔레트
vram $3F00-$3F0F (16byte) : a0a1a2a3_b0b1b2b3_c0c1c2c3_d0d1d2d3에서
- 픽셀(x, y)에서
- 타일 단위 (스크롤 고려) 좌표변환 네임테이블에서 타일번호 가져옴
- (메타타일 단위의)속성 테이블을 참조해서 (0,1,2,3)을 판별
- (타일 단위의)패턴 테이블을 참조해서 (a,b,c,d)를 판별
- 배경 팔레트 a0a1a2a3_b0b1b2b3_c0c1c2c3_d0d1d2d3에서 색상 코드를 가져와서
- 팔레트에서 색상코드에 맞는 색을 가져옴
- 뱅크 스위칭(Bank Switching)
- 화면 960타일인데 타일은 256종류라 표현하기 힘들게 된다면 게임 진행 중에 패턴 테이블의 내용 바꿔버림
스프라이트는
OAM (Object Attribute Memory): 00 => (0,1,2,3)
vram과는 별도의 다른 메모리.
개당 4바이트, 최대 64개의 스프라이트 정보
0| Y position
1| 76543210
||||||||
|||||||+- Bank ($0000 or $1000) of tiles
+++++++-- Tile number of top of sprite (0 to 254; bottom half gets the next tile)
2| 76543210
||||||||
||||||++- Palette (4 to 7) of sprite
|||+++--- Unimplemented (read 0)
||+------ Priority (0: in front of background; 1: behind background)
|+------- Flip sprite horizontally
+-------- Flip sprite vertically
3| X position
패턴 테이블: 00 => (a,b,c,d)
vram $1000-$1FFF (4KB): 배경
스프라이트 팔레트:
vram $3F10-$3F1F (16byte) : a0a1a2a3_b0b1b2b3_c0c1c2c3_d0d1d2d3에서
- 픽셀(x, y)에서
- OAM의 스프라이트 정보중 2번째 바이트의 팔레트 정보를 참조해서 (0,1,2,3)을 판별
- (타일 단위의)패턴 테이블을 참조해서 (a,b,c,d)를 판별
- 스프라이트 팔레트 a0a1a2a3_b0b1b2b3_c0c1c2c3_d0d1d2d3에서 색상 코드를 가져와서
- 팔레트에서 색상코드에 맞는 색을 가져옴
타일
그림 | 색상 인덱스(0 ~ 3)
. . . . . . * * | 00 00 00 00 00 00 01 01
. . . . . * * * | 00 00 00 00 00 01 01 01
. . . . * * * * | 00 00 00 00 01 01 01 01
. . . * * * * * | 00 00 00 01 01 01 01 01
. . * @ @ * * * | 00 00 01 11 11 01 01 01
. * * * # @ * * | 00 01 01 01 10 11 01 01
. * * * # @ @ @ | 00 01 01 01 10 11 11 11
* * * * # @ # * | 01 01 01 01 10 11 10 01
하드웨어 제약 때문에 hi비트랑 lo비트나눔
hi | lo
0 0 0 0 0 0 0 0 0x00 | 0 0 0 0 0 0 1 1 0x03
0 0 0 0 0 0 0 0 0x00 | 0 0 0 0 0 1 1 1 0x07
0 0 0 0 0 0 0 0 0x00 | 0 0 0 0 1 1 1 1 0x0F
0 0 0 0 0 0 0 0 0x00 | 0 0 0 1 1 1 1 1 0x1F
0 0 0 1 1 0 0 0 0x18 | 0 0 1 1 1 1 1 1 0x3F
0 0 0 0 1 1 0 0 0x0C | 0 1 1 1 0 1 1 1 0x77
0 0 0 0 1 1 1 1 0x0F | 0 1 1 1 0 1 1 1 0x77
0 0 0 0 1 1 1 0 0x0E | 1 1 1 1 0 1 0 1 0xF5
8 + 8 = 16 byte
저장시 lo8개 hi8개 순서로 Pattern Table에 저장
| $0000-$0FFF | Pattern Table 0 |
| $1000-$1FFF | Pattern Table 1 |
lo | hi
0x03 0x07 0x0F 0x1F 0x3F 0x77 0x77 0xF5 | 0x00 0x00 0x00 0x00 0x18 0x0C 0x0F 0x0E
스캔라인
262 scanline
3 | Vertical Sync |
+---------------------------------------+
14 | VBLANK |
+----------+-------------------+--------+
240 | border | VISIBLE_FRAME | border |
| HBLANK | <-- 256 pixel --> | HBLANK |
+----------+-------------------+--------+
5 | VBLANK |
<-- 341 ppu pixels -->
cpu 1 cycle당 ppu 3 step
ppu step중 nmi 트리거
256w x 240h
for 240
256 픽셀 렌더링
85 PPU
241
vBlank(vertical blanking)
Cpu.Nmi
cpu에서 ppu
| cpu addr | ||
|---|---|---|
| $0200-$02FF | RAM - 일반적으로 Shadow OAM for Sprite (256 bytes) | OAM에 넣기전 cpu에 있는 256 bytes |
| $2000-$2007 | PPU (V-RAM) 레지스터 (8) | |
| $2008-$3FFF | PPU - Mirror($2000-$2007) x 1024 | |
| $4014 | OAM DMA | 앞선 cpu 256bytes를 ppu OAM에 복사 |
PPU 레지스터
- https://www.nesdev.org/wiki/PPU_registers#PPUADDR
| from CPU | PPU (V-RAM) 레지스터 | |
|---|---|---|
| $2000 | PPUCTRL | PPU 제어 설정 (네임테이블 선택, NMI 활성화 등) |
| $2001 | PPUMASK | 화면 표시 설정 (배경/스프라이트 렌더링 온/오프) |
| $2002 | PPUSTATUS | PPU 상태 플래그 (VBlank 시작 여부 등) |
| $2003 | OAMADDR | OAM 주소 지정 |
| $2004 | OAMDATA | OAM 데이터 읽기/쓰기 |
| $2005 | PPUSCROLL | 카메라 스크롤 위치 제어 (X, Y 좌표 축 입력) |
| $2006 | PPUADDR | VRAM 주소 설정 (2번에 걸쳐 16비트 주소 작성) |
| $2007 | PPUDATA | VRAM 데이터 읽기/쓰기 |
Loopy 레지스터
- https://www.nesdev.org/wiki/PPU_scrolling#PPU_internal_registers
| Loopy 레지스터 | ||
|---|---|---|
| v | Current VRAM Address | 현재 PPU가 배경을 그리기 위해 참조하고 있는 실제 VRAM 주소 (가장 중요) |
| t | Temporary VRAM Address | CPU가 주소나 스크롤을 설정할 때 임시로 저장해두는 공간 |
| x | Fine X Scroll | 픽셀 단위의 미세한 가로 스크롤 값 (3비트) |
| w | Write Toggle | $2005, $2006 레지스터는 8비트인데 주소는 16비트(실제론 15비트)라, 첫 번째 쓴 값인지 두 번째 쓴 값인지 구별하는 스위치 (0 또는 1) |
PPU 주소
| PPU addr | |
|---|---|
| $0000-$0FFF | Pattern Table 0 |
| $1000-$1FFF | Pattern Table 1 |
| $2000-$23BF | Name Table 0 - 배치정보 960byte |
| $23C0-$23FF | Attribute Table 0 - 64byte |
| $2400-$27BF | Nametable 1 |
| $27C0-$27FF | Attribute Table 1 |
| $2800-$2BBF | Nametable 2 |
| $2BF0-$2BFF | Attribute Table 2 |
| $2C00-$2FBF | Nametable 3 |
| $2FC0-$2FFF | Attribute Table 3 |
| $3000-$3EFF | Name/Attribute Table Mirror - 0/1/2/3 |
| $3F00-$3F0F | Palette - Background |
| $3F10-$3F1F | Palette - Sprite |
| $3F1F-$3FFF | Palette - Mirror x 6 |
| Palette - $3F00-$3F1F 32바이트 | 배경 16 / 스프라이트 16 | |
|---|---|---|
| $3F00 | Palette(Background) - sub 0 | Universal background color (backdrop) |
| $3F01-$3F03 | Palette(Background) - sub 0 | 3 colors |
| $3F04 | Palette(Background) - sub 1 | $3F00 - Mirror |
| $3F05-$3F07 | Palette(Background) - sub 1 | 3 colors |
| $3F08 | Palette(Background) - sub 2 | 3F00 - Mirror |
| $3F09-$3F0B | Palette(Background) - sub 2 | 3 colors |
| $3F0C | Palette(Background) - sub 3 | $3F00 - Mirror |
| $3F0D-$3F0F | Palette(Background) - sub 3 | 3 colors |
| $3F10 | Palette(Sprite) - sub 0 | $3F00 - Mirror |
| $3F11-$3F13 | Palette(Sprite) - sub 0 | 3 colors |
| $3F14 | Palette(Sprite) - sub 1 | $3F00 - Mirror |
| $3F15-$3F17 | Palette(Sprite) - sub 1 | 3 colors |
| $3F18 | Palette(Sprite) - sub 2 | $3F00 - Mirror |
| $3F19-$3F1B | Palette(Sprite) - sub 2 | 3 colors |
| $3F1C | Palette(Sprite) - sub 3 | $3F00 - Mirror |
| $3F1D-$3F1F | Palette(Sprite) - sub 3 | 3 colors |
OAM DMA https://www.nesdev.org/wiki/DMA
미러링
비디오 램은 2KB임.
VRAM[0x0000-0x03FF] 1KB
VRAM[0x0400-0x07FF] 1KB
여기에 Nametable을 저장할꺼임. Nametable 0~3까지 하면 $2000-$2FFF, 즉 4k(0x0FFF)범위인데 메모리는 2KB(0x07FF)라 모자람.
Name Table
+-----+-----+
| NT0 | NT1 |
+-----+-----+
| NT2 | NT3 |
+-----+-----+
Actual VRAM
+-----+-----+
| A | B |
+-----+-----+
그래서 동일한 메모리를 가리키도록 미러링을 하는데 여기서 수평/수직으로 하는 방법이 있음.
Vertical Mirroring
- 가로 스크롤에 유리
- 세로로 한 화면 정도 이동하는 건 충분히 가능
+-----+-----+
| A | B |
+-----+-----+
| A | B |
+-----+-----+
NT0과 NT2가 동일한 메모리 A(1KB)참조
NT1과 NT3가 동일한 메모리 B(1KB)참조
Horizontal Mirroring
- 세로 스크롤에 유리
+-----+-----+
| A | A |
+-----+-----+
| B | B |
+-----+-----+
NT0과 NT1가 동일한 메모리 A(1KB)참조
NT2과 NT3가 동일한 메모리 B(1KB)참조
- Gauntlet - 2KB 카트리지에 VRAM을 박아놔서 미러링 안하고 가로+세로 스크롤이 가능.
- PPU의 2KB + 카트리지 2KB = 4KB(0x0FFF)로 A/B/C/D에 맞춤.
NMI (non-maskable interrupt)
- https://www.nesdev.org/wiki/NMI
팔레트(.pal) 파일
- https://www.nesdev.org/wiki/.pal
- pal 생성기
- https://bisqwit.iki.fi/utils/nespalette.php
- http://drag.wootest.net/misc/palgen.html
64 colors × 3 byte(8bpc sRGB) = 192 bytes
- cpu가 $2006(PPUADDR)를 사용하여 ppu한테 vram주소 지정
- 2바이트가 필요하지만, cpu가 1바이트씩 보내니 ppu내 loopy register T(Temporary VRAM Address)에 기록시작
- 2번째에 loopy register T(Temporary VRAM Address)가 차게되면 loopy register V(Current VRAM Address)에 복사
- cpu가 $2007(PPUDATA)로 데이터쓰면 ppu가 앞서 저장한 V에서 vram주소를 얻어 vram에 기록
- vram에 데이터를 기록 후 V를 증가시켜 다음 주소를 가리키도록함
APU (Audio Processing Unit)
- https://www.nesdev.org/wiki/APU
- https://www.emulationonline.com/systems/nes/apu-audio/
| 채널 | |
|---|---|
| Pulse 1 | |
| Pulse 2 | |
| Triangle | |
| Noise | |
| DMC |
레지스터
| $4000 | PULSE1_CONTROL_REGISTER | # DDLC VVVV (duty, length halt, constant vol, volume) |
| $4001 | PULSE1_SWEEP_REGISTER | # EPPP NSSS (enable, period, negate, shift) |
| $4002 | PULSE1_TIMER_LOW_REGISTER | # TTTT TTTT (timer low 8 bits) |
| $4003 | PULSE1_TIMER_HIGH_REGISTER | # LLLL LTTT (length index, timer high 3 bits) |
| $4004 | PULSE2_CONTROL_REGISTER | |
| $4005 | PULSE2_SWEEP_REGISTER | |
| $4006 | PULSE2_TIMER_LOW_REGISTER | |
| $4007 | PULSE2_TIMER_HIGH_REGISTER | |
| $4008 | TRIANGLE_LINEAR_REGISTER | # CRRR RRRR (control flag, linear reload) |
| $400A | TRIANGLE_TIMER_LOW_REGISTER | |
| $400B | TRIANGLE_TIMER_HIGH_REGISTER | |
| $400C | NOISE_CONTROL_REGISTER | # –LC VVVV (length halt, constant vol, volume) |
| $400E | NOISE_MODE_PERIOD_REGISTER | # M— PPPP (mode, period index) |
| $400F | NOISE_LENGTH_REGISTER | # LLLL L— (length index) |
Pulse
Triangle
Noise
DMC (Delta Modulation Channel)
-
https://www.nesdev.org/wiki/APU_DMC
-
DPCM(Delta Pulse Code Modulation)
- DMC 채널이 사용하는 오디오 인코딩 방식
Mapper
-
NES 본체는 기본적으로 32KB PRG-ROM (프로그램)과 8KB CHR-ROM (그래픽)만 직접 접근 가능.
-
게임이 커지면서 더 많은 메모리가 필요
-
매퍼가 CPU가 특정 주소에 쓰면 ROM의 다른 부분을 바꿔서 연결
-
https://www.nesdev.org/wiki/Mapper
-
https://www.nesdev.org/wiki/List_of_mappers
-
Mapper 0(NROM)으로 기본 구조 확인.
-
Mapper 3 => Mapper 2 순으로 구현하여 뱅크 스위칭의 감을 익히기.
-
Mapper 1을 구현하여 레지스터 제어 및 상태 머신(State Machine) 이해하기.
-
Mapper 4(MMC3)에 도전하여 IRQ(인터럽트) 시스템 완성하기.
| 0 | NROM | 맵퍼가 없는 상태 | Super Mario Bros |
| 1 | MMC1 | 매퍼 칩 상태(State)’를 관리 | Zelda 1 |
| 2 | UNROM | 코드(PRG) 교체 | Mega Man |
| 3 | CNROM | 그래픽(CHR) 교체 | |
| 4 | MMC3 | 스캔라인(Scanline) IRQ |
Mapper 0
-
https://www.nesdev.org/wiki/NROM
-
NROM
- 닌텐도가 초기에 출시한 NES 카트리지들은 내부 회로 기판(PCB)에 고유한 모델 번호를 인쇄
- 별도의 매퍼칩 없이 ROM만 내장된(ROM only) 시리즈 이름이 NES-NROM-128/NES-NROM-256
etc
그 외
- chip5
- https://www.reddit.com/r/EmuDev/comments/6zjrqz/how_to_make_a_nes_emulator/?tl=ko
chip-8
-
https://www.amazon.com/Chip-8-Emulation-Dummies-Step-Step-ebook/dp/B0FKD1BB8N
-
https://github.com/Timendus/chip8-test-suite gameboy - https://github.com/adtennant/GameboyCPUTests
-
https://nickkossolapov.github.io/fame-boy/building-a-game-boy-emulator-in-fsharp/
- https://github.com/nickkossolapov/fame-boy
-
https://linoscope.github.io/writing-a-game-boy-emulator-in-ocaml/
- https://github.com/linoscope/CAMLBOY
gameboy - color 아타리 2600 SNES, Genesis, GBA…
툴
- cc65 - a freeware C compiler for 6502 based systems
- Running .NET on the NES | BRK252
- https://github.com/jonathanpeppers/dotnes
개발
https://www.thenew8bitheroes.com/
Nostalgic Assets Workshop: NAW - https://nesrocks.itch.io/naw
NES_Tile_Editor
-
https://electricadventures.net/?p=1091
-
https://github.com/tony-cruise/ProgrammingGamesForTheNES/blob/main/Support/NES_Tile_Editor_Installer.msi NEXXT studio https://frankengraphics.itch.io/nexxt NES CHR Editor https://www.ninjasftw.com/squirrel/nes_chr/
-
YY_CHR.NET
- https://www.romhacking.net/utilities/958/
online simulator - https://tony-cruise.github.io/6502Simulator.html
https://nesdoug.com/ https://github.com/BubaVV/nesdoug https://dag7.gitbook.io/nesdoug-nes-guide
https://github.com/clbr/neslib
- https://famistudio.org/
- FamiStudio is a simple music editor for the Nintendo Entertainment System or Famicom
https://nerdy-nights.nes.science/ - nerdy-nights mirror
https://github.com/munshkr/nesasm https://github.com/pce-devel/huc/releases
https://github.com/nesdoug/NESIFIER convert images to NES palette