Link

6502의 인터럽트

서론

6502는 매우 단순한 구조의 CPU이지만, 현대 컴퓨터 시스템에서 제공하는 중요한 기능 중 하나인 인터럽트를 제공한다. 인터럽트는 프로그램의 실행 도중 우선적으로 처리해야 하는 일련의 작업들을 처리하기 위해 제공되는 기능이다. 예를 들어 입출력 장치로부터 전송된 데이터를 처리해야 하거나 전원이 차단되는 등의 일이 발생하면 컴퓨터는 현재 작업 중인 내용을 잠시 중단하고 긴급한 작업을 우선적으로 처리한 다음에 원래의 작업으로 복귀 해야 한다.

CPU가 인터럽트를 지원한다는 것은 주변 장치로부터 전송되는 인터럽트 신호를 감지하고 현재 작업을 일시적으로 중단한 다음에 인터럽트 처리를 위한 일련의 코드를 수행하고 다시 원래의 작업으로 복귀하기 위한 장치를 CPU 차원에서(하드웨어적으로) 제공함을 의미한다. 그런 의미에서 6502는 인터럽트를 제공한다. 다음 그림은 6502의 핀맵을 보여준다.

6502 pinout

강조 표시된 /IRQ, /NMI, /RES는 6502의 인터럽트 신호핀이다. CPU와 연결된 주변 장치가 인터럽트 신호를 발생시키면 해당 핀으로 인터럽트 신호가 전달되어 인터럽트를 요청한다. 이 신호 앞에 붙은 바(bar) 기호는 신호가 Low 활성이라는 뜻이다. 즉, 이 신호선에는 인터럽트가 발생했을 때 Low 신호가 인가된다.

IRQ와 NMI는 각각 Interrupt Request, Non-Maskable Interrupt를 가리키고, RES는 Reset을 가리키는데, 이 신호들에 대한 자세한 설명은 뒤에서 언급할 것이다.


인터럽트의 종류

인터럽트는 분류 기준에 따라 크게 Maskable 인터럽트와 Non-Maskable 인터럽트로 나뉜다.

(1). Maskable 인터럽트 (IRQ)

Maskable 인터럽트 신호는 흔히 IRQ라고 표현하기도 하는데, 인터럽트 요청이 들어와도 상태 레지스터의 Interrupt Disable(인터럽트 불능) 비트를 1로 세트되어 있으면 인터럽트 처리를 거부할 수 있다. 따라서 Maskable 인터럽트를 처리하기 위해서는 Interrupt Disable 비트를 0으로 클리어 하여 허가 상태로 둬야 한다. 6502에서는 이를 제어하기 위해 다음 두 가지 명령어를 제공한다.

SEI
Set Interrupt Disable; 인터럽트 사용을 불허한다. 인터럽트 요청이 들어와도 무시된다.

CLI
Clear Interrupt Disable; 인터럽트 사용을 허가한다.


SEI와 CLI
대부분의 CPU도 Maskable 인터럽트 기능을 제공하며, 인터럽트의 허가 여부를 나타내는 비트가 존재하는데, 6502의 I비트는 Interrupt Enable이 아니라, Interrupt Disable 비트임에 주의해야 한다. 가령 AVR의 경우 외부 인터럽트를 사용하려면 SEI 명령어를 사용해야 하지만, 6502는 CLI 명령어를 사용해야 한다.


Maskable 인터럽트가 처리중일 때는 인터럽트 서비스 루틴(ISR, 인터럽트를 처리하기 위한 일련의 코드)이 중간에 다른 인터럽트 요청에 의해 중단 되는 것을 방지하기 위해서 인터럽트 불능 비트인 I 플래그를 1로 세트한다.

(2). Non-Maskable 인터럽트 (NMI)

Non-Maskable 인터럽트는 인터럽트 요청을 무시할 수 없는 우선순위가 높은 인터럽트로, 흔히 NMI라고 줄여서 표현한다.

(3). 또 다른 분류

일반적으로 컴퓨터 시스템에서 인터럽트는 또 다른 분류 기준에 따라 외부 인터럽트, 내부 인터럽트, 소프트웨어 인터럽트로 분류된다.

외부 인터럽트는 CPU 외부에서 발생하는 인터럽트로, CPU와 연결된 I/O 장치나 타이머 등에서 발생하는 신호다. 내부 인터럽트는 CPU의 동작 중 프로그램에서 발생하는 인터럽트로, 잘못된 명령어의 수행이나 0으로 나누는 등의 예외(exception)가 여기에 해당한다. 트랩(trap)이라고도 한다.

마지막으로, 소프트웨어 인터럽트는 소프트웨어적으로(프로그램 코드로) 발생시킬 수 있는 인터럽트 신호 말한다. 6502에서는 소프트웨어 인터럽트로 BRK(Break) 명령어를 제공한다.

BRK
Break; 소프트웨어적으로 인터럽트를 요청한다. 일반적인 IRQ 신호와 다르게 상태 레지스터의 break 플래그를 1로 세트한다.


인터럽트 서비스 루틴(ISR, Interrupt Service Routine)과 인터럽트 벡터(Interrupt Vector)

인터럽트가 발생하면 컴퓨터는 하던 작업을 중단하고 인터럽트 요청을 처리하기 위한 일련의 코드를 수행한다. 이런 코드를 보고 인터럽트 서비스 루틴(ISR)이라고 하는데, 보통 서브루틴 형태로 존재한다. 메모리상에 인터럽트 처리를 위한 서브루틴(ISR)을 정의해두고 이러한 서브루틴의 시작주소를 인터럽트 벡터(Interrupt Vector)라고 하는 메모리상의 특정 영역에 저장해두면 인터럽트가 발생했을 때 CPU가 인터럽트 벡터에 저장된 ISR의 시작주소를 읽어서 ISR로 점프한다. ISR이 실행된 다음에는 다시 원래 작업하던 프로그램의 명령어 주소로 복귀한다.


인터럽트 벡터에는 어떤 값이 있는가?
인터럽트 벡터에 있는 값은 CPU의 구현에 따라 다르다. 6502는 인터럽트 벡터에 ISR의 시작 주소를 두고 있기 때문에 인터럽트가 발생하면 인터럽트 벡터에 있는 ISR 시작 주소를 읽어서 해당 주소로 점프(PC값을 ISR 주소로 바꾼다)한다. 그러나 어떤 프로세서는 인터럽트 벡터에 ISR로 점프하는 점프 명령어를 기록 해두고 인터럽트가 발생하면 인터럽트 벡터로 점프한 다음에 점프 명령어를 실행하는 방식을 사용한다.


​다음은 6502의 인터럽트 벡터이다. 6502는 인터럽트의 종류에 따라 벡터 주소를 정의해 두고 있다.
예를 들어 IRQ 신호가 발생하면 CPU는 $FFFE-$FFFF 번지에 저장된 주소값을 읽어서 PC를 이 주소값으로 대체한다.

신호벡터B(BRK) 플래그 설정 여부
NMI$FFFA/$FFFBno
RESET$FFFC/$FFFDno
IRQ$FFFE/$FFFFno
BRK$FFFE/$FFFFyes


6502의 인터럽트 처리 과정

지금까지 인터럽트의 종류와 ISR, 인터럽트 벡터에 대해 다뤄보았다. 그런데 여전히 남아 있는 문제가 있다. 지금까지 다룬 내용만으로는 대략적인 인터럽트 처리 과정은 이해해도 구체적으로 어떻게 작업을 중지했다가 다시 복귀할 수 있는지에 대해서는 알 수 없다.

​중단된 작업의 복구를 위해서는 현재 작업에 대한 정보를 저장해둘 필요가 있다. 이를 위해서 6502는 인터럽트가 발생했을 때 복귀 주소의(PC) 값과 상태 레지스터 P(Processor Status Register)의 값을 스택에 저장한다. 그리고 인터럽트 벡터에 기록된 ISR의 주소를 읽어서 해당 주소로 점프한다. ISR의 실행을 마치면 스택에 저장된 상태 레지스터 값과 복귀 주소 값을 가져와서 중단됐던 내용을 복구한다.

(1). 인터럽트 시퀀스(Interrupt Sequence)

6502에서 인터럽트 요청을 처리해서 ISR을 실행시키기 까지는 7클럭 사이클을 필요로 하는데, 다음 그림은 이러한 인터럽트 시퀀스(interrupt sequence)를 잘 표현하고 있다.

인터럽트 시퀀스

6502의 인터럽트 처리는 요약하자면 다음과 같다.

  1. 인터럽트 신호 /IRQ를 통해 인터럽트 요청
  2. 현재 실행 중인 명령어를 마저 실행한다.
    ISR이 실행되기 이전까지 7클럭 사이클이 소요된다(이 과정을 인터럽트 시퀀스라고 한다):
  3. 내부 연산
  4. 복귀 주소의 상위 바이트를 스택에 저장한다.
  5. 복귀 주소의 하위 바이트를 스택에 저장한다.
  6. 상태 레지스터를 스택에 저장한다.
  7. IRQ 벡터의 하위 바이트를 메모리 $FFFE로부터 가져온다.
  8. IRQ 벡터의 상위 바이트를 메모리 $FFFF로부터 가져온다.
    ISR의 첫 실행:
  9. 인터럽트 벡터($FFFE-$FFFF)에서 얻어온 ISR 주소로 점프하여 ISR을 실행한다.

(2). 인터럽트의 종료

앞서 언급했듯이 인터럽트 서비스 루틴(ISR)은 일종의 서브루틴이라고 했다. 서브루틴의 종료를 위해서 RTS 명령어가 필요한 것처럼 ISR의 종료를 위해서는 RTI라는 명령어가 필요한데, RTS와 RTI는 둘 다 서브루틴을 종료하고 복귀 주소로 돌아간다는 점에서 비슷하지만, RTI는 스택에 저장된 상태 레지스터 값을 꺼내와 복구시킨다는 점에서 차이가 있다.

RTI Return from Interrupt Service Routine; ISR을 종료하고 스택으로부터 복귀 주소와 상태 레지스터의 값을 꺼내 PC와 상태레지스터를 인터럽트 이전의 상태로 복원한다.

​또한 RTI는 6클럭 사이클을 취하는데, 이 6 사이클 동안의 과정은 앞서 언급한 인터럽트 시퀀스의 역순이다.

(3). 기타

레지스터 값의 저장과 복원
6502는 인터럽트가 발생하면 복귀 주소(PC)와 상태 레지스터 P의 값만 자동적으로 저장/복구하기 때문에 ISR 안에서 레지스터 값을 변경하는 경우에는 ISR의 선두와 후미에 별도로 레지스터 값을 스택에 저장하거나 복구하는 명령어를 기술해야 한다.
​ 한편, 6502는 기존의 NMOS 공정에서 설계된 6502와 CMOS 공정으로 설계된 65C02가 존재하는데, 65C02는 인덱스 레지스터 X와 Y를 위한 추가적인 스택 연산 명령어를 제공하고 있다.

NMOS 650265C02
PHP, PHA, PLA, PLPPHP, PHA, PLA, PLP, PHX, PHY, PLX, PLY

ISR을 코딩할 때는 이 점을 염두에 두고 있어야 한다. 65C02에서 기존의 NMOS 6502 명령어만을 사용하는 것도 가능하지만, 65C02를 사용하는 경우에는 65C02에서 제공하는 명령어를 사용하는 것이 성능상으로나 메모리 공간상으로나 이득이다. 두 CPU의 ISR을 어셈블리어로 짜면 대략 다음과 같은 형태일 것이다.

; NMOS 6502
ISR:
    PHA
    TXA
    PHA
    ...
    PLA
    TAX
    PLA
    RTI

; CMOS 6502 (65C02)
ISR:
    PHA
    PHX
    ...
    PLX
    PLA
    RTI


십진수 연산
6502에는 10진수 연산(BCD) 모드를 지원하는데, 이를 나타내는 모드 비트는 상태 레지스터의 D(Decimal) 플래그이다. 65C02에서는 인터럽트가 발생했을 때 자동으로 이 플래그가 0으로 클리어되지만 NMOS 6502는 그런 기능을 제공하지 않기 때문에 ISR에서 16진수 연산을 하기 위해서는 별도로 CLD 명령어를 실행해줘야 한다.

예제

직접 예제를 구성해 보는 것으로 이 글을 마무리하고자 한다.
3행 부터 6행 까지의 인터럽트 벡터 구성 코드는 주석 처리된 72, 73행 코드로 대체 가능하다.
이 프로그램은 asm6로 어셈블 가능하다.

int1.asm

    .org $0200
    ; Set IRQ/BRK Interrupt Vector
    lda #<ISR ; ISR의 하위 바이트
    sta $FFFE
    lda #>ISR ; ISR의 상위 바이트
    sta $FFFF

start:
    jsr init
    jsr setNum
    lda #$12
    jsr addNum
    ldx result
    lda #$34
    jsr subNum
    ldy result

    brk

    jmp start

init:
    lda #0
    ldx #0
    ldy #0
    sta num1
    sta num2
    sta result
    rts

setNum:
    pha
    lda #$10
    sta num1
    lda #$20
    sta num2
    pla
    rts

addNum:
    pha
    lda num1
    clc
    adc num2
    sta result
    pla
    rts

subNum:
    pha
    lda num1
    sec
    sbc num2
    sta result
    pla
    rts

num1:
    .db 0
num2:
    .db 0
result:
    .db 0

ISR:
    lda #$aa
    ldx #$bb
    ldy #$cc
    rti

; BRK Interrupt Vector ($FFFE-$FFFF)
;    .org $fffe
;    .dw ISR