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