Examples:Fixed-Width Integer Types Examples: Registers, Bit Fields, Bytes and Timers-Free Embedded C Programming Course

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

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.

11
Worked Examples
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.

#ExampleSkill
1Complete program using all eight typesRanges and wrap
2Structure layout with and without fixed-width typesPortability
3GPIO register overlay (simulated on the PC)Register definitions
4Extract and insert bit fields in a 32-bit registerMasks and shifts
5Build uint16_t / uint32_t values from bytesProtocols
6Wrap-safe timeouts with a uint32_t tickTiming
7Averaging ADC samples: choose the accumulator widthOverflow
8Parsing signed int16_t sensor dataSigned fields
9Printing with PRIu32 and PRIX32inttypes.h
10Spot the bad type choiceReview
11Mini exercisesSelf-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
uint8_t / int8_t
B0
uint16_t / int16_t
B0
B1
uint32_t / int32_t
B0
B1
B2
B3
uint64_t / int64_t
B0
B1
B2
B3
B4
B5
B6
B7
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
StructureARM Cortex-M464-bit Linux PCCompatible?
struct Bad12 bytes16 bytesNo
struct Good12 bytes12 bytesYes

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)
PD15
bits 31:30
01
output
PD14
bits 29:28
01
output
PD13
bits 27:26
01
output
PD12
bits 25:24
01
output
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]
before
31
1
30
0
29
1
28
0
27
0
26
1
25
0
24
1
23
1
22
1
21
0
20
0
19
0
18
0
17
1
16
1
15
1
14
1
13
1
12
1
11
0
10
0
9
0
8
0
7
0
6
0
5
0
4
0
3
1
2
1
1
1
0
1
after
1
0
1
0
0
1
0
1
1
1
0
0
0
0
1
1
0
0
1
1
1
1
0
0
0
0
0
0
1
1
1
1
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
AxisBytes (little-endian)uint16_tint16_t (correct)
X10 001616
YF6 FF65526−10
Z00 C049152−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.

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *