22-bit Capacitance to Digital Converter

A new capacitance displacement sensor interface for my seismometer with improved performance and low cost.

Similar projects worth following
I would like to improve the sensitivity of the data acquisition system used in my homemade seismometer. I'm hoping circuit will deliver more than 21-bits (126.4 dB of dynamic range) to push the seismometer sensitivity below 10nm.


My homemade seismometer uses a fully differential capacitance displacement sensor to detect the movement of the earth. The sensor interface is an AD7745 capacitance-to-digital converter. The AD7745 has some advantages:

  • It's easy to interface. It provides excitation drivers and can interface directly with a fully differential capacitance bridge.
  • It's low power -- less than 1mA supply current.
  • It uses the I2C interface for communication.
  • Relatively inexpensive at $11 or so in low quantities.

And disadvantages:

  • The AD7745 was released around 1995 and is showing its age relative to other high-bit ADC converters.
  • Advertised as a 24-bit converter, it can only really produce about 16-bits of noise free resolution.
  • Susceptible to EM interference because there is no input filtering.
  • It's not ratiometric. It relies on a 1.17V internal voltage reference. 
  • Pretty poor tempco of the converted capacitance.

A chopper-stabilized sensor interface:

See the log files for all the details. The system uses a square wave modulator to drive the top and bottom of the capacitor sensor bridge between zero and VCC (the supply voltage). The maximum differential capacitance that the sensor can develop across the output is ±4pF when the displacement is ±5mm. At zero displacement all of the capacitors in the bridge are 2pF and the differential output capacitance is zero.

   V(OUT+ - OUT-) = Vout = 2VCC ∆C/C1

    If C1 = C2 = 10pF, Max Vout = ±0.8VCC.

If this output is fed to a 24-bit ADC using VCC for its reference (ratiometric conversion) then it should be able to produce a noise free resolution approaching 22bits (>130dB dynamic range). If theory matches reality then the capacitor sensor should be able to improve the displacement sensor performance by a factor of about 10x-30x, which would allow it to resolve displacements of less than 10nm. But that's still a long way from a professional instrument's capability.

This is the circuit I’ve designed to implement the concept.

The 100kHz clock generator for the modulation is a PIC10F202 — a 6 pin micro controller with an accurate internal clock. The exciters are driven by two Schmitt trigger inverters. The AC amplifiers are TP07 (or OPA376) opamps biased at TBD VCC. The SPDT switches are Vishay DG3157E -- which have low charge injection (<2pC). 

I found a 24-bit sigma-delta ADC with decent noise performance — the MAX11200. When sampling at 245kHz (decimated to 5 Hz) the ADC is expected to have a noise free resolution of 21 bits — that’s a noise floor of 3.45µV p-p when using a 3.6V VREF ( input range = ±0.8VCC = 5.76V).

The LP2985 LDO has a bypass cap to reduce the noise on VCC. I'm pretty paranoid about noise on this circuit.

One last thing: This approach would lend itself to a force balance wide-bandwidth type seismometer.


Bill of Materials spreadsheet

ms-excel - 10.00 kB - 07/20/2019 at 14:17


These are the Gerber files to create the 2-sided PCB. You can upload this zip file to OSH Park, or JLCPCB and get boards built.

Zip Archive - 62.12 kB - 07/19/2019 at 21:18



Baxter, L. K., "Capacitive Sensors", revised 2000-07-20. This is a nice treatise on interfacing to capacitance sensors. Chapter 5 covers signal conditioning circuitry.

Adobe Portable Document Format - 127.25 kB - 05/31/2019 at 12:11


  • Software

    Bud Bennett07/18/2019 at 20:36 0 comments

    These code snippets are what I'm using to interface with the C2D. This first one is the class for the C2D converter. When instantiated it performs a self calibration for gain and offset. Otherwise, the user must customize it for use.

    #!/usr/bin/env python3
    #  Connections are:
    #     CLK => SCLK  
    #     DOUT =>  MISO
    #     DIN => MOSI
    #     CS => CE0
    import time
    import sys
    import spidev
    import math
    class C2D:
        # commands
        SelfCalibration = 0x90
        SystemOffsetCal = 0xA0
        SystemGainCal = 0xB0
        Convert1p0 = 0x80
        Convert2p5 = 0x81
        Convert5p0 = 0x82
        Convert10 = 0x83
        # register addresses
        regSTAT = 0xC0
        regCTRL1 = 0xC2
        regCTRL2 = 0xC4
        regCTRL3 = 0xC6
        regDATA = 0xC8
        regSOC = 0xCA
        regSGC = 0xCC
        regSCOC = 0xCE
        regSCGC = 0xD0
        def __init__(self, debug=False):
            self.spi = spidev.SpiDev()
            # Enable Self Calibration
            # Performing System Self Calibration.
        def command(self, register):
        def writeReg(self, register, dataList):
            registerData = [register]
            for data in dataList:
        def readReg(self, register, dataList):
            registerData = [register+1]
            for data in dataList:
            r = self.spi.xfer2(registerData)
            return r[-len(dataList):] # toss out the first byte
        def twosComplement(self,data):
            result = data[0] << 16 | data[1] << 8 | data[2]
            if result > (1 << 23) - 1:
                result = result - (1 << 24)
            return result
        def convert2volts(self, data):
            v = data/(2**23-1) * 3.6
            return v
        def readADC(self):
            r = self.readReg(self.regDATA,[0,0,0])
            return self.twosComplement(r)
        def readSelfCalOffset(self):
            r = self.readReg(self.regSCOC,[0,0,0])
            return self.twosComplement(r)
        def readSelfCalGain(self):
            r = self.readReg(self.regSCGC,[0,0,0])
            return self.twosComplement(r)/2**23
        def readSystemOffset(self):
            r = self.readReg(self.regSOC,[0,0,0])
            return int(self.twosComplement(r)/4)
        def readSystemGain(self):
            r = self.readReg(self.regSGC,[0,0,0])
            return self.twosComplement(r)
        def meanstdv(self, x):
            Calculate mean and standard deviation of data x[]:
            mean = {\sum_i x_i \over n}
            std = math.sqrt(\sum_i (x_i - mean)^2 \over n-1)
            n, mean, std = len(x), 0, 0
            for a in x:
                mean = mean + a
            mean = mean / float(n)
            for a in x:
                std = std + (a - mean)**2
            if(n > 1):
                std = math.sqrt(std / float(n-1))
                std = 0.0
            return mean, std
    if __name__ == '__main__':
        # instantiate C2D
        cap2dig = C2D(debug=True)
        print("-1 = {0}".format(cap2dig.twosComplement([0xff,0xff,0xff])))
        print("1 = {0}".format(cap2dig.twosComplement([0x00,0x00,0x01])))
        # set CTRL3 register
        CTRL3 = cap2dig.readReg(cap2dig.regCTRL3,[0])
        print("CTRL3 = {}".format(hex(CTRL3[0])))
        #config register: SCYCLE = 1, SIGBUF = 0
        CTRL1 = cap2dig.readReg(cap2dig.regCTRL1, [0])
        print("CTRL1 = {}".format(hex(CTRL1[0])))
        print("Self Cal Offset = {0}".format(int(cap2dig.readSelfCalOffset())))
        print("Self Cal Gain = {0}".format(cap2dig.readSelfCalGain()))
        print("System Offset = {0}".format(cap2dig.readSystemOffset()))
        print("System Gain = {0}".format(cap2dig.readSystemGain()))
        result_array = []
        oldSTAT = 0x00
        n = 0
        sd_avg2 = float(0)
            while True:
                # start conversion
                # wait for result
                STAT = cap2dig.readReg(cap2dig.regSTAT,[0])
                if (STAT != oldSTAT):
                    print("STAT = {}".format(hex(STAT[0])))
                    oldSTAT = STAT
                val = cap2dig.readADC()
                print ("ADC Result: {0}".format(int(val)))
                if (len(result_array) == 10):
                    n += 1
                    mean,sd = cap2dig.meanstdv(result_array)
                    result_array = []
                    print("\n\tmean: {0} Counts".format(mean))
                    print("\tstd dev: {0:.4f} Counts".format(sd))
    Read more »

  • The Answer is Blowin' in the Wind

    Bud Bennett07/18/2019 at 17:41 0 comments

    The diet cola experiment was a success. The raw data over 2.5 days shows the daily peak-peak variation dropped almost 10X:

    But there are still periods of jiggyness(?):

    The artifacts that have waveforms that start with large amplitudes and tail off are probably small magnitude quakes. The artifacts that appear to be ball-shaped are apparently caused by wind. The sharp spike around 2019-07-16 00:00 is probably a nearby lightning strike. I was monitoring the weather outside over this period (except at night) and found that the jiggyness correlated closely with an uptick in the windspeed. The times indicated above are UTC (00:00 UTC is 6:00pm MDT). That last bit of jiggyness occurred around 2:00am MDT and I happened to be awake enough to hear it. 

    I did not see these artifacts with the AD7745 because the noise floor is higher and the filter BW is now 0.01Hz - 0.5Hz vs. 0.025Hz - 0.5Hz for the AD7745. Also, the wind tends to increase around the same time of day lately, which corresponds to the transition from rising to falling in the raw waveform. This led me to assume that it was somehow related to temperature. Another interesting item: the wind seems to cause the house foundation to tilt, which is evidenced by the flattening of the raw data curves at the peaks. 

    Moving On:

    I took the seismometer electronics apart and installed the latest C2D board with a 6-inch ribbon cable, shielding and foam insulation. I located the sensor inside the seismometer enclosure and ran the ribbon cable outside the enclosure to the RPi-B+. This should keep the RPi-B+ from causing temperature variation inside the enclosure. The ribbon cable running under one side of the enclosure may cause a small air leak, but I'm hoping that the cola cans absorb it. So far, everything appears to be working but I'm not getting the 21.5 bits of dynamic range that I saw on the bench:

    At this point I don't have any explanation as to the decreased performance when the C2D is coupled to the sensor. It is 3-4X better than the AD7745. That will have to be good enough until I have the time and ambition to investigate further.

  • 21.5 Bits!

    Bud Bennett07/16/2019 at 13:31 2 comments

    Another C2D board to play with:

    Built a second C2D using noise optimized components and TP07 opamps. The TP07 opamps are quite a bit less expensive than the OPA376 opamps, but the common mode range of the TP07 inputs is GND to VCC - 2V, so the COMMON bias voltage was dropped to 1.53V. The full scale range of the C2D converter is not affected because a ±4pF capacitance change at the input produces ±1.44V at the output.

    I verified that the C2D was operating correctly and generating appropriate full-scale output voltages by connecting a 4.7pF capacitor between the two CAP inputs and EXC pins. This second board even has an offset voltage with the inputs unconnected that is within a few mV of the first board.

    When I connected the new C2D to a Raspberry Pi 3B+ with nothing connected to the inputs, the resulting noise waveform was a bit disappointing. The peak-peak count variation was less than 10, but there was a -15 count spike that occurred with a period of exactly 60 seconds. I made sure that there were no other daemon programs of mine running that would cause this kind of interference. Varying the sample rate of the C2D had no effect on the 60 second spike. The standard deviation of the ADC output was around 2 counts, which is 18.5 bits of NFDNR.

    I then connected the C2D to a Raspberry Pi Zero-W, again with no inputs connected. This time the output waveform noise was much worse -- 20 counts p-p -- but the 60 second spike disappeared. So now I have some evidence that the C2D board is being influenced by its proximity to the RPi.

    Getting Some DIstance:

    I found a 6-inch long ribbon cable with 0.1 inch header sockets on both ends that I used to separate the C2D from the RPi. I also added copper foil shielding and foam insulation to the C2D board. Things improved a lot, but there were still some strange drift in the output that I could not explain. This disappeared when I connected the GND of the C2D to earth ground.

    Here's the output of the C2D connected to the RPi-3B+, sampled at 2 sps and bandpass filtered to 0.01Hz - 0.5Hz: 

    I dumped the waveform data to ascii and imported it to a spreadsheet to extract the standard deviation -- 0.7344 bits, or 21.54 bits of noise-free dynamic range! That's right inline with the design target.

    When connected to the RP--ZeroW, the raw output over 10 minutes (1200 samples) looks like this (the ZeroW doesn't have a ringserver installed):

    This is unfiltered data, which includes a small amount of temperature drift, but the p-p variation is 6 counts. Again, the NFDNR is 21.5 bits when averaged down to 1 sps (equivalent to the filtered BW of the previous data.)

    (I must declare at this point that I did not expect to ever see the output of a 24-bit ADC with just a ±2-count wiggle -- that's only 0.12ppm!)

    Diet Cola Experiment is Ongoing:

    The seismometer with the first C2D board (unshielded without insulation) has been running for a few days now. It still hasn't settled to a recognizable equilibrium, but it seems to be getting closer. Here's the raw data over the last 1.5 days:

    I'm expecting the 24-hr peak-peak swing to be less than 50,000 counts. Prior to the diet cola the swing was about 300,000 counts. This has to be a good thing. Here's the above data with a 0.01Hz - 0.5Hz bandpass applied:

    There is a bit of increased noise as the raw waveform changes direction from rising to falling. I wish that I could divine the mechanism that causes this. It seams to be less pronounced than pre-diet-cola. The spiking could also be associated with some monsoon weather that is a regular occurrence this time of year. I will let it go for a few more days before making any conclusions.

    Next Steps:

    1. Complete the diet cola testing.
    2. Replace the first C2D with the second C2D which has copper shielding, insulation and ribbon cable.

  • The Perfect is the Enemy of the Good.

    Bud Bennett07/13/2019 at 04:58 7 comments

    The seismometer has been running for a couple of days with the new C2D converter. I made some changes to the original setup to accommodate the C2D converter:

    1. The 15 inch long cabling from the sensor to the C2D converter was too long. I thought that the amount of parasitic capacitance to ground was excessive. I shortened the cables both the EXC and CAP leads to about 6 inches. The CAP leads were set about 1/4" apart and covered with copper foil tied to earth ground. The EXC leads do not have any shielding.
    2. The Raspberry Pi unit was moved into the inside of the enclosure with the seismometer due to the shorter cable lengths. It is just resting on the glass plate near the cap sensor.
    3. I removed the foam insulation and copper foil shielding from the C2D sensor to make it easier to assemble to the Raspberry Pi. The C2D must be connected to the cabling while the seismometer and C2D are outside of the enclosure, otherwise it is too difficult to connect the cabling. (See note about NP0 capacitors below.)

    The reason that the Raspberry Pi was previously mounted outside the enclosure was to avoid the heat of the RPi from causing temperature variation inside the enclosure. Going forward I expect to put the RPi inside a small metal enclosure to both shield it and reduce the rate of temperature variation that it can produce.

    There were some problems with the code crashing early on, but those are now fixed and there is data for 24 hours to evaluate. There's good news and bad news. First the good news: the new C2D converter is between 2X and 4X lower noise than the AD7745. The bad news: I was expecting 10X-30X improvement.

    This morning (2019-07-12) the seismometer recorded a 4.9 magnitude quake in central California:

    The quake was nearly 700 miles( 1100km) distant. I don't ever remember getting a clear record of a < 5 magnitude quake in California before. But there are problems.

    The above waveform is the raw data from the seismometer sensor over a 24 hour period. The daily peak-peak variation in the data is nearly 150,000 counts. I believe that all of this is due to temperature variation of the mechanical system of the seismometer. The spring and frame are metal, which tends to expand and contract with temperature. 150k counts is about 1% of the range of the seismometer -- it's not really a problem. But the jagged artifacts near the 23:00 hour are a real problem. They cause the baseline of the waveform to jiggle and tend to obscure the data unless the corner of the high-pass filter is moved upward, which would decrease the sensitivity of the seismometer at lower frequencies.

    Checking the Damping:

    I thought that the damping mechanism may need to be re-tuned so I performed another step response test with the following result:

    It looks fine (when sampled at 2 Hz).

    Reducing the Rate of Temperature Swing:

    Now I'm thinking that the glitching is caused by the seismometer mechanism responding to the change in temperature within the enclosure, and if the temperature variation could be reduced it would reduce/eliminate the glitching. One way to do this is to increase the thermal mass within the enclosure. The simplest approach that I could think of to increase the thermal mass was to stack 24  12 oz. diet cola cans along both sides of the frame. I did this yesterday morning. It will require several days to let the seismometer settle to a new equilibrium before I will know if there is any improvement.

    A Note About NP0 Capacitors:

    I had thought that NP0 capacitors exhibited zero temperature variation. The C2D was showing a bit of variation which was obviously due to temperature. It turns out that NP0 capacitors can have up to ±30ppm/C of temperature coefficient. In my system this translates to 500 counts per °C. So in addition to shielding the board from electrical interference I must also insulate the board to prevent even small rapid temperature variations from affecting the output. Slow temperature changes will...

    Read more »

  • A bit of Noise Optimization

    Bud Bennett07/06/2019 at 14:43 0 comments

    The MCP6V81 zero-drift opamps arrived yesterday, along with a few small ceramic capacitors with ±0.25pF tolerances. I quickly replaced the OPA376 opamps for these in the input integrators (U6 & U7). I also changed C1 & C2, in the AC amps, from 20pF to 5pF. The GS8591 opamps in the output integrator were left in the circuit, as well as some other small noise contributors that I did not believe matter much at this point.

    I had to wrap the board with copper foil and 1/2" of foam to improve interference and temperature effects.

    Then I left it to collect data overnight at 2 sps with an active ringserver. I can now process the resulting data with ObsPy via the ringserver. Here's 1 hour of data with a 2-pole bandpass filter 0.025Hz - 0.5Hz:

    There is a noticeable and significant improvement over the circuit using the OPA376. I ran this data through a spreadsheet, which came up with a standard deviation of 2.0769bits. That sounds pretty good, but when you account for peak-peak variation the resulting dynamic range is "only" 20.02 bits of noise-free resolution (SNR = 136.2dB or 22.6 bits). But if you consider the 16 bit NFDNR of the AD7745, this circuit is 16X better.

    Going Forward - Next Steps:

    1. Connect the current board to the seismometer and evaluate performance.
    2. If it shows promise with the real capacitance sensor, then construct another board with all of the noise optimization and evaluate it.
    3. Try to find out what is causing the poor temperature drift.

    I'm glad that I did not mess with the seismometer yesterday. It recorded its biggest-ever response to the 7.1 magnitude quake in California yesterday -- ±400,000 counts (that's nearly 0.25mm, 5% of the total dynamic range of the sensor).

  • Noise Visualization

    Bud Bennett07/03/2019 at 16:01 0 comments

    The capacitance to digital converter is now working steadily at 2 sps. I have the data plotting to a web page to visualize the data. Here's raw data from the C2D:

    You can see why the standard deviation of the noise varies from each group of samples. The signal has a non-time-invariant component. At first I thought that this was due to interference or temperature drift so I covered the board with electrical tape and then copper foil connected to GND. It did not change much. Here's a plot of 1 hour of data from the shielded unit:

    There is a slight improvement, but not much. I have the unit located in an isolated room to reduce any external interference from me sitting next to it ('s that sensitive.) 

    Eventually, started to think that the 1/f noise component of the OPA376 was causing a lot of what I was seeing. The plot above has a frequency range, or bandwidth, of roughly 0.0003Hz - 1Hz. If the noise density was flat there would be no increased noise voltage with an increased observation time period. But 1/f noise should show more noise as the observation period is increased. 

    I resorted to Mathematica to help me understand what I thought I was seeing. Here's the analysis of the expected peak-peak counts observed over a 10 second period with a 2 sps ADC rate:

    I just got lucky here -- it predicts about 10 counts of variation over a 10 second period. Thats about what I see on the above plots. I changed to observed period to 1 hour (3600 seconds) to see what to expect from the 1/f noise.

    Lo and behold!!! The peak to peak variation rises to over 200 counts. So I'm betting that the low frequency noise terms will decrease dramatically when the MCP6V81 is substituted for the OPA376. The new opamps are in the mail. 

    [Edit 2019-07-04: New information.]

    I implemented a ringserver to collect data and allow high-pass and low-pass filtering of the data.  The C2D converter ran overnight and I collected the data: it's not noise...and it appears to be temperature related. The increasing drift as the house cools down at night is pretty obvious now. Along the way the various noise elements add and subtract from the trend at much lower numbers. The next step is to insulate the board and see if that reduces the temperature effect.

    And here's the same data processed by a 0.01Hz to 1Hz 2-pole high-pass and 2-pole low-pass filters:

    Just for grins and giggles I connected the AD7745 C2D sensor and plotted its filtered noise pattern without a sensor connected:

    Both of these are ±4pF full scale output. It appears that we're moving in the right direction. The noise has improved by a factor ~4X over the AD7745, and I expect more improvement with better opamps.

  • Debugging the ADC

    Bud Bennett07/01/2019 at 19:30 0 comments

    I had not used a SPI interface with the Raspberry Pi before starting on this circuit. The documentation for the Python implementation is sparse compared to what's available for C. It required a day of tinkering to obtain the first valid data from the ADC. I had to get my Rigol oscilloscope involved. After that things progressed quickly.

    At first, the only data returned from the ADC was 0xFF. I found two things that needed understanding before the data would come. The Raspberry Pi has a wicked fast SPI interface. It was cruising along at more than 100MHz, but the poor ADC couldn't go faster than 5MHz, according to the spec. I had to slow it down with the following line of code, which sets the SCLK frequency at 1MHz.


    Now I could see square waves pinging between VDD and GND.

    Next I determined that the SPI had to keep clocking data into the ADC after it was given a command byte in order to clock data out of the ADC and into the Raspberry Pi. There isn't a built-in read or write command for the SPI protocol -- it's doing both at the same time. So if you need to just write a single-byte command to start an ADC conversion it is pretty simple:


    If you need to read or write data from/to a register then the SPI must send a command byte followed by the required bytes for the read/write: 

    r = spi.xfer2([regDATA, 0, 0, 0])  # for a read 

    spi.xfer2([regSOC, 0x1f, 0xCA, 0x02])  # for a write 

    And the last thing to understand is that the data comes little-endian. For the read statement above, r is a list of 4 bytes, for example:

    r = [0xff, 0x0f, 0x12, 0xD1]

    r[0] can be tossed, r[1] is the MSB and r[3] is the LSB.

    Here's the code that I cobbled together to get an understanding of the ADC:

    #!/usr/bin/env python
    # Bitbang'd SPI interface with an MCP3008 ADC device
    # MCP3008 is 8-channel 10-bit analog to digital converter
    #  Connections are:
    #     CLK => SCLK  
    #     DOUT =>  MISO
    #     DIN => MOSI
    #     CS => CE0
    import time
    import sys
    import spidev
    import math
    SelfCalibration = 0x90
    SystemOffsetCal = 0xA0
    SystemGainCal = 0xB0
    Convert2p5 = 0x81
    regSTAT = 0xC0
    regCTRL1 = 0xC2
    regCTRL2 = 0xC4
    regCTRL3 = 0xC6
    regDATA = 0xC9
    regSOC = 0xCA
    regSGC = 0xCC
    spi = spidev.SpiDev(),0)
    #spi.mode = 0b11
    def buildReadCommand(channel):
        startBit = 0x01
        singleEnded = 0x08
        return [startBit, singleEnded|(channel<<4), 0]
    def processAdcValue(result):
        '''Take in result as array of three bytes. 
           Return the two lowest bits of the 2nd byte and
           all of the third byte'''
        byte2 = (result[1] & 0x03)
        return (byte2 << 8) | result[2]
    def command(register):
    def writeReg(register, dataList):
        registerData = [register]
        for data in dataList:
    def readReg(register, dataList):
        registerData = [register+1]
        for data in dataList:
        r = spi.xfer2(registerData)
        return r
    def readAdc():
        r = spi.xfer2([regDATA,0,0,0])
        result = r[1]*256*256 + r[2]*256 + r[3]
        if result > (2**23)-1:
            result = (result - 2**24)
        return result
    def convert2volts(data):
        v = data/(2**23-1) * 3.6
        return v
    def meanstdv(x):
        Calculate mean and standard deviation of data x[]:
        mean = {\sum_i x_i \over n}
        std = math.sqrt(\sum_i (x_i - mean)^2 \over n-1)
        n, mean, std = len(x), 0, 0
        for a in x:
            mean = mean + a
        mean = mean / float(n)
        for a in x:
            std = std + (a - mean)**2
        if(n > 1):
            std = math.sqrt(std / float(n-1))
            std = 0.0
        return mean, std
    if __name__ == '__main__':
        CTRL1 = readReg(regCTRL1, [0])
        print("CTRL1 = {}".format(hex(CTRL1[1])))
        CTRL3 = readReg(regCTRL3, [0])
        print("CTRL3 = {}".format(hex(CTRL3[1])))
        print("Enable Self Calibration")
        CTRL3 = readReg(regCTRL3,[0])
        print("CTRL3 = {}".format(hex(CTRL3[1])))
    Read more »

  • First Pass Early Results

    Bud Bennett06/30/2019 at 19:11 0 comments

    The PCB and components arrived simultaneously. The assembly was longer than usual due to the quantity of parts and some shorted pins on the SC70 components. I used an old 26-pin header instead of a newer 40-pin header to use up some inventory.

    I applied power and no smoke was visible (always a welcome occurrence). When I measured the output voltages I got some weird results. The signals to the output integrator were slowly sinking toward ground and the integrator outputs weren't biased to the proper voltage. It was at this point that I took a close look at the schematic and discovered that the output integrators did not have any DC bias. I shorted out the capacitors in the demodulator, which stopped the slow drifting but the outputs still had improper voltages - like the opamps were installed backward. I reviewed the datasheet pinout for the MCP6V81 and found that there is an alternative pinout for the SOT23 package that has a "U" designation. I had unknowingly ordered the U part from Digikey instead of the standard pinout. After swapping out the MCP6V81U parts for two Gainsil GS8591 zero-drift opamps from my inventory, the board started behaving as expected.

    The initial data:

    Vcommon = 1.803V

    VCC = 3.609V @ 7mA

    Open inputs:

          Vout+ = 1.823V

          Vout- = 1.893V

          Vdiff = -77.6mV

    I was a bit disappointed that there was such a large differential offset voltage, but then I calculated the error as only 0.11pF.

    With 4.7pF caps connected between EXCs and INPUTs:

          Vout+ = 3.421V

          Vout- = 0.308V

          Vdiff = 3.119V

    Swapped EXC connections to capacitors:

          Vout+ = 0.231V

          Vout- = 3.495V

          Vdiff = -3.276V

    If the capacitors were exactly 4.7pF then output voltage should have been 3.3925V. The above reading indicate that the capacitors were -5.72% low. When the offset capacitance is taken into account, the full scale readings were within 0.06% of each other.

    So far so good. Now I have to figure out how to read ADC data over the SPI on the Raspberry Pi.

  • Noise Analysis

    Bud Bennett06/01/2019 at 14:40 0 comments

    [Edit 2019-06-29: entire log was scrapped and replaced. I was totally wrong about how to go about analyzing the various noise contributions to the output of the circuit. I may still be wrong, but I think the following analysis is on the correct path.]

    The purpose of this exercise is to analyze the circuit topology to determine the contribution of each source of noise and then select component values that minimize the overall noise at the circuit's output. The goal is to keep the peak-peak noise level below the amount necessary to guarantee 22-bits of dynamic range from the ADC.

    The positive and negative signal paths use identical circuitry -- it is only necessary to calculate the noise in one path and then mulriply by √2 to get the total noise at the differential outputs. The demodulator appears to be essentially noiseless -- consisting of a small switch resistance -- so I will ignore it.

    I determined that the best approach was to insert a pseudo noise source into the simulated circuit and see what how the noise source affected the output voltage. Then it is a matter of just adding up all of the noise contributions to get the total.

    Noise contribution of U7:

    I inserted a voltage source, EN7, to see what the voltage noise of U7 contributed to the output voltage. (This is not the circuit used for simulation. It is just an equivalent schematic.)

    I made EN7 a sinewave source with a magnitude of 10mV peak. EN7 emulates the equivalent input voltage of the opamp U7. The simulation produced the following ouput.

    The simulation indicates that the gain from EN7 to the output is 2.072 V/V. The noise contribution is present at DC and also at the modulation frequencies and its harmonics. Therefore U7 should probably be a zero-drift chopper amplifier that has no 1/f noise component at DC.

    I found through simulation that the value of C1 should be as small as possible, and R1 should be large, to minimize the noise contribution of U7. But R1 is also a noise source that should be minimized as well. 

    Noise contribution of U3:

    I inserted a voltage source at the positive input of U3 to simulate its contribution.

    The simulation indicated that there was no significant low frequency noise contribution from U3 because U7 removed it. Unfortunately, the input voltage noise of U3 is not reduced by U7 at the 100kHz modulation frequency. The gain to the output from EnU3 at 100kHz is 2.84 V/V. I found this out by increasing the frequency of EnU3 to 100.050kHz and measuring the amplitude of the resultant waveform at the output. U7 should have low input voltage noise at frequencies above 90kHz.

    Noise Contribution of R8:

    The noise voltage source is now in series with R8 to simulate its contribution to the output.

    The noise from R8 only contributes at DC with a gain of 1. The takeaway: make R8 as small as possible.

    Noise Contribution of R1:

    A voltage source is inserted in series with R1 to estimate its contribution to the output.

    The gain from R1 to the output is 0.2 V/V at 100kHz and harmonics. U7 removes the noise at DC. The tradeoff here is to make R1 as large as possible, to reduce noise from U7, but not so large that its thermal noise dominates.

    Noise Contribution of U5:

    The simulation indicates that the gain from ENU5 to the output is 2.2 V/V. This noise is only injected at DC.

    Noise Contribution of R10 and R6:

    Simulation predicts the gain from these two resistors ( at DC) to the output is 2.16 V/V.


    Read more »

  • Nulling Feedback Demodulator

    Bud Bennett05/31/2019 at 12:38 0 comments

    This idea originated from L.K. Baxter's "Capacitive Sensors" paper, which I have included in the files section of this project. Baxter covers signal conditioning in Chapter 5, but only using block diagrams. This is the diagram of a simple synchronous demodulator from the paper ( I implemented this as a first pass.)

    But it was this block diagram, which I call a nulling feedback demodulator, and his comments that I did not immediately understand in any meaningful way:

    He is using current source symbols for AC voltage sources. It was unclear to me how V2 was implemented, but the highlighted comment about the SPDT CMOS switch is the clue. V2 is a voltage variable AC source controlled by the output of the integrated demodulator signal. I eventually understood enough to translate the above diagram into this simple differential circuit concept to use with my cap sensor:

    All of the switches are in phase with the modulator. This circuit includes capacitors in the demodulator to eliminate the DC terms present in the AC amplifier that should get removed by the chopper operation.

    The overall gain of the circuit only depends upon the cap sensor, the feedback capacitors C1 & C2, and the excitation voltage, which is the supply voltage:

        Vout = (∆C/C1 - -∆C/C2) VCC = 2∆C/C1 VCC = ±VCC/2 , for ∆C =±4pF, C1 = C2 = 16pF.

    In order to more fully understand the operation of this circuit I simulated it using LTSpice. The LTSpice schematic is too messy to present here, but the simulated signals lend some understanding of what the circuit is doing and the sensitivities of the component values on the circuit performance.

    Without the nulling feedback the AC amplifier outputs are required to slew and settle to large differential outputs, as shown here for a full scale cap sensor ∆C.

    The amplifier (a simulated ADA4500, with a 16pF and 10Meg for feedback elements) must get to the final output voltage and then settle quickly before the end of the sampling period, in this case 2.5µs. The sloped square waves are caused by the resistor used for DC bias that is discharging the capacitor during the interval -- this causes a gain error.

    Here's the what the amplifier outputs look like when producing the same full scale ∆C when nulled in a steady state with a 100kHz excitation frequency:

    The simulated amplifier is the same ADA4500, with 10pF and 10MegΩ feedback elements. There is an initial spike when the charge on the capacitors is applied to the amplifier inputs, but since the charges are nearly equal the amplifier settles to a low output voltage and there is very little error produced by the 10MegΩ feedback resistor -- eliminating the gain error due to discharge. Baxter is correct in claiming that a lower bandwidth amplifier can be used, with lower slew and settle requirements. In addition, the resistor feedback element used for DC bias can be made lower, which reduces the sensitivity to leakage currents at the inputs. But it doesn't eliminate it.

    I got a shock when I simulated a 10nA leakage current injected into the input of one of the AC amplifiers. The leakage current produced a 100mV error in the output voltage. After a long struggle to understand and fix the problem I implemented the following circuit.

    The leakage current created a DC voltage across the AC amplifier. Because the Demodulator switch samples only one edge of the AC amp output a DC component would appear to get passed through to the output because the charge at the input was not properly cancelled. The integrator wrapped around the AC amplifier sets the average output voltage of the AC amp to equal the common voltage node. Now...

    Read more »

View all 10 project logs

Enjoy this project?



Anthony wrote 04/02/2023 at 11:03 point

To measure the AC output of the amplifier it's not necessary to use a chopper circuit. It's much easier to use a Fourier transform to detect the signal. This is a lot simpler than it may seem and is an enhancement of the simpler chopper detection. The measurement is also done at one frequency and is inherently free from any DC artifacts like offset and drift. The hardware is much simpler although the software is slightly more complicated.

Chopper detection is a form of auto correlation where a square wave is multiplied by its equal in both frequency and phase and the result of the multiplication integrated to calculate the amplitude.

A better way is to multiply the input signal by the sine and cosine of the square wave fundamental harmonic and integrate those two multiples. The cosine value is the real part of a vector and the sine is the imaginary part of  the vector. With these two values, the phase and amplitude of the square wave fundamental harmonic can be calculated using normal vector arithmetic.

For instance, the absolute amplitude is the square root of the sum of cosine squared and sine squared.

Because only the fundamental harmonic of the square wave is being measured, the effective signal bandwidth around the fundamental is greatly reduced which greatly reduces the noise.

Think of this as a poor man's FFT where only one frequency is measured.

Only a single power supply is required with both the AC amplifier and ADC biased at 1/2 the power supply voltage with capacitor coupling. This eliminates all DC drift and offset because all signal processing will be done as pure AC. The bias voltage does not need to be precise, but should be a low noise source. A simple resistor divider can be used at the input of the non-inverting input of the AC amplifier with a capacitor to ground to reduce noise. A large value resistor is used for feedback to the inverting input to give a DC gain of one. The amplifier output can then be coupled directly to ADC.

Calculating the Fourier transform is very simple. The ADC samples at an integer multiple of the the source frequency. At minimum, it must sample at four times the square wave frequency. Each sample represents one point on both a cosine wave and sine wave. Note that a sine wave is simply a cosine wave shifted 90 degrees. For the cosine, the four samples are at 0 degrees, 90 degrees, 180 degrees and 270 degrees. For the sine, the four samples are at 90 degrees, 180 degrees, 270 degrees and 0 degrees.

Multiplication is commutative so it's possible to integrate first and multiply second. An array is used to hold the four sample values and for each cycle, the samples are added to the array and after sufficient cycles are measured, the sine and cosine correlation is done. A table containing the cosine values at 0 degrees, 90 degrees, 180 degrees and 270 degrees is used to multiply the sample array. The sine calculation uses the same table, but shifted by 90 degrees. The resulting vector amplitude can be calculated and also phase if desired.

Here is a sample of the code from where the brake current is being measured by a transformer coupled chopper circuit. The measurement is done in very harsh conditions because the elliptical generator output can go up to 300V while the brake current is PWM modulated which also increases the noise significantly.

The ADC is triggered by timer counter 0 overflow while the chopper is driven by timer counter 2 operating at 244 Hz.

I've also used the same technique to use a computer sound card to create an LCR meter. In fact, it's quite feasible to use a sound card as a detector in your project which would greatly reduce the hardware requirements. The sound card has very low noise and can resolve down to the nanovolt level using the above method.

bool checkADC(void){
   * This function implements a frequency tuned AC detector
   * The ADC samples at exactly 32 times the chopper frequency
   * Autocorrelation is used to extract the real and imaginary parts of the chopper fundamental harmonic
   * Bandwidth is inversely proportional to sample size. 100 cycles are used as a compromise between noise and speed
  static byte sampleCount = 0;
  static unsigned int cycleCount = 0;
  //Wait for ADC to finish
  if(ADCSRA & B00010000){
    ADCSRA |= B00010000;        //Clear ADC done flag
    TIFR0 |= B00000001;         //Clear TOV0 flag
    samples[sampleCount] += ADC;
    sampleCount ++;
    if(sampleCount > 31){
      sampleCount = 0;
      cycleCount ++;
      if(cycleCount > 100){
        cycleCount = 0;
        //Calculate chopper fundamental harmonic vector values using correlation with cosine and sine
        float cosineSum = 0.0;
        float sineSum = 0.0;
        for(byte cosineIndex = 0; cosineIndex <= 31; cosineIndex ++){
          byte sineIndex = (cosineIndex + 8) % 32;
          cosineSum += float(samples[cosineIndex]) * cosine[cosineIndex];
          sineSum += float(samples[cosineIndex]) * cosine[sineIndex];
        //Calculate scalar value of chopper fundamental harmonic
        current.value = sqrt(sq(cosineSum) + sq(sineSum)) * currentCalibration;
        //Send current value to host for display
        if(queueAppendByteVerbatim(&rs232writeBuffer, START) == true){
          if(queueAppendByteVerbatim(&rs232writeBuffer, BRAKE_CURRENT) == true){
            queueAppendFloat(&rs232writeBuffer, ¤t);
        memset (samples, 0, 32 * sizeof(samples[0]));
        return true;
      return false;
      return false;
    return false;

  Are you sure? yes | no

Does this project spark your interest?

Become a member to follow this project and never miss any updates