MODULE 3 · LESSON 3.2 · EXAMPLES
Fixed-Width Integer Types Examples: Registers, Bit Fields, Bytes and Timers
Eleven practical examples with verified output: all eight stdint types, structure layout, a simulated GPIO register overlay, bit-field macros, byte assembly, wrap-safe timeouts and ADC averaging.
Beginner → Intermediate
Level
Real output
Compiled and run
Fixed-Width Integer Types Examples: What This Page Covers
These fixed-width integer types examples show uint8_t, uint16_t, uint32_t and friends doing real embedded work: describing registers, packing bytes, timing, averaging and structure layout. Every program was compiled and run; the host-side register simulation runs on your PC exactly as it would run on the STM32F407 except for the base address.
| # | Example | Skill |
|---|
| 1 | Complete program using all eight types | Ranges and wrap |
| 2 | Structure layout with and without fixed-width types | Portability |
| 3 | GPIO register overlay (simulated on the PC) | Register definitions |
| 4 | Extract and insert bit fields in a 32-bit register | Masks and shifts |
| 5 | Build uint16_t / uint32_t values from bytes | Protocols |
| 6 | Wrap-safe timeouts with a uint32_t tick | Timing |
| 7 | Averaging ADC samples: choose the accumulator width | Overflow |
| 8 | Parsing signed int16_t sensor data | Signed fields |
| 9 | Printing with PRIu32 and PRIX32 | inttypes.h |
| 10 | Spot the bad type choice | Review |
| 11 | Mini exercises | Self-test |
Example 1: A Complete Program Using All Eight Fixed-Width Types
This first of the fixed-width integer types examples checks every type at once. Problem: Declare one variable of each type, initialise it with its limit macro, and print its size and value portably.
all_types.c
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
int main(void)
{
uint8_t u8 = UINT8_MAX;
uint16_t u16 = UINT16_MAX;
uint32_t u32 = UINT32_MAX;
uint64_t u64 = UINT64_MAX;
int8_t s8 = INT8_MIN;
int16_t s16 = INT16_MIN;
int32_t s32 = INT32_MIN;
int64_t s64 = INT64_MIN;
printf(“%-9s %2zu bytes max = %” PRIu8 “\n”, “uint8_t”, sizeof u8, u8);
printf(“%-9s %2zu bytes max = %” PRIu16 “\n”, “uint16_t”, sizeof u16, u16);
printf(“%-9s %2zu bytes max = %” PRIu32 “\n”, “uint32_t”, sizeof u32, u32);
printf(“%-9s %2zu bytes max = %” PRIu64 “\n”, “uint64_t”, sizeof u64, u64);
printf(“%-9s %2zu bytes min = %” PRId8 “\n”, “int8_t”, sizeof s8, s8);
printf(“%-9s %2zu bytes min = %” PRId16 “\n”, “int16_t”, sizeof s16, s16);
printf(“%-9s %2zu bytes min = %” PRId32 “\n”, “int32_t”, sizeof s32, s32);
printf(“%-9s %2zu bytes min = %” PRId64 “\n”, “int64_t”, sizeof s64, s64);
printf(“\nAfter adding 1 to each maximum (unsigned wrap):\n”);
u8++; u16++; u32++; u64++;
printf(“u8=%u u16=%u u32=%” PRIu32 ” u64=%” PRIu64 “\n”, (unsigned)u8, (unsigned)u16, u32, u64);
return 0;
}
Output (verified by compiling and running)
uint8_t 1 bytes max = 255
uint16_t 2 bytes max = 65535
uint32_t 4 bytes max = 4294967295
uint64_t 8 bytes max = 18446744073709551615
int8_t 1 bytes min = -128
int16_t 2 bytes min = -32768
int32_t 4 bytes min = -2147483648
int64_t 8 bytes min = -9223372036854775808
After adding 1 to each maximum (unsigned wrap):
u8=0 u16=0 u32=0 u64=0
Same sizes on every platform that provides these types
Observe: Each unsigned maximum wraps to 0 when incremented, and each signed minimum is exactly −2N−1. The same program prints the same numbers on the STM32F407 (apart from needing a UART printf retarget).
Example 2: Structure Layout With and Without Fixed-Width Types
Among fixed-width integer types examples, this one explains most porting bugs. Problem: A message structure is shared between the STM32F407 firmware and a PC test tool. Why does the layout break when it uses int and long?
layout_compare.c
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
struct Bad { char c; int i; long l; }; /* sizes depend on the platform */
struct Good { uint8_t c; uint32_t i; uint32_t l; };/* identical everywhere */
int main(void)
{
printf(“struct Bad : size %zu, offsets c=%zu i=%zu l=%zu\n”, sizeof(struct Bad),
offsetof(struct Bad, c), offsetof(struct Bad, i), offsetof(struct Bad, l));
printf(“struct Good: size %zu, offsets c=%zu i=%zu l=%zu\n”, sizeof(struct Good),
offsetof(struct Good, c), offsetof(struct Good, i), offsetof(struct Good, l));
return 0;
}
Output on a 64-bit Linux PC (LP64)
struct Bad : size 16, offsets c=0 i=4 l=8
struct Good: size 12, offsets c=0 i=4 l=8
struct Bad versus struct Good (each cell is one byte; grey = padding)
Bad on ARM (12 B)
c
pad
pad
pad
i0
i1
i2
i3
l0
l1
l2
l3
Bad on PC (16 B)
c
pad
pad
pad
i0
i1
i2
i3
l0
l1
l2
l3
l4
l5
l6
l7
Good on both (12 B)
c
pad
pad
pad
i0
i1
i2
i3
l0
l1
l2
l3
| Structure | ARM Cortex-M4 | 64-bit Linux PC | Compatible? |
|---|
struct Bad | 12 bytes | 16 bytes | No |
struct Good | 12 bytes | 12 bytes | Yes |
The ARM value for struct Bad follows from the rules: char at offset 0, int aligned to 4, long (4 bytes on ARM) at offset 8, total 12. On the PC long is 8 bytes and 8-byte aligned, giving 16. Alignment and padding are covered in Module 15.
Example 3: A GPIO Register Overlay, Simulated on the PC
The most important of the fixed-width integer types examples for firmware work is the register overlay. Problem: Configure PD12–PD15 (the four LEDs on the STM32F407 Discovery board) as outputs and switch them using a struct of volatile uint32_t registers. To run the logic on a PC, the base address points at an ordinary variable.
gpio_sim.c
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
typedef struct {
volatile uint32_t MODER, OTYPER, OSPEEDR, PUPDR, IDR, ODR, BSRR, LCKR;
volatile uint32_t AFR[2];
} GPIO_TypeDef;
/* On the STM32F407: #define GPIOD ((GPIO_TypeDef *)0x40020C00UL)
On the PC we point at an ordinary variable instead. */
static GPIO_TypeDef fake_gpiod;
#define GPIOD (&fake_gpiod)
static void show(const char *what)
{
printf(“%-26s MODER=0x%08” PRIX32 ” ODR=0x%08″ PRIX32 “\n”, what, GPIOD->MODER, GPIOD->ODR);
}
int main(void)
{
GPIOD->MODER = 0;
show(“reset state”);
/* PD12..PD15 as general purpose output = binary 01 in MODER[2n+1:2n] */
for (int pin = 12; pin <= 15; pin++) {
GPIOD->MODER &= ~(3U << (pin * 2)); /* clear the 2-bit field */
GPIOD->MODER |= (1U << (pin * 2)); /* write 01 = output */
}
show(“PD12..PD15 outputs”);
GPIOD->ODR |= (1U << 12); /* green LED on */
show(“PD12 high”);
GPIOD->ODR ^= (1U << 13); /* orange LED toggled */
show(“PD13 toggled”);
GPIOD->ODR &= ~(1U << 12); /* green LED off */
show(“PD12 low”);
return 0;
}
Output (verified by compiling and running)
reset state MODER=0x00000000 ODR=0x00000000
PD12..PD15 outputs MODER=0x55000000 ODR=0x00000000
PD12 high MODER=0x55000000 ODR=0x00001000
PD13 toggled MODER=0x55000000 ODR=0x00003000
PD12 low MODER=0x55000000 ODR=0x00002000
MODER after configuring PD12..PD15 as outputs (two bits per pin)
PD11 … PD0
bits 23:0
00 × 12
input (reset state)
MODER = 0x55000000
Key point: On the real board only the base address line changes. Every field offset (ODR at +0x14, BSRR at +0x18) comes from the uint32_t layout, which is why the same struct works on hardware and in a host-side unit test.
Example 4: Extracting and Inserting Bit Fields in a 32-bit Register
Problem: Read and modify a field inside a register without disturbing the other bits. This is the read-modify-write pattern used for every peripheral configuration.
bitfields.c
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#define FIELD_MASK(width) ((UINT32_C(1) << (width)) – 1U)
#define GET_FIELD(reg, pos, width) (((reg) >> (pos)) & FIELD_MASK(width))
#define SET_FIELD(reg, pos, width, val) \
((reg) = ((reg) & ~(FIELD_MASK(width) << (pos))) | (((uint32_t)(val) & FIELD_MASK(width)) << (pos)))
int main(void)
{
uint32_t reg = 0xA5C3F00Fu;
printf(“reg = 0x%08” PRIX32 “\n”, reg);
printf(“bits [7:4] = 0x%” PRIX32 “\n”, GET_FIELD(reg, 4, 4));
printf(“bits [15:8] = 0x%02” PRIX32 “\n”, GET_FIELD(reg, 8, 8));
printf(“bits [31:28] = 0x%” PRIX32 “\n”, GET_FIELD(reg, 28, 4));
SET_FIELD(reg, 8, 8, 0x3C); /* write 0x3C into bits [15:8] */
printf(“after SET [15:8]=3C = 0x%08” PRIX32 “\n”, reg);
SET_FIELD(reg, 0, 4, 0x1A5); /* value too wide: masked to 0x5 */
printf(“after SET [3:0]=1A5 = 0x%08” PRIX32 “\n”, reg);
return 0;
}
Output (verified by compiling and running)
reg = 0xA5C3F00F
bits [7:4] = 0x0
bits [15:8] = 0xF0
bits [31:28] = 0xA
after SET [15:8]=3C = 0xA5C33C0F
after SET [3:0]=1A5 = 0xA5C33C05
Clear-then-set for bits [15:8]
Why UINT32_C(1)? In 1 << 31 the literal is a signed int, so the shift is undefined. UINT32_C(1) (or 1U) is unsigned and always safe up to bit 31.
Example 5: Building uint16_t and uint32_t Values from Bytes
Problem: A sensor sends multi-byte values over I2C or UART as separate uint8_t bytes. Assemble them correctly in both byte orders.
bytes_to_words.c
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
static uint16_t be16(const uint8_t *p) { return (uint16_t)((p[0] << 8) | p[1]); }
static uint16_t le16(const uint8_t *p) { return (uint16_t)((p[1] << 8) | p[0]); }
static uint32_t le32(const uint8_t *p)
{
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
int main(void)
{
uint8_t frame[6] = { 0x12, 0x34, 0x78, 0x56, 0x34, 0x12 };
printf(“big-endian 16-bit @0 = 0x%04X\n”, be16(&frame[0]));
printf(“little-endian 16-bit @0 = 0x%04X\n”, le16(&frame[0]));
printf(“little-endian 32-bit @2 = 0x%08” PRIX32 “\n”, le32(&frame[2]));
uint32_t wrong = frame[5] << 24; /* promoted to int: UB if bit 31 set */
uint32_t right = (uint32_t)frame[5] << 24;
printf(“0x12 << 24 (both fine here): 0x%08” PRIX32 ” / 0x%08″ PRIX32 “\n”, wrong, right);
uint8_t top = 0xF0;
printf(“(uint32_t)top << 24 = 0x%08” PRIX32 “\n”, (uint32_t)top << 24);
uint32_t v = 0xDEADBEEFu;
uint8_t *bytes = (uint8_t *)&v;
printf(“memory order of 0xDEADBEEF: %02X %02X %02X %02X (little-endian host)\n”, bytes[0], bytes[1], bytes[2], bytes[3]);
return 0;
}
Output (verified by compiling and running)
big-endian 16-bit @0 = 0x1234
little-endian 16-bit @0 = 0x3412
little-endian 32-bit @2 = 0x12345678
0x12 << 24 (both fine here): 0x12000000 / 0x12000000
(uint32_t)top << 24 = 0xF0000000
memory order of 0xDEADBEEF: EF BE AD DE (little-endian host)
Same bytes 0x12 0x34, two byte orders
Big-endian (network order)
(p[0] << 8) | p[1]
0x1234
Little-endian (Cortex-M, x86)
(p[1] << 8) | p[0]
0x3412
Cast before shifting: A uint8_t is promoted to int in p[3] << 24. With a byte of 0x80 or more the shift reaches the sign bit of a 32-bit int: undefined behaviour. Always write (uint32_t)p[3] << 24.
Example 6: Wrap-Safe Timeouts With a uint32_t Tick Counter
Timing is where fixed-width integer types examples matter most. Problem: A millisecond tick (uint32_t, like HAL_GetTick()) is about to wrap. Compare two ways to test for a 500 ms timeout.
tick_timeout.c
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
static uint32_t tick_ms; /* incremented every millisecond */
static int expired_naive(uint32_t start, uint32_t timeout) { return tick_ms >= start + timeout; }
static int expired_safe (uint32_t start, uint32_t timeout) { return (uint32_t)(tick_ms – start) >= timeout; }
int main(void)
{
tick_ms = UINT32_MAX – 99; /* 100 ms before the counter wraps */
uint32_t start = tick_ms;
uint32_t timeout = 500; /* want to wait 500 ms */
printf(“start = %” PRIu32 ” start + timeout = %” PRIu32 ” (wrapped!)\n”, start, (uint32_t)(start + timeout));
printf(“%8s %12s %8s %8s\n”, “elapsed”, “tick_ms”, “naive”, “safe”);
for (int step = 0; step <= 6; step++) {
uint32_t elapsed = (uint32_t)step * 100u;
tick_ms = start + elapsed;
printf(“%6” PRIu32 “ms %12” PRIu32 ” %8d %8d\n”, elapsed, tick_ms, expired_naive(start, timeout), expired_safe(start, timeout));
}
return 0;
}
Output (verified by compiling and running)
start = 4294967196 start + timeout = 400 (wrapped!)
elapsed tick_ms naive safe
0ms 4294967196 1 0
100ms 0 0 0
200ms 100 0 0
300ms 200 0 0
400ms 300 0 0
500ms 400 1 1
600ms 500 1 1
The naive test tick_ms >= start + timeout is already true at 0 ms because start + timeout wrapped to a tiny number; it reports the timeout as expired immediately. The subtraction test (uint32_t)(tick_ms - start) >= timeout uses modular arithmetic: it gives the correct elapsed time across the wrap and expires exactly at 500 ms.
Number line of a 32-bit tick, wrapping at 4,294,967,295
start = UINT32_MAX − 99
wrap to 0
now = start + 500 = 400
now − start = 400 − 4294967197 = 500 (mod 232)
Example 7: Averaging ADC Samples: Choose the Accumulator Width
Problem: Average 32 readings from a 12-bit ADC. The readings fit in uint16_t, but what about their sum?
adc_average.c
#include <stdio.h>
#include <stdint.h>
#define N 32
int main(void)
{
uint16_t samples[N];
for (int i = 0; i < N; i++) samples[i] = 4000; /* steady 12-bit reading */
uint16_t sum16 = 0;
uint32_t sum32 = 0;
for (int i = 0; i < N; i++) { sum16 += samples[i]; sum32 += samples[i]; }
printf(“true sum = %d\n”, N * 4000);
printf(“uint16_t sum = %u -> average %u (WRONG)\n”, sum16, sum16 / N);
printf(“uint32_t sum = %u -> average %u (correct)\n”, sum32, sum32 / N);
printf(“max safe samples for a uint16_t sum of 12-bit values: %u\n”, 65535u / 4095u);
return 0;
}
Output (verified by compiling and running)
true sum = 128000
uint16_t sum = 62464 -> average 1952 (WRONG)
uint32_t sum = 128000 -> average 4000 (correct)
max safe samples for a uint16_t sum of 12-bit values: 16
- A 12-bit sample is at most 4095, so 16 of them fit in 16 bits (16 × 4095 = 65,520) but 32 do not.
- The accumulator must be wider than the elements:
uint32_t can sum over 1,000,000 12-bit samples. - Storing samples in
uint16_t but summing in uint32_t uses half the RAM of storing everything as 32-bit.
Rule: Size an accumulator for count × maximum value, not for a single value.
Example 8: Parsing Signed int16_t Sensor Data
Problem: An accelerometer returns three 16-bit signed values. Assemble them from bytes without losing the sign (this applies the 2’s complement knowledge from Module 2).
signed_parse.c
#include <stdio.h>
#include <stdint.h>
int main(void)
{
/* 6-byte accelerometer frame: X, Y, Z as little-endian int16_t */
uint8_t raw[6] = { 0x10, 0x00, 0xF6, 0xFF, 0x00, 0xC0 };
int16_t axis[3];
for (int i = 0; i < 3; i++) {
axis[i] = (int16_t)((uint16_t)raw[2 * i] | ((uint16_t)raw[2 * i + 1] << 8));
}
printf(“X = %d Y = %d Z = %d\n”, axis[0], axis[1], axis[2]);
uint16_t y_wrong = (uint16_t)raw[2] | ((uint16_t)raw[3] << 8);
printf(“Y read as uint16_t = %u (wrong sign)\n”, y_wrong);
float g = axis[0] / 16384.0f; /* +/-2 g range: 16384 LSB per g */
printf(“X in g = %.4f\n”, g);
return 0;
}
Output (verified by compiling and running)
X = 16 Y = -10 Z = -16384
Y read as uint16_t = 65526 (wrong sign)
X in g = 0.0010
| Axis | Bytes (little-endian) | uint16_t | int16_t (correct) |
|---|
| X | 10 00 | 16 | 16 |
| Y | F6 FF | 65526 | −10 |
| Z | 00 C0 | 49152 | −16384 |
The fix is the type: Assemble in uint16_t (no sign surprises while shifting), then cast the finished 16-bit word to int16_t.
Example 9: Printing With PRIu32, PRIX32 and Friends
Our last of the fixed-width integer types examples covers output. Problem: Print a table of peripheral base addresses so the output is correct on a 32-bit ARM target and a 64-bit PC.
print_regs.c
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
int main(void)
{
struct { const char *name; uint32_t addr; } regs[] = {
{ “GPIOD base”, 0x40020C00u }, { “RCC base”, 0x40023800u }, { “USART2 base”, 0x40004400u },
{ “ADC1 base”, 0x40012000u }, { “TIM2 base”, 0x40000000u }, { “SRAM1 start”, 0x20000000u },
};
printf(“%-14s %-12s %s\n”, “peripheral”, “hex”, “decimal”);
for (unsigned i = 0; i < sizeof regs / sizeof regs[0]; i++)
printf(“%-14s 0x%08” PRIX32 ” %” PRIu32 “\n”, regs[i].name, regs[i].addr, regs[i].addr);
int16_t t = –273;
uint64_t us = UINT64_C(3600) * 1000000u;
printf(“t = %” PRId16 ” (x0.1 C), uptime = %” PRIu64 ” us\n”, t, us);
return 0;
}
Output (verified by compiling and running)
peripheral hex decimal
GPIOD base 0x40020C00 1073875968
RCC base 0x40023800 1073887232
USART2 base 0x40004400 1073759232
ADC1 base 0x40012000 1073815552
TIM2 base 0x40000000 1073741824
SRAM1 start 0x20000000 536870912
t = -273 (x0.1 C), uptime = 3600000000 us
The same source prints correctly whether uint32_t is unsigned int (PC) or unsigned long (ARM newlib), because PRIX32 expands to the right conversion for each.
Example 10: Spot the Bad Type Choice
Each declaration below is a real defect pattern. Decide what is wrong before opening the answer.
int rx_byte; /* UART data */
Wastes 3 bytes per byte and is signed: a received 0xFF becomes 255 in an int only after promotion. Use uint8_t.
long ms_tick; /* wraps? */
4 bytes on ARM but 8 on a PC, and signed. Use uint32_t.
unsigned char temp; /* can be negative */
Cannot store −5 °C. Use int8_t or int16_t.
uint8_t adc; /* 12-bit ADC */
Overflows: 4095 does not fit in 8 bits. Use uint16_t.
uint16_t uptime_ms;
Wraps every 65.5 seconds. Use uint32_t (or uint64_t for long uptimes).
int *reg = (int *)0x40020C14;
int is not guaranteed to be 32 bits and the pointer target is not volatile. Use volatile uint32_t *.
uint8_t i; for (i = 0; i < 300; i++)
Never terminates: an 8-bit counter can never reach 300. Use uint16_t or int.
Example 11: Mini Exercises
Exercise 1: What is (uint8_t)(200 + 100)?
300 mod 256 = 44.
Exercise 2: What is sizeof(GPIO_TypeDef) if it has ten uint32_t members?
10 × 4 = 40 bytes (0x28).
Exercise 3: Write the expression that extracts bits [11:8] of a uint32_t reg.
(reg >> 8) & 0xFU.
Exercise 4: What type holds the sum of 100 samples of a 16-bit ADC (max 65535)?
100 × 65535 = 6,553,500, which needs 23 bits: uint32_t.
Exercise 5: Which macro prints a uint64_t in hex with 16 digits?
"%016" PRIX64.
Exercise 6: Why is (uint32_t)p[3] << 24 better than p[3] << 24?
The bare p[3] is promoted to int; shifting a value of 0x80 or more into bit 31 of a signed int is undefined behaviour.
Key Takeaways from the Fixed-Width Integer Types Examples
- The fixed-width integer types examples show identical sizes and wrap points on every platform: that is the point of the types.
- Structures built from
uint8_t/uint32_t keep one layout on ARM and PC; int and long do not. - Register blocks are structs of
volatile uint32_t; the base address is the only target-specific part, so they can be simulated on a PC. - Read-modify-write with masks and shifts changes one field and leaves the others intact.
- Cast operands to
uint32_t before shifting to avoid promotion surprises. - Wrap-safe timing uses unsigned subtraction. Accumulators must be wider than the samples they sum.
- Print with
PRIu32/PRIX32 so the same code works on 32- and 64-bit targets.
Ready for the quiz?
Next: choose the correct type for a use case, range calculations and interview questions on stdint.h.
2 Comments