하드웨어 추상화 계층(HAL) 완벽 이해하기: AVR vs STM32

 하드웨어 추상화 계층(HAL) 완벽 이해하기: AVR vs STM32

하드웨어 추상화 계층(HAL)이란 무엇일까요? 단순히 ST에서 주는 라이브러리를 쓰는 게 아닙니다. AVR 스타일의 코딩에서 벗어나, 하드웨어 변경에도 끄떡없는 견고한 펌웨어를 설계하는 핵심 비결을 공개합니다.

안녕하세요! 10년 차 펌웨어 아키텍트입니다. 여러분, 혹시 예전에 AVR(ATmega128 등)로 LED를 켤 때 기억나시나요? PORTA |= 0x01; 처럼 레지스터에 직접 값을 썼었죠. 직관적이고 빠릅니다. 그런데 STM32로 넘어오니 어떤가요? HAL_GPIO_WritePin(...) 처럼 함수 이름도 길고 파라미터도 복잡해집니다.

"그냥 제공해 주는 함수 쓰면 되는 거 아냐?"라고 생각하실 수 있습니다. 하지만 실무에서 프로젝트 규모가 커지면 이 벤더 라이브러리(Vendor HAL)가 도리어 스파게티 코드의 주범이 되곤 합니다. 오늘은 그 이유와 해결책인 '진정한 하드웨어 추상화'에 대해 이야기해 보려 합니다. 😊

1. 레지스터 제어 vs 벤더 HAL 🤔

임베디드 개발의 첫걸음은 보통 레지스터 제어입니다. 데이터시트를 펴고 비트 연산을 하죠. 하지만 32비트 MCU인 STM32는 레지스터가 너무 방대합니다. 그래서 ST는 HAL(Hardware Abstraction Layer)이라는 라이브러리를 제공합니다.

💡 알아두세요!
ST사에서 제공하는 HAL 드라이버는 엄밀히 말하면 '벤더 드라이버(Vendor Driver)'입니다. 우리가 아키텍처적으로 설계해야 할 진정한 의미의 '추상화 계층'과는 구분해야 합니다.

두 방식의 차이를 표로 정리해 보았습니다.

구분 레지스터 직접 제어 (AVR 스타일) 벤더 HAL (STM32 스타일)
성능 최상 (오버헤드 없음) 보통 (함수 호출 오버헤드 존재)
이식성 최악 (MCU 바뀌면 코드 다시 짜야 함) 좋음 (같은 STM32 계열끼리 호환)
가독성 낮음 (암호 같은 비트 연산) 보통 (함수명으로 유추 가능)

 

2. 벤더 HAL도 결국 '남의 코드'다 📊

많은 주니어 개발자분들이 main.c 파일 안에서 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); 같은 코드를 직접 사용합니다. 이것이 바로 하드웨어 의존적인 코드입니다.

⚠️ 주의하세요!
만약 회로가 변경되어 LED가 Port A의 5번 핀에서 Port C의 13번 핀으로 바뀐다면? 소스 코드 전체를 뒤져서 수십 군데를 다 고쳐야 합니다. 이것이 유지보수 지옥의 시작입니다.

그래서 우리는 우리만의 방화벽, 즉 Wrapper 함수를 만들어야 합니다. 이것이 바로 임베디드 소프트웨어의 계층 구조입니다.

  • Application Layer: 비즈니스 로직 (하드웨어 몰라야 함)
  • Middleware / Wrapper: 우리만의 추상화 (LED_On())
  • Drivers (Vendor HAL): ST 제공 라이브러리 (HAL_GPIO_...)
  • Hardware: 실제 MCU 및 회로

 

3. 실전 예시: 나만의 언어로 감싸기 🧮

진정한 추상화는 '무엇(What)'을 하는지만 남기고 '어떻게(How)'를 숨기는 것입니다. 의존성 역전 원칙(DIP)의 시작이기도 하죠. 코드로 비교해 볼까요?

Before: 하드웨어 의존적 코드 (Spaghetti)

// main.c - 비즈니스 로직에 하드웨어 제어가 섞임
void Do_Work() {
  if (sensor_value > 100) {
    // PA5 핀이 LED인지 릴레이인지 코드만 봐선 모름
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); 
  }
}

After: 추상화된 코드 (Clean)

// led_driver.c - 하드웨어 제어는 여기서만 담당
void LED_On(void) {
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
}

// main.c - 하드웨어가 뭔지 몰라도 됨
void Do_Work() {
  if (sensor_value > 100) {
    LED_On(); // "LED를 켠다"라는 의도만 명확함
  }
}

보시다시피, LED_On() 함수를 만들어서 HAL 함수를 감쌌습니다. 이제 main.c는 LED가 어느 핀에 연결되어 있는지 알 필요가 없습니다. 회로가 바뀌면 led_driver.c만 수정하면 끝입니다. 아주 깔끔하죠?

🔢 내 코드 의존성 점수 확인하기

main.c 파일에서 HAL_GPIO_ 함수가 몇 번이나 등장하나요?

등장 횟수:

 

4. 마무리: 핵심 내용 요약 📝

오늘은 하드웨어 추상화의 기본 개념에 대해 알아보았습니다.

💡

오늘의 아키텍처 요약

✨ 벤더 HAL은 드라이버다: 그 자체로 완벽한 추상화가 아닙니다.
📊 계층 구조: App -> Wrapper -> HAL -> HW 순서를 기억하세요.
🛡️ 방화벽 구축: 나만의 함수(LED_On)로 벤더 함수를 감싸세요.

자주 묻는 질문 ❓

Q: 함수를 한 번 더 감싸면 속도가 느려지지 않나요?
A: 이론적으로는 함수 호출 오버헤드가 발생하지만, 현대의 MCU 성능에서는 무시할 수 있는 수준입니다. 오히려 유지보수성 향상으로 얻는 이득이 훨씬 큽니다.
Q: 간단한 프로젝트에서도 이렇게 해야 하나요?
A: LED 하나 켜는 1회성 프로젝트라면 필요 없습니다. 하지만 제품 양산을 목표로 한다면 처음부터 습관을 들이는 것이 좋습니다.


홈으로 이동

다음 이전