Learning Goals 5 min
- Format numbers neatly with fixed widths and a controlled number of decimal places — so a temperature display always reads
27.3and never27.31239or3. - Learn the
sprintf()idiom — the C standard library's one-line solution to "print this number into a string with this format". - Put a live sensor reading (DHT11 or thermistor or LDR — your pick) onto the LCD with a fixed layout that never ghosts and never wobbles in column position.
Warm-Up 10 min
Two days ago we manually padded numbers with if (v < 10) print(" "); chains. Today we replace those chains with sprintf() — a one-line, well-tested function from the C standard library that builds a formatted string in a buffer. After today you'll wonder how you ever lived without it.
Why floats are awkward on Arduino
Hidden gotcha: Arduino's sprintf on AVR chips (UNO, Nano) does not support %f by default. That's a flash-size optimisation. You'll see ? printed instead of your float. The standard workaround: convert the float to a string with dtostrf first, then format with %s.
New Concept · sprintf and dtostrf 25 min
Basic sprintf — integers
char buf[17];
int count = 42;
sprintf(buf, "Count: %4d", count); // "Count: 42"
lcd.print(buf);Three pieces:
buf[17]— a fixed-size character buffer. 16 chars for the LCD row + 1 for the null terminator that ends every C string."Count: %4d"— a format string. Most characters print literally;%4dmeans "an integer (d), right-aligned in a 4-column field".count— the value to substitute for%4d.
The most useful format specifiers
| Specifier | Meaning | Example: value 7 |
|---|---|---|
%d | Integer, no padding | 7 |
%4d | Integer, right-aligned in 4 cols | 7 |
%04d | Integer, zero-padded to 4 cols | 0007 |
%-4d | Integer, left-aligned in 4 cols | 7 |
%s | String | (whatever the string is) |
%c | Single character | (whatever) |
Floats with dtostrf
The standard pattern is two lines: convert the float to a string with dtostrf, then drop the string into the format with %s:
char tempBuf[8];
char buf[17];
float tC = 27.34;
dtostrf(tC, 5, 1, tempBuf); // "27.3" right-aligned in 5 cols
sprintf(buf, "Temp: %s C", tempBuf); // "Temp: 27.3 C"
lcd.print(buf);dtostrf(value, width, precision, buffer):
value— the float.width— total characters including the decimal point and sign. 5 means "27.3" (4 chars) gets one leading space.precision— digits after the decimal point.buffer— char array to write into. Must be large enough.
One-line helper for temperature
Once you've done the dance a few times, wrap it in a helper:
void lcdTemp(int col, int row, float t) {
char tb[8], buf[10];
dtostrf(t, 5, 1, tb);
sprintf(buf, "%s\xDF" "C", tb); // "%s°C" with a literal degree byte
lcd.setCursor(col, row);
lcd.print(buf);
}Now any sensor sketch is one line: lcdTemp(0, 0, 27.3);. Reusable just like readDistanceCm().
Common widths to remember
- Temperature (°C):
dtostrf(t, 5, 1, ...)— " 27.3", "-10.5", "105.7". Covers -99 to 999. - Humidity (% RH):
%3d— " 67", "100". - Distance (cm):
dtostrf(cm, 5, 1, ...)— " 47.3", "399.9". - Counter (seconds):
%5lu— 5 cols, long unsigned, " 123" or "65535".
Worked Example · DHT11 readout on LCD 25 min
Step 1 — wiring
LCD (bare or I²C) plus DHT11 on D2 (or A1 — anywhere convenient). +5 V and GND shared.
Step 2 — the sketch
Save as lcd-dht11.ino (bare-LCD version shown):
#include <LiquidCrystal.h>
#include <DHT.h>
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
DHT dht(8, DHT11);
unsigned long lastRead = 0;
byte degChar[8] = {
0b01100, 0b10010, 0b10010, 0b01100,
0b00000, 0b00000, 0b00000, 0b00000
};
void setup() {
lcd.begin(16, 2);
lcd.createChar(0, degChar);
// Static labels
lcd.setCursor(0, 0);
lcd.print("Temp:");
lcd.setCursor(0, 1);
lcd.print("Hum :");
dht.begin();
}
void showTemp(float t) {
char tb[8], buf[12];
if (isnan(t)) {
sprintf(buf, " --.-");
} else {
dtostrf(t, 5, 1, tb);
sprintf(buf, "%s", tb);
}
lcd.setCursor(8, 0);
lcd.print(buf);
lcd.write(byte(0)); // custom degree
lcd.print("C");
}
void showHum(float h) {
char buf[8];
if (isnan(h)) {
sprintf(buf, " --");
} else {
sprintf(buf, "%3d%%", (int)h);
}
lcd.setCursor(8, 1);
lcd.print(buf);
}
void loop() {
if (millis() - lastRead < 2000) return;
lastRead = millis();
showTemp(dht.readTemperature());
showHum(dht.readHumidity());
}Step 3 — upload, observe the layout
The LCD should show something like:
Temp: 27.3°C Hum : 67%
Each row has a left-aligned label and a right-aligned value with units. Numbers update every 2 seconds. Watch the readings cross digit boundaries (e.g. 9.9 → 10.0, or 99% → 100%) and confirm there's no ghosting — the leading space in the width-5 float field overwrites cleanly.
Step 4 — break it, then fix it
Temporarily change dtostrf(t, 5, 1, tb) to dtostrf(t, 0, 1, tb) — no width, no padding. Re-upload and watch the digits move horizontally as the temperature crosses thresholds (e.g. 9.9 → 10.0 shifts the ".0" to the right). The %symbol on the next column may overlap. Restoring width = 5 fixes it. Width matters.
Step 5 — handle the NaN case
Disconnect the DHT11's data wire briefly. The library returns NaN; your showTemp prints --.- instead of garbage. Reconnect — the real reading flows back in. Good fault tolerance.
Basic 7 min
Goal: Add a third row of static text (impossible on 16×2, possible on 20×4) — or, on 16×2, use the second half of row 1 for a status label: Hum: 67% humid or Hum: 67% dry.
Challenge 1 7 min
Goal: Use sprintf + %lu to print the seconds-since-boot on the bottom row as mm:ss. Now your sketch's "up time" is a clean clock.
Challenge 2 6 min
Goal: Combine the DHT11 with the HC-SR04 from L02-23. Show temperature on row 0, distance on row 1. Both update at their natural rates (DHT every 2 s, distance every 200 ms) without interfering.
Challenge 3 · Polished sensor card 15 min
Build a single LCD "card" for one of your earlier projects, polished to product quality:
- Personal Thermometer card (L02-12 redux on LCD): top row temperature with custom degree symbol, bottom row status string (cool/normal/warm/FEVER).
- Distance gauge (L02-23 redux): top row distance number with units, bottom row zone label and an arrow icon indicating whether the trend is "getting closer" or "moving away" (compute from the last two readings).
- Weather snapshot (L02-20 redux): two combined rows with temp, humidity, and one of the headline labels from L02-20.
It's polished when:
- All values use
sprintf+dtostrffor fixed-width display — no manual padding loops. - The screen never ghosts and never flickers, even when values cross digit boundaries.
- At least one custom character (degree symbol, arrow, comfort icon) appears.
- NaN / out-of-range states show a clear sentinel like
--.-instead of garbage. - A photo of your card could pass for a real consumer device.
Recap 5 min
Today you traded clunky if (v < 10) print(" "); chains for the C-standard pair sprintf + dtostrf. The pattern is the same — build a fixed-width string in a buffer, drop it into the LCD with one print(buf) — but now the formatting is declarative (%3d, %5.1f, %02u) instead of imperative. Floats need the dtostrf + %s two-step on AVR Arduinos because %f isn't supported by default. With a small helper function per value type (lcdTemp, lcdHum, lcdDist) any sensor sketch can drive the LCD in one line per value. Tomorrow we ship Cluster E with the Digital Thermometer With LCD — the cluster's capstone project.
sprintf(buf, fmt, ...)- The C-standard function that writes a formatted string into a buffer using format specifiers. The same
printflanguage used by every C/C++ program ever. - Format string
- A template like
"Temp: %4d C"where literal characters print as-is and%-prefixed specifiers get replaced with values. - Format specifiers
%dint,%uunsigned,%luunsigned long,%sstring,%cchar,%xhex. Prefix with width and padding:%4d,%04d,%-4d.dtostrf(value, width, precision, buf)- Float-to-string on AVR. Width = total chars including dot, precision = digits after dot, buf = receiver array. Workaround for
%fmissing from AVRsprintf. - Character buffer
- A fixed-size
char[N]array that holds the assembled output. Always size large enough for the longest possible output plus 1 for the null terminator. - Null terminator (
'\0') - The zero byte that marks the end of a C string. Every string-producing function writes one automatically; you must reserve room for it.
- Fixed-width display
- Always rendering a value with the same number of columns, padding shorter values with spaces or zeros. Eliminates ghosting and keeps layouts steady.
- String vs char[]
- Arduino's
Stringclass is heap-allocated and convenient but unpredictable on small RAM.char[N]withsprintfis the embedded-style choice — predictable, fast, no fragmentation.
Extra Mission 5 min
Part 1 — Design a sensor card for a place
Pick a place where one or two readings really matter. Ideas: a greenhouse, a pet's tank, a bedroom, a garden shed. On paper, design a 16×2 "card" that shows those readings at a glance.
Your design must include:
- The sensors you would use, from those you have used in Level 2.
- The full 16×2 layout: labels, value fields and units.
- The
sprintfformat for each value, such as"%3d%%". - The sentinel the card shows for an invalid reading, such as
--.-.
Part 2 — Make it
Code the card for at least one sensor in your design. Use sprintf, plus dtostrf for decimals, so every field keeps its width. Refresh at the sensor's natural rate — every 2 s for the DHT11. Unplug the sensor's data wire for a moment to check the sentinel appears.
Bring back next class: your design, your hw-l02-31.ino sketch, and two photos: a live reading and the sentinel.