Ground Embedded KO Subscribe

Notes / C Language

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.

GROUND EMBEDDED · NOTEC Language 06Pointers 1volatile uint32_t *ODR 2 = (uint32_t*)0x40020014; 3*ODR |= (1u << 5); 4while (1) { 5 if (flag) toggle(); 6}

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.

0x2000000010x0x200000040x20000000p (address of x)0x20000008?next variable
x holds the value 10 and p holds the address of x. Running *p = 20 changes the value in slot 0x20000000.

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);
  1. Turn the number 0x48000014 into "an address where a 32-bit value lives": (volatile uint32_t *)
  2. Go to that address and work with the value: the leading *
  3. 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 const pointer instead of a whole struct as a function argument?
FREE PDFGet the 3 free ebooksIf you want to keep reading this topic as one book: BLDC motor control, serial communication and sensors at book length. PDFs are in Korean.Get them free →