The BigNumber library wraps the bc
arbitrary precision calculator into a set of Arduino routines that seem like a reasonable basis for DDS calculations requiring more than the half-dozen digits of a floating point number or the limited range of scaled fixed point numbers tucked into an long int
.
Treating programming as an experimental science produces some Arduino source code and its output as a GitHub Gist:
// BigNumber exercise | |
#include "BigNumber.h" | |
//-- Helper routine for printf() | |
int s_putc(char c, FILE *t) { | |
Serial.write(c); | |
} | |
void setup () | |
{ | |
Serial.begin (115200); | |
fdevopen(&s_putc,0); // set up serial output for printf() | |
Serial.println ("BigNumber exercise"); | |
Serial.println ("Ed Nisley - KE4ZNU - April 2017"); | |
#define WHOLES 10 | |
#define FRACTS 10 | |
printf("Fraction digits: %d\n",FRACTS); | |
BigNumber::begin (FRACTS); | |
char *pBigNumber; | |
#define BUFFLEN (WHOLES + FRACTS) | |
char NumString[BUFFLEN]; | |
BigNumber Tenth = "0.1"; // useful constants | |
BigNumber Half = "0.5"; | |
BigNumber One = 1; | |
BigNumber Two = 2; | |
BigNumber ThirtyTwoBits = Two.pow(32); | |
Serial.println(ThirtyTwoBits); | |
BigNumber Oscillator = "125000000"; | |
Serial.println(Oscillator); | |
BigNumber HertzPerCount; | |
HertzPerCount = Oscillator / ThirtyTwoBits; | |
Serial.println(HertzPerCount); | |
BigNumber CountPerHertz; | |
CountPerHertz = ThirtyTwoBits / Oscillator; | |
Serial.println(CountPerHertz); | |
BigNumber TestFreq = "60000"; | |
Serial.println(TestFreq); | |
BigNumber DeltaPhi; | |
DeltaPhi = TestFreq * CountPerHertz; | |
Serial.println(DeltaPhi); | |
long DeltaPhiL; | |
DeltaPhiL = DeltaPhi; | |
printf("Long: %ld\n",DeltaPhiL); | |
Serial.println("0.1 Hz increment …"); | |
Serial.println(TestFreq + Tenth); | |
DeltaPhi = (TestFreq + Tenth) * CountPerHertz; | |
Serial.println(DeltaPhi); | |
TestFreq = DeltaPhi * HertzPerCount; | |
Serial.println(TestFreq); | |
Serial.println("Rounding DeltaPhi up …"); | |
DeltaPhi += Half; | |
Serial.println(DeltaPhi); | |
TestFreq = DeltaPhi * HertzPerCount; | |
Serial.println(TestFreq); | |
pBigNumber = DeltaPhi.toString(); | |
printf("String: %04x → %s\n",pBigNumber,pBigNumber); | |
free(pBigNumber); | |
DeltaPhiL = DeltaPhi; | |
printf("Unsigned: %ld\n",DeltaPhiL); | |
pBigNumber = "59999.9"; | |
TestFreq = pBigNumber; | |
Serial.println(TestFreq); | |
DeltaPhi = TestFreq * CountPerHertz; | |
Serial.println(DeltaPhi); | |
Serial.println("Rounding DeltaPhi up …"); | |
DeltaPhi = TestFreq * CountPerHertz + Half; | |
Serial.println(DeltaPhi); | |
DeltaPhiL = DeltaPhi; | |
int rc = snprintf(NumString,BUFFLEN,"%ld",DeltaPhiL); | |
if (rc > 0 && rc < BUFFLEN) { | |
printf("String length: %d\n",rc); | |
} | |
else { | |
printf("Whoops: %d for %ld\n",rc,DeltaPhiL); | |
strncpy(NumString,"123456789",sizeof(NumString)); | |
NumString[BUFFLEN-1] = 0; | |
printf(" forced: %s\n",NumString); | |
} | |
printf("Back from string [%s]\n",NumString); | |
DeltaPhi = NumString; | |
Serial.println(DeltaPhi); | |
TestFreq = DeltaPhi * HertzPerCount; | |
Serial.println(TestFreq); | |
} | |
void loop () { | |
} | |
BigNumber exercise | |
Ed Nisley - KE4ZNU - April 2017 | |
Fraction digits: 10 | |
4294967296 | |
125000000 | |
0.0291038304 | |
34.3597383680 | |
60000 | |
2061584.3020800000 | |
Long: 2061584 | |
0.1 Hz increment … | |
60000.1000000000 | |
2061587.7380538368 | |
60000.0998830384 | |
Rounding DeltaPhi up … | |
2061588.2380538368 | |
60000.1144349536 | |
String: 045e → 2061588.2380538368 | |
Unsigned: 2061588 | |
59999.9 | |
2061580.8661061632 | |
Rounding DeltaPhi up … | |
2061581.3661061632 | |
String length: 7 | |
Back from string [2061581] | |
2061581 | |
59999.9037798624 |
All that happened incrementally, as you might expect, with the intent of seeing how it works, rather than actually doing anything.
Some musings, in no particular order:
The library soaks up quite a hunk of program space:
Sketch uses 13304 bytes (43%) of program storage space. Maximum is 30720 bytes.
I think you could cut that back a little by eliminating unused bc
routines, like square root / exponential / modulus.
That test code also blots up quite a bit of RAM:
Global variables use 508 bytes (24%) of dynamic memory, leaving 1540 bytes for local variables. Maximum is 2048 bytes.
All the BigNumber
variables live inside the setup()
function (or whatever it’s called in Arduino-speak), so they count as local variables. They’re four bytes each, excluding the dynamically allocated storage for the actual numbers at roughly a byte per digit. With 10 decimal places for all numbers, plus (maybe) an average of half a dozen integer digits, those ten BigNumbers
soak up 200 = 10 × (4 + 16)
bytes of precious RAM.
You can load a BigNumber
from an int
(not a long
) or a string, then export the results to a long
or a string. Given that controlling a DDS frequency with a knob involves mostly adding and subtracting a specific step size, strings would probably work fine, using snprintf()
to jam the string equivalent of a long
into a BigNumber
as needed.
You must have about ten decimal places to hold enough significant figures in the HertzPerCount
and CountPerHertz
values. The library scale factor evidently forces all the numbers to have at least that many digits, with the decimal point stuck in front of them during string output conversions.
The biggest integers happen in the Oscillator
and ThirtyTwoBits
values, with 9 and 10 digits, respectively.
It looks useful, although I’m uncomfortable with the program space required. I have no way to estimate the program space for a simpleminded DDS controller, other than knowing it’ll be more than I estimate.
While poking around, however, I discovered the Arduino compiler does provide (limited) support for long long int
variables. Given a 64 bit unit for simple arithmetic operations, a simpler implementation of fixed point numbers may be do-able: 32 bits for the integer and fraction should suffice! More on that shortly.
At first I thought that running bignums on a microcontroller was a bit odd, but then I remembered doing arbitrary precision math on my old 6502-based Atari 800. Its libraries worked in BCD, so numbers ate up more RAM, but decimal accuracy was excellent. It seemed to use something like Taylor expansions to compute trig functions, and the CPU ran at 1.79MHz (basically the colorburst frequency divided by 2), so they were slow (I think a sine took several hundred milliseconds: a perceptible amount of time). But it worked. Since I’m both old-fashioned and a math nerd, I actually still use bc frequently.
Use an Arduino as a math co-processor for the main Arduino? Throwing hardware at a software problem, but Arduinos are cheap.
Why bother? ARM and similar 32bit cores are everywhere and prices are similar to Arduino but provide a lot more processing power. Last one that caught my attention was ESP32 with 240Mhz dual core and 12$ price tag :)
If I’m not mistaken, it’s even supported in Arduino software sou you can use some of the libraries
Mostly because the analog stuff runs at +5 V, making 3.3 V microcontrollers a pain.
Besides, all I need is one lousy high-precision division … [mutter]
I saw you liked Arduinos, so I added an Arduino to your Arduino? Just say no to multiprocessing microcontrollers!