Computer Science
탄탄한 기반 실력을 위한
전공과 이론 지식 모음
Today I Learned!
배웠으면 기록을 해야지
TIL 사진
Flutter 사진
Flutter로 모바일까지
거꾸로캠퍼스 코딩랩 Flutter 앱개발 강사
스파르타코딩클럽 즉문즉답 튜터
카카오테크캠퍼스 3기 학습코치
프로필 사진
박성민
임베디드 세계에
발을 들인 박치기 공룡
임베디드 사진
EMBEDDED SYSTEM
임베디드 SW와 HW, 이론부터 실전까지
ALGORITHM
알고리즘 해결 전략 기록
🎓
CAU 소프트웨어학부
텔레칩스 차량용 임베디드 스쿨 3기
애플 개발자 아카데미 1기
깃허브 사진
GitHub
프로젝트 모아보기
Instagram
인스타그램 사진

Embedded System/MCU

[MCU] GPIO 실습 : USER Button과 LED 켜기

sm_amoled 2025. 10. 28. 13:50

이전 GPIO의 HW와 설정에 대해서 정리한 내용에 대해서 실제 보드의 코드로 잘 이해했는지 확인해보자.

STM32F429ZIT6 / NUCLEO-144 Boards 를 기준으로 작성되었습니다.

LED 켜보기

메모리 주소를 이용해 HAL 없이 LED를 켜보자

우선 다큐먼트와 회로도에서 LED가 어디에 연결되어있는지 확인해야한다. 내가 켜보고싶은 파란색 LED인 LD2 는 GPIO Pin PB7 과 연결되어있다.

위 회로도를 볼 때, LD2가 ON일 때는 PB7의 출력이 HIGH이고, LD2가 OFF일 때는 PB7의 출력이 LOW가 되어야 함을 알 수 있다. 정리하자면 LD2를 toggle하기 위해선 PB7에 HIGH와 LOW 신호를 control 할 수 있어야 한다.

그렇다면 PB7에 어떻게 출력 신호를 줘야할까? 우리가 사용하는 Cortex-M4 의 아키텍처는 Memory-Mapped I/O 방식을 사용하므로, 우선 내가 값을 설정해주려는 GPIO에게 할당된 메모리 주소를 찾아야 한다.

메뉴얼의 Memory Map 에서 GPIOB의 메모리 주소를 찾아보자.

GPIOB의 메모리 맵은 0x40020400~ 0x400207FF 에 할당되어있음을 확인할 수 있다. 그 다음, 레퍼런스 메뉴얼의 오른쪽 링크를 통해 GPIO Register Map 으로 가서 7번 Pin에 대한 Configuration을 찾아보면 된다.

우리는 이전 게시글 덕분에, 여기에서 LED를 켜고 끄기 위해서는 3가지가 필요함을 알고있다.

  • LED와 연결된 CLK 활성화
  • LED와 연결된 핀의 Config Register 세팅
  • LED와 연결된 핀의 BSRR 레지스터 값 SET/RESET 하기

실습 자료 들을 활용해서 베어메탈로 코드를 작성해보면 아래와 같다. 와우!

// *** 필요한 상수 ***
#define GPIOB_BASE 0x40020400U

#define RCC_BASE         0x40023800U
#define AHB1ENR_OFFSET  0x30U

#define MODER_OFFSET    0x00U
#define PUPDR_OFFSET     0x0CU
#define BSRR_OFFSET     0x18U

// *** LED 설정 코드 ***
// AHB1ENR의 B의 enable을 설정 (0b0010)
*((volatile uint32_t*) (RCC_BASE + AHB1ENR_OFFSET)) |= 0x2;

// GPIOB PB7 을 Output 모드로 설정
*((volatile uint32_t*) (GPIOB_BASE + MODER_OFFSET)) &= 0x00 << 7 * 2;
*((volatile uint32_t*) (GPIOB_BASE + MODER_OFFSET)) |= 0x01 << 7 * 2;

// GPIOB PB7 을 pull-down으로 기본 값을 0으로 설정
*((volatile uint32_t*) (GPIOB_BASE + PUPDR_OFFSET)) &= 0x00 << 7 * 2;
*((volatile uint32_t*) (GPIOB_BASE + PUPDR_OFFSET)) |= 0x10 << 7 * 2; // 주석 해도 무방

// *** LED를 켜고 끄는 코드 (함수) ***
void turn_led_on(void)
{
    *((volatile uint32_t*) (GPIOB_BASE + BSRR_OFFSET)) = 0x1 << 7;
}

void turn_led_off(void)
{
    *((volatile uint32_t*) (GPIOB_BASE + BSRR_OFFSET)) = 0x1 << (7 + 16);
}

이번에는 버튼으로 입력을 받아보자.

하드웨어 파악하기

우신 MCU 보드에서 버튼을 육안으로 찾아보면 파란색 버튼과 검은색 버튼이 있다.

이 버튼들이 보드에 연결되어있는 주변장치이므로, 보드의 User Manual 에서 어떤 핀과 연결이 되어있는지, 어떻게 조작할 수 있는지에 대해 확인해보자.

우선 문서에서 ‘Button’ 이라고 검색을 했을 때 User Button이라는 이름을 찾을 수 있다.

그리고 Push Buttons 이라는 이름으로 섹션이 작성되어 있다. 여기에서 알 수 있는 정보로는

  • B1 USER button은 기본적으로 PC13 과 연결되어있거나 PA0과 연결되어 있다.
    • SB173이 ON / SB180이 OFF 라면 PC13 과 연결되어있음
    • SB173이 OFF / SB180이 ON 이라면 PA0과 연결되어있음

찾아보니 SB(Solder Bridge)는 보드에서의 하드웨어 설정을 변경하기 위해 납땜으로 브릿지를 연결하는 것이라고 한다. 동일한 문서에서도 Solder Bridge와 관련된 항목 중에서 User Button (B1-USER) 에 대해서 PC13(심지어 주로 이렇게 연결이 되어있는지 볼드 처리까지 되어있다) 또는 PA0, 또는 연결되지 않았을 수도 있다고 나와있다.

당장 보드를 살펴보니 SB라는 글자가 엄청 많이 작성되어있었고 (홀리쉿!) , 뒷면에 SB173과 SB180 을 찾아서 어떤게 연결되어있는지 확인해줬다.

SB180은 납땜이 끊어져있고, SB173은 납땜이 연결되어 있는 것을 보아 요 보드에서의 User Button은 PC13과 연결되어 있다는걸 알 수 있다. 즉, USER 버튼을 누르면 PC13 으로 그 신호가 전달된다.

이걸 보고 PC13 과 연결된 USER BUTTON의 회로가 풀다운 회로인지 알 수 있어야 함.
여기에 PC13을 PULLUP으로 설정하면 신호를 입력받을 수 없는 회로이기 때문에 동작하지 않는다는 것은 내가 이해를 했는데, Floating (No Pullup Pulldown)으로 I/O를 Config 해도 동작하는 이유는 이미 USER BUTTON 회로가 Pull Down 회로이기 때문임.
이에 대한 내용은 곧 새로운 게시글로 올라올 예정!

다 울었니? 이제 데이터 시트를 보자

그렇다면 이제 PC13 에서 어떻게 Input 신호를 받아들일 수 있는지를 확인해보자.

이전에 확인했던 대로, PC13은 GPIO로 연결이 되어있으므로 GPIO 에서 Input과 관련된 내용을 찾아보았다.

 

GPIO 포트를 Input 으로 사용한다면 다음 사항을 알고 있어야 한다.

  • Output Buffer 가 비활성화 + Schmitt Trigger Input이 활성화가 필요하다.
  • PUPDR 의 값에 따라서 풀업/풀다운 레지스터의 세팅이 필요하다.
  • AHB1 CLK에 따라 핀으로 들어오는 Input 값이 IDR에 반영된다.
  • IDR에 Read 접근으로 핀의 I/O 포트의 값을 읽을 수 있다.

즉, GPIO 포트의 이용을 위해서는 다음의 설정을 해줘야한다.

  1. Output Buffer의 비활성화 + Schmitt Trigger Input의 활성화를 위해 MODER를 세팅
  2. PUPDR의 설정
  3. AHB1 CLK을 GPIO 포트로 연결(활성화)

그런데 Schmitt Trigger 가 MODER에 따라서 자동으로 활성화/비활성화 된다는 것은 파악했는데, 근거가 되는 문장은 아직 찾지 못했음.

실습

파악한 내용을 토대로 실제로 USER 버튼을 통한 Input 을 받을 수 있는지 확인하기 위해서 Cube IDE를 이용한 설정 + 코드로 다시 구현을 해보자.

Cube IDE와 HAL 을 이용한 Input 받기

User 버튼과 연결된 PC13을 CubeIDE에서 GPIO_Input으로 Pin Configuration을 해준 뒤, 자동으로 생성되는 GPIO_Init의 설정을 확인해보았다.

static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET);

  /*Configure GPIO pin : PC13 */
  // USER Button과 연결된 PC13의 Input 모드로의 설정
  GPIO_InitStruct.Pin = GPIO_PIN_13;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

  /*Configure GPIO pin : PB7 */
  // 디버깅을 위한 LED와 연결된 PB7의 Output 모드로의 설정
  GPIO_InitStruct.Pin = GPIO_PIN_7;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}

여기에서 PC13 에 대해서 NOPULL 을 제거하더라도 정상적으로 동작함을 확인할 수 있었다. (왜나면 NOPULL이 기본값이라서.)

또는 PULLDOWN으로 설정했을 때에도 정상적으로 동작하며, PULLUP으로 지정하는 경우 로직이 동작하지 않는다.

PULLUP으로 지정 시 기본적으로 HIGH 상태를 유지하는데, 이때 USER BUTTON을 누르면 BUTTON 쪽 VCC와 GPIO 쪽 VCC가 만나서 그냥 High가 유지되기 때문임.

  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /*Configure GPIO pin : PC13 */
  // USER Button과 연결된 PC13의 Input 모드로의 설정
  GPIO_InitStruct.Pin = GPIO_PIN_13;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  //  GPIO_InitStruct.Pull = GPIO_NOPULL;    // 주석처리해도 동작한다.
  //  GPIO_InitStruct.Pull = GPIO_PULLDOWN;  // 동작한다.
  HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

버튼을 누르는 중에만 LED에 불이 들어오는 로직

우선 HAL 라이브러리를 활용해 작성해본 코드.

while(1) {
  if(HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13)) {
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET);
  } else {
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET);
  }
}

여기에서 값을 Input으로 받는 부분에 집중해보자. 우리가 결국 찾아내고 싶은 값인 PC13 핀으로 들어오는 데이터는 (GPIOC의 Base Address + IDR Offset) 주소에 있는 값 중 13번째 bit 에서 찾을 수 있다. GPIOC의 Base 주소와 Address Offset을 확인하고 여기에서 값을 가져와 실제로 잘 가져와지는지 확인해보자.

// GPIOC의 Base Address : 0x40020800 
// IDR의 Address Offset : 0x10
// 원하는 Pin : PC13

// (GPIOC의 Base Address + IDR Offset) 주소에 있는 값 중 13번째 bit

while(1) {
      if((*((volatile uint32_t*) (0x40020800 + 0x10)) >> 13) & 0x1) {
          HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET);
      } else {
          HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET);
      }
}

버튼을 누르면 LED를 토글하는 로직

  GPIO_PinState old_input = GPIO_PIN_RESET;
  GPIO_PinState new_input = GPIO_PIN_RESET;

  while (1)
  {
      new_input = HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13);

      if(new_input != old_input && new_input == GPIO_PIN_SET) {

          HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_7);

      }
      old_input = new_input;
  }

여기에서도 Input 으로 받는 현재 USER BUTTON의 입력 상태를 위와 동일하게 PC13의 IDR 메모리 주소를 사용해서 직접 값을 가져올 수 있다.

  GPIO_PinState old_input = GPIO_PIN_RESET;
  GPIO_PinState new_input = GPIO_PIN_RESET;

  while (1)
  {
      // 유저 버튼으로부터 새로운 입력 받아오기
      new_input = (*((volatile uint32_t*) (0x40020800 + 0x10)) >> 13) & 0x1;

      // 이전 입력과 다른 입력상태라면 LED를 토글시키기
      if(new_input != old_input && new_input == GPIO_PIN_SET) {
            uint32_t odr = *((volatile uint32_t*) (GPIOB_BASE + ODR_OFFSET));
            *((volatile uint32_t*) (GPIOB_BASE + BSRR_OFFSET)) = ((odr & (1 << 7)) << 16) | (~odr & (1 << 7));
      }

      // 입력을 다시 저장
      old_input = new_input;
  }

파일을 분리해보자

my_driver.h

#ifndef INC_MY_DRIVER_H_
#define INC_MY_DRIVER_H_

#include <stdint.h>

#define GPIOB_BASE 0x40020400U
#define GPIOC_BASE 0x40020800U

#define RCC_BASE         0x40023800U
#define AHB1ENR_OFFSET  0x30U

#define MODER_OFFSET    0x00U
#define PUPDR_OFFSET     0x0CU
#define IDR_OFFSET      0x10U
#define ODR_OFFSET      0x14U
#define BSRR_OFFSET     0x18U

void init_GPIO(void);

uint32_t read_user_button(void);

void turn_led_on(void);

void turn_led_off(void);

void toggle_led(void);

#endif /* INC_MY_DRIVER_H_ */

my_driver.c

#include "my_driver.h"

void init_GPIO(void)
{

    // AHB1ENR의 B와 C의 enable을 설정 (0b0110)
    *((volatile uint32_t*) (RCC_BASE + AHB1ENR_OFFSET)) |= 0x6;

    // GPIOB PB7 을 Output 모드로 설정
    *((volatile uint32_t*) (GPIOB_BASE + MODER_OFFSET)) &= 0x00 << 7 * 2;
    *((volatile uint32_t*) (GPIOB_BASE + MODER_OFFSET)) |= 0x01 << 7 * 2;

    // GPIOB PB7 을 pull-down으로 기본 값을 0으로 설정
    *((volatile uint32_t*) (GPIOB_BASE + PUPDR_OFFSET)) &= 0x00 << 7 * 2;
    *((volatile uint32_t*) (GPIOB_BASE + PUPDR_OFFSET)) |= 0x10 << 7 * 2; // 주석 해도 무방

    // GPIOC PC13을 Input 모드로 설정
    *((volatile uint32_t*) (GPIOC_BASE + MODER_OFFSET)) &= 0x00 << 13 * 2;
    *((volatile uint32_t*) (GPIOC_BASE + MODER_OFFSET)) |= 0x00 << 13 * 2;

    // GPIOC PC13을 Pull-down으로 설정
    *((volatile uint32_t*) (GPIOC_BASE + PUPDR_OFFSET)) &= 0x00 << 13 * 2;
    *((volatile uint32_t*) (GPIOC_BASE + PUPDR_OFFSET)) |= 0x10 << 13 * 2;
}

uint32_t read_user_button(void)
{
    return (*((volatile uint32_t*) (GPIOC_BASE + IDR_OFFSET)) >> 13) & 0x1;
}

void turn_led_on(void)
{
    *((volatile uint32_t*) (GPIOB_BASE + BSRR_OFFSET)) = 0x1 << 7;
}

void turn_led_off(void)
{
    *((volatile uint32_t*) (GPIOB_BASE + BSRR_OFFSET)) = 0x1 << (7 + 16);
}

void toggle_led(void)
{
    uint32_t odr = *((volatile uint32_t*) (GPIOB_BASE + ODR_OFFSET));
    *((volatile uint32_t*) (GPIOB_BASE + BSRR_OFFSET)) = ((odr & (1 << 7)) << 16) | (~odr & (1 << 7));
}

그런데, 이렇게 드라이버를 만들더라도 우선은 SoC에서 제공하는 헤더를 사용하는 것이 좋다.
예를 들면 stm32f429xx.h 를 사용하는게 굳이 이렇게 GPIOB_BASE, ODR_OFFSET 같은 것들을 별도로 정의해서 사용하는 것보다 더 낫다. (이미 공식으로 다 만들어둠 + 네이밍 컨벤션 등이 약속되어있음) 필요에 따라서 별도로 이렇게 Define 할 수는 있는데 그거는 상황에 따라서.

320x100