Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

들어가며

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/

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

에뮬레이터

카트리지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

기타

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

ADC구현하면 거의 다 한것

어디에서 사용?

  • 애플 II
  • 반다이 다마고치

리코 2A03/2A07 - Ricoh 2A03 or RP2A03

  • https://en.wikipedia.org/wiki/Ricoh_2A03
  • 게임용 CPU
    • RP2A03/RP2A03 = MOS 6502(BCD기능 비활성화된 버전) + NES용 APU
TV규격 NTSCTV규격 PAL
지역일본, 미국, 한국 대부분유럽, 호주
FPS60.1 Hz50 Hz
CPU 클럭1.789773 MHz1.662607 MHz
화면 라인 수262312
NES CPU+APURP2A03RP2A07
NES PPURP2C02RP2C07
  • 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

레지스터

메모리는 리틀엔디언(Little Endian)

레지스터(6개)Bit
X8Index
Y8Index
A8Accumulator 결과 저장
SP8Stack Pointer$0100에서 $01FF 사이에 위치한 256바이트 스택을 지원
P8Status
PC16Program Counter
P(Status)bit
N7Negative FlagSet if bit 7 set
V6Overflow FlagSet if sign bit is incorrect
U5UnusedAlways 1, not used
B4Break CommandThe interrupt was triggered by software (BRK)
D3Decimal Mode FlagDecimal mode. The NES ignores it, but the flag exists
I2Interrupt DisableIRQ interrupts are disabled
Z1Zero FlagSet if A = 0
C0Carry FlagSet if overflow in bit 7

SP를 0xFD 로

https://www.nesdev.org/wiki/CPU_power_up_state

사이클내부 동작R/W 상태스택 포인터(SP) 변화
초기 상태전원이 켜지며 SP가 0x00으로 시작 (하드웨어 기본값)-SP = 0x00
Cycle 3PC High를 스택에 저장하는 시늉 (실제론 읽고 버림)Read (가짜)SP = 0xFF (0x00에서 감소)
Cycle 4PC Low를 스택에 저장하는 시늉 (실제론 읽고 버림)Read (가짜)SP = 0xFE (0xFF에서 감소)
Cycle 5Status(P)를 스택에 저장하는 시늉 (실제론 읽고 버림)Read (가짜),SP = 0xFD (0xFE에서 감소)
Cycle 6~7리셋 벡터($FFFC, $FFFD)에서 시작 주소를 읽어옴ReadSP = 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-$00FFRAM - Zero Page (256 bytes - 0.25KB)
$0100-$01FFRAM - Stack (256 bytes - 0.25KB)
$0200-$02FFRAM - 일반적으로 Shadow OAM for Sprite (256 bytes)
$0300-$07FFRAM - RAM - General-purpose
$0800-$1FFFRAM - Mirror($0000-$07FF) x 3 (2KB)
$2000-$2007PPU (V-RAM) 레지스터 (8)
$2008-$3FFFPPU - Mirror($2000-$2007) x 1024
$4000-$4003APU - Pulse 1
$4004-$4007APU - Pulse 2
$4008-$400BAPU - Triangle
$400C-$400FAPU - Noise
$4010-$4013APU - DMC
$4014OAM DMA
$4015APU - Status
$4016APU - 조이패드
$4017APU - 프레임 카운터 & 조이패드 2
$4018-$401F미사용 영역 - APU and I/O functionality
$4020-$5FFFUnmapped - 카트리지
$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)
opcode256 ( 공식(151) + 비공식(105))
Instruction 56Op 151
NOPx1
LDAx8
LDXx5
LDYx5
STAx7
STXx3
STYx3
INX INY DEX DEY4 - 각각 x1
INCx4
DECx4
ADCx8
SBCx8
TAX TXA TAY TYA TSX TXS6 - 각각 x1
ANDx8
ORAx8
EORx8
BITx2
ASLx5
LSRx5
ROLx5
RORx5
CMPx8
CPXx3
CPYx3
JMPx2
JSR RTS2 - 각각 x1
BRK RTI2 - 각각 x1
CLC SEC CLI SEI CLD SED CLV7 - 각각 x1
PHA PHP PLA PLP4 - 각각 x1
BCC BCS BEQ BNE BPL BMI BVC BVS8 - 각각 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

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
4Number of PRG-ROM blocks (each block = 16KB)
5Number of CHR-ROM blocks (each block = 8KB)
6Flags 6
7Flags 7
8-15Padding

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

bit76543210
buttonABSelectStartUpDownLeftRight

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-$02FFRAM - 일반적으로 Shadow OAM for Sprite (256 bytes)OAM에 넣기전 cpu에 있는 256 bytes
$2000-$2007PPU (V-RAM) 레지스터 (8)
$2008-$3FFFPPU - Mirror($2000-$2007) x 1024
$4014OAM DMA앞선 cpu 256bytes를 ppu OAM에 복사

PPU 레지스터

  • https://www.nesdev.org/wiki/PPU_registers#PPUADDR
from CPUPPU (V-RAM) 레지스터
$2000PPUCTRLPPU 제어 설정 (네임테이블 선택, NMI 활성화 등)
$2001PPUMASK화면 표시 설정 (배경/스프라이트 렌더링 온/오프)
$2002PPUSTATUSPPU 상태 플래그 (VBlank 시작 여부 등)
$2003OAMADDROAM 주소 지정
$2004OAMDATAOAM 데이터 읽기/쓰기
$2005PPUSCROLL카메라 스크롤 위치 제어 (X, Y 좌표 축 입력)
$2006PPUADDRVRAM 주소 설정 (2번에 걸쳐 16비트 주소 작성)
$2007PPUDATAVRAM 데이터 읽기/쓰기

Loopy 레지스터

  • https://www.nesdev.org/wiki/PPU_scrolling#PPU_internal_registers
Loopy 레지스터
vCurrent VRAM Address현재 PPU가 배경을 그리기 위해 참조하고 있는 실제 VRAM 주소 (가장 중요)
tTemporary VRAM AddressCPU가 주소나 스크롤을 설정할 때 임시로 저장해두는 공간
xFine X Scroll픽셀 단위의 미세한 가로 스크롤 값 (3비트)
wWrite Toggle$2005, $2006 레지스터는 8비트인데 주소는 16비트(실제론 15비트)라, 첫 번째 쓴 값인지 두 번째 쓴 값인지 구별하는 스위치 (0 또는 1)

PPU 주소

PPU addr
$0000-$0FFFPattern Table 0
$1000-$1FFFPattern Table 1
$2000-$23BFName Table 0 - 배치정보 960byte
$23C0-$23FFAttribute Table 0 - 64byte
$2400-$27BFNametable 1
$27C0-$27FFAttribute Table 1
$2800-$2BBFNametable 2
$2BF0-$2BFFAttribute Table 2
$2C00-$2FBFNametable 3
$2FC0-$2FFFAttribute Table 3
$3000-$3EFFName/Attribute Table Mirror - 0/1/2/3
$3F00-$3F0FPalette - Background
$3F10-$3F1FPalette - Sprite
$3F1F-$3FFFPalette - Mirror x 6
Palette - $3F00-$3F1F 32바이트배경 16 / 스프라이트 16
$3F00Palette(Background) - sub 0Universal background color (backdrop)
$3F01-$3F03Palette(Background) - sub 03 colors
$3F04Palette(Background) - sub 1$3F00 - Mirror
$3F05-$3F07Palette(Background) - sub 13 colors
$3F08Palette(Background) - sub 23F00 - Mirror
$3F09-$3F0BPalette(Background) - sub 23 colors
$3F0CPalette(Background) - sub 3$3F00 - Mirror
$3F0D-$3F0FPalette(Background) - sub 33 colors
$3F10Palette(Sprite) - sub 0$3F00 - Mirror
$3F11-$3F13Palette(Sprite) - sub 03 colors
$3F14Palette(Sprite) - sub 1$3F00 - Mirror
$3F15-$3F17Palette(Sprite) - sub 13 colors
$3F18Palette(Sprite) - sub 2$3F00 - Mirror
$3F19-$3F1BPalette(Sprite) - sub 23 colors
$3F1CPalette(Sprite) - sub 3$3F00 - Mirror
$3F1D-$3F1FPalette(Sprite) - sub 33 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

레지스터

$4000PULSE1_CONTROL_REGISTER# DDLC VVVV (duty, length halt, constant vol, volume)
$4001PULSE1_SWEEP_REGISTER# EPPP NSSS (enable, period, negate, shift)
$4002PULSE1_TIMER_LOW_REGISTER# TTTT TTTT (timer low 8 bits)
$4003PULSE1_TIMER_HIGH_REGISTER# LLLL LTTT (length index, timer high 3 bits)
$4004PULSE2_CONTROL_REGISTER
$4005PULSE2_SWEEP_REGISTER
$4006PULSE2_TIMER_LOW_REGISTER
$4007PULSE2_TIMER_HIGH_REGISTER
$4008TRIANGLE_LINEAR_REGISTER# CRRR RRRR (control flag, linear reload)
$400ATRIANGLE_TIMER_LOW_REGISTER
$400BTRIANGLE_TIMER_HIGH_REGISTER
$400CNOISE_CONTROL_REGISTER# –LC VVVV (length halt, constant vol, volume)
$400ENOISE_MODE_PERIOD_REGISTER# M— PPPP (mode, period index)
$400FNOISE_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(인터럽트) 시스템 완성하기.

0NROM맵퍼가 없는 상태Super Mario Bros
1MMC1매퍼 칩 상태(State)’를 관리Zelda 1
2UNROM코드(PRG) 교체Mega Man
3CNROM그래픽(CHR) 교체
4MMC3스캔라인(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…

개발

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