Displaying sensor readings, building log messages, or formatting data for serial output — Arduino string formatting with sprintf is one of those skills that separates beginner sketches from professional code. In this tutorial, you’ll learn how to use sprintf(), dtostrf(), and snprintf() on Arduino, how to avoid the pitfalls of float formatting on AVR processors, and practical techniques for padding, alignment, and building structured output strings that look clean on LCD displays, serial monitors, and OLED screens.
Table of Contents
- Why Use sprintf Instead of Multiple print() Calls
- sprintf Basics and Format Specifiers
- The Float Problem on AVR Arduino
- dtostrf: Arduino’s Float-to-String Solution
- snprintf: Buffer-Safe sprintf
- Padding and Alignment Techniques
- LCD and OLED Display Formatting
- When to Use the String Class (and When Not To)
- FAQ
Why Use sprintf Instead of Multiple print() Calls
The most common way beginners format output in Arduino is chaining multiple Serial.print() calls:
// Beginner approach — works but verbose
Serial.print("Temperature: ");
Serial.print(temperature);
Serial.print("°C Humidity: ");
Serial.print(humidity);
Serial.println("%");
This works, but has several problems:
- You can’t reuse the formatted string (e.g., for displaying on both LCD and serial)
- No control over decimal places, field width, or padding
- Can’t store the result in a variable for further processing
- More function call overhead than a single formatted write
Using sprintf() lets you build the complete string first, then use it anywhere:
char buf[64];
sprintf(buf, "Temperature: %dC Humidity: %d%%", temperature, humidity);
Serial.println(buf);
lcd.print(buf); // Same string, different destination
sprintf Basics and Format Specifiers
sprintf(buffer, format, args...) works identically to C’s standard printf() but writes to a char array instead of stdout. Here are the format specifiers most useful in Arduino work:
| Specifier | Type | Example Output |
|---|---|---|
%d |
int | 42 |
%u |
unsigned int | 65535 |
%ld |
long | 1234567890 |
%s |
char* | Hello |
%c |
char | A |
%x |
hex (lower) | ff3c |
%X |
hex (upper) | FF3C |
%05d |
zero-padded int | 00042 |
%-10s |
left-aligned string | Hello |
%% |
literal % | % |
Notice that %f (float) is intentionally missing — that’s because on AVR-based Arduinos (Uno, Nano, Mega), sprintf does NOT support floating point by default. This is the single most confusing aspect of Arduino string formatting.
The Float Problem on AVR Arduino
If you try this on an Arduino Uno:
float temperature = 24.67;
char buf[32];
sprintf(buf, "Temp: %f", temperature);
Serial.println(buf);
// Output: "Temp: " (empty float!) or garbage
You won’t get the number you expect. This happens because the AVR-libc implementation of sprintf deliberately excludes float support to save flash memory (the full printf with floats adds ~2 KB of code). The %f specifier is silently ignored or outputs unexpected characters.
You have four solutions:
Option 1 — Integer arithmetic: Multiply the float by a power of 10 and use integer formatting with manual decimal point.
float temp = 24.67;
int tempInt = (int)temp;
int tempFrac = abs((int)(temp * 100) % 100);
sprintf(buf, "Temp: %d.%02d C", tempInt, tempFrac);
// Output: "Temp: 24.67 C"
Option 2 — dtostrf(): Arduino’s built-in float-to-string function (covered in the next section).
Option 3 — Enable float printf: Add this to your sketch to link the full printf with float support (costs ~1.5 KB of flash):
// Add to sketch before setup():
extern void __floats_in_avr_libc_printf_enable();
// Or via linker flag in platform.local.txt:
// build.extra_flags = -Wl,-u,vfscanf -lscanf_flt -Wl,-u,vfprintf -lprintf_flt
Option 4 — ARM Arduino (Nano 33 IoT, Due, RP2040): On ARM-based boards, float printf works natively without any workarounds.
dtostrf: Arduino’s Float-to-String Solution
dtostrf() is an AVR-specific function that converts a float (double) to a string. It’s the cleanest solution for float formatting on Uno/Nano/Mega.
Signature:
char* dtostrf(double val, signed char width, unsigned char prec, char* s);
// val — the float to convert
// width — minimum total field width (negative = left-aligned)
// prec — number of decimal places
// s — output char buffer
// returns: pointer to s
Examples:
char buf[10];
float temperature = 24.678;
dtostrf(temperature, 6, 2, buf);
// buf = " 24.68" (6 chars wide, 2 decimal places, right-aligned)
dtostrf(temperature, -8, 1, buf);
// buf = "24.7 " (8 chars wide, 1 decimal place, left-aligned)
dtostrf(temperature, 4, 0, buf);
// buf = " 25" (0 decimal places, rounded)
// Combine with sprintf:
char floatStr[8];
char output[32];
dtostrf(temperature, 5, 2, floatStr);
sprintf(output, "Temp:%s C Hum:%d%%", floatStr, humidity);
Serial.println(output);
Buffer size warning: Always allocate at least width + 1 bytes for the dtostrf output buffer (the +1 is for the null terminator). A common bug is allocating exactly width bytes and getting a buffer overflow. For safety, use char buf[16] for most temperature/sensor values.
Negative numbers: dtostrf handles negative values correctly — the minus sign counts towards the total width, so dtostrf(-24.5, 6, 1, buf) gives " -24.5" (6 chars, right-aligned).
snprintf: Buffer-Safe sprintf
Regular sprintf() has a critical vulnerability: if your format string produces more characters than the buffer can hold, it writes past the end of the buffer — a classic buffer overflow. On Arduino, this can corrupt the stack, cause seemingly random crashes, or corrupt other variables.
snprintf(buffer, maxLen, format, args...) adds a maximum length parameter:
char buf[32];
int written = snprintf(buf, sizeof(buf), "Sensor %d: %.2f", sensorId, value);
if (written >= sizeof(buf)) {
// Output was truncated! Handle this case.
Serial.println("Warning: output truncated");
}
snprintf never writes more than maxLen - 1 characters (always null-terminates). It returns the number of characters that WOULD have been written — if this is >= maxLen, your string was truncated. Always use sizeof(buf) for the length parameter rather than a hardcoded number, so the check stays correct if you ever change the buffer size.
On ARM-based Arduino boards, snprintf supports %f natively. On AVR, combine it with dtostrf as shown in the previous section.
Padding and Alignment Techniques
Clean, aligned output is crucial when displaying tables of values on a serial monitor or LCD. Here are the key techniques:
Right-aligned integers with leading spaces:
sprintf(buf, "%5d", 42); // " 42"
sprintf(buf, "%5d", 1234); // " 1234"
sprintf(buf, "%5d", 99999);// "99999"
Zero-padded integers:
sprintf(buf, "%05d", 42); // "00042" — useful for timestamps
sprintf(buf, "%04d", 7); // "0007"
Left-aligned strings:
sprintf(buf, "%-10s|", "Hello"); // "Hello |" — padded to 10 chars
Fixed-width table output:
// Print a 3-column sensor table
void printSensorRow(int id, float temp, int humidity) {
char tempStr[8];
dtostrf(temp, 6, 2, tempStr);
char row[40];
snprintf(row, sizeof(row), "Sensor %02d | %s C | %3d%%", id, tempStr, humidity);
Serial.println(row);
}
// Output:
// Sensor 01 | 24.67 C | 68%
// Sensor 12 | 100.00 C | 100%
Time formatting (HH:MM:SS):
// Format millis() as HH:MM:SS
void printTime() {
unsigned long t = millis() / 1000;
int h = t / 3600;
int m = (t % 3600) / 60;
int s = t % 60;
char buf[10];
sprintf(buf, "%02d:%02d:%02d", h, m, s);
Serial.println(buf);
}
LCD and OLED Display Formatting
For 16×2 LCD displays, proper formatting is essential — you have exactly 16 characters per row and any overflow just wraps (or is cut off). Here are patterns for common LCD display needs:
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);
void displaySensorData(float temp, int humidity) {
char line1[17]; // 16 chars + null terminator
char line2[17];
char tempStr[7];
dtostrf(temp, 5, 1, tempStr);
snprintf(line1, sizeof(line1), "Temp: %s C", tempStr);
snprintf(line2, sizeof(line2), "Humidity: %3d%%", humidity);
lcd.clear();
lcd.setCursor(0, 0); lcd.print(line1);
lcd.setCursor(0, 1); lcd.print(line2);
}
// Output:
// Line 1: "Temp: 24.5 C "
// Line 2: "Humidity: 68% "
Clearing to end of line: When updating LCD in place (without lcd.clear() to avoid flicker), pad shorter strings to fill the full 16 characters:
// Pad string to fill LCD line (prevents leftover characters)
snprintf(line1, sizeof(line1), "%-16s", myString); // Left-align, pad to 16
When to Use the String Class (and When Not To)
Arduino provides a high-level String class with operator overloading, which is tempting for formatting:
String msg = "Temp: " + String(temperature, 2) + "C";
Serial.println(msg);
This looks clean, but has serious downsides on AVR Arduino:
- Heap fragmentation: String creates and destroys dynamic memory allocations. In long-running sketches, this fragments the heap, eventually causing random crashes.
- Memory overhead: Each String object has 16+ bytes of overhead on top of the string data.
- Unpredictable timing: Memory allocation can take variable time, affecting timing-sensitive code.
Rule of thumb: Use char arrays + sprintf/dtostrf for performance-critical or long-running sketches. Use the String class only in simple sketches that run for a short time or on ARM boards with abundant RAM (like Nano 33 IoT, which has 32 KB RAM vs Uno’s 2 KB).
A safe alternative that avoids heap fragmentation while keeping some String convenience:
// Build strings in static buffers — no heap allocation
static char tempBuf[8];
static char msgBuf[32];
dtostrf(temperature, 6, 2, tempBuf);
snprintf(msgBuf, sizeof(msgBuf), "T:%sC H:%d%%", tempBuf, humidity);
FAQ
Why does sprintf print ‘?’ or nothing for my float values?
This is the AVR float printf limitation. The default AVR-libc sprintf does not support %f, %e, or %g format specifiers. The float argument is ignored and the specifier outputs nothing or a ‘?’ depending on the libc version. Use dtostrf() to convert your float to a string first, then use %s to include it in sprintf.
How large should my sprintf buffer be?
Add up the maximum possible length of all formatted elements plus literal characters in your format string. Add 1 for the null terminator. For safety, add another 10-20% buffer. For example: "Sensor %02d: %s C, %3d%%" with max int 99, temp string 7 chars, humidity max 100 → 2 + 7 + 5 literal chars + 3 + 1 null = ~20 chars → use 32 to be safe. The snprintf return value tells you the actual required length.
Does sprintf work the same on all Arduino boards?
No. AVR-based boards (Uno, Nano, Mega, Leonardo) use AVR-libc’s sprintf which lacks float support. ARM-based boards (Nano 33 IoT, Due, Nano RP2040 Connect, Arduino Zero) use newlib-nano’s sprintf which fully supports %f and other float specifiers. The dtostrf() function is AVR-specific and may not be available on all ARM boards.
Is there a way to format directly to the Serial port without a buffer?
Yes — use Serial.printf() on boards that support it (ESP32, Nano 33 IoT, RP2040-based boards). This works like printf() but writes directly to the serial port without a manual buffer. On standard AVR Arduino (Uno/Nano), this method doesn’t exist and you must use a char buffer with sprintf.
Why does my sprintf output have extra spaces or wrong alignment?
Check that your format specifier matches the data type exactly. On Arduino, %d is for int (16-bit on AVR). If you pass a long, use %ld. If you pass an unsigned int, use %u. Type mismatches in printf family functions cause undefined behaviour that often manifests as weird spacing, garbage characters, or values that are off by powers of 2.
Write better Arduino code today. Mastering sprintf, dtostrf, and snprintf will make your sensor readouts cleaner, your LCD displays more professional, and your debugging much faster. Browse our full range of Arduino boards and display modules at Zbotic to build your next formatted-output project.
Add comment