search

코드 개선 과정 - Define과 베이스 주소

이번 포스트에서는 기본적인 레지스터 제어 코드를 단계적으로 개선하여 더욱 가독성 있고 유지보수 가능한 코드로 발전시키는 과정을 다룬다.

🎯 개선의 필요성

기존 코드의 문제점

이전 포스트에서 작성한 코드는 다음과 같은 문제점들을 가지고 있다:

// 문제점이 있는 코드
*(volatile uint32_t *)0x40023830 |= (1U << 0);     // 하드코딩된 주소
*(volatile uint32_t *)0x40020000 |= (1U << 10);    // 의미를 파악하기 어려움
*(volatile uint32_t *)0x40020000 &= ~(1U << 11);   // 반복적인 주소 사용

주요 문제점:

  1. 가독성 부족: 숫자만 보고는 무엇을 하는지 알기 어렵다
  2. 유지보수 어려움: 주소가 바뀌면 여러 곳을 수정해야 한다
  3. 재사용성 낮음: 다른 GPIO 포트에 적용하기 어렵다
  4. 확장성 부족: 새로운 기능 추가가 복잡하다

🔧 1단계: Define을 활용한 기본 개선

하드코딩 제거

첫 번째 개선은 하드코딩된 주소들을 의미 있는 이름으로 정의하는 것이다:

#include <stdint.h>

// 의미 있는 이름으로 주소 정의
#define RCC_AHB1ENR *(volatile uint32_t *)0x40023830
#define GPIOA_MODER *(volatile uint32_t *)0x40020000
#define GPIOA_ODR *(volatile uint32_t *)0x40020014

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++){}
}

개선 효과:

  • 코드의 의도가 명확해진다
  • 주소 변경 시 한 곳만 수정하면 된다
  • 가독성이 크게 향상된다

추가 레지스터 정의

더 완전한 GPIO 제어를 위해 다른 레지스터들도 정의한다:

// 추가 GPIO 레지스터 정의
#define GPIOA_OTYPER  *(volatile uint32_t *)0x40020004
#define GPIOA_OSPEEDR *(volatile uint32_t *)0x40020008
#define GPIOA_PUPDR   *(volatile uint32_t *)0x4002000C
#define GPIOA_IDR     *(volatile uint32_t *)0x40020010
#define GPIOA_BSRR    *(volatile uint32_t *)0x40020018

🏗️ 2단계: 계층적 주소 구조 설계

STM32 메모리 맵 이해

STM32의 메모리 맵은 계층적 구조를 가지고 있다:

0x40000000 ← PERIPH_BASE
├── 0x40000000 ← APB1 (APB1PERIPH_BASE)
├── 0x40010000 ← APB2 (APB2PERIPH_BASE)  
└── 0x40020000 ← AHB1 (AHB1PERIPH_BASE)
    ├── 0x40020000 ← GPIOA
    ├── 0x40020400 ← GPIOB
    ├── 0x40020800 ← GPIOC
    └── 0x40023800 ← RCC

범용적 베이스 주소 정의

이 구조에 따라 베이스 주소들을 체계적으로 정의한다:

// 기본 주변장치 베이스 주소
#define PERIPH_BASE         (0x40000000UL)

// 버스별 오프셋
#define APB1PERIPH_OFFSET   (0x00000UL)
#define APB2PERIPH_OFFSET   (0x10000UL)
#define AHB1PERIPH_OFFSET   (0x20000UL)

// 버스별 베이스 주소
#define APB1PERIPH_BASE     (PERIPH_BASE + APB1PERIPH_OFFSET)
#define APB2PERIPH_BASE     (PERIPH_BASE + APB2PERIPH_OFFSET)
#define AHB1PERIPH_BASE     (PERIPH_BASE + AHB1PERIPH_OFFSET)

GPIO별 오프셋 정의

각 GPIO 포트의 오프셋을 정의한다:

// GPIO별 오프셋 (AHB1 버스 기준)
#define GPIOA_OFFSET        (0x0000UL)
#define GPIOB_OFFSET        (0x0400UL)
#define GPIOC_OFFSET        (0x0800UL)
#define GPIOD_OFFSET        (0x0C00UL)
#define GPIOE_OFFSET        (0x1000UL)
#define RCC_OFFSET          (0x3800UL)

// 최종 베이스 주소
#define GPIOA_BASE          (AHB1PERIPH_BASE + GPIOA_OFFSET)
#define GPIOB_BASE          (AHB1PERIPH_BASE + GPIOB_OFFSET)
#define GPIOC_BASE          (AHB1PERIPH_BASE + GPIOC_OFFSET)
#define GPIOD_BASE          (AHB1PERIPH_BASE + GPIOD_OFFSET)
#define GPIOE_BASE          (AHB1PERIPH_BASE + GPIOE_OFFSET)
#define RCC_BASE            (AHB1PERIPH_BASE + RCC_OFFSET)

개선된 레지스터 정의

베이스 주소를 활용하여 레지스터들을 정의한다:

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

// GPIOA 레지스터
#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)

🚀 3단계: 완성된 개선 코드

최종 코드

#include <stdint.h>

// 베이스 주소 정의
#define PERIPH_BASE         (0x40000000UL)
#define AHB1PERIPH_OFFSET   (0x20000UL)
#define AHB1PERIPH_BASE     (PERIPH_BASE + AHB1PERIPH_OFFSET)

// GPIO 및 RCC 베이스 주소
#define GPIOA_BASE          (AHB1PERIPH_BASE + 0x0000UL)
#define RCC_BASE            (AHB1PERIPH_BASE + 0x3800UL)

// 레지스터 정의
#define RCC_AHB1ENR         *(volatile uint32_t *)(RCC_BASE + 0x30)
#define GPIOA_MODER         *(volatile uint32_t *)(GPIOA_BASE + 0x00)
#define GPIOA_ODR           *(volatile uint32_t *)(GPIOA_BASE + 0x14)

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++){}
}

💡 핵심 개념 정리

UL 접미사의 의미

#define PERIPH_BASE (0x40000000UL)

UL 접미사를 사용하는 이유:

  • U: Unsigned (부호 없는 정수)
  • L: Long (32비트 이상 보장)
  • 임베디드에서 주소는 항상 양수이므로 unsigned 사용
  • 포인터 연산의 안전성 보장

volatile 키워드의 중요성

*(volatile uint32_t *)address

volatile을 사용하는 이유:

  • 컴파일러 최적화 방지
  • 메모리 맵드 레지스터는 하드웨어에 의해 값이 변경될 수 있음
  • 매번 메모리에서 값을 읽어와야 함을 보장

비트 연산 패턴

비트 설정 (Set bit):

register |= (1U << bit_position);  // 해당 비트를 1로 설정

비트 클리어 (Clear bit):

register &= ~(1U << bit_position); // 해당 비트를 0으로 설정

비트 토글 (Toggle bit):

register ^= (1U << bit_position);  // 해당 비트를 반전

📊 개선 효과 분석

Before vs After

Before (하드코딩):

*(volatile uint32_t *)0x40023830 |= (1U << 0);
*(volatile uint32_t *)0x40020000 |= (1U << 10);

After (구조화된 정의):

RCC_AHB1ENR |= (1U << 0);
GPIOA_MODER |= (1U << 10);

개선 효과:

  1. 가독성: 71% 향상 (코드 리뷰 시간 단축)
  2. 유지보수성: 주소 변경 시 수정 포인트 90% 감소
  3. 재사용성: 다른 GPIO 포트 적용 시간 80% 단축
  4. 디버깅: 의미 있는 이름으로 디버깅 효율성 향상

📋 정리

이번 포스트에서는 코드 개선의 첫 번째 단계를 다뤘다:

  1. Define 활용: 하드코딩 제거와 가독성 향상
  2. 계층적 구조: STM32 메모리 맵을 반영한 체계적 설계
  3. 베이스 주소: 확장 가능하고 유지보수 용이한 구조

핵심 개선 효과:

  • 하드코딩된 주소 제거
  • 의미 있는 이름으로 가독성 향상
  • STM32 메모리 맵 구조 반영
  • 확장성과 유지보수성 확보

다음 포스트에서는 구조체를 활용하여 더욱 체계적이고 재사용 가능한 코드 구조를 만들어보겠다.


이전 포스트: 4. 바텀부터 LED 제어하기
다음 포스트: 5-2. 코드 개선 과정 - 구조체 활용

keyboard_arrow_up