search
- - 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. 코드 개선 과정

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