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

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