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.
Try It Yourself 20 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.
Hint
Add a comfort-zone label after the % value:
const char* comfortFor(float h) {
if (h < 40) return "dry ";
if (h < 60) return "OK ";
if (h < 80) return "humid";
return "tropc";
}
// in showHum:
sprintf(buf, "%3d%% %s", (int)h, comfortFor(h));Note the trailing spaces in each label string to keep the field width constant — same fixed-width trick applied to strings.
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.
Hint
unsigned long s = millis() / 1000;
unsigned int m = s / 60;
unsigned int ss = s % 60;
char buf[6];
sprintf(buf, "%02u:%02u", m, ss);
lcd.setCursor(11, 1);
lcd.print(buf);%02u = unsigned int, 2 columns wide, zero-padded. So 02:07 always shows leading zeros instead of 2: 7.
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.
Hint
Two separate lastX timestamps:
if (millis() - lastDht >= 2000) { lastDht = millis(); showTemp(dht.readTemperature()); }
if (millis() - lastDist >= 200) { lastDist = millis(); showDist(readDistanceCm()); }Two timers, one loop, no delay() — the Cluster F shape. Each sensor refreshes at its own rate without one blocking the other. We'll formalise this exact pattern in L02-36 ("Doing Two Things at Once").
Mini-Challenge · 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.
Homework 5 min
Build a layout library. In your project notebook, design a one-page reference of LCD layouts for the six sensors you've used in L2 so far:
- TMP36 / thermistor temperature.
- DHT11 humidity.
- LDR light percent.
- Soil moisture probe.
- HC-SR04 distance.
- Pot reading (0–1023 or scaled %).
For each sensor draw the proposed 16×2 layout (label, value field, unit) and write the sprintf/dtostrf format string you'd use. Pick a custom character (if any) you'd want to add.
Then pick one of the six and actually code it as hw-l02-31.ino. It should:
- Use a static label + fixed-width value layout.
- Use
sprintffor the value formatting. - Refresh at the sensor's natural rate.
- Handle invalid readings gracefully.
Bring back next class:
- Your six-sensor layout reference page.
- Your
hw-l02-31.inosketch (one sensor coded up). - A photo of the LCD showing the working sketch.