-
Arduino vs. Significant Figures: Preliminary 64-bit Fixed Point Exercise
Although it’s not advertised, the Arduino / AVR compiler mostly does the right thing with
long long=uint64_tvariables: add & subtract work fine, but multiplication & division discard anything that doesn’t fit into 64 bits. Fitting a 32 bit integer and a 32 bit fraction into such a thing should eliminate (most) problems with significant figures.The general idea is to set up a
structgiving access to the two 32 bit halves for direct manipulation, then overlay /unionthem with a single 64 bit integer for arithmetic purposes:struct ll_s { uint32_t low; uint32_t high; }; union ll_u { uint64_t ll_64; struct ll_s ll_32; };Of course, the integer part still falls one bit shy of holding 2³². At the cost of one bit’s worth of resolution, you can still compute 2³² / 125×10⁶ by pre-dividing each quantity by 2:
2^63 = [80000000 00000000] 2^63 / 125/2 M = [00000022 5c17d04d]
The low-order digit should be 0xe, not 0xd, but I think that’s survivable.
Unfortunately,
printfdoesn’t handle 64 bit quantities, necessitating some awkward conversion routines. “Printing” to a string seems the least awful method, as I’ll eventually squirt the strings to a display, not send them to the serial port:void PrintFractionLL(char *pBuffer,uint64_t *pLL) { uint64_t Fraction; Fraction = (uint32_t)*pLL; // copy 32 fraction bits, high order = 0 Fraction *= ONEGIG; // times 10^9 for conversion Fraction >>= 32; // align integer part in low long sprintf(pBuffer,"%09lu",(uint32_t)Fraction); // convert low long to decimal } void PrintIntegerLL(char *pBuffer,uint64_t *pLL) { sprintf(pBuffer,"%lu",*((uint32_t *)pLL+1)); } void PrintDecimalLL(char *pBuffer,uint64_t *pLL) { PrintIntegerLL(pBuffer,pLL); pBuffer += strlen(pBuffer); // pointer to end of integer part *pBuffer++ = '.'; // drop in the decimal point, tick pointer PrintFractionLL(pBuffer,pLL); }The result seems nearly indistinguishable from the Right Answer:
Integer: 34 Fraction: 359738367 Decimal: 34.359738367
This whole mess has a bunch of rough edges, but it looks promising. The code coalesced while fiddling around, so the
unionnotation didn’t get much love at first.The Arduino source code as a GitHub Gist:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersLong long integer exercise Ed Nisley – KE4ZNU – May 2017 Long long size = 8 bytes .. value = [12345678 9abcdef0] divided result = [01234567 89abcdef] 2^32 = [00000001 00000000] 125M = [00000000 07735940] 2^32 / 125M = [00000000 00000022] Scaled fixed point tests 2^63 = [80000000 00000000] 2^63 / 125/2 M = [00000022 5c17d04d] Integer: 34 Fraction: 359738367 Decimal: 34.359738367 Hz Per Ct: 0.029103830 60000: 60000.000000000 60 kHz as count: 2061584.302070550 0.1 Hz as count: 3.435973836 60000.1 Hz as count: 2061587.738044387 60000.1 Hz from count: 60000.078519806 60000.2 Hz as count: 2061591.174018223 60000.2 Hz from count: 60000.194935128 60000.2 Hz rnd count: 60000.224038958 Union size: 8 TestFreq: [0000ea60 395a9e00] Union ll: [0000ea60 395a9e00] From union: 60000.224038958 Buffer length: 15 Trunc dec: 60000.224 This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters// Long long integer exercise for 60 kHz crystal tester //– Helper routine for printf() int s_putc(char c, FILE *t) { Serial.write(c); } char Buffer[10+1+10]; // string buffer for long long conversions #define ONEGIG 1000000000LL uint64_t CtPerHz; // will be 2^32 / 125 MHz uint64_t HzPerCt; // will be 125 MHz / 2^32 uint64_t TenthHzCt; // 0.1 Hz in counts struct ll_s { uint32_t low; uint32_t high; }; union ll_u { uint64_t ll_64; struct ll_s ll_32; }; //———– // Long long print-to-buffer helpers // Assumes little-Endian layout void PrintHexLL(char *pBuffer,uint64_t *pLL) { sprintf(pBuffer,"%08lx %08lx",*((uint32_t *)pLL+1),(uint32_t)*pLL); } // converts 9 decimal digits void PrintFractionLL(char *pBuffer,uint64_t *pLL) { uint64_t Fraction; Fraction = (uint32_t)*pLL; // copy 32 fraction bits, high order = 0 Fraction *= ONEGIG; // times 10^9 for conversion Fraction >>= 32; // align integer part in low long sprintf(pBuffer,"%09lu",(uint32_t)Fraction); // convert low long to decimal } void PrintIntegerLL(char *pBuffer,uint64_t *pLL) { sprintf(pBuffer,"%lu",*((uint32_t *)pLL+1)); } void PrintDecimalLL(char *pBuffer,uint64_t *pLL) { PrintIntegerLL(pBuffer,pLL); pBuffer += strlen(pBuffer); // pointer to end of integer part *pBuffer++ = '.'; // drop in the decimal point, tick pointer PrintFractionLL(pBuffer,pLL); } //———– void setup () { Serial.begin (115200); fdevopen(&s_putc,0); // set up serial output for printf() Serial.println ("Long long integer exercise"); Serial.println ("Ed Nisley – KE4ZNU – May 2017"); unsigned long long LongLong; uint64_t LongLong2; LongLong = 0x123456789abcdef0LL; printf("Long long size = %d bytes\n",sizeof(LongLong)); printf(" .. value = [%08lx %08lx]\n",(long)(LongLong >> 32),(long)(LongLong & 0x00000000ffffffffLL)); LongLong /= 16; printf(" divided result = [%08lx %08lx]\n",(long)(LongLong >> 32),(long)LongLong); LongLong = 1LL << 32; printf(" 2^32 = [%08lx %08lx]\n",(long)(LongLong >> 32),(long)LongLong); LongLong2 = 125000000LL; printf(" 125M = [%08lx %08lx]\n",(long)(LongLong2 >> 32),(long)LongLong2); LongLong /= LongLong2; printf("2^32 / 125M = [%08lx %08lx]\n",(long)(LongLong >> 32),(long)LongLong); Serial.println("Scaled fixed point tests"); uint64_t TestFreq,TestCount; CtPerHz = 1LL << 63; // start with 2^31 to avoid overflow PrintHexLL(Buffer,&CtPerHz); printf("2^63 = [%s]\n",Buffer); CtPerHz /= 125000000LL / 2; // divided by 2 to to match 2^31 PrintHexLL(Buffer,&CtPerHz); printf("2^63 / 125/2 M = [%s]\n",Buffer); PrintIntegerLL(Buffer,&CtPerHz); printf("Integer: %s\n",Buffer); PrintFractionLL(Buffer,&CtPerHz); printf("Fraction: %s\n",Buffer); PrintDecimalLL(Buffer,&CtPerHz); printf("Decimal: %s\n",Buffer); HzPerCt = 125000000LL; // 125 MHz / 2^32, directly to fraction part PrintDecimalLL(Buffer,&HzPerCt); printf("Hz Per Ct: %s\n",Buffer); TestFreq = 60000LL << 32; PrintDecimalLL(Buffer,&TestFreq); printf("60000: %s\n",Buffer); TestCount = 60000LL * CtPerHz; PrintDecimalLL(Buffer,&TestCount); printf("60 kHz as count: %s\n",Buffer); TenthHzCt = CtPerHz / 10; // 0.1 Hz as counts PrintDecimalLL(Buffer,&TenthHzCt); printf("0.1 Hz as count: %s\n",Buffer); TestCount += TenthHzCt; PrintDecimalLL(Buffer,&TestCount); printf("60000.1 Hz as count: %s\n",Buffer); TestFreq = (TestCount >> 32) * HzPerCt; PrintDecimalLL(Buffer,&TestFreq); printf("60000.1 Hz from count: %s\n",Buffer); TestCount = (60000LL * CtPerHz) + (2 * TenthHzCt); PrintDecimalLL(Buffer,&TestCount); printf("60000.2 Hz as count: %s\n",Buffer); TestFreq = (TestCount >> 32) * HzPerCt; PrintDecimalLL(Buffer,&TestFreq); printf("60000.2 Hz from count: %s\n",Buffer); TestFreq = ((TestCount + (TenthHzCt / 2)) >> 32) * HzPerCt; PrintDecimalLL(Buffer,&TestFreq); printf("60000.2 Hz rnd count: %s\n",Buffer); union ll_u LongLongUnion, TempLLU; printf("Union size: %d\n",sizeof(LongLongUnion)); LongLongUnion.ll_64 = TestFreq; PrintHexLL(Buffer,&TestFreq); printf("TestFreq: [%s]\n",Buffer); PrintHexLL(Buffer,&LongLongUnion.ll_64); printf("Union ll: [%s]\n",Buffer); TempLLU.ll_64 = LongLongUnion.ll_32.low; TempLLU.ll_64 *= ONEGIG; // times 10^9 for conversion TempLLU.ll_64 >>= 32; // align integer part in low long sprintf(Buffer,"%lu.%09lu",LongLongUnion.ll_32.high,TempLLU.ll_32.low); printf("From union: %s\n",Buffer); sprintf(Buffer,"%lu.%09lu",LongLongUnion.ll_32.high,TempLLU.ll_32.low); printf("Buffer length: %d\n",strlen(Buffer)); Buffer[strlen(Buffer) – 9 + 3] = 0; printf(" Trunc dec: %s\n",Buffer); } //———– void loop () { }