Lecture 3. 버튼과 LED 제어하기(GPIOTE)

강의목표

  • nRF52 개발 보드에 내장된 버튼의 입력을 받아 RGB LED를 제어할 수 있다.
  • GPIOTE(GPIO Task & Event) 레지스터를 제어할 수 있다.
  • GPIOTE의 인터럽트 기능을 활용하여 이벤트를 처리할 수 있다.

준비물

  • Roverdyn nRF52 개발 보드 × 1 (구매하기)
  • 전원 공급용 Type-C USB 케이블 × 1
  • Windows 데스크톱 혹은 랩톱 × 1
  • VS Code 개발 환경

GPIOTE 레지스터 설정 및 사용하기

GPIOTE는 General Purpose Input/Output Task & Event의 약자로서 말 그대로 GPIO의 작업과 이벤트를 담당하는 레지스터입니다. GPIO의 작업으로는 SET, CLEAR 등과 같이 핀의 출력을 설정하였을 때 어떠한 동작을 할 것인지 설정하는 것이 있으며 Event는 GPIO의 값의 변화에 따라 동작하는 모드입니다. GPIOTE는 단순히 GPIO를 이용하는 것 보다 더 간단하고 효율적으로 GPIO의 동작을 제어하거나 이벤트를 캡쳐하여 효율적인 펌웨어를 개발하는데 도움을 줍니다.

GPIOTE Event

GPIOTE 이벤트는 각 GPIO의 입력에 따라 동작하는 모드입니다. 각 이벤트는 다음과 같은 이벤트를 입력 받을 수 있습니다.

  • Rising Edge : GPIO핀 값이 Low -> High로 변할 때 동작
  • Falling Edge : GPIO핀 값이 High->Low로 변할 때 동작
  • Any changes : GPIO핀 값의 상태가 High 혹은 Low에서 다른 상태로 변할 때 동작

GPIOTE Task

GPIOTE Task는 GPIO의 입력 혹은 동작에 따라 특정 동작을 지정하여 실행시킬 수 있습니다. 매크로에 가까운 개념으로 인터럽트와 결합하여 다양한 동작을 수행시킬 수 있습니다.

본 튜토리얼은 GPIOTE의 Event 기능을 이용하여 내장 버튼의 입력을 받아 개발 보드의 LED를 제어합니다. 이를 위해서 GPIOTE의 핀 번호 할당, 인터럽트 설정을 하여 각 LED를 제어할 수 있습니다. 또한, LED 켜지는 순서를 제어하기 위해서 간단한 알고리즘을 구현하여야 합니다.

로버다인의 nRF52 개발 보드는 모두 세 개의 버튼이 있으며 이 중 하나는 Reset 핀에 연결되어 하드웨어 리셋을 수행할 수 있습니다. 각 버튼은 다음과 같은 핀에 연결되어 있습니다.

  • 버튼1 : P0.19
  • 버튼2 : p0.20
  • 리셋버튼 : p0.21/RESET

 리셋 버튼은 일반 GPIO포트인 P0.21로 변경하여 사용할 수 있으나 별도의 설정이 필요합니다.

GPIOTE 설정 및 사용하기

GPIO의 레지스터는 몇 가지 설정을 하여 사용할 수 있습니다. 다음은 기본적인 GPIO의 설정입니다.

  • NRF_GPIO->DIRSET : 핀의 출력 방향을 설정합니다.
  • NRF_GPIO->OUTSET : 핀을 출력으로 설정하였을 때 핀을 High로 설정합니다.
  • NRF_GPIO->OUTCLR : 핀을 출력으로 설정하였을 때 핀을 Low로 설정합니다.

GPIO 레지스터의 값을 입력하기 위해서는 설정하고자 하는 핀의 비트를 0 혹은 1로 설정하여 제어할 수 있습니다. 이를 확인하기 위해 먼저 nRF52 시리즈의 데이터 시트를 확인합니다.

 위 데이터시트는 nRF52832의 데이터시트입니다. GPIO 레지스터 챕터에서 확인할 수 있는 내용으로, NRFGPIO->DIRSET 레지스터에 관한 내용입니다. 해당 데이터시트를 자세히 보면 Bit number가 0인  부분의 아이디는 ‘A’로 표시되어 있으며, 해당 비트를 0으로 설정하면 해당 핀(PIN0)을 Input으로 설정하고 1로 설정하면 Output으로 설정됩니다. 만약 22번핀(P0.22)를 출력으로 설정하고자 한다면 NRF_GPIO->DIRSET = (1 << 22) 으로 설정하면 위 데이터시트상 ID가 ‘W’인 22번 핀의 입출력 방향이 출력으로 설정됩니다. 만약 중복으로 설정하고자 한다면 Or(|) 연산자를 붙여 설정하면 됩니다.

 Zephyr RTOS는 간단한 Sleep 함수가 있습니다. 메인 쓰레드에서 사용할 수 있는 k_msleep 이라는 함수는 매개 변수로  밀리초(ms) 단위로 입력받아 CPU의 동작을 중지 시킬 수 있습니다. 예를 들어 k_msleep(1000)으로 설정하면 1초간 지연 시킨다는 뜻입니다. 또 다른 함수로는 k_usleep이 있으며 k_msleep 함수와 비슷하지만 매개 변수로는 마이크로초 단위(us)를 입력 받습니다.

 보다 자세한 내용은 아래 소스코드를 참조해주세요.

소스 코드

				
					/*
        MIT License

        Copyright (c) 2024. Seonguk Jeong, Roverdyn Inc.

        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:

        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.

        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
*/
#include <zephyr/kernel.h>

/* Button pin no. definition */
#define USER_BTN1       19
#define USER_BTN2       20

/* LED pin no. definition */
#define LED_RED         22
#define LED_GREEN       23
#define LED_BLUE        24

/* Define Event Lisenter */
void GPIOTE_IRQHandler();

/* Global Variables */
static uint8_t led_order = 0;

int main(void)
{
        // Debug message for start
        printk("Application start.\n");

        // Set gpio direction for LEDs & turn off
        NRF_GPIO->DIRSET = (1 << LED_RED) | (1 << LED_GREEN) | (1 << LED_BLUE);
        NRF_GPIO->OUTSET = (1 << LED_RED) | (1 << LED_GREEN) | (1 << LED_BLUE);

        // Set Button GPIO as Input
        NRF_GPIO->PIN_CNF[USER_BTN1] = 0x0C;
        NRF_GPIO->PIN_CNF[USER_BTN2] = 0x0C;

        // Set Interrupt Event for buttons
        NRF_GPIOTE->INTENSET = 0x01;                    // Enable EVENT_IN[0]
        NRF_GPIOTE->INTENSET = (0x01 << 4);                   // Enable EVENT_IN[4]
        
        // Set GPIOTE for user button 1
        NRF_GPIOTE->CONFIG[0] = 0x01;                   // Set Event mode for EVENT_IN[0]
        NRF_GPIOTE->CONFIG[0] |= (USER_BTN1 << 8);      // Set pin no. for EVENT_IN[0]
        NRF_GPIOTE->CONFIG[0] |= (0x02 << 16);          // Set HiToLo polarity for EVENT_IN[0]
        
        // Set GPIOTE for user button 2
        NRF_GPIOTE->CONFIG[4] = 0x01;                   // Set Event mode for EVENT_IN[4]
        NRF_GPIOTE->CONFIG[4] |= (USER_BTN2 << 8);      // Set pin no. for EVENT_IN[4]
        NRF_GPIOTE->CONFIG[4] |= (0x02 << 16);          // Set HiToLo polarity for EVENT_IN[4]
        
        // Enable IRQHandler for interrupt
        IRQ_DIRECT_CONNECT(GPIOTE_IRQn, 0, GPIOTE_IRQHandler, 0);
        irq_enable(GPIOTE_IRQn);

        // Main loop
        while(true){
                k_usleep(1);
        }
        return 0;
}

void GPIOTE_IRQHandler(){
        // Interrupt event for button 1
        if(NRF_GPIOTE->EVENTS_IN[0] == 1){
                // Button 1 : Shift color to left
                NRF_GPIOTE->EVENTS_IN[0] = 0;
                if(led_order == 0){
                        led_order = 2;
                }else{
                        led_order--;
                }

                // Turn off all LEDs
                NRF_GPIO->OUTSET = (1 << LED_RED) | (1 << LED_GREEN) | (1 << LED_BLUE);
                switch(led_order){
                        case 0:
                                printk("Button 1 pressed : LED Red\n");
                                NRF_GPIO->OUTCLR = (1 << LED_RED);
                        break;
                        case 1:
                                printk("Button 1 pressed : LED Green\n");
                                NRF_GPIO->OUTCLR = (1 << LED_GREEN);
                        break;
                        case 2:
                                printk("Button 1 pressed : LED Blue\n");
                                NRF_GPIO->OUTCLR = (1 << LED_BLUE);
                        break;
                }                
        }

        // Interrupt event for button 2
        if(NRF_GPIOTE->EVENTS_IN[4] == 1){
                // Button 2 : Shift color to right
                NRF_GPIOTE->EVENTS_IN[4] = 0;
                if(led_order == 2){
                        led_order = 0;
                }else{
                        led_order++;
                }

                // Turn off all LEDs
                NRF_GPIO->OUTSET = (1 << LED_RED) | (1 << LED_GREEN) | (1 << LED_BLUE);
                switch(led_order){
                        case 0:
                                printk("Button 2 pressed : LED Red\n");
                                NRF_GPIO->OUTCLR = (1 << LED_RED);
                        break;
                        case 1:
                                printk("Button 2 pressed : LED Green\n");
                                NRF_GPIO->OUTCLR = (1 << LED_GREEN);
                        break;
                        case 2:
                                printk("Button 2 pressed : LED Blue\n");
                                NRF_GPIO->OUTCLR = (1 << LED_BLUE);
                        break;
                }  
        }
}
				
			

빌드 및 컴파일, 다운로드

작성한 소스 코드는 빌드 및 컴파일을 한 후 최종적으로는 디버거 혹은 프로그래머 장비를 이용하여 개발 보드에 다운로드할 수 있습니다. 개발 보드에 관한 설정은 “nRF52  개발환경 구축하기” 튜토리얼을 확인하여 설정할 수 있습니다.

 작성한 코드를 빌드하기 위해서는 우선 다운로드 하고자 하는 보드를 설정해주어야 합니다. 대상 보드를 정의하기 위하여 좌측 패널의 메뉴 중 “Run and Debug” 메뉴를 클릭한 후 “NRF CONNECT:WELCOME” 패널 내부의 “Create a new board” 메뉴를 클릭합니다.

화면 상단에 패널이 나타나면 새로운 보드의 이름을 작성한 후 엔터키를 눌러줍니다. 보드의 이름은 영문으로 작성하여야 합니다.

다음으로 보드의 이름을 한번 더 작성합니다. 다만 이 이름은 기계가 인식할 수 있는 방법으로 작성합니다. 대게 자동으로 작성되어 있으므로 엔터키를 눌러 다음으로 넘어갑니다.

보드가 사용하는 MCU의 종류를 입력합니다. nRF52 개발 보드는 nRF52810-QFAA 혹은 nRF52832-QFAA 두 종류를 사용합니다.

새로 작성한 보드의 정보가 저장될 위치를 설정합니다. 기본적으로 프로젝트 내부의 “board” 폴더가 지정되어 있습니다.

마지막으로 보드의 제조사를 설정합니다. 해당 내용은 작성하지 않아도 동작에 지장이 없습니다.

위와 같이 새 보드의 정의 및 파일 생성이 완료되었다면 빌드 환경을 구성할 수 있습니다. 빌드 환경은 위의 보드 추가 방법과 같이 동일한 “Run & Debug” 패널의 “NRF CONNECT: APPLICATION” 항목에서 설정할 수 있습니다.

 APPLICATION 패널 중 우측에 위치한 “Add Build Configuration” 아이콘을 눌러 설정창을 열어줍니다.

빌드 환경 설정 창이 나타나면 위 그림과 같이 “Custom board” 항목을 눌러준 다음 이전에 생성한 보드를 선택합니다. 위 예제에서는 “nrf52_devboard_52832” 항목을 선택하였습니다. 이는 사용자가 만든 보드 이름에 따라 달라질 수 있습니다. 또한 Configuration 항목을 prj.conf 로 선택합니다. 이후 우측 하단의 “Build configuration” 버튼을 눌러 빌드 환경을 구성합니다.

이상 없이 빌드가 완료되었다면 다시 APPLICATION  패널로 이동하여 “Flash All Linked Devices” 항목을 눌러 컴파일된 펌웨어의 다운로드를 시작합니다. 이 때, 타겟 보드는 컴퓨터와 Jlink 혹은 기타 디버거, 프로그래머와 연결이 되어 있어야 합니다.

위와 같이 빌드를 시작하며 컴파일이 완료된 후 자동으로 컴파일된 펌웨어를 개발 보드로 전송합니다. 전송이 완료되면 개발 보드를 재부팅 하거나 전원을 분리하였다가 다시 꽂으면 새로운 펌웨어로 동작합니다.

동작 시켜보기

이 예제를 실행시키기 위해서는 프로그램을 컴파일 및 다운로드한 후 리셋 버튼을 누르거나 전원을 분리 후 재인가 하여 동작 시킬 수 있습니다. 

 동작 시 보드에 내장된 버튼을 눌러 각 LED의 색상을 변경할 수 있습니다. 1번 버튼을 누를 경우 파랑->초록->빨강 순서대로 색상이 변경되며 2번 버튼을 누를 경우 빨강->초록->파랑 순서대로 색상이 변경됩니다.

위 코드를 수정하여 버튼의 동작을 바꿀 수 있습니다. 예를 들어 1회 클릭이 아닌 두 번 클릭으로 색상이 변경되도록 하던지, 아니면 두 버튼을 동시에 누르면 LED가 꺼지도록 할 수 있습니다.

 다양한 방법으로 이 예제를 활용해 보시기 바랍니다.

Shopping cart close