How to Improve ADC Resolution

joeyd999

Joined Jun 6, 2011
5,234
Resolution I understand but to get 16 bit accuracy, the converter has to be 16-bit accurate.
How do you get that from a converter that's only accurate to 12 bits?
One thing I like about the SARs in the PICs is that they have very well defined, accurate, and sharp transitions between codes. This helps to improve differential nonlinearity greatly when dithering and oversampling. The product has been in production for nearly a year -- and I've shipped thousands of them. They work as advertised.

Note: the sharp transitions work against you if you don't have enough white noise on the input signal.

I did make one error in my overall description, and it has to do with this comment from the 18F87K90 errata:

A/D Offset The A/D may have high offset error, up to a maximum of 50 LSB; it can be used if the A/D is calibrated for the offset.
One of the channels simply samples a 0V input signal. This is subtracted from the other 3 each sample period. The remaining three signal chains are then accumulated/boxcar'd/fir'd.

The errata also notes:

The A/D will meet the Microchip standard A/D specification when used as a 10-bit A/D. When used as a 12-bit A/D, the possible issues include ... high DNL error (up to a maximum of ±4 LSBs) and multiple missing codes (up to a maximum of 20)
If they're there, I've never seen them.

@joeyd999
Did you do it in MikroC ?
No. .asm.
 

OBW0549

Joined Mar 2, 2015
3,566
Resolution I understand but to get 16 bit accuracy, the converter has to be 16-bit accurate.
How do you get that from a converter that's only accurate to 12 bits? :confused:
What the dithering does is it "spreads out" the differential nonlinearities of the ADC (that is, the irregularities of the code-to-code transition thresholds of the ADC) over a large range of codes, and the oversampling/averaging attenuates the overall effect of the individual irregularities to yield an increase in both resolution and accuracy. Σ-Δ converters take this effect to the extreme when they use a mere 1-bit ADC (i.e., a simple comparator) and cascaded integrators to yield very high resolution and accuracy.

(Note that dithering/averaging only improves differential nonlinearity; it cannot make any improvement in integral nonlinearity.)
 

joeyd999

Joined Jun 6, 2011
5,234
@joeyd999
By any chance can you show me how to do it even in asm
I had to redact some stuff for privacy reasons. The code shows the accumulation (in the interrupt) & boxcarring (in the "userspace" polling code), but not the FIR.

Also, I am only boxcarring channels 1 and 2. Sorry, I've written so much code since I wrote this that it's difficult for me to remember all the details off the top of my head.

Code:
;JoeyD999 dithering/oversample code circa 2015
;PIC18F87K90

;************************************************************
;** A/D Interrupt -- Conversions started by TMR0 @ 256 us  **
;**   ch0 -- Vbat                      **
;**   ch1 -- Vo                                            **
;**   ch2 -- Temp                                          **
;**   ch3 -- Vref                      **
;**   ch4 -- Vss (for offset -- see errata)                **
;**   64 accumulations/channel                             **
;**   65.536 ms / accumulation                             **
;**   15.258 accumulations/second                          **
;************************************************************

adint    bcf    pir1,adif        ;clear interrupt
  
    bcf    _fadsat            ;assume a/d not saturated
    bbs    adresh,7,0,adins    ;  result negative...not saturated  

    movfw    adresh            ;get high a/d value
    iorlw    b'11110000'        ;fill in high bytes
    andwf    adresl,w        ;and merge low

    incf    wreg,w            ;result 0xff?
    skpnz                ;no, not saturated

    bsf    _fadsat            ;yes, flag it

adins    bbs    adcon0,chs2,0,advss    ;just converted vss
    bbs    adcon0,chs1,0,aditt    ;just converted temp
    bbs    adcon0,chs0,0,adivo    ;just converted vo

;just converted vbat (don't compensate for offset as vref is different)

    bsf    adcon0,chs0        ;set next conversion for vo
    bcf    adcon1,vcfg1        ;set external 2.5V vref

    movf    adresl,w        ;get a/d low value
    addwf    _vbat,f,1        ;sum low
    movf    adresh,w
    addwfc    _vbat+1,f,1        ;and high
    clrw
    addwfc    _vbat+2,f,1
  
    bra    intdone      

;just converted vss (save for offset of vo and temp)

advss    bcf    adcon0,chs2        ;set next conversion for vbat
    bsf    adcon1,vcfg1        ;set internal 4.096V Vref

    movff    adresl,_vss        ;save result
    movff    adresh,_vss+1
    clrf    _vss+2,1        ;assume positive
    btfsc    adresh,7        ;negative?
    decf    _vss+2,f,1        ;set high byte

    bra    intdone            ;get out

;just converted for vo

adivo    bcf    adcon0,chs0        ;set next conversion for tempt
    bsf    adcon0,chs1

    btfsc    _fadsat            ;a/d saturated?
    bsf    _fvosat            ;yes, flag it

    movf    adresl,w        ;get a/d low value
    addwf    _vo,f,1            ;sum low
    movf    adresh,w
    addwfc    _vo+1,f,1        ;and high
    clrw
    addwfc    _vo+2,f,1

    movf    _vss,w,1        ;subtrack zero offset
    subwf    _vo,f,1
    movf    _vss+1,w,1
    subwfb    _vo+1,f,1
    movf    _vss+2,w,1
    subwfb    _vo+2,f,1  

    bra    intdone      
  
;just converted for Temp

aditt    bcf    adcon0,chs1        ;set next conversion for vss
    bsf    adcon0,chs2

    btfsc    _fadsat            ;a/d saturated?
    bsf    _fttsat            ;yes, flag it

    movf    adresl,w        ;get a/d low value
    addwf    _tempt,f,1            ;sum low
    movf    adresh,w
    addwfc    _tempt+1,f,1            ;and high
    clrw
    addwfc    _tempt+2,f,1

    movf    _vss,w,1        ;subtrack zero offset
    subwf    _tempt,f,1
    movf    _vss+1,w,1
    subwfb    _tempt+1,f,1
    movf    _vss+2,w,1
    subwfb    _tempt+2,f,1  

;round robin for 64 sums each channel

    decfsz    _adcnt,f,1        ;enough sums?
    bra    intdone            ;no, get out

;shift and copy to static holders

;-- Vbatt --

    rrcf    _vbat+2,f,1        ;shift vbatt prior to copy
    rrcf    _vbat+1,f,1
    rrcf    _vbat,f,1

    rrcf    _vbat+2,f,1
    rrcf    _vbat+1,f,1
    rrcf    _vbat,f,1

    movff    _vbat,vbat        ;copy accumulators to static holders
    movff    _vbat+1,vbat+1

    clrf    _vbat,1            ;clear accumulators
    clrf    _vbat+1,1
    clrf    _vbat+2,1

;-- Vo --

    movff    _vo,vo            ;copy dynamics to statics
    movff    _vo+1,vo+1
    movff    _vo+2,vo+2

    clrf    _vo,1            ;clear dynamics
    clrf    _vo+1,1
    clrf    _vo+2,1

;-- Temp --

    movff    _tempt,tempt
    movff    _tempt+1,tempt+1
    movff    _tempt+2,tempt+2

    clrf    _tempt,1          
    clrf    _tempt+1,1
    clrf    _tempt+2,1

;export saturation bits

    bcf    fvosat            ;assume not saturated
    bcf    fttsat

    btfsc    _fvosat            ;set if saturated
    bsf    fvosat

    btfsc    _fttsat            ;set if saturated
    bsf    fttsat

    bcf    _fvosat            ;clear dynamics
    bcf    _fttsat

    movlfb    _adcnt,cadcnt        ;reset counter

    bsf    _adrdy            ;indicate conversion ready

    bra    intdone            ;and get out


;***************************************************
;** POLLAD -- Return new A/D reading if available **
;***************************************************
;**   nz=new data                                 **
;**   z if no new data available                  **
;**   15.258 conversion/second                    **
;***************************************************

pollad    bcf    adrdy            ;clear transducer ready flag for next pass

    retbc    _adrdy            ;a/d data ready?
    bcf    _adrdy            ;yes, clear flag

    bsf    adrdy            ;indicate data ready to program

;align bits for boxcar filter

    clrc                ;align bits for boxcar filter
    rlcf    vo,f,1
    rlcf    vo+1,f,1
    rlcf    vo+2,f,1

    clrc
    rlcf    vo,f,1
    rlcf    vo+1,f,1
    rlcf    vo+2,f,1

    clrc
    rlcf    tempt,f,1
    rlcf    tempt+1,f,1
    rlcf    tempt+2,f,1

    clrc
    rlcf    tempt,f,1
    rlcf    tempt+1,f,1
    rlcf    tempt+2,f,1

;locate next a/d record in boxcar filter

    movlw    2*3            ;6 bytes per record
    mulwf    adidx,1            ;mulitply index
    incf    adidx,w,1        ;bump index for next pass
    andlw    b'00001111'        ;  modulo 16
    movwf    adidx,1            ;save back

    lfsr    0,adqueue        ;point to queue
    movfw    prodl            ;  and add index
    addwf    fsr0l
    movfw    prodh
    addwfc    fsr0h            ;fsr0 -> first byte of 2*3 byte record

;subtract oldest reading from boxcar filter
;  and replace with new reading

    movfw    indf0
    subwf    bxvo,f,1
    movff    vo,postinc0

    movfw    indf0
    subwfb    bxvo+1,f,1
    movff    vo+1,postinc0

    movfw    indf0
    subwfb    bxvo+2,f,1
    movff    vo+2,postinc0

    movfw    indf0
    subwf    bxtt,f,1
    movff    tempt,postinc0

    movfw    indf0
    subwfb    bxtt+1,f,1
    movff    tempt+1,postinc0

    movfw    indf0
    subwfb    bxtt+2,f,1
    movff    tempt+2,postinc0

;add new reading to boxcar

    movfwb    vo
    addwf    bxvo,f,1
    movfwb    vo+1
    addwfc    bxvo+1,f,1
    movfwb    vo+2
    addwfc    bxvo+2,f,1

    movfwb    tempt
    addwf    bxtt,f,1
    movfwb    tempt+1
    addwfc    bxtt+1,f,1
    movfwb    tempt+2
    addwfc    bxtt+2,f,1


;*** REDACTED FOR PRIVACY REASONS ***

;compute ... (FIR filtering done in these routines)

    call    comp..            ;compute ..
    call    comp..            ;compute ..
    call    comp..            ;compute ..
    call    comp..            ;compute ..

;*** END REDACTED ***

;desample

gtdsmp    call    desample        ;desample A/D readings to display update rate

    bsf     update            ;indicate update to lcd

    return

;****** END POLLAD ******
 

OBW0549

Joined Mar 2, 2015
3,566
Regarding my comments in #42 above about differential nonlinearity and the effects of dithering, I ran across this excellent application note by Walt Kester of Analog Devices, Inc. He does a good job of explaining, in a LOT more detail, what I was trying to get across in that post.
 

Attachments

Thread Starter

R!f@@

Joined Apr 2, 2009
9,918
I asked joey about how to do this and he said to post it so here goes.

This attempt is a learning curve on how to improve the 10 bit ADC accuracy or resolution (what ever is correct).

My aim is to display 30.00 VDC from a 10 bit ADC at 10mv intervals. The 1mv problem will solved with separate 18 to 22 bit ADC.

I read a lot and it seems that I need to take multiple samples together and add them in a register first...

So first the loop counter. Measurement loops here.
C:
unsigned int i // i is 16 bit integer.
unsigned long int ADC_Temp // ADC_Temp is 32 bit integer.
for (i=16; i>0; i--){ // Loop Counter
ADC_Temp += ADC_Read (0);
}
I am thinking here the ADC_Temp will have ADC values added up for 16 ADC samples.

Correct or Wrong ? :D
 

joeyd999

Joined Jun 6, 2011
5,234
I asked joey about how to do this and he said to post it so here goes.

This attempt is a learning curve on how to improve the 10 bit ADC accuracy or resolution (what ever is correct).

My aim is to display 30.00 VDC from a 10 bit ADC at 10mv intervals. The 1mv problem will solved with separate 18 to 22 bit ADC.

I read a lot and it seems that I need to take multiple samples together and add them in a register first...

So first the loop counter. Measurement loops here.
C:
unsigned int i // i is 16 bit integer.
unsigned long int ADC_Temp // ADC_Temp is 32 bit integer.
for (i=16; i>0; i--){ // Loop Counter
ADC_Temp += ADC_Read (0);
}
I am thinking here the ADC_Temp will have ADC values added up for 16 ADC samples.

Correct or Wrong ? :D
How often are you going to update the display?

What else is the MCU going to be doing besides taking a/d readings and updating the display?

Consider using interrupts. Burning millions of clock cycles waiting for an answer is not a very efficient use of your MCU.
 

Thread Starter

R!f@@

Joined Apr 2, 2009
9,918
CPU is doing nothing much.
I have interrupt enabled.
My CPU does most of the time is Read 4 ADC's and update the display with 3 of them.
3 of them are 2 voltages and 1 current measurement.
The 4th one is used for temperature. So this does not require averaging. Just check ADC for certain reading and trigger shutdown when over heat.
I am using interrupt to update LCD at every 200ms or so using a flag that gets set at every 200ms ( timer1)
Timer 0 is used to drive a buzzer at ~2.7KHz during overload and over heat conditions and also fade in and out, or flash an LED. JohnInTX taught me this part
All is working without Delays.
 
Last edited:

joeyd999

Joined Jun 6, 2011
5,234
CPU is doing nothing much.
I have interrupt enabled.
My CPU does most of the time is Read 4 ADC's and update the display with 3 of them.
3 of them are 2 voltages and 1 current measurement.
The 4th one is used for temperature. So this does not require averaging. Just check ADC for certain reading and trigger shutdown when over heat.
I am using interrupt to update LCD at every 200ms or so using a flag that gets set at every 200ms ( timer1)
Timer 0 is used to drive a buzzer at ~2.7KHz during overload and over heat conditions and also fade in and out, or flash an LED. JohnInTX taught me this part
All is working without Delays.
What is your clock frequency?

Here's what I would do:

1. Free up timer 0. For the buzzer signal, I would use the EUSART (if available) TX line to drive it. Set the Baud to 2x the desired frequency, and, using the TX interrupt, reload the TX buffer with the value '10101010' (assuming idle high) or '01010101' (idle low). The instruction cycle overhead with this method is nearly zero. And the buzzer can be turned on or off simply by enabling/disenabling the TX interrupt. Also, the pitch can be easily and quickly changed by adjusting the Baud. Nice thing about this: there will be no 'discontinuities' in the resulting waveform, producing a 'cleaner' sound (i.e. no jitter due to interrupt latency).

2. Set timer 0 to continuously roll over at some reasonably fast rate, say 256uS. Use this interrupt as you 'system clock' or main time base to activate other portions of your program. For instance, the GO bit can be set here to start a new conversion every 256 microseconds, giving 3,906.25 conversions per second (distributed over 4 channels ~= 976.5/s. A counter in this routine can provide additional clocks for other peripherals, like the display.

Here is an .asm version of how I do this:

Code:
;********************************************************
;** Timer 0 -- Main Program Heartbeat Every 256uS      **
;**         -- -and- Start A/D Conversion:             **
;********************************************************

tmr0int    bcf    intcon,tmr0if        ;turn off t0 int

    movf    _timer,w,1        ;get current time
    incf    _timer,f,1        ;and increment it
    xorwf    _timer,w,1        ;compare with last
    iorwf    _tmrchg,f,1        ;and save changed bits

    bsf    adcon0,go        ;start a/d every 256uS

    bra    intdone            ;and getout
I "synchronize" the system clock as the first call of the main loop like this:

Code:
;******************************************************************
;** GETTIM -- Get current time and changed bits since last check **
;******************************************************************
gettim    clrf    tmrchg1            ;clear bits from previous pass

    bcf    intcon,tmr0ie        ;disable ints

    BANKSEL    _timer        ;set for different bank

    movff    _timer,timer        ;copy timer data
    movff    _tmrchg,tmrchg
    clrf    _tmrchg,1        ;clear for next time

    movlb    0

    bsf    intcon,t0ie        ;and reinable ints

    return

;******** END GETTIM *********
A set of labels then gives me "triggers" that I can use in my main program for various timed functions:

Code:
;****************************************
;** System Timer Constants Definitions **
;****************************************

T256us    equ    256            ;time base definitions (uS)
T512us    equ    512
T1ms    equ    1024
T2ms    equ    2048
T4ms    equ    4096
T8ms    equ    8192
T16ms    equ    16384
T32ms    equ    32768

;Timer 0 Bit Constant Definitions

#define TB512us    timer,0,0
#define TB1ms    timer,1,0
#define TB2ms    timer,2,0  
#define TB4ms    timer,3,0
#define TB8ms    timer,4,0
#define TB16ms    timer,5,0
#define TB32ms    timer,6,0
#define TB64ms    timer,7,0

;Timer 0 Change Constant Definitions

#define TC256us    tmrchg,0,0
#define TC512us    tmrchg,1,0
#define TC1ms    tmrchg,2,0
#define TC2ms    tmrchg,3,0
#define TC4ms    tmrchg,4,0
#define TC8ms    tmrchg,5,0
#define TC16ms    tmrchg,6,0
#define TC32ms    tmrchg,7,0
The "TB" version gives me a "square wave" with the period indicated. The "TC" version gives me a single "pulse" exerted for the entirety of one pass of the main program loop. You can now define individual counters for different functions. For instance, for a ~200mS LCD update rate (which I think is too fast -- 3 per second should be maximum), you could count 50 occurrences of TC4ms. or 25 occurrences of TC8ms.

3. Set up your A/D code as an interrupt. Use the channel bits to decide which channel has been converted. As quickly as possible, switch to the next channel before doing anything else so as to give the S/H cap as much time as possible to charge. For your accumulators, think in terms of bits. How many samples do you need, assuming full-scale input voltage, to completely saturate your accumulator without a carry out of the MSB?

Your converter is 10 bits. A two byte accumulator is 16 bits. This implies you can sum up to 2^6 (64) conversions into one accumulation cycle without overflowing the accumulator. If you do this, then, it turns out, your 16 bit accumulators will become, essentially, fixed-point fractions, with the binary point at the MSB. IOW, the decimal equivalent of the accumulation would be within the range of 0.000 <= X < 1.000, where X is the accumulated fixed-point number which has a value representing a fraction of the full-scale input voltage. Converting this to actual voltage is easy. Just multiply Vref * X (and keep cognizant of your binary point location).

Also, assuming relatively white input noise, 64 samples increases your resolution by 6 bits, and increases S/N by SQR(64) or a factor of 8.

For easy code, I'd accumulate all 4 channels, whether you need to or not.

Get this part working, and then we can work on additional filtering as, and if, necessary.
 

Thread Starter

R!f@@

Joined Apr 2, 2009
9,918
PIC is 16F886 and has EUSART. This method is new and I have to see if I can do this.
What frequency do you recommend...4 or 8 MHz ?

Thank you @joeyd999 for doing this.

{edit}
Can I use TMR1 for the Time base ?
 

joeyd999

Joined Jun 6, 2011
5,234
PIC is 16F886 and has EUSART. This method is new and I have to see if I can do this.
What frequency do you recommend...4 or 8 MHz ?
Use whatever is appropriate. If you have no MCU power restrictions (which I normally have), use a fast clock if you want. This app will run fine at 4 MHz -- at least in assembly. C will add some overhead, but not enough to matter, I think.

Can I use TMR1 for the Time base ?
You can use whatever resources you want. I save the other hardware timers for other purposes -- CCP, external hardware synchronization, etc.

Remember: User interface timing (LEDs, LCD, buzzers, etc.) is generally on the order of 10's to 100's of milliseconds or longer. Precise timing (to the microsecond) it not usually required -- and timer latency and jitter go unnoticed.
 

Thread Starter

R!f@@

Joined Apr 2, 2009
9,918
OK...so to save time I will use my current TMR0 and TMR1 set up and use either timer's ( what ever is easy ) interrupt to scan ADC at 256us. Is it ok ?
If not I will try to rewrite the code as per your instruction.
 

joeyd999

Joined Jun 6, 2011
5,234
OK...so to save time I will use my current TMR0 and TMR1 set up and use either timer's ( what ever is easy ) interrupt to scan ADC at 256us. Is it ok ?
If not I will try to rewrite the code as per your instruction.
I am providing examples. You should implement as is appropriate and practical for your application.

If you want to make this really easy, start the ADC each 1.024mS. This will give you 4 channels of 64 accumulations about 4 times per second. Then, update the display each time the accumulations are complete.

Assuming no further filtering is necessary, your job is done.
 

joeyd999

Joined Jun 6, 2011
5,234
Is my approach in #46 correct for accumulations ?
I don't like it because it ties up the CPU in an idle loop while waiting for conversions. Also, you don't require 32 bit accumulators (assuming 64 conversions) -- this just wastes RAM and clock cycles.

Also, remember that your accumulators must be cleared prior to the next set of conversions. I usually keep two sets of registers: 1 used in the interrupt routine, and another used for the main code. The interrupt routine copies the "dynamic" set into the "static" set at the end of an accumulation period, sets a flag to indicate to the main code that an accumulation is ready, and zeros the "dynamic" set.
 

Thread Starter

R!f@@

Joined Apr 2, 2009
9,918
I am reading over and over...so far bits and pieces are getting into my head.
I am going to start a fresh code to get it thru my head.

I will keep you posted.
 

Thread Starter

R!f@@

Joined Apr 2, 2009
9,918
One Question....
Should I use the mikroC ADC library and do the ADC bit settings separately ?
 
Top