← Embedded C 강의 목록으로
🔌
응용
응용 · 선수: 22강 · 응용 · 선수: 23강

23–24. HAL 추상화·모듈 분리·이식성 · 종합 — UART 명령으로 LED·타이머 제어

레지스터 코드(GPIOC->BRR=...)가 응용 로직 곳곳에 박혀 있으면 칩을 바꿀 때 모든 곳을 고쳐야 합니다. HAL(Hardware Abstraction Layer)은 "무엇을 한다"(LED를 켠다)는 응용과 "어떻게 한다"(특정 레지스터를 쓴다)는 구현을 갈라놓습니다. 같은 app.c 를 세 HAL 구현 — PC(printf)·STM32(레지스터)·아두이노 우노(AVR ATmega328P) — 로 돌려, 아키텍처가 ARM에서 AVR로 바뀌어도 응용 코드는 한 줄도 안 바뀜을 봅니다. 마지막 캡스톤. 따로 배운 조각들 — USART(19)·인터럽트(18)·링버퍼(22)·SysTick(16)·GPIO(14)·명령 파싱/모듈 분리(22·23) — 을 하나의 펌웨어로 엮습니다. PC 터미널에서 on/off/blink/help 명령을 타이핑하면 보드가 해석해 LED와 타이머를 제어합니다. 설계 핵심은 역할 분리 — 인터럽트는 받기만, 메인은 처리만, 파서는 해석만. 각 모듈은 독립 테스트 가능하고, 명령 파서는 PC에서 검증합니다.

HAL추상화이식성모듈분리계층화AVR캡스톤통합명령파서링버퍼생산자소비자펌웨어
소요 시간
⏱ 약 2시간 + 약 2~3시간
난이도
📊 고급
선수 조건
🎯 22강
결과물
펌웨어를 응용/HAL/하드웨어 계층으로 나누고 인터페이스(헤더)와 구현(소스)을 분리해, HAL 구현만 교체하면 같은 응용이 PC·STM32·AVR에서 동작하게 만들 수 있습니다. / USART·인터럽트·링버퍼·SysTick·GPIO·파서를 하나의 펌웨어로 통합하고, 생산자/소비자 분업과 순수 모듈 단위 테스트로 깔끔한 구조의 명령 제어 펌웨어를 완성할 수 있습니다.

이 강의에서 배우는 것

  • 1펌웨어를 응용/HAL/하드웨어 계층으로 나누는 이유를 설명한다
  • 2인터페이스(헤더)와 구현(소스)을 분리한다
  • 3같은 응용을 여러 HAL 구현으로 이식한다
  • 4의존 방향(응용→HAL, 역방향 금지)을 지킨다
  • 5PC용 가짜 HAL로 응용 로직을 단위 테스트한다
  • 6여러 주변장치·모듈을 하나의 펌웨어로 통합한다
  • 7인터럽트(수신)와 메인(처리)의 역할을 분리한다(생산자/소비자)
  • 8링버퍼로 UART 입력을 모으고 줄 단위로 조립한다
  • 9명령을 파싱해 LED/타이머 동작에 연결한다
  • 10파서를 모듈로 분리해 PC에서 단위 테스트한다

23. HAL 추상화·모듈 분리·이식성

레지스터 코드(GPIOC->BRR=...)가 응용 로직 곳곳에 박혀 있으면 칩을 바꿀 때 모든 곳을 고쳐야 합니다. HAL(Hardware Abstraction Layer)은 "무엇을 한다"(LED를 켠다)는 응용과 "어떻게 한다"(특정 레지스터를 쓴다)는 구현을 갈라놓습니다. 같은 app.c 를 세 HAL 구현 — PC(printf)·STM32(레지스터)·아두이노 우노(AVR ATmega328P) — 로 돌려, 아키텍처가 ARM에서 AVR로 바뀌어도 응용 코드는 한 줄도 안 바뀜을 봅니다.

소개

칩을 STM32에서 8051로 바꾸거나 같은 동작을 PC에서 테스트하고 싶을 때, 레지스터 코드가 응용에 박혀 있으면 전부 고쳐야 합니다. HAL은 응용과 구현을 갈라 이 문제를 풉니다.

핵심 개념

1) 계층화와 의존 방향

c
응용(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) 이식성 — 구현만 교체

c
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 사용)

핵심 예제

c
/* 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);
}
c
/* 같은 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 한 방향(역방향 금지)

과제

  1. hal_button_read() 를 hal.h 에 추가하고 세 구현(PC 가짜 입력·STM32 IDR·AVR PINx)으로 구현
  2. app.c 에 레지스터 코드가 한 줄도 없는지 검사
  3. 두 번째 LED를 추가하려면 어느 파일을 바꿔야 하는지 설명

24. 종합 — UART 명령으로 LED·타이머 제어

마지막 캡스톤. 따로 배운 조각들 — USART(19)·인터럽트(18)·링버퍼(22)·SysTick(16)·GPIO(14)·명령 파싱/모듈 분리(22·23) — 을 하나의 펌웨어로 엮습니다. PC 터미널에서 on/off/blink/help 명령을 타이핑하면 보드가 해석해 LED와 타이머를 제어합니다. 설계 핵심은 역할 분리 — 인터럽트는 받기만, 메인은 처리만, 파서는 해석만. 각 모듈은 독립 테스트 가능하고, 명령 파서는 PC에서 검증합니다.

소개

1편의 Hello, Embedded C! 에서 시작해 8051·STM32 레지스터, 인터럽트·통신·타이머, 디바운싱·링버퍼·HAL 패턴까지 온 여정의 마무리입니다. 이 캡스톤이 끝나면 "레지스터를 직접 다루면서도 구조가 깔끔한" 펌웨어를 짤 수 있습니다.

핵심 개념

1) 전체 아키텍처와 모듈

c
[PC 터미널] --UART--> RX 인터럽트 --push--> [링버퍼] --pop--> 메인 루프
                                                          │
                          줄 조립 → command_parse() → handle_command()
                                                          │
                          LED(PC13) / blink 모드 / UART 응답
          SysTick(1ms) ──(blink 모드면 500ms 토글)──> LED

순수 모듈(ringbuffer·command)은 칩에 의존하지 않아 그대로 PC에서 테스트됩니다. main.c 만 하드웨어 설정·통합을 맡습니다.

2) 생산자/소비자 분업과 명령 프로토콜

명령동작응답
onLED 켜기·blink 해제LED ON
offLED 끄기·blink 해제LED OFF
blink500ms 자동 점멸BLINK mode
help명령 목록cmds: ...
ℹ️

RX 인터럽트(생산자)는 rb_push만(짧게), 메인(소비자)은 rb_pop으로 꺼내 \n/\r에서 한 줄을 확정해 파싱·실행. blink_mode·g_ms는 ISR·메인 공유라 volatile.

핵심 예제

c
/* command.c — 순수 파서(하드웨어 비의존, PC 테스트 가능) */
command_t command_parse(const char *line) {
    if (line[0] == '\0')      return CMD_NONE;
    if (streq(line, "on"))    return CMD_ON;
    if (streq(line, "off"))   return CMD_OFF;
    if (streq(line, "blink")) return CMD_BLINK;
    if (streq(line, "help"))  return CMD_HELP;
    return CMD_UNKNOWN;
}
c
/* main.c — 통합(요약) */
while (1) {
    if (rb_pop(&rx_rb, &c)) {
        if (c == '\n' || c == '\r') {
            if (idx > 0u) { line[idx]='\0'; handle_command(command_parse(line)); idx=0u; }
        } else if (idx < LINE_MAX-1u) line[idx++] = (char)c;
    }
}

파서 검산: "on"→ON, "blink"→BLINK, "xyz"→UNKNOWN, ""→NONE. 빌드: gcc pc_test.c command.c -o pc_test. 시뮬레이터 UART #1에 on/off/blink/help를 입력해 LED·점멸·응답을 확인합니다.

자주 하는 실수

Q. 명령을 입력해도 반응이 없어요.

A. 줄 끝(\n/\r)이 와야 한 줄로 확정됩니다. 터미널이 Enter에 개행을 보내는지, RXNEIE와 NVIC_EnableIRQ(USART1_IRQn)를 켰는지 확인하세요.

Q. 빠르게 입력하면 글자가 사라져요.

A. 메인이 rb_pop을 충분히 자주 호출하는지, 링버퍼 크기가 충분한지 보세요. ISR에서 무거운 일을 하면 다음 바이트를 놓칩니다(ISR은 push만).

Q. 파서를 고쳤는데 보드 테스트가 번거로워요.

A. 그래서 파서를 순수 모듈로 분리했습니다. command.c 만 pc_test.c와 gcc로 빌드해 로직을 먼저 검증하고 보드엔 그 다음에 올리세요.

정리

  • 캡스톤은 USART·인터럽트·링버퍼·SysTick·GPIO·파서를 하나로 엮는다
  • 인터럽트는 받아서 push만, 메인은 pop·조립·파싱·실행을 맡는다(분업)
  • 순수 모듈(ringbuffer/command)은 PC에서 단위 테스트하고 보드에 올린다
  • 명령 프로토콜로 펌웨어를 외부에서 제어·확장할 수 있다
  • 레지스터 직접 제어와 깔끔한 구조는 양립한다 — 24편 완주!

과제

  1. blink 속도를 바꾸는 "fast"/"slow" 명령을 파서와 핸들러에 추가
  2. 인자가 있는 명령(예: "pwm 50")을 파싱하도록 확장
  3. I2C/SPI 센서·RTOS·DMA 등 다음 학습 주제 중 하나를 골라 이 캡스톤에 어떻게 붙일지 설계 스케치
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드(과제·정답 포함)는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 주세요.

댓글은 회원만 쓸 수 있습니다.

광고·욕설·비방·개인정보·불법 내용이 담긴 댓글은 숨기거나 지웁니다. 댓글은 닉네임과 함께 공개됩니다. 개인정보처리방침