Float value calculations in non FPU MCU - Page 1
Forum section: electronics / beginners

author: Veketti
author_rank: Regular Contributor
timestamp: April 05, 2023, 05:46:45 am
content: Hello,Quick guestion regarding following calculation. Which one is better (and why) to be used on 8 bit PIC 16F family microcontroller which doesn't have FPU?Code:unsigned int ForceReading = 0;unsigned long ADCValues2 = 0;// ADCValues2 is 12 bit ADC reading 0 to 4096ForceReading = (15267*ADCValues2-12244000)/10000; // Is this better orForceReading = 1.5267*ADCValues2-1224.4; // this one?I know that the result will be filtered the decimal, but that is ok.Thank you in advance.

author: iMo
author_rank: Super Contributor
timestamp: April 05, 2023, 06:20:48 am
content: Single precision floating point maintains 6-7 digits, based on the math operation.With 32bit integer it is 9, provided you will do it with care.

author: JustMeHere
author_rank: Frequent Contributor
timestamp: April 05, 2023, 06:53:28 am
content: I'd compile to assembly code and compare the two programs assembly code. There should be an option on your compiler to do this.

author: brucehoult
author_rank: Super Contributor
timestamp: April 05, 2023, 07:51:26 am
content: Neither.Code:(50027 * ADCValues2 - 40121139 + 16384) >> 15;With ADCValues2 limited to 4095, the largest product will be 134 million, needing 28 bits. This will easily fit into a 16x16 -> 32 unsigned multiply.It will be faster to shift left by 1 and then just drop the 2 bottom bytes.

author: Nominal Animal
author_rank: Super Contributor
timestamp: April 05, 2023, 07:57:03 am
content: AIUI, PIC 16F does not have hardware multiplication at all. I would use:Code:int convert_adc(unsigned int x){int32_t c = -80242646; /* slightly optimized from 65536*1224.4 */int32_t m = 100054; /* 65536*1.5267, rounded */x &= 4095;while (x > 0) {if (x & 1)c += m;x >>= 1;m *= 2;}/* Divide by 65536, but round the result, halfway away from zero. */if (c < 0)return -((32768 - c) >> 16);elsereturn ((32768 + c) >> 16);}This gives you almost exactly the same results as the floating point solution rounded half away from half (-0.5≃-1, +0.5≃+1), except that eleven ADC readings yield a result that is incorrectly rounded:adc 15 yields -1202, correct is -1201.4995adc 296 yields -773, correct is -772.4968adc 427 yields -573, correct is -572.4991adc 839 yields 56, correct is 56.5013adc 1251 yields 685, correct is 685.5017adc 2749 yields 2973, correct is 2972.4983adc 3161 yields 3602, correct is 3601.4978adc 3292 yields 3802, correct is 3801.4964adc 3573 yields 4231, correct is 4230.4991adc 3704 yields 4431, correct is 4430.4968adc 3985 yields 4860, correct is 4859.4995The conversion itself is based on ForceReading = (100054*ADCValues2 - 80242646)/65536 when rounded, with halfway away from zero; very similar to brucehoults suggestion above.

author: brucehoult
author_rank: Super Contributor
timestamp: April 05, 2023, 08:25:06 am
content: If he is programming in C then I would assume the compiler already has a runtime library function for 16x16 -> 32 that is more optimised than your simple loop.

author: Veketti
author_rank: Regular Contributor
timestamp: April 05, 2023, 08:26:58 am
content: Wow, good that I asked. I would never had considered anything like this. Thank you.@Nominal Animal, one clarification though. You said it doesn't have hardware multiplication, but in your code there isCode:m *= 2;. Is this not considered as it is just multiply by two?

author: Veketti
author_rank: Regular Contributor
timestamp: April 05, 2023, 08:27:56 am
content: Quote from: brucehoult on April 05, 2023, 08:25:06 amYes, C language using mikroC PRO for pic.

author: langwadt
author_rank: Super Contributor
timestamp: April 05, 2023, 08:38:10 am
content: Quote from: Veketti on April 05, 2023, 08:26:58 amwould assume the compiler is smart enough to realise hat is just a shift

author: Veketti
author_rank: Regular Contributor
timestamp: April 05, 2023, 08:46:32 am
content: Quote from: langwadt on April 05, 2023, 08:38:10 amSo the compiler would do this like m << 1; ?

author: Veketti
author_rank: Regular Contributor
timestamp: April 05, 2023, 08:51:31 am
content: Quote from: brucehoult on April 05, 2023, 07:51:26 amThis is amazing. Is there any chance you could explain how did you end up to this code?

author: brucehoult
author_rank: Super Contributor
timestamp: April 05, 2023, 08:55:38 am
content: Quote from: Veketti on April 05, 2023, 08:46:32 amIt would be a very very poor compiler if it did not.Sadly it doesn't seem to be easy to find information about this compiler.

author: brucehoult
author_rank: Super Contributor
timestamp: April 05, 2023, 08:58:05 am
content: Quote from: Veketti on April 05, 2023, 08:51:31 amSure. I just multiplied 1.5267 and 1224.4 by 32768 (2**15) instead of 10000. The 16384 is to get rounding instead of chopping. The ">> 15" is the same as "/ 32768".The whole point is to avoid a very very slow real divide by 10000 at the end and instead divide by a power of 2.

author: iMo
author_rank: Super Contributor
timestamp: April 05, 2023, 09:03:25 am
content: Pretty known, frankly, everybody who did with pics in last ~two decades know ithttps://www.mikroe.com/mikroc-pic

author: Veketti
author_rank: Regular Contributor
timestamp: April 05, 2023, 09:11:13 am
content: Thank you so much! Now I think I'm starting to get hang of these bitwise operations. However one more clarification. 15 bits is 32767 so where does that extra one come when you say 32768?

author: brucehoult
author_rank: Super Contributor
timestamp: April 05, 2023, 09:19:00 am
content: Quote from: iMo on April 05, 2023, 09:03:25 amYes, I found their site, of course, but it didn't seem to be easy to find out how good the compiler actually is. They have a lot of fancy words, of course.I've never used PIC with C -- I've hand-coded a little -- and everything else (except 6502 and z80) I use the gcc or llvm toolchains, which of course are both extremely solid with well-known capabilities. Even microcontrollers such as AVR and MSP430 are well-supported by gcc, and of course ARM and RISC-V are bread and butter for it.With gcc or llvm one would never even consider it a possibility that a multiply or divide by a power of 2 might not be reduced to a shift, or that accessing array.foo inside a loop with i as a counter would not be reduced to simply adding a constant to a pointer each time around the loop.

author: voltsandjolts
author_rank: Unknown
timestamp: April 05, 2023, 09:20:15 am
content: Quote from: Veketti on April 05, 2023, 09:11:13 am15 bits has 32768 possible values, 0 up to 32767.x >> 1 is same as x/2x >> 2 is same as x/4x >> 3 is same as x/8..x >> 15 is same as x/32768

author: brucehoult
author_rank: Super Contributor
timestamp: April 05, 2023, 09:20:45 am
content: Quote from: Veketti on April 05, 2023, 09:11:13 amThe maximum value of a 15 bit unsigned integer is 32767, but when you include 0 there is a total of 32768 values.

author: DavidAlfa
author_rank: Super Contributor
timestamp: April 05, 2023, 10:01:24 am
content: Fixed point will be much faster than floating point in a non-fpu mcu.Make both, toggle a pin before/after and watch the time it takes.

author: Nominal Animal
author_rank: Super Contributor
timestamp: April 05, 2023, 10:03:11 am
content: Quote from: Veketti on April 05, 2023, 08:26:58 amAs explained, it will be compiled to a bit shift. You can usem <<= 1;instead, becausemis always positive.The reason for the-((32768-c) >> 16)(for negativec) is related. Bit shifts of negative values yieldundefinedresults per the C standards. While the compilers I use (GCC, Clang, ICC) handle them in a specific (same sane) manner, I just don't know how mikroe handles it. So, I avoided it.The code I showed is very straightforward to write in assembly. It is likely the compiler uses some canned assembly routine to do 32-bit × 32-bit = 32-bit multiplication, and I suspect you're so tight on Flash that the explicit version of the bit-shift based multiplication will be shorter and faster, unless you use 32×32-bit multiplications elsewhere in your code also.In assembly, each 32-bit shift by one bit is four byte shifts through carry, and each addition is four additions with carry. The inner loop is something like thirteen instructions, two conditional jumps, and one comparison, so my entire suggested function should fit in 20-40 instructions (40-80 bytes of Flash + 8 bytes for the two 32-bit constants) –– but I haven't done PIC 16F assembly, so that is just an estimate.

author: radiolistener
author_rank: Super Contributor
timestamp: April 05, 2023, 10:04:05 am
content: integer arithmetic is more fast and precise, so it's definitely preferred

author: Nominal Animal
author_rank: Super Contributor
timestamp: April 05, 2023, 10:42:08 am
content: Quote from: Veketti on April 05, 2023, 09:11:13 amThe value of bitiis 2i, with left shifts using positivei, and right shifts using negativei, when we use the most common labeling system where the least signiicant (integral) bit isi=0, increasing towards more significant bits.(Fractional bitkhas value 2-k= 1 / 2k.)If you have (nonnegative) integeru, thenu<<n==u× 2nandu>>n==u/ 2n==u× 2-nAgain, the C standard says that the shifts are undefined ifuis negative. Most hardware uses two's complement for signed integers, so that left shift does as above, except when the result overflows. Right shift has typically two variants: arithmetic right shift, and binary right shift. Arithmetic right shift duplicates the most significant bit, binary right shift shifts in zero. In PIC16F, we'd use shift through carry (ONLY), with carry set based on the most significant bit before shifting.20= 1.21= 2, 22= 4, 23= 8, 24= 16, 25= 32, and so on.2-1= 1/2, 2-2= 1/4, 2-3= 1/8, 2-4= 1/16, 2-5= 1/32, and so on.Shifts by 8 bits means dropping or inserting entire bytes.PIC16F does not have a hardware multiplication, and can only shift one bit at a time, as far as I know. (I could be wrong, as I don't use PICs myself, and don't have a PIC16F assembly or binary emulator.)This means that the most efficient way to implement (A×B+C)/2D, is toIf abs(A) > abs(B), swapAandB.IfA< 0, negate bothAandB.Initialize the accumulator (result) toC.We now have positiveAsmaller in magnitude thanB.WhileA> 0:If the least significant bit ofAis set, addBto accumulator.ShiftAright by one bit.ShiftBleft by one bit.To apply rounding halfway up, add 2D-1to accumulator.To apply rounding halfway away from zero, subtract (if accumulator negative) or add (if accumulator positive) 2D-1from/to accumulator.While any of the three least significant bits ofDare nonzero:Shift accumulator right by one bit.DecrementDby one.Dis now a multiple of 8, or zero.Drop (D/8) least significant bytes of the result.We examine three least significant bits ofD, because 23=8, meaning the three least significant bits have value 0 through 7, inclusive.Thus, it makes sense to keepDa multiple of 8 –– and thus the divisor 256 (D=8), 65536 (D=16), 16777216 (D=24), or 4294967296 (D=32), or perhaps a multiple of 8, plus zero, one, or two.To implementDa multiple of 8 minus zero, one, or two efficiently, we'd need to shift accumulatorleftandincrement Dby one in the second loop, but since that easily overflows, I recommend the above (which should not overflow unless the product itself overflows) –– although you can easily extend it to 40-bit, 48-bit, 56-bit, or even 64-bit accumulator andBandC, when writing it in assembly.Even when you use C, the compiler has to either generate code that does the above, or call a function it provides that does the above but with variable arguments; I think knowing what and how that is done in the hardware will help you design your code better (by choosing better compromises), because you have very little RAM and Flash on PIC16F's.Here, we haveA=0..4095,B=1.5267×2D, andC≃1224.4×2D(but sinceBisn't exact, adjustingCwill yield more exactly correct results – I selected one via brute-force test program, although there are formulae you can use), so we can omit several steps from the above.Again, if you do use 32-bit integer multiplication between two variables elsewhere in your code, the above will be a bit faster but waste Flash space, and you're likely better off using justCode:int convert_adc(unsigned int x){int32_t c = (int32_t)100054 * (int32_t)x - 80242646;/* Divide by 65536, but round the result, halfway away from zero. */if (c < 0)return -((32768 - c) >> 16);elsereturn ((32768 + c) >> 16);}This and my earlierconvert_adc()do produce identical results. If you do not multiply integer variables elsewhere in your code, my earlier function is likely better.

author: Psi
author_rank: Super Contributor
timestamp: April 05, 2023, 11:25:20 am
content: ah, the fun of fudging decimal operations by multiplying them up and dividing them back down later, and only doing divides that are a power of 2.It's just something you get used to when using a MCUs without a FPU.Less of an issue today with STM32s, but very common with 8/16bit MCUs.Of course you can just declare a float and force the compiler to emulate a FPU in software.But always leave that for only those situations where you absolutely have to, because damn it takes up a lot of flash and cpu cycles.

author: brucehoult
author_rank: Super Contributor
timestamp: April 05, 2023, 12:01:35 pm
content: Quote from: Psi on April 05, 2023, 11:25:20 amIt depends on your performance needs.On a 16 MHz AVR (e.g. 328P) the Arduino floating point library does in the region of 100,000 floating point add/subtract/multiply a second. If you're only working on, say, a 100 Hz control loop then you can actually afford to do a heck of a lot of FP calculations each time.But then those chips have got a huge 32 KB of flash to store all those library routines as well as your own code. You can't afford to be so wasteful if you only have 1 KB or less of program space. OP doesn't say exactly which PIC 16F he is using but they range from 512 to 16k instructions, and up to 2 KB of RAM.

author: Nominal Animal
author_rank: Super Contributor
timestamp: April 05, 2023, 10:26:38 pm
content: Even when you use C or some other language to program these tiny (RAM and ROM/Flash-wise) microcontrollers, it is very useful to be able to read the resulting assembly code. Although the generated code completely depends on the toolchain used (not just brand/make of the compiler and linker, but even the particular version), it does let you easily compare different ways of implementing the same operation.Most importantly, learning toreadassembly is much more valuable/useful than being able towriteefficient assembly.TheCompiler Explorersite (https://godbolt.org/) is an excellent resource for this, because it lets you create a function or a snippet in various languages, and compile it to assembly (machine instructions) for a number of instruction sets and architectures. I do not believe it includes mikroC PRO, though, which is unfortunate.At their core, assembly languages are very simple imperative languages, and are rather easy to learn; especially if you learn how (simplest) processors work internally. 8-bit PICs (just like 6502, 68HC, AVR, and other 8-bitters) are perfect for learning your first assembly language, as long as you remember that while they have more similarities than differences, they do differ architecturally from each other. For example, 8-bit PICs and 6502 have the zero page "file", 256 first bytes of RAM, that are easy to access, but very few registers otherwise; whereas AVR has lots of registers, but all arithmetic/binary operations are done between registers and not directly to/from RAM.Many of the 8-bit architectures (except 6502 I guess) are such that most instructions take a fixed number of cycles, with just a handful of exceptions (that take significantly longer). On PICs, the instructions are even of fixed size. So, to compare two assembly snippets that do the same thing, you really only need to recognize conditionals and jumps, and count instructions, to estimate-compare the snippets,without actually deconstructing how they do what they do.The next step up from that is learning to writemicrobenchmarks(and alsounit teststo test the correctness of the output) on the actual hardware, to test and compare different implementations in real life. It is extremely common for one implementation to be compact but slow, and another to be larger but faster; and for the implementations to differ in what kind of input they deal with "best".(You may notice I explicitly listed the 11 exceptions where the code I've shown yields a result that is different from the exact floating-point result rounded to the nearest integer, with halfway ties away from zero; this is the kind ofunit testIalwaysdo when I suggest an implementation, unless I explicitly yell that my suggestion is completely untested. I have thousands of these, most archived, one per directory, with a text description of the test case and key findings.)Using my crude test bench:D=8:round((adc*391 - 313853)/256), 1555 out of 4096 correct results, error range -2.0418 to +1.5247D=9:round((adc*782 - 627531)/512), 1556 out of 4096 correct results, error range -1.7141 to +1.8451D=10:round((adc*1563 - 1253121)/1024), 2702 out of 4096 correct results, error range -1.1695 .. +1.133D=11:round((adc*3127 - 2508203)/2048), 3443 out of 4096 correct results, error range -0.7894 .. +0.8063D=12:round((adc*6253 - 5014388)/4096), 3729 out of 4096 correct results, error range -0.6670 .. +0.6693D=13:round((adc*12507 - 10030829)/8192), 3958 out of 4096 correct results, error range -0.5612 .. +0.5635D=14:round((adc*25013 - 20059561)/16384), 3982 out of 4096 correct results, error range -0.5456 .. +0.5552D=15:round((adc*50027 - 40121322)/32768), 4085 out of 4096 correct results, error range -0.5032 .. +0.5036D=16:round((adc*100054 - 80242643)/65536), 4085 out of 4096 correct results, error range -0.5032 .. +0.5036D=17:round((adc*200108 - 160485285)/131072), 4085 out of 4096 correct results, error range -0.5032 .. +0.5036D=18:round((adc*400215 - 320968626)/262144), 4096 out of 4096 correct results, error range -0.4998 .. +0.4998This means that using my methods, you needD=18 and 32-bit math to get exact correctly rounded results.My crude test bench program, which takesDas a command-line parameter:Code:// SPDX-License-Identifier: CC0-1.0//// gcc -Wall -O2 find.c -lm -o f#ifndef CONST_A#define CONST_A 1.5267#endif#ifndef CONST_B#define CONST_B 1224.4#endif#define STRINGIFY_(s) #s#define STRINGIFY(s) STRINGIFY_(s)#include <stdlib.h>#include <inttypes.h>#include <stdio.h>#include <string.h>#include <errno.h>#include <math.h>static int parse_int(const char *from, int *to){if (!from || !*from) {errno = EINVAL;return -1;}errno = 0;const char *end = from;long val = strtol(from, (char **)&end, 0);if (errno)return -1;/* Skip trailing spaces, and check garbage at end of input. */while (*end == '\t' || *end == '\n' || *end == '\v' ||*end == '\f' || *end == '\r' || *end == ' ')end++;if (*end) {errno = EINVAL;return -1;}/* Check for overflow. */if ((long)((int)val) != val) {errno = ERANGE;return -1;}if (to)*to = val;return 0;}static double min_err, max_err;int evaluate(const int64_t a, const int64_t b, const int d){const int64_t half = ((int64_t)1 << d) >> 1;int okay = 0;int i, c;min_err = 0.0;max_err = 0.0;for (int x = 0; x < 4096; x++) {double v = CONST_A * x - CONST_B;if (v < 0)c = (int)(v - 0.5);elsec = (int)(v + 0.5);int64_t y = a*x - b;if (y < 0)i = -((half - y) >> d);elsei = (half + y) >> d;double err = (double)i - v;min_err = fmin(min_err, err);max_err = fmax(max_err, err);okay += (c == i);}return okay;}int main(int argc, char *argv[]){double minimum_error, best_min_err, best_max_err;int maximum_okays, d;if (argc != 2 || !strcmp(argv, "-h") || !strcmp(argv, "--help")) {const char *arg0 = (argc > 0 && argv && argv && argv) ? argv : "(this)";fprintf(stderr, "\n");fprintf(stderr, "Usage: %s [ -h | --help ]\n", arg0);fprintf(stderr, " %s D\n", arg0);fprintf(stderr, "\n");fprintf(stderr, "This finds the optimum values for A and B using D fractional bits,\n");fprintf(stderr, "when approximating v = x*" STRINGIFY(CONST_A) " - " STRINGIFY(CONST_B) " for x = 0 .. 4095, using\n");fprintf(stderr, " v = ROUND_BITS(A*x - B, D)\n");fprintf(stderr, "where ROUND_BITS(y, D) divides y by 2**D, rounding ties away from zero.\n");fprintf(stderr, "For N-bit math, D <= N-13.\n");fprintf(stderr, "\n");return 0;}if (parse_int(argv, &d) || d < 1 || d > 51) {fprintf(stderr, "%s: Invalid number of fractional bits.\n", argv);return 1;}const int64_t a0 = (int64_t)(CONST_A * (double)((int64_t)1 << d) + 0.5);const int64_t b0 = (int64_t)(CONST_B * (double)((int64_t)1 << d) + 0.5);printf("Centering search on ROUND_BITS(x * %" PRIi64 " - %" PRIi64 ", %d).\n", a0, b0, d);int64_t best_a = a0;int64_t best_b = b0;maximum_okays = evaluate(best_a, best_b, d);minimum_error = max_err - min_err;best_min_err = min_err;best_max_err = max_err;const int64_t aval[] = { a0, a0+1, a0-1, a0+2, a0-2 };for (size_t ai = 0; ai < sizeof aval / sizeof aval; ai++) {const int64_t a = aval;int64_t db = 0;int okaysa, okaysb;double error;do {db++;okaysa = evaluate(a, b0 + db, d);error = max_err - min_err;if (okaysa > maximum_okays ||(okaysa == maximum_okays && error < minimum_error) ||(okaysa == maximum_okays && error == minimum_error && fmax(max_err, -min_err) < fmax(best_max_err, -best_min_err))) {best_a = a;best_b = b0 + db;maximum_okays = okaysa;minimum_error = error;best_min_err = min_err;best_max_err = max_err;}okaysb = evaluate(a, b0 - db, d);error = max_err - min_err;if (okaysb > maximum_okays ||(okaysb == maximum_okays && error < minimum_error) ||(okaysb == maximum_okays && error == minimum_error && fmax(max_err, -min_err) < fmax(best_max_err, -best_min_err))) {best_a = a;best_b = b0 - db;maximum_okays = okaysb;minimum_error = error;best_min_err = min_err;best_max_err = max_err;}} while (okaysa + 128 >= maximum_okays || okaysb + 128 >= maximum_okays);}printf("ROUND_BITS(x * %" PRIi64 " - %" PRIi64 ", %d):\n", best_a, best_b, d);printf(" %d out of 4096 values are exactly correct (%.3f%%)\n", maximum_okays, 100.0*maximum_okays/4096.0);printf(" Error range is %+.6f .. %+.6f (%.6f)\n", best_min_err, best_max_err, minimum_error);return 0;}

author: Siwastaja
author_rank: Super Contributor
timestamp: April 06, 2023, 11:08:22 am
content: In a nutshell, convert divisions to a combination of multiplication and division by 2^n, because dividing by 2^n is blazing fast compared to dividing by anything else. Multiplications are cheap compared to divisions, especially if you have a hardware multiplier.Furthermore, division by 2^(8*n) tends to be even better, especially on simple or 8-bit CPUs. For example, a 32-bit value is stored in 4 registers on a 8-bit machine. While say /16384 is still much faster than say /10000, it requires some bitshifting across registers; not bad, but dividing by 65536 is just accessing two of the 4 registers and discarding the rest, even better.Whether you write the division by 2^n as bitshift or division is matter of taste. Unless the compiler is completely unusable crap, the result is the same.With the usual <16 bit ADCs with <13 bits of ENOB, 32-bit variables give quite a lot of headroom for arithmetics. You can easily getbetternumerical accuracy than withfloats, with significant performance advantage, too. Even if you have a FPU available, register-width fixed point integer arithmetics is still sometimes preferable, e.g. in interrupt handlers where floating point operations increase ISR latency because of FPU register stacking.With 8-bit CPUs, 24-bit types would be appealing but not all compilers implement them.