search

고급 초기환경 설정 (STM32CubeIDE 활용)

이번 포스트에서는 Empty Project 말고 STM32CubeIDE에서 제공하는 기능을 이용해서 개발을 진행하는 고급 환경구성 방법을 다룬다.

🎯 고급 환경구성 개요

이제 Empty Project 말고 STM32CubeIDE에서 제공하는 것을 이용해서 개발을 진행한다.

이를 통해 더욱 체계적이고 효율적인 개발 환경을 구축할 수 있다.

⚙️ 1. RCC 설정

HSE와 HSI 비교

RCC 설정

HSI vs HSE:

  • HSI: 16MHz로 고정 (내부 클럭)
  • HSE: 외부 클럭 (8MHz, 우리가 사용하는 보드 외부 클럭은 8MHz)

설정 방법:

  • HSE를 사용하도록 설정
  • HSE를 8MHz로 설정

PLL 설정과 클럭 분배

해당 clock이 PLL 회로로 들어가 클럭을 100MHz로 뻥튀기한다.

HCLK이 APB Prescaler를 거쳐 Peripheral Clock이 된다:

  • Timer는 100MHz로 설정된다
클럭 분배

🔧 2. Debugging 설정

Serial Wire 설정

Debugging용 Wire를 Serial Wire로 설정한다.

디버깅 설정

Serial Wire Debug의 장점:

  • JTAG보다 적은 핀 사용
  • 효율적인 디버깅 인터페이스
  • STM32에서 표준으로 사용

📍 3. GPIO 설정

GPIO 핀 구성

다음과 같이 GPIO를 설정한다:

GPIO 설정 과정

설정 완료 확인

설정이 완료된 모습:

설정 완료

GPIO 설정 요점:

  • 사용할 핀들을 적절한 모드로 설정
  • Input/Output/AF/Analog 모드 선택
  • Pull-up/Pull-down 설정
  • Output 타입 및 속도 설정

⏰ 4. Timer 설정

Timer 기본 설정

다음과 같이 Timer를 설정한다:

Timer 설정

Timer 설정 목적:

  • 정확한 타이밍 제어
  • 주기적인 인터럽트 발생
  • PWM 신호 생성

NVIC 설정

Timer Interrupt를 활성화하기 위해 NVIC를 활성화한다:

NVIC 설정

NVIC(Nested Vectored Interrupt Controller):

  • 인터럽트 우선순위 관리
  • 중첩 인터럽트 처리
  • 효율적인 인터럽트 핸들링

📁 5. Project Manager 설정

Code Generator 설정

Code generator에서 **”Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”**를 선택한다:

Project Manager 설정

이 설정의 장점:

  • 관련 파일이 각각 따로 만들어져 보기 편하다
  • 주변장치별로 파일이 분리되어 유지보수 용이
  • 코드 구조가 명확해짐

🚀 6. 프로젝트 생성

최종 생성

다 했으면 톱니바퀴 아이콘을 눌러서 프로젝트를 생성한다:

프로젝트 생성

생성 과정:

  1. 설정 검증
  2. 코드 생성
  3. 프로젝트 파일 구성
  4. 초기화 코드 생성

🛠️ 컴파일 환경 구성

STM32CubeCLT 설치

컴파일 하기 위한 프로그램 설치 (STM32CubeCLT_1.18.0):

STM32CubeCLT 설치

컴파일러 확인

컴파일러 (arm-none-eabi-gcc):

컴파일러

링커 (arm-none-eabi-ld):

링커

설치 확인

CMD에서 STM32와 Version 확인:

버전 확인

📊 생성된 프로젝트 구조

파일 구조

STM32CubeIDE가 생성하는 표준 프로젝트 구조:

Project/
├── Core/
│   ├── Inc/
│   │   ├── main.h
│   │   ├── stm32f4xx_hal_conf.h
│   │   ├── stm32f4xx_it.h
│   │   └── gpio.h (설정에 따라 생성)
│   └── Src/
│       ├── main.c
│       ├── stm32f4xx_hal_msp.c
│       ├── stm32f4xx_it.c
│       ├── system_stm32f4xx.c
│       └── gpio.c (설정에 따라 생성)
├── Drivers/
│   ├── CMSIS/
│   └── STM32F4xx_HAL_Driver/
└── Middlewares/ (필요시)

주요 파일 설명

main.c:

  • 메인 애플리케이션 로직
  • 사용자 코드 영역 제공

stm32f4xx_hal_msp.c:

  • MSP(MCU Support Package) 초기화
  • 저수준 하드웨어 초기화

stm32f4xx_it.c:

  • 인터럽트 핸들러 구현
  • 시스템 인터럽트와 사용자 인터럽트

gpio.c/gpio.h:

  • GPIO 초기화 코드
  • 주변장치별 파일 분리 시 생성

💡 고급 환경구성의 장점

1. 자동 코드 생성

  • 초기화 코드 자동 생성
  • 에러 없는 기본 설정
  • 표준 HAL 라이브러리 활용

2. 시각적 설정

  • GUI를 통한 직관적 설정
  • 실시간 설정 확인
  • 핀 충돌 자동 감지

3. 유지보수성

  • 설정 변경이 용이
  • 코드 재생성 가능
  • 표준화된 구조

4. 호환성

  • ST 공식 지원
  • 다양한 STM32 시리즈 지원
  • 업데이트와 버그 수정

📋 정리

이번 포스트에서는 STM32CubeIDE를 활용한 고급 초기환경 설정을 다뤘다:

  1. RCC 설정: HSE 사용과 PLL을 통한 클럭 설정
  2. 디버깅 설정: Serial Wire Debug 인터페이스 설정
  3. GPIO 설정: 시각적 인터페이스를 통한 핀 설정
  4. Timer 설정: Timer와 NVIC 인터럽트 설정
  5. 프로젝트 관리: 주변장치별 파일 분리 설정
  6. 컴파일 환경: STM32CubeCLT 도구 설치

핵심 장점:

  • 자동화된 코드 생성
  • 시각적이고 직관적인 설정
  • 에러 없는 기본 구성
  • 표준화된 프로젝트 구조

이제 초기환경 세팅의 전 과정을 완료했다. 다음 단계에서는 이를 바탕으로 더 복잡한 기능들을 구현할 수 있다.


이전 포스트: 6. SW Stack과 모듈화


🎉 초기환경 세팅 시리즈 완료!

축하한다! Cortex-M 초기환경 세팅의 모든 과정을 완료했다. 이제 다음 단계로 넘어가 더 고급 기능들을 구현해보자.

코드 개선 과정 - 고급 기법과 성능

이번 포스트에서는 구조체 기반 코드의 고급 활용 기법과 성능 최적화 방법을 다룬다.

🔧 고급 활용 기법

BSRR 레지스터 활용

BSRR(Bit Set/Reset Register)을 사용하면 원자적 연산이 가능하다:

// 기존 방식 (Read-Modify-Write, 3단계 연산)
GPIOA->ODR |= (1U << 5);   // 1. 읽기 → 2. 수정 → 3. 쓰기
GPIOA->ODR &= ~(1U << 5);  // 1. 읽기 → 2. 수정 → 3. 쓰기

// BSRR 방식 (원자적 연산, 1단계)
GPIOA->BSRR = (1U << 5);       // Set PA5 (직접 쓰기)
GPIOA->BSRR = (1U << (5+16));  // Reset PA5 (직접 쓰기)

BSRR 레지스터 구조:

Bit 31-16: Reset bits (1 쓰면 해당 핀 Reset)
Bit 15-0:  Set bits   (1 쓰면 해당 핀 Set)

예시: BSRR = 0x00100020
- Bit 20 (16+4): GPIOA 핀 4를 Reset
- Bit 5: GPIOA 핀 5를 Set

다중 핀 제어

구조체를 사용하면 다중 핀 제어가 효율적이다:

// 여러 핀을 한 번에 설정
GPIOA->ODR |= (1U << 5) | (1U << 6) | (1U << 7);  // PA5, PA6, PA7 동시 ON

// BSRR을 사용한 다중 핀 제어 (더 효율적)
GPIOA->BSRR = (1U << 5) | (1U << 6);  // PA5, PA6 Set
GPIOA->BSRR = (1U << (7+16));         // PA7 Reset

// 동시에 Set과 Reset (가장 효율적)
GPIOA->BSRR = (1U << 5) | (1U << 6) | (1U << (7+16));  // PA5,6 Set, PA7 Reset

비트 필드 매크로 정의

더욱 명확한 코드를 위해 비트 위치와 마스크를 정의한다:

// GPIO MODER 레지스터 비트 정의
#define GPIO_MODER_INPUT    (0x00)
#define GPIO_MODER_OUTPUT   (0x01)
#define GPIO_MODER_AF       (0x02)
#define GPIO_MODER_ANALOG   (0x03)

// 핀별 비트 위치 계산 매크로
#define GPIO_MODER_PIN(pin) ((pin) * 2)
#define GPIO_MODER_MASK(pin) (0x3U << GPIO_MODER_PIN(pin))

// 개선된 핀 설정 함수
void GPIO_SetMode(GPIO_TypeDef *GPIOx, uint32_t pin, uint32_t mode)
{
    uint32_t pos = GPIO_MODER_PIN(pin);
    GPIOx->MODER &= ~(0x3U << pos);     // 기존 값 클리어
    GPIOx->MODER |= (mode << pos);      // 새 값 설정
}

// 핀 읽기 함수
uint32_t GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint32_t pin)
{
    return ((GPIOx->IDR & (1U << pin)) ? 1 : 0);
}

// 핀 쓰기 함수  
void GPIO_WritePin(GPIO_TypeDef *GPIOx, uint32_t pin, uint32_t value)
{
    if (value) {
        GPIOx->BSRR = (1U << pin);        // Set
    } else {
        GPIOx->BSRR = (1U << (pin + 16)); // Reset
    }
}

🚀 개선된 최종 코드

헬퍼 함수를 사용한 버전

#include <stdint.h>

// 베이스 주소 정의
#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;

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

// GPIO 모드 정의
#define GPIO_MODER_OUTPUT   (0x01)
#define GPIO_MODER_PIN(pin) ((pin) * 2)

// 헬퍼 함수들
void GPIO_SetMode(GPIO_TypeDef *GPIOx, uint32_t pin, uint32_t mode);
void GPIO_WritePin(GPIO_TypeDef *GPIOx, uint32_t pin, uint32_t value);
void delay();

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

    /*2. Set PA5 as output pin */
    GPIO_SetMode(GPIOA, 5, GPIO_MODER_OUTPUT);

    while(1)
    {
        /*3. Toggle PA5 using BSRR */
        GPIO_WritePin(GPIOA, 5, 1);  // Set
        delay();
        GPIO_WritePin(GPIOA, 5, 0);  // Reset
        delay();
    }
}

// 함수 구현
void GPIO_SetMode(GPIO_TypeDef *GPIOx, uint32_t pin, uint32_t mode)
{
    uint32_t pos = GPIO_MODER_PIN(pin);
    GPIOx->MODER &= ~(0x3U << pos);     // 기존 값 클리어
    GPIOx->MODER |= (mode << pos);      // 새 값 설정
}

void GPIO_WritePin(GPIO_TypeDef *GPIOx, uint32_t pin, uint32_t value)
{
    if (value) {
        GPIOx->BSRR = (1U << pin);        // Set
    } else {
        GPIOx->BSRR = (1U << (pin + 16)); // Reset
    }
}

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

📊 성능과 메모리 분석

메모리 사용량 비교

구조체를 사용해도 실제 메모리 사용량은 변하지 않는다:

// Define 방식
#define GPIOA_ODR *(volatile uint32_t *)0x40020014
GPIOA_ODR |= (1U << 5);

// 구조체 방식  
GPIOA->ODR |= (1U << 5);

// 둘 다 컴파일 후 동일한 어셈블리 코드 생성

컴파일 결과 비교

두 방식 모두 최적화 후에는 동일한 어셈블리 코드를 생성한다:

; Define 방식과 구조체 방식 모두 다음과 같이 컴파일됨

; 최적화 전 (Debug 모드)
ldr r3, =0x40020014    ; GPIOA ODR 주소 로드
ldr r2, [r3]           ; 현재 값 읽기
orr r2, r2, #32        ; 비트 5 설정 (1 << 5)
str r2, [r3]           ; 값 쓰기

; 최적화 후 (Release 모드, -O2)
mov r2, #32            ; 즉시값 32 (1 << 5)
str r2, [r3, #24]      ; GPIOA->BSRR에 직접 쓰기 (더 효율적)

BSRR vs ODR 성능 비교

ODR 사용 (Read-Modify-Write):

GPIOA->ODR |= (1U << 5);  // 3 cycles: Read + Modify + Write

BSRR 사용 (Direct Write):

GPIOA->BSRR = (1U << 5);  // 1 cycle: Write only

성능 차이:

  • BSRR이 ODR보다 약 3배 빠르다
  • 원자적 연산으로 인터럽트 안전성 보장
  • 다중 핀 제어 시 더욱 효율적

🛡️ 인터럽트 안전성

문제 상황

// 인터럽트가 발생할 수 있는 상황
GPIOA->ODR |= (1U << 5);   // 3단계 연산 중간에 인터럽트 발생 가능

// 만약 인터럽트에서 같은 레지스터를 수정한다면?
void interrupt_handler() {
    GPIOA->ODR |= (1U << 6);  // 충돌 가능성!
}

문제점:

  1. 경쟁 상태(Race Condition): 메인 코드와 인터럽트가 동시에 ODR 수정
  2. 데이터 손실: 중간에 끼어든 수정으로 인한 값 손실
  3. 예측 불가능한 동작: 타이밍에 따라 결과가 달라짐

BSRR을 사용한 해결

// 원자적 연산으로 안전함
GPIOA->BSRR = (1U << 5);   // 1단계 연산, 인터럽트 안전

void interrupt_handler() {
    GPIOA->BSRR = (1U << 6);  // 충돌 없음! 각자 독립적으로 동작
}

장점:

  1. 원자성: 단일 쓰기 연산으로 완료
  2. 안전성: 인터럽트와 메인 코드가 서로 영향 없음
  3. 효율성: 더 빠른 실행 속도

💡 핵심 개념 정리

구조체와 하드웨어의 일치

STM32의 실제 하드웨어 레지스터 배치와 구조체가 정확히 일치한다:

// 하드웨어 레지스터 주소    구조체 멤버
// 0x40020000         ←→    MODER
// 0x40020004         ←→    OTYPER  
// 0x40020008         ←→    OSPEEDR
// 0x4002000C         ←→    PUPDR
// 0x40020010         ←→    IDR
// 0x40020014         ←→    ODR
// 0x40020018         ←→    BSRR

타입 안전성의 실제 효과

// 컴파일 타임에 에러 감지
GPIOA->INVALID_REGISTER = 0;  // 컴파일 에러!
GPIOA->MODER = "string";      // 타입 에러!

// IDE 자동완성으로 개발 효율성 향상
GPIOA->  // 자동으로 사용 가능한 멤버들을 표시

📋 정리

이번 포스트에서는 구조체 기반 코드의 고급 기법을 다뤘다:

  1. BSRR 활용: 원자적 연산으로 성능과 안전성 향상
  2. 다중 핀 제어: 효율적인 다중 GPIO 제어 방법
  3. 비트 필드 매크로: 가독성과 유지보수성 향상
  4. 성능 분석: 메모리 사용량과 컴파일 결과 비교
  5. 인터럽트 안전성: 원자적 연산의 중요성

핵심 개선 효과:

  • 성능 3배 향상 (BSRR 사용)
  • 인터럽트 안전성 확보
  • 타입 안전성으로 버그 예방
  • IDE 지원으로 개발 효율성 증대

다음 포스트에서는 실제 프로젝트 적용 예시와 디버깅 기법을 알아보겠다.


이전 포스트: 5-2. 코드 개선 과정 - 구조체 활용
다음 포스트: 5-4. 코드 개선 과정 - 실제 프로젝트 적용

코드 개선 과정 - 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. 코드 개선 과정 - 구조체 활용

- - posts/cortex-m-initial-setup-guide/

Cortex-M 초기환경 세팅 가이드

STM32F411 Nucleo-64 보드를 사용하여 초기환경 설정부터 고급 기능까지 단계별로 정리한다.

📚 학습 로드맵

이 가이드는 총 7개의 주요 섹션으로 구성되어 있으며, 각 섹션은 독립적인 포스트로 작성되어 있다.

1️⃣ STM32 보드 소개

  • STM32F411RE Nucleo-64 보드 스펙 소개
  • 핀 맵과 LED 연결 확인
  • System Architecture 이해
  • RCC의 역할과 저전력 설계 개념

2️⃣ STM32CubeIDE 프로젝트 생성

  • STM32CubeIDE 기본 프로젝트 설정
  • RCC 클럭 설정 (HSI vs HSE)
  • SYS 디버깅 설정
  • GPIO 기본 설정

3️⃣ 개발 환경 구성

  • STM32CubeCLT 설치 및 설정
  • 컴파일러와 링커 도구 확인
  • 프로젝트 매니저 설정
  • 빌드 환경 구성

4️⃣ 바텀부터 LED 제어하기

  • 레지스터 직접 제어를 통한 LED 제어
  • RCC 클럭 설정 실습
  • GPIO 모드 레지스터 설정
  • LED Toggle 구현

5️⃣ 코드 개선 과정

  • Define을 활용한 가독성 향상
  • 범용적 설계를 위한 베이스 주소 정의
  • 구조체 사용의 중요성과 장점
  • 최종 개선된 코드 구현

6️⃣ SW Stack과 모듈화

  • SW Stack 개념과 계층 구조
  • 인접 Layer를 통한 통신 원칙
  • 폴더 구조 분리와 모듈화
  • Driver 패턴 구현

7️⃣ 고급 초기환경 설정

  • STM32CubeIDE를 활용한 고급 설정
  • Timer 설정과 인터럽트 구성
  • 실제 프로젝트 환경 구성
  • 프로덕션 레벨 설정

🎯 학습 목표

이 가이드를 통해 다음을 습득할 수 있다:

  • 기초: STM32 하드웨어 구조와 레지스터 이해
  • 실습: 레지스터 직접 제어를 통한 하드웨어 제어
  • 설계: 구조체와 모듈화를 통한 코드 개선
  • 아키텍처: SW Stack을 고려한 체계적인 코드 구조
  • 도구: STM32CubeIDE와 개발 도구 활용

💡 추천 학습 순서

  1. 순차 학습: 1번부터 7번까지 순서대로 학습하는 것을 권장한다
  2. 실습 중심: 각 섹션의 코드를 직접 작성하고 테스트해본다
  3. 응용: 기본 LED 제어를 다른 GPIO 제어로 확장해본다
  4. 복습: 구조체와 모듈화 개념은 반복 학습이 중요하다

각 섹션의 링크를 클릭하여 상세 내용을 확인할 수 있다.

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

이번 포스트에서는 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. 코드 개선 과정 - 고급 기법과 성능

- - posts/cortex-m-led-control-basics/

바텀부터 LED 제어하기

이번 포스트에서는 STM32CubeIDE의 HAL 라이브러리를 사용하지 않고, 레지스터를 직접 제어하여 LED를 제어하는 방법을 알아본다.

🎯 개발 목표

아래 순서대로 LED 제어 프로그램을 구현한다:

#include <stdint.h>

int main(void)
{
    /*1. Enable clock access to GPIOA */
    /*2. Set PA5 as output pin*/
    
    while(1){
        /*3. Set PA5 high*/
        /*4. Set PA5 low*/
    }
}

⚡ 1단계: RCC 클럭 설정

Clock Tree 이해

STM32에서는 RCC가 GPIOA에 클럭을 공급해야 한다.

Clock Tree

클럭 종류:

내부 CLOCK

  • HSI (High Speed Internal Clock): System 동작용 CLOCK
  • LSI (Low Speed Internal Clock): RTC 내부 CLOCK
  • 특징: 온도나 주변환경에 따라 클럭이 변할 수 있음

외부 CLOCK

  • 내부 클럭에 비해 정확한 클럭 제공
  • 안정적인 동작 보장

RCC 레지스터 설정

메모리 맵:

RCC 레지스터
  • RCC Base Address: 0x4002 3800
  • AHB1ENR Offset: 0x30
  • 최종 주소: 0x4002 3830

GPIOAEN을 1로 설정하면 GPIOA에 클럭이 공급된다.

/*1. Enable clock access to GPIOA */
*(volatile uint32_t *)0x40023830 |= (1U << 0); 

📍 2단계: GPIO 레지스터 설정

GPIO Mode Register 설정

GPIO Mode Register

설정 값:

  • Register Address: 0x4002 0000
  • 설정 값: MODER[11:10] = 2'b01 (Output mode)
/*2. Set PA5 as output pin*/
*(volatile uint32_t *)0x40020000 |= (1U << 10);   // Set bit 10
*(volatile uint32_t *)0x40020000 &= ~(1U << 11);  // Clear bit 11

참고: 1Uunsigned int를 의미한다.

GPIO Output Type Register

GPIO Output Type Register

기본값이 Push-Pull이므로 별도 설정이 필요하지 않다.

🔄 3단계: LED Toggle 구현

GPIO Output Data Register (ODR)를 사용하여 LED를 제어한다.

GPIO ODR Register
// LED ON
*(volatile uint32_t *)0x40020014 |= (1U << 5);
for(int i=0; i<100000; i++){}

// LED OFF
*(volatile uint32_t *)0x40020014 &= ~(1U << 5);
for(int i=0; i<100000; i++){}

📝 4단계: 전체 코드

#include <stdint.h>

int main(void)
{
   /*1. Enable clock access to GPIOA */
   *(volatile uint32_t *)0x40023830 |= (1U << 0); // RCC AHB1ENR

   /*2. Set PA5 as output pin */
   *(volatile uint32_t *)0x40020000 |= (1U << 10);// GPIOA_MODER output mode
   *(volatile uint32_t *)0x40020000 &= ~(1U << 11);// GPIOA_MODER output mode

   while(1)
   {
      /*3. Set PA5 high */
      *(volatile uint32_t *)0x40020014 |= (1U <<5);
      for(int i = 0; i<100000; i++){}
      
      /*4. Set PA5 low */
      *(volatile uint32_t *)0x40020014 &= ~(1U <<5);
      for(int i = 0; i<100000; i++){}
   }
}

🔧 5단계: 코드 개선 - 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++){}
}

🏗️ 6단계: 범용적 설계를 위한 베이스 주소 정의

모든 GPIO에 적용할 수 있도록 범용적으로 설계한다:

Memory Map
#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)

#define GPIOA_OFFSET        (0x0000UL)
#define GPIOB_OFFSET        (0x0400UL)
#define GPIOC_OFFSET        (0x0800UL)
#define GPIOD_OFFSET        (0x0C00UL)
#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 RCC_BASE            (AHB1PERIPH_BASE + RCC_OFFSET)

💡 주요 개념 정리

volatile 키워드의 중요성

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

volatile을 사용하는 이유:

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

비트 연산의 활용

비트 설정 (Set bit):

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

비트 클리어 (Clear bit):

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

비트 토글 (Toggle bit):

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

🧪 테스트 및 검증

예상 동작

  1. 초기화: RCC에서 GPIOA 클럭 활성화
  2. 설정: PA5 핀을 Output 모드로 설정
  3. 동작: LED가 약 0.1초 간격으로 깜박임

디버깅 팁

1. 클럭 설정 확인

// RCC AHB1ENR 레지스터 값 확인
uint32_t rcc_value = *(volatile uint32_t *)0x40023830;
// GPIOA 클럭이 활성화되었는지 확인 (bit 0이 1인지)

2. GPIO 모드 확인

// GPIOA MODER 레지스터 값 확인
uint32_t moder_value = *(volatile uint32_t *)0x40020000;
// PA5가 output 모드로 설정되었는지 확인 (bit 10=1, bit 11=0)

📋 정리

이번 포스트에서는 레지스터 직접 제어를 통한 LED 제어 방법을 다뤘다:

  1. RCC 설정: GPIOA 클럭 활성화
  2. GPIO 설정: PA5를 Output 모드로 설정
  3. LED 제어: ODR 레지스터를 통한 ON/OFF 제어
  4. 코드 개선: Define을 활용한 가독성 향상
  5. 범용 설계: 베이스 주소를 활용한 확장 가능한 구조

다음 포스트에서는 구조체를 활용하여 더욱 체계적인 코드 구조를 만들어보겠다.


이전 포스트: 3. 개발 환경 구성
다음 포스트: 5. 코드 개선 과정

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 레포지토리의 실습 내용을 바탕으로 작성되었습니다:

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

- - posts/0.cortex-m-board-introduction/

STM32 보드 소개

이번 포스트에서는 STM32F411RE Nucleo-64 보드의 스펙과 하드웨어 구조를 자세히 알아본다.

사용 보드: STM32F411RE Nucleo-64

Nucleo-64 보드

핵심 스펙

STM32F411RE Nucleo-64 보드의 주요 사양은 다음과 같다:

  • MCU: STM32F411RE
  • Core: ARM Cortex-M4 (DSP 및 FPU 지원)
  • Flash Memory: 512KB
  • SRAM: 128KB
  • Maximum CPU Speed: 100MHz
  • ART Accelerator: 고성능 액세스 지원

중요: 임베디드 개발에서 보드 스펙을 정확히 아는 것은 개발의 첫걸음이다!
사용하는 보드 스펙은 정확히 알고 가자 “512KB Flash Memory, 127KB RAM, 100MHz CPU”

STM32F411RE MCU

보드 특징

해당 보드는 고성능 액세스 라인에 속하며, 다음과 같은 특징을 가진다:

  • ARM Cortex-M4 core with DSP and FPU
  • 512KB Flash memory
  • 100MHz CPU
  • ART Accelerator (고성능 메모리 액세스)

핀 구성과 LED 연결

User LD2 (사용자 LED)

우리가 제어할 수 있는 LED는 User LD2이다:

LED 연결 구조
User LD2

  • 연결 핀: PA5 (Arduino D13 핀과 동일)
  • 동작 방식:
    • HIGH (3.3V) → LED ON
    • LOW (0V) → LED OFF

핀 맵 이해

중요한 핀 맵 정보:

핀 맵
  • 같은 행의 핀들은 내부적으로 연결되어 있다
  • 암/수 커넥터의 차이만 있을 뿐 전기적으로는 동일하다
  • PA5 핀은 GPIO Output으로 설정하여 LED를 제어할 수 있다

: 핀 맵은 개발 과정에서 지속적으로 참조해야 하므로 별도로 저장해두는 것이 좋다.

System Architecture

STM32F411의 시스템 구조를 이해하는 것은 효율적인 프로그래밍의 기초가 된다.

System Architecture

주요 구성 요소

1. Core (ARM Cortex-M4)

  • 모든 instruction 제어를 수행한다
  • DSP와 FPU 기능을 내장하고 있다

2. Memory 구조

  • FLASH: Instruction Memory 역할 (ROM과 유사하지만 읽기/쓰기 가능)
  • SRAM: Data Memory 역할

3. Bus 시스템

  • AHB1 Bus: 100MHz로 동작하며, GPIO가 연결되어 있다
  • APB1/APB2 Bus: 저속 주변장치들이 연결되어 있다

4. RCC (Reset & Clock Control)

  • 시스템의 모든 클럭을 관리한다
  • 각 주변장치에 선택적으로 클럭을 공급한다

RCC의 역할과 저전력 설계

RCC의 중요성

RCC (Reset & Clock Control) 는 STM32의 핵심 구성 요소 중 하나다:

  • 클럭 관리: HCLK, APB CLK, AHB CLK를 각 버스에 제공한다
  • 선택적 공급: 사용하는 주변장치에만 클럭을 공급한다
  • 저전력 구현: 불필요한 주변장치의 클럭을 차단하여 전력을 절약한다

저전력 설계 원리

ARM Core는 저전력으로 설계되어 있어 다음과 같은 특징을 가진다:

  • 클럭 기반 동작: 클럭이 공급될 때만 동작한다
  • 선택적 활성화: 필요한 주변장치만 활성화한다
  • 전력 효율성: 사용하지 않는 기능은 클럭을 차단한다

예시: GPIOA를 사용하려면

  1. RCC에서 AHB1의 GPIOA에 클럭을 공급하도록 설정한다
  2. 나머지 GPIO (B, C, D 등)는 사용하지 않으면 클럭을 공급하지 않는다
  3. 결과적으로 불필요한 전력 소모를 방지할 수 있다

GPIO 메모리 맵

GPIOA 메모리 맵 구조

데이터시트에서 확인할 수 있는 GPIOA의 메모리 맵은 다음과 같다:

GPIO 메모리 맵
  • Base Address: 0x4002 0000
  • 주요 레지스터들:
    • MODER (0x00): Mode register
    • OTYPER (0x04): Output type register
    • OSPEEDR (0x08): Output speed register
    • PUPDR (0x0C): Pull-up/pull-down register
    • IDR (0x10): Input data register
    • ODR (0x14): Output data register

정리

이번 포스트에서는 STM32F411RE Nucleo-64 보드의 기본 사양과 하드웨어 구조를 살펴보았다:

  1. 보드 스펙: ARM Cortex-M4, 512KB Flash, 128KB SRAM, 100MHz
  2. LED 연결: PA5 핀에 연결된 User LD2
  3. 시스템 구조: Core, Memory, Bus, RCC의 역할
  4. 저전력 설계: RCC를 통한 선택적 클럭 공급

다음 포스트에서는 STM32CubeIDE를 사용하여 실제 프로젝트를 생성하고 기본 설정을 진행해보겠다.


다음 포스트: 2. STM32CubeIDE 프로젝트 생성

STM32CubeIDE 프로젝트 생성

이번 포스트에서는 STM32CubeIDE를 사용하여 새 프로젝트를 생성하고 기본적인 설정을 진행한다.

기본 프로젝트 설정

프로젝트 생성 단계

1. STM32CubeIDE 실행

  • STM32CubeIDE를 실행한다
  • “File → New → STM32 Project” 선택

2. MCU 선택

  • Target Selection에서 STM32F411RE 검색 및 선택
  • Nucleo-64 보드 선택

3. 프로젝트 설정

1. RCC (클럭) 설정

클럭 설정은 STM32 개발에서 가장 중요한 초기 설정 중 하나다.

1) 클럭 소스 이해

내부 클럭 (HSI - High Speed Internal)

  • 주파수: 16MHz 고정
  • 특징:
    • 외부 크리스털이 필요 없다
    • 온도나 전압 변화에 영향을 받을 수 있다
    • 정확도가 외부 클럭보다 낮다

외부 클럭 (HSE - High Speed External)

MCU 외부에 실제 크리스털 오실레이터 부품을 달아서 사용하는 클럭이며 보통 보드에 크리스털이나 클럭 모듈이 납땜되어 있음

  • 주파수: 8MHz (우리 보드 기준)
  • 특징:
    • 외부 크리스털 오실레이터를 사용한다
    • 높은 정확도와 안정성을 제공한다
    • 정밀한 타이밍이 필요한 애플리케이션에 적합하다

2) RCC 설정 방법

1. Clock Configuration 탭으로 이동

RCC 클릭

2. HSE 설정

RCC → High Speed Clock (HSE) → Crystal/Ceramic Resonator 선택

3. PLL 설정

  • HSE(8MHz)를 PLL 회로를 통해 100MHz로 증폭한다
  • 이 과정을 통해 시스템 클럭을 최대 성능으로 설정할 수 있다

4. 최종 클럭 설정

  • HCLK: 100MHz (시스템 클럭)
  • APB1: 50MHz (저속 주변장치)
  • APB2: 100MHz (고속 주변장치)
  • Timer Clock: 100MHz

중요: PLL을 사용하면 저주파수 입력을 고주파수로 변환하여 시스템 성능을 최대화할 수 있다.

2. SYS (시스템) 설정

Debug 설정

디버깅을 위한 시스템 설정을 진행한다.

Debug 인터페이스 선택:

SYS → Debug → Serial Wire 선택

Serial Wire Debug (SWD)의 장점:

  • 핀 수 절약: JTAG 대비 적은 핀 사용 (SWDIO, SWCLK만 필요)
  • 고속 디버깅: 효율적인 디버깅 인터페이스 제공
  • 실시간 추적: 실시간으로 프로그램 실행 상태를 모니터링할 수 있다

참고: SWD는 ARM Cortex-M 시리즈에서 표준으로 사용되는 디버깅 인터페이스다.

3. GPIO 설정

PA5 핀을 Output으로 설정

User LD2를 제어하기 위해 PA5 핀을 GPIO Output으로 설정한다.

설정 단계:

1. Pinout & Configuration 탭에서 PA5 핀 찾기

2. PA5 클릭 후 GPIO_Output 선택

3. GPIO 설정 확인

  • GPIO output level: Low (초기값)
  • GPIO mode: Output Push Pull
  • GPIO Pull-up/Pull-down: No pull-up and no pull-down
  • Maximum output speed: Low

GPIO 모드 설명

Output Type 옵션:

Push Pull

  • 기본 설정으로 대부분의 용도에 적합하다
  • HIGH/LOW 양방향으로 확실한 신호를 출력한다
  • LED 제어에 적합하다

Open Drain

  • 주로 I2C 통신이나 와이어드 OR 로직에 사용한다
  • HIGH 임피던스 상태와 LOW만 출력할 수 있다

Pull-up/Pull-down 옵션:

  • No pull: 외부 풀업/풀다운 저항을 사용할 때
  • Pull-up: 내부 풀업 저항 사용
  • Pull-down: 내부 풀다운 저항 사용

4. 설정 결과 확인

Clock Tree 확인

Clock Configuration 탭에서 최종 클럭 설정을 확인할 수 있다:

  • Input: HSE 8MHz
  • PLL: x12.5 (8MHz × 12.5 = 100MHz)
  • SYSCLK: 100MHz
  • AHB: 100MHz
  • APB1: 50MHz (÷2)
  • APB2: 100MHz (÷1)

GPIO 설정 확인

Pinout view에서 다음을 확인할 수 있다:

  • PA5가 녹색으로 표시된다 (GPIO_Output으로 설정됨)
  • 핀 라벨이 GPIO_Output으로 변경된다

간단한 테스트 코드

생성된 main.c에 LED 제어 코드를 추가해보자:

/* USER CODE BEGIN WHILE */
while (1)
{
    /* USER CODE END WHILE */
    
    // LED ON
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
    HAL_Delay(500);  // 0.5초 대기
    
    // LED OFF
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
    HAL_Delay(500);  // 0.5초 대기
    
    /* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */

정리

이번 포스트에서는 STM32CubeIDE를 사용한 프로젝트 생성과 기본 설정을 다뤘다:

다음 포스트에서는 개발에 필요한 도구들을 설치하고 빌드 환경을 구성해보겠다.


이전 포스트: 1. STM32 보드 소개
다음 포스트: 3. 개발 환경 구성

keyboard_arrow_up