diff options
| author | Miquel Sabaté Solà <mssola@mssola.com> | 2026-09-29 22:19:51 +0200 |
|---|---|---|
| committer | Miquel Sabaté Solà <mssola@mssola.com> | 2026-09-29 22:19:51 +0200 |
| commit | ff47b013fbdde4a28ea7b58239cceeca85172e0b (patch) | |
| tree | f958f011119768b69e4682d93d58add0ffcc20fa | |
| parent | 7af0538816193be7c88c76a556b908bedf1585df (diff) | |
| download | code.nes-main.tar.gz code.nes-main.zip | |
This is all prone to errors and probably a bad idea on production, but
it was a fun exercise with 6502 assembly.
Signed-off-by: Miquel Sabaté Solà <mssola@mssola.com>
| -rw-r--r-- | Makefile | 9 | ||||
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | asm/README.md | 6 | ||||
| -rw-r--r-- | asm/soft_irq.s | 106 |
4 files changed, 120 insertions, 2 deletions
@@ -18,14 +18,14 @@ clean: @rm -rf out @find . -type f -name "*.o" -delete @find . -type f -name "*.nes" -delete - @mkdir -p out/basics out/scroll out/space out/fx out/rand out/sound + @mkdir -p out/basics out/scroll out/space out/fx out/rand out/sound out/asm .PHONY: deps deps: @which $(CC65) >/dev/null 2>/dev/null || (echo "ERROR: $(CC65) not found." && false) .PHONY: build -build: basics space scroll sound fx rand +build: basics space scroll sound fx rand asm .PHONY: basics basics: @@ -89,3 +89,8 @@ fx: rand: $(E) " CC rand/rand" $(Q) $(CC65) $(CCOPTS) rand/rand.s -C config/nrom.cfg -o out/rand/rand.nes + +.PHONY: asm +asm: + $(E) " CC asm/soft_irq" + $(Q) $(CC65) $(CCOPTS) asm/soft_irq.s -C config/nrom.cfg -o out/asm/soft_irq.nes @@ -42,6 +42,7 @@ The examples are distributed like this: - [sound](./sound/README.md): making the NES/Famicom beep-beep and stuff. - [fx](./fx/README.md): simple graphical effects that can be pulled off. - [rand](./rand/README.md): different strategies for producing random numbers. +- [asm](./asm/README.md): special tricks with 6502 assembly. ## Other projects diff --git a/asm/README.md b/asm/README.md new file mode 100644 index 0000000..3c17348 --- /dev/null +++ b/asm/README.md @@ -0,0 +1,6 @@ +For now there is only one example, `soft_irq.s`, which shows how you can tell +apart a hardware IRQ from a software IRQ, and how to get the "break mark" from a +software IRQ. In the end, this is all quite finnicky and prone to errors, so +just avoid doing anything with that and treat this example as a cool exercise on +6502 programming. I'd say that the better option in order to make assertions and +stuff like that would be to use Lua scripts on the emulator of your choice. diff --git a/asm/soft_irq.s b/asm/soft_irq.s new file mode 100644 index 0000000..1a00d20 --- /dev/null +++ b/asm/soft_irq.s @@ -0,0 +1,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 |
