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:

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