PC-Speaker 61h
Part 3: of the asm-8086/88 Series
December 17, 2024 - Tommy Dräger
In our previous article we made an digression to the different screen modes. Before we move on with loading bitmaps I want to take a deeper look at the PIT (Programmable Interval Timer 8253/8254) and the PC-Speaker in order to create simple beep sounds that will later be refactored into arpeggiated chiptune music!
Download Boilercode
git clone [email protected]:MilesTails01/i8086_boilercode.git
after downloading you need to unpack DOSBOX.zip and TOOLS.zip
cd \bin
tar -xf TOOLS.zip
tar -xf DOSBOX.zip
Usage
build.bat // Compile, Link and Start the source
debug.bat // Starts AFDEBUG and loads the compiled exe
start.bat // Start the program without rebuilding it
Port 0x42 - PIT (Programmable Interval Timer)
the PIT (Programmable Interval Timer) is (was) a piece on your circuit board. I highly recommend to take some time to study that little piece of hardware. In short: it's an IC that can create electronic pulses in 3 different frequencies. Those timed signals can be used to feed a signal to the PC Speaker or it can be used as a timed interval that will trigger an Interupt Request (IRQ) every X milliseconds. Have a look at those manuals:
- https://wiki.osdev.org/Programmable_Interval_Timer
- http://www.brokenthorn.com/Resources/OSDevPit.html
Summary
The oscillator used by the PIT chip runs at (roughly) 1.193182 MHz. The reason for this requires a trip back into history (to the later half of the 1970's)...
The original PC used a single "base oscillator" to generate a frequency of 14.31818 MHz because this frequency was commonly used in television circuitry at the time. This base frequency was divided by 3 to give a frequency of 4.77272666 MHz that was used by the CPU, and divided by 4 to give a frequency of 3.579545 MHz that was used by the CGA video controller. By logically ANDing these signals together a frequency equivalent to the base frequency divided by 12 was created. This frequency is 1.1931816666 MHz (where the 6666 part is recurring). At the time it was a brilliant method of reducing costs, as the 14.31818 MHz oscillator was cheap due to mass production and it was cheaper to derive the other frequencies from this than to have several oscillators. In modern computers, where the cost of electronics is much less, and the CPU and video run at much higher frequencies the PIT lives on as a reminder of "the good ole' days".
- Channel 0 used is used for Interupt Requests timing IRQ every 54.9254ms
- Channel 1 used to "refresh" the capicitors of the DRAM (due to leakage)
- Channel 2 used for the PC Speaker! (PC Speaker Port = I/O port 0x61)
I/O port Usage
0x40 Channel 0 data port (read/write)
0x41 Channel 1 data port (read/write)
0x42 Channel 2 data port (read/write)
0x43 Mode/Command register (write only, a read is ignored)
Bits Usage
6 and 7 Select channel :
0 0 = Channel 0
0 1 = Channel 1
1 0 = Channel 2
1 1 = Read-back command (8254 only)
4 and 5 Access mode :
0 0 = Latch count value command
0 1 = Access mode: lobyte only
1 0 = Access mode: hibyte only
1 1 = Access mode: lobyte/hibyte
1 to 3 Operating mode :
0 0 0 = Mode 0 (interrupt on terminal count)
0 0 1 = Mode 1 (hardware re-triggerable one-shot)
0 1 0 = Mode 2 (rate generator)
0 1 1 = Mode 3 (**square wave generator**)
1 0 0 = Mode 4 (software triggered strobe)
1 0 1 = Mode 5 (hardware triggered strobe)
1 1 0 = Mode 2 (rate generator, same as 010b)
1 1 1 = Mode 3 (square wave generator, same as 011b)
0 BCD/Binary mode: 0 = 16-bit binary, 1 = four-digit BCD
Port 0x61 (PC-Speaker)
https://wiki.osdev.org/PC_Speaker
Summary
The PC Speaker can be connected directly to the output of timer number 2 on the Programmable Interval Timer by setting bit 0 of port 0x61 to active
- the speaker itself has only two possible positions, "in" and "out" (2 bit audio .. BEEEEEP)
- Typically, 256 positions (8 bits) are considered adequate to play comprehensible audio
- We can use Pulse-Width Modulation to give the impression of different frequencies
The PC Speaker takes approximately 60 millionths of a second to change positions. This means that if the position of the speaker is changed from "in" to "out" and then changed back in less than 60 microseconds, the speaker did not have enough time to fully reach the "out" position. By precisely adjusting the amount of time that the speaker is left "out", the speaker's position can be set to anywhere between "in" and "out", allowing the speaker to form more complex sounds.
- To play standard audio, the CPU needs to be interrupted 44100 times every second.
- We will use the PIT to get that interuption interval
Step By Step to Beep
Step 1
We need a signal first! A frequence signal. something that is trigger 44100 times per second! Luckily we can use that PIT that I explained ealier. remember: the PIT chip can be configured on the first 8bit of port 0x43.
mov al, 0B6h ; 0B6h = [10][11][011][0]binary
; - Bit 0 : 0 ( BCD mode: 0 for binary )
; - Bit 1-3 : 011 ( Operating mode 3: Square Wave Generator )
; - Bit 4-5 : 11 ( Access mode: lobyte/hibyte )
; - Bit 6-7 : 10 ( Select Channel 2 )
out 43h, al ; Send the configuration byte to PIT command port (43h)
Step 2
Remember the PIT is having a frequency of 1.193180MHz. So if we want to get the frequency of the Note C3 (130.83Hz) for example we need to divide 1193180 by 9121. But 9121 is a 16bit integer which can get tricky in an 8 bit bus. But this is how its done:
mov ax, 9121 ; we will refactor this line later (to avoid hardcoding)
mov dx, 42h ; DX = 42h (PIT Channel 2 Data Port)
out dx, al ; Send the low byte to PIT
mov al, ah ; Load the high byte of the frequency divisor from AX
out dx, al ; Send the high byte to PIT
Step 3
The frequency is triggering in the correct timing. Now its time to enable that speaker. BEEEP
in al, 61h ; Read the current state of port 61h (Speaker Control Port)
or al, 03h ; Set bits 0 and 1 to enable the speaker (00000011b)
out 61h, al ; Write the modified value back to port 61h to activate the speaker
Step 4
optional: How long you want to hold that Beep? I show the definition of the DELAY_MS prodedure later.
mov bx, [di] ; Load the duration (in milliseconds) from DURATIONS array into BX
call DELAY_MS ; Call the delay procedure to pause for the note's duration
Step 5
optional: Disable the PC Speaker after the delay. You could theoreticly hold that beep tone forever, but we wanna make some music here.
in al, 61h ; Read the current state of port 61h
and al, fc16h ; Clear bits 0 and 1 to disable the speaker (11111100b)
out 61h, al ; Write the modified value back to port 61h to deactivate the speaker
In order to find the correct frequencies and duration take a look at that image:
Demonstration
Complete Code
; #################################
; # MAIN.ASM #
; #################################
; #################################
; # STACK #
; #################################
STACK SEGMENT PARA STACK
db 64 dup(0)
STACK ENDS
; #################################
; # DATA #
; #################################
DATA SEGMENT PARA 'DATA'
BUFFER db 64000 dup(12)
MSG db 'Hello World','$'
VIDEOMODE_320x200 dw 13h
VIDEOMODE_640x480 dw 12h
VID_MEM_SEG dw 0A000h
VID_MEM_OFF dw 0000h
BUF_MEM_OFF dw 0000h
VID_WIDTH dw 320
VID_HEIGHT dw 200
NOTES dw 9121, 8609, 8126, 7670, 7239, 6833, 6449, 6087
; 5746, 5423, 5119, 4831, 4560, 4304, 4063, 3834,
; 3619, 3416, 3224, 3043, 2873, 2711, 2559, 2415,
; 2280, 2152, 2031, 1917, 1809, 1715, 1612, 1521,
; 1436, 1355, 1292, 1207, 1140 ; divisors to calculate the frequencies
DURATIONS dw 10, 10, 10, 10, 10, 10, 10, 10 ; Duration of notes in (ms)
NOTE_COUNT dw 8 ; Number of notes
DATA ENDS
; #################################
; # CODE #
; #################################
CODE SEGMENT PARA 'CODE'
ASSUME cs:CODE, ds:DATA, ss:STACK
mov ax, DATA
mov ds, ax
mov ax, STACK
mov ss, ax
; #################################
; # MAIN #
; #################################
call SET_CONFIG
call REFRESH
call PLAY_MUSIC
ret
; =============================
; | FUNCTIONS |
; =============================
SET_CONFIG PROC NEAR
call SET_VIDEO_MODE
ret
SET_CONFIG ENDP
;
; *****************************
;
SET_VIDEO_MODE PROC
mov ax, VIDEOMODE_320x200 ;set video mode
int 10h ;10h bios interupt
ret
SET_VIDEO_MODE ENDP
;
; *****************************
;
REFRESH PROC NEAR
mov ax, VID_MEM_SEG ; ax = segment for display memory
mov di, VID_MEM_OFF ; di = segment for display memory
mov es, ax ; es:di = address for display memory
mov dx, VID_HEIGHT
FILL:
mov ax, dx ; pixel color
mov cx, VID_WIDTH ; 320
rep stosb ; 320 x stosb
dec dx
cmp dx, 0000h
jne FILL
ret
REFRESH ENDP
;
; *****************************
;
PLAY_MUSIC PROC NEAR
mov si, offset NOTES ; Point to NOTES array
mov di, offset DURATIONS ; Point to DURATIONS array
mov cx, NOTE_COUNT ; Number of notes to play
NEXT_NOTE:
push cx
mov ax, [si] ; load note frequency
cmp ax, 0 ; avoid frequency 0 divison
je END_NOTE
; Step 1: Configure PIT
mov al, 0B6h ; 0B6h = 10110110b
; - Binary Breakdown:
; - Bits 7-6: 10 (Select Channel 2)
; - Bits 5-4: 11 (Access mode: lobyte/hibyte)
; - Bits 3-1: 011 (Operating mode 3: Square Wave Generator)
; - Bit 0: 0 (BCD mode: 0 for binary)
out 43h, al ; Send the configuration byte to PIT command port (43h)
; Step 2: Send frequency divisor
mov dx, 42h ; DX = 42h (PIT Channel 2 Data Port)
mov al, [si] ; Load the low byte of the frequency divisor from NOTES array
out dx, al ; Send the low byte to PIT
mov al, ah ; Load the high byte of the frequency divisor from AX
out dx, al ; Send the high byte to PIT
; Step 3: Enable the PC Speaker
in al, 61h ; Read the current state of port 61h (Speaker Control Port)
or al, 03h ; Set bits 0 and 1 to enable the speaker
out 61h, al ; Write the modified value back to port 61h to activate the speaker
; Step 4: Delay for the duration of the note
mov bx, [di] ; Load the duration (in milliseconds) from DURATIONS array into BX
call DELAY_MS ; Call the delay procedure to pause for the note's duration
; Step 5: Disable the PC Speaker
in al, 61h ; Read the current state of port 61h
and al, fc16h ; Clear bits 0 and 1 to disable the speaker
out 61h, al ; Write the modified value back to port 61h to deactivate the speaker
END_NOTE:
pop cx ; Restore the loop counter from the stack
add si, 2 ; Move SI to point to the next frequency divisor in NOTES array (each divisor is 2 bytes)
add di, 2 ; Move DI to point to the next duration in DURATIONS array (each duration is 2 bytes)
loop NEXT_NOTE ; Decrement CX and loop back to NEXT_NOTE if CX != 0
ret ; Return from PLAY_MUSIC procedure
PLAY_MUSIC ENDP
;
; *****************************
;
DELAY_MS PROC NEAR
mov cx, BX
DELAY_MS_LOOP:
push cx
mov cx, 0FFFFh
DELAY_MS_INNER_LOOP:
dec cx
jnz DELAY_MS_INNER_LOOP
pop cx
dec cx
jnz DELAY_MS_LOOP
ret
DELAY_MS ENDP
;
; *****************************
;
CODE ENDS
END