Pointer Casting Examples in Embedded C: Byte Order, Frame Codecs, Alignment and Register Decoding-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 9 · LESSON 9.5 · EXAMPLES

Pointer Casting Examples in Embedded C: Byte Order, Frame Codecs, Alignment and Register Decoding

Eight worked examples with real output: byte-swap helpers, a CRC-16 frame codec, IEEE-754 field decoding, an alignment-checked word copy, record serialisation, sign extension, GPIO_MODER decoding and a union register overlay. Study this one of the pointer casting examples in embedded C closely.

9
Worked Examples
Intermediate → Advanced
Level
Real output
Compiled and run

Pointer Casting Examples in Embedded C: What This Page Covers

These pointer casting examples in embedded C apply Lesson 9.5: byte-swap helpers for network order, a frame codec with a CRC-16, decoding an IEEE-754 float with memcpy, a word-copy routine that checks alignment, a packed record serialised field by field, sign extension of ADC samples, decoding a 32-bit GPIO register and a union overlay of a register. Every program was compiled with GCC and run; the output is real. These pointer casting examples in embedded C build on each other.

#ExampleSkill
1Byte-swap helpers and network orderEndianness
2A frame codec with CRC-16Shifts, bytes
3Decoding an IEEE-754 floatmemcpy punning
4Word copy with an alignment checkAlignment
5Serialising a record field by fieldPadding vs wire format
6Sign extension of 12-bit samplesCasts and widths
7Decoding GPIOA_MODERRegister fields
8A union overlay of a registerType punning
9Mini exercisesSelf-test

Example 1: Byte-Swap Helpers and Network Order

Problem: Send a port number and an IPv4 address in network (big-endian) byte order from a little-endian MCU. swap16 and swap32 reorder the bytes with shifts and masks. The conversion functions swap only on a little-endian host, so the same source is correct on a big-endian CPU. The byte dumps show the effect: the port 0x1F90 sits in memory as 90 1F and goes onto the wire as 1F 90; the address 0xC0A80101 changes from 01 01 A8 C0 to C0 A8 01 01, the familiar 192.168.1.1. On the Cortex-M4 the compiler recognises these patterns and emits a single REV or REV16 instruction. Among the pointer casting examples in embedded C this pattern is very common.

byteswap.c
#include <stdio.h> #include <stdint.h> static uint16_t swap16(uint16_t v) { return (uint16_t)((v << 8) | (v >> 8)); } static uint32_t swap32(uint32_t v) { return (v >> 24) | ((v >> 8) & 0xFF00u) | ((v << 8) & 0xFF0000u) | (v << 24); } static int host_is_little(void) { const uint16_t one = 1; return *(const uint8_t *)&one == 1; } /* host to network (big-endian) conversions that work on either kind of host */ static uint16_t to_be16(uint16_t v) { return host_is_little() ? swap16(v) : v; } static uint32_t to_be32(uint32_t v) { return host_is_little() ? swap32(v) : v; } static void show(const char *label, const void *obj, size_t n) { const uint8_t *b = obj; printf(“%-18s”, label); for (size_t i = 0; i < n; i++) printf(” %02X”, b[i]); printf(“\n”); } int main(void) { uint16_t port = 0x1F90; /* 8080 */ uint32_t addr = 0xC0A80101u; /* 192.168.1.1 */ printf(“host is %s-endian\n”, host_is_little() ? “little” : “big”); show(“port in memory:”, &port, sizeof port); port = to_be16(port); show(“port, wire order:”, &port, sizeof port); show(“addr in memory:”, &addr, sizeof addr); addr = to_be32(addr); show(“addr, wire order:”, &addr, sizeof addr); printf(“swap32(0x11223344) = 0x%08X, swap16(0xABCD) = 0x%04X\n”, (unsigned)swap32(0x11223344u), swap16(0xABCD)); return 0; }
Output (verified by compiling and running)
host is little-endian port in memory: 90 1F port, wire order: 1F 90 addr in memory: 01 01 A8 C0 addr, wire order: C0 A8 01 01 swap32(0x11223344) = 0x44332211, swap16(0xABCD) = 0xCDAB
Portable detection: Compile-time checks are better than the run-time test above: GCC defines __BYTE_ORDER__ and __ORDER_LITTLE_ENDIAN__. The run-time version is shown because it needs no macro knowledge.

Example 2: A Frame Codec with CRC-16

Problem: Exchange commands over a UART with error detection. The frame is 0xAA, a length, a command, the payload and a 16-bit CRC, all multi-byte values in big-endian order. The CRC is the standard CRC-16/CCITT-FALSE (polynomial 0x1021, start value 0xFFFF); the program first checks the implementation against the well-known test vector “123456789” whose CRC is 0x29B1. The builder writes bytes with shifts, and the parser reads the CRC the same way, so no pointer to a wider type is ever formed and the alignment of the buffer does not matter. A single flipped bit is detected by the CRC (result −3). Keep these pointer casting examples in embedded C for revision.

frame_codec.c
#include <stdio.h> #include <stdint.h> #include <stddef.h> /* CRC-16/CCITT-FALSE: polynomial 0x1021, initial value 0xFFFF */ static uint16_t crc16(const uint8_t *p, size_t n) { uint16_t crc = 0xFFFFu; while (n–) { crc ^= (uint16_t)(*p++ << 8); for (int bit = 0; bit < 8; bit++) crc = (crc & 0x8000u) ? (uint16_t)((crc << 1) ^ 0x1021u) : (uint16_t)(crc << 1); } return crc; } /* frame: AA | len | cmd | payload | crc_hi | crc_lo (all multi-byte fields big-endian) */ static size_t frame_build(uint8_t *out, uint8_t cmd, const uint8_t *pl, uint8_t n) { size_t i = 0; out[i++] = 0xAA; out[i++] = (uint8_t)(n + 1); /* length counts cmd + payload */ out[i++] = cmd; for (uint8_t k = 0; k < n; k++) out[i++] = pl[k]; uint16_t c = crc16(out + 2, (size_t)n + 1); /* CRC over cmd + payload */ out[i++] = (uint8_t)(c >> 8); out[i++] = (uint8_t)c; return i; } static int frame_parse(const uint8_t *f, size_t size, uint8_t *cmd, const uint8_t **pl, uint8_t *n) { if (size < 5 || f[0] != 0xAA) return –1; uint8_t len = f[1]; if (len < 1 || (size_t)len + 4 != size) return –2; uint16_t got = (uint16_t)((f[size – 2] << 8) | f[size – 1]); uint16_t calc = crc16(f + 2, len); if (got != calc) return –3; *cmd = f[2]; *pl = f + 3; *n = (uint8_t)(len – 1); return 0; } int main(void) { const uint8_t check[] = { ‘1’,‘2’,‘3’,‘4’,‘5’,‘6’,‘7’,‘8’,‘9’ }; printf(“crc16(\”123456789\”) = 0x%04X (standard check value 0x29B1)\n”, crc16(check, sizeof check)); uint8_t buf[16]; uint8_t payload[] = { 0x01, 0x90 }; /* 400 as big-endian */ size_t n = frame_build(buf, 0x21, payload, sizeof payload); printf(“frame (%zu bytes):”, n); for (size_t i = 0; i < n; i++) printf(” %02X”, buf[i]); printf(“\n”); uint8_t cmd, plen; const uint8_t *pl; int rc = frame_parse(buf, n, &cmd, &pl, &plen); printf(“parse: rc=%d cmd=0x%02X payload=%u bytes, value=%u\n”, rc, cmd, plen, (unsigned)((pl[0] << 8) | pl[1])); buf[3] ^= 0x04; /* flip one bit */ printf(“after a bit flip: rc=%d\n”, frame_parse(buf, n, &cmd, &pl, &plen)); return 0; }
Output (verified by compiling and running)
crc16(“123456789”) = 0x29B1 (standard check value 0x29B1) frame (7 bytes): AA 03 21 01 90 CD E2 parse: rc=0 cmd=0x21 payload=2 bytes, value=400 after a bit flip: rc=-3
Hardware CRC: The STM32F407 has a CRC unit, but it computes the Ethernet CRC-32 on 32-bit words, not CCITT-16 on bytes. A software CRC-16 is the usual choice for byte-oriented protocols.

Example 3: Decoding an IEEE-754 Float

Problem: Inspect how a 32-bit float is stored, for example to debug sensor data in a hex dump. memcpy copies the four bytes into a uint32_t without violating strict aliasing. The fields are 1 sign bit, 8 exponent bits (biased by 127) and 23 mantissa bits with an implied leading 1. For 1.0 the exponent is 127 (unbiased 0) and the mantissa is 0; for −2.5 the sign bit is set, the exponent 128 and the mantissa 0x200000 (the fraction .25 in binary 01 followed by zeros). The value 0.1 cannot be represented exactly, which shows as the repeating mantissa pattern 0x4CCCCD. The last lines go the other way and build 3.0 from fields.

float_fields.c
#include <stdio.h> #include <stdint.h> #include <string.h> static void decode(float f) { uint32_t u; memcpy(&u, &f, sizeof u); /* legal reinterpretation */ uint32_t sign = u >> 31; uint32_t exponent = (u >> 23) & 0xFFu; uint32_t mantissa = u & 0x7FFFFFu; printf(“%10.4f bits=0x%08X sign=%u exp=%3u (unbiased %4d) mantissa=0x%06X\n”, f, (unsigned)u, (unsigned)sign, (unsigned)exponent, (int)exponent – 127, (unsigned)mantissa); } int main(void) { decode(1.0f); decode(-2.5f); decode(0.1f); decode(100.0f); decode(0.0f); /* build a float from its fields: sign 0, exponent 128 (=2^1), mantissa .5 -> 1.5 * 2 = 3.0 */ uint32_t built = (0u << 31) | (128u << 23) | 0x400000u; float f; memcpy(&f, &built, sizeof f); printf(“built from fields: %.1f\n”, f); return 0; }
Output (verified by compiling and running)
1.0000 bits=0x3F800000 sign=0 exp=127 (unbiased 0) mantissa=0x000000 -2.5000 bits=0xC0200000 sign=1 exp=128 (unbiased 1) mantissa=0x200000 0.1000 bits=0x3DCCCCCD sign=0 exp=123 (unbiased -4) mantissa=0x4CCCCD 100.0000 bits=0x42C80000 sign=0 exp=133 (unbiased 6) mantissa=0x480000 0.0000 bits=0x00000000 sign=0 exp= 0 (unbiased -127) mantissa=0x000000 built from fields: 3.0
Cortex-M4F: The STM32F407 has a single-precision FPU, so float arithmetic is done in hardware, while double is emulated in software and is much slower. Compile with -mfpu=fpv4-sp-d16 -mfloat-abi=hard and use 1.0f literals.

Example 4: Word Copy with an Alignment Check

Problem: Copy faster by moving four bytes at a time, but only when that is legal. The test ((uintptr_t)d | (uintptr_t)s) & 3 is zero only if both addresses are multiples of 4, because OR-ing keeps any set low bit. Only then are the pointers converted to uint32_t *. The first call copies 15 bytes as 3 words plus 3 single bytes; the second call has an unaligned destination and falls back to 14 byte copies. Real library memcpy implementations use exactly this idea, plus extra code for the case where only one pointer is unaligned. The declaration _Alignas(4) (C11) guarantees the arrays are aligned; without it a uint8_t array may start at any address.

aligned_copy.c
#include <stdio.h> #include <stdint.h> #include <stddef.h> #include <string.h> static unsigned word_copies, byte_copies; static void fast_copy(void *dst, const void *src, size_t n) { uint8_t *d = dst; const uint8_t *s = src; int both_aligned = (((uintptr_t)d | (uintptr_t)s) & 3u) == 0; /* low two bits of both are 0 */ if (both_aligned) { uint32_t *dw = (uint32_t *)d; /* now safe: the address is aligned */ const uint32_t *sw = (const uint32_t *)s; while (n >= 4) { *dw++ = *sw++; n -= 4; word_copies++; } d = (uint8_t *)dw; s = (const uint8_t *)sw; } while (n–) { *d++ = *s++; byte_copies++; } /* tail or unaligned case */ } int main(void) { _Alignas(4) uint8_t src[16] = “ABCDEFGHIJKLMNO”; _Alignas(4) uint8_t dst[16]; memset(dst, 0, sizeof dst); fast_copy(dst, src, 15); /* both aligned, 15 bytes */ printf(“aligned : \”%s\” words=%u bytes=%u\n”, dst, word_copies, byte_copies); word_copies = byte_copies = 0; memset(dst, 0, sizeof dst); fast_copy(dst + 1, src, 14); /* destination off by one */ printf(“unaligned: \”%s\” words=%u bytes=%u\n”, dst + 1, word_copies, byte_copies); return 0; }
Output (verified by compiling and running)
aligned : “ABCDEFGHIJKLMNO” words=3 bytes=3 unaligned: “ABCDEFGHIJKLMN” words=0 bytes=14
Aliasing caveat: Accessing a uint8_t array through a uint32_t * technically violates strict aliasing. Library code gets away with it because it is compiled with special flags or written in assembly; in your own code use memcpy and let the compiler choose the instructions.

Example 5: Serialising a Record Field by Field

Problem: Store or transmit a record in a fixed format that does not depend on the compiler’s padding or the CPU’s byte order. In memory the structure occupies 12 bytes (padding after id and at the end); the wire format needs only 8: id, four timestamp bytes least significant first, two bytes of temperature and the flags. record_write and record_read move each field explicitly with shifts, so the format is the same on every compiler and CPU, and the buffer needs no alignment. The negative temperature −57 is cast to uint16_t first (so the shift acts on the bit pattern 0xFFC7) and back to int16_t when reading, restoring −57.

serialise.c
#include <stdio.h> #include <stdint.h> #include <stddef.h> typedef struct { uint8_t id; uint32_t timestamp; int16_t temp; /* tenths of a degree */ uint8_t flags; } record_t; static size_t record_write(uint8_t *out, const record_t *r) /* wire format: little-endian, no padding */ { size_t i = 0; out[i++] = r->id; for (int s = 0; s < 32; s += 8) out[i++] = (uint8_t)(r->timestamp >> s); out[i++] = (uint8_t)((uint16_t)r->temp); out[i++] = (uint8_t)((uint16_t)r->temp >> 8); out[i++] = r->flags; return i; } static void record_read(const uint8_t *in, record_t *r) { r->id = in[0]; r->timestamp = (uint32_t)in[1] | ((uint32_t)in[2] << 8) | ((uint32_t)in[3] << 16) | ((uint32_t)in[4] << 24); r->temp = (int16_t)(uint16_t)(in[5] | (in[6] << 8)); r->flags = in[7]; } int main(void) { record_t a = { 7, 0x00123456u, –57, 0x81 }; uint8_t wire[16]; size_t n = record_write(wire, &a); printf(“sizeof(record_t) in memory = %zu, wire size = %zu\n”, sizeof(record_t), n); printf(“wire bytes:”); for (size_t i = 0; i < n; i++) printf(” %02X”, wire[i]); printf(“\n”); record_t b; record_read(wire, &b); printf(“decoded: id=%u ts=0x%08lX temp=%d flags=0x%02X\n”, b.id, (unsigned long)b.timestamp, b.temp, b.flags); return 0; }
Output (verified by compiling and running)
sizeof(record_t) in memory = 12, wire size = 8 wire bytes: 07 56 34 12 00 C7 FF 81 decoded: id=7 ts=0x00123456 temp=-57 flags=0x81
Why not write the struct directly? Sending sizeof(record_t) bytes from memory leaks padding bytes (uninitialised data), depends on the compiler, and is wrong when sender and receiver differ in endianness or struct layout.

Example 6: Sign Extension of 12-Bit Samples

Problem: A sensor delivers 12-bit two’s-complement values in the low bits of a 16-bit word. A cast to int16_t does not make them negative, because the sign bit of the 12-bit field (bit 11) is not the sign bit of the 16-bit type. sign_extend masks the field and uses the trick (raw ^ sign) - sign, which flips the sign bit and subtracts it: 0x800 becomes −2048, 0xFFF becomes −1 and 0xFC7 becomes −57. The last lines illustrate conversion rules: converting an out-of-range value to a signed type is implementation-defined (GCC wraps modulo 2n), while conversion to an unsigned type is always defined as reduction modulo 2n.

sign_extend.c
#include <stdio.h> #include <stdint.h> /* the sample is a signed value held in the low ‘bits’ bits of a word */ static int32_t sign_extend(uint32_t raw, unsigned bits) { uint32_t sign = 1u << (bits – 1); raw &= (1u << bits) – 1u; /* keep only the valid bits */ return (int32_t)((raw ^ sign) – sign); /* classic branch-free sign extension */ } int main(void) { uint16_t raw12[] = { 0x000, 0x001, 0x7FF, 0x800, 0xFFF, 0xFC7 }; for (int i = 0; i < 6; i++) printf(“12-bit 0x%03X -> %5ld\n”, raw12[i], (long)sign_extend(raw12[i], 12)); /* what a plain cast does: a 12-bit field in a uint16_t is positive until sign extended */ uint16_t v = 0xFC7; printf(“\n(int16_t)0x0FC7 = %d (still positive: bit 11 is not the sign bit of 16 bits)\n”, (int16_t)v); printf(“(int16_t)0xFFC7 = %d (a real 16-bit negative)\n”, (int16_t)0xFFC7); printf(“(int8_t)200 = %d (implementation-defined: wraps on GCC)\n”, (int8_t)200); printf(“(uint8_t)-1 = %u (defined: modulo 256)\n”, (uint8_t)-1); return 0; }
Output (verified by compiling and running)
12-bit 0x000 -> 0 12-bit 0x001 -> 1 12-bit 0x7FF -> 2047 12-bit 0x800 -> -2048 12-bit 0xFFF -> -1 12-bit 0xFC7 -> -57 (int16_t)0x0FC7 = 4039 (still positive: bit 11 is not the sign bit of 16 bits) (int16_t)0xFFC7 = -57 (a real 16-bit negative) (int8_t)200 = -56 (implementation-defined: wraps on GCC) (uint8_t)-1 = 255 (defined: modulo 256)
ARM instruction: The Cortex-M4 has SBFX (signed bit-field extract) which does this in one instruction; GCC often uses it for the C code above.

Example 7: Decoding GPIOA_MODER

Problem: Read a GPIO mode register in a debugger and understand it. MODER has two bits per pin (00 input, 01 output, 10 alternate function, 11 analog). The decoder shifts the register right by 2 × pin and masks two bits. The reset value of GPIOA_MODER on the STM32F407 is 0xA8000000: PA13, PA14 and PA15 are in alternate-function mode (10), because those pins carry the debug interface (SWDIO, SWCLK and JTDI) after reset; all other pins are inputs. The second value, 0x55000000, is GPIOD with PD12 to PD15 set to output (01). No pointer cast is involved: a 32-bit register value is simply a number, and shifting and masking extracts its fields.

moder_decode.c
#include <stdio.h> #include <stdint.h> static const char *mode_name(unsigned m) { static const char *const names[4] = { “input”, “output”, “alt-func”, “analog” }; return names[m & 3u]; } static void decode_moder(const char *port, uint32_t moder) { printf(“%s MODER = 0x%08X\n”, port, (unsigned)moder); for (unsigned pin = 0; pin < 16; pin++) { unsigned mode = (moder >> (pin * 2u)) & 3u; /* two bits per pin */ if (mode != 0) printf(” P%s%-2u : %s\n”, port + 4, pin, mode_name(mode)); } } int main(void) { decode_moder(“GPIOA”, 0xA8000000u); /* reset value of GPIOA_MODER on the STM32F407 */ decode_moder(“GPIOD”, 0x55000000u); /* PD12..PD15 as outputs: the Discovery LEDs */ printf(“GPIOD pins as 2-bit fields: mask for pin 13 = 0x%08X, output value = 0x%08X\n”, 3u << 26, 1u << 26); return 0; }
Output (verified by compiling and running)
GPIOA MODER = 0xA8000000 PA13 : alt-func PA14 : alt-func PA15 : alt-func GPIOD MODER = 0x55000000 PD12 : output PD13 : output PD14 : output PD15 : output GPIOD pins as 2-bit fields: mask for pin 13 = 0x0C000000, output value = 0x04000000
Reading real registers: In a debugger read the register as a 32-bit word and apply this decoding on paper; writing a small script or helper function for the registers you use often saves time.

Example 8: A Union Overlay of a Register

Problem: Look at a 32-bit value as a word, as two half-words and as four bytes without pointer casts. A union places all members at the same address; type punning through a union is permitted in C99 and later. On a little-endian CPU byte[0] is the least significant byte, so 0xAABBCCDD gives half[0] = 0xCCDD and byte[0] = 0xDD. Writing byte[1] = 0 clears bits 15:8, producing 0xAABB00DD, and writing half[1] replaces the upper 16 bits. On a big-endian machine the same program would print different numbers: the union exposes the byte order of the host, so it is a tool for debugging and for hardware-specific code, not for portable file formats (Module 16 covers unions in depth).

union_overlay.c
#include <stdio.h> #include <stdint.h> typedef union { uint32_t word; uint16_t half[2]; uint8_t byte[4]; } reg_t; int main(void) { reg_t r = { .word = 0xAABBCCDDu }; printf(“word = 0x%08X\n”, (unsigned)r.word); printf(“half[0] = 0x%04X, half[1] = 0x%04X\n”, r.half[0], r.half[1]); printf(“byte[0] = 0x%02X, byte[3] = 0x%02X\n”, r.byte[0], r.byte[3]); r.byte[1] = 0x00; /* change one byte through the overlay */ printf(“after byte[1] = 0: word = 0x%08X\n”, (unsigned)r.word); r.half[1] = 0x1234; printf(“after half[1] = 0x1234: word = 0x%08X\n”, (unsigned)r.word); printf(“sizeof(reg_t) = %zu\n”, sizeof(reg_t)); return 0; }
Output (verified by compiling and running)
word = 0xAABBCCDD half[0] = 0xCCDD, half[1] = 0xAABB byte[0] = 0xDD, byte[3] = 0xAA after byte[1] = 0: word = 0xAABB00DD after half[1] = 0x1234: word = 0x123400DD sizeof(reg_t) = 4
Not for protocols: A union overlay gives host-dependent results. Use shifts and masks (Examples 1, 2 and 5) whenever the byte order must be fixed.

Example 9: Mini Exercises

Exercise 1: What bytes does 0x0A0B0C0D occupy in memory on the Cortex-M4?
0D 0C 0B 0A (little-endian).
Exercise 2: Why does Example 2 build the CRC from bytes instead of casting the buffer to uint16_t *?
The CRC field starts at an arbitrary offset, so the cast could be misaligned; shifts also fix the byte order.
Exercise 3: How do you test whether a pointer is 8-byte aligned?
((uintptr_t)p & 7u) == 0.
Exercise 4: Why does Example 5 cast the signed temperature to uint16_t before shifting?
Right-shifting a negative signed value is implementation-defined; the unsigned cast keeps the bit pattern and gives well-defined shifts.
Exercise 5: What is sign_extend(0x7FF, 12)?
2047, because bit 11 is clear.
Exercise 6: Which GPIO pins have non-input mode in GPIOA_MODER = 0xA8000000?
PA13, PA14 and PA15 (alternate function, the debug pins).
Exercise 7: Why can a union overlay give different results on another CPU?
The bytes of a word are stored in a different order on a big-endian CPU, so the members see different parts of the value.

Key Takeaways from the Pointer Casting Examples in Embedded C

  • These pointer casting examples in embedded C show that shifts and masks are the portable way to read and write multi-byte fields.
  • Check alignment (low address bits) before forming a wider pointer.
  • memcpy is the safe tool for reinterpreting bytes as another type.
  • A wire format should be defined field by field, independent of padding and byte order.
  • Sign extension needs the real field width, not a plain cast.
  • Registers are numbers: extract fields by shifting and masking.

Ready for the quiz?

Next: predict byte-order results, find the alignment and aliasing bugs and answer the interview questions.

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 *