Loading header...

Fixed-Width Types and Conversions

In embedded systems, the exact width of a value often matters as much as the value itself. A register may be 8 bits wide, a packet field may be 16 bits, and a timer count may wrap at 32 bits. Using int casually can make code less portable and more error-prone than it looks.

Learning Objectives

By the end of this lesson, you should be able to:

  • choose appropriate fixed-width integer types from <stdint.h>;
  • distinguish signed and unsigned behavior in comparisons and arithmetic;
  • recognize common implicit conversion bugs;
  • write explicit casts only where they are justified and safe.

Why Fixed Width Matters

The C standard does not guarantee that:

  • int is 16 bits;
  • long is 32 bits;
  • char is signed.

It only guarantees minimum ranges and relative ordering. On one MCU, int may be 16 bits. On another, it may be 32 bits.

For hardware-facing code, that ambiguity is unnecessary.

Preferred Types

Use <stdint.h> types when width matters:

Type Meaning
uint8_t exactly 8-bit unsigned
int8_t exactly 8-bit signed
uint16_t exactly 16-bit unsigned
uint32_t exactly 32-bit unsigned
uintptr_t unsigned integer wide enough for a pointer
size_t object size and array indexing type

Example:

#include <stdint.h>

uint8_t led_mask = 0x20u;
uint16_t adc_code = 1023u;
int16_t temperature_c = -10;

Signed Versus Unsigned

Unsigned values cannot represent negatives. Signed values can, but overflow rules differ.

Common trap

uint8_t retries = 0;
if (retries < 0) {
    // never true
}

That condition is meaningless because retries can never be negative.

Another trap

int16_t error = -1;
uint16_t count = 10;

if (error < count) {
    // maybe not what you expected after promotions
}

Mixed signed/unsigned comparisons can promote values in surprising ways. Read the warning and make the types intentional.

Integer Promotions

In C, small integer types often get promoted to int or unsigned int before arithmetic happens.

Example:

uint8_t a = 200;
uint8_t b = 100;
uint8_t c = a + b;

The addition usually happens in a wider type first, then narrows when assigned back to c. The final result wraps to fit in 8 bits:

$$
200 + 100 = 300 \rightarrow 44 \text{ in } uint8_t
$$

because 300 mod 256 = 44.

Conversions at Peripheral Boundaries

Registers often demand precise masking:

uint16_t adc = read_adc();
uint8_t low_byte = (uint8_t)(adc & 0xFFu);

The cast is appropriate here because:

  • the mask makes the narrowing explicit;
  • the destination width is intentional;
  • the code documents hardware intent.

By contrast, this is suspicious:

uint8_t timeout = compute_timeout_ms();

If compute_timeout_ms() returns a wider type, truncation may silently occur.

Literals and Suffixes

Use suffixes to make constant intent clear:

  • 1u for unsigned;
  • 1000UL for unsigned long;
  • UINT32_C(1000000) for width-aware constants when needed.

Example:

#define F_CPU 16000000UL
uint32_t period_us = 1000000u / 50u;

This avoids accidental signed arithmetic or width mismatch on smaller targets.

Practical Guidelines

  • Use fixed-width types for registers, packets, and file formats.
  • Use size_t for array lengths and object sizes.
  • Use unsigned types for bit masks and raw register fields.
  • Use signed types for quantities that can legitimately be negative.
  • Narrow only after range has been checked or masked deliberately.

Worked Example

Suppose you read a 12-bit ADC value and convert it to millivolts with a 3300 mV reference:

uint16_t adc = 3000;
uint32_t mv = ((uint32_t)adc * 3300u) / 4095u;

The cast to uint32_t before multiplication is important. Without it, a smaller target might overflow intermediate arithmetic depending on type sizes.

Common Mistakes

  • Using plain int for hardware register values.
  • Mixing signed and unsigned types without checking promotions.
  • Assuming casts fix logic rather than merely silencing warnings.
  • Narrowing into uint8_t because "the value is probably small."
  • Forgetting that literal types influence expression width.

Summary

Fixed-width types make embedded C more predictable. They help you match hardware widths, avoid portability surprises, and reason about overflow and conversion behavior clearly. Most conversion bugs are not syntax problems. They are intent problems. Make the widths and signedness explicit so the compiler can help you instead of surprising you.

Further Reading

Mind Map

mindmap root((Fixed Width Types)) Core idea Width is explicit Signedness is intent Hardware fields match Conversions need proof Type choices uint8_t registers uint16_t ADC codes uint32_t timers size_t lengths uintptr_t addresses Calculations uint8 wraps mod 256 ADC mV equals code times Vref over max Mask before narrowing Cast before multiply Design rules Use unsigned masks Signed for negatives Check range first Add literal suffixes Avoid mixed compares Practical checks Enable conversion warnings Review promotions Test boundary values Inspect packet bytes Confirm register width Mistakes Plain int for registers Cast hides bug char signedness assumed Silent truncation Wrong literal type