Programmable Sound Generator
The PSG (Programmable Sound Generator) is one of the two pieces of sound hardware in the Mega Drive, inherited from SG-1000 and Master System. It only produces simple sounds, but it's also easy to program and makes for a good companion to the FM channels.
Overview
The PSG is pretty simple sound hardware. It provides the following:
- 3x square wave channels (channels 0 to 2)
- 1x noise channel (channel 3)
It's generally used for sound effects and background instruments, and the noise channel is often used for hit-hats. You can put it into better use if you wish, however ("Door into Summer" from Knuckles Chaotix is a good example of this).
68000 and Z80
The PSG can be accessed by both the 68000 and the Z80. Everything you do involves writing bytes to the PSG port, so first you need to know its address. The PSG port is at the following locations for each CPU:
- 68000: at
$C00011
- Z80: at
$7F11
68000 assembly constant
PsgPort: equ $C00011
Z80 assembly constant
PsgPort: equ $7F11
Changing the volume
In order to get sound out of a PSG channel you need to set two things: its volume and its pitch. Let's start with the volume.
PSG channels don't have volume but rather "attenuation" (if it makes sense): every time you increase it, you make the channel quieter. Attenuation goes from 0 to 15, where every step is -2dB: 0 is full volume, 1 is -2dB, 2 is -4dB, and so on until 14 at -28dB (15 will mute the channel).
Changing a channel's attenuation is done by writing a single byte to the PSG port. The byte to write is as follows:
$90
OR (channel << 5)
OR attenuation
68000 assembly sample
; d0 = channel (0..3)
; d1 = attenuation (0..15)
ror.b #3, d0
or.b d1, d0
or.b #$90, d0
move.b d0, (PsgPort)
Z80 assembly sample
; a = channel (0..3)
; b = attenuation (0..15)
rrca
rrca
rrca
or b
or $90
ld (PsgPort), a
Changing the pitch
Channels 0 to 2 are square waves, and have a reasonable pitch range (albeit lacking on the bass side). They have a frequency ranging from 0 to 1023, albeit like with volume, it works backwards (i.e. lower value = higher pitch).
The following is a mapping of PSG frequencies to semitones for the lowest complete octave it supports. To get higher pitches, halve the value for every octave you go up:
Semitone | Frequency |
---|---|
C-3 | 851 |
C#3 | 803 |
D-3 | 758 |
D#3 | 715 |
E-3 | 675 |
F-3 | 637 |
F#3 | 601 |
G-3 | 568 |
G#3 | 536 |
A-3 | 506 |
A#3 | 477 |
B-3 | 450 |
Changing the pitch of a square wave channel requires writing two bytes to the PSG port. The first byte is as follows:
$80
OR (channel << 5)
OR (frequency AND 0x0F
)
The second byte is simply:
frequency >> 4
68000 assembly sample
; d0 = channel (0..3)
; d1 = frequency (0..1023)
; d2 = scratch
; Split the frequency into
; its two parts
move.w d1, d2
lsr.w #4, d2
and.b #$0F, d1
and.b #$3F, d2
; Prepare the first byte (the
; second one is d2 as-is)
ror.b #3, d0
or.b d1, d0
or.b #$80, d0
; Send the bytes
move.b d0, (PsgPort)
move.b d2, (PsgPort)
Z80 assembly sample
; c = channel (0..3)
; de = frequency (0..1023)
; a,b = scratch
; We need the low 4 bits
; of the frequency first
ld a, e
and $0F
ld b, a
; Build the 1st byte
ld a, c
rrca
rrca
rrca
or b
or $80
; Send 1st byte
ld (PsgPort), a
; We need the upper six
; bits of frequency now
ld a, e
and $F0
rrca
rrca
rrca
rrca
ld b, a
ld a, d
rlca
rlca
rlca
rlca
or a, b
; Send 2nd byte
ld (PsgPort), a
Noise channel
The noise channel works differently. It can output two kinds of sounds:
- Periodic noise (really 1/16 duty cycle)
- White noise (actual noise)
The range of pitches is more limited:
- High pitch
- Medium pitch
- Low pitch
- Channel 2's frequency
The noise channel doesn't have its own frequency, but instead offers three different fixed pitches. Alternatively, you can borrow channel 2's frequency for finer control of the noise pitch, but in that case you probably want to mute said channel.
Changing the noise tone involves writing a single byte to the PSG port. The following values are used for each of the possible combinations:
PSG_PERIODIC_HIGH: equ $E0
PSG_PERIODIC_MID: equ $E1
PSG_PERIODIC_LOW: equ $E2
PSG_PERIODIC_CH2: equ $E3
PSG_WHITE_HIGH: equ $E4
PSG_WHITE_MID: equ $E5
PSG_WHITE_LOW: equ $E6
PSG_WHITE_CH2: equ $E7
If you use channel 2's frequency, you'll also need to manipulate that channel separately.
PSG envelopes
The PSG has no hardware support for envelopes, you have to fake them in software by manipulating the volume and frequency registers regularly (e.g. every frame).
Echo has the EEF format for storing PSG envelopes, if you're undecided on what to use. It handles both volume changes over time as well as pitch (which is useful for more complex "instruments" like whistles).