Products > Programming

PIC C divide by 100

(1/4) > >>

G'day all,

I have a Webasto heater in my car. It reports battery voltage in mV.
Among other things, I have an interface where an external status LED flashes stuff out on request of a remote, and I wanted to add battery voltage as one of the parameters. I figured 3 digits was enough, so I needed to divide the voltage by 100 (or thereabouts).

I don't use anything other than addition or subtraction anywhere in the code, so the challenge was to do a "near enough" divide by 100 with close enough rounding to give me +/- 100mV on the output without pulling any maths routines. The limitations are :
- Voltage is in a 16 bit register in mV, so it's never going to be high enough to hit the MSB.
- Result is in an 8 bit register because if it exceeds 160 then I have bigger things to worry about.

This is what I came up with :
- heater_voltage is the raw result from the heater (u16). Both volt and vtemp are u8.

--- Code: --- // Hacky divide by 98.81
vtemp=heater_voltage >> 7 & 0xff; // 7
volt=vtemp;
vtemp=vtemp >> 2; // 9
volt += vtemp;
vtemp=vtemp >> 3; // 12
volt += vtemp;
vtemp=vtemp >> 1; // 13
volt += vtemp;

--- End code ---

It does the job because precision is not a requirement. I wanted to see if I could minimise the generated code size and stay within a "yeah, that's useful".
This isn't a contest of any kind, I'm more interested to see of someone comes up with a "yeah, here's how to do it better".

ledtester:
Just using the first 3 of your bit positions might yield a better result:

1/(1/128+1/512+1/4096) = 99.90243902439024390244

and it'll be faster!

Update: I decided to write a program to compare the two methods against the true divide by 100 value and here the results over the range 0 to 16383. The diff column is true value minus algorithm value and the table shows the number of times each of the diff values were attained.

--- Code: ---        7-9-12  7-9-12-13
diff
-2        2600       1444
-1        9904       6140
0         3856       6680
1           24       2100
2            0         20
total:   16384      16384

--- End code ---

So the 7-9-12 scheme tends to underestimate the true value more than the 7-9-12-13 scheme. After some thought I guess this makes sense as we are omitting the fractional parts when we divide by shifting and those fractional parts can add up. The contribution of the 13th-bit compensates for that truncation.

artag:
If you only want an 8-bit (or less  - 160 counts) then you could shift right by 6 bits and then put the remaining bits through a lookup table.
Faster but more codespace, though I wouldn't imagine speed is an issue.

mariush:
You could adapt this code to your needs, basically, doing it twice to perform two divisions by 10

--- Code: ---unsigned divu10(unsigned n) {
unsigned q, r;
q = (n >> 1) + (n >> 2);
q = q + (q >> 4);
q = q + (q >> 8);
q = q + (q >> 16);
q = q >> 3;
r = n - (((q << 2) + q) << 1); return q + (r > 9);
}

--- End code ---

From a post on that same page ... If you can use a 32 bit register/variable, division by 10 is basically  :  (\$n * 6554 ) >> 16;

--- Quote from: ledtester on August 13, 2022, 04:14:40 pm ---So the 7-9-12 scheme tends to underestimate the true value more than the 7-9-12-13 scheme. After some thought I guess this makes sense as we are omitting the fractional parts when we divide by shifting and those fractional parts can add up. The contribution of the 13th-bit compensates for that truncation.

--- End quote ---

Yep. That was exactly the reason I went with the 7-9-12-13. For the range values required the truncation always resulted in an "under" so adding the extra element got it "closer". I also played with 7-9-13-13 which was mathematically more of a midway point, but the 12-13 seemed to give more "consistent" results.

--- Quote from: mariush on August 13, 2022, 06:55:43 pm ---You could adapt this code to your needs, basically, doing it twice to perform two divisions by 10
... If you can use a 32 bit register/variable, division by 10 is basically  :  (\$n * 6554 ) >> 16;
--- End quote ---

Yes, I looked at a few of those, but I was trying to minimise the math, and both 16 and 32bit uses a lot of space on an 8 bit processor. That's why after the first shift I broke it down to 8 bit values, because I can for the range of inputs expected and it halves the code size.

Of course the smallest way to do it in code terms was iterative subtraction, but it was only fractionally smaller than the multiple shift/add method and a *lot* slower.

More an exercise of optimising the routine for the task rather than general purpose. When you can say with certainty your input is going to be between 10800 and 15000 it becomes an exercise in "how accurate does it have to be for the available level of precision".

Something different than the usual "homework" questions anyway.

Appreciate the input!