Dual-tone multi-frequency, commonly known as DTMF, is a technology used in telecoms systems that uses audible tones to represent keys on a touch-tone phone.
You have probably heard this will navigating a menu on your phone, such as “Press 1 for …, Press 2 for….”, etc.
While browsing on AliExpressing for some parts for another project, it suggested I may like the MT8870 module (affiliate link). This is a small 5V DTMF decoder module with digital outputs. I thought this would be ideal for an RC2014 project.
We’re interested in 7 pins on this module. +5v and GND are for powering the module. STQ, D0, D1, D2, and D3 are for the decoded tones. The STQ line goes high when a decoded tone is present. The actual code is a 4 bit value stored on D0, D1, D2, and D3.
We can pass this output to a 74HCT245 transceiver which we can enable using a 74HCT688 to decode an input address on the RC2014, along with a 74HCT32 to check the RD and IORQ lines.
I’m using an RC2014 Classic 2 for this project, so inout port 1 is free, but I know this isn’t necessarily the case on more advanced RC2014s. Because of this I’ve put an 8 inline switch module on the 74HCT688 so I change the address if necessary.
I am also adding some LEDs on the input lines so I can see the incoming data visually. The MT8870 has some SMD LEDs on board to do this, but the built-in pin headers I want to use will mean these face the RC2014 module PCB and so won’t be visible. I’ve gone for green for the STQ line, blue for the data lines.

The PCB looks like this.

Decoding the input using Z80 assembly language
I want to write a small Z80 assembly language program to print out any incoming DTMF tones to the terminal.
I’m using the SCM monitor program on my RC2014 Classic 2 computer to load and execute my assembly language program. This gives a handy print character to the terminal routine I can use. I pass the ASCII character I want to print in register A, the value $02 in register C, and execute RST $30. I can wrap this into my own subroutine called printchar. This gives me the flexibility to swap to another output method such as an LCD screen in future.
; pass the character to print in register A
; This uses SCM to print back to the terminal.
printchar:
push bc
ld c, $02 ; SCM API function $02 - print character
rst $30
pop bc
ret
I will need to debounce any input I read from the decoder module. This is because the value will be present for longer than the first read I make. To do this I will need to check if STQ is high and compare this to the last value read. If it’s the same, I know I have already handled this value. However, when STQ goes low, I need to reset the last value read to a dummy value else it won’t detect multiple presses of the same button.
Once I have the data, I can map it to the correct character. I do this by having the valid decoded characters in order, and then simply the incoming character value as an offset to this list.
My final code looks like this.
OUTPUT DTMFDecoder.z80
ORG $9000
; Input data from the DTMF encoder is
; Bits 0-3 = number data (D0, D1, D2, D3)
; Bit 4 = new data flag (STQ)
; Bits 5-7 = unused.
; The input port where we can find the DTMF decoder
DTMFDecoder EQU 1
; masks for handling the input data.
NEW_INPUT_MASK EQU %00010000
DATA_INPUT_MASK EQU %00001111
; Flags to show if the data is new or not.
DataRead EQU $FF
DataClear EQU $00
; Register C holds the data read flag. This is set to DataRead when we've
; read a byte, and set to DataClear when we are safe to read a byte.
ld c, DataClear
mainloop:
; read the data from the input port DTMFDecoder and store this in register B.
in a, (DTMFDecoder)
ld b, a
; see if the new data flag bit has been set. If it has go to datapresent.
and NEW_INPUT_MASK
jr nz, datapresent
; clear Register C as we have no data to read, and loop again.
ld c, DataClear
jr mainloop
datapresent:
; we have data present so check if register C is set to DataRead. If it
; is we know we have already handled that input so can loop again.
ld a, DataRead
cp c
jr z, mainloop
; set register C to DataRead to flag we have handled this input.
ld c, DataRead
; get the input into register A and mask out the button press bit.
ld a, b
and DATA_INPUT_MASK
; decode the character from the input and find it in the charmap.
ld hl, charmap
; add the value of a to hl
add a, l
ld l, a
adc a, h
sub l
ld h, a
; get the character from the charmap and print it.
ld a, (hl)
call printchar
; now loop again.
jr mainloop
; pass the character to print in register A
; This uses SCM to print back to the terminal.
printchar:
push bc
ld c, $02 ; SCM API function $02 - print character
rst $30
pop bc
ret
; The DTMF characters in order.
charmap:
db 'D1234567890*#ABC'