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

0 Members and 1 Guest are viewing this topic.

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Homebrew Lock-In Amplifier
« on: April 26, 2024, 12:03:59 pm »
Some time ago I bought the necessary chips to assemble a homebrew lock-in amplifier and I would like to assemble it and experiment with it.
Has anyone assembled the lock-in amplifier based on the AD630 that is circulating on the internet?
With which PCB can it be assembled with guarantees that it will work well?
Has anyone experimented with it and what interesting experiments can be done?
I know there are a lot of questions, but they can be summed up in just one. Can anyone tell us about your experiences with this instrument?


Homebrew Lock-In amplifier:
https://www.holographyforum.org/data/pdf/aa-Collection_a_k/aa-Laser/aa-lockin/Homebrew_lockin_amplifier.html
https://www.instructables.com/Lock-in-Amplifier/

More professional version:
https://physicsopenlab.org/2019/08/20/lock-in-amplifier/
https://www.ebay.com/itm/275596051897?epid=2282365081


What is a Lock-In Amplifier:
https://en.wikipedia.org/wiki/Lock-in_amplifier
https://www.zhinst.com/en/resources/principles-of-lock-in-detection
https://www.thinksrs.com/downloads/pdfs/applicationnotes/AboutLIAs.pdf
https://www.liquidinstruments.com/digital-lock-in-amplifier/


Application notes:
https://www.analog.com/en/resources/analog-dialogue/articles/low-power-synchronous-demodulator.html
https://www.analog.com/en/resources/analog-dialogue/articles/synchronous-detectors-facilitate-precision.html
Synchronous System Measures uOhm  https://www.analog.com/media/en/technical-documentation/application-notes/AN-306.pdf
« Last Edit: May 07, 2024, 07:16:20 am by Picuino »
 

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #1 on: April 26, 2024, 07:42:00 pm »
I have no experiance with this plan / kit, but I have quite a bit experiance with using  lock-in amplifiers and I have build my own version of a lock-in amplifier. My version looks quite different though and has plenty mistakes to learn from.

There are a few issue with this plan:

1) The outputs are directly from the amplfiers. To avoid capacitive loading to the amplifiers it is a good idea to have some 50 or 100 ohm in series to the outputs.
2) The usualy way is to used the output DC coupled. So the capacitor for the output is a thing to drop - a good place for one of the 50 ohm resistors.
3) The amplifier is rather basic. The higher gain setting of more than about x 1000 have a limited bandwidth. This may be just a little much gain for one stage. It limits the use to relatively low frequencies (e.g. 1 kHZ or less). This can be OK for some uses, but could be an issue for others.  In many cases one may have a custom pre-amplifier for the signal source anyway. A protected power source may be substitude a better amplifier to some degree.
4) a point missing are a way to detect clipping, like 2 comparators or simple diode / capacitor detectors for the peak values. Not absolutely needed but a nice to have point.
5) Another usefull and commonly found part with the input is a mains frequency notch filter, possibly also the 2x mains.
6) The output filter has settings for rather low time constants - these make little sense with an amplifier that is limited BW. It is more that a longer time scale could make sense, though today a medium time scale like 100 ms and than digital averaging low pass filtering at the putput would be a good way. So the longer time constants may not be that important. Using electrolytic capacitors with the filter is not ideal. At least the 100 ms range should get way without electrolytic capacitors.
7) depending on how the output is read / displayed one may want more output gain as on option (e.g. with a signal with low SNR)
8) The reference input is directly to the chip - this is kind of a start to add a reference section there. The reference section ideally includes quite some circuitry, so a start, but yet quite ready. One usually wants a bit of protection and signal forming, so that one can start with a crude signal and still get a reasonable sine or optionally square wave with defiened amplitude. Ideally one would also get a fine phase adjust with a precise 90 deg. jump option (e.g. via a PLL). The 90 deg. phase shift part is very handy to adjust the phase. Quite often the ref. side also include some simple generator option (e.g. comes easy with a PLL or so).

Old style analog lockin amplifiers are to a large part replaced with digital solutions. So digitize the signal with enough dynamic range (e.g. a sound card or similar) and than do much of the LI technique digitally.

For the experiments that can be done, there are a lot of optics experiments (e.g. look at absorbtion or reflection). This can be with modulated LEDs as a light source or a mechical chopper.
Another nice experiment can be a field mill to measure electic fields. Utrasonic , acoustics is possible to, though it may need a higher BW amplifier to not get too much phase shift from there.
LVDT mechanical displacement sensor can be read out with a lock-in. Similar DMS can use a carrier frequency amplifier, which is a lockin pluse oscillartor for the reference.
A LCR bridge could use a lock in amplifier - though this really needs the phase shift part.
I have done photo-acoustic detection of light: so use a microphone to detect the pressure rise from modulated light to heat up some gas (e.g. water wapor to absorb 950 nm) or solid surface.
 
The following users thanked this post: ch_scr, Picuino

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #2 on: April 27, 2024, 11:53:07 am »
Thank you for your advice.

I was also thinking about making a LIA with a digital processor, but I need first to have a reference analog LIA to check that the digital one works well as I program it.

I will make the preamp with 2 separate stages. One stage with 4 options (x1, x2, x5 and x10) and another stage that can have 2 options (x1 and x10) so I can choose many options between x1 and x100 with simpler knobs and it won't lower the bandwidth too much. If I need more amplification, it can be done with an external preamp.

Another advice I am missing is about the PCB routing, which is not shown in the tutorial and I think it can be important in such a sensitive instrument. How do I connect the grounds? Is a ground plane enough or do I have to wire all the lines to a single point with separate routes?

 

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #3 on: April 27, 2024, 01:18:17 pm »
With very high gain and precision in mind it is usually better to have a single ground point (star ground). A ground plane is more thing to avoid EMI and good for a digital circuit.
The point with a lock-in amplifier is to keep the input and reference part separate, so avoid the ref. side to cause any input signal with the same frequency. In the plan there is not yet much for the ref. side. For quite some applications one would want a phase adjustment there.
One would want at least some protection.

If one needs fine steps with the gain depends on what is used to read / display the output: with an analog meter one may need 1, 2,5,10 steps for the gain. With a digital read out with sufficient resolution steps of 1 , 10, 100, 1000 are enough. Only a gain range from 1 to 100 may be a bit small. 
Depending on the precision of the amplifier at the input one may want a 2nd AC coupling step before the phase sensitive detection.
The display / output side also determines how much gain is useful at the output (DC) side. With a rel. low resolution ADC or an analog movement one can use quite some DC gain there. Chances are the AD630 output can be stable to something like 10 µV. So the output side should ideally be able to resole to that level. If a good dmm is used the range settings there can provider some of the total gain.
 

Offline RoGeorge

  • Super Contributor
  • ***
  • Posts: 6381
  • Country: ro
Re: Homebrew Lock-In Amplifier
« Reply #4 on: April 28, 2024, 07:10:29 am »
In case you don't have the time to build a dedicated LIA, this oscilloscope trick might be a good-enough replacement:
https://www.eevblog.com/forum/projects/oscilloscope-with-trace-averaging-as-a-lock-in-amplifier-(rigol-ds1054z)/

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #5 on: April 29, 2024, 12:00:46 pm »
I don't really have much time right now. I am preparing the project for this summer when I will have enough time to assemble and test it.

I want to make a real LIA, which is fast enough to decode sounds in real time (10kHz minimum) from a higher frequency signal.

I don't care if it has a lot of quality, just enough for basic experiments to work. That's why I'm considering making the LIA with cheaper, simpler powered components (amplifiers). The problem is that I can't replace the AD630 with a simpler power supply. Sometimes I consider doing a digital LIA directly, but then I would not have any standard to compare.
 

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #6 on: April 29, 2024, 12:14:42 pm »
One can replace the AD630 with a CMOS swich (e.g. DG419) a few resistors and OP-amp.

For simple testing one should include some simple generator that also generate a quadrature signal. Many of the experents may wand a ref signal from a generator and it easier to start with 2 or 4 x the frequency and than make it an accurate 50:50 signal and get a quadrature signal than to generate this from a PLL. A simple µC to generatore the ref. signal and a source drive signal could be a good idea, as it allows to get at least some phase shift rather easy.

Parts of the design depend on how one looks at the output / result. E.g. when using an ADC there anyway, one would not really need the very long time constants at the output. The resolution at the output recording also determines how many gain steps and outout gain is useful. HIgh resolution there can substiture some gain steps.
 
The following users thanked this post: Picuino

Offline RoGeorge

  • Super Contributor
  • ***
  • Posts: 6381
  • Country: ro
Re: Homebrew Lock-In Amplifier
« Reply #7 on: April 29, 2024, 12:17:58 pm »
A LIA only works if you have a strong and clean reference signal, and in sync with the small and noisy signal to be measure, and only works at a constant frequency, or with very slow variations.  A LIA does not decode an unknown signal, it only averages the signal by a pattern given by the reference signal.

In regards to the max frequency, a classic LIA usually goes up to 100kHz or so, but they are very sensitive and very low noise in their analog input stage.  In contrast with that, the oscilloscope method is less sensitive, more noisy, but can work at frequencies as high as the oscilloscope can display, so virtually hundreds of MHz.  Which one to use depends of the measurement that needs to be done.

for basic experiments

For what kind of experiments do you need the LIA, what do you plan to measure with it?
« Last Edit: April 29, 2024, 12:20:25 pm by RoGeorge »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #8 on: April 29, 2024, 01:17:11 pm »
I want start measuring several experiments:
 * Milliohms in pcb traces
 * Coupling between different twisted pairs of cables.
 * Voice with a laser in a crystal. (Microphone)
 

Offline Terry Bites

  • Super Contributor
  • ***
  • Posts: 2452
  • Country: gb
  • Recovering Electrical Engineer
Re: Homebrew Lock-In Amplifier
« Reply #9 on: April 29, 2024, 01:41:09 pm »
I agree with Kleinstein, Analog switches are great for low cost mod demod.  The switch method gives the same result as mutiplying the input with a squarewave. Plenty of examples in AD and LT app notes.
 

Offline shabaz

  • Regular Contributor
  • *
  • Posts: 206
Re: Homebrew Lock-In Amplifier
« Reply #10 on: April 29, 2024, 02:10:30 pm »
This won't meet the OP's needs, but it might be of slight interest to beginners: I created an educational project. I was hoping people could create new experiments with it to improve on my simple ones, but there's no traction (at least, I'm not aware of anyone trying it out).

The project is called Wave Miner (Github link) and has 10 starter experiments so far.

It uses a DSP board from AliExpress and a simple breakout board to BNC connectors (the photo shows it attached to a Raspberry Pi for programming the DSP; the Pi can be detached afterward).

The Gerber files for that breakout board are at that Github link. From the 10 starter experiments, experiment #9 is a very crude lock-in amplifier.

Here is a video demo of the LIA  (before I constructed the breakout board).


The experiments are very basic and for fun/learning rather than serious use, and the DSP used is extremely limited, but on the plus side, very little construction is needed (the breakout board is entirely through-hole construction for simplicity).
« Last Edit: April 29, 2024, 02:12:09 pm by shabaz »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #11 on: April 30, 2024, 03:42:48 pm »
One can replace the AD630 with a CMOS swich (e.g. DG419) a few resistors and OP-amp.

For simple testing one should include some simple generator that also generate a quadrature signal. Many of the experents may wand a ref signal from a generator and it easier to start with 2 or 4 x the frequency and than make it an accurate 50:50 signal and get a quadrature signal than to generate this from a PLL. A simple µC to generatore the ref. signal and a source drive signal could be a good idea, as it allows to get at least some phase shift rather easy.

Parts of the design depend on how one looks at the output / result. E.g. when using an ADC there anyway, one would not really need the very long time constants at the output. The resolution at the output recording also determines how many gain steps and outout gain is useful. HIgh resolution there can substiture some gain steps.

So, if I generate a square signal from a microcontroller and with the same microcontroller take samples with the ADC synchronized with the square signal, I could make a simple Lock-In Amplifier without multiplying the analog signal. I could do that with a simple microcontroller as a proof of concept.

The microcontroller should internally perform the operation of adding or subtracting the value read by the ADC depending on the state of the output signal.
« Last Edit: April 30, 2024, 03:44:25 pm by Picuino »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #12 on: April 30, 2024, 03:51:46 pm »
I am trying to set up an STM32 board (NUCLEO-L412KB) for testing the digital LIA. If I don't succeed, I already have other boards running with 16-bit PICs. They are slower, but I think they will also work without problems and can be powered at 5V (an advantage).
Ultimately I can buy an Arduino UNO R4 and do the project with it. That way it would be much more replicable by others who want to take advantage of the project to assemble it themselves.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #13 on: April 30, 2024, 04:14:23 pm »
I also have a microchip MCP6G02 circuit (dual programmable R-R amplifier x1, x10 and x50) to make the input stage.

It has much worse quality than the AD620, but it is much easier to start testing. It has a wide range of programmable gains from the microcontroller.

https://ww1.microchip.com/downloads/aemDocuments/documents/OTH/ProductDocuments/DataSheets/22004b.pdf
 

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #14 on: April 30, 2024, 04:53:22 pm »
The MCP6G02 is not really a good choice for an input amplifier. The noise is quite high - so if at all only for later stages.  In addition it only is for a 5 V supply and thus has a limited dynamic range.

The amplifier part for the input would be similar for an analog an digital solution.
 

Offline RoGeorge

  • Super Contributor
  • ***
  • Posts: 6381
  • Country: ro
Re: Homebrew Lock-In Amplifier
« Reply #15 on: April 30, 2024, 08:15:50 pm »
So, if I generate a square signal from a microcontroller and with the same microcontroller take samples with the ADC synchronized with the square signal, I could make a simple Lock-In Amplifier without multiplying the analog signal.

It is not possible to take samples without multiplying.  Sampling is multiplying, just that the multiplying factors only take the values 0 and 1, and yes, the setup you proposed will work.  :-+

The classic LIA uses analog multipliers because back then there was no other option.  Cheap and fast ADC were not yet available, resolution was lame, there were no fast enough CPUs and no DSPs to compete with analog.  Even today, it is hard to achieve digitally the same specs as a good analog LIA.

Where a dedicated LIA shines is in the analog performance:  expensive ones are very carefully designed, their performance is close to the theoretical limits, with very low noise, very high dynamic range, very good separation between signals and between channels, they often have a low jitter PLL to generate accurate quadrature signals, etc.

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #16 on: April 30, 2024, 08:57:19 pm »
The theory for lock in amplifiers often assumes an analog multiplication with a sine wave. However the actual implementation often uses the +-1 square wave multiplication (like the AD630 or with CMOS switches). A problem is that the analog multiplication (e.g. AD633) tends to be drifty and not very stable.  One can limit the response to the sine part with an extra bandpass filter before the multiplier.
Working around the multiplier drift is tricky, e.g. with an additional feedback of a compensating square wave.

It is already quite some time (starting around 2000) that good digital lock-in amplifiers are superior to analog ones in most aspects, especially at not so high a frequency.
One can get really low noise, close to the limits with both versions. Even with a primarily analog LI it makes sense to do at least the final integration / low pass filtering at least partially digital. An analog low pass filter has an anoying long settling time, while digital averaging or a FIR low pass fitler is easy and settling faster.

For a digital lockin, the way that DSOs implement the averaging mode is a good starting point. Boxcar averaging looks different from classic lockin function, but one can still start with the waveform averaging. The the lockin does the multipication with the ref. waveform (e.g. a sine or square wave) first and than averages / integrates. The boxcar method first averages the data point by point in the time domain and to get the lockin result one multiplies only at the end after averaging.  From the math side the result is the same - one can swap the squence. The boxcar method needs more memory and fewer multiplications. In addition one could provide additional information from the waveform or fast look at different harmonics or afterwards apply a different test function besides the classic sine and square.

The ADC for a digital lockin amplifier should be relatively good, so that one has a reasonable dynamic range. The µC internal ADCs of often 12 bit or less would limit the dynamic range and thus the ability to detect really small signals. So this is more something for a fast 16-24 bit ADC, maybe audio type ADCs.
 

Offline mawyatt

  • Super Contributor
  • ***
  • Posts: 3419
  • Country: us
Re: Homebrew Lock-In Amplifier
« Reply #17 on: April 30, 2024, 10:16:51 pm »
There is no known better signal distinguishing factor that Bi-Phase Modulation (multiplying by +- 1 or BPSK). Any other type of basic waveform modulation is inferior to BPSK when it comes to signal recovery from noise within coherent systems, and why it's chosen for deep space communications such a Voyager.

With this in mind it should be no surprise that noise buried signal recovery utilizing Synchronous Sampling or Demodulation by means of Bi-Phase demodulation is the best form of signal recovery and why we utilized such in many of our applications dated back over half a century ago.

Honestly, would be surprised if a Lock-In amplifier based upon Sine-Wave Demodulation would outperform one based upon Bi-Phase Demodulation. If so this might point to an inferior Bi-Phase Demodulation design rather than the fundamentally superior sine-wave approach.

Intuitively if one considers a mixing or multiplication process, flipping the waveform in polarity (+- 1 multiplication) has less of the multiplying waveform unwanted artifacts than a sinusoid multiplicand such as close in phase noise.

For example it's well known that for a low noise heterodyne systems with the usual means of a diode based mixer for down conversion, when the diode mixer is heavily driven produces the lowest noise result, and the heavily driven approaching a Bi-Phase multiplication by the LO. This not only improves the mixer conversion loss but also reduces the amplitude uncertainty or noise from the driving signal (LO) further improving the result.

So one might suspect the same for utilizing a pure Bi-Phase vs. Sinusoid multiplicand for Synchronous Sampling or Demodulation as in a Lock-In amplifier application.

Anyway, just some random thoughts on such.

Best,     
Curiosity killed the cat, also depleted my wallet!
~Wyatt Labs by Mike~
 

Offline RoGeorge

  • Super Contributor
  • ***
  • Posts: 6381
  • Country: ro
Re: Homebrew Lock-In Amplifier
« Reply #18 on: April 30, 2024, 11:52:37 pm »
I was wandering if using a poly-phase mixer could improve a LIA, because of the reduced noise factor specific to poly phase mixers.  :)

Should be easy to double the frequency of the reference signal (in order to divide by 2 again, while producing precise quadrature I/Q of the same frequency as the reference) then to use the I/Q to drive the switches of the poly-phase mixer.



Side note now that the quadrature of the reference signal was mentioned, the schematic of the kit in the first post is not exactly a LIA, in my opinion that schematic is rather a synchronous detector.  What I'm trying to say is, a LIA (for me) has to have 2 mixers, one for each of the I/Q components of the quadrature reference, in order to extract both the amplitude and the phase of the fain signal.

It's 2:30 AM here and didn't check against the datasheets, but the schematic of the kit seems to have only a single analog multiplier, and no I/Q splitting of the reference signal.  Though, sometimes the architecture with 2 mixers is called a two-phase lock-in amplifier.  Well, I've always thought a LIA should be able to tell both the phase and the amplitude of the small signal.  If it doesn't have quadrature reference and 2 multipliers, I think of it as a synchronous detector (and I don't call it a LIA).  But I'm not sure if I use the proper naming.
« Last Edit: May 01, 2024, 12:02:54 am by RoGeorge »
 

Offline mawyatt

  • Super Contributor
  • ***
  • Posts: 3419
  • Country: us
Re: Homebrew Lock-In Amplifier
« Reply #19 on: May 01, 2024, 01:49:22 am »
The PPM will naturally create I and Q outputs. The 8 phase version seems to be the "sweet spot" wrt to performance vs complexity.

Best,
Curiosity killed the cat, also depleted my wallet!
~Wyatt Labs by Mike~
 

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #20 on: May 01, 2024, 08:03:42 am »
A basic lock in amplifier has 1 phase only, but there are better models to have 2 phases in quadrature. In some applications this can help quite a bit, while for others (e.g. optics) it is not really helping (only faster settling to check the quadrature part). So far I have not seen an analog build lock in that uses more than 2 phases.
The extra effort for an extra quadrature channel is not that high, if one has a PLL for the reference section anyway. A PLL for the reference section make absolute sense to get stable phase shifts, especially the 90 deg. steps. Much of the effort is in the input amplifier, filters and ref. side PLL / phase shift, not so much the actual phase sensitive detector.

It depends on the application if a sine demodulation makes sense or the simpler +-1 case is better.  In some cases if one looks more at phase shifts or frequency dependent effects one does not want the contribution from the harmonics. Its not about noise, but avoiding side effects / systematic errors that complicate the interpretation of the data.
The point is fitlering out the harmonics, not how the actual demodulator is build.
When it comes to noise the best demodulation function corresponds to the signal waveform, or at least the stable part of it.
With optics experiments one may well have a more square modulation can also use the power in the harmonics, tough if there is jitter (e.g. from a mechanical chopper) it could help to exclude some of the transition region.
For a digital implementation one has anyway the option to use the boxcar like averaging first and than use a suitable demodulation waveform, that may be neither sine not square suitable for the experiment. One could also still save the full (or somewhat filtered) averaged waveform and decide even later which way to look at the data.
Especially digital (but also a few analog ones) lock-in amplifiers also allow to look at the harmonics separately (sepcially 2 x the frequency, 3 x the frequency can have artifacts from the demodulator).
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #21 on: May 01, 2024, 07:57:57 pm »
The MCP6G02 is not really a good choice for an input amplifier. The noise is quite high - so if at all only for later stages.  In addition it only is for a 5 V supply and thus has a limited dynamic range.

The amplifier part for the input would be similar for an analog an digital solution.

Ok. I have ordered an AD8422 amplifier that can be used with 5V single supply. I am going to use it along with a microcontroller to make a milliohm meter as a starter project.

https://www.analog.com/media/en/technical-documentation/data-sheets/ad8422.pdf
 

Offline RoGeorge

  • Super Contributor
  • ***
  • Posts: 6381
  • Country: ro
Re: Homebrew Lock-In Amplifier
« Reply #22 on: May 01, 2024, 09:26:03 pm »
Also from Analog Devices:  AN-306, Synchronous System Measures \$\mu\$\$\Omega\$s
https://www.analog.com/media/en/technical-documentation/application-notes/AN-306.pdf

Offline zrq

  • Frequent Contributor
  • **
  • Posts: 295
  • Country: 00
Re: Homebrew Lock-In Amplifier
« Reply #23 on: May 02, 2024, 12:42:09 am »
I have exactly the same thought.
But sine wave may still have advantages for systems with nonlinearity, the modern DSP lockins usually support demodulation at an arbitrary harmonic frequency, although myself only used second harmonic for peak locking which should also work with a square wave modulation.
BTW sometimes in physics experiments we also do boxcar averaging for low duty cycle signals.

For the original post, working with sound cards is probably a good idea for low frequency experiments. Inexpensive sound cards can have >120 dB dynamic range, probably even more if you don't care about harmonics and spurs for lock-in detection, which is in priniple comparable with todays' best lock-in amplifers from SRS or ZI, not to mention the older analog ones. Although not sure if channel cross-talk can be limiting. I was thinking about playing with my Focusrite but couldn't get the time...

Also although I have a very bad feeling towards red pitaya, they also works as a DSP LIA for higher frequency. Koheron also sells a interesting Zynq based board which my colleagues built laser phase noise characterizations systems around.
 

Offline gnuarm

  • Super Contributor
  • ***
  • Posts: 2246
  • Country: pr
Re: Homebrew Lock-In Amplifier
« Reply #24 on: May 02, 2024, 02:20:47 am »
I have done photo-acoustic detection of light: so use a microphone to detect the pressure rise from modulated light to heat up some gas (e.g. water wapor to absorb 950 nm) or solid surface.

So, what were you using this for?

Many years ago, I did a bit of work with photo-acoustic signal collection.  We were using it to obtain spectra from samples that could not be put into solution so easily.  We used a rotating disc with slots cut evenly, to produce a modulated light beam.  Then the wavelength of the light was varied to produce a scan.  The AC component of the signal gave us the amplitude of the absorbance of the sample. 

Is this anything like what you were doing?  Back in '75, this was pretty virgin territory.
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 RoGeorge

  • Super Contributor
  • ***
  • Posts: 6381
  • Country: ro
Re: Homebrew Lock-In Amplifier
« Reply #25 on: May 02, 2024, 06:08:17 am »
About experiments that can be performed, I always wanted to try this one, too.  ;D
Not to spy, but to see if it really is possible. 



First time seen such setup in a movie, with a surveillance van spying on some important figures with an infra-red laser, when somebody from the surveilled room sees the laser spot by looking through red wine.  I would like to see the eavesdropping scene again, to figure it if it was possible at all to hear dialogues from the glass vibrations of a window.  Anybody happens to know the title of that movie?



Back to LIA,

AD8422 amplifier ... to use it along with a microcontroller to make a milliohm meter

That would be a good start.  For PCB trace and finding short-circuited capacitors (MLCC that decouples the power traces often fails as a solid short-circuit, hard to detect since the power traces are thich, and often there are many capacitors in parallel), Jaromir's audible "Shorty" is a very useful tool (from a practical standpoint, not as an accurate miliohm-meter).



Somebody here added a digital display to it:  https://www.eevblog.com/forum/testgear/finding-short-on-motherboards-with-a-shorty-(with-display)/



If the LIA project is mostly to learn/experiment with electronics ideas, then the poly phase mixer (AKA N-path filter) can work too as a synchronous mixer, and might be easier to build:
N path filters: basics & demo - icdutwentenl



If you just need a LIA for physics experiments in general, then it might be cheaper to hunt for a second hand instrument made by Standford Research or by some other brand name.

Saying this because the analog part is hard to do in DIY regime, and even if you manage to somehow build one with similar performance as a commercial LIA, that build would cost you much more than a second hand LIA (aside from a serious time investment).
« Last Edit: May 02, 2024, 06:14:04 am by RoGeorge »
 

Offline zrq

  • Frequent Contributor
  • **
  • Posts: 295
  • Country: 00
Re: Homebrew Lock-In Amplifier
« Reply #26 on: May 02, 2024, 08:44:53 am »
Is this "listening device" idea can be achieved by a laser doppler vibrometer? The bistatic idea in the figure also looks plasuable. I was also interested in it but still, no time to look into it and form a project.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #27 on: May 02, 2024, 09:27:51 am »
Also from Analog Devices:  AN-306, Synchronous System Measures \$\mu\$\$\Omega\$s
https://www.analog.com/media/en/technical-documentation/application-notes/AN-306.pdf
Something like that I want to do, but with a square signal coming from a microcontroller and the decoding done in the same microcontroller so that it can send the readings by UART to a PC.

The decoding I am preparing does the following:
 * Outputs a 1kHz output signal on one pin of the uC.
 * Takes 10000 ADC readings per second, synchronized with the output signal (5 readings per level).
 * Adds up all the ADC readings in an A register.
 * Adds or subtracts the ADC readings according to the output signal in a B register.
 * After 1024 output edges it averages the A register (average input level or DC signal) and averages the B register (reading of the input signal variation or resistance).
« Last Edit: May 02, 2024, 09:33:29 am by Picuino »
 

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #28 on: May 02, 2024, 09:44:08 am »
For a µOhms measurement one usually wants a relatively low frequency. So not so much 1 kHz but more like 10 Hz. At 1 kHz one has a good chance to already get some effect from the inductance. One can separate it from the phase, but this requires the amplifier to not have extra phase shift.

For this application the digital version makes absolute sense.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #29 on: May 02, 2024, 09:54:24 am »
Although the synchronization is a software problem, it should not be too complicated to change the working frequency and check the effect it has.

I am concerned about several effects:
 * That it does not measure the mains signal (50Hz or harmonics).
 * That it does not measure the 1kHz (or less) injected signal itself through the grounds or through induction on the test leads.
 * The test leads should be in 4-wire (Kelvin) configuration, of course, but there is always a small point where they should be joined and considering how sensitive the device can be I will have to be especially careful about this.

Until I assemble the prototype I cannot test and check these effects.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #30 on: May 02, 2024, 10:01:13 am »
For a µOhms measurement one usually wants a relatively low frequency. So not so much 1 kHz but more like 10 Hz. At 1 kHz one has a good chance to already get some effect from the inductance. One can separate it from the phase, but this requires the amplifier to not have extra phase shift.

For this application the digital version makes absolute sense.

I could also add and subtract in another variable C the ADC measurement 90º out of phase with respect to the reference signal, in order to measure the self-inductance of the resistor, comparing both measurements (B without phase and C with 90º phase).

I leave it as an extension to the project.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #31 on: May 02, 2024, 10:18:32 am »
RoGeorge:

Thanks for the links, I hadn't seen your post with the forum page change.
The idea I have is to experiment with the components and learn. Yes, for more serious experiments it is best to buy a second hand LIA.

The "Shorty" looks like quite a useful device, I didn't know it, thanks.
https://hackaday.io/project/3635-shorty-short-circuit-finder
https://www.eevblog.com/forum/testgear/finding-short-on-motherboards-with-a-shorty-(with-display)/

My idea is to be able to measure the milliohms with a numerical value. I want to use it for printed circuit boards and for screw connections (terminals). It may also be useful in other occasions.
My Agilent 34401A can do those measurements without a problem anyway, so measuring milliohms is not a real need. I want to experiment with the concept of synchronous measurement, which I have never used before.
« Last Edit: May 02, 2024, 10:37:35 am by Picuino »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #32 on: May 02, 2024, 06:43:58 pm »
While the order for low noise amplifiers is coming in, I have been experimenting with a Microchip MCP6N11-10.
https://ww1.microchip.com/downloads/en/DeviceDoc/25073A.pdf

Attached is the schematic I have mounted on a prototyping board to measure small resistors.

The program for the Arduino nano is as follows:

Code: [Select]
/*

   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.

*/

const int PIN_VDD_OPAMP = 6;
const int PIN_PWM_OUT = 3;
const int PIN_ANALOG = A0;

const int SAMPLES_PER_EDGE = 80;
const int EDGES_PER_MEASURE = 5000 / SAMPLES_PER_EDGE;

volatile unsigned int adc_value;
volatile unsigned char adc_on;
volatile long adc_sum;
volatile unsigned int adc_samples;
volatile unsigned char timer0_state;

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

  // Power up instrumentation amplifier
  pinMode(PIN_VDD_OPAMP, OUTPUT);
  digitalWrite(PIN_VDD_OPAMP, HIGH);

  // Set up output reference signal pin
  pinMode(PIN_PWM_OUT, OUTPUT);
  setup_timer0();

  // Set up ADC
  setup_adc();

  // Main Loop
  float resistance;
  adc_on = 0;
  adc_sum = 0;
  adc_samples = 0;
  timer0_state = 0;
  adc_on = 1;
  for (;;) {
    if (adc_samples > SAMPLES_PER_EDGE * EDGES_PER_MEASURE * 2) {
      adc_on = 0;
      if (adc_sum < 0)
        adc_sum = 0;
      resistance = adc_sum;
      resistance = resistance * (1.0 / (SAMPLES_PER_EDGE * EDGES_PER_MEASURE));
      Serial.println(resistance);
      adc_sum = 0;
      adc_samples = 0;
      timer0_state = 0;
      adc_on = 1;
    }
  }
}

void loop() {}

void setup_adc(void) {
  analogRead(PIN_ANALOG);
  cli(); //stop interrupts

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

  sei(); //allow interrupts
}


void setup_timer0(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 = 26; //  f = 16000000 / ((OCR0A + 1) * 64) = 9259 Hz
  // 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_on) {

    // ADC manage
    adc_value = (ADCL | (ADCH << 8));
    if (adc_samples > 0) {
      if (timer0_state < SAMPLES_PER_EDGE) {
        adc_sum -= adc_value;
      }
      else {
        adc_sum += adc_value;
      }
    }

    ADCSRA |= (1 << ADSC);  // ADC Start Conversion
    adc_samples++;
    delayMicroseconds(12);  // Wait for Sample and Hold

    // Update next state
    if (++timer0_state >= (SAMPLES_PER_EDGE * 2)) {
      timer0_state = 0;
    }

    // Update output reference signal
    if (timer0_state < SAMPLES_PER_EDGE) {
      PORTD |= (1 << PIN_PWM_OUT);
    }
    else {
      PORTD &= ~(1 << PIN_PWM_OUT);
    }
  }
}

The first problem I am encountering is that the output signal from the amplifier (Vout) has little noise but a lot of variation between high and low level.
On the oscilloscope I measure variations from 0.50V to 0.58V in the amplified square wave.

Attached is the wave of Vout.


Measures sended by Arduino Nano board (with a lot of noise):
Code: [Select]
113.42
113.01
113.09
113.11
107.85
114.78
115.18
115.32
109.24
107.46
109.21
112.78
107.72
109.01
113.22
107.68
109.41
113.01
113.54
113.30
113.22
107.67
109.30
113.46
113.24
113.34
113.62
113.48
113.36
113.14
113.08
107.76
114.96
114.58
115.14
115.06
115.22
114.90
115.05
114.92
114.90
« Last Edit: May 02, 2024, 06:51:57 pm by Picuino »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #33 on: May 02, 2024, 07:07:16 pm »
Averaging input with the scope, the signal looks better.

 

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #34 on: May 02, 2024, 07:19:19 pm »
The code may have a problem with identifying the output state for the last sample of one type. The ADC reading is done with the old settings and the check for the math is with the updates counter, so 1 reading that is used wrong.

With the current setting one a rather small part of the ADC range is actually used. One may wand some DC shift or AC coupling.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #35 on: May 02, 2024, 08:15:45 pm »
New program with continuous sampling:

Code: [Select]
/*

   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.

*/

const int PIN_VDD_OPAMP = 6;
const int PIN_PWM_OUT = 3;
const int PIN_ANALOG = A0;

const int SAMPLES_PER_EDGE = 80;
const int EDGES_PER_MEASURE = 5000 / SAMPLES_PER_EDGE;

volatile long adc_sum;
volatile long adc_result;
volatile unsigned int adc_value;
volatile unsigned int adc_samples;
volatile unsigned char timer0_state;
volatile unsigned char output_edge;
volatile unsigned char adc_measure_end;



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

  // Power up instrumentation amplifier
  pinMode(PIN_VDD_OPAMP, OUTPUT);
  digitalWrite(PIN_VDD_OPAMP, HIGH);

  // Set up output reference signal pin
  pinMode(PIN_PWM_OUT, OUTPUT);
  setup_timer0();

  // Set up ADC
  setup_adc();

  // Main Loop
  float resistance;
  adc_sum = 0;
  adc_samples = 0;
  timer0_state = 0;
  for (;;) {
    if (adc_measure_end == 1) {
      adc_measure_end = 0;
      resistance = adc_result;
      resistance = resistance * (1.0 / (SAMPLES_PER_EDGE * EDGES_PER_MEASURE));
      Serial.println(resistance);
    }
  }
}

void loop() {}

void setup_adc(void) {
  analogRead(PIN_ANALOG);
  cli(); //stop interrupts

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

  sei(); //allow interrupts
}


void setup_timer0(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 = 26; //  f = 16000000 / ((OCR0A + 1) * 64) = 9259 Hz
  // 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) {
  // ADC manage
  adc_value = (ADCL | (ADCH << 8));
  if (output_edge == 1) {
    adc_sum -= adc_value;
  }
  if (output_edge == 0) {
    adc_sum += adc_value;
  }

  ADCSRA |= (1 << ADSC);  // ADC Start Conversion
  adc_samples++;
  delayMicroseconds(12);  // Wait for Sample and Hold

  // Update next state
  if (++timer0_state >= (SAMPLES_PER_EDGE * 2)) {
    timer0_state = 0;
  }

  // Update output reference signal
  if (timer0_state < SAMPLES_PER_EDGE) {
    PORTD |= (1 << PIN_PWM_OUT);
    output_edge = 1;
  }
  else {
    PORTD &= ~(1 << PIN_PWM_OUT);
    output_edge = 0;
  }

  // Take one measure after several samples
  if (adc_samples > SAMPLES_PER_EDGE * EDGES_PER_MEASURE * 2) {
    adc_measure_end = 1;
    adc_result = adc_sum;
    adc_sum = 0;
    adc_samples = 0;
  }

}

Same error in the measurements:
Code: [Select]
112.91
113.61
112.85
112.79
112.99
113.02
112.92
112.65
112.78
112.89
113.14
112.81
112.90
112.59
112.80
112.54
112.44
112.39
112.34
112.19
112.12
111.70
111.89
111.71
112.10
111.82
111.44
111.78
111.46
111.78
112.02
112.03
111.91
112.08
112.20
111.95
112.28
112.41
112.53
112.73
112.37
112.36
112.54
112.75
112.59
112.38
112.94
113.09
112.98
112.76
113.23
112.77
113.00
112.72
112.96
113.23
112.65
112.76
112.94
112.90
113.11
113.01
112.76
112.87
112.77
112.61
112.98
112.77
112.94
113.08
112.88
113.07
113.01
113.13
112.89
112.93
112.65
113.11
113.20
112.97
113.52
113.30
113.30
113.43
113.32
113.28
113.13
113.18
113.41
113.34
113.38
113.09
113.43
113.25
113.19
112.89
113.00
113.11
113.27
113.19
113.12
113.22
113.20
112.81
113.31
112.92
113.22
 

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #36 on: May 02, 2024, 08:26:15 pm »
The new data look a lot less noisy. Especially the consecutive readings are relatively similar. There is a chance to have some thermal variations if the actual signal or amplifier gain.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #37 on: May 03, 2024, 04:23:12 pm »
I have made several changes to the software and have filtered with resistors and capacitors the MCP6N11 power supply trying to make the measurements more stable and they don't seem to improve anymore:

Code: [Select]
/*
   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.
*/

const int PIN_PWM_OUT = 3;
const int PIN_ANALOG = A0;

const int SAMPLES_PER_EDGE = 80;
const int EDGES_PER_MEASURE = 5000 / SAMPLES_PER_EDGE;

volatile long adc_sum;
volatile unsigned int adc_enable;
volatile unsigned int adc_value;
volatile unsigned int adc_samples;
volatile unsigned char timer0_state;
volatile unsigned char output_level;
volatile unsigned char output_level_old;
volatile unsigned char adc_measure_end;


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

  // Set up output reference signal pin
  pinMode(PIN_PWM_OUT, OUTPUT);
  timer0_setup();

  // Set up ADC
  adc_setup();

  // Main Loop
  float resistance;
  adc_init();
  for (;;) {
    if (adc_measure_end == 1) {
      resistance = adc_sum;
      resistance = resistance * (1.0 / (SAMPLES_PER_EDGE * EDGES_PER_MEASURE));
      Serial.println(resistance);
      adc_init();
    }
  }
}

void loop() {}


void adc_init(void) {
  adc_measure_end = 0;
  adc_sum = 0;
  timer0_state = 0;
  adc_samples = 0;
  PORTD |= (1 << PIN_PWM_OUT);
  output_level = 1;
  output_level_old = 1;
  delayMicroseconds(200);
  adc_enable = 1;
}


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

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

  sei(); //allow interrupts
}


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 = 26; //  f = 16000000 / ((OCR0A + 1) * 64) = 9259 Hz
  // 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) {
      // Read ADC measure
      adc_value = (ADCL | (ADCH << 8));

      // Add measure to accumulator
      if (output_level_old == 1) {
        adc_sum -= adc_value;
      }
      if (output_level_old == 0) {
        adc_sum += adc_value;
      }
    }

    // ADC Start Conversion
    ADCSRA |= (1 << ADSC);
    adc_samples++;
    delayMicroseconds(12);  // Wait for Sample and Hold
    output_level_old = output_level;

    // Update next state
    if (++timer0_state >= (SAMPLES_PER_EDGE * 2)) {
      timer0_state = 0;
    }

    // Update output reference signal
    if (timer0_state < SAMPLES_PER_EDGE) {
      PORTD |= (1 << PIN_PWM_OUT);
      output_level = 1;
    }
    else {
      PORTD &= ~(1 << PIN_PWM_OUT);
      output_level = 0;
    }

    // Take one measure after several samples
    if (adc_samples > SAMPLES_PER_EDGE * EDGES_PER_MEASURE * 2) {
      adc_measure_end = 1;
      adc_enable = 0;
    }
  }
}


Output measures:
Code: [Select]
84.22
84.49
84.40
84.42
84.47
84.18
84.47
84.45
84.26
84.09
84.23
84.36
83.79
84.15
83.90
84.02
83.97
84.12
84.11
83.96
83.67
84.02
84.06
84.05
84.36
83.81
83.97
83.95
83.90
83.93
84.06
83.98
83.81
84.08
83.78
83.95
83.70
83.55
83.75
83.99
83.78
83.83
83.71
83.64
83.53
83.67
83.67
83.73
83.44
83.67
83.65
83.80
83.46
83.63
83.67
83.30
83.47
83.51
83.80
83.43
83.64

The real resistance under test is, more or less, R_ut = 0.4 Ohms.

The signal in Vout have an amplitude of 430mV = 88 dots of ADC

 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #38 on: May 03, 2024, 04:29:24 pm »
It seems that making many measurements simply removes some of the noise, but it is not able to give more resolution than the ADC itself would give with just a couple of measurements.
The principle of extracting more information buried in the noise doesn't seem to work in this circuit.
I would have to prototype with soldering to be 100% sure. But I don't think I would gain much. I would expect to get at least one decimal place more accuracy than the ADC itself gives.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #39 on: May 03, 2024, 04:44:51 pm »
I can't find the “Shorty” software to study its operation.
The original link is broken:
+ https://hackaday.io/project/3635-shorty-short-circuit-finder
+ https://github.com/jaromir-sukuba/shorty/blob/master/fw/shorty.ino  (Link to firmware)

Where can I get it?
Can someone upload it?
 

Offline RoGeorge

  • Super Contributor
  • ***
  • Posts: 6381
  • Country: ro
Re: Homebrew Lock-In Amplifier
« Reply #40 on: May 03, 2024, 05:15:20 pm »
I've took a peek at the other shorty with display, from kripton2035 links/pages, has sources:
http://kripton2035.free.fr/Continuity%20Meters/continuity-short.html
http://kripton2035.free.fr/Resources/shorty.ino

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #41 on: May 03, 2024, 05:17:39 pm »
After checking that the signal coming out of the Arduino (D3) is quite noisy, I connected the output to a BC557 PNP transistor to turn resistor R1 on and off at Vcc.
The result is a signal on R1 with much less noise, but the amplified signal is still quite noisy.

The problem I think is in the 3.3V reference voltage. I will have to generate it myself from the outside, because the one the Arduino delivers, even filtering it, doesn't get rid of having enough noise.
« Last Edit: May 03, 2024, 05:21:23 pm by Picuino »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #42 on: May 03, 2024, 05:19:22 pm »
I've took a peek at the other shorty with display, from kripton2035 links/pages, has sources:
http://kripton2035.free.fr/Continuity%20Meters/continuity-short.html
http://kripton2035.free.fr/Resources/shorty.ino

I'm going to include the program in a post, just in case:

Code: [Select]
#include <avr/io.h>
#include <util/delay.h>

#ifndef F_CPU
#define F_CPU 12000000UL
#endif
#define _BAUD   9600              // Baud rate (9600 is default)
#define _UBRR  (F_CPU/16)/_BAUD - 1 // Used for UBRRL and UBRRH


#define  BUZZER_PORT PORTD
#define  BUZZER_DDR DDRD
#define  BUZZER 6

#define LED_TIMER_ON  500
#define LED_TIMER_MAX  550

unsigned int adc_val;
#define AVERAGE_SIZE 5
unsigned int adc_val_avg[AVERAGE_SIZE], adc_val_acc;
unsigned char adc_val_avg_ptr, i, probe_state;
unsigned int led_timer, led_;
unsigned int probe_timer;

int main (void)
{
  UBRR0H = ((_UBRR) & 0xF00);         // define speed
  UBRR0L = (uint8_t) ((_UBRR) & 0xFF);   // for serial uart
  UCSR0B |= _BV(TXEN0);
  UCSR0B |= _BV(RXEN0);

  DDRD &= ~(1 << DDD3);            // input en D3 button
  DDRD |= (1 << DDD6);          // output on D6 buzzer
  PORTD |= (1 << PORTD3);
  DDRC |= (1 << DDC3);          // analog output 3 for pnp seeding
  DDRB |= (1 << DDC5);          // analog input 5 for adc sampling
  PORTC &= ~(1 << PORTC3);
  OCR0A = 120;
  TCCR0A = 0x42;
  TCCR0B = 0x05;
  ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS0);
  ADMUX = 0x45;
  //  ADMUX |= (1 << ADLAR); // Left adjust ADC result to allow easy 8 bit reading

  probe_timer = 0;
  led_timer = 0;
  probe_state = 1;

  while (1)
  {
    //if probe is active, blink a LED
    led_timer++;
    if ((led_timer > LED_TIMER_ON) & (probe_state == 1))
      PORTB |= (1 << 5);
    else
      PORTB &= ~(1 << 5);
    if (led_timer > LED_TIMER_MAX)
      led_timer = 0;

    //now decide, if the probe wasn't inactive for too long
    probe_timer++;
    if (probe_timer > 15000)
    {
      if (probe_state == 1)
        probe_state = 0;
    }

    //inactive probe code
    if (probe_state == 0)
    {
      PORTC |= (1 << PORTC3);      // change A3=1
      DDRD &= ~(1 << DDD6);     // stop buzzer D6
      _delay_ms(1);
      if ((PIND & (0x08)) == 0) // check D3 button state
      {
        probe_state = 1;     // reactivate device if button is pressed
        probe_timer = 0;
      }
    }

    //active probe code
    if (probe_state == 1)
    {
      PORTC &= ~(1 << PORTC3);    // turns off A3=0
      _delay_ms(1);

      //take adc sample
      ADCSRA |= (1 << ADSC);        // Start conversion
      while (ADCSRA & (1 << ADSC)); // wait for conversion to complete
      adc_val = ADCW;          // ADCL reads only the low byte (8bit) of the ADC conversion result and ADCH reads only the High byte.
      // ADC & ADCW are indeed the same

      //take value and put it into floating average
      adc_val_avg[adc_val_avg_ptr++] = adc_val;
      if (adc_val_avg_ptr > AVERAGE_SIZE) adc_val_avg_ptr = 0;
      adc_val_acc = 0;
      for (i = 0; i < AVERAGE_SIZE; i++)
        adc_val_acc = adc_val_acc + adc_val_avg[i];
      adc_val_acc = adc_val_acc / 20;
      adc_val = adc_val_acc;

      //decide what to do
      if (adc_val > 200) adc_val = 200;
      adc_val = adc_val + 2;
      if (adc_val < 200)
      {
        OCR0A = adc_val;       // The Output Compare Register A contains an 8-bit value that is continuously compared with the counter value (TCNT0)
        DDRD |= (1 << DDD6);      // turns on buzzer D6
        probe_timer = 0;
      }
      else
      {
        DDRD &= ~(1 << DDD6);     // turns off buzzer D6
      }                   // DDRD is the direction register for Port D (Arduino digital pins 0-7)
    }


  }

}
/*

  void usart_tx_b(uint8_t data)
  {
  while (!(UCSR0A & _BV(UDRE0)));
  UDR0 = data;
  }

  void usart_tx_hexa (uint8_t value)
  {
  uint8_t temp;
  temp = value;
  usart_tx_b('0');
  usart_tx_b('x');
  temp = ((temp>>4)&0x0F);
  if (temp<10) temp = temp + '0';
  else temp = temp + 'A'- 10;
  usart_tx_b(temp);
  temp = value;
  temp = ((temp>>0)&0x0F);
  if (temp<10) temp = temp + '0';
  else temp = temp + 'A' - 10;
  usart_tx_b(temp);
  usart_tx_b(' ');
  }
*/
 

Offline RoGeorge

  • Super Contributor
  • ***
  • Posts: 6381
  • Country: ro
Re: Homebrew Lock-In Amplifier
« Reply #43 on: May 03, 2024, 05:36:48 pm »
The problem I think is in the 3.3V reference voltage.

I don't recall AVR chips to have a 3.3V reference, so I guess the 3.3V reference is in fact the MCU Vcc.  Have you double check with the MCU datasheet, if it really has a 3.3V reference?

If your Arduino board is with a classic ATmega48, that MCU has an internal bandgap Vref.  Try using the 1.1V range (full scale) for the ADC.  That will use the internal Vref instead of Vcc as Vref.



I've tried to look at your source code, but when I've seen that 1 page of license written half in CAPS LOCK, i've give up and didn't read the code at all.  ;D

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #44 on: May 03, 2024, 06:10:26 pm »
For a resistance measurement the current source / driving resistor should use the same reference voltage as the ADC. So in this case likely the supply.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #45 on: May 03, 2024, 06:11:00 pm »
The 3.3 volt reference comes from an LDO regulator on the Arduino Nano board itself.
I replaced it with a follower op amp that amplifies a 4 volt resistive divider. It has much less noise than the Arduino's 3.3V reference, but the readings still vary much more than I expected.

The problem is that the amplified signal Vout has a lot of variation. The oscilloscope with 16-sample average input gives the following Vout values for the resistor I am testing now: from 187mV to 197mV amplitude.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #46 on: May 03, 2024, 06:15:15 pm »
For a resistance measurement the current source / driving resistor should use the same reference voltage as the ADC. So in this case likely the supply.

Yes, I am using the 5V coming from a lab power supply to power the amplifier, the resistor R1 and the internal voltage reference of the ADC.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #47 on: May 04, 2024, 08:49:47 am »
I replaced the MCP6N11 amplifier with a better one that I found in my SMD component book, the AD8220.
The results have improved.

Output measurements:
Code: [Select]
21.20
21.22
20.96
20.99
21.17
21.10
21.08
21.07
21.02
21.02
21.12
20.97
21.18
21.10
21.09
21.13
21.04
21.06
21.15
21.21
21.01
21.20
21.30
21.06
21.28
21.16
21.13
21.00
21.04
21.15
21.17
21.02
21.09
20.94
21.12
21.18
20.95
21.01
21.09
21.15
21.11
21.08
21.02
21.03
21.16
21.05
21.07
21.02
21.16
21.02
21.22
21.10
21.08
20.99
21.11
20.97
21.12
20.97
21.17
21.06
21.09
21.10
21.04
20.97
21.18
21.07
21.02
21.10
21.05
21.08
21.19
21.07
21.09
20.95
21.15
21.02
21.13
21.00
21.01
21.11
21.16
21.05
21.09
21.10
21.14
21.08
21.04
21.06
21.00
21.20
21.09
21.02
21.07
21.10
21.04
21.00
21.03
21.15
21.20
21.08
20.97
21.03
20.95
21.14
21.09
21.02
21.05
21.01
21.04
20.97
20.99
21.03
20.97
21.07
21.08
21.16
21.08
20.97
20.93
21.08
21.10
21.02
21.14
20.99
21.00
21.13
21.12
21.12
21.09
21.16
21.15
21.15
21.16
21.09
21.12
21.01
21.12
20.95
21.21
21.13
21.08
21.04
21.06
21.20
21.13
21.07
21.10
21.06
21.22
21.14
21.09
21.10
21.05
20.93
21.16
21.16
21.11
21.12
21.19
21.05
21.12
21.04
21.08
20.93
21.07
21.08
21.00
20.97
21.17
21.03
21.11
20.92
21.11
21.11
21.10
21.04
21.11
21.05
20.98
21.27
21.14
21.15
21.08
21.00
21.07
21.21
21.01
21.01
21.06
21.05
21.10
21.18
21.05
21.16
21.08
21.16
21.08
21.13
21.22
21.11
21.08
21.13
21.01
21.18
21.11
21.02
20.97
21.09
21.01
21.22
21.10
21.08
21.04

I have added a calibration value to the program so that the results are measured in milliohms. In this case it is measuring about 20 milliohms from a small wire about 20cm long.


Arduino code:
Code: [Select]
/*
   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.
*/

const int PIN_PWM_OUT = 3;
const int PIN_ANALOG = A0;

const int SAMPLES_PER_EDGE = 80;
const int EDGES_PER_MEASURE = 5000 / SAMPLES_PER_EDGE;
const float BOARD_CALIBRATION = 4.0;

volatile long adc_sum;
volatile unsigned int adc_enable;
volatile unsigned int adc_value;
volatile unsigned int adc_samples;
volatile unsigned char timer0_state;
volatile unsigned char output_level;
volatile unsigned char output_level_old;
volatile unsigned char adc_measure_end;


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

  // Set up output reference signal pin
  pinMode(PIN_PWM_OUT, OUTPUT);
  timer0_setup();

  // Set up ADC
  adc_setup();

  // Main Loop
  float resistance;
  adc_init();
  for (;;) {
    if (adc_measure_end == 1) {
      resistance = adc_sum;
      resistance = resistance * (BOARD_CALIBRATION / (SAMPLES_PER_EDGE * EDGES_PER_MEASURE));
      Serial.println(resistance);
      adc_init();
    }
  }
}

void loop() {}


void adc_init(void) {
  adc_measure_end = 0;
  adc_sum = 0;
  timer0_state = 0;
  adc_samples = 0;
  PORTD |= (1 << PIN_PWM_OUT);
  output_level = 1;
  output_level_old = 1;
  delayMicroseconds(200);
  adc_enable = 1;
}


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

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

  sei(); //allow interrupts
}


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 = 26; //  f = 16000000 / ((OCR0A + 1) * 64) = 9259 Hz
  // 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) {
      // Read ADC measure
      adc_value = (ADCL | (ADCH << 8));

      // Add measure to accumulator
      if (output_level_old == 1) {
        adc_sum -= adc_value;
      }
      if (output_level_old == 0) {
        adc_sum += adc_value;
      }
    }

    // ADC Start Conversion
    ADCSRA |= (1 << ADSC);
    adc_samples++;
    delayMicroseconds(12);  // Wait for Sample and Hold
    output_level_old = output_level;

    // Update next state
    if (++timer0_state >= (SAMPLES_PER_EDGE * 2)) {
      timer0_state = 0;
    }

    // Update output reference signal
    if (timer0_state < SAMPLES_PER_EDGE) {
      PORTD |= (1 << PIN_PWM_OUT);
      output_level = 1;
    }
    else {
      PORTD &= ~(1 << PIN_PWM_OUT);
      output_level = 0;
    }

    // Take one measure after several samples
    if (adc_samples > SAMPLES_PER_EDGE * EDGES_PER_MEASURE * 2) {
      adc_measure_end = 1;
      adc_enable = 0;
    }
  }
}
« Last Edit: May 04, 2024, 08:52:44 am by Picuino »
 

Offline Sariel

  • Contributor
  • Posts: 34
  • Country: il
Re: Homebrew Lock-In Amplifier
« Reply #48 on: May 04, 2024, 11:38:31 am »
I made a very nice Lock in amplifier based on AD630.

A short time ago, I actually posted a LinkedIn post about this with some results:

https://www.linkedin.com/posts/sariel-hodisan-792363120_lock-in-amplifiers-lias-are-incredibly-activity-7180141558162178049-_iK3?utm_source=share&utm_medium=member_android
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #49 on: May 04, 2024, 08:53:30 pm »
I have corrected a software problem that caused some ADC conversions to be lost. The ADC conversion now starts at the beginning of the interrupt routine so that there are no delays with overlapping ADC conversions.
It is tested and works better than before.

Arduino program Version 1.0:
Code: [Select]
/*
   Version 1.0 (04/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.
*/

const int PIN_PWM_OUT = 3;
const int PIN_ANALOG = A0;

#define CLK_BOARD  16000000
#define UART_BAUDS  9600
#define TIMER0_PRESET 26        // Must be >= 26
#define TIMER0_FREQ  (CLK_BOARD / (((TIMER0_PRESET) + 1) * 64))

const int SAMPLES_PER_LEVEL = 20;
const int LEVELS_PER_MEASURE = TIMER0_FREQ / (SAMPLES_PER_LEVEL * 2);
const float BOARD_CALIBRATION = 4.0;

volatile long adc_sum;
volatile unsigned int adc_enable;
volatile unsigned int adc_value;
volatile unsigned int adc_samples;
volatile unsigned int timer0_state;
volatile unsigned char output_level;
volatile unsigned char output_level_old;
volatile unsigned char adc_measure_end;


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

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

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

  // Main Loop
  float resistance;
  adc_init();
  for (;;) {
    if (adc_measure_end == 1) {
      resistance = adc_sum;
      resistance = resistance * (BOARD_CALIBRATION / (1.0 * SAMPLES_PER_LEVEL * LEVELS_PER_MEASURE));
      Serial.println(resistance);
      adc_init();
    }
  }
}

void loop() {}


void adc_init(void) {
  adc_measure_end = 0;
  adc_sum = 0;
  timer0_state = 0;
  adc_samples = 0;
  PORTD |= (1 << PIN_PWM_OUT);
  output_level = 1;
  output_level_old = 1;
  delayMicroseconds(200);
  adc_enable = 1;
}


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

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

  sei(); //allow interrupts
}


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

    // ADC Start Conversion
    ADCSRA |= (1 << ADSC);
    adc_samples++;
    delayMicroseconds(15);  // Wait for Sample and Hold

    // Read and accumulate old ADC value
    if (adc_samples) {
      // Read ADC old measure
      adc_value = ADCW;

      // Add measure to accumulator
      if (output_level_old == 1) {
        adc_sum -= adc_value;
      }
      if (output_level_old == 0) {
        adc_sum += adc_value;
      }
    }

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

    // Update output reference signal
    output_level_old = output_level;
    if (timer0_state < SAMPLES_PER_LEVEL) {
      PORTD |= (1 << PIN_PWM_OUT);
      output_level = 1;
    }
    else {
      PORTD &= ~(1 << PIN_PWM_OUT);
      output_level = 0;
    }

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


Output measures:
Code: [Select]
22.57
22.48
22.52
22.50
22.43
22.49
22.40
22.41
22.54
22.35
22.38
22.54
22.53
22.51
22.40
22.35
22.40
22.45
22.48
22.52
22.53
22.44
22.51
22.40
22.41
22.47
22.42
22.53
22.48
22.30
22.35
22.49
22.42
22.41
22.52
22.41
22.35
22.33
22.36
22.38
22.37
22.46
22.49
22.47
22.41
22.38
22.41
22.43
22.55
22.32
22.39
22.46
22.19
22.48
22.34
22.36
22.40
22.37
22.40
22.45

I think that this time I have managed to recover part of the signal “buried” in the noise.


Attached: Vout
« Last Edit: May 04, 2024, 08:58:09 pm by Picuino »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #50 on: May 04, 2024, 09:59:13 pm »
« Last Edit: May 04, 2024, 10:07:21 pm by Picuino »
 

Offline RoGeorge

  • Super Contributor
  • ***
  • Posts: 6381
  • Country: ro
Re: Homebrew Lock-In Amplifier
« Reply #51 on: May 05, 2024, 08:02:34 am »
What I would try:
- split the 4k7 in two of 2k2 or so, one to the +5V, and one to the GND, with the DUT in the middle
- remove the transistor, and use 4 MCU pins in parallel instead of GND, and another 4 parallel pins as V+ for the 2k2+DUT+2k2 divider (the average Rds ON is about 60ohms for 1 DO, and the mismatch between the upper and the lower side is about 10% or less)
- once the divider connected between the MCU pins, alternate the polarity (output 11110000 then 00001111 repeatedly), and measure the peak to peak amplitude, such that the thermocouple voltages and opamp offset are eliminated
- trigger the ADC always at the same distance from the polarity flipping edge, somewhere at the middle
- use the low-noise mode conversion if possible (don't recall the exact conditions), the one in which other unnecessary blocks of the microcontroller are powered down, so to make less noise during a conversion
- keep all arithmetic with integers (maybe keep a running average on the MCU, not sure), and only use floating point to scale the received data on the PC
- use the highest possible clock for the CPU, and use a constant time loop (an infinite loop that always execute in the same amount of machine cycles rather than interrupts, if possible), to minimize the jitter
« Last Edit: May 05, 2024, 08:09:49 am by RoGeorge »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #52 on: May 05, 2024, 10:06:57 am »
Thank you very much RoGeorge.

I have an ADC AD7691: https://www.analog.com/media/en/technical-documentation/data-sheets/ad7691.pdf
My next step will be to sample with this more accurate adc that can separate the analog from the digital part.
With that I hope to get some more accuracy, but I'm afraid I'm not going to get much error reduction due to the noise. Anyway I will try to check the results.

I will take into account the advice to try to reduce jitter.

As for the signal directly from the digital outputs, it doesn't seem to be a good idea, because when I remove the transistor and feed the resistor under test directly from the digital output of the microcontroller, the noise increases.
I will try to separate as much as possible the analog part from the digital part.
 

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #53 on: May 05, 2024, 01:09:49 pm »
Using the µC to directly drive the DUT is a 2 sided thing: if eliminates the transistor and gets maybe closer to a ratiometric measurement. However it also add the somewhat temperature dependent output resistance. So the gain drift and fluctuations from that side can get worse.
With the current test current of some 1 mA  0,1 mOhm is  100 nV at the DUT. So the observed noise is not that bad.
It depends on the avearging time if the observed noise is about what one can expect from the AD8220. With a lock-in amplifier one can trade speed for noise. With relatively short integration one may still get some effect of mains hum.
 

Offline RoGeorge

  • Super Contributor
  • ***
  • Posts: 6381
  • Country: ro
Re: Homebrew Lock-In Amplifier
« Reply #54 on: May 05, 2024, 01:28:58 pm »
What are the aimed specs?  What range of resistance, with what accuracy, and at what resolution should the instrument measure?

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #55 on: May 05, 2024, 07:20:04 pm »
With the current test current of some 1 mA  0,1 mOhm is  100 nV at the DUT. So the observed noise is not that bad.
It depends on the avearging time if the observed noise is about what one can expect from the AD8220. With a lock-in amplifier one can trade speed for noise. With relatively short integration one may still get some effect of mains hum.
At this time the measurement time is approximately 1 second in which more than 9200 samples are taken.
100nV of noise is OK, but that is the residual noise after averaging all samples. In the individual samples the Vout signal coming out of the amplifier AD8220 has a noise greater than 20mV and I think it is very high. Anyway for a prototype board mounted circuit the final noise it is not too bad.

What are the aimed specs?  What range of resistance, with what accuracy, and at what resolution should the instrument measure?
For now I am just experimenting to see what can be achieved with this type of circuitry.
With what I have already achieved, the circuit would have a resolution of 0.1 milliohm and a range of about 4 ohms (this would require coupling the input of the instrumentation amplifier capacitively).
Good range and resolution for measure ESR of capacitors or measure the resistance of copper traces of a PCB.
« Last Edit: May 05, 2024, 07:23:09 pm by Picuino »
 

Offline jbb

  • Super Contributor
  • ***
  • Posts: 1161
  • Country: nz
Re: Homebrew Lock-In Amplifier
« Reply #56 on: May 05, 2024, 08:02:02 pm »
At risk of totally derailing the proceedings, I have an alternative suggestion: the Texas Instruments ADS1235. It has a 24 bit ADC, Programmable Gain Amplifier (PGA) and control logic for AC excitation. Seems like it can apply positive excitation, sample the ADC, apply negative excitation and sample the ADC again for you.

https://www.ti.com/product/ADS1235
 

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #57 on: May 05, 2024, 08:03:50 pm »
It is normal for the individual readings from the ADC to have quite some noise. Some noise is actually good, so that oversampling works as intended and is not stuck a certain ADC levels.

For low noise one may acutually want some anti aliasing filter (e.g. for some 5 -10 kHz). Other wise the µC internal ADC could pic up noise up to some 50 kHz (or 15 kHz from the amplifier). The limited BW of the AD8220 here also does some filtering, but more could improve things a little more.
The settling time of the fitler would likely want skipping some samples to reduce the effect of the settling speed.

20 mV at the output of an amplifier with a gain of 1000 is not that bad.
 

Offline trobbins

  • Frequent Contributor
  • **
  • Posts: 775
  • Country: au
Re: Homebrew Lock-In Amplifier
« Reply #58 on: May 06, 2024, 01:19:20 am »
Back in the 90's I used an AD630 to extract a battery impedance measurement from industrial noisy live battery systems using a portable tool that applied about 0.1Arms sinewave into a battery (cell or string) and kelvin probe extracted a signal that was amplified and presented to the AD630.   An ICL8038 sine generator provided reasonable amplitude stability, muting, and a sync signal for the AD630.   The test frequency was aligned to nominal zero-phase shift response of target batteries, and not aligned to any mains related processes.  At that time only a 12-bit ADC was practical, with software flipping the sensed signal polarity and averaging, to achieve a max error tolerance of <0.3% along with temperature errors mainly due to the 8038 and current sense resistor, for FS reading of 50 milliohm and resolution down to single micro-ohm level.  The AD630 was not the main concern for performance, but was a pricey part.
 

Offline RoGeorge

  • Super Contributor
  • ***
  • Posts: 6381
  • Country: ro
Re: Homebrew Lock-In Amplifier
« Reply #59 on: May 06, 2024, 05:41:35 am »
A few measures to lower the noise:

- keep the breadboard wires short and the loops short.  A circular shape is the largest area a string can enclose, and the larger the area of a loop, the more induced noise.  If the breadboarding wires are too long, then run them parallel, or twisted.  Preferably cut the wires just the right length, and run then as if it were a PCB (for example use single core wires stripped from a LAN patch cord, and cut them on the spot, just the right length, LAN wires are made of copper, and thick enough for the breadboard contacts).

Not so good (wires making loops with big area, components with long legs also making loops):


Better (wires were cut just the right length and routed straight, components terminals not very long):

Well, maybe the capacitors and resistors terminals could have been cut even shorter, like here:  https://www.eevblog.com/forum/projects/t20347/?action=dlattach;attach=1242762;image but you got the idea, avoid big area loops.

- avoid ground loops.  Use star-connection for GND wires, with a single GND point from where all the ground wires are leaving to different places in the circuit.  Do not chain the GND wires one after another.  A single GND point also separates the analog and digital GNDs.

- if the above is not possible in practice, at least keep the analog and digital current loops separated:
https://www.analog.com/media/en/training-seminars/tutorials/MT-031.pdf
https://www.analog.com/media/en/technical-documentation/application-notes/AN-202.pdf

- enclose the low signal stages in a grounded metal shield, shielding makes a big difference, it eliminates the noises induced from the outside of the circuit
« Last Edit: May 06, 2024, 06:38:35 am by RoGeorge »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #60 on: May 06, 2024, 11:10:21 am »
At risk of totally derailing the proceedings, I have an alternative suggestion: the Texas Instruments ADS1235. It has a 24 bit ADC, Programmable Gain Amplifier (PGA) and control logic for AC excitation. Seems like it can apply positive excitation, sample the ADC, apply negative excitation and sample the ADC again for you.
I am going to add an 18bit ADC to see if it improves the result over the 10bit ADC I am using now. Anyway the resolution improvement should be achieved thanks to the averaging of many measurements.


Back in the 90's I used an AD630 to extract a battery impedance measurement from industrial noisy live battery systems using a portable tool that applied about 0.1Arms sinewave into a battery (cell or string) and kelvin probe extracted a signal that was amplified and presented to the AD630.   An ICL8038 sine generator provided reasonable amplitude stability, muting, and a sync signal for the AD630.   The test frequency was aligned to nominal zero-phase shift response of target batteries, and not aligned to any mains related processes.  At that time only a 12-bit ADC was practical, with software flipping the sensed signal polarity and averaging, to achieve a max error tolerance of <0.3% along with temperature errors mainly due to the 8038 and current sense resistor, for FS reading of 50 milliohm and resolution down to single micro-ohm level.  The AD630 was not the main concern for performance, but was a pricey part.
I am going to increase the test current to check if with that change I can increase the resolution or if, on the contrary, the noise also goes up.


RoGeorge:
I will replace the most critical cables (analog part) with cables taken from a network cable, with the right size.
I used to always do the assemblies this way, but it's a lot of work and it doesn't always improve the result.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #61 on: May 06, 2024, 05:58:15 pm »
With 10 times more current (R2=470 Ohm), I have achieved 10 times more resolution.
I now measure tens of microohms instead of hundreds of microohms.


Output measures:

Code: [Select]
21.499 mOhm
21.502 mOhm
21.496 mOhm
21.502 mOhm
21.504 mOhm
21.499 mOhm
21.508 mOhm
21.502 mOhm
21.500 mOhm
21.503 mOhm
21.508 mOhm
21.500 mOhm
21.505 mOhm
21.504 mOhm
21.507 mOhm
21.500 mOhm
21.512 mOhm
21.499 mOhm
21.505 mOhm
21.504 mOhm
21.505 mOhm
21.493 mOhm
21.496 mOhm
21.502 mOhm
21.499 mOhm
21.501 mOhm
21.502 mOhm
21.495 mOhm
21.492 mOhm
21.499 mOhm
21.486 mOhm
21.488 mOhm
21.499 mOhm
21.505 mOhm
21.494 mOhm
21.509 mOhm
21.501 mOhm
21.490 mOhm
21.501 mOhm
21.485 mOhm
21.492 mOhm
21.495 mOhm
21.493 mOhm
21.497 mOhm
21.492 mOhm
21.497 mOhm
21.501 mOhm
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #62 on: May 06, 2024, 06:14:07 pm »
If I reduce the gain of the amplifier (from 1050 to 105), the resolution is again worse.

DUT = 0.020 Ohm
Measure time = 1 second

Configuration 1:
R2 = 4700 Ohm
I_R2 = 1,04 mA
Amplifier gain = 1050
Output signal variation = 21 mV
Output signal noise = 20mVpp
Measures = 21.500 +- 0.1 mOhm

Configuration 2:
R2 = 470 Ohm
I_R2 = 10,4 mA
Amplifier gain = 1050
Output signal variation = 210 mV
Output signal noise = 20mVpp
Measures = 21.500 +- 0.01 mOhm

Configuration 3:
R2 = 470 Ohm
I_R2 = 10,4 mA
Amplifier gain = 105
Output signal variation = 21 mV
Output signal noise = 20mVpp
Measures = 21.500 +- 0.1 mOhm

EDIT:
The signal-to-noise ratio remains constant at approximately 50000 at full range (5Vpp in Vout).
« Last Edit: May 07, 2024, 10:49:06 am by Picuino »
 

Offline RoGeorge

  • Super Contributor
  • ***
  • Posts: 6381
  • Country: ro
Re: Homebrew Lock-In Amplifier
« Reply #63 on: May 06, 2024, 06:39:52 pm »
Nice!  :-+

Is this with the internal 10bit ADC of ATmega328, or with the 18bits ADC?

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #64 on: May 06, 2024, 07:02:28 pm »
The high gain of the amplifier also works as a kind of AA filter due to the limited bandwidth. So less gain can cause additional noise aliasing.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #65 on: May 06, 2024, 07:33:19 pm »
Changing the measurement time (summation time) also affects the result.

New program without measure error due to an error in the first sample of the ADC:
Code: [Select]
/*
   Version 1.1 (06/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.
*/

const int PIN_PWM_OUT = 3;
const int PIN_ANALOG = A6;
const int PIN_ANALOG_MUX = 6;

#define CLK_BOARD  16000000
#define UART_BAUDS  115200
#define TIMER0_PRESET 27        // Must be >= 27
#define TIMER0_FREQ  (CLK_BOARD / (((TIMER0_PRESET) + 1) * 64))  // 9250

const float MEASURE_TIME = 1;  // Seconds
const long SAMPLES_PER_LEVEL = 40;  // ADC samples per output level
const long LEVELS_PER_MEASURE = 2 * (long)((MEASURE_TIME * TIMER0_FREQ) / (SAMPLES_PER_LEVEL * 2));
const float BOARD_CALIBRATION = 8.0;  // Converts measure to milliohms

volatile long adc_sum;
volatile unsigned int adc_enable;
volatile unsigned int adc_value;
volatile unsigned long adc_samples;
volatile unsigned int level_state;
volatile unsigned char output_level;
volatile unsigned char output_level_old;
volatile unsigned char adc_measure_end;

char buff[50];
float resistance;

void setup() {
  Serial.begin(UART_BAUDS);
  Serial.print("MEASURE_TIME = "); Serial.println(1.0 * SAMPLES_PER_LEVEL * LEVELS_PER_MEASURE / TIMER0_FREQ);
  Serial.print("SAMPLES_PER_LEVEL = "); Serial.println(SAMPLES_PER_LEVEL);
  Serial.print("LEVELS_PER_MEASURE = "); Serial.println(LEVELS_PER_MEASURE);


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

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

  // Main Loop
  adc_init();
  for (;;) {
    if (adc_measure_end == 1) {
      resistance = adc_sum;
      resistance *= (BOARD_CALIBRATION / (SAMPLES_PER_LEVEL * LEVELS_PER_MEASURE));
      Serial.print(resistance, 2);
      Serial.println("\tmOhm");
      adc_init();
    }
  }
}

void loop() {}


void adc_init(void) {
  cli();
  PORTD |= (1 << PIN_PWM_OUT);
  output_level = 0;
  level_state = 0;
  delayMicroseconds(100);

  adc_sum = 0;
  adc_samples = 0;
  output_level = 0;
  adc_measure_end = 0;
  adc_enable = 1;
  sei();
}


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);
  ADCSRB = 0x00;

  sei(); //allow interrupts
}


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

    // ADC Start Conversion
    ADCSRA |= (1 << ADSC);
    delayMicroseconds(12);  // Wait for Sample and Hold

    // Read and accumulate old ADC value
    if (adc_samples) {
      // Read ADC old measure
      adc_value = ADCW;

      // Add measure to accumulator
      if (output_level_old == 0) {
        adc_sum -= adc_value;
      }
      if (output_level_old == 1) {
        adc_sum += adc_value;
      }
    }
    adc_samples++;

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

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

    // Take one measure after several samples
    if (adc_samples > SAMPLES_PER_LEVEL * LEVELS_PER_MEASURE) {
      PORTD |= (1 << PIN_PWM_OUT);
      output_level = 0;
      adc_measure_end = 1;
      adc_enable = 0;
    }
  }
}




Code: [Select]
MEASURE_TIME = 0.10
SAMPLES_PER_LEVEL = 40
LEVELS_PER_MEASURE = 22
22.04 mOhm
21.55 mOhm
21.41 mOhm
21.90 mOhm
21.52 mOhm
21.76 mOhm
22.08 mOhm
21.52 mOhm
21.89 mOhm
21.86 mOhm
21.71 mOhm
21.98 mOhm
21.60 mOhm
21.79 mOhm
22.03 mOhm
21.50 mOhm
21.98 mOhm
21.65 mOhm
21.57 mOhm
22.15 mOhm
21.48 mOhm
21.95 mOhm
21.99 mOhm
21.65 mOhm
21.74 mOhm
21.59 mOhm
21.81 mOhm
21.76 mOhm
22.13 mOhm
21.75 mOhm
21.70 mOhm
22.00 mOhm
21.54 mOhm
21.89 mOhm
21.90 mOhm
21.43 mOhm
22.26 mOhm
21.35 mOhm
21.89 mOhm
21.80 mOhm
21.38 mOhm
21.95 mOhm
21.79 mOhm
21.50 mOhm
21.99 mOhm
21.90 mOhm
21.33 mOhm
22.08 mOhm
21.33 mOhm
21.75 mOhm
21.58 mOhm
21.57 mOhm
21.78 mOhm
21.28 mOhm
21.67 mOhm
22.05 mOhm
21.76 mOhm
21.28 mOhm
21.86 mOhm
22.06 mOhm
21.40 mOhm
21.72 mOhm
21.83 mOhm
21.46 mOhm
22.15 mOhm
21.25 mOhm
21.77 mOhm



Code: [Select]
MEASURE_TIME = 0.99
SAMPLES_PER_LEVEL = 40
LEVELS_PER_MEASURE = 222
21.62 mOhm
21.68 mOhm
21.73 mOhm
21.62 mOhm
21.67 mOhm
21.64 mOhm
21.71 mOhm
21.66 mOhm
21.69 mOhm
21.68 mOhm
21.67 mOhm
21.69 mOhm
21.71 mOhm
21.74 mOhm
21.78 mOhm
21.64 mOhm
21.71 mOhm
21.72 mOhm
21.66 mOhm
21.59 mOhm
21.70 mOhm
21.71 mOhm
21.60 mOhm
21.68 mOhm
21.69 mOhm
21.68 mOhm
21.57 mOhm
21.63 mOhm
21.66 mOhm
21.64 mOhm
21.71 mOhm
21.74 mOhm
21.63 mOhm
21.76 mOhm
21.74 mOhm
21.74 mOhm
21.76 mOhm
21.70 mOhm
21.68 mOhm
21.65 mOhm
21.64 mOhm
21.67 mOhm
21.61 mOhm
21.52 mOhm
21.65 mOhm
21.64 mOhm
21.64 mOhm
21.67 mOhm
21.70 mOhm
21.53 mOhm
21.54 mOhm



Code: [Select]
MEASURE_TIME = 10.00
SAMPLES_PER_LEVEL = 40
LEVELS_PER_MEASURE = 2232
21.62 mOhm
21.59 mOhm
21.63 mOhm
21.60 mOhm
21.63 mOhm
21.67 mOhm
21.68 mOhm
21.72 mOhm
21.77 mOhm
21.72 mOhm
21.76 mOhm
21.75 mOhm
21.74 mOhm
21.61 mOhm
21.59 mOhm


EDIT:
Attached the schematic version 1.1 (with minor changes)
« Last Edit: May 06, 2024, 08:40:54 pm by Picuino »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #66 on: May 06, 2024, 07:35:13 pm »
Nice!  :-+

Is this with the internal 10bit ADC of ATmega328, or with the 18bits ADC?
All this measures still with the internal 10-bit ADC.
I wanted to do the tests before changing the results too much with the new ADC.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #67 on: May 07, 2024, 01:17:32 pm »
Before continuing, I am going to measure with the multimeter (Agilent 34401A) the AC voltage (noise) at various points in the circuit to see if there is a way to reduce the amplifier's output noise.
First I keep the Arduino nano in an infinite loop without doing anything (cli(); for(;;);)

Voltages:
GND to GND (test probes in short circuit) = 5 uVAC
GND to VCC (main 5V supply) = 80 uVAC
GND to VCC (main 5V supply) = 2 uVAC  (without Arduino nano connected)

From now on I disconnect the Arduino nano from the 5V power supply and only power it via USB so that it does not produce so much noise on the power supply.

R5-R6 voltage divisor = 3 uVAC
LMV358 output pin 1 ( 2.5Vref ) = 1 uVAC
DUT pin + = 4 uVAC
DUT pin - = 4 uVAC
Instrumentation amplifier output = 12000 uVAC   (Gain = 1050)
Instrumentation amplifier output =     540 uVAC   (Gain = 105)

Instrumentation amplifier Vcc = 5 uVAC
Instrumentation amplifier Vcc = 4 uVAC
« Last Edit: May 07, 2024, 01:24:02 pm by Picuino »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #68 on: May 07, 2024, 01:23:07 pm »
I have changed the position of the gain resistor so that it is as close to the circuit as possible.
Now the noise has decreased quite a bit.


Instrumentation amplifier output = 2200 uVAC   (Gain = 1050)
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #69 on: May 07, 2024, 01:29:26 pm »
The measures shows lower noise:

Code: [Select]
MEASURE_TIME = 0.99
SAMPLES_PER_LEVEL = 40
LEVELS_PER_MEASURE = 222
21.09 mOhm
21.10 mOhm
21.06 mOhm
20.98 mOhm
20.99 mOhm
20.98 mOhm
21.11 mOhm
21.00 mOhm
21.01 mOhm
21.00 mOhm
21.03 mOhm
21.04 mOhm
21.06 mOhm
20.99 mOhm
21.04 mOhm
20.98 mOhm
21.00 mOhm
21.11 mOhm
21.06 mOhm
21.04 mOhm
20.99 mOhm
21.02 mOhm
21.06 mOhm
21.07 mOhm
21.04 mOhm
21.10 mOhm
21.08 mOhm
21.10 mOhm
20.98 mOhm
21.12 mOhm
21.02 mOhm
21.08 mOhm

EDIT:
Conclusion: the position of the gain resistor R1 is critical. Keep the resistor as close as possible to the integrated and free of noise.
« Last Edit: May 07, 2024, 01:35:39 pm by Picuino »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #70 on: May 07, 2024, 03:30:09 pm »
I have added a quadrature accumulator to be able to make Impedance measurements.
To achieve this I had to make some changes in the schematic:
 * I have eliminated the transistor (it seems that the noise was not coming from there).
 * I have added capacitive coupling to the instrumentation amplifier.

New program:
Code: [Select]
/*
   Version 2.0 (07/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.
*/

const int PIN_PWM_OUT = 3;
const int PIN_ANALOG = A6;
const int PIN_ANALOG_MUX = 6;

#define CLK_BOARD  16000000
#define UART_BAUDS  115200
#define TIMER0_PRESET 27        // Must be >= 27
#define TIMER0_FREQ  (CLK_BOARD / (((TIMER0_PRESET) + 1) * 64))  // 9250

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 = 8.0;  // 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 int adc_value;
volatile unsigned long adc_samples;
volatile unsigned int level_state;
volatile unsigned char level_state_old;

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("SAMPLES_PER_LEVEL = ");
  Serial.print(SAMPLES_PER_LEVEL);
  Serial.println(" Samples");


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

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

  // Main Loop
  adc_init();
  for (;;) {
    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");
      adc_init();
    }
  }
}

void loop() {}


void adc_init(void) {
  cli();
  PORTD |= (1 << PIN_PWM_OUT);
  level_state = 0;
  delayMicroseconds(100);

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


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);
  ADCSRB = 0x00;

  sei(); //allow interrupts
}


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

    // ADC Start Conversion
    ADCSRA |= (1 << ADSC);
    delayMicroseconds(12);  // Wait for Sample and Hold

    // Read and accumulate old ADC value
    if (adc_samples) {
      // Read ADC old measure
      adc_value = ADCW;

      // Add measure to accumulator
      if (level_state_old < SAMPLES_PER_LEVEL) {
        adc_acc_inphase -= adc_value;
      }
      else {
        adc_acc_inphase += adc_value;
      }
      if ((level_state_old >=  0.5 * SAMPLES_PER_LEVEL) && (level_state_old < 1.5 * SAMPLES_PER_LEVEL)) {
        adc_acc_quadrature -= adc_value;
      }
      else {
        adc_acc_quadrature += adc_value;
      }
    }

    adc_samples++;

    // 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_PWM_OUT);
    }
    else {
      PORTD &= ~(1 << PIN_PWM_OUT);
    }

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



Measurements of a one-turn coiled cable with a diameter of 5 cm:
Code: [Select]
MEASURE_TIME = 1.00 s
MEASURE_FREQUECY = 744.00 Hz
SAMPLES_PER_LEVEL = 6 Samples
485.72 mOhm R  -0.18 mOhm Zc
485.68 mOhm R  -0.26 mOhm Zc
485.81 mOhm R  -0.21 mOhm Zc
486.26 mOhm R  -0.21 mOhm Zc
486.30 mOhm R  -0.29 mOhm Zc
486.78 mOhm R  -0.19 mOhm Zc
487.34 mOhm R  -0.22 mOhm Zc
487.63 mOhm R  -0.28 mOhm Zc
487.70 mOhm R  -0.27 mOhm Zc
487.77 mOhm R  -0.31 mOhm Zc
487.68 mOhm R  -0.25 mOhm Zc
487.91 mOhm R  -0.29 mOhm Zc
487.96 mOhm R  -0.28 mOhm Zc
488.01 mOhm R  -0.24 mOhm Zc
487.99 mOhm R  -0.24 mOhm Zc
487.98 mOhm R  -0.25 mOhm Zc
488.02 mOhm R  -0.20 mOhm Zc
488.25 mOhm R  -0.25 mOhm Zc
488.23 mOhm R  -0.24 mOhm Zc
488.44 mOhm R  -0.31 mOhm Zc
488.39 mOhm R  -0.28 mOhm Zc
488.41 mOhm R  -0.26 mOhm Zc
488.35 mOhm R  -0.32 mOhm Zc


Measurements of a capacitor (1000uF, 16V):
Code: [Select]
MEASURE_TIME = 1.00 s
MEASURE_FREQUECY = 744.00 Hz
SAMPLES_PER_LEVEL = 6 Samples
88.13 mOhm R  130.24 mOhm Zc
108.88 mOhm R  161.26 mOhm Zc
108.79 mOhm R  161.40 mOhm Zc
108.70 mOhm R  161.41 mOhm Zc
108.73 mOhm R  161.39 mOhm Zc
108.71 mOhm R  161.38 mOhm Zc
108.87 mOhm R  161.48 mOhm Zc
108.74 mOhm R  161.47 mOhm Zc
108.74 mOhm R  161.47 mOhm Zc
108.75 mOhm R  161.36 mOhm Zc
108.76 mOhm R  161.42 mOhm Zc
108.75 mOhm R  161.52 mOhm Zc
108.78 mOhm R  161.48 mOhm Zc
108.83 mOhm R  161.53 mOhm Zc
108.76 mOhm R  161.51 mOhm Zc
108.80 mOhm R  161.44 mOhm Zc
108.83 mOhm R  161.44 mOhm Zc
108.69 mOhm R  161.44 mOhm Zc
108.88 mOhm R  161.45 mOhm Zc
108.81 mOhm R  161.49 mOhm Zc
108.91 mOhm R  161.40 mOhm Zc
108.92 mOhm R  161.49 mOhm Zc
108.89 mOhm R  161.64 mOhm Zc
108.92 mOhm R  161.53 mOhm Zc
109.02 mOhm R  161.53 mOhm Zc
108.91 mOhm R  161.44 mOhm Zc
108.93 mOhm R  161.55 mOhm Zc
108.95 mOhm R  161.55 mOhm Zc
108.94 mOhm R  161.56 mOhm Zc
108.99 mOhm R  161.63 mOhm Zc


« Last Edit: May 07, 2024, 08:23:07 pm by Picuino »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #71 on: May 07, 2024, 03:56:02 pm »
Attached:
Signal in instrumentation amplifier output measuring a capacitor of 1000uF.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #72 on: May 08, 2024, 09:42:36 am »
I am trying to start up the ADC AD7691, but it just makes strange measurements.
With both inputs connected to 2.5V it should measure zero, but it makes full scale measurements:

Code: [Select]
65534
65533
65532
65533
65534
65532
65533
65533
65533
65532
65534
65532
65532
65533
65534
65532
65533
65533
65533
65533
65533
65533
65534
65532
65533
I am only getting the first 16 bits, discarding the last two bits.

« Last Edit: May 08, 2024, 09:45:39 am by Picuino »
 

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #73 on: May 08, 2024, 09:50:27 am »
The input range of the AD7691 is +-Vref. So the result should be signed and not an unsigned number. So the results looks like there is a slight negative offset of some 3 LSBs, which looks reasonable.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #74 on: May 08, 2024, 10:13:49 am »
 :-+
Yes, I have changed the negative input by connecting it to ground and the readings are correct.
 

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.
 

Offline 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 »
 

Offline 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 »
 

Offline 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 »
 

Offline 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?
 

Offline 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

 

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.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #125 on: May 11, 2024, 02:33:45 pm »
I have no protoboards left to solder, so I have to place an order and wait. In the meantime I'm going to try to design a custom PCB for the assembly I already have done.
I have added one small change: another connection from the microcontroller to the integrator with a larger resistor so I can make two triangular waves. One of higher frequency than the other.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #126 on: May 12, 2024, 07:17:50 am »
I found a small piece of prototyping board to solder and managed to assemble the analog part with SMD components.
« Last Edit: May 12, 2024, 07:27:57 am by Picuino »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #127 on: May 12, 2024, 07:28:31 am »
I am starting it up and everything works fine except the amplifier gain, which is zero.
The amplifier output is always 2.5V, regardless of the input signal. I have checked continuity and the reference and output pins of the amplifier are not connected.
I have also re-soldered the 47 Ohm resistor on pins 2 and 3.
I don't know what to do anymore. The circuit was working fine on the breadboard.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #128 on: May 12, 2024, 09:29:31 am »
I have changed the amplifier integrated for another one and it still does not amplify.
When I change the reference voltage (pin 6), the output voltage (pin 7, V_out) also changes with the same value. But the pins are not shorted together.

The negative input has a ramp signal of almost 5Vpp and the output maintain 2.5V of DC.

When I connect the ramped signal to the positive input and the negative input to ground, the output only increases in value when the positive input rises above 4 volts.

Attached:
Yellow = V_out
Blue = V_in+
V_in- = GND
 

Offline gnuarm

  • Super Contributor
  • ***
  • Posts: 2246
  • Country: pr
Re: Homebrew Lock-In Amplifier
« Reply #129 on: May 12, 2024, 10:42:24 am »
I'm not completely following your description of what you are measuring.  When you say Vin+, is that pin 1 or pin 4 on the connector on the schematic?

You have a DC blocking cap on pin 1.  If you ground that connector pin, it will be only an AC ground.   What is the signal like on pin 1 of U1?
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 #130 on: May 12, 2024, 10:56:07 am »
Vin- = pin 1 U1 / pin 1 J4
Vin+ = pin 4 U1 / pin 4 J4

If I ground pin 4 by connecting it to pin 3 of J4, R3 grounds pin 1 also in DC.

EDIT:
The schematic is the same as the one I had mounted on the breadboard. Therefore the schematic should not have any errors.
« Last Edit: May 12, 2024, 11:00:23 am by Picuino »
 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #131 on: May 12, 2024, 11:27:46 am »
I think you are (significantly) overdriving the input. The input voltage range is only -0.1V...3V. If pin4 is grounded, the maximum AC input voltage on connector pin 1 would be 200mVpp. And even that would still drive the output into saturation (gain 1000).
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #132 on: May 12, 2024, 11:38:06 am »
Yes, I am overdriving the input. It is the only way to get the output to show variation. With any other smaller signal, the output remains unchanged at 2.5V.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #133 on: May 12, 2024, 01:24:11 pm »
I have mounted on the breadboard another instrumentation amplifier circuit with an AD8421 to start from scratch.
https://www.analog.com/media/en/technical-documentation/data-sheets/ad8421.pdf
I continue to encounter problems. It does not amplify.

I don't know what the hell I'm doing wrong.
I've changed ICs, I've tried a different IC on the breadboard, I'm also trying it on solder board and in all cases the instrumentation amplifiers are not working.
And this after it was working yesterday without problems.

I'm pretty patient, but in this case I'm starting to get nervous.
 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #134 on: May 12, 2024, 01:55:32 pm »
Again, take care of the (rather narrow) input voltage range of the AD8421 with 5V single-supply. See red curve in figure 14 of the datasheet.
You need to bias the inputs with a common mode voltage of about 3V above the negative supply rail (which is GND in your case). 2.5V is already outside the range.
The other opamp was better suited for 5V single-supply.
« Last Edit: May 12, 2024, 01:58:27 pm by gf »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #135 on: May 12, 2024, 02:18:12 pm »
I mounted the first AD8220 I removed, again on the breadboard. Now it works correctly.

Conclusion: The AD8220 integrated circuits work correctly, but what doesn't work is the soldered PCB and I don't know why.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #136 on: May 12, 2024, 02:35:27 pm »
Eureka, I got it.
The breadboard at the top has pads that connect to the bottom. I had trimmed the pins of R1 on the AD8220 board, but it was still making connection to the bottom of the board.
Diagnosis: Rg (R1) shorted to ground.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #137 on: May 12, 2024, 03:34:39 pm »
I already have readings from several components

Program:
Code: [Select]
/*
   Version 4.4 (12/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.0

#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 SAMPLES_16

#ifdef  SAMPLES_32
#define SAMPLES_PER_WAVE 32
#define ADC_PRESCALER (0b110)

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

#define TIMER0_PERIOD (110 - 1)
#define TIMER0_PHASE_ADJUST (0.40)
#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 float BOARD_CALIBRATION = 0.2520 / (SAMPLES_PER_MEASURE);  // Converts measure to milliohms
const float BOARD_PHASE_ADJUST = -0.1074;  // Radians adjust
const float BOARD_ADDED_RESISTOR = 0.0;  // Substract board probes resistor. Sistematic Error

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,
};
#endif


#ifdef  SAMPLES_16
#define SAMPLES_PER_WAVE 16
#define ADC_PRESCALER (0b111)

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

#define TIMER0_PERIOD (220 - 1)
#define TIMER0_PHASE_ADJUST (0.4)
#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 float BOARD_CALIBRATION = 0.2512 / (SAMPLES_PER_MEASURE);  // Converts measure to milliohms
const float BOARD_PHASE_ADJUST = +0.1074;  // Radians adjust
const float BOARD_ADDED_RESISTOR = 69.0;  // Substract board probes resistor. Sistematic Error

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,
};
#endif


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

float impedance_inphase;
float impedance_quadrature;
float impedance_sign;


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) {
      read_float_values();
      phase_adjust();
      print_values();

      measure_init();
    }
  }
}


void read_float_values(void) {
  // Read accumulator values
  impedance_inphase = -adc_acc_inphase;
  impedance_quadrature = -adc_acc_quadrature;

  // Rescale values
  impedance_inphase *= BOARD_CALIBRATION;
  impedance_quadrature *= BOARD_CALIBRATION;
}


void phase_adjust(void) {

  // Get impedance sign
  if (impedance_quadrature > 0) {
    impedance_sign = 1;
  }
  else {
    impedance_sign = -1;
  }
  impedance_quadrature = abs(impedance_quadrature);

   
  // Phase adjust
  float module = sqrt(impedance_inphase * impedance_inphase + impedance_quadrature * impedance_quadrature);
  float phase;
  if (abs(impedance_inphase) < 0.1) {
    phase = 90;
  }
  else {
    phase = atan(impedance_quadrature / impedance_inphase);
  }
  phase *= impedance_sign;
  phase += BOARD_PHASE_ADJUST;
  impedance_inphase = module * cos(phase);
  impedance_quadrature = module * sin(phase);
 
  // Get new impedance sign
  if (impedance_quadrature > 0) {
    impedance_sign = 1;
  }
  else {
    impedance_sign = -1;
  }
  impedance_quadrature = abs(impedance_quadrature);
 
  // Substract board resistance (sistematic error)
  impedance_inphase -= BOARD_ADDED_RESISTOR;
}



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(void) {
  Serial.print(impedance_inphase, 1);
  Serial.print("\tmOhm R  \t");

  if (impedance_sign > 0) {
    Serial.print(impedance_quadrature, 1);
    Serial.print("\tmOhm Z_L \t");
    if (impedance_quadrature > 5.0) {
      Serial.print(impedance_quadrature * 1000.0 / (TIMER2_FREQ * 2.0 * 3.1415927));
      Serial.println("\tuHenrys");
    }
    else {
      Serial.println();
    }
  }
  else {
    Serial.print(impedance_quadrature, 1);
    Serial.print("\tmOhm Z_C \t");
    if (impedance_quadrature > 5.0) {
      Serial.print(1000000000.0 / (impedance_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) |
           (ADC_PRESCALER);  // 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
  GTCCR = (1 << TSM) | (1 << PSRASY) | (1 << PSRSYNC); // halt all timers
  TCNT0 = 0; // Initialize Timer0 counter
  TCNT2 = 0; // Initialize Timer2 counter
  GTCCR = 0; // release all timers
  sei(); // Allow interrupts

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

  TCNT0 = TIMER0_PERIOD * TIMER0_PHASE_ADJUST; // Initialize Timer0 counter
}


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



Resistor 1 ohm 5% (I have calibrated the board so that this component gives an accurate value):
Code: [Select]
SAMPLE_FREQUENCY = 9090.00 Hz
MEASURE_SIGNAL_FREQUENCY = 565.00 Hz
SAMPLE_TIME = 1.00 s
1001.3 mOhm R  0.4 mOhm Z_C
1001.3 mOhm R  0.5 mOhm Z_C
1001.4 mOhm R  0.4 mOhm Z_C
1001.2 mOhm R  0.3 mOhm Z_C
1001.2 mOhm R  0.4 mOhm Z_C
1001.3 mOhm R  0.4 mOhm Z_C
1001.3 mOhm R  0.4 mOhm Z_C
1001.2 mOhm R  0.4 mOhm Z_C
1001.1 mOhm R  0.3 mOhm Z_C
1000.9 mOhm R  0.3 mOhm Z_C
1000.8 mOhm R  0.4 mOhm Z_C
1001.0 mOhm R  0.4 mOhm Z_C
1001.0 mOhm R  0.4 mOhm Z_C
1000.9 mOhm R  0.3 mOhm Z_C
1000.9 mOhm R  0.3 mOhm Z_C
1000.9 mOhm R  0.3 mOhm Z_C
1000.9 mOhm R  0.4 mOhm Z_C
1001.0 mOhm R  0.4 mOhm Z_C
1000.7 mOhm R  0.4 mOhm Z_C
1000.9 mOhm R  0.3 mOhm Z_C
1000.6 mOhm R  0.2 mOhm Z_C
1000.9 mOhm R  0.2 mOhm Z_C
1000.7 mOhm R  0.1 mOhm Z_C
1000.9 mOhm R  0.2 mOhm Z_C
1000.7 mOhm R  0.2 mOhm Z_C
1000.8 mOhm R  0.2 mOhm Z_C
1000.8 mOhm R  0.2 mOhm Z_C
1000.8 mOhm R  0.3 mOhm Z_C
1000.8 mOhm R  0.4 mOhm Z_C
1001.0 mOhm R  0.3 mOhm Z_C
1000.7 mOhm R  0.3 mOhm Z_C
1000.9 mOhm R  0.3 mOhm Z_C
1000.8 mOhm R  0.3 mOhm Z_C
1000.7 mOhm R  0.3 mOhm Z_C
1000.8 mOhm R  0.3 mOhm Z_C
1000.9 mOhm R  0.3 mOhm Z_C
1000.8 mOhm R  0.4 mOhm Z_C
1000.9 mOhm R  0.2 mOhm Z_C
1000.7 mOhm R  0.3 mOhm Z_C
1000.9 mOhm R  0.3 mOhm Z_C
1000.9 mOhm R  0.3 mOhm Z_C
1000.9 mOhm R  0.3 mOhm Z_C
1000.6 mOhm R  0.3 mOhm Z_C
1000.8 mOhm R  0.4 mOhm Z_C
1000.7 mOhm R  0.3 mOhm Z_C


Capacitor 1000uF 16V 20%:
Code: [Select]
-0.7 mOhm R  310.7 mOhm Z_C 906.69 uFarads
-0.8 mOhm R  310.3 mOhm Z_C 907.67 uFarads
-0.8 mOhm R  310.6 mOhm Z_C 906.91 uFarads
-1.0 mOhm R  310.3 mOhm Z_C 907.80 uFarads
-1.1 mOhm R  310.3 mOhm Z_C 907.90 uFarads
-1.2 mOhm R  310.2 mOhm Z_C 908.01 uFarads
-1.2 mOhm R  310.4 mOhm Z_C 907.49 uFarads
-1.2 mOhm R  310.3 mOhm Z_C 907.87 uFarads
-1.4 mOhm R  310.3 mOhm Z_C 907.79 uFarads
-1.3 mOhm R  310.4 mOhm Z_C 907.46 uFarads
-1.3 mOhm R  310.3 mOhm Z_C 907.76 uFarads
-1.4 mOhm R  310.3 mOhm Z_C 907.71 uFarads
-1.2 mOhm R  310.3 mOhm Z_C 907.75 uFarads
-1.3 mOhm R  310.3 mOhm Z_C 907.92 uFarads
-1.4 mOhm R  310.2 mOhm Z_C 908.22 uFarads
-1.3 mOhm R  310.3 mOhm Z_C 907.94 uFarads
-1.3 mOhm R  310.1 mOhm Z_C 908.40 uFarads
-1.5 mOhm R  310.3 mOhm Z_C 907.85 uFarads
-1.4 mOhm R  310.2 mOhm Z_C 908.20 uFarads
-1.4 mOhm R  310.2 mOhm Z_C 908.20 uFarads
-1.4 mOhm R  310.2 mOhm Z_C 907.98 uFarads
-1.5 mOhm R  310.5 mOhm Z_C 907.36 uFarads
-1.4 mOhm R  310.3 mOhm Z_C 907.77 uFarads
-1.4 mOhm R  310.3 mOhm Z_C 907.85 uFarads
-1.4 mOhm R  310.2 mOhm Z_C 908.01 uFarads


Inductance 68uH 20%:
Code: [Select]
35.0 mOhm R  171.1 mOhm Z_L 48.20 uHenrys
35.1 mOhm R  171.1 mOhm Z_L 48.20 uHenrys
34.8 mOhm R  170.5 mOhm Z_L 48.03 uHenrys
34.7 mOhm R  170.6 mOhm Z_L 48.04 uHenrys
35.0 mOhm R  171.0 mOhm Z_L 48.17 uHenrys
34.8 mOhm R  171.0 mOhm Z_L 48.18 uHenrys
34.8 mOhm R  170.8 mOhm Z_L 48.11 uHenrys
34.8 mOhm R  170.7 mOhm Z_L 48.08 uHenrys
34.4 mOhm R  170.2 mOhm Z_L 47.95 uHenrys
34.5 mOhm R  169.9 mOhm Z_L 47.85 uHenrys
34.1 mOhm R  169.7 mOhm Z_L 47.80 uHenrys
34.2 mOhm R  169.8 mOhm Z_L 47.83 uHenrys
34.1 mOhm R  169.7 mOhm Z_L 47.79 uHenrys
34.2 mOhm R  170.0 mOhm Z_L 47.87 uHenrys
34.1 mOhm R  169.9 mOhm Z_L 47.87 uHenrys
34.1 mOhm R  170.0 mOhm Z_L 47.88 uHenrys
34.2 mOhm R  169.9 mOhm Z_L 47.87 uHenrys
34.1 mOhm R  170.0 mOhm Z_L 47.89 uHenrys
33.9 mOhm R  170.0 mOhm Z_L 47.88 uHenrys
34.1 mOhm R  170.1 mOhm Z_L 47.90 uHenrys
34.3 mOhm R  170.1 mOhm Z_L 47.90 uHenrys

Obviously I have to calibrate the board resistor better so that the capacitor shows its ESR.
But I like the preliminary results very much.
« Last Edit: May 12, 2024, 03:57:16 pm by Picuino »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #138 on: May 12, 2024, 04:08:15 pm »
I think it's time to go further.

The initial purpose of this thread was to make a Lock-in Amplifier and for that I have to get a more generic instrument that is capable of generating a square signal and a sine signal of the same frequency. The instrument must samples faster and has a wider bandwidth.
Finally the outputs must be continuous and use a low pass filter.

I don't really know where to start. Perhaps making a multistage input amplifier so that the gain of each stage is lower and the total bandwidth is higher.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #139 on: May 12, 2024, 04:12:31 pm »
I have placed an order for samples with Analog Devices and have been turned down because the shipping address is not a work address.
I will try to reorder the samples to have them shipped to work and if I get rejected again I will have to order them from Mouser or Digikey.
Components are not cheap, but more expensive is any other hobby. Even going out on a bike.
 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #140 on: May 12, 2024, 07:30:31 pm »
SAMPLE_FREQUENCY = 9090.00 Hz
MEASURE_SIGNAL_FREQUENCY = 565.00 Hz
SAMPLE_TIME = 1.00 s

Why 9090 Hz? It must be exactly 565 * 16, and that would be 9040.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #141 on: May 12, 2024, 08:19:23 pm »
It is the result of rounding an integer operation. It is already corrected.
I have also corrected the working frequency, which could not be that high because the ADC operates at a somewhat lower frequency.

The following posts I am going to move them to a different thread that only deals with milliohm meter because in this thread I want to continue with other different setups more related to Lock-in Amplifiers.

New thread for milliohm meter: https://www.eevblog.com/forum/projects/milliohm-meter-picuino
« Last Edit: May 12, 2024, 08:32:02 pm by Picuino »
 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #142 on: May 13, 2024, 09:07:25 am »
I think it's time to go further.

The initial purpose of this thread was to make a Lock-in Amplifier and for that I have to get a more generic instrument that is capable of generating a square signal and a sine signal of the same frequency. The instrument must samples faster and has a wider bandwidth.
Finally the outputs must be continuous and use a low pass filter.

I don't really know where to start. Perhaps making a multistage input amplifier so that the gain of each stage is lower and the total bandwidth is higher.

Start by specifying your requirements in quantitative terms.
Which carrier frequency (or frequencies)? How much bandwidth? Which signal levels do you want to measure?
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #143 on: May 13, 2024, 10:07:19 am »
In this second assembly I want to get just a little bit more. With 100kHz bandwidth I will be satisfied.
Signal levels from 100nV (max resolution) to 100mV (full scale max input).
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
« Last Edit: May 13, 2024, 11:28:09 am by Picuino »
 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #145 on: May 13, 2024, 12:06:03 pm »
100nV and 100kHz bandwidth do not go well together.
If you aim for only 10dB SNR at a signal level of 100nV, we are talking about 0.1nV/sqrt(Hz) :phew:
Which opamp do you have in mind?
Maybe your rethink and lower your expectations?

Note that your current circuit can achieve the low noise level because the filter has such a low bandwidth.
(1Hz equivalent noise bandwidth with 1s measuring time)

And what is the desired carrier frequency?
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #146 on: May 13, 2024, 12:12:37 pm »
In principle I will experiment with the ADC of the board itself. I don't have any external ADC of that speed and I would have to work with an FPGA which I am not familiar with.
I will design a bandwidth around 100 khz and see how far the circuit can go.

EDIT:
The instrumentation amplifier will be the same AD8220 that I already have. I have not started with the design. I will try to use of the samples I have, while I look at new Opamps to place a new order.
« Last Edit: May 13, 2024, 12:22:03 pm by Picuino »
 

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #147 on: May 13, 2024, 12:32:50 pm »
A lockin amplifier has a certain bandwidth for the input signal, that can be relatively large, like up to 100 kHz. That would be the maximum carrier frequency.
The other point is the bandwidth after the mixer / demodulator and output fitler. Here the bandwidth is usually relatively low (like 1 Hz  or 10 Hz) and thus low bandwidth is what allows for low noise.
With a digital implementation the filtering often is a FIR type / sliding average and not necessary a classic 1st / 2nd order low pass.

The ADC inside the STM32 is only 12 bit. This can be OK for some signals, but could be limiting with more demanding cases. To at least use most of the resolution one would than want fine (like 1:2) steps for the gain, and not just decades. It can would for a demonstartion / proof of principle, but would not be a really good instrument. AFAIR the early digital lock in amplifiers used 16 and 18 bit ADCs.

So far the example use as a mohm meter is using a carrier frequency that is generated from the same circuit. This is a common and easy case. A general lock-in would also allow an external signal (e.g. from a mechanical chopper). If one wants a mechanical chopper, one could still consider starting with the electronic clock and have a PLL type speed regulator for the motor.
 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #148 on: May 13, 2024, 12:45:49 pm »
For the 3rd use case mentioned here
https://www.eevblog.com/forum/projects/homebrew-lock-in-amplifier/msg5475103/#msg5475103
it rather looks like the bandwidth after demodulation is expected to cover audio (i.e. at least 4 kHz, better 10+ kHz).
This rules out a narrow 1-10 Hz filter.

EDIT: And I wonder which kind of modulation the vibrations of the glass apply to to the excitation signal. Really AM? Isn't rather time of flight modulated, resulting in FM (Doppler effect)?
« Last Edit: May 13, 2024, 12:58:17 pm by gf »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #149 on: May 13, 2024, 12:59:44 pm »
Making a Lock-in Amplifier similar to those currently on the market is beyond my intentions.
It would be very difficult and expensive, more so than buying a second hand LIA.
As the STM32 has 2 ADCs, one of them can be used to sample the reference signal and the other ADC to sample the input signal.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #150 on: May 13, 2024, 05:08:30 pm »
EDIT: And I wonder which kind of modulation the vibrations of the glass apply to to the excitation signal. Really AM? Isn't rather time of flight modulated, resulting in FM (Doppler effect)?

With the low frequency of sound and the associated low velocity of motion, the doppler shift would be almost impossible to detect. I am sure that in this case what needs to be detected is AM modulation.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #151 on: May 13, 2024, 05:12:07 pm »
I have to study the detector. In principle I am going to use a simple infrared led as detector and another infrared led (the same type of led) with output through a couple of small holes in series to collimate the light beam.

A laser would be more efficient and collimated, but I don't have one and I'm a little afraid to work with infrared lasers.

As I am going to test with a small crystal on my own table, I don't think there will be any problems.
 

Offline PicuinoTopic starter

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

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



I have installed Octave and I can't see the graphs.
This is the error I get:
Code: [Select]
>> sin_integer

error: parse error near line 15 of file C:\Users\User\sin_integer.m

  syntax error

>>> P = abs(Y).**2;
                ^
>>
 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #153 on: May 13, 2024, 07:47:53 pm »
error: parse error near line 15 of file C:\Users\User\sin_integer.m

  syntax error

>>> P = abs(Y).**2;

I'm running an older version of Octave.
Seems that operator .** was deprecated in the meantime?
https://savannah.gnu.org/bugs/?60882
Try .^ instead.

I also noticed that you don't place the first sample at phase 0, but at pi/SAMPLES_PER_PERIOD.
I have updated the script accordingly.

Code: [Select]

nsamples = 16;
MAXMUL = 127;

% sampling phase = 0
% x = repmat(exp(-1j*[0:nsamples-1]/nsamples*2*pi)',1,MAXMUL);

% sampling phase = pi/nsamples
x = repmat(exp(-1j*[0:2*nsamples-1]/2/nsamples*2*pi)(2:2:end)',1,MAXMUL);

y = zeros(nsamples,MAXMUL);
for i=1:MAXMUL
  % quantize to 2*i+1 levels
  y(:,i) = floor(0.5+0.5i+x(:,i)*i)/i;
end

Y = zeros(nsamples,MAXMUL);
for i=1:MAXMUL
  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 MAXMUL])
ylim([-65 -30])

 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #154 on: May 13, 2024, 08:22:20 pm »
Now it works.
I have seen that the best (low) multiplier is 61.


 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #155 on: May 13, 2024, 08:23:15 pm »
I'm going to copy the script in the other thread, with your permission.
 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #156 on: May 13, 2024, 08:25:05 pm »
Now it works.
I have seen that the best (low) multiplier is 61.

At least the best one below 64 (which is only imporant if you still want to optimize the multiplication performance).
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #157 on: May 17, 2024, 05:33:40 pm »
I already have the STM32 microcontroller sampling the ADC at 5333kHz.
The problem I have now is the large computational power needed to multiply two streams from the two ADCs at more than 5MHz in real time and implement, in addition, an IIR or FIR filter for the output.
And all that for the microprocessor to do the same as a simple AD630, but worse.

I think you should start with a much lower ADC speed and go up from there.
I would also have to be clear about extra functions that the microcontroller is capable of to justify its use.
 

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #158 on: May 17, 2024, 06:11:53 pm »
Usually one does not want an exact copy of the ref. signal to do the multiplication. One usually wants at least a constant amplitude for the reference signal.
The operation of the AD630 is different: it only does a +-1 multiplication, not a true multiplication. The multiplication with a clean sine can suppress the other harmonics.


To reduce the computational effort one can initially apply a little low pass filtering, kind of part of the anti aliasing. Even just averaging 2 readings can reduce the computational effort to nearly half.
It can make sense to run the ADC at a little less than it's maximum speed. The max speed is kind of set by the noise / INL starting to get worse from there.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #159 on: May 18, 2024, 10:04:37 am »
I didn't realize that the STM32 board I am currently using (NUCLEO-L412KB) does not have a DAC.
I have placed an order for another board, a NUCLEO-G431KB, which runs at more than twice the speed (170MHz) and has 2 ADCs and 2 buffered DACs.
With them I can generate a reference output signal (1 DAC) and sample two signals, the reference output signal and the one coming from the DUT.


By the way I have also bought some instrumentation amplifiers INA826 from Texas Instruments, which work at 3.3V although they are a bit slow.
For further amplification I have bought precision opamps OPA2325, also from Texas Instruments.

EDIT:
https://www.st.com/resource/en/datasheet/stm32g431kb.pdf
https://www.ti.com/lit/ds/symlink/ina826.pdf
https://www.ti.com/lit/ds/symlink/opa2325.pdf


Other amplifiers that I have not purchased, but should consider (recommended by TI):
https://www.ti.com/lit/ds/symlink/ina333.pdf
https://www.ti.com/lit/ds/symlink/opa322.pdf
https://www.ti.com/lit/ds/symlink/opa320.pdf
https://www.ti.com/lit/po/sbat003a/sbat003a.pdf
« Last Edit: May 19, 2024, 11:02:57 am by Picuino »
 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #160 on: May 18, 2024, 11:46:11 am »
The problem I have now is the large computational power needed to multiply two streams...

The multiplication is not the problem. AFAIK, the SMLAL instruction takes only one cycle. And gcc does recognize the pattern

Code: [Select]
int64_t c;
int32_t a, b;
c += (int64_t)a * b;

and emits a SMLAL instruction. The problem is more the necessary glue code to shuffle the data around.

Note that the clock frequency is only 5x higher than the clock frequency of your AVR, but you want a 500x higher data rate. And now you expect that the more efficient instruction set of the ARM processor can compensate the remaining gap of factor 100x? No, it cannot.

Nevertheless I think that it may be feasible to run the algorithm of your Milliohm meter (single channel) at 4 MSa/s, if you do it smart. Collect the samples with double buffered DMA, and process all samples in a received DMA buffer at once. At that speed it is no longer possible to process an interrupt for each sample. That's ways too much overhead per sample.

If you look here, you can see that the innermost loop (label .L2, which is executed for each sample) has 11 instructions, and I think that's 14...17 clock cycles, and the code outside the loop (which uses significantly more cycles) runs only once for 256 samples, so let's assume ~1 extra cycle per sample. At 80 MHz clock and 4 MSa/s, you can spend 20 cycles/sample. So I think that's in the ballpark.

And if you decimate before demodulation (see here), as Kleinstein suggested, then the overhead per sample becomes even smaller. Here, the loop at label .L2 now processes 8 samples at each iteration.

Quote
I have placed an order for another board, a NUCLEO-G431KB, which runs at more than twice the speed (170MHz) and has 2 ADCs and 2 DACs.

I'm unsure if the amount of RAM is sufficient. It depends of course on the exact algorithms you want to run at the end. It may be possible to put constant tables in flash, but I'm not sure if data access to flash also has zero wait states.



Btw, see also:
https://wiki.st.com/stm32mcu/wiki/Getting_started_with_ADC
https://www.st.com/resource/en/application_note/an2834-how-to-optimize-the-adc-accuracy-in-the-stm32-mcus-stmicroelectronics.pdf
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #161 on: May 18, 2024, 12:09:28 pm »
In this case I want to make a more generic Lock-in amplifier.
The routine I am thinking of is something like this:
* Fill with DMA two 256-sample (16bit type) buffers coming from the two ADCs.
* With the half-transfer buffer interrupt, multiply the two half buffers into another (32bit type) half mult-buffer.
* With the full-transfer buffer interrupt, do the same with the other half of the buffer.
* With another timer interrupt, sum all the mult-buffer values and pass them through an IIR filter to deliver the value to the DAC output.

The result should do, by software, the same as an AD630 plus a low-pass filter.
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #162 on: May 18, 2024, 12:16:54 pm »
Analog Devices has a chip for digital synchronous detection, the ADA2200, which is now obsolete.
https://www.analog.com/media/en/technical-documentation/data-sheets/ADA2200.pdf

It may be interesting to study it.
 

Offline gnuarm

  • Super Contributor
  • ***
  • Posts: 2246
  • Country: pr
Re: Homebrew Lock-In Amplifier
« Reply #163 on: May 18, 2024, 09:30:04 pm »
This very much sounds like a problem for an fpga.  You won't be constrained by the processing speed of your CPU, and you can make the multipliers work with any data size you wish.  You will need to learn a new language.  More importantly, you will need to learn to think in terms of a multiprocessing language.  In fpgas, everything happens in parallel, or much of it, anyway. 
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 #164 on: May 19, 2024, 07:22:10 am »
I have two FPGA development boards. A Cyclone II and an Alhambra Zum, but I have never run them.

Keep in mind that my intention is to make a homebrew lock-in amp and working with FPGAs doesn't seem very compatible. I would need to add external ADCs with parallel connections, which would probably involve designing a dedicated PCB and soldering components with difficult encapsulations. It would have a lot of power, but it certainly wouldn't be homebrew.

In the world of microcontrollers there are all kinds of models from the most basic 8-bit to the most powerful 32-bit models. There are options from THT and also SMD packages with easy to solder pins, to the smallest and most difficult.
In the FPGA world, however, I don't see models in between. All I find are unwieldy models for home assembly.
 

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #165 on: May 19, 2024, 08:10:00 am »
For a simple solution the µC with 12 bit ADCs is ok. The computation power should not be that much of an issue. After all even with 2x5 MSPS the CPU should still be able to keep up. It should not be much more than 1 MAC operation and 1 table lookup per sample.  The speed would mainly effect the upper frequency limit and many application for a lock-in amplifier are lower frequency, like 100 Hz-1 kHz.
There is also the alternative way of first average the time domain data and do the muliplication with the sine (or other function for correlation) only at the lower output rate.

The more limiting factor is more the resolution of the ADC, so that the headroom for noise is limited. It can be OK for not so noisy signals, but one application for a lock-in amplifier is with signals that are burried in a lot of noise. AFAIR the lock in amplifier I use a lot had settings for the headroom of 40 and 60 dB. So in the higher reserve setting the signal would be something like 1000 times smaller than the input dynamic range.  So an external ADC would not be such a bad idea for a higher performance lockin. It could still make sense to use a µC with an external ADC.

The computational more demanding case could be if one wants a really external reference and thus a PLL for the reference part. A software PLL is possible and has some advantages over a hardware solution as it is easier to adapt over a large frequency range (e.g. changing the loop filter).

 

Offline gnuarm

  • Super Contributor
  • ***
  • Posts: 2246
  • Country: pr
Re: Homebrew Lock-In Amplifier
« Reply #166 on: May 19, 2024, 09:14:22 am »
I have two FPGA development boards. A Cyclone II and an Alhambra Zum, but I have never run them.

Keep in mind that my intention is to make a homebrew lock-in amp and working with FPGAs doesn't seem very compatible. I would need to add external ADCs with parallel connections, which would probably involve designing a dedicated PCB and soldering components with difficult encapsulations. It would have a lot of power, but it certainly wouldn't be homebrew.

In the world of microcontrollers there are all kinds of models from the most basic 8-bit to the most powerful 32-bit models. There are options from THT and also SMD packages with easy to solder pins, to the smallest and most difficult.
In the FPGA world, however, I don't see models in between. All I find are unwieldy models for home assembly.

fpgas are available in quad flat packs, often 100 pin.  You can fashion DAC by using a very fast clock and generating a pulse train with a high speed clock.  The modulation would be pulse density and use a simple RC filter to remove the higher frequency artifacts.  If you need limited resolution, but higher sample rates, you can use multiple output pins with an R/2R ladder.  N bits uses N output lines.

ADC is similar, but the effective sample rate would need to be moderately low.  There's a sample rate/resolution trade off.  15 bits at 48 kSPS is realistic using a clock rate of ~32 MHz (if I remember the math correctly).  I'm not sure what sample rates you require.  An effective sample rate above 100 kSPS would likely need a separate ADC chip. 

I will say, I don't know what you mean by "homebrew".  Can you explain what this means to you? 
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 #167 on: May 19, 2024, 10:22:20 am »
For example the other experiment I have done I consider it homebrew.
It uses an Arduino Nano as microcontroller (Atmega328P) easy to get and easy to program. As analog part I have used 8-pin SMD chips (MSOP-8) that can be soldered with soldering iron (no hot air station needed). I will also test an INA826, much cheaper than AD8220 and with an even simpler package (SOIC-8).

I consider FPGAs far from a homebrew project for now.

In any case, I have achieved acceptable results with a simple Atmega328 at 16MHz and with 10bits ADC at 10ksps ADC. With the STM32 I should be able to get something practical and better (170MHz and 12bit ADC at 5Msps).
 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #168 on: May 19, 2024, 10:29:31 am »
There is also the alternative way of first average the time domain data and do the muliplication with the sine (or other function for correlation) only at the lower output rate.

Some STM32 models (including the STM32L412xx) have a hardware oversampler that does exactly this, so you can benefit from the resolution enhancement and SNR reduction without overstraining the DSP code with too high a sample rate.

 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #169 on: May 19, 2024, 10:44:32 am »
I'm actually going to switch microcontroller to an STM32G431KB, which does have DACs and runs at double CPU speed.
This other micro also has that function, which I will study to see if it is useful. The problem is that I wanted to be able to sample 100kHz signals with several samples and that is difficult to do below 1Msps.
That microcontroller I want to run at 160MHz and the ADC clock speed at 80MHz (1/2), which produces 5333Msps.

Maybe there is enough speed left to decimate 4 samples in one and reduce the sampling speed to 1.33Msps.
 

Offline gnuarm

  • Super Contributor
  • ***
  • Posts: 2246
  • Country: pr
Re: Homebrew Lock-In Amplifier
« Reply #170 on: May 19, 2024, 10:51:51 am »
For example the other experiment I have done I consider it homebrew.
It uses an Arduino Nano as microcontroller (Atmega328P) easy to get and easy to program. As analog part I have used 8-pin SMD chips (MSOP-8) that can be soldered with soldering iron (no hot air station needed). I will also test an INA826, much cheaper than AD8220 and with an even simpler package (SOIC-8).

I consider FPGAs far from a homebrew project for now.

In any case, I have achieved acceptable results with a simple Atmega328 at 16MHz and with 10bits ADC at 10ksps ADC. With the STM32 I should be able to get something practical and better (170MHz and 12bit ADC at 5Msps).

Sorry, I have no idea why an MCU board bought off the Internet is "homebrew" and an fpga board bought off the Internet is not homebrew. 

As to the MCU with ADCs, remember that when run at high sample rates, the lower one or two bits are essentially "noise".  Sounds like you don't really know what you need and are just toying around trying to get something to work.  Good luck.
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 #171 on: May 19, 2024, 11:24:25 am »
Yes, I am playing with the STM32 with two targets. To know how to use it (I had not implemented it before) and to make with that microcontroller a Lock-in amplifier that is able to do something practical.
It is not a job where I have to achieve a certain goal. I am a Technology teacher and I just try to keep up to date myself and make some practical instrument for demonstrations in class.


EDIT:
I don't rule out running one of my FPGA boards or another more modern one. But I have to go in parts. For now I'm too busy with the STM32.
« Last Edit: May 19, 2024, 11:27:23 am by Picuino »
 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
Re: Homebrew Lock-In Amplifier
« Reply #172 on: May 19, 2024, 11:59:10 am »
Of course, another goal is to make easy replicable projects by anyone else. FPGAs is a little bit more difficult than uC.
 

Offline gf

  • Super Contributor
  • ***
  • Posts: 1295
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #173 on: May 19, 2024, 12:23:42 pm »
Maybe there is enough speed left to decimate 4 samples in one and reduce the sampling speed to 1.33Msps.

The hardware oversampler can do this without any software overhead. It samples at 5.33 MSa/s and delivers decimated samples at 1.33 MSa/s to the processor. However, the summation of 4 samples each corresponds to a 1st order boxcar filter, and that's not a proper decimation filter. This implies that the (analog) anti-aliasing filter in front of the ADC must be steeper in order to provide sufficient attenuation for frequencies >= 1233 kHz (unless you can ensure otherwise that no interfering signals >= 1233 kHz are present at the ADC input). For example, a 4th order Butterworth with fc=160kHz gives >70dB attenuation for frequencies >= 1233 kHz, while 100kHz still fall into the passband, being attenuated only about 0.1dB.
« Last Edit: May 19, 2024, 12:29:01 pm by gf »
 

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14367
  • Country: de
Re: Homebrew Lock-In Amplifier
« Reply #174 on: May 19, 2024, 01:06:21 pm »
An anti aliasing filter is a good idea with a SAR or similar ADC. The main reason would be avoiding extra noise, not so much a strong interference signal. For the noise suppresion there is no need for the AA fitler to get very strong attenuation, just 20 dB already do a lot for the noise.  A strong interference is often more an issue at low frequencies, with mains hum and the 2nd harmonic.
Especially with the limited ADC resolution filtering at the low end or notches can make sense.

Averaging 4 samples is not a perfect filter, but it is still not that bad compared to a more conventional digital filter.

Using a similar sampling and math also on the ref. channel is an unusual solution for a lock-in amplifier. It could be a first step to do a software PLL, though for the ref. side a simple on/off digital input is often sufficient.
Digitizing 2 inputs and directly multiply and average would be part of getting the cross-correlation function. This is related to a LI and may be an interesting addition, but it is not the normal LI function.

Many on the articles decribing LI operation are a bit short on the reference part and often asume quite some ideallization there. For the ref. side there are different cases, depending on the experiment / setup:
1) generate the ref. / driving signal - this is the easiest case
2) generate the ref. frequency and PLL drive a chopper: the PLL / regulator part could be external
3) read an external ref signal and use it directly for the PSD / detection. This may need a way to correct for duty cycle changes
4) read an external ref signal and use a PLL to generate a clean ref. signal for the PSD.
5) read an external ref signal and use a PLL to generate a clean ref., with a delay for the input data to compensate for the group delay of the PLL

There are cases where one would read a 2nd signal, e.g. to compensate for intesity variations of a light source. The 2nd channel tends to be less demanding, with often a better S/N ratio. The amplitude ref. is however often separate from a frequency/phase reference.

 

Offline PicuinoTopic starter

  • Frequent Contributor
  • **
  • Posts: 975
  • Country: 00
    • Picuino web
« Last Edit: May 21, 2024, 05:27:11 pm by Picuino »
 

Offline zrq

  • Frequent Contributor
  • **
  • Posts: 295
  • Country: 00
Re: Homebrew Lock-In Amplifier
« Reply #176 on: May 21, 2024, 06:33:13 pm »
I just did some very quick and dirty tests using labview and my 3rd gen Focusrite 2x2 sound card to work as a lock-in ampliifer, apparently one can easily get >110 dB dynamic range at 311Hz and 1112 Hz carrier frequency for 1 Hz IF bandwidth, characterized by comparing the demodulated signal strength when (Line) input is open and when the loopback signal reaches full scale. And it appears to be limited by the crosstalk from the internal generator (using the headphone output), particularly at higher carrier frequencies, so by doing a simple "zeroing" it can be even better.
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf