I have recently built a DTMF decoder module for the RC2014 computer.
Now I want to go the opposite way, and have my RC2014 generate DTMF tones.
I own one of Ed Brindley’s excellent YM2149 Sound Cards for the RC2014, so I want to use this to generate the DTMF tones. Mine has Colin Piggot’s Quazar ZXTT card attached to match the clock speeds used in the ZX Spectrum. This gives a clock speed of 1.7734MHz, which will be important when generating the DTMF tones.
DTMF works by sending two tones for each key. One high frequency, one low frequency. These are mapped to the numbers 0 to 9, characters * and #, and the letters A to D.
1209 Hz | 1336 Hz | 1477 Hz | 1633 Hz | |
697 Hz | 1 | 1 | 3 | A |
770 Hz | 4 | 5 | 6 | B |
852 Hz | 7 | 8 | 9 | C |
941 Hz | * | 0 | # | D |
The AY-3-8910 has 3 channels of sound available, so we only need two of these to generate a DTMF tone.
We need to calculate a divider for the based on the clock speed for each tone. This is the clockspeed divided by (16 x the frequency).
So for a 697Hz tone being generated with a clock speed of 1.7734Mhz, the calculation would be 1.7734 / ( 16 * 697) = 159.
We can do the same for the other tones, which gives us the following values.
697 Hz | 159 |
770 Hz | 144 |
852 Hz | 130 |
941 Hz | 118 |
1209 Hz | 92 |
1336 Hz | 83 |
1477 Hz | 75 |
1633 Hz | 68 |
We are going to use channels A and B on the AY-3-8910 to generate our DTMF tone. So to play the DTMF tone for “1”, we need to play a 697 Hz tone on channel A, and a 1209H Hz tone on channel B. Looking at the table above, this means channel A needs the value 159, and channel B the value 92.
A simple Z80 assembly language program to play these tones would look something like this.
AY_REGISTER_PORT EQU 0xD8
AY_DATA_PORT EQU 0xD0
CHANNEL_A EQU 0x00
CHANNEL_B EQU 0x02
ld a, CHANNEL_A
out (AY_REGISTER_PORT), a
ld a, 159
out (AY_DATA_PORT), a
ld a, CHANNEL_B
out (AY_REGISTER_PORT), a
ld a, 92
out (AY_DATA_PORT), a
We can expand this and write a Z80 assembly language program to play the tones for each key once using the following code. In this example I keep the tone pairs in memory and then look up the values before playing them for a short duration then stopping the sound output before moving to the next pair.
OUTPUT DTMFAYEncoder.z80
ORG $9000
AY_REGISTER_PORT EQU 0xD8
AY_DATA_PORT EQU 0xD0
CHANNEL_A EQU 0x00
CHANNEL_B EQU 0x02
ENABLE EQU 0x07
AMPLITUDE_A EQU 0x08
AMPLITUDE_B EQU 0x09
VOLUME EQU 0x0F
; the tones we need for the AY chip
DIVIDER_697 EQU 159
DIVIDER_770 EQU 144
DIVIDER_852 EQU 130
DIVIDER_941 EQU 118
DIVIDER_1209 EQU 92
DIVIDER_1336 EQU 83
DIVIDER_1477 EQU 75
DIVIDER_1633 EQU 68
init:
; set volume on channel A
ld a, AMPLITUDE_A
out (AY_REGISTER_PORT), a
ld a, VOLUME
out (AY_DATA_PORT), a
; set volume on channel B
ld a, AMPLITUDE_B
out (AY_REGISTER_PORT), a
ld a, VOLUME
out (AY_DATA_PORT), a
; iterate over all the codes we have and play them out
ld b, tonecodelen
ld hl, tonecodes
.loop:
ld d, (hl)
call playTone
call enableAB
call delay
call stopTone
call shortdelay
inc hl
djnz .loop
; stop the tones
call stopTone
ret
; -----------------------------
; SUBROUTINES
; -----------------------------
; a short delay
delay:
push bc
ld bc, $8888
call dodelay
pop bc
ret
; an even shorter delay
shortdelay:
push bc
ld bc, $1500
call dodelay
pop bc
ret
; dodelay does the actual delaying
; pass the delay length in BC
dodelay:
push de
push af
.loop:
dec bc
ld a, b
or c
jr nz, .loop
pop af
pop de
ret
; enable channels A and B on the AY chip
enableAB:
push af
ld a, ENABLE
out (AY_REGISTER_PORT), a
ld a, 0xFC
out (AY_DATA_PORT), a
pop af
ret
; stop tones playing on the AY chip
stopTone:
push af
ld a, ENABLE
out (AY_REGISTER_PORT), a
ld a, 0x3F ; disable all channels
out (AY_DATA_PORT), a
pop af
ret
; play a tone
; pass the ASCII character for the tone in D
playTone:
push af
push bc
push de
call getTone
ld a, CHANNEL_A
out (AY_REGISTER_PORT), a
ld a, b
out (AY_DATA_PORT), a
ld a, CHANNEL_B
out (AY_REGISTER_PORT), a
ld a, c
out (AY_DATA_PORT), a
pop de
pop bc
pop af
ret
; get the tones two tones for character in D
; return the two tones in registers BC
getTone:
push af
push hl
ld e, 0
ld hl, tonecodes
.loop:
ld a, (hl)
cp d
jr z, .gottone
inc hl
inc e
inc e
jr .loop
.gottone:
ld a, e
ld hl, tones
; we now need to add A to HL
add a, l
ld l, a
adc a, h
sub l
ld h, a
; get the first tone in B
ld b, (hl)
inc hl
; get the second tone in C
ld c, (hl)
pop hl
pop af
ret
; the tone codes in order. We use this to get
; the both tone codes from tones
tonecodes: dc '1234567890*#ABCD'
tonecodelen EQU $ - tonecodes
tones:
tone1: dc DIVIDER_697, DIVIDER_1209 ; 1
tone2: dc DIVIDER_697, DIVIDER_1336 ; 2
tone3: dc DIVIDER_697, DIVIDER_1477 ; 3
tone4: dc DIVIDER_770, DIVIDER_1209 ; 4
tone5: dc DIVIDER_770, DIVIDER_1336 ; 5
tone6: dc DIVIDER_770, DIVIDER_1477 ; 6
tone7: dc DIVIDER_852, DIVIDER_1209 ; 7
tone8: dc DIVIDER_852, DIVIDER_1336 ; 8
tone9: dc DIVIDER_852, DIVIDER_1477 ; 9
tone0: dc DIVIDER_941, DIVIDER_1336 ; 0
tonestar: dc DIVIDER_941, DIVIDER_1209 ; *
tonehash: dc DIVIDER_941, DIVIDER_1477 ; #
toneA: dc DIVIDER_697, DIVIDER_1633 ; A
toneB: dc DIVIDER_770, DIVIDER_1633 ; B
toneC: dc DIVIDER_852, DIVIDER_1633 ; C
toneD: dc DIVIDER_941, DIVIDER_1633 ; D
It’s easy to test our code as we build the DTMF decoder module. We can simply plug the output from the sound card into the DTMF decoder. We can see the decoded tones showing on the debugging LEDs.