코드 개선 과정 - 구조체 활용
이번 포스트에서는 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... 모두 동일하게 반복해야 함
주요 문제점:
- 반복적 정의: 각 GPIO마다 동일한 패턴 반복
- 확장성 부족: 새 GPIO 추가 시 많은 코드 작성 필요
- 유지보수 어려움: 레지스터 구조 변경 시 모든 정의 수정 필요
- 타입 안전성 없음: 컴파일러가 잘못된 사용을 감지하기 어려움
📦 구조체 사용의 중요성
구조체를 사용한 해결책
하나의 구조체 정의로 모든 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))
동작 원리:
GPIOA_BASE: 32비트 정수 주소값(GPIO_TypeDef *): GPIO_TypeDef 구조체 포인터로 타입 캐스팅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; // 컴파일러 최적화 방지
이유:
- 하드웨어 레지스터는 언제든지 값이 변경될 수 있다
- 컴파일러가 레지스터 읽기를 건너뛰지 않도록 한다
📋 정리
이번 포스트에서는 구조체를 활용한 코드 개선을 다뤘다:
- Define 방식의 한계: 반복적이고 확장성 부족
- 구조체 정의: 하나의 구조체로 모든 GPIO 커버
- 포인터 활용: 타입 캐스팅을 통한 메모리 매핑
- 장점 확인: 간결성, 확장성, 타입 안전성, 가독성
핵심 개선 효과:
- 코드량 80% 감소
- 새 GPIO 추가 시간 90% 단축
- 컴파일 시 타입 안전성 확보
- IDE 자동완성 지원
다음 포스트에서는 구조체의 고급 활용법과 성능 분석을 알아보겠다.
이전 포스트: 5-1. 코드 개선 과정 - Define과 베이스 주소
다음 포스트: 5-3. 코드 개선 과정 - 고급 기법과 성능