I am looking for some input on how to optimize printing the digits of an integer number, say uint32_t num = 1234567890;
, to a character display with an Arduino UNO. The main metrics to consider are memory usage and complied size. The display is so slow that no improvement in speed would be meaningful and minimum code length, while nice, isn't a requirement.
Currently, I am extracting the least significant digit using num%10
and then removing this digit by num/10
and so on until all the digits of num
are extracted. Using recursion I can reverse the order of the printing so very few operations are needed (as explicit lines of code) to print the digits in proper order. Using for
loops I need to find the number of characters used to write the number, then store them before being able to print them in the correct order, requiring an array and 3 for
loops.
According to the Arduino IDE, when printing an assortment of signed and unsigned integers, recursion uses 2010/33 bytes of storage/memory, while iteration uses 2200/33 bytes verses 2474/52 bytes when using the Adafruit_CharacterOLED
library that extends class Print
.
Is there a way to implement this better than the functions I've written using recursion and iteration below? If not, which would you prefer and why? I feel like there might be a better way to do this with less resources--but maybe I'm Don Quixote fighting windmills and the code is already good enough.
Background
I'm working with a NHD-0420DZW character OLED display and have used the Newhaven datasheet and LiquidCrystal library as a guide to write my own library and the display is working great. However, to minimize code bloat, I chose to not make my display library a subclass of Print
, which is a part of the Arduino core libraries. In doing this, significant savings in storage space (~400 bytes) and memory (~19 bytes) have already been realized (the ATmega328P has 32k storage with 2k RAM, so resources are scarce).
If I use recursion, the print method is rather elegant. The number is divided by 10 until the base case of zero is achieved. Then the least significant digit of the smallest number is printed (MSD of num), and the LSD of the next smallest number (second MSD of num) and so on, causing the final print order to be reversed. This corrects for the reversed order of digit extraction using %10
and /10
operations.
// print integer type literals to display (base-10 representation)
void NewhavenDZW::print(int8_t num) {print(static_cast<int32_t>(num));}
void NewhavenDZW::print(uint8_t num) {print(static_cast<uint32_t>(num));}
void NewhavenDZW::print(int16_t num) {print(static_cast<int32_t>(num));}
void NewhavenDZW::print(uint16_t num) {print(static_cast<uint32_t>(num));}
void NewhavenDZW::print(int32_t num) {
if(num < 0) { // print negative sign if present
send('-', HIGH); // and make num positive
print(static_cast<uint32_t>(-num));
} else
print(static_cast<uint32_t>(num));
}
void NewhavenDZW::print(uint32_t num) {
if(num < 10) { // print single digit numbers directly
send(num + '0', HIGH);
return;
} else // use recursion to print nums with more
recursivePrint(num); // than two digits in the correct order
}
// recursive method for printing a number "backwards"
// used to correct the reversed order of digit extraction
void NewhavenDZW::recursivePrint(uint32_t num) {
if(num) { // true if num>0, false if num==0
recursivePrint(num/10); // maximum of 11 recursive steps
send(num%10 + '0', HIGH); // for a 10 digit number
}
}
Since the digit extraction method starts at the LSD, rather than the MSD, the extracted digits cannot be printed directly unless I move the cursor and tell the display to print right-to-left. So I have to store the digits as I extract them before I can write them to the display in the correct order.
void NewhavenDZW::print(uint32_t num) {
if(num < 10) {
send(num + '0', HIGH);
return;
}
uint8_t length = 0;
for(uint32_t i=num; i>0; i/=10) // determine number of characters
++length; // needed to represent number
char text[length];
for(uint8_t i=length; num>0; num/=10, --i)
text[i-1] = num%10 + '0'; // map each numerical digit to
for(uint8_t i=0; i<length; i++) // its char value and fix ordering
send(text[i], HIGH); // before printing result
}
Ultimately, recursion takes the least storage space, but is likely to use the most memory.
After reviewing the code kindly provided by Igor G and darune, as well as looking at the number of instructions listed on godbolt (as discussed by darune and old_timer) I believe that Igor G's solution is the best overall. It compiles to 2076 bytes vs. 2096 bytes for darune's function (using an if
statement to stop leading zeros and be able to print 0
) during testing. It also requires less instructions (88) than darune's (273) when the necessary if
statement is tacked on.
Using Pointer Variable
void NewhavenDZW::print(uint32_t num) {
char buffer[10];
char* p = buffer;
do {
*p++ = num%10 + '0';
num /= 10;
} while (num);
while (p != buffer)
send(*--p, HIGH);
}
Using Index Variable
This is what my original for
loop was trying to do, but in a naive way. There is really no point in trying to minimize the size of the buffer array as Igor G has point out.
void NewhavenDZW::print(uint32_t num) {
char text[10]; // signed/unsigned 32-bit ints are <= 10 digits
uint8_t i = sizeof(text) - 1; // set index to end of char array
do {
text[i--] = num%10 + '0'; // store each numerical digit as
num /= 10; // its associated char value
} while (num);
while (i < sizeof(text))
send(text[i++], HIGH); // print num in the correct order
}
The Alternative
Here's darune's function with the added if statement, for those who don't want to sift through the comments. The condition pow10 == 100
is the same as pow10 == 1
, but saves two iterations of the loop to print zero while having the same compile size.
void NewhavenDZW::print(uint32_t num) {
for (uint32_t pow10 = 1000000000; pow10 != 0; pow10 /= 10)
if (num >= pow10 || (num == 0 && pow10 == 100))
send((num/pow10)%10 + '0', HIGH);
}
Try this one. My avr-gcc-5.4.0 + readelf
tells that the function body is only 138 bytes.
void Send(uint8_t);
void OptimizedPrintf(uint32_t val)
{
uint8_t buffer[sizeof(val) * CHAR_BIT / 3 + 1];
uint8_t* p = buffer;
do
{
*p++ = (val % 10) + '0';
val /= 10;
} while (val);
while (p != buffer)
Send(*--p);
}