Ttelmah
Joined: 11 Mar 2010 Posts: 18606

Scaled integer maths 
Posted: Tue Apr 21, 2020 1:41 am 


OK.
In the forum you will quite often have the 'old hands' saying 'use integer
maths'. What is this about?. Why?.
So lets start with a very basic design. A PIC 18F4520, used as a 0 to 5v
voltmeter. Sending the value to the serial port.
Two solutions. The first the 'obvious' one. Just use fp maths:
Code: 
#include <18F4520.h>
#device adc=10
#fuses NOWDT
#use delay (INTERNAL=16Mhz)
#use rs232(UART1, baud=9600, bits=8, ERRORS, STREAM=PC)
//always do the 'setup' first
void main(void)
{
float volts;
setup_ADC_ports(AN0, VSS_VDD);
setup_adc(ADC_CLOCK_DIV_16);
//Read the data sheet for this. Here the Tad must be 0.7 to 25uSec
//So from 16MHz, /16 gives 1uSec. Ideal.
set_adc_channel(0);
//Now the ADC needs time to 'acquire' the signal. This can be programmed
//as a multiplier from this clock. However With the long delays in the loop
//I'm not using this.
fprintf(PC,"Awake\n");
while (TRUE)
{
delay_ms(500);
volts=read_adc()*5.0/1024.0;
fprintf(PC,"Reading %5.3fv\n",volts);
}
}

Now this merrily goes off and prints the voltage on the AN0 input
as n.nnnv every half a second. Perfect solution?.
But is it?.
The first thing to note is the size. Here 1670 bytes. Then run the code
and test how long it takes to do the maths, and print the result. 24.4mSec.
Now compare with this:
Code: 
//Same setup code
void main(void)
{
int32 volts;
setup_ADC_ports(AN0, VSS_VDD);
setup_adc(ADC_CLOCK_DIV_16);
//Read the data sheet for this. Here the Tad must be 0.7 to 25uSec
//So from 16MHz, /16 gives 1uSec. Ideal.
set_adc_channel(0);
//Now the ADC needs time to 'acquire' the signal. This can be programmed
//as a multiplier from this clock. However With the long delays in the loop
//I'm not using this.
fprintf(PC,"Awake\n");
while (TRUE)
{
delay_ms(500);
volts=(int32)read_adc()*5000/1024;
fprintf(PC,"Reading %5.3lwv\n",volts);
delay_cycles(1);
}
}

Now the first interesting thing is the code is about half the size (818 bytes).
Then it is faster. Only a little, but about 450uSec in total.
So what is going on?.
The compiler is very intelligent. On the sum:
read_adc()*5.0/1024.0;
It presolves the right hand half of this, and turns it into a single
multiplication by 0.0048828125. So this sum takes just 222uSec.
On the equivalent integer sum, this can't be presolved like this so it
stays as a multiplication then division. So actually ends up taking
slightly longer. However then there are the multiple
divisions to generate the digits in the printf. These take 412uSec
each in the floating point version, while only 298 in the integer
version. So at the end there is a total saving of nearly 0.5mSec
in speed. The total output takes just under 24mSec as given.
The saving is much more noticeable if you output the results to a
string, rather than delaying for the serial. It is the serial delays
that are giving much of the time involved here.
Now think about what it does. You have an ADC reading of 512.
For this you expect to see 2.5v. For the float version, we have:
512*5 = 2560
then
2560/1024 = 2.5
Exactly right.
Now the integer version:
512*5000 = 2560000
then
2560000/1024 = 2500
which the %w format displays as 2.500
Again exactly right.
So the key here is that if instead of using floating point values like
x.xxv, you work in smaller units and use integers, you can save a
lot of space, and significant time. Here I'm doing the sum in 'integer mV'.
Thousandths of a volt, and can use integer numbers of these.
Key thing is that to keep in 'integer' the maths used must multiply
first. If you tried instead to use /1024 earlier in the sum, data would be
lost. So a little care is needed.
There is also another factor, which is accuracy. An int32 gives over 9
usable digits. A fp value only just over 6. So values are actually more
accurate processed this way.
So, think about it, when designing your code...
This is a link to a thread in the main forum, where using this for a
more complex sum is discussed:
<http://www.ccsinfo.com/forum/viewtopic.php?t=56091&postdays=0&postorder=asc&start=0>
This raises the next important part to this.
On an unsigned integer number, the following divisions can be done
really quickly by simply 'shifting' the value:
/2, /4, /8, /16, /32, /64, /256 and so on.
In fact /256 and /65536 can be done even faster by just taking the
required bytes out of the result.
In this thread I do the integer equivalent of *0.48828125, by just
multiplying by 32000, and then taking the upper 16bits of the result.
This is several times faster and smaller. So again. Think about it... 
