EEVblog® Electronics Community Forum
Electronics => Microcontrollers => Topic started by: viper on October 27, 2025, 09:42:03 pm
-
Using an ESP32-S3, which has some ability with floats but literally everything I have learned says to find work arounds to avoid FP which possible, and FP has several potential hang ups like rounding and such.
I'm not saying I am firm on one side or the other, and possible I will have no choice but to use some FP as I do some tougher DSP work, but sounds like a possible considerable time penalty for using it.
There is nothing in my project that really needs both high precision and speed. It would be one or the other.
-
... as I do some tougher DSP work, but sounds like a possible considerable time penalty for using it.
In that case, you really should do some studying to get to know the differences between those two approaches. And it's not an easily answered topic. On an 8-bitter 32 bit integer calculations can be slower then floating point operations, there are lots of microcontrollers with built in floating point instructions. "real DSP's" have all sorts of extras to speed up specific types of calculations, etc.
For many "hobby level" projects, you can just choose a uC family, and then stick with it for all projects, but if you or your projects outgrow that, then there are a gazillion of choices these days. I've been happy for 15+ years myself with the (previous) Atmel AVR's, (Overall CPU usage <0.1%) until I wanted to experiment with TFT LCD's. Then I suddenly wanted an uC that could shove data around at least 10x faster then the old AVR's.
I suspect that you will get different and maybe even contradicting advise in this thread, and it will not be of much use without a lot of (self) study.
-
Do a test, write a simple DSP example and see how fast you can run it. Then compare the speed of that to the expected complexity of your actual design to get a rough idea how fast it will run.
Using floats on an 8 bit mcu running at 20mhz works fine to do a fair number of float calculations, more than you think, mixed in with general integer stuff.
So a 32bit ESP32-S3 running at 240mhz will be orders of magnitude better than that.
I would try floats first.
-
Actually, I just checked, the ESP32-S3 has a single precision floating point unit, it's not doing it in software, so speed wont be an issue.
Obviously it does depend how accurate you need your floating calcs to be.
-
Do a test, write a simple DSP example and see how fast you can run it. Then compare the speed of that to the expected complexity of your actual design to get a rough idea how fast it will run.
Using floats on an 8 bit mcu running at 20mhz works fine to do a fair number of float calculations, more than you think, mixed in with general integer stuff.
So a 32bit ESP32-S3 running at 240mhz will be orders of magnitude better than that.
I would try floats first.
and since the ESP32-S3 afaict only has a single precision FPU, keep that in mind when programming in C, unless you need doubles make sure to specify constants as floats and use the float math functions, the default is doubles (I think the AVR GCC cheats and defaults to floats)
-
I'm not actually sure, if you do double precision float math on a MCU with only a single precision FPU, does it use the FPU to help with double precision calculations or does it fall back to pure int math and not use the FPU at all? One would hope it can use it to accelerate the double precision cals somehow
-
Indeed, float vs integer can very much depend which operations are being carried out.
Even simple 32-bit integers don't need to be incredibly slow on small micros.. as long as the calculations stay inside the addition/subtraction domain. Unfortunately, often times some multiplication gets involved as thats how high dynamic range is quickly utilized.
Similarly, floats shine in the logarithmic domain, like calculating the reciprocal.. All you need to do is multiply the FP exponent by *-1, which can be done with a 2x 8-bit integer subtractions. In integer domain, you need to do long tail division or beg your MCU has at least an integer divider which is faster than half a dozen cycles.
Or square root. A very very rough approximation would be to simply divide by the exponent by 2 and don't even bother correcting the mantissa:
https://bits.stephan-brumme.com/squareRoot.html
And there is the famous fast inverse square root example..
Unfortunately, some people get principal about using floats on micro's. A while ago I got nearly harassed on Reddit /r/embedded for using floats on an AVR temperature sensor. It was running at 1 sample per second. There were no power constraints. The floating point library took half the codespace of the ATMEGA328P, which means it fits and runs. It may be a code smell for such application, but not all projects need to be able to go to the moon, and for a hobby project with minimal time investment it put down all the checkmarks.
I'm not actually sure, if you do double precision float math on a MCU with only a single precision FPU, does it use the FPU to help with double precision calculations or does it fall back to pure int math and not use the FPU at all? One would hope it can use it to accelerate the double precision cals somehow
It would be converting between `double` FP64 and `float` FP32 a lot.
This is a trap with floats. E.g. cos() and sin() are the double FP64 variants in <math.h>, while cosf and sinf are the float FP32 variants. Take a good look at which routines/instructions get used for which operations, as one or the other may incur a lot of useless FP32/FP64 conversions.
E.g. https://godbolt.org/z/sno11Eebc
-
Unfortunately, some people get principal about using floats on micro's. A while ago I got nearly harassed on Reddit /r/embedded for using floats on an AVR temperature sensor. It was running at 1 sample per second. There were no power constraints. The floating point library took half the codespace of the ATMEGA328P, which means it fits and runs. It may be a code smell for such application, but not all projects need to be able to go to the moon, and for a hobby project with minimal time investment it put down all the checkmarks.
Agreed, I've been quite surprised with how many float calcs I can do in one of my ATMega64A products without causing any slowdown to the control loops or i/o loops.
(Most of the internal/external calc loops are either 50hz or 25hz).
The main loop has a float calc doing some sin/cos stuff, most of the IO has some float scaling/cal stuff going on per sample and there's a function doing exponential tracking of a current to PWM curve using floats.
-
Using an ESP32-S3, which has some ability with floats but literally everything I have learned says to find work arounds to avoid FP which possible, and FP has several potential hang ups like rounding and such.
Ignore the dogmas. Use floating point if you want unless you run out of processor power. It is simple like that. Being aware of rounding and resolution is necessary though. OTOH, code becomes so much easier to follow if variables represent SI units (like Volt, Ampere, meters, Hertz, rpm, Pascal, etc, etc).
-
Even when/if you do run out of processing power due to floats. It's going to be one specific block of float code that's using all the processing power and causing all the slowdown issues.
So best to just convert that block to integer math if needed. Not the whole thing.
-
The most intense part here is the DSP filter that will run a calculation. It could be around 1M multiplies to do every 5 sec or so. I am also realizing the heart of these equations for the DSP rely on floating math, so I may have little choice, or at least I could prob do some conversions.
I appreciate and agree this is hard to really 'answer' without a specific use case. Was just trying to get ideas in my head as I work if I should be thinking of ways to use integers for this and that.
-
Many years ago I implemented a DSP algorithm which started life as code which uses floating point. I optimised it using fixed point until it was fast enough. Not more.
-
Using an ESP32-S3, which has some ability with floats but literally everything I have learned says to find work arounds to avoid FP which possible, and FP has several potential hang ups like rounding and such.
I'm not saying I am firm on one side or the other, and possible I will have no choice but to use some FP as I do some tougher DSP work, but sounds like a possible considerable time penalty for using it.
There is nothing in my project that really needs both high precision and speed. It would be one or the other.
As has been widely explained above, if you have a processor with a floating point unit, floating point math is going to be about as fast as fixed point if not faster.
So for general applications it's usually easier to just use floating point if you have an FPU available.
There are specific DSP algorithms that only work well with fixed point, and others that have desirable properties when used with fixed point. For instance, some algorithms rely on exact cancellation of rounding errors, or specific overflow behavior. Because the truncation of floating point is data dependent, your filter parameters can vary slightly with your input data. In a lot of cases this variation is totally inconsequential, and the better average dynamic range is only a benefit. In some cases it matters quite a bit.
Mostly if you need to use fixed point for one of these reasons, you will know it. If you are just learning and not trying to absolutely maximize your dynamic range, don't get too hung up on this. Just use floating point unless you have a specific reason not to. Even if you are going to want fixed point eventually usually it makes sense to first prototype with floating point until you are happy with the general operation. Then do the optimized design which might involve transitioning to fixed point.
-
The choice of number representation really depends on your hardware and project requirements. Floating point has lower precision and resolution for small signals at the same bit width, but it handles large dynamic ranges easily. Fixed-point is more efficient and faster on hardware without an FPU, but you may need more bits to cover the same range.
On FPGA, using floating-point is generally challenging due to implementation complexity, whereas on modern CPU/MCU, floating-point is typically supported in hardware. Therefore, the choice largely depends on the available resources.
If you convert a filter with float coefficients to fixed-point, the filter parameters will change slightly due to rounding, so this should be considered during the filter design stage.
-
literally everything I have learned says to find work arounds to avoid FP which possible
As others have said, that "rule" is obsolete dogma these days. It used to be necessary with 8-bit microcontrollers running around 1MHz that had to do the calculation in software. Larger/modern microcontrollers (like ESP32) have floating point units (FPU) to offload the work to, and clock rates around 80MHz.
I remember having to work something out in real time and just for giggles I broke the rule and threw a floating point calc into ISR. It was an ESP32 and my ISR only used up about 5% of the available execution time (toggle a GPIO to profile the time spent). We're spoilt these days!
-
Afaik the naked fp single precision multiply on the ESP32 takes 4cycles, with some overhead say 8cycles.
At 80MHz clock that is say 100ns. One million mults is then around 0.1sec..
-
Afaik the naked fp single precision multiply on the ESP32 takes 4cycles, with some overhead say 8cycles.
At 80MHz clock that is say 100ns. One million mults is then around 0.1sec..
Though technically correct, you should consider that you need to copy the data to and back out from the FPU.
So it will be at minimum 2 copies, 4 cycles, 1 copy. Especially if you can create cache-misses you can get significant slowdowns that are difficult to track down.
In the end using fixed point the time is lost doing the checks around and the additional scalings.
floating point is a bit easier, but less deterministic.
You may still need to run a few isfinites().
I'd recommend to create your algorithm and do benchmark on-target.
Fixed point will require more planning, but it could still be more performant or deterministic.
If you are constantly changing the alrogrithm and there is "plenty" of time, I'd use the floating point. It's just easier to work with.
-
#define PI 3.14159274101f /// 0x40490fdb
#define Pi 3.14159250259f /// 0x40490fda
#define Pi_d 3.1415926535897932384626433832795
-
I'm new to fixed point, and currently working on a home telephone project that requires DTMF detection using the Goertzel algorithm. I'd like to get it to work with an Atmega328P (a Nano), and of course AVR is just 8-bit with no FPU. However, I think it does have signed and unsigned 8-bit multiply instructions. The limiting factor on this project is code size, not ram. So I will be trying both a floating point version and a fixed point version to see how much difference there is.
The Goertzel algorithm is essentially a single-frequency FFT calculation. It uses cos(), division, sqrt(), and lots of multiplication. However, the cos() and division can be avoided by pre-computing the eight DTMF frequency coefficients and hard-coding them. And sqrt() is only required to determine the magnitude of the signal to compare to the pre-set threshold, and magnitude squared would work just as well, compared to a squared threshold.
So I'm not sure there will be any significant difference. Speed isn't really important in this case. I guess multiplying two floats requires more code that multiplying two ints, but they are going to be functions in either case, plus the fixed point version will also need a >>14 loop. So it may come down to just a few bytes difference. Based on what I've done so far, floats are easier to deal with. With fixed point you have to worry about overruns.
-
I'm new to fixed point, and currently working on a home telephone project that requires DTMF detection using the Goertzel algorithm. I'd like to get it to work with an Atmega328P (a Nano), and of course AVR is just 8-bit with no FPU. However, I think it does have signed and unsigned 8-bit multiply instructions. The limiting factor on this project is code size, not ram. So I will be trying both a floating point version and a fixed point version to see how much difference there is.
Side node: Goertzel is useless for DTMF decoding. A better approach is to use two band filters and use a frequency counter (zero crossing detection) to measure the frequencies. This will give you a much more accurate and better defined DTMF decoder. Make sure the sampling rate is >=16kHz (either sampled directly or use oversampling before filtering).
-
Well so far Goertzel is working fine for DTMF. It appears to be what most people use for DTMF in the Arduino world.
-
It's widely used for DTMF decoding across the board and is proven to work well. Claiming it's "useless" is just nonsense.
-
Well so far Goertzel is working fine for DTMF. It appears to be what most people use for DTMF in the Arduino world.
That could be but early in my career one of my mentors (a very smart and experienced engineer) pointed out the flaws in using Goertzel for DTMF decoding (sensitivity and selectivity which means you can't meet the DTMF specs) so we choose to go for the frequency counter route. The fact that many people use a method doesn't mean it is the right one. For the project I worked on at that time, the DTMF decoding needed to work very reliably in commercial call center applications.
-
I have no test data other than specs for this ESP unit which shows 238MOPS for FPU multiply? 240mhz. That is 4 cycles, single precision, but I hear you guys on considering other operations. I'm just trying to get an idea if this is a few mS or a few minutes. :scared:
There is also SIMD, which I have absolutely no experience with. Basically get the engine running decent, then tune to the moon later.
-
Re GCC and single floats, I don't know about "all GCC" but for sure arm32 GCC uses doubles, which are then (IME trig measurements) some 20x slower than single floats, on a single float hardware CPU.
-
As Jeoren3 wrote above for a real world app you have to consider the "environment" the mult lives in.
You have to feed the the the data in, make the multiply, write the result somewhere, increment some pointers or loop counters and/or to make other operations with the result for each mult.
So when you say "I need to make 1million mults within 5 secs" it says little about the reality.
Let us consider for example there is an overhead of 100 cycles per one mult (4 cycles).
So 104 cycles at 240MHz is 433ns and 1million of such events is 0.43secs.
Also look at the FPU spec of the ESP, it could be there is a MAC operation as well (mult and accumulate in one shot, like X = X + A*B) which is usually much faster than mults and additions made separately (when your math needs it).
-
The Goertzel algorithm is essentially a single-frequency FFT calculation. It uses cos(), division, sqrt(), and lots of multiplication. However, the cos() and division can be avoided by pre-computing the eight DTMF frequency coefficients and hard-coding them. And sqrt() is only required to determine the magnitude of the signal to compare to the pre-set threshold, and magnitude squared would work just as well, compared to a squared threshold.
I remember, some time ago (more than 25 years ago ::) ), I implemented a DTMF detector using a simplified correlation approach with only 8-bit integer operations — basically XOR, ADD, and small lookup tables for precomputed sin/cos values with 1-bit samples (8 samples per byte). It worked surprisingly well even over noisy phone lines.
It was first implemented and debugged on a Z80, later I ported it to a PIC16F84. The entire code for detecting DTMF sequences and displaying them on an I2C LCD, including the display and keypad drivers — fit within the PIC16F84 1K program memory.
So, there is actually no need for sin(), cos(), sqrt(), or even multiplication for good DTMF detection. :)
-
If you could make that available, or a link to the process you used, that would be very helpful.
-
If you could make that available, or a link to the process you used, that would be very helpful.
sounds like some variation of the Walsh–Hadamard transform, basically a Fourier transform with square waves instead of sin/cos
-
The code probably still exists somewhere, but it’s stored on a CD-R, and I haven’t had a CD drive for years, so there’s no way to read it now. As far as I remember, the algorithm worked roughly as follows (I might have forgotten some details): the input signal from a comparator was fed into a GPIO pin, which sampled it at a fixed rate. Lookup tables with 1-bit samples for the detectable frequencies were generated using the same sample rate. The input bits were XORed with the corresponding sin/cos table values and accumulated. The number of accumulators therefore equaled twice the number of detectable frequencies. All processing was done in real time. After an accumulation period (about 50 ms, if I recall correctly), the algorithm evaluated the accumulated values — summing the squares of the sin and cos components (I don’t remember the exact evaluation operation to get magnitude, but everything was implemented entirely using ADD instruction), checking against a detection threshold, and selecting the two frequencies with the highest levels. The detected frequency pair was then mapped to the corresponding DTMF symbol.
-
The code probably still exists somewhere, but it’s stored on a CD-R, and I haven’t had a CD drive for years, so there’s no way to read it now. As far as I remember, the algorithm worked roughly as follows (I might have forgotten some details): the input signal from a comparator was fed into a GPIO pin, which sampled it at a fixed rate. Lookup tables with 1-bit samples for the detectable frequencies were generated using the same sample rate. The input bits were XORed with the corresponding sin/cos table values and accumulated. The number of accumulators therefore equaled twice the number of detectable frequencies. All processing was done in real time. After an accumulation period (about 50 ms, if I recall correctly), the algorithm evaluated the accumulated values — summing the squares of the sin and cos components (I don’t remember the exact evaluation operation to get magnitude, but everything was implemented entirely using ADD instruction), checking against a detection threshold, and selecting the two frequencies with the highest levels. The detected frequency pair was then mapped to the corresponding DTMF symbol.
Sounds like you had an IQ mixer, mixed the incoming waveform with local oscillator of an interesting frequency, calculated the magnitude of the output, and integrated.
-
Not really related to the float/fixed topic, but here is my experience with a related problem.
I wanted to obtain the amplitude of a narrow-band signal: basically looking for echo peaks from an ultrasonic transducer. The 'proper' way to do this is to use either the Hilbert transform to generate the analytic signal, and thence the envelope, or to use a quadrature bandpass filter pair to create what is effectively the analytic signal over a limited range of frequencies. The first technique works best in the FFT domain, using relatively short records, the second is more suited to continuous operation with long-term signals.
There was a lot of low-frequency interference (basically amplifier/switch settling) that had to be filtered out to isolate the actual echo.
My fastest 'quick and dirty' technique was based on the observation that the sampling rate gave me about four samples per cycle of the transducer signal. What I did was to sum the absolute value of the differences between four consecutive pairs of samples (five samples in all). Dividing by two gives an estimate of the signal amplitude at frequencies around one-fourth of the sample rate, at the instant of the middle sample. You then move along to the next set of 5 samples, dropping the oldest and picking up the next.
This technique gives bandpass filtering and envelope calculation at the expense of eight fixed-point add/subtracts, four ANDs (to clear the sign bit) and a shift - and even the shift wasn't necessary for my needs. Address calculations probably require as many instructions as the actual computation (as they so often do).
To be fair, this is only a rough approximation to the true signal envelope, but for finding the echo peak, it was perfectly adequate.
-
The most intense part here is the DSP filter that will run a calculation. It could be around 1M multiplies to do every 5 sec or so. I am also realizing the heart of these equations for the DSP rely on floating math, so I may have little choice, or at least I could prob do some conversions.
I appreciate and agree this is hard to really 'answer' without a specific use case. Was just trying to get ideas in my head as I work if I should be thinking of ways to use integers for this and that.
find out if you have a 64 bit result register for multiplication if you are using 32 bit values. I got a bit burnt on my M0+ MCU that claimed single cycle multiplication. I followed advice that using 32 bit values in my code would be faster as the processor would not be going to extra work to "ignore" upper bits. The result was that at 48 MHz it was taking 5+µs to do a multiplication (250 cycles). If you are using 16 bit values then you will be fine.
I don't know anything about DSP stuff but once I started working with an M4F based MCU I noticed a lot of talk about saturating math for DSP use. I don't think an ARM M4F is meant for actual DSP but they have included specific instructions that you don't normally expect to help. So might be worth looking at what the processor can actually do.
-
@radiolistener, Perplexity knows of no publicly available AVR code using the Walsh–Hadamard transform to detect DTMF (if that's what you used). But in general, if you used a comparator to convert the incoming audio to square waves, how did you deal with the audio level? How did you prevent normal speech from triggering DTMF detections? Well, maybe we're moving a bit off-topic with this. I did get my fixed point Goertzel to work, including two >>14 operations. Now I need to see how the float version compares.
-
Presumably not possible in the OP's scenario, but has anyone used the DTMF encoder and decoder chips that are available? Any comments on their performance, ease of use, etc?
I ask because I'm considering DTMF for a low bit rate project I have in mind.
-
Perplexity knows of no publicly available AVR code using the Walsh–Hadamard transform to detect DTMF (if that's what you used). But in general, if you used a comparator to convert the incoming audio to square waves, how did you deal with the audio level? How did you prevent normal speech from triggering DTMF detections?
The algorithm operated on a binary, discretized representation of the signal — essentially it responded to the timing of zero crossings rather than to amplitude. So the signal level wasn’t really relevant. In practice it was used for remote control of a telephone answering machine, and I never noticed it being triggered by normal speech, although I suppose certain sounds could have caused false detections if you tried hard enough.
As for the algorithm itself, I originally found it by disassembling the firmware of that answering machine. I’m not sure what the underlying math was — ChatGPT suggested it might be a “1-bit correlation detector with a Bayesian decision criterion under unknown a priori frequency probabilities”.
I remember that the manual for the answering machine mentioned that DTMF recognition was based on a “new decision-making algorithm under conditions of a priori uncertainty”. There was no other information about the algorithm provided in the documentation.
-
As for the algorithm itself, I originally found it by disassembling the firmware of that answering machine. I’m not sure what the underlying math was — ChatGPT suggested it might be a “1-bit correlation detector with a Bayesian decision criterion under unknown a priori frequency probabilities”
That answer response is content-free word salad.
Not surprising, given the source.
-
That answer response is content-free word salad.
Not surprising, given the source.
I tried feeding your criticism back to ChatGPT, and it replied:
“The phrasing does sound overcomplicated. In simpler terms, it was just a 1-bit correlation detector: the signal from the comparator was compared against precomputed sine/cosine patterns for each frequency, and the two with the highest correlation were selected as the detected DTMF tones.”
I also tried to mention the similarity to an XOR-based phase detector, and it responded:
“In more general terms, this approach can be regarded as a form of nonlinear correlation or binary phase detection.”
That still doesn’t fully clarify the underlying math, though. Perhaps someone familiar with the theory behind such methods could shed some light on the mathematical basis. I used this algorithm quite a bit back in the day for frequency detection on low-end processors, and it performed surprisingly well. Intuitively, I see a connection to the Fourier transform and correlation, but I’m not sure how exactly this approach would be formally classified.
-
Presumably not possible in the OP's scenario, but has anyone used the DTMF encoder and decoder chips that are available? Any comments on their performance, ease of use, etc?
Back in the old days I have used these chips. Typically from Mitel but those are probably long obsolete.
-
That answer response is content-free word salad.
Not surprising, given the source.
I tried feeding your criticism back to ChatGPT, and it replied:
“The phrasing does sound overcomplicated. In simpler terms, it was just a 1-bit correlation detector: the signal from the comparator was compared against precomputed sine/cosine patterns for each frequency, and the two with the highest correlation were selected as the detected DTMF tones.”
I also tried to mention the similarity to an XOR-based phase detector, and it responded:
“In more general terms, this approach can be regarded as a form of nonlinear correlation or binary phase detection.”
That still doesn’t fully clarify the underlying math, though. Perhaps someone familiar with the theory behind such methods could shed some light on the mathematical basis. I used this algorithm quite a bit back in the day for frequency detection on low-end processors, and it performed surprisingly well. Intuitively, I see a connection to the Fourier transform and correlation, but I’m not sure how exactly this approach would be formally classified.
Keep in feeding criticism back to it, and it will generate more answers. Rinse and repeat.
I wonder if it would eventually blunder upon a respectable answer, and whether anyone would be able to determine that that specific response was accurate and useful.
-
The Cortex-M85 has the new Helium floating point and fixed point extension. An ARM engineer wrote a great reference guide about how it works and how to use it. https://armkeil.blob.core.windows.net/developer/Files/pdf/ebook/arm-helium-technology-mve.pdf
-
The code probably still exists somewhere, but it’s stored on a CD-R, and I haven’t had a CD drive for years, so there’s no way to read it now. As far as I remember, the algorithm worked roughly as follows (I might have forgotten some details): the input signal from a comparator was fed into a GPIO pin, which sampled it at a fixed rate. Lookup tables with 1-bit samples for the detectable frequencies were generated using the same sample rate. The input bits were XORed with the corresponding sin/cos table values and accumulated. The number of accumulators therefore equaled twice the number of detectable frequencies. All processing was done in real time. After an accumulation period (about 50 ms, if I recall correctly), the algorithm evaluated the accumulated values — summing the squares of the sin and cos components (I don’t remember the exact evaluation operation to get magnitude, but everything was implemented entirely using ADD instruction), checking against a detection threshold, and selecting the two frequencies with the highest levels. The detected frequency pair was then mapped to the corresponding DTMF symbol.
Sounds like you had an IQ mixer, mixed the incoming waveform with local oscillator of an interesting frequency, calculated the magnitude of the output, and integrated.
Yep. XOR function works as a mixer and having two mixers fed with two LO frequencies 90 degrees out of phase gets you an IQ demodulator.
-
The code probably still exists somewhere, but it’s stored on a CD-R, and I haven’t had a CD drive for years, so there’s no way to read it now. As far as I remember, the algorithm worked roughly as follows (I might have forgotten some details): the input signal from a comparator was fed into a GPIO pin, which sampled it at a fixed rate. Lookup tables with 1-bit samples for the detectable frequencies were generated using the same sample rate. The input bits were XORed with the corresponding sin/cos table values and accumulated. The number of accumulators therefore equaled twice the number of detectable frequencies. All processing was done in real time. After an accumulation period (about 50 ms, if I recall correctly), the algorithm evaluated the accumulated values — summing the squares of the sin and cos components (I don’t remember the exact evaluation operation to get magnitude, but everything was implemented entirely using ADD instruction), checking against a detection threshold, and selecting the two frequencies with the highest levels. The detected frequency pair was then mapped to the corresponding DTMF symbol.
Sounds like you had an IQ mixer, mixed the incoming waveform with local oscillator of an interesting frequency, calculated the magnitude of the output, and integrated.
Yep. XOR function works as a mixer and having two mixers fed with two LO frequencies 90 degrees out of phase gets you an IQ demodulator.
I wonder how much time and prompting it would take to get an LLM to emit that >:D
-
The code probably still exists somewhere, but it’s stored on a CD-R, and I haven’t had a CD drive for years, so there’s no way to read it now. As far as I remember, the algorithm worked roughly as follows (I might have forgotten some details): the input signal from a comparator was fed into a GPIO pin, which sampled it at a fixed rate. Lookup tables with 1-bit samples for the detectable frequencies were generated using the same sample rate. The input bits were XORed with the corresponding sin/cos table values and accumulated. The number of accumulators therefore equaled twice the number of detectable frequencies. All processing was done in real time. After an accumulation period (about 50 ms, if I recall correctly), the algorithm evaluated the accumulated values — summing the squares of the sin and cos components (I don’t remember the exact evaluation operation to get magnitude, but everything was implemented entirely using ADD instruction), checking against a detection threshold, and selecting the two frequencies with the highest levels. The detected frequency pair was then mapped to the corresponding DTMF symbol.
Sounds like you had an IQ mixer, mixed the incoming waveform with local oscillator of an interesting frequency, calculated the magnitude of the output, and integrated.
Yep. XOR function works as a mixer and having two mixers fed with two LO frequencies 90 degrees out of phase gets you an IQ demodulator.
sorta related, a Tayloe mixer
-
As others have said, Goertzel has been used a lot for DTMF detection, and it's adequate when used properly. So, I agree that claiming it's "useless" is, at best, odd.
Now, it's not a magical tool and will work better with a "clean" input signal, ideally with a reasonably "normalized" amplitude, so some amount of pre-filtering and a basic AGC may be required, unless those properties are guaranteed with the source signal. So, yes, depending on the source signal, some other approaches may turn out more effective.
Not too surprised "cargo cult" engineering may rear its head, as in "I've been bitten once, so I'm never gonna use this again".
-
The question of AGC is an interesting one. In playing with Goertzel on a Nano, I've found that with 5V ADC values, you pretty much need the signal to fill up most of that 5V range (centered on 2.5V) for Goertzel to work well. And even after picking the highest level row and column frequencies, both of their levels must also be greater than some predetermined threshold. If the threshold is too low, there would be no way to prevent voice audio from generating false positives. So it seems that DTMF depends on the tones being relatively loud. I'm not sure AGC works for that.
My project is for a home VOIP system such as Ooma and MagicJack. I'll need to detect DTMF both from another extension phone on the same line and from someone at the other end of the conversation. Not surprisingly, the levels produced by those sources are very different. But it's still just two different levels. Well, there is a third non-DTMF signal I need to detect, which is dial tone. So three different levels. But I can switch between three gain levels depending on what I'm listening for, so it doesn't have to be automatic gain control, it just has to be gain control. At least that's my plan.
-
As others have said, Goertzel has been used a lot for DTMF detection, and it's adequate when used properly. So, I agree that claiming it's "useless" is, at best, odd.
It is not. Look at the relatively wide frequency and detection signal level specs of DTMF and how you want to meet those specs with Goertzel. You simply can't because the frequency response of Goertzel is like a parabole. At frequencies which are 'off' but still in spec, you will need signals which are a lot stronger. In addition to that, Goertzel will be sensitive for signals outside the frequency range if they are strong enough. Actually, using Goertzel is the definition of cargo cult engineering. People read a paper written by a few students using Goertzel for DTMF decoding and then think using Goertzel is the defacto standard for DTMF decoding without considering which specs actually need to be met.
-
I attempted to reconstruct the frequency detection method I mentioned earlier. The following Octave code demonstrates it.
For simplicity, it uses the CCITT No.5 multi-frequency signaling scheme (https://en.wikipedia.org/wiki/Signaling_System_No._5), which operates similarly to DTMF but with a different set of frequencies.
% Binary frequency detector simulation with multiple tones
Fs = 48000; % sample rate
pause_len = 0.1; % 100 ms pause
tone_len = 0.1; % 100 ms per tone
freqs = [700 900 1100 1300 1500 1700]; % CCITT5 - Signaling System No. 5 frequencies
% --- Generate composite test signal ---
sig = [];
% signle tones with pause
for k = 1:length(freqs)
% 100 ms pause before first tone only
if k == 1
sig = [sig, zeros(1, round(pause_len*Fs))];
end
% Tone segment
t = (0:1/Fs:tone_len - 1/Fs);
sig = [sig, sin(2*pi*freqs(k)*t)];
% Pause after each tone
sig = [sig, zeros(1, round(pause_len*Fs))];
end
% two tones
for k = 2:length(freqs)
t = (0:1/Fs:tone_len - 1/Fs);
tone = (sin(2*pi*freqs(k)*t) + sin(2*pi*freqs(k-1)*t)) / 2;
sig = [sig, tone];
end
t_sig = (0:length(sig)-1)/Fs;
% --- Comparator simulation ---
bin_sig = uint8(sig > 0) & 1;
% --- Precompute binary sin/cos references ---
win = round(0.01*Fs); % 10 ms window
for k = 1:length(freqs)
f = freqs(k);
ref_sin{k} = uint8(sin(2*pi*f*(0:win-1)/Fs) > 0) & 1; % square wave sin/cos ref table
ref_cos{k} = uint8(cos(2*pi*f*(0:win-1)/Fs) > 0) & 1;
end
% --- Sliding detection ---
step = round(win/4);
nsteps = floor((length(bin_sig)-win)/step);
det = zeros(length(freqs), nsteps);
for i = 1:nsteps
pos = (i-1)*step + 1;
seg = bin_sig(pos:pos+win-1);
for k = 1:length(freqs)
sxor = bitxor(seg, ref_sin{k});
cxor = bitxor(seg, ref_cos{k});
sxor = 1 - 2*sxor; % 0/1 => -1/+1
cxor = 1 - 2*cxor;
xs = sum(sxor); % count bit matches
xc = sum(cxor);
det(k,i) = min((xs^2 + xc^2)/256, 256);
end
end
t_det = (0:(nsteps-1))*step/Fs;
% --- Plot result ---
figure(1); clf;
subplot(2,1,1);
plot(t_sig, sig);
xlabel('Time (s)');
ylabel('Signal');
title('Test signal');
grid on; grid minor;
subplot(2,1,2);
plot(t_det, det');
xlabel('Time (s)');
ylabel('Detection amplitude');
legend(arrayfun(@(f) sprintf('%d Hz', f), freqs, 'UniformOutput', false));
title('Frequency detector output');
grid on; grid minor;
Note: min((xs^2 + xc^2)/256, 256) can be replaced with 256 byte lookup table and ADD, so there is no need for multiplication operation, just XOR and signed ADD
Sample rate can be decreased to 12 kHz, so the reference sin/cos table for 10 ms window will need 120*2 bits = 30 bytes per frequency (180 bytes for CCITT5 or 240 bytes for DTMF).
For optimization, you can accumulate eight bits of the input signal and then perform XOR and ADD operations for all eight samples in a single step.
-
Thanks for posting the code. I'll look it over.
-
Also you can use Chebyshev distance as a simple magnitude approximation without MUL and SQRT operations, so there is no need for 256 byte LUT:
for i = 1:nsteps
pos = (i-1)*step + 1;
seg = bin_sig(pos:pos+win-1);
for k = 1:length(freqs)
sxor = bitxor(seg, ref_sin{k});
cxor = bitxor(seg, ref_cos{k});
sxor = 1 - 2*sxor; % 0/1 => -1/+1
cxor = 1 - 2*cxor;
xs = sum(sxor); % count bit matches
xc = sum(cxor);
%mag = sqrt(xs^2 + xc^2); % Classic Magnitude
%mag = xs^2 + xc^2; % Squared Magnitude
%mag = abs(xs) + abs(xc); % Manhattan norm (L1)
mag = max(abs(xs), abs(xc)); % Chebyshev norm (L∞)
det(k,i) = min(mag, 256);
end
end
This allows the DTMF detector to operate entirely with integer arithmetic, eliminating the need of ADC, amplitude scaling, floating-point calculations, and costly MUL, SQRT, or trigonometric operations, relying solely on integer XOR and ADD. :)
On 32-bit MCU you can process 32 samples at once with just 3-4 processor instructions.
Result:
-
Note that \$0.67913\lvert x \rvert + 0.67913 \lvert y \rvert + 0.28130 \bigl \lvert \lvert x \rvert - \lvert y \rvert \bigr \rvert\$ is within 4% of \$\sqrt{x^2 + y^2}\$, so for 0 ≤ y ≤ x ≤ 255, (174*(x + y) + 72*(x - y) + 80) / 256 is within ±10 (4%) of sqrt(x^2 + y^2). (27% of all possible cases are exact.)
For general 0 ≤ x ≤ 255, 0 ≤ y ≤ 255, you can use(87*(x + y) + 36*(max(x, y) - min(x, y)) + 40) / 128, using signed 16-bit or unsigned 15-bit integer operations.
-
Note that \$0.67913\lvert x \rvert + 0.67913 \lvert y \rvert + 0.28130 \bigl \lvert \lvert x \rvert - \lvert y \rvert \bigr \rvert\$ is within 4% of \$\sqrt{x^2 + y^2}\$, so for 0 ≤ y ≤ x ≤ 255, (174*(x + y) + 72*(x - y) + 80) / 256 is within ±10 (4%) of sqrt(x^2 + y^2). (27% of all possible cases are exact.)
For general 0 ≤ x ≤ 255, 0 ≤ y ≤ 255, you can use(87*(x + y) + 36*(max(x, y) - min(x, y)) + 40) / 128, using signed 16-bit or unsigned 15-bit integer operations.
there are variations, https://dspguru.com/dsp/tricks/magnitude-estimator/
-
On the topic of Pythagorean approximations, here's another one ...
[attachimg=1 width=400]
Here x is the larger of the two values.
From this video:
https://youtu.be/NWBEA2ECX-A