1편에서는 GPIO의 구조와 내부 블록 다이어그램, 데이터시트 읽는 법까지 다뤘다. 마지막에는 아래 코드로 User Button을 눌렀을 때 LED가 켜지는 실습도 해봤는데,
사실 이 코드가 내부에서 뭘 하는지는 설명 안 하고 넘어갔다. HAL_GPIO_ReadPin()이 IDR을 읽는다는 건 알겠는데, 어떤 방식으로? HAL_GPIO_WritePin()은 왜 ODR 대신 BSRR을 쓰는 건지, 그리고 그게 왜 중요한지.
HAL 소스 어떻게 보나
CubeIDE에서 함수에 커서를 올리고 F3 누르면 해당 함수 정의로 바로 이동한다. 또는 함수명에서 Ctrl + 클릭.
파일 위치는 프로젝트마다 다르지만 보통 이쪽이다.
여기가 HAL GPIO 관련 로직이 다 모여있는 곳이다.
HAL_GPIO_ReadPin() 내부
1편에서 쓴 코드:
| |
F3 눌러서 소스 열어보면 이렇다.

함수 본문은 생각보다 단순하다. IDR(Input Data Register) 에서 해당 핀 비트를 읽어서 반환하는 게 전부다.
반환 타입인 GPIO_PinState는 stm32f4xx_hal_gpio.h에 아래처럼 정의돼 있다.

GPIOx->IDR & GPIO_Pin은 IDR 레지스터에서 특정 핀 비트만 마스킹해서 읽는 것이다. PC13이라면 GPIOC->IDR & (1 << 13) 이 된다.
그래서 1편에서 == GPIO_PIN_RESET으로 버튼 눌림을 판단한 게, 실제로는 IDR의 13번 비트가 0인지 확인하는 거다. Active Low 버튼이니까 눌렸을 때 핀이 GND로 당겨지고 → IDR 비트가 0 → GPIO_PIN_RESET 반환.
HAL_GPIO_WritePin() 내부
| |
소스를 열면 ODR이 아닌 BSRR(Bit Set/Reset Register) 을 쓰고 있다.

Set할 때는 GPIO_Pin(= 1 << 5 = 0x0020)을 그대로 쓰고, Reset할 때는 GPIO_Pin << 16(= 0x00200000)을 써서 상위 16비트에 넣는다.
BSRR은 32비트 레지스터인데 상위/하위 16비트의 역할이 다르다.
| 동작 | BSRR에 쓰는 값 | 예시 (PA5) |
|---|---|---|
| PA5 HIGH | GPIO_Pin | 0x00000020 |
| PA5 LOW | GPIO_Pin << 16 | 0x00200000 |
ODR은 “핀 상태 전체를 이 값으로 덮어써라"이고, BSRR은 “이 핀만 Set해라” 또는 “이 핀만 Reset해라"다. 건드리지 않을 핀은 0을 쓰면 그냥 무시된다.
왜 HAL은 ODR 대신 BSRR을 쓰나
ODR로 특정 핀만 바꾸려면 이렇게 된다.
| |
겉으로는 한 줄이지만 실제로는 세 단계다.
문제는 ①과 ③ 사이에 인터럽트가 끼어드는 경우다. 메인 루프에서 PA5를 SET하려는 도중, 인터럽트 핸들러가 PA6을 RESET했다고 하면:
인터럽트에서 바꾼 PA6 상태가 날아간다. 이게 Race Condition이다.
BSRR은 Write 한 번으로 끝난다. 하드웨어가 해당 비트만 건드리기 때문에 Read → Modify → Write 과정 자체가 없다. 인터럽트가 끼어들 틈이 없다는 뜻이다. 이게 Atomic한 이유고, HAL이 BSRR을 쓰는 이유다.
| 방식 | 동작 단계 | 인터럽트 안전 |
|---|---|---|
ODR |= | Read → Modify → Write | ❌ 위험 |
BSRR = | Write | ✅ 안전 (Atomic) |
추후에 저런 Critical Section을 어떻게 관리해야 하는지도 따로 글을 써보려고 한다.
레지스터 직접 접근 버전
HAL 없이 똑같이 동작하는 코드:
HAL은 결국 이걸 함수로 감싼 것뿐이다. 이걸 알고 쓰는 것과 모르고 쓰는 건, 뭔가 이상하게 동작할 때 어디서부터 파고들지가 달라진다.
HAL로 간단한 동작을 구현하는 데 있어서는 충분하다. 함수 이름만 봐도 뭘 하는지 알 수 있고, 포팅도 쉽다.
하지만 막상 뭔가 원하는 대로 안 동작할 때 얘기가 달라진다. 핀이 왜 안 켜지는지, 인터럽트에서 왜 상태가 깨지는지 — 이걸 파고들려면 결국 IDR이 뭘 읽고 있는지, BSRR에 어떤 값이 들어가고 있는지를 봐야 한다. HAL 함수 이름만 알고 있으면 거기서 막히게 된다.
레지스터 수준을 알고 쓰는 것과 모르고 쓰는 건, 코드가 잘 돌아갈 때는 차이가 없다. 차이는 안 돌아갈 때 난다.