Although it’s not advertised, the Arduino / AVR compiler mostly does the right thing with long long
= uint64_t
variables: 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 struct
giving access to the two 32 bit halves for direct manipulation, then overlay / union
them 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, printf
doesn’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 union
notation didn’t get much love at first.
The Arduino source code as a GitHub Gist:
Long 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 |
// 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 () { | |
} | |