C Refresher
Variables & Types
A variable is a named box in memory that holds a value. You must tell C what type of value the box holds before you can use it.
The Basics of Variables
Think of variables like labeled containers:
┌─────────────┐ ┌─────────────────┐ ┌───────────┐
│ age: 25 │ │ temperature:36.6│ │ grade: 'A'│
│ (int) │ │ (float) │ │ (char) │
└─────────────┘ └─────────────────┘ └───────────┘
- Type (
int,float,char) = what kind of thing goes in the box - Name (
age,temperature,grade) = the label on the box - Value (
25,36.6,'A') = what's inside the box right now
1int age = 25; // "age" holds a whole number (integer)2float temperature = 36.6; // "temperature" holds a decimal number3char grade = 'A'; // "grade" holds a single character (use single quotes)Rules for Variable Names
1// ✅ Good names — descriptive, clear2int sensor_count = 0;3float battery_voltage = 3.7;4uint8_t led_brightness = 128;56// ❌ Bad names — unclear, too short7int x = 0; // what is x?8float f = 3.7; // what does f mean?9uint8_t n = 128; // n could be anything1011// ❌ Invalid names — C has strict rules12int 2nd_reading = 0; // can't start with a number13int my-var = 0; // can't use hyphens (use underscores instead)14int float = 0; // can't use C keywords (float is a protected type name)How to Read printf
printf is how you print text and values to the screen. The f stands for "formatted" — you can mix text with variable values using format specifiers.
1int age = 25;2float temp = 36.6f;3char grade = 'A';4char name[] = "ESP32";56// Format specifiers start with % and are replaced by the variable value7printf("I am %d years old\n", age); // %d → integer → "I am 25 years old"8printf("Temperature: %.1f°C\n", temp); // %.1f → float with 1 decimal → "Temperature: 36.6°C"9printf("Grade: %c\n", grade); // %c → single character → "Grade: A"10printf("Device: %s\n", name); // %s → string → "Device: ESP32"Common Format Specifiers
| Specifier | Type | Example Output |
|---|---|---|
%d | int (signed) | 42 |
%u | unsigned int | 255 |
%f | float | 3.140000 |
%.2f | float (2 decimals) | 3.14 |
%c | char (single character) | A |
%s | char[] (string) | Hello |
%x | int (hexadecimal) | ff |
%#x | int (hex with 0x prefix) | 0xff |
Always match the specifier to the type!
Using %d for a float or %f for an int will print garbage. The compiler won't always warn you — this is a common source of confusing bugs.
Normal C vs Embedded C — Type Comparison
C is statically typed. In embedded C, you use fixed-width types from <stdint.h> to guarantee exact bit widths, because the size of standard types like int varies across platforms.
| Normal C Type | Size (typical) | Embedded C Type | Size (guaranteed) | Signed? | Range | When to Use |
|---|---|---|---|---|---|---|
char | 1 byte (8-bit) | int8_t | 8-bit | Yes | -128 to 127 | Small signed values, ASCII |
unsigned char | 1 byte | uint8_t | 8-bit | No | 0 to 255 | Byte data, registers, flags |
short | 2 bytes (16-bit) | int16_t | 16-bit | Yes | -32768 to 32767 | Small signed counters |
unsigned short | 2 bytes | uint16_t | 16-bit | No | 0 to 65535 | ADC readings, PWM duty |
int | 2 or 4 bytes ⚠️ | int32_t | 32-bit | Yes | -2.1B to 2.1B | General-purpose signed |
unsigned int | 2 or 4 bytes ⚠️ | uint32_t | 32-bit | No | 0 to 4.3B | Timestamps, addresses |
long | 4 or 8 bytes ⚠️ | — | — | — | — | Avoid in embedded |
float | 4 bytes | float | 32-bit | — | ±3.4 × 10³⁸ | Sensor values |
double | 8 bytes | double | 64-bit | — | ±1.7 × 10³⁰⁸ | Avoid on ESP32 |
| — | — | size_t | 32-bit on ESP32 | No | 0 to 4.3B | Sizes, array indices |
| — | — | bool | 1 byte | — | true/false | Flags (from <stdbool.h>) |
int size varies across platforms
On a desktop PC, int is typically 32 bits. On an 8-bit AVR (Arduino Uno), int is 16 bits. On ESP32-S3, int is 32 bits. Never assume int is a specific size — always use int32_t / uint32_t when the width matters.
Avoid double on ESP32-S3
The ESP32-S3 has a single-precision FPU that accelerates float operations in hardware. double operations are emulated in software and are 10–20× slower. Always use float (append f to literals: 25.5f not 25.5).
Why Fixed-Width Types Matter
1// ❌ Dangerous — int size varies2int sensor_value = 0;3if (sensor_value > 32767) { // works on 32-bit, overflows on 16-bit int!4 handle_overflow();5}67// ✅ Safe — exact width guaranteed8int32_t sensor_value = 0;9if (sensor_value > 32767) { // always works correctly10 handle_overflow();11}1213// ❌ Dangerous — unsigned int may be 16-bit on some platforms14unsigned int address = 0x40000000; // overflow if int is 16-bit!1516// ✅ Safe — uint32_t is always 32-bit17uint32_t address = 0x40000000; // always correctESP-IDF Specific Types
ESP-IDF defines additional convenience types:
1#include "esp_err.h"2esp_err_t result = ESP_OK; // int32_t — error codes (ESP_OK, ESP_FAIL, etc.)34#include "driver/gpio.h"5gpio_num_t led = GPIO_NUM_2; // enum — GPIO pin number67#include "freertos/FreeRTOS.h"8TickType_t delay = pdMS_TO_TICKS(100); // uint32_t — FreeRTOS tick count9TaskHandle_t task_handle = NULL; // void * — opaque task handle10BaseType_t priority = 1; // long — task priorityPrintf Format Specifiers for Fixed-Width Types
A common pitfall — you can't reliably use %d for fixed-width types across different architectures. You must use macros from <inttypes.h>.
| Type | Format Specifier | Example |
|---|---|---|
int8_t | %d (promoted to int) | printf("%d", val) |
uint8_t | %u (promoted to unsigned int) | printf("%u", val) |
int32_t | PRId32 | printf("%" PRId32, val) |
uint32_t | PRIu32 | printf("%" PRIu32, val) |
1#include <inttypes.h> // provides PRId32, PRIu32, etc.23uint32_t timestamp = 1713427200;45// Notice how the macro is placed OUTSIDE the string quotes6printf("Timestamp: %" PRIu32 "\n", timestamp); // portable across all platforms
