aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mssola@mssola.com>2026-04-09 00:33:04 +0200
committerMiquel Sabaté Solà <mssola@mssola.com>2026-04-10 13:32:44 +0200
commit1978afbc18ffb227f0ce36663648e00d1d2d7945 (patch)
tree10584b63468ca718be84a40ef2c34ef99cb75924
parent21a599651c07ee021d0f211ba45add538acd0f4a (diff)
downloadcode.nes-1978afbc18ffb227f0ce36663648e00d1d2d7945.tar.gz
code.nes-1978afbc18ffb227f0ce36663648e00d1d2d7945.zip
Add the sound/beep.s example
This is the most simple example you can come up when it comes to producing sound on the NES/Famicom. Basically, a single note is delivered to the APU which is continuously on. Signed-off-by: Miquel Sabaté Solà <mssola@mssola.com>
-rw-r--r--Makefile9
-rw-r--r--README.md1
-rw-r--r--scroll/include/all.s1
-rw-r--r--scroll/include/ppu.s12
-rw-r--r--scroll/toggle.s1
-rw-r--r--shared/apu.s8
-rw-r--r--shared/asm.s8
-rw-r--r--shared/oam.s11
-rw-r--r--shared/ppu.s9
-rw-r--r--sound/README.md1
-rw-r--r--sound/beep.s196
11 files changed, 253 insertions, 4 deletions
diff --git a/Makefile b/Makefile
index e7e7901..94fa22a 100644
--- a/Makefile
+++ b/Makefile
@@ -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
+ @mkdir -p out/basics out/scroll out/space out/fx out/rand out/sound
.PHONY: deps
deps:
@which $(CC65) >/dev/null 2>/dev/null || (echo "ERROR: $(CC65) not found." && false)
.PHONY: build
-build: basics space scroll fx rand
+build: basics space scroll sound fx rand
.PHONY: basics
basics:
@@ -69,6 +69,11 @@ scroll:
$(E) " CC scroll/roulette"
$(Q) $(CC65) $(CCOPTS) scroll/roulette.s -C config/mmc3.cfg -o out/scroll/roulette.nes
+.PHONY: sound
+sound:
+ $(E) " CC sound/beep"
+ $(Q) $(CC65) $(CCOPTS) sound/beep.s -C config/nrom.cfg -o out/sound/beep.nes
+
.PHONY: fx
fx:
$(E) " CC fx/blink"
diff --git a/README.md b/README.md
index 380282e..271c4d8 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,7 @@ The examples are distributed like this:
just showing a sprite, but we make it move and perform an action like shooting
bullets.
- [scroll](./scroll/README.md): different scrolling tactics.
+- [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.
diff --git a/scroll/include/all.s b/scroll/include/all.s
index 763feb4..e9f059d 100644
--- a/scroll/include/all.s
+++ b/scroll/include/all.s
@@ -6,7 +6,6 @@
.include "ppu.s"
.include "../../shared/asm.s"
.include "../../shared/joypad.s"
-.include "../../shared/ppu.s"
.include "reset.s"
.include "palettes.s"
diff --git a/scroll/include/ppu.s b/scroll/include/ppu.s
index 34410a0..f62d702 100644
--- a/scroll/include/ppu.s
+++ b/scroll/include/ppu.s
@@ -1,3 +1,15 @@
+;; WRITE_PPU_DATA is a macro that will write into PPUADDR the given address and
+;; into PPUDATA the given byte value.
+.macro WRITE_PPU_DATA address, value
+ bit $2002
+ lda #.HIBYTE(address)
+ sta $2006
+ lda #.LOBYTE(address)
+ sta $2006
+ lda #value
+ sta $2007
+.endmacro
+
.scope PPU
CONTROL = $2000
MASK = $2001
diff --git a/scroll/toggle.s b/scroll/toggle.s
index 57f403f..bba0e97 100644
--- a/scroll/toggle.s
+++ b/scroll/toggle.s
@@ -31,7 +31,6 @@
.include "./include/ppu.s"
.include "./include/oam.s"
.include "./include/globals.s"
-.include "../shared/ppu.s"
.include "../shared/clear.s"
;; Variables used on this example.
diff --git a/shared/apu.s b/shared/apu.s
new file mode 100644
index 0000000..814c446
--- /dev/null
+++ b/shared/apu.s
@@ -0,0 +1,8 @@
+.scope APU
+ m_square_1_envelope = $4000
+ m_square_1_low = $4002
+ m_square_1_high = $4003
+ m_dmc = $4010
+ m_status = $4015
+ m_frame_counter = $4017
+.endscope
diff --git a/shared/asm.s b/shared/asm.s
index 67b195c..e67b7a6 100644
--- a/shared/asm.s
+++ b/shared/asm.s
@@ -20,3 +20,11 @@
.macro JAL ADDR
jmp ADDR
.endmacro
+
+;; The __fallthrough__ special statement allows developers to explicitly tell
+;; the assembler that a "fall through" situation does not happen by mistake.
+.ifndef __NASM__
+ .macro __fallthrough__ arg
+ ;; NOTE: nothing to do :)
+ .endmacro
+.endif
diff --git a/shared/oam.s b/shared/oam.s
new file mode 100644
index 0000000..ff23d64
--- /dev/null
+++ b/shared/oam.s
@@ -0,0 +1,11 @@
+.scope OAM
+ m_addr = $2003
+ m_dma = $4014
+.endscope
+
+.macro OAM_WRITE_SPRITES
+ lda #$00
+ sta OAM::m_addr
+ lda #$02
+ sta OAM::m_dma
+.endmacro
diff --git a/shared/ppu.s b/shared/ppu.s
index 42acb06..8b548e9 100644
--- a/shared/ppu.s
+++ b/shared/ppu.s
@@ -9,3 +9,12 @@
lda #value
sta $2007
.endmacro
+
+.scope PPU
+ m_control = $2000
+ m_mask = $2001
+ m_status = $2002
+ m_scroll = $2005
+ m_address = $2006
+ m_data = $2007
+.endscope
diff --git a/sound/README.md b/sound/README.md
new file mode 100644
index 0000000..a099036
--- /dev/null
+++ b/sound/README.md
@@ -0,0 +1 @@
+TBD
diff --git a/sound/beep.s b/sound/beep.s
new file mode 100644
index 0000000..cae6ec6
--- /dev/null
+++ b/sound/beep.s
@@ -0,0 +1,196 @@
+;;;
+;; Make an annoying "beep" sound.
+
+;;; Nothing remarkable here, go down below until you see some comments :)
+
+.segment "HEADER"
+ .byte 'N', 'E', 'S', $1A
+ .byte $02, $01
+ .res $0A, $00
+
+.segment "VECTORS"
+ .addr nmi, reset, irq
+
+.segment "CHARS"
+ .incbin "../assets/basic.chr"
+
+.segment "CODE"
+
+.include "../shared/asm.s"
+.include "../shared/apu.s"
+.include "../shared/oam.s"
+.include "../shared/ppu.s"
+.include "../shared/clear.s"
+
+.scope Vars
+ zp_flags = $20
+.endscope
+
+.proc reset
+ sei
+ cld
+
+ ldx #$40
+ stx APU::m_frame_counter
+
+ ldx #$FF
+ txs
+
+ inx
+ stx PPU::m_control
+ stx PPU::m_mask
+ stx APU::m_dmc
+
+ bit PPU::m_status
+@vblankwait1:
+ bit PPU::m_status
+ bpl @vblankwait1
+
+ ldx #0
+ lda #0
+@ram_reset_loop:
+ sta $000, x
+ sta $100, x
+ sta $300, x
+ sta $400, x
+ sta $500, x
+ sta $600, x
+ sta $700, x
+ inx
+ bne @ram_reset_loop
+
+ lda #$EF
+@sprite_reset_loop:
+ sta $200, x
+ inx
+ bne @sprite_reset_loop
+
+ OAM_WRITE_SPRITES
+
+@vblankwait2:
+ bit PPU::m_status
+ bpl @vblankwait2
+
+ lda #$3F
+ sta PPU::m_address
+ lda #$00
+ sta PPU::m_address
+
+ lda #$0F
+ ldx #$20
+@palettes_reset_loop:
+ sta PPU::m_data
+ dex
+ bne @palettes_reset_loop
+
+ __fallthrough__ main
+.endproc
+
+.proc main
+ CLEAR_SCREEN
+
+ ;; In order to get sound, you need to enable/disable the channels you
+ ;; want. One typical setup is to enable all of them except for DMC. You do
+ ;; this by writing into $4015 (APU Status). Note that this register can also
+ ;; be read. Reading from this register will give you the state of
+ ;; interrupts, and the length of the counter for each channel. Yes, each
+ ;; channel has an internal counter, but we can discuss this later.
+ ;;
+ ;; Anyways, if we want to enable all channels except DMC, we are in luck
+ ;; because then we just need to set all bits for the low nibble.
+ lda #$0F
+ sta APU::m_status
+
+ ;; This was the configuration for the APU. Now let's produce sound on the
+ ;; Square 1 channel (registers: $4000-$4003).
+
+ ;; Let's configure the Square 1 channel before producing any sound.
+ ;;
+ ;; The low nibble controls the volume: 0 for silent, 1 very low, F
+ ;; maximum. Just to be annoying we will be setting the volume at a maximum
+ ;; level.
+ ;;
+ ;; Then we have two bits which seem quite odd at first. Bit 4 sets/unsets
+ ;; whether the volume is to be kept constant. If unset, then an internal
+ ;; counter will tune it down when running out. Similarly, bit 5 sets/unsets
+ ;; whether the length counter is to be accounted or not. See this counter
+ ;; down below. All in all, here we make the sound constant, so the beep
+ ;; never stops until we mute it (which we don't in this example).
+ ;;
+ ;; Finally we have the "duty" bits, which has four possibilities
+ ;; available. These regulate the tone for the note, and it's basically the
+ ;; percentage of time the square wave is in the "up" position.
+ lda #%10111111
+ sta APU::m_square_1_envelope
+
+ ;; The $4001 address contains the sweep register, which is a way that the
+ ;; APU has in order to produce different pitch effects, or workaround known
+ ;; issues on some tones. I'm not touching it here other than resetting to 0.
+ lda #0
+ sta APU::m_square_1_sweep
+
+ ;; The note to play is 11 bits long. This means that we need two bytes for
+ ;; it, which for square 1 are $4002 and $4003. $4002 is the least
+ ;; significant bits, and the three least significant bits from $4003 are the
+ ;; most significant bits from this 11-bit note definition. How to know which
+ ;; note corresponds to what value is easy via:
+ ;; https://www.nesdev.org/wiki/APU_period_table. Thus, in NTSC, setting $0C9
+ ;; to this 11-bit value gives us a C#.
+ ;;
+ ;; The 5 other bits from $4003 correspond to the length counter. That is,
+ ;; you can regulate for how long this note has to be reproduced, and the APU
+ ;; will (magically) track things for you. This is not available on this
+ ;; configuration because we disabled the option when we configured
+ ;; 'APU::m_square_1_envelope' (see above). Hence, we will set these bits to
+ ;; 0.
+ lda #$C9
+ sta APU::m_square_1_low
+ lda #$00
+ sta APU::m_square_1_high
+
+ ;; NOTE: and that's it :)
+
+ cli
+ lda #%10001000
+ sta PPU::m_control
+ lda #%00011110
+ sta PPU::m_mask
+
+@main_game_loop:
+
+ lda #%10000000
+ ora Vars::zp_flags
+ sta Vars::zp_flags
+@wait_for_render:
+ bit Vars::zp_flags
+ bmi @wait_for_render
+
+ jmp @main_game_loop
+.endproc
+
+.proc nmi
+ bit Vars::zp_flags
+ bpl @next
+
+ pha
+ txa
+ pha
+ tya
+ pha
+
+ lda #%01111111
+ and Vars::zp_flags
+ sta Vars::zp_flags
+
+ pla
+ tay
+ pla
+ tax
+ pla
+@next:
+ rti
+.endproc
+
+.proc irq
+ rti
+.endproc