Pointer Examples in Embedded C: Clamping, Byte Dumps, Register Blocks, Lookups and Frame Decoding-Free Embedded Course Online

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.1 · EXAMPLES

Pointer Examples in Embedded C: Clamping, Byte Dumps, Register Blocks, Lookups and Frame Decoding

Eight worked examples with real output: modifying variables through pointers, byte dumps and endianness, a simulated GPIO block, lookups returning pointers or NULL, in-place updates, frame decoding with a cursor, range checks and output pointers. Study this one of the pointer examples in embedded C closely.

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

Pointer Examples in Embedded C: What This Page Covers

These pointer examples in embedded C apply Lesson 9.1: modifying variables through pointers, looking at the bytes of a value, a simulated register block reached by a base pointer, functions that return a pointer or NULL, in-place updates through a pointer to an element, decoding a frame with a cursor and range checks with pointer comparison. Every program was compiled with GCC and run; the output is real. These pointer examples in embedded C build on each other.

#ExampleSkill
1Changing variables through pointers& and *
2Dumping the bytes of a valuePointer types, endianness
3A simulated GPIO register blockBase pointer
4Find an element: pointer or NULLReturn value design
5Update the maximum in placePointer to element
6Decoding a frame with a cursorByte pointer
7Is this address inside my buffer?Pointer comparison
8Statistics through output pointersSeveral results
9Mini exercisesSelf-test

Example 1: Changing Variables through Pointers

Problem: Limit three unrelated variables to the range 0–100. clamp takes the address of the variable and rewrites it through *v, so one function serves every variable. The array list holds three addresses; list[i] is a pointer and *list[i] is the variable it points to. After the loop the original variables duty, temp and ok have the new values: 100, 0 and 60. Among the pointer examples in embedded C this pattern is very common.

clamp_ptr.c
#include <stdio.h> #include <stdint.h> static void clamp(int *v, int lo, int hi) { if (*v < lo) *v = lo; if (*v > hi) *v = hi; } int main(void) { int duty = 130; /* PWM duty in percent, must be 0..100 */ int temp = –5; int ok = 60; int *list[3] = { &duty, &temp, &ok }; /* an array of pointers to ints */ for (int i = 0; i < 3; i++) { printf(“before: %4d “, *list[i]); clamp(list[i], 0, 100); printf(“after: %3d\n”, *list[i]); } printf(“duty=%d temp=%d ok=%d\n”, duty, temp, ok); return 0; }
Output (verified by compiling and running)
before: 130 after: 100 before: -5 after: 0 before: 60 after: 60 duty=100 temp=0 ok=60
Read it aloud: “list of i is a pointer; star list of i is the int it points to.” Working outward from the name keeps pointer expressions readable.

Example 2: Dumping the Bytes of a Value

Problem: Show exactly how values are stored. A pointer to uint8_t (or unsigned char) may legally examine the bytes of any object. The 32-bit value 0x11223344 appears as 44 33 22 11 and 0xABCD as CD AB: the least significant byte comes first, which is little-endian, as on the Cortex-M4. The float 1.0 is stored as the IEEE-754 pattern 0x3F800000, again least significant byte first. The string stores its characters in order followed by the terminating zero. The last lines use a byte pointer to detect the byte order: if the first byte of the value 1 is 1, the machine is little-endian. Keep these pointer examples in embedded C for revision.

byte_dump.c
#include <stdio.h> #include <stdint.h> #include <string.h> static void dump(const char *label, const void *obj, size_t n) { const uint8_t *b = obj; /* look at any object as bytes */ printf(“%-14s”, label); for (size_t i = 0; i < n; i++) printf(” %02X”, b[i]); printf(“\n”); } int main(void) { uint32_t id = 0x11223344u; uint16_t port = 0xABCD; float f = 1.0f; char txt[] = “STM”; dump(“uint32 11223344”, &id, sizeof id); dump(“uint16 ABCD”, &port, sizeof port); dump(“float 1.0”, &f, sizeof f); dump(“\”STM\””, txt, sizeof txt); uint16_t one = 1; printf(“this machine is %s-endian\n”, (*(uint8_t *)&one == 1) ? “little” : “big”); return 0; }
Output (verified by compiling and running)
uint32 11223344 44 33 22 11 uint16 ABCD CD AB float 1.0 00 00 80 3F “STM” 53 54 4D 00 this machine is little-endian
Why it matters: Protocols such as CAN, Modbus or TCP often use big-endian fields (most significant byte first). Building them from a little-endian value needs explicit shifts, never a cast of the buffer address (Lesson 9.5).

Example 3: A Simulated GPIO Register Block

Problem: Reach the registers of a port through one base pointer instead of one macro per register. The structure lists the registers in the same order and with the same 4-byte spacing as the hardware, so port->ODR is the address base + 0x14. The helpers take the port as a pointer and work for GPIOA to GPIOI. Here sim_gpiod is an ordinary variable so the program runs on a PC; on the STM32F407 the pointer is (gpio_t *)0x40020C00 (CMSIS: GPIOD). MODER uses two bits per pin (01 = output), so PD12 to PD15 give 0x55000000. BSRR is write-only on hardware: the lower 16 bits set pins, the upper 16 bits reset them, so resetting PD12 writes bit 28, and the value read back in this simulation is just the last value written.

gpio_ptr.c
#include <stdio.h> #include <stdint.h> /* a simulated GPIO block with the real STM32F407 register order and offsets */ typedef struct { volatile uint32_t MODER; /* 0x00 */ volatile uint32_t OTYPER; /* 0x04 */ volatile uint32_t OSPEEDR; /* 0x08 */ volatile uint32_t PUPDR; /* 0x0C */ volatile uint32_t IDR; /* 0x10 */ volatile uint32_t ODR; /* 0x14 */ volatile uint32_t BSRR; /* 0x18 */ } gpio_t; static gpio_t sim_gpiod; /* stands in for the block at 0x40020C00 */ static void pin_output(gpio_t *port, unsigned pin) { port->MODER &= ~(3u << (pin * 2)); port->MODER |= (1u << (pin * 2)); /* 01 = general purpose output */ } static void pin_set(gpio_t *port, unsigned pin) { port->BSRR = 1u << pin; } static void pin_reset(gpio_t *port, unsigned pin) { port->BSRR = 1u << (pin + 16); } int main(void) { gpio_t *GPIOD = &sim_gpiod; /* on the board: (gpio_t *)0x40020C00 */ for (unsigned pin = 12; pin <= 15; pin++) pin_output(GPIOD, pin); printf(“MODER = 0x%08X (PD12..PD15 outputs)\n”, (unsigned)GPIOD->MODER); pin_set(GPIOD, 12); pin_reset(GPIOD, 12); printf(“BSRR = 0x%08X (last write: reset bit for PD12 = bit 28)\n”, (unsigned)GPIOD->BSRR); printf(“offset of ODR = 0x%02X\n”, (unsigned)__builtin_offsetof(gpio_t, ODR)); printf(“offset of BSRR = 0x%02X\n”, (unsigned)__builtin_offsetof(gpio_t, BSRR)); return 0; }
Output (verified by compiling and running)
MODER = 0x55000000 (PD12..PD15 outputs) BSRR = 0x10000000 (last write: reset bit for PD12 = bit 28) offset of ODR = 0x14 offset of BSRR = 0x18
Simulation only: On the real chip BSRR reads as 0. This program stores what was written, which a real register does not; do not rely on reading BSRR.

Example 4: Find an Element, Return a Pointer or NULL

Problem: Look up a record by name and let the caller modify it. Returning a pointer into the array avoids copying and lets the caller change the element; returning NULL signals “not found”. The caller must test the result before using it. The change of the MOTOR limit made through the returned pointer is visible on the next lookup, because both lookups return the same element.

find_ptr.c
#include <stdio.h> #include <stddef.h> #include <string.h> typedef struct { const char *name; int limit_ma; } channel_t; static channel_t channels[] = { {“LED_G”, 20}, {“LED_R”, 20}, {“MOTOR”, 500}, {“BUZZER”, 80} }; #define N (sizeof channels / sizeof channels[0]) static channel_t *find_channel(const char *name) /* returns a pointer into the array, or NULL */ { for (size_t i = 0; i < N; i++) if (strcmp(channels[i].name, name) == 0) return &channels[i]; return NULL; } int main(void) { channel_t *m = find_channel(“MOTOR”); if (m) { printf(“found %s, limit %d mA\n”, m->name, m->limit_ma); m->limit_ma = 400; } m = find_channel(“MOTOR”); if (m) printf(“MOTOR limit is now %d mA (changed through the pointer)\n”, m->limit_ma); channel_t *x = find_channel(“SERVO”); printf(“SERVO: %s\n”, x ? “found” : “not found (NULL)”); return 0; }
Output (verified by compiling and running)
found MOTOR, limit 500 mA MOTOR limit is now 400 mA (changed through the pointer) SERVO: not found (NULL)
API rule: Document that the returned pointer refers to the array element and stays valid only as long as the array does. Never return a pointer to a local variable.

Example 5: Update the Maximum in Place

Problem: Find the largest ADC sample and cap it without searching twice. max_element returns a pointer to the best element, so the caller can read it (*peak), compute its index (peak - adc, subtraction of two pointers into the same array gives an element count) and write to it (*peak = 4000). An empty array has no maximum and the function returns NULL. This is exactly the interface of the C++ std::max_element, written in C.

max_ptr.c
#include <stdio.h> #include <stddef.h> static int *max_element(int *a, size_t n) { if (n == 0) return NULL; int *best = &a[0]; for (size_t i = 1; i < n; i++) if (a[i] > *best) best = &a[i]; return best; } static void show(const int *a, size_t n) { for (size_t i = 0; i < n; i++) printf(” %d”, a[i]); printf(“\n”); } int main(void) { int adc[] = { 512, 1900, 733, 4095, 20 }; size_t n = sizeof adc / sizeof adc[0]; printf(“before :”); show(adc, n); int *peak = max_element(adc, n); printf(“peak value %d at index %d\n”, *peak, (int)(peak – adc)); *peak = 4000; /* limit the spike, in place */ printf(“after :”); show(adc, n); printf(“empty array -> %s\n”, max_element(adc, 0) ? “element” : “NULL”); return 0; }
Output (verified by compiling and running)
before : 512 1900 733 4095 20 peak value 4095 at index 3 after : 512 1900 733 4000 20 empty array -> NULL
Pointer difference: peak - adc is an integer count of elements (type ptrdiff_t), not bytes. It is defined only for two pointers into the same array.

Example 6: Decoding a Frame with a Cursor

Problem: Parse a UART frame (start byte, length, payload, checksum) safely. A const uint8_t * cursor walks the buffer; *p++ reads the current byte and advances. Before every read the code checks the distance to the end of the buffer (end - p), so a truncated frame returns an error instead of reading beyond the array. The checksum is the 8-bit sum of the payload: 0x10 + 0x20 + 0x30 = 0x60. The program prints a different negative code for each failure, which makes the diagnosis easy.

frame_cursor.c
#include <stdio.h> #include <stdint.h> #include <stddef.h> /* frame: 0xAA | length | payload[length] | checksum (sum of payload bytes) */ static int decode(const uint8_t *buf, size_t size) { const uint8_t *p = buf; /* cursor */ const uint8_t *end = buf + size; if (p >= end || *p++ != 0xAA) return –1; /* start byte */ if (p >= end) return –2; uint8_t len = *p++; /* length field */ if ((size_t)(end – p) < (size_t)len + 1) return –3; /* payload and checksum present? */ uint8_t sum = 0; printf(” payload:”); for (uint8_t i = 0; i < len; i++) { printf(” %02X”, *p); sum = (uint8_t)(sum + *p); p++; } printf(“\n”); if (*p != sum) { printf(” checksum 0x%02X, expected 0x%02X\n”, *p, sum); return –4; } return len; } int main(void) { uint8_t good[] = { 0xAA, 3, 0x10, 0x20, 0x30, 0x60 }; uint8_t badsum[] = { 0xAA, 3, 0x10, 0x20, 0x30, 0x61 }; uint8_t cut[] = { 0xAA, 5, 0x01, 0x02 }; uint8_t nostart[] = { 0x55, 1, 0x01, 0x01 }; printf(“good -> %d\n”, decode(good, sizeof good)); printf(“badsum -> %d\n”, decode(badsum, sizeof badsum)); printf(“cut -> %d\n”, decode(cut, sizeof cut)); printf(“nostart -> %d\n”, decode(nostart, sizeof nostart)); return 0; }
Output (verified by compiling and running)
payload: 10 20 30 good -> 3 payload: 10 20 30 checksum 0x61, expected 0x60 badsum -> -4 cut -> -3 nostart -> -1
Safety: Never trust the length byte of received data. Check it against the bytes that are actually available, as (end - p) < len + 1 does.

Example 7: Is This Address Inside My Buffer?

Problem: A driver receives a pointer and must be sure it refers to an element of its own buffer. Pointers into the same array can be compared with < and >=. The pointer one past the end (rx_buf + 16) is valid to form and compare but must not be dereferenced; the test therefore uses <, not <=. Comparing with an unrelated variable is technically outside the rules of ISO C (the result of relational comparison is unspecified for different objects), though it works on flat-memory machines such as the Cortex-M4 and is common in embedded code, for example when checking that a pointer lies within the SRAM range.

range_check.c
#include <stdio.h> #include <stdint.h> #include <stddef.h> static uint8_t rx_buf[16]; static int in_buffer(const uint8_t *p) { return p >= rx_buf && p < rx_buf + sizeof rx_buf; /* valid element address */ } int main(void) { uint8_t other; const uint8_t *tests[] = { &rx_buf[0], &rx_buf[15], rx_buf + 16, rx_buf + 7, &other }; const char *names[] = { “first element”, “last element”, “one past the end”, “middle”, “unrelated variable” }; for (int i = 0; i < 5; i++) printf(“%-20s -> %s\n”, names[i], in_buffer(tests[i]) ? “inside” : “outside”); printf(“distance last-first = %d elements\n”, (int)(&rx_buf[15] – &rx_buf[0])); return 0; }
Output (verified by compiling and running)
first element -> inside last element -> inside one past the end -> outside middle -> inside unrelated variable -> outside distance last-first = 15 elements
Portable alternative: Convert to uintptr_t before comparing addresses of unrelated objects; the comparison is then of plain integers.

Example 8: Several Results through Output Pointers

Problem: Return the minimum, maximum and mean of a sample window. A C function returns one value, so the three results travel through pointer parameters and the return value reports success. Each output pointer is optional: passing NULL means “I do not need this one”, which the function honours by testing before writing. The mean uses a 32-bit sum so that five values of a 16-bit type cannot overflow. The integer division truncates: (215+223+198+240+231)/5 = 1107/5 = 221.

stats_ptr.c
#include <stdio.h> #include <stdint.h> #include <stddef.h> static int stats(const int16_t *s, size_t n, int16_t *min, int16_t *max, int32_t *avg) { if (s == NULL || n == 0) return –1; int32_t sum = 0; int16_t lo = s[0], hi = s[0]; for (size_t i = 0; i < n; i++) { if (s[i] < lo) lo = s[i]; if (s[i] > hi) hi = s[i]; sum += s[i]; } if (min) *min = lo; /* every output is optional */ if (max) *max = hi; if (avg) *avg = sum / (int32_t)n; return 0; } int main(void) { int16_t temps[] = { 215, 223, 198, 240, 231 }; /* tenths of a degree */ int16_t lo, hi; int32_t mean; if (stats(temps, 5, &lo, &hi, &mean) == 0) printf(“min=%d max=%d avg=%ld (tenths of a degree)\n”, lo, hi, (long)mean); int16_t only_max; stats(temps, 5, NULL, &only_max, NULL); /* ask for one result only */ printf(“only max requested: %d\n”, only_max); printf(“empty input -> %d\n”, stats(temps, 0, &lo, &hi, &mean)); return 0; }
Output (verified by compiling and running)
min=198 max=240 avg=221 (tenths of a degree) only max requested: 240 empty input -> -1
Convention: Output pointers are written only when the function succeeds. Callers should not use them after a failure.

Example 9: Mini Exercises

Exercise 1: What is printed by int a = 4; int *p = &a; *p += 6; printf("%d", a);?
10. The addition is done on a through p.
Exercise 2: What is the byte order of 0x11223344 in memory on the Cortex-M4?
44 33 22 11 (little-endian).
Exercise 3: Why does Example 3 use a gpio_t * instead of one macro per register?
One base pointer reaches every register by member name, helper functions work for any port, and the structure documents the register layout.
Exercise 4: What does max_element return for an empty array, and why?
NULL, because there is no element to point to; the caller must check.
Exercise 5: What does end - p mean in Example 6?
The number of bytes (elements of uint8_t) remaining between the cursor and the end.
Exercise 6: Why is rx_buf + 16 valid to form but not to dereference?
ISO C allows a pointer one past the end of an array for comparisons and loop limits; there is no object to read there.
Exercise 7: Why make the output pointers of stats optional?
The caller can ask for only the values it needs, and the function avoids writing through NULL.

Key Takeaways from the Pointer Examples in Embedded C

  • These pointer examples in embedded C show that & and * let one function work on any variable.
  • A byte pointer can inspect any object and exposes the byte order.
  • A base pointer to a register structure reaches every register of a peripheral by name.
  • A function may return a pointer into an array, with NULL meaning not found.
  • A cursor pointer plus an end pointer gives safe, bounds-checked parsing.
  • Pointer subtraction yields element counts and comparison checks ranges.

Ready for the quiz?

Next: predict pointer output, find the bugs and answer the pointer 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 *