Generating DTMF Tones Using An AY-3-8910 On A RC2014

RC2014 with DTMF Decoder module

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 Hz1336 Hz1477 Hz1633 Hz
697 Hz113A
770 Hz456B
852 Hz789C
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 Hz159
770 Hz144
852 Hz130
941 Hz118
1209 Hz92
1336 Hz83
1477 Hz75
1633 Hz68

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.