1

I have a function that concatenates a print line, however I am having trouble getting some of the strings to format correctly.

Using Serial.print formats the output from keypad.getKey() correctly, however when I try to output the same format using printConcatLine() it does not give me the same output. I've tried casting it as both a char and an int...

void printConcatLine(const char* mask, ...) {
    va_list params;
    va_start(params, mask);

    while(*mask != '\0') {
      if (*mask == 'i') {
          Serial.print(va_arg(params, int));
      } else if(*mask == 'c') {
          Serial.print(va_arg(params, const char *));
      } else if (*mask == 'f' || *mask == 'd') {
          Serial.print(va_arg(params, double));
      } 
      ++mask;    
    }

    va_end(params);
    Serial.println();
}

char btnPressed = keypad.getKey();
  if (btnPressed) {
    Serial.print("BtnPressed: ");
    Serial.println(btnPressed);
    // Output: "BtnPressed: 1"

    printConcatLine("ci", "BtnPressed: ", btnPressed); 
    // Output: "BtnPressed: 49"

    printConcatLine("cc", "BtnPressed: ", btnPressed); 
    // Output: "BtnPressed: "  
  }

I also tried changing:

va_arg(params, const char *)

to

va_arg(params, char)

which also did nothing.

How can I modify the call to va_arg() to properly output the same output as Serial.print()?

2
  • Incidentally, on chipKIT we provide the handy Serial.printf("...", ..., ..., ...) function which is ideal for this. The formatting is handled by this code, which itself was lifted directly from RetroBSD.
    – Majenko
    Jun 8 '20 at 10:37
  • Since you’re using serial to print the string, why the need to concatenate it? Just print it out in chunks.
    – Delta_G
    Jun 8 '20 at 18:39
2

Edgar beat me to it by a few seconds, but my answer is pretty much the same as his, so read that first.

I will however add a few extra notes and pointers:

  • It's better to assign the va_args to an actual variable. That makes it both easier to read and understand what's going on, and more certain for the overloading of the print function to know what is supposed to happen (at least from the reader's perspective) that mere casting.
  • Bytes and chars are promoted to integer when being passed to a variadic function, so you have to treat them as such
  • float and double happen to be the same on the AVR based Arduinos, so your code will be OK there, but the same can't be said for other chips. If you want your code to be portable then you should split float and double into separate handlers since they use different sizes.
  • It can be good to "pass through" unknown characters in the format string so that you can add extra formatting in there. See my example below.

Here's my variant of the same code. Note the colon and space in the format that is used to add more formatting to the output (this could be enhanced by adding a % before any format characters as printf does). I also like to use a separate pointer to iterate over the format string instead so that the original start pointer is still available if I should ever want it.

Also note the difference between c and i: use c to print a letter from a char, and i to print the number in a char instead.

void printConcatLine(const char *mask, ...) {
    va_list params;
    va_start(params, mask);

    char *ptr = (char *)mask;
    while (*ptr != '\0') {
        if (*ptr == 'c') {
            int c = va_arg(params, int);
            Serial.write(c);
        } else if (*ptr == 'i') {
            int i = va_arg(params, int);
            Serial.print(i);
        } else if (*ptr == 's') {
            const char *s = va_arg(params, const char *);
            Serial.print(s);
        } else if (*ptr == 'f' || *ptr == 'd') {
            // Be careful with this. It's not portable. On AVR float
            // and double are the same, but that's not the case on other
            // microcontrollers. It would be better to split them.
            double d = va_arg(params, double);
            Serial.print(d);
        } else {
            Serial.write(*ptr);
        }
        ptr++;
    }

    va_end(params);
    Serial.println();
}

void setup() {
    Serial.begin(115200);
    char btnPressed = 1;
    float flt = 3.141592653;

    if (btnPressed) {
        printConcatLine("c: sisd", 'Q', "BtnPressed: ", btnPressed, " done ", flt);
    }
}

void loop() {
}
7
  • how much flash space does this save compared to printf? in my test none.
    – Juraj
    Jun 8 '20 at 11:48
  • @Juraj A lot. Take a look at the link in my comment above for a "light weight" printf formatter and you'll see how much is involved in it.
    – Majenko
    Jun 8 '20 at 11:49
  • sorry. no. printf produces a little smaller code. I use my StreamLib for printf wrapper for Serial github.com/jandrassy/StreamLib/blob/…
    – Juraj
    Jun 8 '20 at 12:00
  • @Juraj Maybe on the Arduino, but then the Arduno printf lacks float support, which is included in the code here. If you take out the float support you will most likely find this code considerably smaller. Float support is massive.
    – Majenko
    Jun 8 '20 at 12:02
  • I compiled both versions for the Uno with float support and, to my surprise, Juraj's is smaller (text: 4814, data: 68, bss: 166) than yours (text: 6226, data: 62, bss: 166). Jun 8 '20 at 14:02
2

With the format "ci", the argument is interpreted as an integer, and the output is correct: 49 is the ASCII code of '1'.

With the format "cc", the argument is interpreted as a string (pointer to an array of chars), which is incorrect.

One (bad) solution is to use the "cc" format and pass &btnPressed as an argument. The problem with this approach is that the byte in memory right after btnPressed may not be a zero, and the format expects a NUL-terminated string.

A better approach would be to have one format for a char and a different one for a string. I suggest "c" and "s" respectively. But then you should be aware that there are special promotion rules associated with variadic functions. When playing with your code, the compiler reminded me of this one:

warning: ‘char’ is promoted to ‘int’ when passed through ‘...’

So the function should expect an int, and cast it to char in order to call the correct overload of Serial.print():

void printConcatLine(const char* mask, ...) {
    va_list params;
    va_start(params, mask);

    while(*mask != '\0') {
      if (*mask == 'i') {
          Serial.print(va_arg(params, int));
      } else if(*mask == 's') {
          Serial.print(va_arg(params, const char *));
      } else if(*mask == 'c') {
          Serial.print((char) va_arg(params, int));
      } else if (*mask == 'f' || *mask == 'd') {
          Serial.print(va_arg(params, double));
      } 
      ++mask;    
    }

    va_end(params);
    Serial.println();
}

Used like this:

printConcatLine("sc", "BtnPressed: ", btnPressed); 
// Output: "BtnPressed: 1"
2

I recommend to use printf if you don't need float support in printf. This code produces a little smaller compiled code then the one in Majenko's answer and has the full power of the printf except of float.

#include <StreamLib.h>

void setup() {
    Serial.begin(115200);
    char btnPressed = 1;
    float flt = 3.141592653;

    byte b;
    BufferedPrint bp(Serial, &b, 1);

    if (btnPressed) {
        bp.printf("%c: %s %i %s", 'Q', "BtnPressed: ", btnPressed, " done ");
        bp.println(flt, 4);
    }
}

void loop() {
}

With StreamLib you can of course print float with normal single print function for floats, the same as for Serial or network Client, with second parameter for the number of decimal places.

If you want to create a formatted C-string you can use the CStringBuilder of the StreamLib. It too builds the string with print functions and has 'printf' too.

2
  • Re “smaller program”: smaller compiled code? Jun 8 '20 at 12:10
  • 1
    @EdgarBonet, yes, smaller compiled code
    – Juraj
    Jun 8 '20 at 12:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.