이번 시간에는 일반적인 GPIO의 Bit I/O기능을 구현해 보자.
사실 앞서 만든 Hello World에서 이 기능은 이미 살펴봤으나 이번장에서 다시 한번 정리하고 이 소스로 다음장에서 HAL Library를 뜯어보도록 하겠다.
|
1. 준비물
먼저 사이트에서 HAL Library 매뉴얼을 다운받아놓자. 내용은 사실 별게 없지만 기능별로 함수리스트가 잘 정리되어 있다. 실제 설명은 Library소스에도 주석으로 설명이 잘 되어 있으므로 소스를 참조해도 된다.
그리고 외부 푸시버튼과 LED로도 입출력을 해보기 위해 아래 부품들이 필요하다.
- 푸시버튼 1개
- 330Ω 저항 1개
- 10kΩ 저항 1개
- LED 1개
- 부품을 연결할 수 있는 Bread Board등
2. GPIO
GPIO는 General Purpose Input/Output의 약자로 우리말로 하자면 범용 입출력 정도 되겠다.
말그대로 목적을 정해놓지 않아 범용으로 사용할 수 있는 I/O이다.
이 말은 사용자가 임의로 이번 시간에 활용할 단순 Bit I/O로 정해서 사용할 수도 있고 UART, I²C, SPI등의 정규화된 신호인 Alternate Function, 그리고 Analog신호로도 정의해서 사용할 수 있다는 의미이다.
STM32의 경우 제품의 핀수에 따라 갯수가 달라지지만 우리가 사용하고 있는 NUCLEO-F429ZI/439의 경우 LQFP144로 144핀이고 PA~PG까지, 그리고 각각 16개의 핀이 있으므로 총 112개의 IO가 존재한다.(PH0, PH1이 있지만 OSC입출력으로 사용됨)
단, 단순 Bit I/O는 상관이 없지만 Alternate Function등 특정용도로 사용하기 위해서는 해당 핀이 기능을 지원하는지 데이터 시트를 통해 확인해야 한다.(데이터 시트 53p에 각 핀의 정의가 나와 있다.) CubeMX를 사용한다면 해당 핀이 지원하는 항목만 표시되므로 CubeMX를 통해 확인해도 된다.
아래 그림은 각 GPIO핀의 구조이다.
다시 한번 말하지만 이 블로그는 S/W위주로 설명하는 블로그이다. 따라서 이런 H/W관련 내용들은 필요할 때 간단하게만 살펴보도록 하겠다.
제일 오른쪽에 I/O Pin이 있고 그 내부에 Pull-up/Pull-down 저항이 있는게 보인다. 즉 S/W에서 레지스터 설정으로 핀의 Pull-up/Pull-down 저항을 Enable/Disable할 수 있다.
내부 Pull-up/Pull-down 저항은 데이터시트 136p에 보면 PA10, PB12를 제외하고는 Min 30kΩ, Max 50kΩ이고 일반적인 상태에서는 40kΩ으로 되어 있다. PA10, PB12는 Min 7kΩ, Max 14kΩ, 일반적인 상태에서 10kΩ이다.
그림 가운데 위쪽(Input driver)은 입력으로 사용되었을때의 신호 처리이다. 입력값이 Analog인 경우 바로 내부 Analog값으로 들어가지만 디지털 신호인 경우 슈밋 트리거를 거친 후 다시 Alternate funtion과 입력 레지스터쪽으로 신호가 처리된다.
슈밋 트리거의 경우 오른쪽 그림을 보면 이해가 빠르다. 입력핀으로 전압이 인가될 때 이를 디지털적으로 전압이 Threshold값 이상이면 High, 이하이면 Low로 판단할 수 있지만 Threshold값 근처에서는 Low/High판정이 오락가락 할 수 있다. 따라서 오른쪽 그림의 색깔 있는 부분의 값은 무시하고 Vt+이상으로 올랐을때만 High, 그리고 Vt-이하로 떨어졌을때만 Low상태로 전환하고 그 외의 경우는 원래 상태를 유지하게 하는 회로이다(즉 Threshhold값을 두개 사용한다).
I/O Pin이 출력으로 사용될 경우는 위 GPIO Pin구조 그림에서 가운데 아랫부분(Output driver)의 블럭이 사용된다.
출력 레지스터 값에 따라 출력을 결정하는데 Push-pull, Open-drain 방식을 선택해서 출력할 수 있다.
그리고 기본적으로 모든 GPIO핀의 입출력 전압은 3.3V이다.
하지만 데이터시트를 보면 아래와 같이 나와있다.
입력핀의 경우 FT Pin이면 VDD + 4.0 V라고 되어 있다. 이 말은 VDD=3.3V이므로 최대 7.3V까지 허용한다는 의미이다. 다시 데이터시트 53p를 보면
이렇게 FT핀은 5V 허용이라고 나와 있다.
데이터시트의 53p 아래부분의 표(Table 10)를 보면 각 핀의 정의가 나오는데 I/O Structure부분을 보면 해당 핀이 FT인지 TT인지 알수 있다. 이 표상에서는 PA4, PA5를 제외하면 대부분 FT이다.
그렇다면 대부분의 핀은 Max 7.3V이지만 5V는 사용가능하다는 의미인듯 하다.
이 부분은 정확하지 않다. 하지만 내가 본 책에도 이런식으로 적혀 있는걸로 봐서는 5V정도까지는 허용된다고 보인다.
잘못된 부분이 있을 경우 추후 수정하도록 하겠다.
3. 회로 구성
GPIO 예제를 만들기 위해 먼저 회로를 만들자.
첫번째는 Hello World예제에서 구현했듯 NUCLEO보드의 User Button을 누르면 보드의 LED1(Green)에 불이 들어오게 한다. 이전 예제와 달리 토글되지 않고 버튼을 누른 상태에서만 불이 들어오게 할 것이다.
두번째는 보드의 버튼과 LED를 사용하지 않고 외부에 있는 버튼 하나와 LED를 사용할 것이다.
이 예제에서 사용할 핀은 다음과 같다.
- PB0 : NUCLEO 보드의 LED(Green)
- PC13 : NUCLEO 보드의 User Button
- PC12 : 외부 푸시 버튼1 입력용 (외부 LED 점등용)
- PD2 : 외부 LED1 출력용
우리는 MCU내부 Pull-up/Pull-down을 S/W에서 설정해 사용할 것이므로 외부에 별도 Pull-up/Pull-down회로를 만들지 않았다.
위에서 기술한 바와 같이 STM32의 GPIO핀에는 Pull-up/Pull-down회로가 내장되어 있고 S/W에서 설정으로 이를 활성화시킬 수 있다. 따라서 이 예제에서는 외부에서는 별도 회로를 만들 필요가 없다.
단, S/W에서 Pull-up/Pull-down 을 설정했다고 해도 MCU가 리셋되고 다시 부팅이 되는 시점에서 잠깐동안 해당 핀은 floating상태로 유지된다. 따라서 이 핀에 연결된 외부 장치가 floating상태가 아닌 명확한 상태를 지속적으로 유지해야 하는 장치라면 이 방법을 사용하면 안된다. 이런 경우 외부에 Pull-up/Pull-down회로를 구성하거나 Latch등의 사용을 고려해야 한다.
아래에서 간단히 위 회로를 외부의 Pull-up/Pull-down회로로 바꾸고 테스트를 진행하겠다. 지금은 이대로 진행하자.
사실 이부분은 S/W쪽에서 신경 쓸 일은 아니고 외부에 별도 회로가 구성되어 있는지 확인만 하면 된다.
4. 코딩
GPIOTest라는 신규 프로젝트를 생성한다.
그리고 CubeMX에서 아래 그림과 같이 핀설정을 하자.
- PB0 : 보드 LED1(Green), Output Push Pull / Pull-down으로 설정
- PC12 : 외부 버튼입력, Input Mode, Pull-down으로 설정
- PC13 : 보드 User Button, 디폴트로 GPIO_EXTI13으로 설정되어 있던거 GPIO_Input으로 변경. Input Mode, Pull-down으로 설정
- PD2 : 외부 LED출력, Output Push Pull / Pull-down으로 설정
이렇게 설정하고 저장을 누르면 소스가 생성된다.
단, PC13의 경우 위에 기술한대로 MCU이미지에서 PC13을 찾아 클릭한 후 나오는 팝업 메뉴에서 GPIO_EXTI13으로 디폴트 설정되어 있는걸 GPIO_Input으로 변경한다.
이는 HelloWorld 프로젝트 때와는 달리 PC13을 인터럽트 방식이 아니라 일반 Bit I/O로 사용하겠다는 의미이다.
그러면 먼저 보드의 User Button을 눌렀을때 보드의 LED1(Green)이 켜지도록 코딩해보자.
CubeMX의 Project Manager - Code Generator의 "Generate peripheral initialization as a pair of '.c/.h' files per peripheral" 항목은 원하는 대로 하자. 예제에서는 별도 체크하지 않고 main.c에서 관리하도록 했다.
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_ETH_Init();
MX_USART3_UART_Init();
MX_USB_OTG_FS_PCD_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13));
}
/* USER CODE END 3 */
}
위 소스와 같이 main함수의 while문 안에 PC13번 입력값을 그대로 PB0에 쓰기하도록 해서 User Button이 눌러지면 LED1이 켜지고 떼면 꺼지도록 구현하였다.
한문장이 가독성이 떨어진다고 생각되면 아래와 같이 if문을 써도 무방하다.
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_SET) {
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
}
else {
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
}
}
/* USER CODE END 3 */
여기서 쓰인 함수의 자세한 내용은 다음 시간으로 미루고 우선 프로토타입을 보자.
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
이 함수는 주어진 핀의 현재 상태를 읽어서 GPIO_PinState enum형식으로 리턴해준다.
Parameters
- GPIOx : GPIO_TypeDef 구조체형 포인터. GPIOA~GPIOG중 읽으려는 GPIO를 적어준다. stm32f429xx.h 파일에 GPIOA, GPIOB, ..., GPIOK 까지 define문으로 정의가 되어 있다.
- GPIO_Pin : 해당 GPIO의 핀번호 0~15 중 하나를 적어준다. 이 값은 stm32f4xx_hal_gpio.h에 각각 GPIO_PIN_n으로 define되어 있다.
Return
- GPIO_PinState라는 열거형을 리턴한다. 이 열거형에서 GPIO_PIN_RESET은 0, GPIO_PIN_SET은 1이다.
위의 CubeMX 그림과 같이 PC13번 Label을 USER_BTN으로 변경했다면 CubeMX가 main.h에 정의한 define문에 따라
GPIOC를 USER_BTN_GPIO_Port로 GPIO_PIN_13을 UER_BTN_Pin으로 적어줘도 무방하다.
다음으로 HAL_GPIO_WritePin에 대한 프로토타입을 보자
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
이 함수는 주어진 핀의 현재 상태를 마지막 파라미터 PinState값으로 변경한다.
리턴값은 없으며 HAL_GPIO_ReadPin의 리턴값으로 사용된 GPIO_PinState 열거형이 세번째 파라미터로 정의되었다는 점 이외에는 HAL_GPIO_ReadPin 함수와 동일하다.
이 상태에서 컴파일하고 실행을 해보자.
보드의 User Button을 누르고 LED1이 점등되는지 확인하자.
Hello World와 다른 점은 여기에서는 PC13을 눌렀을때 인터럽트를 발생시키지 않는다는 점이다.
따라서 Hello World에서와 같이 HAL_GPIO_EXTI_Callback()함수를 별도로 재정의 하지 않았다.
단순히 main의 while무한루프안에서 매번 핀상태를 확인하고 이에 대한 처리를 하도록 하였다.
차이점은 무었일까?
main의 while문은 인터럽트가 발생해 그에 대한 Handler가 실행을 시작하면 작동을 멈춘다. 인터럽트 처리가 우선이기 때문이다.
하지만 Hello World의 경우 인터럽트 방식이므로 자신보다 우선순위가 높은 인터럽트가 발생하지 않는한 main의 while문안의 명령어보다 우선순위가 높다.
따라서 외부에서 어떤 신호를 수신했을때 처리해야 하는 작업이 우선순위가 높고 중요한 작업이라면 인터럽트 방식으로 처리하는게 신호 수신에 안정적이다.
예를들어 외부에서 들어오는 펄스가 10us정도의 짫은 길이로 들어오고 while()문 안의 모든 명령이 1ms정도 소요된다고 하면(즉 한 Loop에 1ms) 인터럽트 방식이 아니면 이 펄스는 놓칠 가능성이 높다.
while문 안에 HAL_Delay(1000); 을 추가하고 버튼을 눌러보자. 반응이 일정하지 않을 것이다.
이와 달리 이전에 만든 Hello World에서 while문안에 HAL_Delay(1000)을 넣고 테스트 해보면 Delay시간과 상관없이 정상 작동할 것이다.
이처럼 들어오는 신호의 중요도에 따라 일반 GPIO_Input으로 할지 인터럽트 방식을 할지 결정해야 한다.
이것 이외에는 아직까지 Hello World 프로젝트와 유사하다.
계속 진행해보자
이번에는 동일한 함수를 사용하지만 외부 버튼을 입력받아 다시 외부 LED로 출력을 보낼 것이다.
사실 H/W적으로 버튼과 LED가 외부에 있다는것 뿐이지 S/W구성은 동일하다. 위에서 사용한 구문을 그대로 사용하되 GPIO 타입과 핀번호만 달라질 뿐이다.
main함수의 while문에 한줄만 추가하면 된다.
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13)); // NUCLEO보드 버튼과 LED
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_12)); // 외부 버튼과 LED
}
컴파일해서 외부 버튼을 눌렀을때 외부 LED가 점등되는지 확인하자.
HAL_GPIO_ReadPin, HAL_GPIO_WritePin이외에도 Hello World 예제에서 사용한 HAL_GPIO_TogglePin함수도 있다.
이 함수는 해당 핀의 현재 값을 반전시키는 함수이다. 한번 호출할 때 마다 핀 상태가 LOW-HIGH상태로 바뀐다. 입력되는 파라미터는 HAL_GPIO_ReadPin과 동일하다.
위의 소스에서 첫번째 NUCLEO보드의 버튼을 눌러을때의 처리를 아래와 같이 바꾸고 테스트해보자. User Button을 누를때마다 LED1이 켜졌다 꺼졌다 할 것이다.
//HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13)); // NUCLEO보드 버튼과 LED
if(HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_SET)
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
5. "No pull-up and no pull-down"
우리는 위에서 푸시버튼의 입력핀에 대해 각각 PC12, PC13모두 Pull-down으로 설정해서 사용했다.
MCU내부 각 GPIO핀에 Pull-up/Pull-down 저항이 내장되어 있어 S/W에서 이를 세팅해 사용한 것이다.(정확히는 MCU의 Peripheral Register를 조작한 것이다.)
그런데 위의 회로부분에서 잠깐 설명했지만 MCU가 리셋되는 시점에서 우리가 작성한 main함수의 GPIO초기화 함수를 호출하기 전까지는 이 핀은 floating상태가 되어 버린다.
만약 이 핀에 연결된 외부장치가 절대 floating상태의 입력을 받으면 안되는 장치일 경우 문제가 된다. 따라서 이런 경우는 외부에 Pull-up/Pull-down 회로를 구현하거나 Latch등을 사용해야 한다.
단, 이런 상황이라면 대부분은 H/W엔지니어가 이를 고려해 회로를 설계해놨을 것이기 때문에 S/W쪽에서는 크게 신경쓰지 않아도 된다.
하지만 CubeMX에서 Pull-up/Pull-down 설정은 눈여겨 봐야 할 것이다.
일단 기존 소스는 그대로 두고 CubeMX에서 PC12, PG3의 GPIO Pull-up/Pull-down방식을 No pull-up and no pull-down으로 모두 바꾸자.
그리고 기존대로 테스트 해보면 푸시버튼에 연결된 해당 핀은 floating상태이므로 정상적인 동작을 하지 않을 것이다.
이제 처음 회로를 아래와 같이 바꿔보자.
기존 회로에 Pull-up회로를 추가한 것이다. (처음 예제의 내부 Pull-up/Pull-down저항을 사용할 때는 Pull-down으로 설정했지만 이 회로는 Pull-up으로 구성했다.)
회로를 바꾸고 테스트해보면 정상적으로 동작하며 Pull-down이 아니라 Pull-up회로로 구성했으므로 처음 예제와 달리 버튼을 누르지 않은 상태에서 LED가 켜지고 누르면 꺼진다.
즉 외부에 Pull-up/Pull-down회로가 구현되어 있다면 GPIO Pull-up/Pull-down을 No pull-up and no pull-down으로 설정해도 외부 Pull-up/Pull-down회로에 따라 정상적으로 동작한다.
사실 GPIO Pull-up/Pull-down을 어떻게 설정하든 외부 회로방식으로 처리된다.
Pull-up/Pull-down에 관한 자세한 내용은 아래 링크에 적어놓았다.
내부에 Pull-up/Pull-down회로가 있으므로 외부에 별도 회로를 구성할 일은 별로 없겠지만 위에서 적어놓은대로 내부 저항값이 현재 상황과 맞지 않는 경우는 부득이하게 내부 저항은 사용할 수 없고 외부에 별도 회로를 구성해서 사용해야 한다.
6. 마무리
이번장에서는 기본적인 GPIO의 HAL함수 사용법을 익히고 간단하지만 외부 회로와도 연결해 보았다.
사실 S/W적으로 단순 Bit I/O제어는 어려울게 전혀 없다.
하지만 이렇게만 알고 넘어가면 HAL라이브러리와 MCU의 내부 동작방식을 완전히 이해하기 어렵다. 그래서 다음 시간에는 우리가 아무 생각없이 사용한 위의 함수들을 파헤쳐 보기로 하고 이번장을 마친다.
'임베디드 > STM32' 카테고리의 다른 글
8. [STM32] GPIO - Peripheral Register 2 (0) | 2024.08.13 |
---|---|
7. [STM32] GPIO - Peripheral Register 1 (0) | 2024.07.26 |
5. [STM32] NUCLEO F429/439 보드 Pinmap (0) | 2024.07.22 |
4. [STM32] Entry Point (0) | 2024.07.12 |
3. [STM32] 디버깅 (0) | 2024.07.12 |