search

코드 개선 과정 - 구조체 활용

이번 포스트에서는 Define 방식의 한계를 극복하고 구조체를 활용하여 더욱 체계적이고 재사용 가능한 코드를 만드는 방법을 다룬다.

🚨 Define 방식의 한계

기존 Define 방식의 문제점

이전 포스트에서 개선한 코드도 여전히 다음과 같은 문제가 있다:

// 문제가 있는 방식 - 반복적이고 비효율적
#define GPIOA_MODER   *(volatile uint32_t *)(GPIOA_BASE + 0x00)
#define GPIOA_OTYPER  *(volatile uint32_t *)(GPIOA_BASE + 0x04)
#define GPIOA_OSPEEDR *(volatile uint32_t *)(GPIOA_BASE + 0x08)
#define GPIOA_PUPDR   *(volatile uint32_t *)(GPIOA_BASE + 0x0C)
#define GPIOA_IDR     *(volatile uint32_t *)(GPIOA_BASE + 0x10)
#define GPIOA_ODR     *(volatile uint32_t *)(GPIOA_BASE + 0x14)
#define GPIOA_BSRR    *(volatile uint32_t *)(GPIOA_BASE + 0x18)
#define GPIOA_LCKR    *(volatile uint32_t *)(GPIOA_BASE + 0x1C)
#define GPIOA_AFRL    *(volatile uint32_t *)(GPIOA_BASE + 0x20)
#define GPIOA_AFRH    *(volatile uint32_t *)(GPIOA_BASE + 0x24)

// GPIOB에 대해서도 동일하게 반복
#define GPIOB_MODER   *(volatile uint32_t *)(GPIOB_BASE + 0x00)
#define GPIOB_OTYPER  *(volatile uint32_t *)(GPIOB_BASE + 0x04)
#define GPIOB_OSPEEDR *(volatile uint32_t *)(GPIOB_BASE + 0x08)
// ... 계속 반복

// GPIOC, GPIOD, GPIOE... 모두 동일하게 반복해야 함

주요 문제점:

  1. 반복적 정의: 각 GPIO마다 동일한 패턴 반복
  2. 확장성 부족: 새 GPIO 추가 시 많은 코드 작성 필요
  3. 유지보수 어려움: 레지스터 구조 변경 시 모든 정의 수정 필요
  4. 타입 안전성 없음: 컴파일러가 잘못된 사용을 감지하기 어려움

📦 구조체 사용의 중요성

구조체를 사용한 해결책

하나의 구조체 정의로 모든 GPIO에 적용할 수 있다:

// GPIO 레지스터 구조체 정의
typedef struct{
   volatile uint32_t MODER;      // 0x00 - Mode register
   volatile uint32_t OTYPER;     // 0x04 - Output type register  
   volatile uint32_t OSPEEDR;    // 0x08 - Output speed register
   volatile uint32_t PUPDR;      // 0x0C - Pull-up/pull-down register
   volatile uint32_t IDR;        // 0x10 - Input data register
   volatile uint32_t ODR;        // 0x14 - Output data register
   volatile uint32_t BSRR;       // 0x18 - Bit set/reset register
   volatile uint32_t LCKR;       // 0x1C - Configuration lock register
   volatile uint32_t AFR[2];     // 0x20, 0x24 - Alternate function registers (AFRL, AFRH)
}GPIO_TypeDef;

구조체 멤버 설명

주요 레지스터별 용도:

MODER (Mode Register)

  • 각 핀의 모드 설정 (Input/Output/AF/Analog)
  • 2비트씩 할당 (핀 0: [1:0], 핀 1: [3:2], …)

OTYPER (Output Type Register)

  • Output 타입 설정 (Push-pull/Open-drain)
  • 1비트씩 할당

ODR (Output Data Register)

  • 출력 데이터 설정
  • 1비트씩 할당 (1: High, 0: Low)

IDR (Input Data Register)

  • 입력 데이터 읽기
  • 읽기 전용 레지스터

BSRR (Bit Set/Reset Register)

  • 원자적 비트 설정/리셋
  • 상위 16비트: Reset, 하위 16비트: Set

AFR[2] (Alternate Function Register)

  • 대체 기능 설정
  • AFR[0]: 핀 0-7, AFR[1]: 핀 8-15

🎯 구조체 포인터 정의

각 GPIO를 구조체 포인터로 정의

// 구조체 포인터로 각 GPIO 정의
#define GPIOA ((GPIO_TypeDef *)(GPIOA_BASE))
#define GPIOB ((GPIO_TypeDef *)(GPIOB_BASE))
#define GPIOC ((GPIO_TypeDef *)(GPIOC_BASE))
#define GPIOD ((GPIO_TypeDef *)(GPIOD_BASE))
#define GPIOE ((GPIO_TypeDef *)(GPIOE_BASE))

포인터 타입 캐스팅 이해

#define GPIOA ((GPIO_TypeDef *)(GPIOA_BASE))

동작 원리:

  1. GPIOA_BASE: 32비트 정수 주소값
  2. (GPIO_TypeDef *): GPIO_TypeDef 구조체 포인터로 타입 캐스팅
  3. GPIOA->MODER: 구조체 멤버 접근 연산자 사용

메모리 매핑:

GPIOA_BASE (0x40020000) ──┐
                          │
                          ▼
┌─────────────────────────────┐
│ MODER    (0x40020000)      │ ← GPIOA->MODER
│ OTYPER   (0x40020004)      │ ← GPIOA->OTYPER  
│ OSPEEDR  (0x40020008)      │ ← GPIOA->OSPEEDR
│ PUPDR    (0x4002000C)      │ ← GPIOA->PUPDR
│ IDR      (0x40020010)      │ ← GPIOA->IDR
│ ODR      (0x40020014)      │ ← GPIOA->ODR
│ BSRR     (0x40020018)      │ ← GPIOA->BSRR
│ LCKR     (0x4002001C)      │ ← GPIOA->LCKR
│ AFR[0]   (0x40020020)      │ ← GPIOA->AFR[0]
│ AFR[1]   (0x40020024)      │ ← GPIOA->AFR[1]
└─────────────────────────────┘

🚀 구조체를 사용한 최종 코드

완성된 코드

#include <stdint.h>
#include "stm32f411xe.h"  // STM32 표준 헤더 파일

// 베이스 주소 정의
#define PERIPH_BASE (0x40000000UL)
#define AHB1PERIPH_OFFSET (0x20000UL)
#define AHB1PERIPH_BASE (PERIPH_BASE + AHB1PERIPH_OFFSET)
#define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000UL)
#define RCC_AHB1ENR *(volatile uint32_t *)0x40023830

// GPIO 구조체 정의
typedef struct{
   volatile uint32_t MODER;
   volatile uint32_t OTYPER;
   volatile uint32_t OSPEEDR;
   volatile uint32_t PUPDR;
   volatile uint32_t IDR;
   volatile uint32_t ODR;
   volatile uint32_t BSRR;
   volatile uint32_t LCKR;
   volatile uint32_t AFR[2];
}GPIO_TypeDef;

// GPIO 포인터 정의
#define GPIOA ((GPIO_TypeDef *)(GPIOA_BASE))

void delay();

int main(void)
{
   /*1. Enable clock access to GPIOA */
   RCC_AHB1ENR |= (1U << 0);

   /*2. Set PA5 as output pin */
   GPIOA->MODER |= (1U << 10);
   GPIOA->MODER &= ~(1U << 11);

   while(1)
   {
      /*3. Set PA5 high */
      GPIOA->ODR |= (1U << 5);
      delay();
      
      /*4. Set PA5 low */
      GPIOA->ODR &= ~(1U << 5);
      delay();
   }
}

void delay(){
    for(int i = 0; i<100000; i++){}
}

✨ 구조체 사용의 장점

1. 간결성과 재사용성

Before (Define 방식):

// 각 GPIO마다 개별 정의 필요 (40줄 이상)
#define GPIOA_ODR *(volatile uint32_t *)(GPIOA_BASE + 0x14)
#define GPIOB_ODR *(volatile uint32_t *)(GPIOB_BASE + 0x14)
#define GPIOC_ODR *(volatile uint32_t *)(GPIOC_BASE + 0x14)
// ... 계속

After (구조체 방식):

// 하나의 구조체로 모든 GPIO 커버 (10줄)
GPIOA->ODR
GPIOB->ODR  
GPIOC->ODR

2. 확장성

새로운 GPIO 포트 추가가 매우 쉽다:

// 새 GPIO 추가 (단 한 줄!)
#define GPIOF ((GPIO_TypeDef *)(GPIOF_BASE))

// 즉시 모든 레지스터 사용 가능
GPIOF->MODER = 0x12345678;
GPIOF->ODR |= (1U << 3);

3. 타입 안전성

IDE와 컴파일러가 타입 검사를 수행한다:

// 잘못된 사용 시 컴파일 에러 발생
GPIOA->INVALID_REGISTER = 0;  // 컴파일 에러!

// IDE에서 자동완성 지원
GPIOA->  // 여기서 자동완성으로 사용 가능한 멤버들을 보여줌

4. 가독성 향상

// 구조체 멤버 접근이 더 직관적
GPIOA->MODER |= (1U << 10);   // 명확한 의미
GPIOA->ODR ^= (1U << 5);      // 토글 의미 명확

💡 핵심 개념 정리

구조체와 메모리 매핑의 관계

구조체는 메모리에 연속적으로 배치된다:

typedef struct{
   volatile uint32_t MODER;    // +0x00
   volatile uint32_t OTYPER;   // +0x04
   volatile uint32_t OSPEEDR;  // +0x08
   // ...
}GPIO_TypeDef;

이는 STM32의 실제 하드웨어 레지스터 배치와 정확히 일치한다.

volatile 키워드의 지속적 중요성

구조체에서도 volatile은 여전히 중요하다:

volatile uint32_t MODER;  // 컴파일러 최적화 방지

이유:

  • 하드웨어 레지스터는 언제든지 값이 변경될 수 있다
  • 컴파일러가 레지스터 읽기를 건너뛰지 않도록 한다

📋 정리

이번 포스트에서는 구조체를 활용한 코드 개선을 다뤘다:

  1. Define 방식의 한계: 반복적이고 확장성 부족
  2. 구조체 정의: 하나의 구조체로 모든 GPIO 커버
  3. 포인터 활용: 타입 캐스팅을 통한 메모리 매핑
  4. 장점 확인: 간결성, 확장성, 타입 안전성, 가독성

핵심 개선 효과:

  • 코드량 80% 감소
  • 새 GPIO 추가 시간 90% 단축
  • 컴파일 시 타입 안전성 확보
  • IDE 자동완성 지원

다음 포스트에서는 구조체의 고급 활용법과 성능 분석을 알아보겠다.


이전 포스트: 5-1. 코드 개선 과정 - Define과 베이스 주소
다음 포스트: 5-3. 코드 개선 과정 - 고급 기법과 성능

SW Stack과 모듈화

이번 포스트에서는 ARM 레포지토리에서 실제로 다룬 SW Stack 개념과 폴더 분리를 통한 모듈화 방법을 알아본다.

📁 폴더 분리의 필요성

GPIO 관련 기능 분리

기존에 main.c에 모든 코드가 집중되어 있던 것을 GPIO 관련된 기능들을 따로 분리한다.

분리 전 문제점:

  • 모든 코드가 main.c에 집중
  • 기능별 구분이 어려움
  • 재사용성 낮음
  • 유지보수 어려움

분리 후 장점:

  • 기능별 명확한 구분
  • 코드 재사용 가능
  • 유지보수 용이
  • 협업 효율성 향상

🏗️ SW Stack 개념

SW의 흐름과 계층 구조

SW의 흐름은 위에서 아래로 진행된다. SW가 HW를 제어하는 계층적 구조다.

SW Stack 구조

SW Stack 구조도

+------------------------+
|         SW            |
|  +-----------------+  |
|  |   Application   |  | <- 애플리케이션 레이어
|  +-----------------+  |
|  |     API/HAL     |  | <- 하드웨어 추상화 계층
|  +-----------------+  |
|  |     Driver      |  | <- 드라이버 계층
|  +-----------------+  |
+------------------------+
|         HW            | <- 하드웨어
+------------------------+

📂 폴더 구조 설계

계층별 폴더 분리

project/
├── App/                 # 애플리케이션 계층
│   ├── main.c
│   └── app_config.h
├── HAL/                 # 하드웨어 추상화 계층
│   ├── gpio_hal.c
│   ├── gpio_hal.h
│   ├── led_hal.c
│   └── led_hal.h
├── Driver/              # 드라이버 계층
│   ├── gpio_driver.c
│   ├── gpio_driver.h
│   └── system_config.h
└── Hardware/            # 하드웨어 정의
    ├── stm32f411xx.h
    └── memory_map.h

🔧 GPIO 모듈 분리 실습

1. GPIO 드라이버 계층 (gpio_driver.h)

#ifndef GPIO_DRIVER_H
#define GPIO_DRIVER_H

#include <stdint.h>

// GPIO 베이스 주소 정의
#define GPIOA_BASE    0x40020000
#define GPIOB_BASE    0x40020400
#define GPIOC_BASE    0x40020800

// GPIO 레지스터 구조체
typedef struct {
    volatile uint32_t MODER;    // Mode register
    volatile uint32_t OTYPER;   // Output type register
    volatile uint32_t OSPEEDR;  // Speed register
    volatile uint32_t PUPDR;    // Pull-up/pull-down register
    volatile uint32_t IDR;      // Input data register
    volatile uint32_t ODR;      // Output data register
    volatile uint32_t BSRR;     // Bit set/reset register
    volatile uint32_t LCKR;     // Lock register
    volatile uint32_t AFR[2];   // Alternate function registers
} GPIO_TypeDef;

// GPIO 포트 정의
#define GPIOA  ((GPIO_TypeDef*)GPIOA_BASE)
#define GPIOB  ((GPIO_TypeDef*)GPIOB_BASE)
#define GPIOC  ((GPIO_TypeDef*)GPIOC_BASE)

// 함수 선언
void gpio_clock_enable(GPIO_TypeDef* gpio);
void gpio_set_mode(GPIO_TypeDef* gpio, uint8_t pin, uint8_t mode);
void gpio_write_pin(GPIO_TypeDef* gpio, uint8_t pin, uint8_t state);

#endif

2. GPIO 드라이버 계층 (gpio_driver.c)

#include "gpio_driver.h"

// RCC 레지스터 주소
#define RCC_BASE      0x40023800
#define RCC_AHB1ENR   (*(volatile uint32_t*)(RCC_BASE + 0x30))

void gpio_clock_enable(GPIO_TypeDef* gpio) {
    if (gpio == GPIOA) {
        RCC_AHB1ENR |= (1 << 0);  // GPIOA 클록 활성화
    } else if (gpio == GPIOB) {
        RCC_AHB1ENR |= (1 << 1);  // GPIOB 클록 활성화
    } else if (gpio == GPIOC) {
        RCC_AHB1ENR |= (1 << 2);  // GPIOC 클록 활성화
    }
}

void gpio_set_mode(GPIO_TypeDef* gpio, uint8_t pin, uint8_t mode) {
    gpio->MODER &= ~(3 << (pin * 2));      // 기존 모드 클리어
    gpio->MODER |= (mode << (pin * 2));    // 새 모드 설정
}

void gpio_write_pin(GPIO_TypeDef* gpio, uint8_t pin, uint8_t state) {
    if (state) {
        gpio->BSRR = (1 << pin);           // Set pin
    } else {
        gpio->BSRR = (1 << (pin + 16));    // Reset pin
    }
}

3. LED HAL 계층 (led_hal.h)

#ifndef LED_HAL_H
#define LED_HAL_H

// LED 상태 정의
typedef enum {
    LED_OFF = 0,
    LED_ON  = 1
} LED_State_t;

// LED 초기화 및 제어 함수
void led_init(void);
void led_set_state(LED_State_t state);
void led_toggle(void);

#endif

4. LED HAL 계층 (led_hal.c)

#include "led_hal.h"
#include "gpio_driver.h"

// LED 핀 정의
#define LED_PORT    GPIOA
#define LED_PIN     5

void led_init(void) {
    // GPIO 클록 활성화
    gpio_clock_enable(LED_PORT);
    
    // LED 핀을 출력 모드로 설정
    gpio_set_mode(LED_PORT, LED_PIN, 1);  // 1 = Output mode
}

void led_set_state(LED_State_t state) {
    gpio_write_pin(LED_PORT, LED_PIN, state);
}

void led_toggle(void) {
    static LED_State_t current_state = LED_OFF;
    current_state = (current_state == LED_OFF) ? LED_ON : LED_OFF;
    led_set_state(current_state);
}

5. 애플리케이션 계층 (main.c)

#include "led_hal.h"

// 간단한 딜레이 함수
void delay(volatile uint32_t count) {
    while(count--);
}

int main(void) {
    // LED 초기화
    led_init();
    
    while(1) {
        led_toggle();        // LED 토글
        delay(1000000);      // 딜레이
    }
    
    return 0;
}

🎯 모듈화의 장점

1. 코드 재사용성

  • GPIO 드라이버는 다른 프로젝트에서도 사용 가능
  • LED HAL은 다른 LED 프로젝트에서 재사용 가능

2. 유지보수성

  • 각 계층별로 독립적인 수정 가능
  • 버그 발생 시 해당 계층만 집중 디버깅

3. 확장성

  • 새로운 하드웨어 추가 시 드라이버 계층만 수정
  • 새로운 기능 추가 시 해당 계층에만 추가

4. 협업 효율성

  • 계층별로 업무 분담 가능
  • 인터페이스가 명확해 협업 시 충돌 최소화

📝 계층 간 통신 규칙

1. 단방향 의존성

  • 상위 계층이 하위 계층을 호출
  • 하위 계층은 상위 계층을 직접 호출하지 않음

2. 인터페이스 표준화

  • 각 계층 간 명확한 인터페이스 정의
  • 헤더 파일을 통한 함수 원형 제공

3. 데이터 캡슐화

  • 각 계층의 내부 구현은 숨김
  • 공개 인터페이스만을 통한 접근

🚀 실제 프로젝트 적용 팁

1. 점진적 리팩토링

// Before: 모든 코드가 main.c에
int main(void) {
    // GPIO 클록 설정
    RCC_AHB1ENR |= (1 << 0);
    
    // GPIO 모드 설정
    GPIOA->MODER &= ~(3 << 10);
    GPIOA->MODER |= (1 << 10);
    
    while(1) {
        GPIOA->BSRR = (1 << 5);
        delay(1000000);
        GPIOA->BSRR = (1 << 21);
        delay(1000000);
    }
}

// After: 계층별 분리
int main(void) {
    led_init();
    
    while(1) {
        led_toggle();
        delay(1000000);
    }
}

2. 설정 파일 활용

// config.h
#define LED1_PORT    GPIOA
#define LED1_PIN     5

#define LED2_PORT    GPIOB
#define LED2_PIN     3

3. 에러 처리 추가

typedef enum {
    HAL_OK,
    HAL_ERROR,
    HAL_BUSY,
    HAL_TIMEOUT
} HAL_StatusTypeDef;

HAL_StatusTypeDef led_init(void);

📚 참고 자료

이 포스트는 다음 GitHub 레포지토리의 실습 내용을 바탕으로 작성되었습니다:

다음 포스트에서는 더 복잡한 하드웨어 추상화 계층 구현에 대해 알아보겠습니다.

keyboard_arrow_up