Embedded C

Data Types & Integer Overflow

The C standard doesn't guarantee an int is 32 bits — only that it's at least 16. On a desktop that ambiguity almost never bites anyone; on embedded, where the same codebase might target an 8-bit AVR and a 32-bit Cortex-M, it's exactly the kind of thing that silently changes behavior between platforms. This page covers what the types actually guarantee, why unsigned shows up constantly in embedded code, and what actually happens — bit for bit — when a value overflows.

Quick reference
What it is
How C represents integers in memory, and the fixed-width types (stdint.h) embedded code uses instead of the ambiguous built-in ones.
Mental model
An unsigned integer is a car odometer — it rolls over to zero silently when it runs out of digits. A signed integer overflowing is undefined behavior — the "odometer" analogy doesn't even apply, the compiler is allowed to assume it never happens.
Key numbers
An 8-bit unsigned value: 0 to 255. An 8-bit signed value: -128 to 127 — same 256 total values, split differently.
Reach for it when
Choosing a type for anything that crosses a hardware register, a wire protocol, or a subtraction between two counters that could ever produce a negative logical result.

Why stdint.h exists

char, short, int, long, and long long each only guarantee a minimum width — the standard leaves the exact size up to the compiler and target architecture. An int is 16 bits on some small microcontroller toolchains and 32 bits on almost everything else; writing code that assumes one or the other is silently non-portable. <stdint.h> fixes this with fixed-width types that mean exactly what they say on every conforming compiler:

TypeWidthRange
uint8_t8 bits0 to 255
int8_t8 bits-128 to 127
uint16_t16 bits0 to 65,535
int16_t16 bits-32,768 to 32,767
uint32_t32 bits0 to 4,294,967,295
int32_t32 bits-2,147,483,648 to 2,147,483,647

Any embedded codebase that talks to hardware registers or a wire protocol should reach for these instead of int/ unsigned — a struct sent over CAN or read from an I2C register genuinely has a fixed bit width, and the type should say so explicitly rather than relying on whatever int happens to mean on this particular toolchain.

Signed vs. unsigned representation

Both a signed and unsigned 8-bit integer are 256 possible bit patterns — the difference is purely how those patterns are interpreted. Unsigned reads the bits directly as a magnitude: 0000 0000 through 1111 1111 is 0 through 255. Signed integers use two's complement: the top bit becomes a sign bit, so the same 256 patterns split into -128 through 127 instead — half the positive range, in exchange for representing negative numbers at all.

Why embedded code reaches for uint

What rollover actually is

Unsigned arithmetic in C is defined to wrap modulo 2n — this is not a bug or undefined behavior, it's exactly what the standard guarantees will happen. Adding 1 to the maximum value of a uint8_t doesn't error, doesn't saturate — it silently becomes 0.

A uint8_t at its maximum value 0xFF, all bits set, becomes 0x00 when incremented — the carry bit simply falls off the end and is discarded uint8_t counter = 255; counter++; 1 1 1 1 1 1 1 1 0xFF = 255 +1 — carry bit discarded 0 0 0 0 0 0 0 0 0x00 = 0 (wrapped) Defined behavior, not a bug — this is exactly what the C standard guarantees for unsigned overflow.
Signed overflow has no such guarantee — incrementing an int8_t past 127 is undefined behavior, and an optimizer is legally allowed to assume it never happens.

The bug this actually causes in the field almost never looks like the diagram above — it looks like an unsigned subtraction going the wrong direction:

uint32_t deadline = start_time + TIMEOUT_MS;
uint32_t remaining = deadline - now();

// if now() has already passed deadline, "remaining" doesn't go negative —
// there's no negative in unsigned arithmetic. It wraps to a number near
// UINT32_MAX, and code checking "if (remaining > 0) keep_waiting();"
// concludes there's still billions of milliseconds left to wait.

Integer promotion and the comparison trap

Small types are promoted to int before most operations — a uint8_t in an expression is temporarily treated as an int, which is usually invisible but occasionally matters. Far more dangerous: comparing a signed value to an unsigned one silently converts the signed value to unsigned first.

int x = -1;
unsigned int y = 0;
if (x > y) {
    // this branch RUNS — x is converted to UINT_MAX before comparing,
    // and UINT_MAX really is greater than 0
}

Real-world applications

Gotchas

Field notes