Back to index

STM32 Inductance Meter

View source on GitHub

Background

As part of another project, I needed to make my own inductors in the 100-1000uH range. I wrote a python script to control my oscilloscope and function generator to measure them, but I ultimately wanted something standalone where I could just plug in an inductor. I had my STM32L476G-Discovery development board left over from a class in college so I decided to use that.

Theory

The project is pretty simple in theory: apply a sine wave to a resistor and the inductor under test in series then measure the total voltage across both and the voltage across just the inductor.

The impedance of the inductor can then be determined by the ratio of voltages.

The impedance of an ideal inductor is proportional to the inductance and frequency. Plotting the angular frequency vs impedance gives the inductance as its slope. By measuring impedance at multiple frequencies and using the slope, I minimize the impact of any stray signals that may be present at only one frequency. I can think of three possible sources of interference:

Since I did not include an antialiasing filter, any of these could be aliased to the same frequency as the signal and affect the measurement.

In theory I should be able to determine the ESR (equivalent series resistance) of the inductor from the y intercept of the graph, however this proved impractical. The parasitic capacitance of the inductor causes the graph to bend slightly upward which artificially increases the slope and pushes the y intercept below 0. This does not have a significant impact on the inductance measurement, but does render the y intercept useless.

In order to accurately measure the amplitude of the sine waves, as well as to reduce noise, I use a discrete Fourier transform at the frequency of the applied sine wave.

Image from Wikipedia

Using the DFT has a few advantages:

Normally there's a couple gotchas with using DFTs, but I carefully avoid them:

Aliasing is another potential problem, which I have ignored entirely. Frequencies above 666kHz will alias. In the future I may add antialiasing filters. See the improvements section below. In practice however, aliasing does not appear to be a significant source of error.

Implementation

The implementation is pretty straightforward: generate a sine wave at a bunch of different frequencies, measure the impedance at each frequency, then use linear regression to find the slope, which will be the inductance.

There are two tricky parts:

Generating sines

The sine wave generation is mostly straightforward. The DAC uses DMA to load each sample from a sine lookup table. A timer triggers each load every 15 clock cycles (f=1.333Msps).

The tricky part is changing frequencies. To change frequency, the length of the DMA buffer must be changed to represent the new period and the data in the buffer must be changed to reflect that of the new sine wave. However, you cannot change the buffer length while the DMA channel is active. You must change it between samples. The new data also needs to be copied in to the buffer, but only after the DMA has already read the data being overwritten.

Luckily, there are two status flags in the DMA interrupt status register that indicate when the transfer is half done (HTIFx) and when it is completely done(TCIFx). When it is half complete I can copy the first half of the new data in. After it is 100% complete I can change the length (between the last sample and the first sample) and copy the second half.

Normally interrupts would be great for this. However, the interrupt latency is 12 cycles on a cortex-m4. Since changing the DMA buffer length needs to be done in under 15 cycles, doing it in the interrupt would only leave 3 cycles to change it.

Instead of using interrupts, I poll the status bits. I use inline assembly so I can load all of the constants I need into registers before starting the polling loop, ensuring it switches the length as quickly as possible once the bit gets flipped. See src/dac.c for the details.


__asm volatile(
        // wait for the DAC to read the last element
        "loop%=:\n" // wait for the TC flag
        "       ldr     r0, [%[rTCIF]]\n"
        "       cmp     r0, #1\n"
        "       bne     loop%=\n"
        // turn off DMA, Use %[DMAen] as a 0 since only the LSB matter when bit-banding
        // and since all BB'd addressed are word aligned it's always 0;
        "       str     %[rDMAen], [%[rDMAen]]\n"
        "       str     %[rLen], [%[rDMAlen]]\n"
        "       str     r0, [%[rDMAen]]\n"
                : // no outputs, so the volatile keyword is redundant
                : [rTCIF]"l"(&BB(DMA2->ISR)[DMA_ISR_TCIF5_Pos]), // tcif5 flag bit
                        [rDMAen]"l"(&BB(DMA2_Channel5->CCR)[DMA_CCR_EN_Pos]), // dma en bit
                        [rLen] "l" (sine->len), // new len to load
                        [rDMAlen] "l" (&DMA2_Channel5->CNDTR) // address to but new len in
                : "cc", "r0"
        );

Calculating the DFT

Calculating the DFT at one frequency is just the sum of products of the data and cosine (for the real part) or -sine (for the imaginary part). These are stored in static variables and updated by the ISR that fires after the ADC collects half a buffer of data.

I originally had terrible performance until I realized that since the sums were marked volatile (due to the ISR modifing them), the code in the IRQ handler itself was writing the sum back to memory every time it modified them instead of keeping them in registers. This is especially painful since the sums are int64_t and reading or writing them to memory needs to load or store both registers.

Electronics

The electronics are simple. Op amps buffer the DAC output and the ADC inputs. One of the op amps is used as a virtual ground. I used an LM324 op amp because it's what I had on hand, but it's really a poor choice. It is prone to crossover distortion and has an abysmal slew rate of 0.5V/us. I added resistors from the op amp output to ground to fix the crossover distortion, but there's nothing to be done about the slew rate. Finally, its output can't get closer than ~0.5V from ground when sourcing a few milliamps of current. A much better op amp choice would be a MCP6294.

Results

I'm pretty pleased with the results. Readings are consistent both with itself and with the original oscilloscope and function generator based version. I can get ~2uH of resolution when measuring a 1080uH inductor, which is pretty good.

I was hoping to also calculate the ESR of the inductor by using the y intercept of the frequency vs impedance graph. Unfortunately the parasitic capacitance causes a slight bend upward in the graph which while not effecting the slope in a major way, does push the y intercept negative, which indicates the measurement is nonsensical.

Improvements

The biggest improvement would be adding gain to the op amp measuring the voltage across the inductor. Currently the voltage is quite low, even with a large (1080uH) inductor and most of the ADC resolution is wasted. A digipot could be used to set the gain, though increasing the gain would certainly call for a better op amp.

I'd also like to run the ADC at a faster sample rate, which would require the DFT calculation to be faster. The Cortex-M4 has instructions as part of the DSP extension that would work well: SMLAL{TT,TB,BT,BB}. These would allow me to load both sine and cosine values packed as two 16 bit halfwords in a word and directly multiply them with the packed halfwords from the ADC. Unfortunately, GCC 10.x has a regression that prevents them from being generated. I've bisected GCC and found the commit it was introduced in. I'm currently trying to see if I can fix it and submit a patch upstream.

Adding antialiasing filters would reduce the chance of unwanted signals aliasing to a frequency being measured. Since I'm sampling at a much higher frequency than the signal, a simple RC filter would probably be more than adequate. Additionally, adding a low pass filter to the DAC output could eliminate the stairstep effect.


Back to index