C Types and Macros
Introduction
When you write int temperature = 23, the computer does not store the word "int" or the idea of temperature — it stores a pattern of bits in a fixed number of bytes. Choosing the right type means choosing how many bytes you reserve and how those bytes should be interpreted (signed number, character, fraction, true/false).
This article revisits primitive types and introduces macros. Both show up constantly in embedded code: sensor values, register widths, baud rates, and pin numbers. Getting them wrong is like using the wrong size box — the value either does not fit, or you misread what is inside.
Why types matter in embedded work
Imagine you send a temperature of 255 over a serial link as one byte. On the receiver, is that byte signed (−1) or unsigned (255)? The wire carries the same bits; only the type tells the program how to read them. In firmware, mismatched types cause subtle bugs: wrong ADC readings, broken protocols, and memory corruption when you copy too many bytes into too small a buffer.
Before pointers and structs, be clear on what each primitive type actually stores on your machine — especially the MCU you flash to, not only the laptop you compile on.
Primitive data types (typical 32-bit system)
A type answers two questions: how many bytes, and what do those bytes mean?
| Type | Typical size (bytes) | What it is good for |
|---|---|---|
char |
1 | Single character or tiny integer |
short |
2 | Small integers when you need to save space |
int |
4 | General-purpose integers (often 32-bit on Cortex-M) |
long |
4 or 8 | Larger range; size depends on platform |
long long |
8 | Very large integers |
float |
4 | Approximate decimals (has rounding error) |
double |
8 | More precise floats; often overkill on small MCUs |
In plain terms
Think of types as labeled containers. A char is a tiny cup (1 byte). An int is a bigger jar (often 4 bytes). You cannot pour a litre of water into the cup without spilling — that is overflow. And if you label the cup "signed", values above 127 might suddenly look negative.
Fixed-width types for firmware
On embedded targets, prefer <stdint.h>:
1 2 3 4 5 | |
These names promise the same size on every platform. See Data representation for how bits encode signed and unsigned values.
bool (C99) — yes or no
Before C99, programmers used int for true/false. Now:
1 2 3 4 5 6 7 | |
bool makes intent obvious: this variable is not a counter or a pin number — it is a flag. Under the hood it is still stored as a small integer (0 or 1), but your teammates (and the compiler) understand the purpose.
From source code to running program (short version)
When you compile C, the compiler needs to know how much space each variable uses so it can lay out memory correctly.
1 2 3 4 5 6 | |
On a Linux PC:
1 2 | |
The compiler reads your types, generates machine instructions, and the operating system runs the result. On an MCU there is often no operating system — the linker places code in flash and the processor starts from reset. The types still matter the same way; only the environment differs. The full build story is in GCC toolchain in depth.
Macros — text substitution before compilation
The preprocessor runs first. It is not smart — it does not understand C. It copies and pastes text according to # directives.
Object-like macros — named constants
1 2 3 | |
Everywhere the compiler later sees LED_PIN, it sees 13 instead. That is useful for configuration values you might change in one place. For values that need a real type, const uint8_t LED_PIN = 13; is sometimes clearer — the compiler can type-check it.
Function-like macros — beware of traps
1 2 | |
If you write RADTODEG1(10 + 30), it becomes ((10 + 30) * 57.29578) — correct.
If you write RADTODEG2(10 + 30), it becomes (10 + 30 * 57.29578) — wrong, because multiplication binds tighter than addition.
In plain terms
A macro is like a find-and-replace in Word before the compiler reads your file. It does not know math rules — it only swaps text. Parentheses tell the replacement which chunk is the parameter.
Safe macro habits:
- Parenthesize parameters and the whole expression:
#define SQR(x) ((x) * (x)) - Avoid side effects:
MAX(i++, j++)with a macro can increment twice - Prefer
static inlinefunctions when you want real type checking and debugging
Macros you see in hardware headers
1 2 3 | |
SET_BIT(GPIOA_ODR, 5) expands to code that turns bit 5 on in a register. Vendor SDKs (STM32 HAL, CMSIS) use hundreds of these so register names match the datasheet.
Include guards
Headers use macros so they are not pasted twice:
1 2 3 4 | |
Without this, duplicate #include causes "redefinition" errors.
Relevant topics
Starting points
- Print
sizeoffor every primitive type on your laptop and, if possible, with your MCU compiler (arm-none-eabi-gcc). - Rewrite a magic number in existing code as a
#definewith proper parentheses. - Break a macro on purpose (like
RADTODEG2) and fix it — that teaches precedence better than any slide. - Compare
#define SQR(x) ((x)*(x))withstatic inline int sqr(int x) { return x*x; }in a debugger.
Focus points
intsize varies — never assume from one machine to another; usestdint.hin firmware.- Macros are not functions — no types, no scoping, easy to surprise yourself.
- Document units in macro names:
TIMEOUT_MS,BAUD_RATE,VREF_MV. boolclarifies intent but still stores a numeric value.
Key points
- Types define how many bytes and how to interpret them — critical on embedded targets.
stdint.hgives portable fixed-width integers for protocols and registers.- Macros run before compilation as text substitution; parenthesize aggressively.
#defineconstants and include guards are everyday tools in firmware projects.