From 1978afbc18ffb227f0ce36663648e00d1d2d7945 Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Thu, 9 Apr 2026 00:33:04 +0200 Subject: Add the sound/beep.s example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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à --- Makefile | 9 ++- README.md | 1 + scroll/include/all.s | 1 - scroll/include/ppu.s | 12 ++++ scroll/toggle.s | 1 - shared/apu.s | 8 +++ shared/asm.s | 8 +++ shared/oam.s | 11 +++ shared/ppu.s | 9 +++ sound/README.md | 1 + sound/beep.s | 196 +++++++++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 253 insertions(+), 4 deletions(-) create mode 100644 shared/apu.s create mode 100644 shared/oam.s create mode 100644 sound/README.md create mode 100644 sound/beep.s 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 -- cgit v1.2.3