DTMF Detector / Decoder Circuit

Note1: The FCC frowns upon you connecting devices to the telephone system (the old copper line network a.k.a. POTS). In my testing, this device is connected to a small PBX. When I’m done it will be connected to a VoIP ATA, not a copper phone line.

Note2: If you make a circuit such as this to connect to a POTS line, make sure you install the 2 specified fuses to protect the device from voltage spikes / lightning strikes.

I am working on a project where I want to press keys on my telephone and send commands via WIFI to a computer. To do that, I must be able to detect and decode the DTMF tones generated when pressing a key on the telephone handset.

(What is DTMF?)

As I started researching decoding DTMF, I found this little board does just that which I could connect to an Microcontroller Unit (MCU) quite easily. I ordered one up. This ended up coming via slow boat from Thailand. Literally. It took a good month to show up and when it did the RJ connectors are the wrong size. I assume these are for the Australian market as that is where they are sold from.

The mini-board uses an MT-8870 chip to do the decoding, so I started looking at just sourcing the chip myself. There is an Arduino library already in place, but as I continued research I found the only MT-8870 chips available are surface mount. I have very little experience in SMT and at this point in the game am better off avoiding it. I could find some old through-hole DIP MT-8870 chips on eBay.

As I contemplated the problem, it occurred to me I had once written code to emulate a 1200 baud modem using a Teensy MCU, it should be possible to decode DTMF directly on the MCU if it is fast enough.

The normal way to decode DTMF would be to use Fast Fourier Transform (FFT). FFT requires a pretty fast CPU, though. Not sure a cheap MCU would be up to it. As I continued to research, I learned of the Goertzel Algorithm. This is a specialized case of FFT which can detect individual frequencies with much less processing.

As I learned about the Goertzel algorithm, I found this very clean explanation of how it works. Unfortunately, the link to the source code is dead. After several days I finally found it here. There is also an Arduino library module of the algorithm.  These two examples gave me a good starting point for the software side.

If I was going to use an MCU to decode DTMF, I was going to need to to processes the DTMF signal properly to present it as 0-3.2 volts to the MCU’s Analog to Digital converter (ADC). Anything beyond that voltage range would destroy the MCU.

Further, I was going to have to be able to greatly attenuate the ring signal. This can be as high as 120 Volts.

Looking at the Circuit

The basis for my circuit comes from here(Marantz PMD Recorder) and here.

The full schematic of my circuit can be found as a PDF here.

Note, there is a Eagle schematic below, in Nov 3 update.

A bill of materials (BOM) can be found here.

There are 3 main parts to the circuit: the DTMF processor, the Tone Injector, and a square to sine wave converter. The square to sine wave converter is not implemented and will not be discussed.

DTMF Processor Circuit

The DTMF (and RING) signal  comes from the telephone(tip and ring). 2 fuses, F1 and F2, protect the circuit from lightning. The capacitor C1 is non-polarized and high voltage to block DC voltage.

Transformer TR1 is a normal 600:600 transformer used in telephony to isolate our circuit from the telephone network. An example of this transformer can be found here.

The values for F1, F2, C1 and TR1 are critical especially for a POTS line.

There are two ways to look at this circuit – what happens when DTMF tone occurs, and what happens when a RING signal occurs. To design it, I started by  getting the circuit to work for the DTMF tone, and then added further protection from the RING signal.

The DTMF tone starts on the phone line as a signal oscillating from 6.6 to 9.8V. When it passes thru C1 and TR1, it becomes -0.9 to +0.9V. The signal needs to be positive, so we add 3.3V to it and get 2.5 to 4.1V on the input side of C2.

C2 will block the DC component of the signal (the 3.3V we added). R2 and R3 form a voltage divider. This will center the frequency. That is, the signal crosses the X axis at 3.3V/2 or 1.65 V. At this point in the circuit, the DTMF signal can be properly read by the MCU.

The RING signal is very high voltage which will destroy the MCU. My PBX generates a -76 to +74V RING signal. D1 and D2 bleed most of the voltage off so the RING signal is between -0.7 and 7.4V on the input side of C2.

The voltage divider attenuates the RING signal a bit, but it still peaks at 4.8V – way too high. The zener diodes D3 & D4 bleed off the rest of the excess voltage. The tested voltage of the RING signal at the MCU pin is 0.9 to 2.2V.

The DTMF output signal is now MCU safe and can be connected to an ADC port on the MCU.

Tone Injector

While this post is primarily concerned with DTMF detection, I also want to notify the user  their key sequences were accepted. This is done by generating a tone with the MCU and injecting it onto the phone line using the Tone Injector circuit.

As the tone comes in, R8 and R9 create a voltage divider to attenuate the level of the tone to an appropriate value. I determined these values by first using a potentiometer and and just listening to the tone until I got a pleasing volume.

C3 simply blocks the DC voltage from where I connect this sub-circuit to the DTMF circuit.

The RING signal can make its way back from the DTMF circuit, so D5 and D6 are used to limit the voltage of any signal coming back from the DTMF circuit.

The Software Environment

For this demo, I decided to use a Teensy 3.2 MCU. For the most part, code written for an Arduino will work on a Teensy and vice versa. I have done a lot of work with Teensy’s. They are much faster than Arduinos, and I have several just laying around so I started with them.

I expect to design the final version of the project on an Arduino 33 IoT. It too is much faster than an Atmel Arduino like the Nano or Uno and it includes an integrated WIFI module.

The speed of the CPU is a major concern. The ADC is sampled 8,000 times a second. This is easy for a 72MHz Teensy. An 8MHz Arduino simply cannot sample as fast.

I did not test my code on an Arduino, but I reduced the clock speed of the Teensy to see the effects. At 24MHz, the Teensy was still sampling OK. At 16MHz it started becoming flaky. At 8MHz it did not function.

It may be possible to use this software on an 8MHz Arduino, but the sampling rate would need to be reduced. I did not experiment to see if it would work.

Since this is a new project, I downloaded and installed the latest version of the Arduino IDE (1.8.16). I then installed the latest Teensy Teensyduino (1.55) over that.

I also found I needed a few libraries which were not installed in the IDE by default. I like to keep these libraries in the code directory rather than installed into the IDE.

In these libraries are a FIFO queue handler, cppQueue, a TM1637 LED display driver, and a file of the frequencies for various musical notes.

In reviewing the few C versions of code I could find which implement the Goertzel algorithm, none did what I wanted. Because other things could be happening on the MCU besides sampling the signal, I want the sampler to run as a timed interrupt.

The Code

The code for the project can be found here.

As I mentioned, this is the beginning of a bigger project, so there is some stub code in here already for that project. The main project, sjdtmf.ino, contains a state machine which gets far enough to allow testing of the dtmf.cpp module.

I use the code::blocks editor/IDE. sjdtmf.cbp is it’s project information file.

globals.h and globals.cpp contain constants and code that could be called from any module.

Most of the pertinent code, however, can be found in the dtmf.cpp module.

dtm_init initializes the module. You pass the sample rate and block size.  This module will calculate the Goertzel constants for each of the frequencies (stored in dtm_coeffs) and then start an interrupt timer with the correct interval to sample at 8,000 samples per second.

If you change the sample rate passed to dtm_init() you will also need to calculate the appropriate timer interval. This could be done automatically, I just didn’t do so myself.

dtm_tick is the heart of the algorithm – haha. It is called once every 125 microseconds which results in an 8,000 samples/second sample rate.

First, it reads the ADC to see what voltage is coming from the DTMF processor circuit:

sample = analogRead(pin_dtmfIn);

It then uses that value to recompute Q1 and Q2 values of the Goertzel algorithm for each DTMF frequency:

for(i = 0;i < dtm_toneLen; i = i + 1) {
    Q0 = dtm_coeffs[i] * dtm_Q1[i] - dtm_Q2[i] + (float) (sample - dtm_adcCenter);
    dtm_Q2[i] = dtm_Q1[i];
    dtm_Q1[i] = Q0;
    }

Note the calculation (sample – dtm_adcCenter). This adjusts the ADC input to a proper value for the Goertzel algorithm.

If there is NO DTMF signal, the voltage to the ADC should be 1.65V (3.3V/2) and the ADC should read 512 (1023/2). Unless the resistors R2/R3 are perfectly matched, the ADC won’t quite read 512 when there is no signal. This is corrected with a proper value for dtm_adcCenter.

To determine the value for dtm_adcCenter, I monitored the ADC when there was no signal and got 505. If you cannot determine the corrected value, then using 512 is a good estimate.

Subtracting dtm_adcCenter (505) from the ADC gives values from -505 to 518 – the proper values for the algorithm.

The next chunk of code toggles a digital pin on the MCU:

if (flag)
    digitalWrite(pin_freqTest, HIGH);
else
    digitalWrite(pin_freqTest, LOW);
flag = !flag;

One of the implementations I reviewed did this and I thought it was quite clever. If I want to know exactly what the real sample rate is, I simply connect an oscilloscope or frequency counter to the MCU’s pin. Then multiply by 2 to get the sample rate. With my 72MHz Teensy I was getting exactly 8K samples / second.

Finally,

dtm_sampleCount = dtm_sampleCount + 1;                      
if (dtm_sampleCount >= dtm_blockSize) {                     
    dtm_detect();                                           
    dtm_reset();                                            
    }

This increments the sample count. If we’ve seen enough samples to fill a block, then we try to detect a key, and finally reset the algorithm to read another block of samples.

dtm_detect – If dtm_tick is the heart, then dtm_detect must be the brain of the  DTMF module. It determines which, if any, frequencies were seen, if they combine to create a key press, what the key pressed is, and then queue it for consumption by the user.

First, the magnitude for each DTMF frequency is calculated in the array magnitudes:

for(i = 0; i < dtm_toneLen; i = i + 1) {
     magnitudes[i] = sqrt(
        dtm_Q1[i] * dtm_Q1[i] +
        dtm_Q2[i] * dtm_Q2[i] -
        dtm_coeffs[i] * dtm_Q1[i] *dtm_Q2[i]
        );
    } // for

This is taken straight from the algorithm.

Next, we determine if any of the thresholds exceed an arbitrary threshold. If so a bit is set in a bitmap of frequencies:

found = false;                                              
for (i = 0; i < dtm_toneLen; i = i + 1) { 
    if (magnitudes[i] >= dtm_threshold) {
        found = true;					                    
        bitmap = bitmap | bitIx;
        }
    bitIx = bitIx << 1;
    } // for

The value for dtm_threshold was determined empirically using every handset I could find and I have a box full of old handsets! However, in my final code, this value will be stored in eeprom and be changeable by the end-user.

If any frequency signal exceeded the threshold above, we then compare the bitmap of the frequencies found to bitmaps of valid combinations of frequencies. If a match is found, we then locate the key would have been pressed to generate the frequencies seen and place it in the variable curKey.

if (found) {						                        
    for (i = 0; i < dtm_toneLen*2; i = i + 1)		        
        if (bitmap == dtm_bitmaps[i])                       
            break;
    if (i < dtm_toneLen*2)				                    
        curKey = dtm_keymap[i];				                
    else
        curKey = '\0';					                    
    }
else
    curKey = '\0';					                        

Note the usage of the boolean found here. This is a vestige of some old code and could be reduced to just checking if bitmap != 0.

The last chunk of code looks at the transition of the current and previous key pressed and determines what needs to be done. curKey/lastKey may contain the ASCII value of a key (0,1,2…) or it may contain ‘\0’ indicating no key pressed. There are 4 possibilities:

lastKey and curKey both EMPTY: There has been no change, so we do nothing.

lastKey EMPTY and curKey VALID: a new key was pressed. Save it in lastKey and start the timer.

lastkey VALID and curKey EMPTY: the key was released. Calculate duration of key press. If the key was pressed long enough (> dtm_durationTimeout), then consider this a valid keypress and enqueue it into the FIFO queue. Finally, set lastKey to EMPTY.

lastKey == curKey: Same key is still being pressed, do nothing.

If code makes it to this point, there was an unexpected condition. Reset everything.

dtm_read / dtm_available / dtm_peek – When keys are pressed they are detected and placed into the FIFO queue by a timer-based interrupt. This leaves the main program free to do other things. It can then look to see if there are any keys in the queue to process. The read/available/peek functions provided by the DTMF module function like the SERIAL library, allowing the main program to access the keys.

For example, in the module sjdtmf.ino and in the idle state (case st_idle:) , I look to see if a key has been pressed, and if so change the state to st_keyPressed so the key can be processed:

    case st_idle:

        if (swDebounce(pin_switch)) {                       // was reset switch pressed?
            state = st_swPressed;
            break;
            }

        if (dtm_available() > 0) {                          // was a key pressed
            state = st_keyPressed;
            break;
            }

        break; //st_idle

Demo Video

I put together a short video of my breadboarded version of this DTMF project. It can be seen here:

Nov 2, 2021 Update

Since posting blog I have created a proper prototype:

While doing this I found some problems:

The capacitor for C1 in the BOM is redonkulously large. The next BOM will have an alternate that is reasonably sized.

The cleaned up circuit was getting a “ringing” on the line every time I pressed a key. Finally tracked it down to the LED display I’m using. I put a 100uF capacitor between the Vcc going into the LED and ground. This resolved that problem.

Once I have the problems 100% resolved, I’ll update the BOM and schematic.

Nov 3 Update

Created Eagle CAD project for the schematic.

A PDF of Eagle CAD schematic for the project can be found by clicking on this picture:

The Eagle Project can be found here.

Note that the board has not been laid out since this version is just a breadboard prototype for me.

Change notes:

F3 and D7 have been added to protect the board from excessive current or a reversed power supply.

Added zero ohm resisters in parallel to all fuses. These can be used as jumpers if the fuses are not installed.

C4 was added which keeps the signal clean when the LED is being updated.

I attempted to compile my code on the Nano 33 IoT I expect to use and it immediately failed because intervalTimer.h is a teensy-only module. There is a special timer module for the Nano 33 which I will implement. It has been so long since I used a normal Arduino, I’m not sure what library one would use to replace intervalTimer, but I’m sure one must exist.

Nov 9 Update: Nano 33 IoT Notes

The obvious changes to replace the Teensy with a Nano 33 IoT were to implement a timer library (SAMDTimer), and a fast analogRead function, analogReadFast(). It would have been nice if the Nano would just start working after those changes, but that would be too simple! It would not properly sense the DTMF tones.

The main issue is the difference in clock speed. The Nano is 48MHz while the Teensy is 72MHz. Though the Teensy worked properly when I set it to use 48Mhz, I couldn’t get the Nano to work. The frequency on pin 4, indicating the actual sample rate, had a lot of jitter on it.

It finally occurred to me to time the dtm_tick() function. NOTE: because this is an interrupt called function, micros and millis do not work in it (took me a couple of trys to realized that). Instead, I had to manually call dtm_tick to test its performance.

I found dtm_tick() took 116 microsecs to complete when dtm_detect() was NOT called. The sample rate is once every 125 microseconds, so clearly the MCU was not up to the task. (Running the same test on the Teensy, it too only 24 microsecs to execute dtm_tick).

I then started reducing the sample rate to see how low I could get it. I got it down to 4K samples / sec (vs the original of 8K) with dtm_tick() now being called once every 250 microseconds.

Sensing the DTMF now worked fine, but the tone generator sounded pretty cruddy because even with dtm_tick() taking only half the cycles, it is still too much to get a clean tone. So I modified the program such than when I need to generate a tone, I turn off dtm_tick – the user is expecting a response at that momement anyway.

That worked great. The tone is very clean. Even on the teensy, it was noisy.

Now that I have DTMF working properly on the Nano, I can begin learning how to get it to communicate via WiFi to another system I wish to control via DTMF!

Advertisement
This entry was posted in c-arduino, c-electronics, c-tinys and tagged . Bookmark the permalink.

2 Responses to DTMF Detector / Decoder Circuit

  1. Pingback: VeeCad / Stripboard Revisited | Big Dan the Blogging Man

  2. Pingback: Arduino 33 IoT and Raspberry Pi Pico Eval | Big Dan the Blogging Man

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.