Author Topic: ADC/DAC Conversion to "decimal", best practices....  (Read 14428 times)

0 Members and 1 Guest are viewing this topic.

Offline ChristopherTopic starter

  • Frequent Contributor
  • **
  • Posts: 429
  • Country: gb
ADC/DAC Conversion to "decimal", best practices....
« on: June 08, 2016, 06:20:50 pm »
So I'm doing a project with the use of some high-precision DACs and ADCs and am after some advice on how to do the conversion!

Just say I have a 12-bit ADC. I want to convert the output to a string using some kind of itoa() function (included in my C compiler)

I do not want to use Floating points, for obvious reasons.

So, I have 12 bits, which represents 0-10V, so each bit is worth 10/2^12=2.4414mV

My ADC result is 0-4095. I need to convert this to be 0-10V as a string.

Because the numbers aren't very easily divisible by 10 without floats (hardware cannot use an easier reference),  I'm struggling a bit. We were never taught this kinda thing at college either....

Also, the DAC part should be the same, just in reverse?
 

Offline helius

  • Super Contributor
  • ***
  • Posts: 3639
  • Country: us
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #1 on: June 08, 2016, 06:53:32 pm »
Think carefully about how you will handle over-range, and whether you need to compensate for nonlinearity over the whole range.
If you are fine with using itoa() or printf("%d") then the conversion to base 10 is already being done for you. All you need to handle is the conversion from a range (n/4096) to two range parts: (n/10) and (n/10000).
A naive way of doing it is to first multiply the value by 100000 and then divide by 4096:
long voltage = adc_val * 100000UL / 4096;
There are other tricks with tables that can have better performance.
The key word to search for is "fixed point representations" (like floating point, but the exponent doesn't change).
 

Offline JPortici

  • Super Contributor
  • ***
  • Posts: 3461
  • Country: it
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #2 on: June 08, 2016, 07:11:04 pm »
I like the naive way. you want to avoid divisions as much as possible, even when you have an hardware divider and not only because computational speed.
"cheating" like this (multiply then shift out bits) will be FAST and will retain the greatest amount of information after you throw away those 12 bits
 

Offline Kleinstein

  • Super Contributor
  • ***
  • Posts: 14172
  • Country: de
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #3 on: June 08, 2016, 07:11:51 pm »
Performance of the  *100000UL/4096 is not that bad:  4096 is a power of 2 and thus shifts can be used instead of a full division. The 100000 can also be adjusted to do software scaling in case the full scale is not at  exactly 10 V but maybe 10.05 V.
 

Offline hamster_nz

  • Super Contributor
  • ***
  • Posts: 2803
  • Country: nz
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #4 on: June 08, 2016, 07:17:08 pm »
My ADC result is 0-4095. I need to convert this to be 0-10V as a string.
First suggestion would be to convert it to milivolts (0-10,000) (maybe more on this in a later post), then convert to a string with repeated subtraction. Here's some pseudocode:
Code: [Select]
  set string to "00.000"
  while value > 10000
     string[0] += 1
     value -= 10000

  while value > 1000
     string[1] += 1
     value -= 1000
  ((( Skip the decimal place )))
  while value > 100
     string[3] += 1
     value -= 100

  while value > 10
     string[4] += 1
     value -= 10

  while value > 1
     string[5] += 1
     value -= 1;

  ((Suppress leading zeros))
  if string[0] = '0' then
     string[0] = ' '   
Gaze not into the abyss, lest you become recognized as an abyss domain expert, and they expect you keep gazing into the damn thing.
 

Offline nctnico

  • Super Contributor
  • ***
  • Posts: 26891
  • Country: nl
    • NCT Developments
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #5 on: June 08, 2016, 07:17:58 pm »
It is a good custom to scale ADC values to units like V, mV, uV, A, mA, Hz, etc and append the unit to the variable names so it is absolutely clear what the variable is representing. It makes maintaining firmware much easier and therefore saves money.
There are small lies, big lies and then there is what is on the screen of your oscilloscope.
 

Offline MK14

  • Super Contributor
  • ***
  • Posts: 4527
  • Country: gb
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #6 on: June 08, 2016, 07:21:49 pm »
Just multiply the raw ADC value (0 .. 4095) by 2442 (using 32 bit unsigned int arithmetic), which gives the answer in microvolts.

When you display the answer (microvolts) you can put in a decimal point, to make it volts.

9,999,999 (microvolts) = 9[.]999999 volts = 9999[.]999 millivolts (if you prefer).

EDIT:
Only the first 5 digits, are useful/correct.
So (ADC * 2442)/1000 using 32 unsigned ints, gives you a directly displayable value, in millivolts.

EDIT2;
As other(s) have mentioned, it is good programming practice to label the constants, and use sensible units within the variables. I've NOT done that here, as I just wanted to show you how to do it, simply.

tl;dr
print this (ensuring 32 bits are used, answers in millvolts using only integers):
(ADC * 2442)/1000
« Last Edit: June 08, 2016, 07:46:47 pm by MK14 »
 

Offline helius

  • Super Contributor
  • ***
  • Posts: 3639
  • Country: us
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #7 on: June 08, 2016, 07:36:15 pm »
As alert readers have already commented, the slightly more optimized version would look like
long voltage_dmV /* decimillivolt */ = adv_value * 100000UL >> 12;
But the itoa() or printf() functions may be using divide instructions anyway, so don't assume you're saving time, profile!
You probably also will need to use division to separate the whole volts from the fraction part, unless you do grovely things with snprintf() and string insertions.
 

Offline nctnico

  • Super Contributor
  • ***
  • Posts: 26891
  • Country: nl
    • NCT Developments
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #8 on: June 08, 2016, 07:49:03 pm »
Another note about printing fixed point values: you'll also need to round the result to the number of decimals you want to display.
There are small lies, big lies and then there is what is on the screen of your oscilloscope.
 

Offline hamster_nz

  • Super Contributor
  • ***
  • Posts: 2803
  • Country: nz
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #9 on: June 08, 2016, 07:49:34 pm »
Code: [Select]
adc_mv = adc*2 + adc/2 - adc/16 + adc/256

Exercise for the reader (or compier) to convert to efficient left/right shifts.

Also assumes full range is ADC count of 4096 you will need to add one or two more terms to get 4095 to map to 10,000.
Gaze not into the abyss, lest you become recognized as an abyss domain expert, and they expect you keep gazing into the damn thing.
 

Offline MK14

  • Super Contributor
  • ***
  • Posts: 4527
  • Country: gb
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #10 on: June 08, 2016, 07:51:58 pm »
Code: [Select]
adc_mv = adc*2 + adc/2 - adc/16 + adc/256

Exercise for the reader (or compier) to convert to efficient left/right shifts.

Also assumes full range is ADC count of 4096 you will need to add one or two more terms to get 4095 to map to 10,000.

(ADC*2442)/1000

Should be considerably faster (no divides), more accurate (as it uses the CORRECT 4095 value), and quicker to type (shorter).

EDIT:

BUT your solution ONLY needs 16 bit variables, which may be an advantage.
« Last Edit: June 08, 2016, 07:58:23 pm by MK14 »
 

Offline ChristopherTopic starter

  • Frequent Contributor
  • **
  • Posts: 429
  • Country: gb
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #11 on: June 08, 2016, 08:00:48 pm »
Code: [Select]
adc_mv = adc*2 + adc/2 - adc/16 + adc/256

Exercise for the reader (or compier) to convert to efficient left/right shifts.

Also assumes full range is ADC count of 4096 you will need to add one or two more terms to get 4095 to map to 10,000.

(ADC*2442)/1000

Should be considerably faster (no divides), more accurate (as it uses the CORRECT 4095 value), and quicker to type (shorter).

EDIT:

BUT your solution ONLY needs 16 bit variables, which may be an advantage.
Yes! Perfect! I understand how you got these numbers, hot to change them for other resolutions and have made a spreadsheet for future awesomeness..

I guess a DAC will be similar, just in reverse. I already have made sweet functions similar to itoa and scanf for converting strings to ints and back. That's the easy part for me !

Thanks everyone!
 

Offline hamster_nz

  • Super Contributor
  • ***
  • Posts: 2803
  • Country: nz
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #12 on: June 08, 2016, 10:23:13 pm »
Should be considerably faster (no divides), more accurate (as it uses the CORRECT 4095 value), and quicker to type (shorter).

EDIT:

BUT your solution ONLY needs 16 bit variables, which may be an advantage.

Yeah, here is it properly worked out.
Code: [Select]
Term     value   Qty Sub total
n x 2     8190     1      8190
n         4095         
n / 2     2047     1      2047
n / 4     1023         
n / 8      511         
n / 16     255    -1      -255
n / 32     127         
n / 64      63         
n / 128     31         
n / 256     15     1        15
n / 512      7         
n / 1024     3     1         3
n / 2048     1         
   Final sum of terms    10000

Code: [Select]
adc_mv <= (adc<<1)  + (adc>>1) - (adc>>4) + (adc>>8) + (adc>>10);

Are you sure it is more accurate? as 4095*2442/1000 = 9,999

Are you sure it will be faster? It really depends on target CPU...

Also a lot of this is on shaky foundations, as the ADC values are 'bins' - when properly calibrated anything from 0mv to 2.442mv should end up reading as 0, so maybe it should be displaying 1mV rather than 0mV. Likewise the 4095th bin might actually cover 9.9976mV to 10.000mV, giving options for the display of 9.997, 9.998, 9.999 or 10.000V as the displayed value (depending on rounding preferences).

Yeach - I hate edge cases :)



Gaze not into the abyss, lest you become recognized as an abyss domain expert, and they expect you keep gazing into the damn thing.
 
The following users thanked this post: bilibili

Offline MK14

  • Super Contributor
  • ***
  • Posts: 4527
  • Country: gb
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #13 on: June 08, 2016, 10:45:10 pm »
Should be considerably faster (no divides), more accurate (as it uses the CORRECT 4095 value), and quicker to type (shorter).

EDIT:

BUT your solution ONLY needs 16 bit variables, which may be an advantage.

Yeah, here is it properly worked out.
Code: [Select]
Term     value   Qty Sub total
n x 2     8190     1      8190
n         4095         
n / 2     2047     1      2047
n / 4     1023         
n / 8      511         
n / 16     255    -1      -255
n / 32     127         
n / 64      63         
n / 128     31         
n / 256     15     1        15
n / 512      7         
n / 1024     3     1         3
n / 2048     1         
   Final sum of terms    10000

Code: [Select]
adc_mv <= (adc<<1)  + (adc>>1) - (adc>>4) + (adc>>8) + (adc>>10);

Are you sure it is more accurate? as 4095*2442/1000 = 9,999

Are you sure it will be faster? It really depends on target CPU...

Also a lot of this is on shaky foundations, as the ADC values are 'bins' - when properly calibrated anything from 0mv to 2.442mv should end up reading as 0, so maybe it should be displaying 1mV rather than 0mV. Likewise the 4095th bin might actually cover 9.9976mV to 10.000mV, giving options for the display of 9.997, 9.998, 9.999 or 10.000V as the displayed value (depending on rounding preferences).

Yeach - I hate edge cases :)

I agree, the "fastest" probably depends on the specific cpu and compiler etc. Sometimes the obviously faster method ends up taking 10 times as long, because of unforeseen optimizations and/or the compiler making a mess of things.

I also agree, I can be out (my formula above) by 1 part in 10,000 but since there are only 4096 values, I decided that was "good enough".

ADCs can get very complicated.
Some people would drop the least significant bit, or few bits, as they quite often just represent noise and/or instability, rather than particularly useful values.
Many give at least 1 or 2 bit values worth of jitter, each time you read it, even when it is fixed solidly to a specific voltage (depending on the quality of the ADC).

There are many ways of doing this, none of which is necessarily the "right" or "best" way.
 

Offline dannyf

  • Super Contributor
  • ***
  • Posts: 8221
  • Country: 00
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #14 on: June 08, 2016, 10:47:03 pm »

"
My ADC result is 0-4095. I need to convert this to be 0-10V as a string."

On chips with the right hardware, it iss simple.

On chips without the right hardware, you will need to simplify.

Adc x 10000 / 4096 = adc x 625 / 256.

"/ 256" can be done by disposing of the lsb.

"X 625" can be done by decomposin it into a series of shifts and sums. For example "x 625 = x 512 + x 128 - x 16 + x 1", all of that can be done via shifts plus sums.

You probably want to benchmark it against other alternate approaches to see if it is indeed faster on your target.
================================
https://dannyelectronics.wordpress.com/
 
The following users thanked this post: Ian.M

Offline dannyf

  • Super Contributor
  • ***
  • Posts: 8221
  • Country: 00
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #15 on: June 08, 2016, 10:58:55 pm »
The other way around, "x 4096 / 10000" is more difficult. What I usually do is to approximate it to something of a division by power of 2. For example, I would approximate it to "x 26529 / 64768". You can then decompose that into a series of shifts and sums and .....

Again, do bengmark it on your target to see for sure if you are gaining anything.
================================
https://dannyelectronics.wordpress.com/
 

Offline orin

  • Frequent Contributor
  • **
  • Posts: 445
  • Country: us
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #16 on: June 08, 2016, 11:02:54 pm »
So the original poster wanted it as a string, with 0-10V.

Given the requirement to convert to decimal and a range of 0-10V, you can treat the ADC result as a binary fraction with the binary point to the left of the 12th bit.  Multiply by 10 and you get the most significant decimal digit in the 13th to 16th bits.  Discard the 13th to 16th bits and multiply by 10 again to get the next decimal digit.  Produce 5 digits this way, then round the result to 4 digits.

Only multiplies by 10 are required which can be done with a shift and a couple of adds.

If you want to scale by 4096/4095, you can add an extra bit to the right of the ADC result and set it to 1 for ADC values >= 2048.  You can also round up by adding 1 in the extra bit position (in this case, the first digit can come out as 0xA which needs to be special cased as 10).  Unfortunately, adding the extra bit requires 17 bits to implement; I'd shift the ADC result left by 4 to start with in this case and use 24/32 bit arithmetic, depending on the machine/compiler the code is running on.  I've used this method successfully in PIC assembly code where you really don't want to do multiply or divide.

 

Offline nctnico

  • Super Contributor
  • ***
  • Posts: 26891
  • Country: nl
    • NCT Developments
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #17 on: June 08, 2016, 11:18:01 pm »
Code: [Select]
adc_mv = adc*2 + adc/2 - adc/16 + adc/256
Exercise for the reader (or compier) to convert to efficient left/right shifts.
Any decent compiler already does this so no need to get creative (and probably make things worse). However since many modern microcontrollers have hardware multipliers multiplying the ADC value with the full range (say 10000mV) and then dividing by the ADC range (which is a power of 2) will be faster.
There are small lies, big lies and then there is what is on the screen of your oscilloscope.
 

Offline hamster_nz

  • Super Contributor
  • ***
  • Posts: 2803
  • Country: nz
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #18 on: June 08, 2016, 11:29:46 pm »
Given the requirement to convert to decimal and a range of 0-10V, you can treat the ADC result as a binary fraction with the binary point to the left of the 12th bit.  Multiply by 10 and you get the most significant decimal digit in the 13th to 16th bits.  Discard the 13th to 16th bits and multiply by 10 again to get the next decimal digit.  Produce 5 digits this way, then round the result to 4 digits.

Awesome - love it!  :-+
Gaze not into the abyss, lest you become recognized as an abyss domain expert, and they expect you keep gazing into the damn thing.
 

Offline hamster_nz

  • Super Contributor
  • ***
  • Posts: 2803
  • Country: nz
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #19 on: June 08, 2016, 11:58:27 pm »
Code: [Select]
adc_mv = adc*2 + adc/2 - adc/16 + adc/256
Exercise for the reader (or compiler) to convert to efficient left/right shifts.
Any decent compiler already does this so no need to get creative (and probably make things worse).
True, but I still find great comfort in knowing that it will only be working with numbers are bounded within the range of 0 to around 10500.

If this was in a function, and somebody gives it an input value of 4,095,000 as an input (e.g. if for some silly reason they wanted to display in microvolts) it will still calculate a correct answer.

It might not be important to you, but it is to me.

However Orin's answer is very, super nice:

Code: [Select]
  char digit[5];
  u16 i;
  u16 t = adc;

  /* Slight fudge to map 4095 to 10,000  */
  t += t>>11;

  /* Leading '1' or space */
  digit[0] = (t&0xF000) ?  '1' : ' ';
  t &= 0xFFF;

  /* Other digits */
  for(i = 1; i < 5; i++) {
    digit[i] = (t >> 12)+'0';
    t &= 0xFFF;
    t = t<<1 + t<<3;
 }

Can I steal it? Hats off to you sir!
« Last Edit: June 09, 2016, 02:53:55 am by hamster_nz »
Gaze not into the abyss, lest you become recognized as an abyss domain expert, and they expect you keep gazing into the damn thing.
 

Offline dannyf

  • Super Contributor
  • ***
  • Posts: 8221
  • Country: 00
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #20 on: June 09, 2016, 12:23:10 am »
Quote
Only multiplies by 10 are required which can be done with a shift and a couple of adds.

Pretty sleek indeed.

My implementation of your idea:

Code: [Select]
#define X2(x)  ((x) << 1)
#define X8(x)  ((x) << 3)
#define X10(x) (X8(x) + X2(x))

//convert a 12-bit dat to decimal string
void dat2str8(uint8_t buffer, uint16_t dat) {
  dat = X10(dat); str[0]=(dat >> 12) + '0'; dat &= 0x0fff;
  dat = X10(dat); str[1]=(dat >> 12) + '0'; dat &= 0x0fff;
  ...
}

very good, I have to say.
================================
https://dannyelectronics.wordpress.com/
 

Offline wraper

  • Supporter
  • ****
  • Posts: 16847
  • Country: lv
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #21 on: June 09, 2016, 12:24:54 am »
"cheating" like this (multiply then shift out bits) will be FAST
Depends on MCU, some like 8051 modern variants like Silabs CIP-51 core can divide in a few cycles but shifting a few bits will take more CPU cycles because it can only do "Rotate right through the carry". So your shifting for a few bits basically becomes a for loop of rotate right through the carry n times. About 2 weeks ago I needed to optimize the code for speed by throwing out bit shifting and using division instead.
 

Offline orin

  • Frequent Contributor
  • **
  • Posts: 445
  • Country: us
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #22 on: June 09, 2016, 02:32:03 am »
Code: [Select]
adc_mv = adc*2 + adc/2 - adc/16 + adc/256
Exercise for the reader (or compiler) to convert to efficient left/right shifts.
Any decent compiler already does this so no need to get creative (and probably make things worse).
True, but I still find great comfort in knowing that it will only be working with numbers are bounded within the range of 0 to around 10500.

If this was in a function, and somebody gives it an input value of 4,095,000 as an input (e.g. if for some silly reason they wanted to display in microvolts) it will still calculate a correct answer.

It might not be important to you, but it is to me.

However Orin's answer is very, super nice:

Code: [Select]
  char digit[5];
  u16 i;
  u16 t = adc;

  /* Slight fudge to map 4095 to 10,000  */
  t += t>>11;

  /* Leading '1' or space */
  digit[0] = (t&0xF000) ?  '1' : ' ';
  t &= 0xFFF;

  /* Other digits */
  for(i = 1; i < 5; i++) {
    digit[i] = (t >> 24)+'0';
    t &= 0xFFF;
    t = t<<1 + t<<3;
 }

Can I steal it? Hats off to you sir!


Sure you can.  It's public domain as far as _I_ am concerned.

Orin.
 

Offline orin

  • Frequent Contributor
  • **
  • Posts: 445
  • Country: us
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #23 on: June 09, 2016, 02:47:30 am »
"cheating" like this (multiply then shift out bits) will be FAST
Depends on MCU, some like 8051 modern variants like Silabs CIP-51 core can divide in a few cycles but shifting a few bits will take more CPU cycles because it can only do "Rotate right through the carry". So your shifting for a few bits basically becomes a for loop of rotate right through the carry n times. About 2 weeks ago I needed to optimize the code for speed by throwing out bit shifting and using division instead.


Horses for courses...

FWIW, multiply by 10 doesn't really need shifts, but you could use <<1 for all but the 3rd line:

t = x + x;  // t == 2*x
t = t + t;   // t == 4*x
t = t + x;  // t == 5*x
t = t + t;  // t == 10*x


 
The following users thanked this post: oPossum, hamster_nz

Offline orin

  • Frequent Contributor
  • **
  • Posts: 445
  • Country: us
Re: ADC/DAC Conversion to "decimal", best practices....
« Reply #24 on: June 09, 2016, 03:43:17 am »
So here is some of my original PIC code.  ReadAtoD reads 10 bits from an ADC into the high bits of AtoDRes.

So it is using an actual multiply routine rather than shifts/adds to multiply by 10.  Partly because it's not always 10 in W at the ThreeC label - the same technique works for scaling by any integer 1 to 10.

CallRet is really just jump and DisplayDigit is uninteresting.

Scaling by powers of 10 is merely a matter of where you put the decimal point in the generated digits.  The code at ThreeC doesn't add a decimal point, so you have three multiplies by 10, hence a scale factor of 1000.

Code: [Select]
        call    ReadAtoD

         movlw   .10                     ; Scale by 1000 (2:1 on board)
ThreeC                                  ; Three digits then 'C'
        call    FirstDigit
        call    NextDigit
        call    NextDigit
; SNIP


;--------------------------------------------------------------

;
; FirstDigit - scale by factor in W and print the first digit
;
FirstDigit
movwf ACCbLO
clrf ACCbHI
movf AtoDResLO, W ; Max scale is 1000
movwf ACCaLO
movf AtoDResHI, W
movwf ACCaHI
call D_mpyS
CallRet DisplayDigit

;
; NextDigit - multiply by 10 and display digit
;
NextDigit
movf ACCcLO, W
movwf ACCaLO
movf ACCcHI, W
movwf ACCaHI
movlw .10
movwf ACCbLO
clrf ACCbHI
call D_mpyS
CallRet DisplayDigit
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf