Skip to content

Instantly share code, notes, and snippets.

@edgar-bonet
Created March 1, 2016 12:00
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save edgar-bonet/0b03735d70366bc05fc6 to your computer and use it in GitHub Desktop.
Save edgar-bonet/0b03735d70366bc05fc6 to your computer and use it in GitHub Desktop.
Homodyne detection of a 1 kHz signal
/*
* homodyne.ino: Homodyne detection of a 1 kHz signal.
*
* This program continuously samples analog input 0 and uses an homodyne
* detection scheme to identify a signal at 1 kHz (+/- 24 Hz @ -3dB).
*
* The analog-to-digital converter is set to "free running mode" and
* takes one sample every 104 us. The samples are multiplied by two
* generated signals at 1 kHz (the "local oscillator"), in quadrature to
* one another. The products are then low-pass filtered with a time
* constant of 64 sample periods (6.656 ms), which gives the (I, Q)
* signals with a 24 Hz bandwidth. Finally, the signal power is computed
* as I^2 + Q^2.
*
* The program is intended for an Arduino Uno, and is likely to work on
* any AVR-based Arduino having an ADC and clocked at 16 MHz.
*
* For a detailed explanation, see
* http://arduino.stackexchange.com/a/21175
*
* Copyright (c) 2016 Edgar Bonet Orozco.
* Released under the terms of the MIT license:
* https://opensource.org/licenses/MIT
*/
#include <util/atomic.h>
// Analog input to use, should be between 0 and 5.
const uint8_t analog_in = 0;
// The frequency we want to detect, in Hz.
const float SIGNAL_FREQ = 1000.0;
// Timing bits.
const float SAMPLING_FREQ = F_CPU / (128 * 13.0); // 9.615 kHz
const long PHASE_INC = round(SIGNAL_FREQ / SAMPLING_FREQ * (1L << 16));
const int LOG_TAU = 6; // tau = 64 / SAMPLING_FREQ = 6.656 ms
// Set the ADC to free running mode.
static void configure_adc()
{
ADMUX = _BV(REFS0) // ref = AVCC
| _BV(ADLAR) // left adjust result
| analog_in; // input channel
ADCSRB = 0; // free running mode
ADCSRA = _BV(ADEN) // enable
| _BV(ADSC) // start conversion
| _BV(ADATE) // auto trigger enable
| _BV(ADIF) // clear interrupt flag
| _BV(ADIE) // interrupt enable
| 7; // prescaler = 128
}
// Demodulated (I, Q) amplitudes.
volatile int16_t signal_I, signal_Q;
// Interrupt handler called each time an ADC reading is ready.
ISR(ADC_vect)
{
// Read the ADC and convert to signed number.
int8_t sample = ADCH - 128;
// Update the phase of the local oscillator.
static uint16_t phase;
phase += PHASE_INC;
// Multiply the sample by square waves in quadrature.
int8_t x = sample;
if (((phase>>8) + 0x00) & 0x80) x = -1 - x;
int8_t y = sample;
if (((phase>>8) + 0x40) & 0x80) y = -1 - y;
// First order low-pass filter.
signal_I += x - (signal_I >> LOG_TAU);
signal_Q += y - (signal_Q >> LOG_TAU);
}
/* Return a power reading. */
static uint16_t get_power_reading()
{
int16_t I, Q;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
I = signal_I;
Q = signal_Q;
}
return sq((int8_t)(I >> LOG_TAU)) + sq((int8_t)(Q >> LOG_TAU));
}
/***********************************************************************
* Example usage.
*/
void setup()
{
configure_adc();
Serial.begin(9600);
}
void loop()
{
// Print a power reading every 500 ms.
static const uint16_t print_period = 500;
static uint16_t last_print;
uint16_t now = millis();
if (now - last_print >= print_period) {
Serial.println(get_power_reading());
last_print += print_period;
}
}
@edgar-bonet
Copy link
Author

Simply turn on the LED whenever the power reading is higher than a threshold. The optimal threshold value has to be determined experimentally.

@Lars-Dekkers
Copy link

Okay thanks, I finally got it to work. Can you also maybe tell me how to make it detect a wider range of frequency say for example from 3khz to 4khz? Because the whistle is not a pure frequency

@edgar-bonet
Copy link
Author

edgar-bonet commented Oct 6, 2019

@Lars-Dekkers: You already asked this question on arduino.stackexchange, and you got an answer there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment