이전 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 포트의 이용을 위해서는 다음의 설정을 해줘야한다.
- Output Buffer의 비활성화 + Schmitt Trigger Input의 활성화를 위해 MODER를 세팅
- PUPDR의 설정
- 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 할 수는 있는데 그거는 상황에 따라서.
ㅤ
'Embedded System > MCU' 카테고리의 다른 글
| [MCU] V=IR과 기본 임피던스부터 풀업과 풀다운, 푸시풀과 오픈드레인까지 (0) | 2025.11.02 |
|---|---|
| [MCU] UART 구조와 이해 (0) | 2025.10.30 |
| [MCU] 빌드 프로세스와 컴파일 환경 (1) | 2025.10.27 |
| [MCU] GPIO의 하드웨어 구조와 데이터시트, 침침한 눈을 곁들인 (1) | 2025.10.24 |
| [STM32] HAL 드라이버란? (0) | 2025.10.19 |