포인터: 주소를 다루면 레지스터가 보인다
포인터는 주소를 담는 변수입니다. 특정 주소를 읽고 쓰는 것이 레지스터 제어의 전부라는 점을 정리했습니다.
변수는 메모리 어딘가에 있고, 그 위치가 주소입니다. 포인터는 그 주소를 값으로 가진 변수입니다.
uint32_t x = 10;
uint32_t *p = &x; // x의 주소
*p = 20; // 주소를 따라가서 값을 바꿈, 이제 x는 20
레지스터 제어와의 관계
MCU의 주변장치 레지스터는 정해진 주소에 있습니다. 데이터시트에 GPIOA의 출력 레지스터가 0x40020014라고 적혀 있다면, 그 주소를 포인터로 만들어 쓰면 핀이 움직입니다.
#define GPIOA_ODR (*(volatile uint32_t *)0x40020014)
GPIOA_ODR |= (1u << 5); // PA5 HIGH
STM32의 GPIOA->ODR 같은 표현도 결국 구조체 포인터로 이 주소를 가리키는 것입니다.
자주 하는 실수
- 초기화하지 않은 포인터를 따라가기
- 이미 사라진 지역 변수의 주소를 쓰기
- 포인터 자료형 크기를 잘못 써서 옆 레지스터까지 건드리기
그림으로 보기: 주소와 값
변수는 메모리의 어느 칸에 놓이고, 포인터는 그 칸의 번호를 담는 또 하나의 변수입니다.
p를 읽으면 주소가 나오고, *p를 읽으면 그 주소에 있는 값이 나옵니다. 별표 하나가 "그 주소로 가서"라는 뜻입니다.
포인터 연산은 자료형 크기만큼 움직인다
p + 1은 주소에 1을 더하는 것이 아니라 자료형 하나만큼 건너뜁니다.
uint8_t *b = (uint8_t *)0x20000000;
uint32_t *w = (uint32_t *)0x20000000;
b + 1; // 0x20000001
w + 1; // 0x20000004 (4바이트 건너뜀)
통신 버퍼를 uint8_t *로 받는 이유가 여기에 있습니다. 바이트 단위로 한 칸씩 움직여야 패킷을 순서대로 읽을 수 있습니다.
레지스터에 쓰는 한 줄을 풀어 보기
#define GPIOA_ODR (*(volatile uint32_t *)0x48000014)
GPIOA_ODR |= (1u << 5);
0x48000014라는 숫자를 "32비트 값이 있는 주소"로 바꿉니다.(volatile uint32_t *)- 그 주소로 가서 값을 다룹니다. 맨 앞의
* - 5번 비트만 1로 만듭니다.
|= (1u << 5)
volatile이 빠지면 컴파일러가 "방금 쓴 값이니 다시 읽을 필요 없다"고 판단해 읽기나 쓰기를 지워 버릴 수 있습니다.
현업에서 자주 터지는 문제
| 증상 | 원인 | 확인 방법 |
|---|---|---|
| 하드폴트로 멈춤 | NULL이나 초기화하지 않은 포인터를 따라감 | 폴트 주소 레지스터에 0 근처 값이 찍힘 |
| 값이 가끔 깨짐 | 함수가 끝난 지역 변수의 주소를 돌려줌 | 스택이 덮어써지는 시점에만 발생 |
| 옆 변수가 바뀜 | 배열 끝을 넘어 포인터로 씀 | 맵 파일에서 이웃 변수 확인 |
| 정렬 폴트 | 홀수 주소를 uint32_t *로 읽음 |
Cortex-M0에서 특히 잘 발생 |
포인터를 선언할 때 NULL로 초기화하고, 쓰기 전에 if (p != NULL)로 확인하는 습관만으로 첫 번째 문제는 대부분 사라집니다.
스스로 확인
uint16_t arr[4]에서&arr[2] - &arr[0]의 값은 무엇이고, 주소 차이는 몇 바이트일까요?- 함수 인자로 구조체 전체 대신
const포인터를 넘기면 무엇이 절약될까요?
영상으로 이어 보기공부법과 진로 시리즈 15편2025 임베디드 프로그래밍 공부 순서 | 공부 방법 | 로드맵PROJECT COURSEC 문법을 실제 펌웨어 코드로 써 보고 싶다면밸런싱 로봇 하나를 요구사항, 회로와 PCB, 펌웨어, 앱까지 실무 순서대로 만드는 강의입니다. 10파트 165강.강의 살펴보기 →FREE PDF무료 전자책 3권 받기이 주제를 한 권으로 이어서 읽고 싶다면. BLDC 모터 제어, 직렬통신, 센서를 책 분량으로 묶었습니다. 이메일만 남기면 바로 받습니다.무료로 받기 →