Author Topic: Homebrew Lock-In Amplifier  (Read 10335 times)

0 Members and 1 Guest are viewing this topic.

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #100 on: May 09, 2024, 08:53:47 pm »
The measurements are calibrated (approximately by measuring the full scale resistor)

Measurements of a short cable:
Code: [Select]
SAMPLE_FREQUENCY = 9090.00 Hz
MEASURE_FREQUENCY = 568.13 Hz
SAMPLE_TIME = 1.00 s
435.02 mOhm R  0.10 mOhm Z_L
434.25 mOhm R  0.15 mOhm Z_C 1813534.62 uFarads
433.48 mOhm R  0.31 mOhm Z_L
435.25 mOhm R  0.36 mOhm Z_C 787925.37 uFarads
432.50 mOhm R  1.28 mOhm Z_L
432.45 mOhm R  0.28 mOhm Z_C 989267.43 uFarads
433.10 mOhm R  0.09 mOhm Z_L
435.26 mOhm R  0.11 mOhm Z_L
433.27 mOhm R  1.90 mOhm Z_C 147481.29 uFarads
434.63 mOhm R  2.31 mOhm Z_C 121495.60 uFarads
434.15 mOhm R  0.37 mOhm Z_L
432.38 mOhm R  0.65 mOhm Z_L
437.47 mOhm R  2.32 mOhm Z_C 120932.24 uFarads
434.59 mOhm R  0.40 mOhm Z_L
434.68 mOhm R  1.04 mOhm Z_L
432.84 mOhm R  0.78 mOhm Z_C 360973.96 uFarads
431.79 mOhm R  1.26 mOhm Z_L
431.90 mOhm R  0.33 mOhm Z_L
434.69 mOhm R  2.79 mOhm Z_C 100255.73 uFarads
434.34 mOhm R  0.31 mOhm Z_L
435.86 mOhm R  0.51 mOhm Z_C 549197.68 uFarads
432.65 mOhm R  0.18 mOhm Z_C 1518544.00 uFarads
434.65 mOhm R  1.28 mOhm Z_C 218317.84 uFarads
433.30 mOhm R  1.06 mOhm Z_L
434.93 mOhm R  1.63 mOhm Z_C 171490.93 uFarads
434.82 mOhm R  0.21 mOhm Z_C 1323788.62 uFarads
434.83 mOhm R  0.37 mOhm Z_C 761006.56 uFarads
436.03 mOhm R  0.84 mOhm Z_L
431.63 mOhm R  0.05 mOhm Z_L
433.32 mOhm R  0.76 mOhm Z_L
434.65 mOhm R  0.39 mOhm Z_L
432.88 mOhm R  1.15 mOhm Z_L
432.27 mOhm R  0.55 mOhm Z_L
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #101 on: May 09, 2024, 08:57:45 pm »
The calculations have turned out to be much faster than I expected. Upgrading the two accumulators takes about 5us. Considering that the measurement cycle is greater than 100us, there is still plenty of time left over.

Extract of the program:
Code: [Select]
    const int SIN_INTEGER[] = {9, 26, 39, 46, 46, 39, 26, 9 };

    adc_acc_inphase += adc_value * SIN_INTEGER[0];   // 2.5us
    adc_acc_quadrature += adc_value * SIN_INTEGER[3];  // 2.5 us

 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #102 on: May 09, 2024, 09:21:40 pm »
Another problem that I have now realized is that, in order to best calculate the quadrature signal, it is necessary to have a number of samples per period that are a multiple of 4. Back to the 16 samples.

The number should be even, for perfect rejection of DC offset, but it does not need to be a multiple of 4. If it is not a multiple of 4, then you need separate cos and sin tables. If it is a multiple of 4, you can fetch the cos and sin values from the same lookup table.



I had forgotten something regarding the multiplicator optimization: The optimization should not be limited to an ordinary sine wave, but it should consider a complex sine wave (i.e. both, cos and sin components together). Here is an update.

Code: [Select]

nsamples = 16;

x = repmat(exp(-1j*[0:nsamples-1]/nsamples*2*pi)',1,127);
y = zeros(nsamples,127);
for i=1:127
  % quantize to 2*i+1 levels
  y(:,i) = floor(0.5+0.5i+x(:,i)*i)/i;
end

Y = zeros(nsamples,127);
for i=1:127
  Y(:,i) = fft(y(:,i))/nsamples;
end

P = abs(Y).**2;
Pfund = P(2,:);
harmidx = [ 1 3:nsamples ];
Pharm = sum(P(harmidx,:));
Pspur = max(P(harmidx,:));

plot(10*log10(Pharm./Pfund),";THD;");
hold on
plot(10*log10(Pspur./Pfund),";SFDR;");
title(sprintf("%d samples/period",nsamples))
grid on
hold off
ylabel("dB")
xlim([20 127])
ylim([-65 -30])

« Last Edit: May 09, 2024, 09:59:12 pm by gf »
 

Offline gnuarm

  • Super Contributor
  • ***
  • Posts: 2246
  • Country: pr
Re: Homebrew Lock-In Amplifier
« Reply #103 on: May 09, 2024, 11:02:21 pm »
Yes, much better. The problem is that the multiplication should not be greater than 65535, which is the maximum of an integer, to simplify the arithmetic. That's why I was limiting the multiplier to a maximum value of 64.

Another problem that I have now realized is that, in order to best calculate the quadrature signal, it is necessary to have a number of samples per period that are a multiple of 4.
Back to the 16 samples.

I don't know what "16 samples" means. 

Quote
Anyway, these calculations and macros will come in handy if later I try to make the amplifier with a more powerful and faster micro. Thank you.

This is why I typically don't use MCUs.  I much prefer fpgas.  They provide much more powerful resources and typically don't need to be upgraded, unless you are doing crazy complicated stuff.  It takes a bit of effort to learn to use them effectively, but in the end, they beat MCUs at nearly everything. 
Rick C.  --  Puerto Rico is not a country... It's part of the USA
  - Get 1,000 miles of free Supercharging
  - Tesla referral code - https://ts.la/richard11209
 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #104 on: May 10, 2024, 10:34:08 am »
I already have a first version, probably with some bugs.

Why do you make adc_value global and volatile? It is only used in the ISR. Just make it a local variable in the ISR, and the compiler can renounce several unnecessary load/store instructions and keep adc_value in a register.

Your SIN_INTEGER table is declared int[]. This leads to a 16x16 -> 16 multiplication. Why don't you declare it int8_t[] if you want the compiler to generate cheaper 16x8 -> 16 multiplication?

There is also something wrong with the types in the multiplication. Adc_value is declared unsigned, and the SIN_INTEGER table entries are (signed) int. In this case, C's type conversion rules lead to an unsigned multiplication, but you want it to be signed. Therefore, declare adc_value with type int16_t (i.e signed).

I don't know if the 10-bit value in ADCW is MSB or LSB aligned, and I also can't see how ADCW is declared. If it is unsigned, then either do
Code: [Select]
int16_t adc_value = (ADCW >> 6) - 512;or
Code: [Select]
int16_t adc_value = ADCW - 512;depending on whether ADCW returns a 0...65535 value (with the lower 6 bits zero) or a 0...1023 value.

If ADCW is signed, then either do
Code: [Select]
int16_t adc_value = ADCW >> 6;or
Code: [Select]
int16_t adc_value = ADCW;depending on whether ADCW returns a -32768...32767 value (with the lower 6 bits zero) or a -512...511 value.



Extract of the program:
Code: [Select]
    const int SIN_INTEGER[] = {9, 26, 39, 46, 46, 39, 26, 9 };

    adc_acc_inphase += adc_value * SIN_INTEGER[0];   // 2.5us
    adc_acc_quadrature += adc_value * SIN_INTEGER[3];  // 2.5 us

I would not use switch statements with a case for  each of the 16 samples per period.
If you change samples per period, you need to change the switch statement as well.
And the two switch statements also cost additional cycles.
I would rather spend 12 bytes more RAM and do

Code: [Select]
// 1+1/4 periods
const int8_t SIN_INTEGER[16+16/4] = {9,26,39,46,46,39,26,9,-9,-26,-39,-46,-46,-39,-26,-9,9,26,39,46};

Code: [Select]
ISR(TIMER0_COMPA_vect)
{
  ...
  int16_t adc_value = ...; // see above
  // use a temp variable to prevent 2x load, since level_state_old was declared volatile
  uint8_t idx = level_state_old;
  adc_acc_inphase += adc_value * SIN_INTEGER[idx];
  adc_acc_quadrature += adc_value * SIN_INTEGER[idx+SAMPLES_PER_PERIOD/4];
  // note, that's the point which requires SAMPLES_PER_PERIOD % 4 == 0
  ...
}



Quote
The excitation signal is still square,

Couldn't you increase C5 significantly, in order to get at least a little anti-aliasing filtering in front of the ADC?
I mean something in the order of C5 = 1 / (2 * PI * f * R8), where f is the excitation signal frequency.
[ Sure, that introduces a phase shift, but it can be calibrated out. ]



One more suggestion: For portable code, don't rely on the sizes of short, int, unsigned, long, long long, or the signedness of char, which may be different on different platforms/compilers. Better include stdint.h and use int8_t, uint8_t, int16_t, uint16_t, int32_t, etc. in order to declare variables with a well-defined size and signedness.
« Last Edit: May 10, 2024, 12:58:48 pm by gf »
 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #105 on: May 10, 2024, 12:17:37 pm »
I had forgotten something regarding the multiplicator optimization: The optimization should not be limited to an ordinary sine wave, but it should consider a complex sine wave (i.e. both, cos and sin components together). Here is an update.

According to the updated optimization, the best multiplier < 64 seems to be 55 (for 16 samples per period).
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #106 on: May 10, 2024, 01:55:06 pm »
I solved a problem on the breadboard with the connection between the amplifier and the ADC, which produced a lot of noise.
I have also changed the data types and tested that they work correctly for larger values of the coefficients. The only problem is that now the accumulator instructions take 10us to execute. As it is still a short time I will leave it like that.

Program:
Code: [Select]
/*
   Version 4.1 (10/05/2024)

   Copyright 2024 Picuino

   Permission is hereby granted, free of charge, to any person obtaining a copy
   of this software and associated documentation files (the "Software"), to deal
   in the Software without restriction, including without limitation the rights
   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   copies of the Software, and to permit persons to whom the Software is
   furnished to do so, subject to the following conditions:

   The above copyright notice and this permission notice shall be included
   in all copies or substantial portions of the Software.

   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
   IN THE SOFTWARE.
*/
#include <stdint.h>

#define PIN_SIGNAL_OUT 3
#define PIN_DEBUG_OUT 5
#define PIN_ANALOG  A6
#define PIN_ANALOG_MUX 6

#define PIN_SCK  13
#define PIN_SDI  12
#define PIN_CNV  10

#define TIMER2_PERIOD  220

#define CLK_BOARD  16000000
#define UART_BAUDS  115200
#define MEASURE_TIME  1
#define TIMER0_PERIOD (TIMER2_PERIOD - 1)
#define TIMER0_FREQ  (CLK_BOARD / ((TIMER0_PERIOD + 1) * 8))
#define SAMPLES_PER_MEASURE (16 * (long) ((MEASURE_TIME) * (TIMER0_FREQ) / 16))

const int16_t SIN_INTEGER[] = {9, 26, 39, 46, 46, 39, 26, 9, -9, -26, -39, -46, -46, -39, -26, -9 };
const int16_t COS_INTEGER[] = {46, 39, 26, 9, -9, -26, -39, -46, -46, -39, -26, -9, 9, 26, 39, 46 };

const float BOARD_CALIBRATION = 0.3150 / (SAMPLES_PER_MEASURE);  // Converts measure to milliohms

volatile int32_t adc_acc_inphase;
volatile int32_t adc_acc_quadrature;
volatile int16_t adc_samples;
volatile uint8_t adc_measure_end;
volatile uint8_t level_state;
volatile uint8_t level_state_old;


float resistance_inphase;
float resistance_quadrature;


void setup() {
  Serial.begin(UART_BAUDS);

  Serial.println();
  Serial.print("SAMPLE_FREQUENCY = ");
  Serial.print(1.0 * TIMER0_FREQ);
  Serial.println(" Hz");

  Serial.print("MEASURE_FREQUENCY = ");
  Serial.print(TIMER0_FREQ / 16.0);
  Serial.println(" Hz");

  Serial.print("SAMPLE_TIME = ");
  Serial.print(1.0 * SAMPLES_PER_MEASURE / TIMER0_FREQ);
  Serial.println(" s");

  // Set up output reference signal pin
  pinMode(PIN_SIGNAL_OUT, OUTPUT);
  pinMode(PIN_DEBUG_OUT, OUTPUT);

  // Set up peripherals
  timer0_setup();
  timer2_setup();
  timer_synchronize();
  adc_setup();
  measure_init();
  while(adc_measure_end == 0);
  measure_init();
}


void loop() {
  // Main Loop
  while (1) {
    if (adc_measure_end == 1) {
      resistance_inphase = -adc_acc_inphase;
      resistance_quadrature = -adc_acc_quadrature;
      resistance_inphase *= BOARD_CALIBRATION;
      resistance_quadrature *= BOARD_CALIBRATION;
      print_values(resistance_inphase, resistance_quadrature);
      measure_init();
    }
  }
}


void print_values(float resistance_inphase, float resistance_quadrature) {
  Serial.print(resistance_inphase, 2);
  Serial.print("\tmOhm R  \t");
 
  if (resistance_quadrature > 0) {
    Serial.print(resistance_quadrature, 2);
    Serial.print("\tmOhm Z_L \t");
    if (resistance_quadrature > 5.0) {
      Serial.print(-resistance_quadrature * 1000.0 / ((TIMER0_FREQ / 16.0) * 2.0 * 3.1415927));
      Serial.println("\tuHenrys");
    }
    else {
      Serial.println();
    }
  }
  else {
    Serial.print(-resistance_quadrature, 2);
    Serial.print("\tmOhm Z_C \t");
    if (resistance_quadrature < -5.0) {
      Serial.print(1000000000.0 / (-resistance_quadrature * (TIMER0_FREQ / 16.0) * 2.0 * 3.1415927));
      Serial.println("\tuFarads");
    }
    else {
      Serial.println();
    }
  }
}

void adc_setup(void) {
  analogRead(PIN_ANALOG);
  cli(); // Stop interrupts

  ADMUX = (1 << 6) |
          (0 << ADLAR) |
          (PIN_ANALOG_MUX << 0);
  ADCSRA = (1 << ADEN) |
           (0 << ADSC) |
           (0 << ADATE) |
           (0 << ADIE) |
           (0b111);  // Division factor
  ADCSRB = 0x00;

  sei(); // Allow interrupts
}


void measure_init(void) {
  delayMicroseconds(1000);
  cli();
  adc_acc_inphase = 0;
  adc_acc_quadrature = 0;
  level_state = 0;
  level_state_old = 0;
  adc_samples = 0;
  ADCW = 0;
  sei();

  while ((PIND & (1 << PIN_SIGNAL_OUT)) != 0);
  while ((PIND & (1 << PIN_SIGNAL_OUT)) == 0);
  adc_measure_end = 0;
}


void timer0_setup(void) {
  cli(); // Stop interrupts

  // set compare match register
  TCCR0A = (0 << 6) | // OOM0A. 0=OC0A disconnected. 1=Toggle OC0A on compare match (p.84)
           (0 << 4) | // COM0B. 0=OC0B disconnected. 1=Toggle OC0B on compare match (p.85)
           (2 << 0);  // WGM0.  PWM mode. 1=phase correct 2=CTC  (p.86)
  TCCR0B = (0 << 7) | // FOC0A.
           (0 << 6) | // FOC0B.
           (0 << 3) | // WGM02.
           (2 << 0);  // CLOCK source.
  OCR0A = TIMER0_PERIOD;
  OCR0B = TIMER0_PERIOD / 2;
  TIMSK0 = (0 << 2) | // OCIE0B. Match B Interrupt Enable
           (1 << 1) | // OCIE0A. Match A Interrupt Enable
           (0 << 0);  // TOIE0. Overflow Interrupt Enable
  TIFR0 = 0;
  TCNT0 = 0; // Initialize Timer0 counter

  sei(); // Allow interrupts
}


void timer2_setup(void) {
  cli(); // Stop interrupts

  TCCR2A = (1 << 6) | // OOM2A. 0=OC2A disconnected. 1=Toggle OC2A on compare match (p.128)
           (2 << 4) | // COM2B. 2=Clear OC2B on compare match (p.129)
           (1 << 0);  // WGM2.  PWM mode. 1=phase correct   (p.130)
  TCCR2B = (0 << 7) | // FOC2A.
           (0 << 6) | // FOC2B.
           (1 << 3) | // WGM22.
           (4 << 0);  // CLOCK source.
  OCR2A = TIMER2_PERIOD;
  OCR2B = TIMER2_PERIOD / 2;
  TIMSK2 = (0 << 2) | // OCIE2B. Match B Interrupt Enable
           (0 << 1) | // OCIE2A. Match A Interrupt Enable
           (0 << 0);  // TOIE2. Overflow Interrupt Enable
  TIFR2 = 0;
  TCNT2 = 0; // Initialize Timer2 counter

  sei(); // Allow interrupts
}


void timer_synchronize(void) {
  cli(); // Stop interrupts

  while ((PIND & (1 << PIN_SIGNAL_OUT)) != 0);
  while ((PIND & (1 << PIN_SIGNAL_OUT)) == 0);

  GTCCR = (1 << TSM) | (1 << PSRASY) | (1 << PSRSYNC); // halt all timers
  TCNT0 = TIMER0_PERIOD / 2 + 4; // Initialize Timer0 counter
  GTCCR = 0; // release all timers

  sei(); // Allow interrupts
}


// Timer0 interrupt handler
ISR(TIMER0_COMPA_vect) {
  int16_t adc_value;

  if (adc_measure_end == 0) {
    debug_pin_pulse();

    // ADC Start Conversion
    ADCSRA |= (1 << ADSC);

    // Read last conversion
    adc_value = ADCW;

    // Accumulate values (10us)
    adc_acc_inphase += (int32_t) adc_value * SIN_INTEGER[level_state_old];
    adc_acc_quadrature += (int32_t) adc_value * COS_INTEGER[level_state_old];

    // Update next state
    level_state_old = level_state;
    level_state++;
    level_state &= 0x0F;

    adc_samples++;

    if (adc_samples > SAMPLES_PER_MEASURE) {
      adc_measure_end = 1;
    }
  }
}


// Timer2 interrupt handler
ISR(TIMER2_COMPA_vect) {

}


void debug_pin_pulse(void) {
  PORTD |= (1 << PIN_DEBUG_OUT);
  delayMicroseconds(4);
  PORTD &= ~(1 << PIN_DEBUG_OUT);
}



Output measuring a cable:
Code: [Select]
SAMPLE_FREQUENCY = 9090.00 Hz
MEASURE_FREQUENCY = 568.13 Hz
SAMPLE_TIME = 1.00 s
325.35 mOhm R  1.03 mOhm Z_C
325.32 mOhm R  0.90 mOhm Z_C
325.17 mOhm R  0.84 mOhm Z_C
325.30 mOhm R  1.01 mOhm Z_C
325.15 mOhm R  0.89 mOhm Z_C
325.19 mOhm R  0.91 mOhm Z_C
325.02 mOhm R  0.96 mOhm Z_C
324.94 mOhm R  0.84 mOhm Z_C
324.90 mOhm R  1.00 mOhm Z_C
324.90 mOhm R  0.90 mOhm Z_C
324.84 mOhm R  0.98 mOhm Z_C
324.95 mOhm R  0.96 mOhm Z_C
324.75 mOhm R  0.84 mOhm Z_C
324.87 mOhm R  0.89 mOhm Z_C
324.80 mOhm R  0.90 mOhm Z_C
324.60 mOhm R  0.99 mOhm Z_C
324.45 mOhm R  0.95 mOhm Z_C
324.60 mOhm R  1.02 mOhm Z_C
324.42 mOhm R  0.93 mOhm Z_C
324.38 mOhm R  0.93 mOhm Z_C
324.16 mOhm R  1.07 mOhm Z_C
324.36 mOhm R  0.81 mOhm Z_C
324.38 mOhm R  1.05 mOhm Z_C
324.14 mOhm R  0.84 mOhm Z_C
324.10 mOhm R  0.92 mOhm Z_C
324.07 mOhm R  0.97 mOhm Z_C
324.05 mOhm R  0.91 mOhm Z_C
324.16 mOhm R  0.95 mOhm Z_C
324.02 mOhm R  0.90 mOhm Z_C
324.10 mOhm R  0.92 mOhm Z_C
324.08 mOhm R  0.95 mOhm Z_C
324.11 mOhm R  0.90 mOhm Z_C
323.94 mOhm R  0.97 mOhm Z_C
323.96 mOhm R  1.01 mOhm Z_C
323.92 mOhm R  0.95 mOhm Z_C
323.89 mOhm R  0.89 mOhm Z_C
323.90 mOhm R  0.87 mOhm Z_C
323.93 mOhm R  0.85 mOhm Z_C
323.93 mOhm R  0.95 mOhm Z_C
323.85 mOhm R  0.94 mOhm Z_C
323.89 mOhm R  0.91 mOhm Z_C
323.94 mOhm R  0.86 mOhm Z_C
323.75 mOhm R  0.87 mOhm Z_C
323.76 mOhm R  0.85 mOhm Z_C
323.92 mOhm R  1.00 mOhm Z_C
323.75 mOhm R  0.92 mOhm Z_C
323.95 mOhm R  1.03 mOhm Z_C
323.86 mOhm R  0.81 mOhm Z_C
323.77 mOhm R  0.93 mOhm Z_C
323.73 mOhm R  0.96 mOhm Z_C
323.64 mOhm R  1.09 mOhm Z_C
323.74 mOhm R  0.85 mOhm Z_C
323.66 mOhm R  0.97 mOhm Z_C
323.76 mOhm R  0.96 mOhm Z_C
323.79 mOhm R  0.99 mOhm Z_C
323.74 mOhm R  0.88 mOhm Z_C
323.73 mOhm R  0.92 mOhm Z_C
323.48 mOhm R  0.92 mOhm Z_C
323.74 mOhm R  0.93 mOhm Z_C
323.71 mOhm R  0.93 mOhm Z_C
323.69 mOhm R  0.92 mOhm Z_C
323.49 mOhm R  0.95 mOhm Z_C
323.66 mOhm R  0.86 mOhm Z_C
323.54 mOhm R  0.97 mOhm Z_C
323.58 mOhm R  0.97 mOhm Z_C
323.45 mOhm R  0.99 mOhm Z_C
323.52 mOhm R  1.00 mOhm Z_C
323.52 mOhm R  1.06 mOhm Z_C
323.56 mOhm R  1.02 mOhm Z_C
323.40 mOhm R  1.01 mOhm Z_C
323.22 mOhm R  0.84 mOhm Z_C
322.69 mOhm R  1.00 mOhm Z_C
323.39 mOhm R  0.91 mOhm Z_C
323.92 mOhm R  0.89 mOhm Z_C
323.84 mOhm R  0.82 mOhm Z_C
323.77 mOhm R  0.85 mOhm Z_C
323.81 mOhm R  0.80 mOhm Z_C
323.69 mOhm R  0.87 mOhm Z_C
323.70 mOhm R  0.71 mOhm Z_C
323.66 mOhm R  0.95 mOhm Z_C
323.72 mOhm R  1.00 mOhm Z_C
323.49 mOhm R  0.86 mOhm Z_C
323.70 mOhm R  0.99 mOhm Z_C
323.64 mOhm R  1.03 mOhm Z_C
323.74 mOhm R  0.97 mOhm Z_C
323.68 mOhm R  0.86 mOhm Z_C
323.74 mOhm R  0.91 mOhm Z_C
323.64 mOhm R  0.97 mOhm Z_C
323.64 mOhm R  0.91 mOhm Z_C
323.57 mOhm R  0.88 mOhm Z_C
323.42 mOhm R  0.86 mOhm Z_C
323.63 mOhm R  0.79 mOhm Z_C
323.61 mOhm R  0.93 mOhm Z_C
323.64 mOhm R  0.90 mOhm Z_C
323.54 mOhm R  0.85 mOhm Z_C
323.73 mOhm R  0.91 mOhm Z_C
323.51 mOhm R  0.92 mOhm Z_C
323.67 mOhm R  0.77 mOhm Z_C
323.65 mOhm R  0.93 mOhm Z_C
323.56 mOhm R  0.86 mOhm Z_C
323.64 mOhm R  1.04 mOhm Z_C
323.64 mOhm R  0.83 mOhm Z_C
323.69 mOhm R  0.82 mOhm Z_C
323.71 mOhm R  0.73 mOhm Z_C
323.64 mOhm R  0.90 mOhm Z_C
323.62 mOhm R  1.00 mOhm Z_C
323.72 mOhm R  0.84 mOhm Z_C
323.71 mOhm R  1.04 mOhm Z_C
323.70 mOhm R  0.97 mOhm Z_C
323.64 mOhm R  0.96 mOhm Z_C
323.98 mOhm R  0.93 mOhm Z_C
323.91 mOhm R  1.00 mOhm Z_C
323.86 mOhm R  1.06 mOhm Z_C
323.69 mOhm R  0.88 mOhm Z_C
323.65 mOhm R  0.91 mOhm Z_C
323.63 mOhm R  0.97 mOhm Z_C
323.67 mOhm R  0.90 mOhm Z_C
323.73 mOhm R  0.89 mOhm Z_C
323.82 mOhm R  0.92 mOhm Z_C
323.91 mOhm R  0.92 mOhm Z_C
323.73 mOhm R  0.93 mOhm Z_C
323.85 mOhm R  0.93 mOhm Z_C
323.73 mOhm R  0.98 mOhm Z_C
323.68 mOhm R  0.87 mOhm Z_C
323.62 mOhm R  0.93 mOhm Z_C
323.68 mOhm R  0.89 mOhm Z_C
323.58 mOhm R  0.96 mOhm Z_C
323.68 mOhm R  1.02 mOhm Z_C
323.58 mOhm R  1.01 mOhm Z_C
323.58 mOhm R  0.94 mOhm Z_C
323.57 mOhm R  1.01 mOhm Z_C
323.55 mOhm R  0.87 mOhm Z_C
323.58 mOhm R  0.94 mOhm Z_C
323.65 mOhm R  0.88 mOhm Z_C
323.68 mOhm R  0.81 mOhm Z_C
323.55 mOhm R  1.02 mOhm Z_C
323.77 mOhm R  1.00 mOhm Z_C
323.59 mOhm R  0.89 mOhm Z_C
323.40 mOhm R  0.92 mOhm Z_C
323.52 mOhm R  0.93 mOhm Z_C
323.53 mOhm R  0.90 mOhm Z_C
323.50 mOhm R  1.02 mOhm Z_C


Output measuring a Capacitor of 1000uF 16V:
Code: [Select]
SAMPLE_FREQUENCY = 9090.00 Hz
MEASURE_FREQUENCY = 568.13 Hz
SAMPLE_TIME = 1.00 s
48.23 mOhm R  316.88 mOhm Z_C 884.05 uFarads
48.20 mOhm R  316.77 mOhm Z_C 884.36 uFarads
48.21 mOhm R  316.84 mOhm Z_C 884.16 uFarads
48.30 mOhm R  316.98 mOhm Z_C 883.77 uFarads
48.22 mOhm R  316.84 mOhm Z_C 884.18 uFarads
48.27 mOhm R  316.92 mOhm Z_C 883.96 uFarads
48.19 mOhm R  316.92 mOhm Z_C 883.94 uFarads
48.14 mOhm R  316.90 mOhm Z_C 884.01 uFarads
48.15 mOhm R  316.92 mOhm Z_C 883.96 uFarads
48.03 mOhm R  316.69 mOhm Z_C 884.58 uFarads
48.23 mOhm R  317.01 mOhm Z_C 883.68 uFarads
48.17 mOhm R  316.87 mOhm Z_C 884.08 uFarads
48.09 mOhm R  316.80 mOhm Z_C 884.28 uFarads
48.27 mOhm R  316.84 mOhm Z_C 884.16 uFarads
48.22 mOhm R  316.79 mOhm Z_C 884.32 uFarads
48.17 mOhm R  316.94 mOhm Z_C 883.89 uFarads
48.23 mOhm R  316.99 mOhm Z_C 883.75 uFarads
48.01 mOhm R  316.95 mOhm Z_C 883.86 uFarads
48.16 mOhm R  316.91 mOhm Z_C 883.99 uFarads
48.07 mOhm R  316.90 mOhm Z_C 884.01 uFarads
48.18 mOhm R  316.87 mOhm Z_C 884.08 uFarads
48.11 mOhm R  316.84 mOhm Z_C 884.18 uFarads
48.11 mOhm R  316.82 mOhm Z_C 884.21 uFarads
48.13 mOhm R  316.79 mOhm Z_C 884.31 uFarads
48.22 mOhm R  316.93 mOhm Z_C 883.91 uFarads
48.03 mOhm R  316.92 mOhm Z_C 883.94 uFarads
48.07 mOhm R  316.92 mOhm Z_C 883.96 uFarads
48.16 mOhm R  316.84 mOhm Z_C 884.17 uFarads
48.12 mOhm R  316.83 mOhm Z_C 884.19 uFarads
48.20 mOhm R  316.71 mOhm Z_C 884.55 uFarads
48.12 mOhm R  316.95 mOhm Z_C 883.87 uFarads


Clearly there is a measurement problem with resistive ohms. I'll get into it after converting the square signal to a triangular signal. The triangular signal is closer to the sine signal and allows to measure inductances, which the square signal does not allow as it generates a peak impossible to measure.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #107 on: May 10, 2024, 07:35:25 pm »
New program with improved table of sines.

New schematic with triangular excitation signal.

It makes good resistance, capacitance and inductance measurements. I have yet to calibrate the measured values to see how accurate they are.

Program:
Code: [Select]
/*
   Version 4.2 (10/05/2024)

   Copyright 2024 Picuino

   Permission is hereby granted, free of charge, to any person obtaining a copy
   of this software and associated documentation files (the "Software"), to deal
   in the Software without restriction, including without limitation the rights
   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   copies of the Software, and to permit persons to whom the Software is
   furnished to do so, subject to the following conditions:

   The above copyright notice and this permission notice shall be included
   in all copies or substantial portions of the Software.

   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
   IN THE SOFTWARE.
*/
#include <stdint.h>

#define CLK_BOARD  16000000
#define UART_BAUDS  115200
#define MEASURE_TIME  1
#define SAMPLES_PER_WAVE 16

#define PIN_SIGNAL_OUT 3
#define PIN_DEBUG_OUT 5
#define PIN_ANALOG  A6
#define PIN_ANALOG_MUX 6

#define PIN_SCK  13
#define PIN_SDI  12
#define PIN_CNV  10

#define TIMER2_PERIOD  220
#define TIMER2_FREQ  (CLK_BOARD / ((TIMER2_PERIOD + 1) * 64 * 2))

#define TIMER0_PERIOD (TIMER2_PERIOD - 1)
#define TIMER0_FREQ  (CLK_BOARD / ((TIMER0_PERIOD + 1) * 8))

#define SAMPLES_PER_MEASURE (SAMPLES_PER_WAVE * (long) ((MEASURE_TIME) * (TIMER0_FREQ) / SAMPLES_PER_WAVE))

const int16_t SIN_INTEGER[SAMPLES_PER_WAVE + SAMPLES_PER_WAVE / 4] = {
  9, 26, 39, 46,
  46, 39, 26, 9,
  -9, -26, -39, -46,
  -46, -39, -26, -9,
  9, 26, 39, 46,
};

const float BOARD_CALIBRATION = 0.5040 / (SAMPLES_PER_MEASURE);  // Converts measure to milliohms

volatile int32_t adc_acc_inphase;
volatile int32_t adc_acc_quadrature;
volatile int16_t adc_samples;
volatile uint8_t adc_measure_end;
volatile uint8_t level_state;
volatile uint8_t level_state_old;


float resistance_inphase;
float resistance_quadrature;


void setup() {
  Serial.begin(UART_BAUDS);

  // Set up output reference signal pin
  pinMode(PIN_SIGNAL_OUT, OUTPUT);
  pinMode(PIN_DEBUG_OUT, OUTPUT);

  // Print initial info
  print_info();

  // Set up peripherals
  timer0_setup();
  timer2_setup();
  timer_synchronize();
  adc_setup();
  measure_init();
  while (adc_measure_end == 0);
  measure_init();
}


void loop() {
  // Main Loop
  while (1) {
    if (adc_measure_end == 1) {
      resistance_inphase = -adc_acc_inphase;
      resistance_quadrature = -adc_acc_quadrature;
      resistance_inphase *= BOARD_CALIBRATION;
      resistance_quadrature *= BOARD_CALIBRATION;
      print_values(resistance_inphase, resistance_quadrature);
      measure_init();
    }
  }
}


void print_info(void) {
  Serial.println();
  Serial.print("SAMPLE_FREQUENCY = ");
  Serial.print(1.0 * TIMER0_FREQ);
  Serial.println(" Hz");

  Serial.print("MEASURE_SIGNAL_FREQUENCY = ");
  Serial.print(1.0 * TIMER2_FREQ);
  Serial.println(" Hz");

  Serial.print("SAMPLE_TIME = ");
  Serial.print(1.0 * SAMPLES_PER_MEASURE / TIMER0_FREQ);
  Serial.println(" s");
}

void print_values(float resistance_inphase, float resistance_quadrature) {
  Serial.print(resistance_inphase, 2);
  Serial.print("\tmOhm R  \t");

  if (resistance_quadrature > 0) {
    Serial.print(resistance_quadrature, 2);
    Serial.print("\tmOhm Z_L \t");
    if (resistance_quadrature > 5.0) {
      Serial.print(resistance_quadrature * 1000.0 / (TIMER2_FREQ * 2.0 * 3.1415927));
      Serial.println("\tuHenrys");
    }
    else {
      Serial.println();
    }
  }
  else {
    Serial.print(-resistance_quadrature, 2);
    Serial.print("\tmOhm Z_C \t");
    if (resistance_quadrature < -5.0) {
      Serial.print(1000000000.0 / (-resistance_quadrature * TIMER2_FREQ * 2.0 * 3.1415927));
      Serial.println("\tuFarads");
    }
    else {
      Serial.println();
    }
  }
}

void adc_setup(void) {
  analogRead(PIN_ANALOG);
  cli(); // Stop interrupts

  ADMUX = (1 << 6) |
          (0 << ADLAR) |
          (PIN_ANALOG_MUX << 0);
  ADCSRA = (1 << ADEN) |
           (0 << ADSC) |
           (0 << ADATE) |
           (0 << ADIE) |
           (0b111);  // Division factor
  ADCSRB = 0x00;

  sei(); // Allow interrupts
}


void measure_init(void) {
  delayMicroseconds(1000);
  cli();
  adc_acc_inphase = 0;
  adc_acc_quadrature = 0;
  level_state = SAMPLES_PER_WAVE * 0.25;
  level_state_old = 0;
  adc_samples = 0;
  ADCW = 0;
  sei();

  while ((PIND & (1 << PIN_SIGNAL_OUT)) != 0);
  while ((PIND & (1 << PIN_SIGNAL_OUT)) == 0);
  adc_measure_end = 0;
}


void timer0_setup(void) {
  cli(); // Stop interrupts

  // set compare match register
  TCCR0A = (0 << 6) | // OOM0A. 0=OC0A disconnected. 1=Toggle OC0A on compare match (p.84)
           (0 << 4) | // COM0B. 0=OC0B disconnected. 1=Toggle OC0B on compare match (p.85)
           (2 << 0);  // WGM0.  PWM mode. 1=phase correct 2=CTC  (p.86)
  TCCR0B = (0 << 7) | // FOC0A.
           (0 << 6) | // FOC0B.
           (0 << 3) | // WGM02.
           (2 << 0);  // CLOCK source.
  OCR0A = TIMER0_PERIOD;
  OCR0B = TIMER0_PERIOD / 2;
  TIMSK0 = (0 << 2) | // OCIE0B. Match B Interrupt Enable
           (1 << 1) | // OCIE0A. Match A Interrupt Enable
           (0 << 0);  // TOIE0. Overflow Interrupt Enable
  TIFR0 = 0;
  TCNT0 = 0; // Initialize Timer0 counter

  sei(); // Allow interrupts
}


void timer2_setup(void) {
  cli(); // Stop interrupts

  TCCR2A = (1 << 6) | // OOM2A. 0=OC2A disconnected. 1=Toggle OC2A on compare match (p.128)
           (2 << 4) | // COM2B. 2=Clear OC2B on compare match (p.129)
           (1 << 0);  // WGM2.  PWM mode. 1=phase correct   (p.130)
  TCCR2B = (0 << 7) | // FOC2A.
           (0 << 6) | // FOC2B.
           (1 << 3) | // WGM22.
           (4 << 0);  // CLOCK source.
  OCR2A = TIMER2_PERIOD;
  OCR2B = TIMER2_PERIOD / 2;
  TIMSK2 = (0 << 2) | // OCIE2B. Match B Interrupt Enable
           (0 << 1) | // OCIE2A. Match A Interrupt Enable
           (0 << 0);  // TOIE2. Overflow Interrupt Enable
  TIFR2 = 0;
  TCNT2 = 0; // Initialize Timer2 counter

  sei(); // Allow interrupts
}


void timer_synchronize(void) {
  cli(); // Stop interrupts

  while ((PIND & (1 << PIN_SIGNAL_OUT)) != 0);
  while ((PIND & (1 << PIN_SIGNAL_OUT)) == 0);

  GTCCR = (1 << TSM) | (1 << PSRASY) | (1 << PSRSYNC); // halt all timers
  TCNT0 = TIMER0_PERIOD / 2 + 4; // Initialize Timer0 counter
  GTCCR = 0; // release all timers

  sei(); // Allow interrupts
}


// Timer0 interrupt handler
ISR(TIMER0_COMPA_vect) {
  int16_t adc_value;

  if (adc_measure_end == 0) {

    // ADC Start Conversion
    ADCSRA |= (1 << ADSC);

    // Read last conversion
    adc_value = ADCW;

    // Accumulate values (10us)
    adc_acc_inphase += (int32_t) adc_value * SIN_INTEGER[level_state_old];
    adc_acc_quadrature += (int32_t) adc_value * SIN_INTEGER[level_state_old + SAMPLES_PER_WAVE / 4];

    // Update next state
    level_state_old = level_state;
    level_state++;
    level_state &= 0x0F;

    adc_samples++;

    if (adc_samples > SAMPLES_PER_MEASURE) {
      adc_measure_end = 1;
    }
  }
}


// Timer2 interrupt handler
ISR(TIMER2_COMPA_vect) {

}


void debug_pin_pulse(void) {
  PORTD |= (1 << PIN_DEBUG_OUT);
  delayMicroseconds(4);
  PORTD &= ~(1 << PIN_DEBUG_OUT);
}



 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #108 on: May 10, 2024, 07:43:40 pm »
Some signals:

Amplifier output measuring a capacitor (1000uF)

Amplifier output measuring an inductance (68uH)

Excitation (Triangular wave)
 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #109 on: May 10, 2024, 08:35:59 pm »
Excitation (Triangular wave)

Also check the settling behavior of the integrator when you turn the square wave on. Does it overshoot and possibly even clip? You may need to wait until it has settled to a steady state before you start taking readings.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #110 on: May 10, 2024, 09:00:40 pm »
The square wave output signal oscillates all the time without stopping (generated by timer2 in PWM mode). Even when not measuring, the signal keeps oscillating.
In addition, at startup the program waits one second before taking measurements.
 
The following users thanked this post: gf

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #111 on: May 11, 2024, 08:51:11 am »
New test.

I have found that if I increase the ADC frequency a little above what is recommended, the outputs are even more stable.

I have increased the samples per cycle to 32. I had to modify the program a bit. It is now more flexible. I have also added a name for one “magic number”. The program works well measuring inductances, capacitors and resistors. Although it still has more error than I would like.


Code: [Select]
/*
   Version 4.3 (11/05/2024)

   Copyright 2024 Picuino

   Permission is hereby granted, free of charge, to any person obtaining a copy
   of this software and associated documentation files (the "Software"), to deal
   in the Software without restriction, including without limitation the rights
   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   copies of the Software, and to permit persons to whom the Software is
   furnished to do so, subject to the following conditions:

   The above copyright notice and this permission notice shall be included
   in all copies or substantial portions of the Software.

   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
   IN THE SOFTWARE.
*/
#include <stdint.h>

#define CLK_BOARD  16000000
#define UART_BAUDS  115200
#define MEASURE_TIME  1
#define SAMPLES_PER_WAVE 32

#define PIN_SIGNAL_OUT 3
#define PIN_DEBUG_OUT 5
#define PIN_ANALOG  A6
#define PIN_ANALOG_MUX 6

#define PIN_SCK  13
#define PIN_SDI  12
#define PIN_CNV  10

#define TIMER2_PERIOD  220
#define TIMER2_FREQ  (CLK_BOARD / ((TIMER2_PERIOD + 1) * 64 * 2))

#define TIMER0_PERIOD (110 - 1)
#define TIMER0_PHASE_ADJUST (-20)
#define TIMER0_FREQ  (CLK_BOARD / ((TIMER0_PERIOD + 1) * 8))

#define SAMPLES_PER_MEASURE (SAMPLES_PER_WAVE * (long) ((MEASURE_TIME) * (TIMER0_FREQ) / SAMPLES_PER_WAVE))

const int16_t SIN_INTEGER[SAMPLES_PER_WAVE + SAMPLES_PER_WAVE / 4] = {
  5,14,23,31,38,43,47,49,
  49,47,43,38,31,23,14,5,
  -5,-14,-23,-31,-38,-43,-47,-49,
  -49,-47,-43,-38,-31,-23,-14,-5,
  5,14,23,31,38,43,47,49,
};

const float BOARD_CALIBRATION = 0.5040 / (SAMPLES_PER_MEASURE);  // Converts measure to milliohms

volatile int32_t adc_acc_inphase;
volatile int32_t adc_acc_quadrature;
volatile int16_t adc_samples;
volatile uint8_t adc_measuring;
volatile uint8_t level_state;
volatile uint8_t level_state_old;


float resistance_inphase;
float resistance_quadrature;


void setup() {
  Serial.begin(UART_BAUDS);

  // Set up output reference signal pin
  pinMode(PIN_SIGNAL_OUT, OUTPUT);
  pinMode(PIN_DEBUG_OUT, OUTPUT);

  // Print initial info
  print_info();

  // Set up peripherals
  timer0_setup();
  timer2_setup();
  timer_synchronize();
  adc_setup();

  // Inits measure
  measure_init();
  while (adc_measuring == 1);
  measure_init();
}


void loop() {
  // Main Loop
  while (1) {
    if (adc_measuring == 0) {
      resistance_inphase = -adc_acc_inphase;
      resistance_quadrature = -adc_acc_quadrature;
      resistance_inphase *= BOARD_CALIBRATION;
      resistance_quadrature *= BOARD_CALIBRATION;

      print_values(resistance_inphase, resistance_quadrature);

      measure_init();
    }
  }
}


void print_info(void) {
  Serial.println();
  Serial.print("SAMPLE_FREQUENCY = ");
  Serial.print(1.0 * TIMER0_FREQ);
  Serial.println(" Hz");

  Serial.print("MEASURE_SIGNAL_FREQUENCY = ");
  Serial.print(1.0 * TIMER2_FREQ);
  Serial.println(" Hz");

  Serial.print("SAMPLE_TIME = ");
  Serial.print(1.0 * SAMPLES_PER_MEASURE / TIMER0_FREQ);
  Serial.println(" s");
}

void print_values(float resistance_inphase, float resistance_quadrature) {
  Serial.print(resistance_inphase, 2);
  Serial.print("\tmOhm R  \t");

  if (resistance_quadrature > 0) {
    Serial.print(resistance_quadrature, 2);
    Serial.print("\tmOhm Z_L \t");
    if (resistance_quadrature > 5.0) {
      Serial.print(resistance_quadrature * 1000.0 / (TIMER2_FREQ * 2.0 * 3.1415927));
      Serial.println("\tuHenrys");
    }
    else {
      Serial.println();
    }
  }
  else {
    Serial.print(-resistance_quadrature, 2);
    Serial.print("\tmOhm Z_C \t");
    if (resistance_quadrature < -5.0) {
      Serial.print(1000000000.0 / (-resistance_quadrature * TIMER2_FREQ * 2.0 * 3.1415927));
      Serial.println("\tuFarads");
    }
    else {
      Serial.println();
    }
  }
}

void adc_setup(void) {
  analogRead(PIN_ANALOG);
  cli(); // Stop interrupts

  ADMUX = (1 << 6) |
          (0 << ADLAR) |
          (PIN_ANALOG_MUX << 0);
  ADCSRA = (1 << ADEN) |
           (0 << ADSC) |
           (0 << ADATE) |
           (0 << ADIE) |
           (0b110);  // Division factor
  ADCSRB = 0x00;

  sei(); // Allow interrupts
}


void measure_init(void) {
  delayMicroseconds(1000);
  cli();
  adc_acc_inphase = 0;
  adc_acc_quadrature = 0;
  level_state = SAMPLES_PER_WAVE * 0.25;
  level_state_old = 0;
  adc_samples = 0;
  ADCW = 0;
  sei();

  while ((PIND & (1 << PIN_SIGNAL_OUT)) != 0);
  while ((PIND & (1 << PIN_SIGNAL_OUT)) == 0);
  adc_measuring = 1;
}


void timer0_setup(void) {
  cli(); // Stop interrupts

  // set compare match register
  TCCR0A = (0 << 6) | // OOM0A. 0=OC0A disconnected. 1=Toggle OC0A on compare match (p.84)
           (0 << 4) | // COM0B. 0=OC0B disconnected. 1=Toggle OC0B on compare match (p.85)
           (2 << 0);  // WGM0.  PWM mode. 1=phase correct 2=CTC  (p.86)
  TCCR0B = (0 << 7) | // FOC0A.
           (0 << 6) | // FOC0B.
           (0 << 3) | // WGM02.
           (2 << 0);  // CLOCK source.
  OCR0A = TIMER0_PERIOD;
  OCR0B = TIMER0_PERIOD / 2;
  TIMSK0 = (0 << 2) | // OCIE0B. Match B Interrupt Enable
           (1 << 1) | // OCIE0A. Match A Interrupt Enable
           (0 << 0);  // TOIE0. Overflow Interrupt Enable
  TIFR0 = 0;
  TCNT0 = 0; // Initialize Timer0 counter

  sei(); // Allow interrupts
}


void timer2_setup(void) {
  cli(); // Stop interrupts

  TCCR2A = (1 << 6) | // OOM2A. 0=OC2A disconnected. 1=Toggle OC2A on compare match (p.128)
           (2 << 4) | // COM2B. 2=Clear OC2B on compare match (p.129)
           (1 << 0);  // WGM2.  PWM mode. 1=phase correct   (p.130)
  TCCR2B = (0 << 7) | // FOC2A.
           (0 << 6) | // FOC2B.
           (1 << 3) | // WGM22.
           (4 << 0);  // CLOCK source.
  OCR2A = TIMER2_PERIOD;
  OCR2B = TIMER2_PERIOD / 2;
  TIMSK2 = (0 << 2) | // OCIE2B. Match B Interrupt Enable
           (0 << 1) | // OCIE2A. Match A Interrupt Enable
           (0 << 0);  // TOIE2. Overflow Interrupt Enable
  TIFR2 = 0;
  TCNT2 = 0; // Initialize Timer2 counter

  sei(); // Allow interrupts
}


void timer_synchronize(void) {
  cli(); // Stop interrupts

  while ((PIND & (1 << PIN_SIGNAL_OUT)) != 0);
  while ((PIND & (1 << PIN_SIGNAL_OUT)) == 0);

  GTCCR = (1 << TSM) | (1 << PSRASY) | (1 << PSRSYNC); // halt all timers
  TCNT0 = TIMER0_PERIOD / 2 + TIMER0_PHASE_ADJUST; // Initialize Timer0 counter
  GTCCR = 0; // release all timers

  sei(); // Allow interrupts
}


// Timer0 interrupt handler
ISR(TIMER0_COMPA_vect) {
  int16_t adc_value;

  if (adc_measuring == 1) {

    // ADC Start Conversion
    ADCSRA |= (1 << ADSC);

    // Read last conversion
    adc_value = ADCW;

    // Accumulate values (10us)
    adc_acc_inphase += (int32_t) adc_value * SIN_INTEGER[level_state_old];
    adc_acc_quadrature += (int32_t) adc_value * SIN_INTEGER[level_state_old + SAMPLES_PER_WAVE / 4];


    // Update next state
    level_state_old = level_state;
    level_state++;
    if (level_state >= SAMPLES_PER_WAVE)
      level_state = 0;

    adc_samples++;
    if (adc_samples > SAMPLES_PER_MEASURE) {
      adc_measuring = 0;
    }
  }
}


// Timer2 interrupt handler
ISR(TIMER2_COMPA_vect) {

}


void debug_pin_pulse(void) {
  PORTD |= (1 << PIN_DEBUG_OUT);
  delayMicroseconds(4);
  PORTD &= ~(1 << PIN_DEBUG_OUT);
}


Measuring a cable:
Code: [Select]
SAMPLE_FREQUENCY = 18181.00 Hz
MEASURE_SIGNAL_FREQUENCY = 565.00 Hz
SAMPLE_TIME = 1.00 s
71.26 mOhm R  1.62 mOhm Z_C
71.19 mOhm R  1.77 mOhm Z_C
71.28 mOhm R  1.70 mOhm Z_C
71.39 mOhm R  1.63 mOhm Z_C
71.27 mOhm R  1.68 mOhm Z_C
71.21 mOhm R  1.49 mOhm Z_C
71.31 mOhm R  1.62 mOhm Z_C
71.18 mOhm R  1.51 mOhm Z_C
71.39 mOhm R  1.58 mOhm Z_C
71.10 mOhm R  1.66 mOhm Z_C
71.15 mOhm R  1.53 mOhm Z_C
71.17 mOhm R  1.50 mOhm Z_C
71.26 mOhm R  1.47 mOhm Z_C
71.15 mOhm R  1.52 mOhm Z_C
71.35 mOhm R  1.73 mOhm Z_C
71.22 mOhm R  1.67 mOhm Z_C
71.14 mOhm R  1.55 mOhm Z_C
71.07 mOhm R  1.41 mOhm Z_C
71.24 mOhm R  1.71 mOhm Z_C
71.31 mOhm R  1.45 mOhm Z_C
71.07 mOhm R  1.44 mOhm Z_C
71.19 mOhm R  1.59 mOhm Z_C
71.16 mOhm R  1.69 mOhm Z_C
71.16 mOhm R  1.51 mOhm Z_C
71.15 mOhm R  1.73 mOhm Z_C
71.25 mOhm R  1.66 mOhm Z_C
71.04 mOhm R  1.71 mOhm Z_C
71.07 mOhm R  1.77 mOhm Z_C


Measuring an inductance:
Code: [Select]
SAMPLE_FREQUENCY = 18181.00 Hz
MEASURE_SIGNAL_FREQUENCY = 565.00 Hz
SAMPLE_TIME = 1.00 s
25.11 mOhm R  198.29 mOhm Z_L 55.86 uHenrys
25.00 mOhm R  198.23 mOhm Z_L 55.84 uHenrys
24.96 mOhm R  198.24 mOhm Z_L 55.84 uHenrys
25.05 mOhm R  198.28 mOhm Z_L 55.85 uHenrys
25.06 mOhm R  198.04 mOhm Z_L 55.79 uHenrys
25.00 mOhm R  198.28 mOhm Z_L 55.85 uHenrys
24.98 mOhm R  198.09 mOhm Z_L 55.80 uHenrys
25.01 mOhm R  198.22 mOhm Z_L 55.84 uHenrys
25.06 mOhm R  198.16 mOhm Z_L 55.82 uHenrys
25.08 mOhm R  198.15 mOhm Z_L 55.82 uHenrys
24.97 mOhm R  198.34 mOhm Z_L 55.87 uHenrys
24.96 mOhm R  198.33 mOhm Z_L 55.87 uHenrys
24.86 mOhm R  198.09 mOhm Z_L 55.80 uHenrys
25.03 mOhm R  198.13 mOhm Z_L 55.81 uHenrys
25.00 mOhm R  198.29 mOhm Z_L 55.86 uHenrys
25.10 mOhm R  198.17 mOhm Z_L 55.82 uHenrys
25.02 mOhm R  198.02 mOhm Z_L 55.78 uHenrys
25.10 mOhm R  198.40 mOhm Z_L 55.89 uHenrys
25.08 mOhm R  198.21 mOhm Z_L 55.83 uHenrys
25.02 mOhm R  198.21 mOhm Z_L 55.83 uHenrys
25.07 mOhm R  198.04 mOhm Z_L 55.79 uHenrys
25.01 mOhm R  198.15 mOhm Z_L 55.82 uHenrys
24.99 mOhm R  198.16 mOhm Z_L 55.82 uHenrys
24.97 mOhm R  198.28 mOhm Z_L 55.85 uHenrys
24.87 mOhm R  198.05 mOhm Z_L 55.79 uHenrys
24.90 mOhm R  198.02 mOhm Z_L 55.78 uHenrys
24.87 mOhm R  198.26 mOhm Z_L 55.85 uHenrys
25.01 mOhm R  198.29 mOhm Z_L 55.86 uHenrys


Measuring a capacitor:
Code: [Select]
SAMPLE_FREQUENCY = 18181.00 Hz
MEASURE_SIGNAL_FREQUENCY = 565.00 Hz
SAMPLE_TIME = 1.00 s
107.67 mOhm R  322.08 mOhm Z_C 874.60 uFarads
107.67 mOhm R  321.94 mOhm Z_C 874.98 uFarads
107.60 mOhm R  321.87 mOhm Z_C 875.15 uFarads
107.64 mOhm R  322.09 mOhm Z_C 874.57 uFarads
107.53 mOhm R  321.84 mOhm Z_C 875.24 uFarads
107.52 mOhm R  322.13 mOhm Z_C 874.47 uFarads
107.47 mOhm R  321.90 mOhm Z_C 875.10 uFarads
107.62 mOhm R  321.91 mOhm Z_C 875.07 uFarads
107.41 mOhm R  321.94 mOhm Z_C 874.98 uFarads
107.52 mOhm R  322.02 mOhm Z_C 874.75 uFarads
107.48 mOhm R  322.00 mOhm Z_C 874.82 uFarads
107.60 mOhm R  321.84 mOhm Z_C 875.26 uFarads
107.47 mOhm R  321.93 mOhm Z_C 875.00 uFarads
107.48 mOhm R  321.68 mOhm Z_C 875.67 uFarads
107.34 mOhm R  321.85 mOhm Z_C 875.22 uFarads
107.38 mOhm R  321.95 mOhm Z_C 874.94 uFarads
107.45 mOhm R  321.84 mOhm Z_C 875.26 uFarads
107.48 mOhm R  321.93 mOhm Z_C 875.01 uFarads
107.21 mOhm R  321.91 mOhm Z_C 875.06 uFarads
107.28 mOhm R  321.87 mOhm Z_C 875.16 uFarads
107.61 mOhm R  321.83 mOhm Z_C 875.29 uFarads
107.34 mOhm R  321.89 mOhm Z_C 875.11 uFarads
107.44 mOhm R  321.96 mOhm Z_C 874.93 uFarads
107.28 mOhm R  321.81 mOhm Z_C 875.34 uFarads
107.29 mOhm R  321.88 mOhm Z_C 875.14 uFarads
107.38 mOhm R  321.86 mOhm Z_C 875.19 uFarads
« Last Edit: May 11, 2024, 08:53:59 am by Picuino »
 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #112 on: May 11, 2024, 09:39:11 am »
Although it still has more error than I would like.

Can you quantify the error?
And how much error would you expect?

How do you determine the "ground truth" value that you are going to compare to the measured value?
The posted readings are meaningless without knowing the true value to compare.

EDIT:

What do you measure if you connect a resistor as DUT, but turn off the square wave?
Do you measure zero (+- random noise), or do you see an offset?
« Last Edit: May 11, 2024, 10:04:38 am by gf »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #113 on: May 11, 2024, 10:43:23 am »
Unfortunately I do not have any capacitance or inductance measuring device or any capacitance or inductance standard, so I have to rely on the nominal measurement which can have +-10% error at best.

The capacitor has 1000uF
The inductance has 68uH

What I am trying for now is to get a stable reading of the first decimal place. That would give me an instrument with a resolution of 50000 points, taking into account that the full scale is 5000 milliohms.

These are the measurements I get if I remove the square signal and connect it to ground and place a resistor as DUT:

Code: [Select]
SAMPLE_FREQUENCY = 18181.00 Hz
MEASURE_SIGNAL_FREQUENCY = 565.00 Hz
SAMPLE_TIME = 1.00 s
0.24 mOhm R  0.63 mOhm Z_C
0.29 mOhm R  0.59 mOhm Z_C
0.28 mOhm R  0.63 mOhm Z_C
0.22 mOhm R  0.63 mOhm Z_C
0.25 mOhm R  0.54 mOhm Z_C
0.29 mOhm R  0.54 mOhm Z_C
0.34 mOhm R  0.60 mOhm Z_C
0.28 mOhm R  0.53 mOhm Z_C
0.42 mOhm R  0.78 mOhm Z_C
0.19 mOhm R  0.72 mOhm Z_C
0.21 mOhm R  0.68 mOhm Z_C
0.15 mOhm R  0.60 mOhm Z_C
0.25 mOhm R  0.71 mOhm Z_C
0.25 mOhm R  0.70 mOhm Z_C
0.16 mOhm R  0.55 mOhm Z_C
0.28 mOhm R  0.70 mOhm Z_C
0.25 mOhm R  0.67 mOhm Z_C
0.17 mOhm R  0.57 mOhm Z_C
0.22 mOhm R  0.80 mOhm Z_C
0.23 mOhm R  0.64 mOhm Z_C
0.16 mOhm R  0.50 mOhm Z_C
0.26 mOhm R  0.65 mOhm Z_C
0.17 mOhm R  0.66 mOhm Z_C
0.10 mOhm R  0.62 mOhm Z_C
0.14 mOhm R  0.63 mOhm Z_C
« Last Edit: May 11, 2024, 10:44:56 am by Picuino »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #114 on: May 11, 2024, 10:56:13 am »
The problem with the resistance (which I can measure with precission) is that the breadboard adds an unknown and relatively high resistance to all contacts.
It is possible that I am demanding too much of a breadboard project and need to move it to a solder board now.
 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #115 on: May 11, 2024, 11:04:27 am »
The problem with the resistance (which I can measure with precission) is that the breadboard adds an unknown and relatively high resistance to all contacts.
It is possible that I am demanding too much of a breadboard project and need to move it to a solder board now.

You have a differential amplifier, so you can do Kelvin (4-wire) measurements.
[ I mean exactly as you did draw it on the schematics. ]
« Last Edit: May 11, 2024, 11:07:18 am by gf »
 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #116 on: May 11, 2024, 11:26:56 am »
What I am trying for now is to get a stable reading of the first decimal place.

In order to reduce uncorrelated random noise by a factor of 10 you need to increase measurement time by a factor of 100 (or take 100 subseqent readings and average them).

Quote
These are the measurements I get if I remove the square signal and connect it to ground and place a resistor as DUT:

So there is a small offset. Good question where it is coming from. Either ADC non-linearity, or your circuit picks up some interferences (either mains harmonics, or whatsoever). What is you mains frequency? 50 or 60 Hz?
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #117 on: May 11, 2024, 11:35:32 am »
50Hz.
And I can see the effects on the output of the instrumentation amplifier.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #118 on: May 11, 2024, 11:37:21 am »
The problem with the resistance (which I can measure with precission) is that the breadboard adds an unknown and relatively high resistance to all contacts.
It is possible that I am demanding too much of a breadboard project and need to move it to a solder board now.

You have a differential amplifier, so you can do Kelvin (4-wire) measurements.
[ I mean exactly as you did draw it on the schematics. ]

Yes, that's what I do. But the breadboard adds resistance to all contacts, so it is impossible to make a true kelvin measurement. I will have to move to a circuit with soldering and shielding.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #119 on: May 11, 2024, 11:42:25 am »
I just realized that the 5V power supply was off, so I was taking readings with the power coming from the Arduino's USB connection.

I have to repeat tests.
 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #120 on: May 11, 2024, 12:50:33 pm »
Quote
You have a differential amplifier, so you can do Kelvin (4-wire) measurements.
[ I mean exactly as you did draw it on the schematics. ]

Yes, that's what I do. But the breadboard adds resistance to all contacts, so it is impossible to make a true kelvin measurement. I will have to move to a circuit with soldering and shielding.

The amplifier has high input impedance, therefore the impedance of the wire from the DUT to the amplijfier is not critical. They just need to be attached to the DUT at the exact points between which you want to measure resistance. Any contact resistance on the current path leads to a common mode signal which should be rejected by the differential amplifier. According to the datasheet CMRR should be about 90dB.

EDIT: What about R9? Do you consider it part of the amplifier, or part of the DUT?
« Last Edit: May 11, 2024, 12:53:53 pm by gf »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #121 on: May 11, 2024, 01:07:24 pm »
In order to reduce uncorrelated random noise by a factor of 10 you need to increase measurement time by a factor of 100 (or take 100 subseqent readings and average them).

I have made measurements with a 10-second period and the values hardly improve.

I have also soldered a smd resistor on the instrumentation amplifier PCB and cut the Breadboard pins to try to improve the output noise. The noise has improved a bit, but the readings are still in error on the first decimal digit.
 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #122 on: May 11, 2024, 01:20:57 pm »
In order to reduce uncorrelated random noise by a factor of 10 you need to increase measurement time by a factor of 100 (or take 100 subseqent readings and average them).

I have made measurements with a 10-second period and the values hardly improve.

I have also soldered a smd resistor on the instrumentation amplifier PCB and cut the Breadboard pins to try to improve the output noise. The noise has improved a bit, but the readings are still in error on the first decimal digit.

Can you measure a 10s period without overflowing the 32-bit signed accumulator (1024*49*565*16*10 = 4535910400 worst case)?

As I said, if it is random noise then you need to go from 1s to 100s in order to gain 1 digit.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #123 on: May 11, 2024, 01:46:33 pm »
EDIT: What about R9? Do you consider it part of the amplifier, or part of the DUT?

R9 was part of the amplifier. It was necessary so that the capacitors would not charge up to 5V. Now it is not needed since the excitation signal is triangular.

EDIT:
But now it comes in handy not to load the capacitor with too much voltage. It is true that its effect would have to be eliminated from the final measurement with some calculations. For now I am not going to do it.
« Last Edit: May 11, 2024, 01:53:21 pm by Picuino »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #124 on: May 11, 2024, 01:50:59 pm »
There is no overflow because not all measurements give 1024 and many measurements are subtracted, not added.

Anyway, for now I'll stick with what I have. When I do the assembly with welds and shielding I'll see if it improves anything.

Measurements of 100 seconds are not practical and I have the impression that I am not going to reduce the error because 10 seconds does not improve compared to 1 second. Besides, to test it I would have to change the program because in that case I could easily have an overflow.
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf