Skip to content
BoKSA

C Types and Macros

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
#include <stdint.h>

uint8_t  sensor_raw;   /* always 1 byte, 0..255 */
int16_t  temperature;  /* always 2 bytes, signed */
uint32_t timestamp_ms;

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
#include <stdbool.h>

bool is_detected = false;

if (sensor_triggered()) {
    is_detected = true;
}

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
#include <stdio.h>

int main(void) {
    printf("Hello World!\n");
    return 0;
}

On a Linux PC:

1
2
gcc program1.c -o program1
./program1

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
#define PI 3.14159
#define CONV_FACTOR 60
#define LED_PIN 13

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
#define RADTODEG1(x) ((x) * 57.29578)
#define RADTODEG2(x)  (x * 57.29578)

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:

  1. Parenthesize parameters and the whole expression: #define SQR(x) ((x) * (x))
  2. Avoid side effects: MAX(i++, j++) with a macro can increment twice
  3. Prefer static inline functions when you want real type checking and debugging

Macros you see in hardware headers

1
2
3
#define BIT(n)              (1U << (n))
#define SET_BIT(reg, bit)   ((reg) |= BIT(bit))
#define CLEAR_BIT(reg, bit) ((reg) &= ~BIT(bit))

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
#ifndef MY_BOARD_H
#define MY_BOARD_H
/* ... declarations ... */
#endif

Without this, duplicate #include causes "redefinition" errors.


Relevant topics


Starting points

  1. Print sizeof for every primitive type on your laptop and, if possible, with your MCU compiler (arm-none-eabi-gcc).
  2. Rewrite a magic number in existing code as a #define with proper parentheses.
  3. Break a macro on purpose (like RADTODEG2) and fix it — that teaches precedence better than any slide.
  4. Compare #define SQR(x) ((x)*(x)) with static inline int sqr(int x) { return x*x; } in a debugger.

Focus points

  • int size varies — never assume from one machine to another; use stdint.h in firmware.
  • Macros are not functions — no types, no scoping, easy to surprise yourself.
  • Document units in macro names: TIMEOUT_MS, BAUD_RATE, VREF_MV.
  • bool clarifies intent but still stores a numeric value.

Key points

  • Types define how many bytes and how to interpret them — critical on embedded targets.
  • stdint.h gives portable fixed-width integers for protocols and registers.
  • Macros run before compilation as text substitution; parenthesize aggressively.
  • #define constants and include guards are everyday tools in firmware projects.