Pointers: handle addresses and the registers appear
A pointer is a variable that holds an address. Reading and writing a specific address is all there is to register control.
A variable lives somewhere in memory, and that location is its address. A pointer is a variable whose value is that address.
uint32_t x = 10;
uint32_t *p = &x; // address of x
*p = 20; // follow the address and change the value; x is now 20
How this relates to register control
An MCU's peripheral registers sit at fixed addresses. If the datasheet says the GPIOA output register is at 0x40020014, turn that address into a pointer, write through it, and the pin moves.
#define GPIOA_ODR (*(volatile uint32_t *)0x40020014)
GPIOA_ODR |= (1u << 5); // PA5 HIGH
An expression like GPIOA->ODR on STM32 is, in the end, a struct pointer aimed at this same address.
Common mistakes
- Following a pointer that was never initialised
- Using the address of a local variable that no longer exists
- Using the wrong pointer type size and touching the neighbouring register
In pictures: address and value
A variable sits in some slot of memory, and a pointer is another variable that holds the number of that slot.
Reading p gives the address, and reading *p gives the value at that address. The single asterisk means "go to that address".
Pointer arithmetic moves by the type size
p + 1 does not add 1 to the address. It steps over one element of the type.
uint8_t *b = (uint8_t *)0x20000000;
uint32_t *w = (uint32_t *)0x20000000;
b + 1; // 0x20000001
w + 1; // 0x20000004 (steps over 4 bytes)
This is why communication buffers are taken as uint8_t *. Moving one byte at a time is what lets you read a packet in order.
Unpacking the one line that writes a register
#define GPIOA_ODR (*(volatile uint32_t *)0x48000014)
GPIOA_ODR |= (1u << 5);
- Turn the number
0x48000014into "an address where a 32-bit value lives":(volatile uint32_t *) - Go to that address and work with the value: the leading
* - Set only bit 5 to 1:
|= (1u << 5)
Without volatile, the compiler may decide "this was just written, no need to read it again" and remove the read or the write.
Problems that come up in real work
| Symptom | Cause | How to check |
|---|---|---|
| Stops in a hard fault | Followed a NULL or uninitialised pointer | The fault address register shows a value near 0 |
| A value is occasionally corrupted | Returned the address of a local variable after the function ended | Happens only when the stack is overwritten |
| A neighbouring variable changes | Wrote through a pointer past the end of an array | Check the neighbours in the map file |
| Alignment fault | Read an odd address as uint32_t * |
Especially common on Cortex-M0 |
Initialising pointers to NULL when declaring them and checking if (p != NULL) before use removes most cases of the first problem.
Check yourself
- For
uint16_t arr[4], what is the value of&arr[2] - &arr[0], and how many bytes apart are the addresses? - What is saved by passing a
constpointer instead of a whole struct as a function argument?