aboutsummaryrefslogtreecommitdiff
path: root/asm/soft_irq.s
blob: 1a00d2026d6be0e7a09d2f386927eb31cf053c81 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
;;;
;; An example that showcases how you can tell apart a hardware IRQ from a
;; software IRQ, and how to get the "break mark" from a software IRQ (see:
;; https://www.masswerk.at/6502/6502_instruction_set.html#BRK).

.segment "HEADER"
    .byte 'N', 'E', 'S', $1A
    .byte $02, $01
    .byte $00
    .byte $00

.segment "VECTORS"
    .addr nmi, reset, irq

.segment "CODE"
;; asan:stack full

zp_ptr  = $00                   ; asan:reserve $02
zp_brk  = $02
m_stack = $100

.proc reset
    ;; Our code will simply call 'brk' to interrupt execution and then loop
    ;; forever.
    brk
    .byte $42

here:
    jmp here
.endproc

.proc nmi
    rti
.endproc

.proc irq
    ;; Save all registers.
    pha
    txa
    pha
    tya
    pha

    ;; On a 'brk' instruction, the status register is pushed onto the stack with
    ;; the 'B' flag set. Otherwise, on a hardware IRQ we will not get any such
    ;; thing. Hence, we first try to pull the status register.
    tsx

    ;; Where is it located? Well, the stack pointer points to the next element,
    ;; and we have pushed onto the stack three times in order to save the
    ;; registers when we entered irq(). Hence, it's at the current stack
    ;; position + 4 + the current stack frame (in 'x' thanks to the previous
    ;; 'tsx').
    lda m_stack + 4, x

    ;; Is the B flag set on the status register?
    and #$10

    ;; If that's not the case, then it's a hardware IRQ.
    ;;
    ;; NOTE: this crucial part is actually prone to errors! It might just be the
    ;; case that the stack contained garbage, we got a hardware IRQ, and
    ;; 'm_stack + 4 + stack frame' just so happened to match '#$10'. In order to
    ;; guarantee this to work, 'pla' usage would need to be coupled with a
    ;; cleaning of the given stack address as well, which is just unrealistic.
    beq is_hardware_irq

    ;; This is a software IRQ. Now, the low byte from the PC address was pushed
    ;; just before the status register. But the address that was pushed was
    ;; actually PC + 2 so to hop over the "break mark". That is, every 'brk'
    ;; instruction is coupled with an extra byte containing debugging
    ;; information for the given 'brk'. Hence, if we want to get the "break
    ;; mark", we have to pick up the address of PC - 1.
    lda m_stack + 5, x
    sec
    sbc #$01
    sta zp_ptr

    ;; The high byte from the PC address was pushed onto the stack before the
    ;; low one. Hence, it's the next byte. Moreover, we call 'sbc #$00' to apply
    ;; the borrow from the previous subtraction just in case it underflowed.
    lda m_stack + 6, x
    sbc #$00
    sta zp_ptr + 1

    ;; And now 'zp_ptr' points to the actual break mark.
    ldy #$00
    lda (zp_ptr), y

    ;; NOTE: when running this example, you should see the "break mark" ($42)
    ;; set into memory address 'zp_brk' ($02).
    sta zp_brk

is_hardware_irq:
    ;; Restore all registers.
    pla
    tay
    pla
    tax
    pla

    rti
.endproc

.segment "CHARS"
.byte $01