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()?
3 Answers 3
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
anddouble
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 splitfloat
anddouble
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() {
}
-
how much flash space does this save compared to printf? in my test none.2020年06月08日 11:48:28 +00:00Commented Jun 8, 2020 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– Majenko2020年06月08日 11:49:45 +00:00Commented Jun 8, 2020 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/…2020年06月08日 12:00:02 +00:00Commented Jun 8, 2020 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– Majenko2020年06月08日 12:02:47 +00:00Commented Jun 8, 2020 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).Edgar Bonet– Edgar Bonet2020年06月08日 14:02:02 +00:00Commented Jun 8, 2020 at 14:02
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"
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.
-
Re "smaller program": smaller compiled code?Edgar Bonet– Edgar Bonet2020年06月08日 12:10:52 +00:00Commented Jun 8, 2020 at 12:10
-
1@EdgarBonet, yes, smaller compiled code2020年06月08日 12:15:06 +00:00Commented Jun 8, 2020 at 12:15
Serial.printf("...", ..., ..., ...)
function which is ideal for this. The formatting is handled by this code, which itself was lifted directly from RetroBSD.