C Programming 3 min read

Pointers

A pointer holds an address. &x takes the address of x, and *p reads or writes what p points at. Adding 1 to a pointer moves it by one element, not one byte. In firmware, a pointer to a fixed address is how C reaches a hardware register.

Three uint16_t array elements in memory at illustrative addresses: samples[0] at 0x2000_0000 holds 150 after being written through p; samples[1] at 0x2000_0002 holds 200 and is where p points after p++; samples[2] at 0x2000_0004 holds 300. Each element is 2 bytes, so p++ moves the address by 2.
p++ moves a uint16_t pointer by one element: 2 bytes. Addresses are illustrative.

Addresses, & and *

Every variable sits at an address in memory. &x gives that address, and a pointer variable such as int *px = &x; stores it. Writing *px follows the pointer: *px += 1 changes x itself. This is how a function changes its caller's data, since arguments are otherwise copies.

Pointer arithmetic

A pointer's type says what it points at, and arithmetic counts in elements of that type. If p points at a uint16_t, p++ adds 2 to the address, and p[i] means *(p + i). That is why array indexing and pointers are so closely related. A pointer that holds NULL points at nothing, and reading through it is undefined behaviour.

pointers.cC
#include <stddef.h>#include <stdint.h>#include <stdio.h>int main(void) {    int x = 7;    int *px = &x;                   /* px holds the address of x */    *px += 1;                       /* change x through px */    printf("x = %d\n", x);    uint16_t samples[3] = {100, 200, 300};    uint16_t *p = samples;          /* points at samples[0] */    *p = 150;    p++;                            /* moves one uint16_t: 2 bytes */    printf("*p = %u\n", (unsigned)*p);    printf("step = %u bytes\n", (unsigned)((char *)p - (char *)samples));    printf("samples[0] = %u\n", (unsigned)samples[0]);    int *none = NULL;    if (none == NULL) {        puts("none is NULL: never dereference it");    }    return 0;}
Output
x = 8
*p = 200
step = 2 bytes
samples[0] = 150
none is NULL: never dereference it

A register at a fixed address

Peripheral registers sit at addresses given in the chip's reference manual. Casting that number to a pointer and dereferencing it reads or writes the register. volatile tells the compiler that every access matters; a later insight, volatile and const, explains why.

led_reg.cC
#include <stdint.h>/* Example address: take the real one from your chip's reference manual. */#define GPIO_OUT (*(volatile uint32_t *)0x40020014u)void led_on(void) {    GPIO_OUT |= 1u << 5;            /* set bit 5 of the output register */}