Author Topic: Homebrew Lock-In Amplifier  (Read 10338 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 #75 on: May 08, 2024, 11:37:34 am »
I already have the first measurements with the new ADC.

Program:
Code: [Select]
/*
   Version 3.0 (08/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.
*/
#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 CLK_BOARD  16000000
#define UART_BAUDS  115200
#define TIMER0_PRESET 27        // Must be >= 10
#define TIMER0_FREQ  (CLK_BOARD / (((TIMER0_PRESET) + 1) * 64))

const float MEASURE_TIME = 1;  // Seconds
const long SAMPLES_PER_LEVEL = 6; // ADC samples per output level. must be an even number.
const long LEVELS_PER_MEASURE = 2 * (long)(((MEASURE_TIME) * (TIMER0_FREQ)) / (2 * SAMPLES_PER_LEVEL));
const float BOARD_CALIBRATION = 0.1664;  // Converts measure to milliohms

volatile long adc_acc_inphase;
volatile long adc_acc_quadrature;
volatile unsigned char adc_enable;
volatile unsigned char adc_measure_end;
volatile unsigned long adc_samples;
volatile unsigned char level_state;
volatile unsigned char level_state_old;

volatile union {
  unsigned int w;
  struct {
    unsigned char lo;
    unsigned char hi;
  };
} adc_value;


float resistance_inphase;
float resistance_quadrature;


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

  Serial.print("MEASURE_TIME = ");
  Serial.print(1.0 * (SAMPLES_PER_LEVEL) * (LEVELS_PER_MEASURE) / (TIMER0_FREQ));
  Serial.println(" s");

  Serial.print("MEASURE_FREQUECY = ");
  Serial.print((TIMER0_FREQ) / (2.0 * SAMPLES_PER_LEVEL));
  Serial.println(" Hz");

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

  Serial.print("SAMPLES_PER_LEVEL = ");
  Serial.print(SAMPLES_PER_LEVEL);
  Serial.println(" Samples");


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

  // Set up peripherals
  timer0_setup();
  spi_setup();
}


void loop() {
  // Main Loop
  measure_init();
  while (1) {
    if (adc_measure_end == 1) {
      resistance_inphase = adc_acc_inphase;
      resistance_quadrature = adc_acc_quadrature;
      resistance_inphase *= (BOARD_CALIBRATION / (SAMPLES_PER_LEVEL * LEVELS_PER_MEASURE));
      resistance_quadrature *= (BOARD_CALIBRATION / (SAMPLES_PER_LEVEL * LEVELS_PER_MEASURE));
      Serial.print(resistance_inphase, 2);
      Serial.print("\tmOhm R  \t");
      Serial.print(resistance_quadrature, 2);
      Serial.println("\tmOhm Zc");
      measure_init();
    }
  }
}


void spi_setup(void) {
  // Define SPI Interface
  pinMode(PIN_SCK, OUTPUT);
  pinMode(PIN_SDI, INPUT);
  pinMode(PIN_CNV, OUTPUT);

  digitalWrite(PIN_SCK, LOW);
  digitalWrite(PIN_CNV, LOW);

  SPCR = (0 << 7) |  // SPI interrupt enable
         (1 << 6) |  // SPI enable
         (0 << 5) |  // Data order
         (1 << 4) |  // Master = 1 / Slave = 0
         (0 << 3) |  // Clock polarity  (1 = High when idle)
         (0 << 2) |  // Clock Phase
         (0 << 0);  // SCK Frequency 0 = fosc/2

  SPSR = (0 << 7) |  // SPI interrupt flag
         (0 << 6) |  // WCOL Write collision flag
         (1 << 0);   // SPI Speed 2x
}


void measure_init(void) {
  cli();

  PORTD |= (1 << PIN_SIGNAL_OUT);
  delayMicroseconds(100);

  adc_acc_inphase = 0;
  adc_acc_quadrature = 0;
  level_state = 0;
  adc_samples = 0;
  adc_measure_end = 0;
  adc_enable = 1;
  sei();
}


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

  //set timer0 interrupt at 2kHz
  TCCR0A = 0; // set entire TCCR2A register to 0
  TCCR0B = 0; // same for TCCR2B
  TCNT0  = 0; //initialize counter value to 0
  // set compare match register
  OCR0A = TIMER0_PRESET;
  // turn on CTC mode
  TCCR0A |= (1 << WGM01);
  // Set CS01 and CS00 bits for 64 prescaler
  TCCR0B |= (1 << CS01) | (1 << CS00);
  // enable timer compare interrupt
  TIMSK0 |= (1 << OCIE0A);

  sei(); //allow interrupts
}


ISR(TIMER0_COMPA_vect) {
  if (adc_enable) {
    if (adc_samples) {
      // ADC Start Conversion
      PORTB |= (1 << (PIN_CNV - 8)); // CoNVert enable
      delayMicroseconds(3);  // Wait for conversion
      PORTB &= ~(1 << (PIN_CNV - 8));  // CoNVert End

      // ADC Start Acquisition
      SPDR = 0;
      while (!(SPSR & (1 << SPIF))); // Wait for transmission complete
      adc_value.hi = SPDR;
      SPDR = 0;
      while (!(SPSR & (1 << SPIF))); // Wait for transmission complete
      adc_value.lo = SPDR;


      // Read and accumulate old ADC value
      // Add measure to accumulator
      if (level_state_old < SAMPLES_PER_LEVEL) {
        adc_acc_inphase -= adc_value.w;
      }
      else {
        adc_acc_inphase += adc_value.w;
      }
      if ((level_state_old >=  0.5 * SAMPLES_PER_LEVEL) && (level_state_old < 1.5 * SAMPLES_PER_LEVEL)) {
        adc_acc_quadrature -= adc_value.w;
      }
      else {
        adc_acc_quadrature += adc_value.w;
      }
    }

    // Update next state
    level_state_old = level_state;
    level_state++;
    if (level_state >= (2 * SAMPLES_PER_LEVEL)) {
      level_state = 0;
    }

    // Update output reference signal
    if (level_state < SAMPLES_PER_LEVEL) {
      PORTD |= (1 << PIN_SIGNAL_OUT);
    }
    else {
      PORTD &= ~(1 << PIN_SIGNAL_OUT);
    }

    // Take one measure after several samples
    adc_samples++;
    if (adc_samples > SAMPLES_PER_LEVEL * LEVELS_PER_MEASURE) {
      adc_measure_end = 1;
      adc_enable = 0;
    }
  }
}


Measurements of a capacitor (1000uF, 16V):
Code: [Select]
MEASURE_TIME = 1.00 s
MEASURE_FREQUECY = 744.00 Hz
SAMPLE_FREQUENCY = 8928 Hz
SAMPLES_PER_LEVEL = 6 Samples
88.81 mOhm R  57.57 mOhm Zc
109.42 mOhm R  71.00 mOhm Zc
109.42 mOhm R  70.99 mOhm Zc
109.42 mOhm R  70.93 mOhm Zc
109.43 mOhm R  70.93 mOhm Zc
109.42 mOhm R  70.98 mOhm Zc
109.41 mOhm R  70.96 mOhm Zc
109.41 mOhm R  70.99 mOhm Zc
109.45 mOhm R  70.98 mOhm Zc
109.43 mOhm R  70.94 mOhm Zc
109.42 mOhm R  70.96 mOhm Zc
109.40 mOhm R  70.97 mOhm Zc
109.38 mOhm R  70.96 mOhm Zc
109.39 mOhm R  70.94 mOhm Zc
109.40 mOhm R  70.97 mOhm Zc
109.40 mOhm R  70.98 mOhm Zc
109.45 mOhm R  70.96 mOhm Zc
109.39 mOhm R  70.98 mOhm Zc
109.41 mOhm R  70.98 mOhm Zc
109.39 mOhm R  70.98 mOhm Zc
109.43 mOhm R  70.99 mOhm Zc
109.40 mOhm R  71.00 mOhm Zc
109.40 mOhm R  71.01 mOhm Zc
109.41 mOhm R  71.00 mOhm Zc
« Last Edit: May 08, 2024, 11:52:30 am by Picuino »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #76 on: May 08, 2024, 11:47:14 am »
Results seem to improve only slightly.
Not even one more decimal place of accuracy is improved.

It seems that it is the base noise that defines the maximum accuracy or resolution that can be achieved.

With a 10bit ADC I have been able to get up to 100000 points of resolution at full scale by adding 10000 samples (100 times the resolution of the ADC).
But with a 16bit ADC the result is not improved because noise contaminates the signal.

EDIT:
I imagine it will be the same principle of Delta-Sigma ADCs, which only convert with 1 bit resolution, but take many samples that are later filtered to obtain up to 24 bits of final resolution.
If anyone knows more about the subject maybe they can tell us more and find out if I am right.
« Last Edit: May 08, 2024, 11:50:22 am by Picuino »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #77 on: May 08, 2024, 11:56:31 am »
Another strange effect that has occurred is that the impedance measurement has dropped quite a bit.
I actually adjusted the calibration of the board so that the resistance measurement gave the same value that previously, but I did not actually calibrate it against a known resistor. On the breadboard it is quite complicated because each connection hole adds approximately 60 to 100 milliohms and it is difficult to keep a low resistance measurement stable with that added error.
 

Online gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #78 on: May 08, 2024, 12:15:38 pm »
I have added a quadrature accumulator to be able to make Impedance measurements.

A square wave stimulus in conjunction with a square wave LO works perfectly for ohmic impedances, i.e. if the DUT's response is not time or frequency dependent.

But an impedance with a reactive component is a function of frequency, so you want to measure it at a particular frequency, and you don't want to get a result that is a weighted sum of the DUT's impedance at different frequencies (fundamental+harmonics). That's meaningless.

In order to measure the DUT's response at a single frequency, you must prevent the harmonics from mixing down to DC, and this can be achieved if either the stimulus or the LO is a sine wave (or both). A quadrature mixer with a (complex) sine wave NCO is not difficult to implement. Just sum up  adc_valuej*cos(2*pi*f/sample_rate*j)  and  -adc_valuej*sin(2*pi*f/sample_rate*j)  over the integration interval, in order to get the I and Q values. Since your sample rate is an integer multiple of your stimulus frequency f, you can use precalculated sin/cos tables. With (say) sample_rate=14*f, the sin/cos tables only need 14 entries for a full period.

You also need to take care of aliasing when the ADC samples the signal. If the sample rate is (say) 14*f, then the harmonics at 13*f, 15*f, 29*f, 31*f, etc. will fold back to 1*f in the first Nyquist band and therefore interfere with the fundamental frequency you want to measure.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #79 on: May 08, 2024, 12:50:10 pm »
I was hoping to keep the microcontroller simple (Arduino) so that it would be easier to understand what I am doing and so that it can be easily replicated by others.

On several occasions I have thought about swapping out the Arduino for a my dear 16-bit PIC that has a DAC to generate a sine output and has MAC instructions and 40-bit registers to do the fast calculations required.
The PIC also has an ADC with higher sampling rate (200ksps) and higher resolution (12bits).
The problem is that with another micro, the experiments become more confusing and difficult to replicate or modify by others. The complexity of the program will also increase.

For now I wanted to perform simple tests that would give me information on how far you can go with something as simple as a 10bit, 10ksps ADC and a square excitation signal.

A small improvement I have come up with is to integrate the square signal so that it becomes a ramped signal. It is simple and avoids many problems of the square signal, while filtering out a lot of harmonics.
The problem is to be able to do the necessary multiplications by the microcontroller. I would have to go to a bigger one.

Another idea is to move to the Arduino UNO Revision 4, with 32-bit microcontroller. The problem is that controlling the registers directly is veeery complex and using the libraries is usually much slower.
« Last Edit: May 08, 2024, 12:51:51 pm by Picuino »
 

Online gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #80 on: May 08, 2024, 03:02:21 pm »
The problem is to be able to do the necessary multiplications by the microcontroller. I would have to go to a bigger one.

The processor is an ATmega328 (avr5 in gcc), right? Then it should have 8x8 and 16x16 multiply instructions.
In this example, I/Q mixing with a complex sine wave and accumulation is about 50 instructions per sample (compiled with AVR gcc).
Don't know how many cycles this is, but I think the calculation is less than 10% of the sampling interval.
[ The sin/cos tables could, of course, also be initialized statically in order to avoid the floating point stuff. ]

« Last Edit: May 08, 2024, 05:27:30 pm by gf »
 

Offline RoGeorge

  • Super Contributor
  • ***
  • Posts: 6381
  • Country: ro
Re: Homebrew Lock-In Amplifier
« Reply #81 on: May 08, 2024, 03:39:00 pm »
IIRC ATmega 328 has a hardware multiplier.  I guess GCC would take advantage of that while multiplying integers, but I never tried if so.  If not, use some inline ASM.

Isn't the driving signal still square wave?  If so, multiplier is +/-1, so addition.
« Last Edit: May 09, 2024, 06:39:15 pm by RoGeorge »
 

Online gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #82 on: May 08, 2024, 03:53:27 pm »
IIRC ATmega 328 have a hardware multiplier.

Sorry, mistake. I have to withdraw my previous statement. It seems that it has only an 8x8 bit multiplier.
 

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #83 on: May 08, 2024, 04:35:32 pm »
A good part of the noise is from the amplifier and maybe the drive signal strength (e.g. contacts or thermal effect on R_on). So the gain with the higher resolution ADC is limited. One point where one can gain is having a higher speed and this way an easier way to avoid aliasing (e.g. noise from higher frequencies folding back).

Even with the HW multipier the AVR may have a hard time keeping up with the math.  One can still do quite a bit, e.g. use only a 8 bit sine table - still quite good suppression on the harmonics. It could also help to use ASM to do math with mixed resolution, like 8 bit x 16 bit multiplication or directly a MAC with a 40 bit accumulator. The compilers usually don't support mixed resolution math and would do higher resolution math.
Another way could be to first average in boxcar (time domain) mode and only do the sine wave multiplication one averaged data. This however needs more memory, that could also be limited with the AVR.

It may really be worth looking for a more powerfull µC with the better ADC.

One could still do square wave drive and sine multiplication. Sine wave drive and square wave demodulation adds a bit extra noise from the harminics, but is still not too bad.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #84 on: May 08, 2024, 05:35:47 pm »
Thanks gf, I had forgotten that the Atmega had 8x8 multiplier. Maybe that will be enough.

Anyway I'm going to start testing with a triangular signal. Since the signal is simply a linear ramp, the multiplications can be done simply with shifts and additions. If for example I have to do x2, x4, x6, x8, all are shifts except x6 which is 2 shifts and a 16 bit integer sum. All very simple.

The triangular signal is not as good as the sine signal, but it is very simple to generate and multiply and is much better than a square signal.

EDIT:
But first I want to generate the square signal and sample separately, with two different timers that are synchronized.
That will take some time.
« Last Edit: May 08, 2024, 05:38:34 pm by Picuino »
 

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #85 on: May 08, 2024, 06:38:46 pm »
There is not need to do separate samples for the quadrature signal. The same data can be used for multiple analysis ways (e.g. quadrature and if needed also harmonics separate).
For the quadrature signal ot could help if the ADC samples per period is a multiple of 4.
With a fixed number of points per period (excitation signal generated by the µC and thus more a carrier - frequency amplifer and not a full lock-in) it is relatively easy to use a memory table for the weight factors (e.g. sine). For the table 8 bit values would be usually good enough and like with a DDS generator one can use symmertry to shorten the table.

A point to consider is doing separate averaging in a boxcar integrator mode. This can help with looking at the amplifier and detect transisent effects like slew rate limits or ringing.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
« Last Edit: May 08, 2024, 07:21:55 pm by Picuino »
 

Offline gnuarm

  • Super Contributor
  • ***
  • Posts: 2246
  • Country: pr
Re: Homebrew Lock-In Amplifier
« Reply #87 on: May 08, 2024, 10:19:01 pm »
A low-pass, boxcar filter is used because it is very, very simple to design.  It is just a running average (think sum) of the previous N samples.  You need a buffer of N samples to implement a FIFO.  On each new sample, the oldest is subtracted from the sum and the new sample is added to the FIFO and the sum. 

The corner frequency of the filter is determined by the length of the FIFO.  I don't recall the formula, but I'm sure your references have that info.
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 gnuarm

  • Super Contributor
  • ***
  • Posts: 2246
  • Country: pr
Re: Homebrew Lock-In Amplifier
« Reply #88 on: May 08, 2024, 10:21:39 pm »
Thanks gf, I had forgotten that the Atmega had 8x8 multiplier. Maybe that will be enough.

You can cascade multiplies for longer word sizes.  four 8x8 multiplies will give you a single 16x16 multiply. 
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 PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #89 on: May 09, 2024, 08:08:45 am »
I have studied which number between 8 and 65 is more efficient to multiply to the sine function and produces less error when converting the result into integers, using 16 points for each wave and the result is 52.

This allows me to multiply the result of the ADC (with values up to 1023) by another number that results in an integer. This makes the multiplication easier.

Attached excel sheet.
« Last Edit: May 09, 2024, 08:15:44 am by Picuino »
 

Offline gnuarm

  • Super Contributor
  • ***
  • Posts: 2246
  • Country: pr
Re: Homebrew Lock-In Amplifier
« Reply #90 on: May 09, 2024, 09:43:28 am »
I have studied which number between 8 and 65 is more efficient to multiply to the sine function and produces less error when converting the result into integers, using 16 points for each wave and the result is 52.

I don't understand what you mean by this.  Maybe I missed something earlier in this thread.  I don't think I read it all.  Why do you need to multiply the ADC data at all? 


Quote
This allows me to multiply the result of the ADC (with values up to 1023) by another number that results in an integer. This makes the multiplication easier.

Attached excel sheet.

Perhaps you are not aware that the data from the ADC doesn't have a decimal point.  You can call it integer, you can treat it as all fraction, or you can treat is as fixed point with some bits integer, and some bits fraction.  You can think of this as multiplying by powers of two, which are done by moving where you consider the radix point to be. 
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 PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #91 on: May 09, 2024, 09:52:55 am »
I am multiplying the ADC reading by a sine wave signal to obtain, at the end of one second, the component of the input signal that exactly matches the sine wave signal I am multiplying by. This is the lock-in amplifier principle.

In a sense it is like doing the fourier transform for a single specific frequency that coincides with the excitation frequency of the experiment or Device Under Test.

Until now I have been using an approximation to the sine wave that consists of taking the positive half-cycle always with value 1 and the negative half-cycle always with value -1. Now I am going to check if using values closer to the sine wave I can improve the results.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #92 on: May 09, 2024, 10:56:04 am »
I have made a small mistake by hand calculating the coefficients with the smallest error. After programming a small macro in Python I have discovered a coefficient (47) that generates still a minor error.


Code: [Select]
import math

def main():
    # Compute dots and sines
    dots_len = 16
    mult_range = list(range(8, 65+1))
    dots = [i for i in range(1, dots_len*2 + 1, 2)]
    sines = [math.sin(d*math.pi/dots_len) for d in dots]

    # Compute errors
    errors = []
    for mul in mult_range:
        errors.append([mul, error_desvest(mul, sines)])
    errors = sorted(errors, key=lambda x: x[1])

    # Print results
    print(["multiplier", "integer sin error"])
    for i in range(len(mult_range)):
        print(errors[i])


def error_desvest(mul, sines):
    desvest = 0
    for s in sines:
        sin = abs(s * mul)
        err2 = (sin - int(sin + 0.5)) ** 2
        desvest += err2
    return desvest / len(sines)

main()


Code: [Select]
['multiplier', 'integer sin error']
[47, 0.01419672743708739]
[52, 0.022252210097423654]
[11, 0.024933494804284462]
[36, 0.025018241501426012]
[41, 0.025572615089517102]
[61, 0.03256542624870799]
[65, 0.044613485564976386]
[42, 0.04811188327041043]
[40, 0.052183114594890244]
[58, 0.05324220298131744]
[16, 0.05341992312335985]
[56, 0.05387694689333673]
[54, 0.056970410716863824]
[60, 0.058588617251868]
[45, 0.06007387714071964]
[43, 0.06363624882408199]
[48, 0.06435534252668071]
[31, 0.06643450640699458]
[46, 0.06809762717126693]
[63, 0.06879879471389944]
[51, 0.06963421974653153]
[12, 0.07024942179786148]
[57, 0.07227833125141978]
[50, 0.07319497061657543]
[25, 0.07589074013395344]
[20, 0.07677131511984413]
[59, 0.07746480845138684]
[49, 0.0784466514760001]
[30, 0.07876000969017867]
[53, 0.07965624974239474]
[9, 0.08076021505278731]
[14, 0.08076527563319745]
[10, 0.08394127698731896]
[29, 0.08459899905181871]
[34, 0.08498865252540211]
[35, 0.08626299054272088]
[55, 0.08635297939335956]
[18, 0.0898143494882416]
[13, 0.09085964458863942]
[37, 0.09594836673475386]
[22, 0.09973397921713785]
[32, 0.1027303821044008]
[38, 0.10488779361514296]
[15, 0.10564462086848388]
[17, 0.10571708031947587]
[24, 0.10675661923312965]
[62, 0.11139433312783675]
[19, 0.11435423006872109]
[21, 0.12387698993609458]
[23, 0.12427839606698096]
[27, 0.1263228020520691]
[39, 0.12734894449264433]
[64, 0.13282014919568044]
[8, 0.1338755781826966]
[44, 0.14189353072879649]
[26, 0.1496127645642278]
[33, 0.1573585798918617]
[28, 0.175894108639449]

« Last Edit: May 09, 2024, 10:59:03 am by Picuino »
 

Online gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #93 on: May 09, 2024, 11:15:57 am »
I have made a small mistake by hand calculating the coefficients with the smallest error. After programming a small macro in Python I have discovered a coefficient (47) that generates still a minor error.

In the frequency domain I get the following THD and SFDR as a function of the integer scaling factor (see plot).
I'd use 117 or 120, since this is the largest number which fits into a signed byte.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #94 on: May 09, 2024, 11:16:31 am »
With 18 samples/period and multiplier=34 it is also a good approximation, better than with 16 samples/period.

Code: [Select]
import math

def main():
    for dots_len in range(12, 40+1, 2):
        # Compute dots and sines
        mult_range = list(range(8, 65+1))
        dots = [i for i in range(1, dots_len*2 + 1, 2)]
        sines = [math.sin(d*math.pi/dots_len) for d in dots]

        # Compute errors
        errors = compute_errors(mult_range, sines)

        # Print results
        print(f"Dots_len={dots_len} \tBest multiplier={errors[0][0]} \terror={int(1000*errors[0][1])}")


def compute_errors(mult_range, sines):
    errors = []
    for mul in mult_range:
        errors.append([mul, error_desvest(mul, sines)])
    errors = sorted(errors, key=lambda x: x[1])
    return errors
   

def error_desvest(mul, sines):
    desvest = 0
    for s in sines:
        sin = abs(s * mul)
        err2 = (sin - int(sin + 0.5)) ** 2
        desvest += err2
    return desvest / len(sines)


main()

EDIT:
I change the image, which had an error in the multiplier.
« Last Edit: May 09, 2024, 11:30:14 am by Picuino »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #95 on: May 09, 2024, 11:19:31 am »
In the frequency domain I get the following THD and SFDR as a function of the integer scaling factor (see plot).
I'd use 117 or 120, since this is the largest number which fits into a signed byte.

How do you calculate points and distortion?
 

Online gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #96 on: May 09, 2024, 11:58:56 am »
How do you calculate points and distortion?

See below. Btw, with samples per period (nsamples in the Octave script below) I mean the number of samples for a full (0...2*pi) period of the waveform, including the point at 0, but excluding the point at 2*pi, since the latter is considered the first point of the next period.

Code: [Select]

nsamples = 18;

x = repmat(sin([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+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 = [ 2:nsamples/2 ] + 1;
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])



EDIT:

Quote
With 18 samples/period and multiplier=34 it is also a good approximation, better than with 16 samples/period.

With 18 samples/period, I rather get a (very good) minimum with a multiplicator of 126. Plots attached.

EDIT:

I consider the spectral purity in the frequency domain more important than the MSE or maximum approximation error in the time domain.
« Last Edit: May 09, 2024, 12:53:45 pm by gf »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #97 on: May 09, 2024, 01:30:09 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.

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.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #98 on: May 09, 2024, 03:33:35 pm »
First configuration achieved.
Two different timers synchronized together give the necessary signals to:
 1. generate a 625Hz square wave output (yellow).
 2. Generate 16 interrupts each output wave to initiate ADC conversion (cyan).

 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #99 on: May 09, 2024, 08:50:15 pm »
I already have a first version, probably with some bugs.
The excitation signal is still square, but the voltage measured by the ADC is averaged with an approximate sine wave.
The measurements are giving more error than with the previous version. I will have to study it better.

Program:
Code: [Select]
/*
   Version 4.0 (09/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.
*/
#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 float BOARD_CALIBRATION = 0.3474 / (SAMPLES_PER_MEASURE);  // Converts measure to milliohms
const int SIN_INTEGER[] = {9, 26, 39, 46, 46, 39, 26, 9 };

volatile long adc_acc_inphase;
volatile long adc_acc_quadrature;
volatile unsigned int adc_samples;
volatile unsigned int adc_value;
volatile unsigned char adc_measure_end;
volatile unsigned char level_state;
volatile unsigned char level_state_old;
volatile unsigned char timer0_first_isr;


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();
}


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;
      Serial.print(resistance_inphase, 2);
      Serial.print("\tmOhm R  \t");
      if (resistance_quadrature > 0) {
        Serial.print(resistance_quadrature, 2);
        Serial.print("\tmOhm Z_C \t");
       
        Serial.print(1000000000.0 / (resistance_quadrature * (TIMER0_FREQ / 16.0) * 2.0 * 3.1415927));
        Serial.println("\tuFarads");
      }
      else {
        Serial.print(-resistance_quadrature, 2);
        Serial.println("\tmOhm Z_L");
      }
      measure_init();
    }
  }
}


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 = 7;
  level_state_old = 0;
  adc_samples = 0;
  adc_measure_end = 0;
  timer0_first_isr = 1;
  ADCW = 0;

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


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) {

  if (adc_measure_end == 0 && timer0_first_isr == 0) {

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

    // Read last conversion
    adc_value = ADCW;

    // Accumulate values
    switch (level_state_old) {
      case 7:
      case 0:
        adc_acc_inphase += adc_value * SIN_INTEGER[0];
        break;
      case 6:
      case 1:
        adc_acc_inphase += adc_value * SIN_INTEGER[1];
        break;
      case 5:
      case 2:
        adc_acc_inphase += adc_value * SIN_INTEGER[2];
        break;
      case 4:
      case 3:
        adc_acc_inphase += adc_value * SIN_INTEGER[3];
        break;
      case 15:
      case 8:
        adc_acc_inphase -= adc_value * SIN_INTEGER[0];
        break;
      case 14:
      case 9:
        adc_acc_inphase -= adc_value * SIN_INTEGER[1];
        break;
      case 13:
      case 10:
        adc_acc_inphase -= adc_value * SIN_INTEGER[2];
        break;
      case 12:
      case 11:
        adc_acc_inphase -= adc_value * SIN_INTEGER[3];
        break;
    }

    switch ((level_state_old + 4) & 0x0F) {
      case 7:
      case 0:
        adc_acc_quadrature += adc_value * SIN_INTEGER[0];
        break;
      case 6:
      case 1:
        adc_acc_quadrature += adc_value * SIN_INTEGER[1];
        break;
      case 5:
      case 2:
        adc_acc_quadrature += adc_value * SIN_INTEGER[2];
        break;
      case 4:
      case 3:
        adc_acc_quadrature += adc_value * SIN_INTEGER[3];
        break;
      case 15:
      case 8:
        adc_acc_quadrature -= adc_value * SIN_INTEGER[0];
        break;
      case 14:
      case 9:
        adc_acc_quadrature -= adc_value * SIN_INTEGER[1];
        break;
      case 13:
      case 10:
        adc_acc_quadrature -= adc_value * SIN_INTEGER[2];
        break;
      case 12:
      case 11:
        adc_acc_quadrature -= adc_value * SIN_INTEGER[3];
        break;
    }

    // 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;
    }
  }
  timer0_first_isr = 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);
}



Output measuring a capacitor of 1000uF and 16V:
Code: [Select]
SAMPLE_FREQUENCY = 9090.00 Hz
MEASURE_FREQUENCY = 568.13 Hz
SAMPLE_TIME = 1.00 s
157.19 mOhm R  252.28 mOhm Z_C 1110.43 uFarads
192.02 mOhm R  313.45 mOhm Z_C 893.73 uFarads
192.88 mOhm R  311.39 mOhm Z_C 899.64 uFarads
193.19 mOhm R  311.87 mOhm Z_C 898.26 uFarads
193.11 mOhm R  312.13 mOhm Z_C 897.52 uFarads
191.35 mOhm R  312.51 mOhm Z_C 896.42 uFarads
190.47 mOhm R  312.65 mOhm Z_C 896.02 uFarads
194.14 mOhm R  312.17 mOhm Z_C 897.39 uFarads
194.67 mOhm R  312.46 mOhm Z_C 896.55 uFarads
193.80 mOhm R  311.56 mOhm Z_C 899.15 uFarads
191.92 mOhm R  312.49 mOhm Z_C 896.48 uFarads
194.46 mOhm R  311.44 mOhm Z_C 899.49 uFarads
192.97 mOhm R  312.05 mOhm Z_C 897.73 uFarads
192.89 mOhm R  310.00 mOhm Z_C 903.69 uFarads
193.21 mOhm R  311.79 mOhm Z_C 898.48 uFarads
195.52 mOhm R  311.11 mOhm Z_C 900.46 uFarads
193.99 mOhm R  311.31 mOhm Z_C 899.89 uFarads
193.34 mOhm R  313.72 mOhm Z_C 892.96 uFarads
192.43 mOhm R  312.59 mOhm Z_C 896.19 uFarads
194.31 mOhm R  311.95 mOhm Z_C 898.03 uFarads
193.04 mOhm R  311.95 mOhm Z_C 898.04 uFarads
192.46 mOhm R  311.98 mOhm Z_C 897.93 uFarads
192.70 mOhm R  312.91 mOhm Z_C 895.28 uFarads
192.61 mOhm R  311.90 mOhm Z_C 898.19 uFarads
190.74 mOhm R  312.19 mOhm Z_C 897.33 uFarads
191.34 mOhm R  312.57 mOhm Z_C 896.26 uFarads
191.62 mOhm R  313.41 mOhm Z_C 893.85 uFarads
192.28 mOhm R  313.01 mOhm Z_C 894.98 uFarads
193.83 mOhm R  311.46 mOhm Z_C 899.45 uFarads
193.31 mOhm R  312.13 mOhm Z_C 897.51 uFarads
194.30 mOhm R  310.69 mOhm Z_C 901.66 uFarads
193.66 mOhm R  310.44 mOhm Z_C 902.39 uFarads
193.57 mOhm R  312.56 mOhm Z_C 896.28 uFarads

 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf