1편에서는 GPIO의 구조와 내부 블록 다이어그램, 데이터시트 읽는 법까지 다뤘다. 마지막에는 아래 코드로 User Button을 눌렀을 때 LED가 켜지는 실습도 해봤는데,
사실 이 코드가 내부에서 뭘 하는지는 설명 안 하고 넘어갔다. HAL_GPIO_ReadPin()이 IDR을 읽는다는 건 알겠는데, 어떤 방식으로? HAL_GPIO_WritePin()은 왜 ODR 대신 BSRR을 쓰는 건지, 그리고 그게 왜 중요한지.
이번 편에서는 그 안을 직접 열어볼 거다. HAL 소스 코드를 뜯으면서 레지스터 레벨까지 내려가 보고, 마지막엔 HAL 없이 레지스터만으로 똑같이 구현해볼 거다.
HAL 소스 어떻게 보나
CubeIDE에서 함수에 커서 올리고 F3 누르면 해당 함수 정의로 바로 이동한다. 또는 함수명에서 Ctrl + 클릭.
파일 위치는 프로젝트마다 다르지만 보통 이쪽이다.
여기가 HAL GPIO 관련 로직이 다 모여있는 곳이다. 이걸 직접 열어서 1편 코드와 대조해보는 게 이번 포스트 목표다.
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 함수 이름만 알고 있으면 그 아래서 멈추게 된다.
레지스터 수준을 알고 쓰는 것과 모르고 쓰는 건, 코드가 잘 돌아갈 때는 차이가 없다. 차이는 안 돌아갈 때 난다.
다음 글에서는 EXTI(External Interrupt) 를 통해 GPIO 입력을 인터럽트 방식으로 처리하는 방법을 정리할 예정이다. 폴링 방식과 어떻게 다른지, NVIC 설정은 어떻게 연결되는지까지 이어서 다룰 생각이다.