Search code examples
c++arduinoc++14microcontrollerarduino-c++

Iteration Vs. Recursion for Printing Integer Numbers to a Character LCD/OLED Display


Question

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).


Recursion

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
    }
}

Iteration

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
}

Update

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);
}

Solution

  • 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);
    }