23. HAL 추상화·모듈 분리·이식성
레지스터 코드(GPIOC->BRR=...)가 응용 로직 곳곳에 박혀 있으면 칩을 바꿀 때 모든 곳을 고쳐야 합니다. HAL(Hardware Abstraction Layer)은 "무엇을 한다"(LED를 켠다)는 응용과 "어떻게 한다"(특정 레지스터를 쓴다)는 구현을 갈라놓습니다. 같은 app.c 를 세 HAL 구현 — PC(printf)·STM32(레지스터)·아두이노 우노(AVR ATmega328P) — 로 돌려, 아키텍처가 ARM에서 AVR로 바뀌어도 응용 코드는 한 줄도 안 바뀜을 봅니다.
이 강의에서 배우는 것
- 1펌웨어를 응용/HAL/하드웨어 계층으로 나누는 이유를 설명한다
- 2인터페이스(헤더)와 구현(소스)을 분리한다
- 3같은 응용을 여러 HAL 구현으로 이식한다
- 4의존 방향(응용→HAL, 역방향 금지)을 지킨다
- 5PC용 가짜 HAL로 응용 로직을 단위 테스트한다
소개
칩을 STM32에서 8051로 바꾸거나 같은 동작을 PC에서 테스트하고 싶을 때, 레지스터 코드가 응용에 박혀 있으면 전부 고쳐야 합니다. HAL은 응용과 구현을 갈라 이 문제를 풉니다.
핵심 개념
1) 계층화와 의존 방향
응용(app.c) "패턴대로 LED 제어" — 하드웨어 모름
↓ (호출)
HAL 인터페이스(hal.h) "hal_led_set(on)" 같은 계약
↓
HAL 구현 hal_pc.c / hal_stm32.c / hal_arduino.c
↓
하드웨어 PC stdout / STM32 GPIO / AVR GPIO의존 방향: 응용 → HAL은 허용, HAL → 응용은 금지. 아래가 위를 알면 분리가 깨져 재사용할 수 없습니다.
2) 이식성 — 구현만 교체
PC 테스트: pc_test.c + app.c + hal_pc.c (printf)
STM32: main.c + app.c + hal_stm32.c (ARM, PC13)
우노: main + app.c + hal_arduino.c (AVR, PB5/D13)
/* app.c 는 셋 모두에서 글자 단위로 동일하다 */3) AVR(우노) 레지스터 매핑
| 동작 | AVR 레지스터 | STM32 대응 |
|---|---|---|
| 출력으로 | DDRB |= (1<<5) | GPIOC->CRH 모드 |
| 켜기(High) | PORTB |= (1<<5) | GPIOC->BSRR/BRR |
| 끄기(Low) | PORTB &= ~(1<<5) | GPIOC->BSRR/BRR |
DDRx(방향)·PORTx(출력)·PINx(입력)가 AVR GPIO의 3대 레지스터. digitalWrite 같은 API 대신 직접 쓰면 레지스터 직접 제어 철학을 AVR에서도 유지합니다. (우노는 AVR이라 Keil로는 빌드 불가 — avr-gcc/Arduino IDE 사용)
핵심 예제
/* hal.h — 계약 (응용은 이것만 의존) */
void hal_led_init(void);
void hal_led_set(uint8_t on); /* 1=켜기, 0=끄기 */
/* app.c — 하드웨어 비의존. 레지스터 코드가 한 줄도 없다 */
void app_blink_pattern(const uint8_t *p, uint16_t len) {
uint16_t i; hal_led_init();
for (i = 0; i < len; i++) hal_led_set(p[i] ? 1u : 0u);
}/* 같은 hal.h 를 세 가지로 구현 */
void hal_led_set(uint8_t on){ printf("LED %s\n", on?"ON":"OFF"); } /* PC */
void hal_led_set(uint8_t on){ if(on)GPIOC->BRR=(1u<<13);else GPIOC->BSRR=(1u<<13);} /* STM32 */
void hal_led_set(uint8_t on){ if(on)PORTB|=(1u<<5);else PORTB&=~(1u<<5);} /* 우노 AVR */검증: gcc pc_test.c app.c hal_pc.c -o pc_test → 패턴대로 LED ON/OFF 출력. Keil이면 hal_pc.c 대신 hal_stm32.c 를 넣으면 같은 app.c 가 PC13을 제어합니다.
자주 하는 실수
Q. HAL을 만들었는데 응용에 여전히 레지스터 코드가 남아 있어요.
A. 레지스터 접근은 전부 HAL 구현으로 내려야 합니다. app.c 에 GPIOC->... 가 한 줄이라도 있으면 이식성이 깨집니다.
Q. 여러 구현을 같이 링크하면 에러가 나요.
A. 같은 함수가 중복 정의됩니다. 한 번에 하나만 링크하세요(PC면 hal_pc.c, STM32면 hal_stm32.c, 우노면 hal_arduino.c).
Q. 추상화를 너무 많이 했더니 느리고 복잡해요.
A. 추상화는 공짜가 아닙니다. 자주 바뀌거나 이식·테스트가 필요한 경계에만 HAL을 두고 과하게 계층을 쌓지 마세요(균형).
정리
- 펌웨어를 응용/HAL/하드웨어로 나누면 이식성과 테스트성이 올라간다
- 헤더는 계약(인터페이스), 소스는 구현 — 응용은 헤더만 의존한다
- 칩을 바꿀 땐 HAL 구현만 교체하고 응용은 그대로 둔다
- 같은 app.c 가 PC·STM32(ARM)·우노(AVR)에서 그대로 동작한다
- 의존 방향은 응용→HAL 한 방향(역방향 금지)
과제
- hal_button_read() 를 hal.h 에 추가하고 세 구현(PC 가짜 입력·STM32 IDR·AVR PINx)으로 구현
- app.c 에 레지스터 코드가 한 줄도 없는지 검사
- 두 번째 LED를 추가하려면 어느 파일을 바꿔야 하는지 설명