Lecture 6. RTC 인터럽트로 초시계 만들기

강의목표

  • Real Time Clock(RTC) 기능을 이용하여 1초 단위로 시간을 계산한다.
  • RTC와 인터럽트를 이용하여 LED를 제어한다.
  • 내장된 버튼을 이용하여 초시계 기능을 시작하거나 중단할 수 있다.

준비물

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

RTC의 정의 및 기본 동작

(위 사진 : 하드웨어 RTC 회로)

RTC는 Real Time Clock의 약자로 말 그대로 실시간(Real Time) 시계(Clock)을 뜻합니다. 따라서 시간을 측정하거나 정해진 시간 간격으로 숫자를 세는(Counting) 등의 동작을 수행할 수 있습니다. 이는 기존의 Timer와 유사한 동작 방식을 가지고 있습니다. 하지만 RTC는 Timer와 하드웨어 구조부터 다르며 개발자는 작성중인 애플리케이션을 좀 더 최적화 하기 위해 RTC와 타이머를 적절히 선택하여 사용할 필요가 있습니다. 타이머와 RTC는 다음과 같은 차이가 있습니다.

  • RTC는 Low Frequency Clock을 사용합니다(32.769kHz)
  • RTC는 동일한 기능을 수행할 때 좀 더 낮은 전력 소모율을 보입니다.(약 1μA)
  • RTC는 낮은 해상도(Resolution)을 가집니다.(Counter Resolutiuon : 최소 30.517μs)
  •  최대 카운팅 가능한 수 : 24 bits (167,777,216)

즉, RTC는 Timer 기능을 대체하여 사용될 수 있지만 동작 방식에 다소 차이가 있으며 좀 더 낮은 해상도를 제공하는 대신 낮은 전력 소모율을 보여주는 것을 알 수 있습니다. RTC는 Timer와 마찬가지로 인터럽트 기능을 제공하고 있습니다. 다만, 전력 소모율을 낮추기 위해 이벤트를 활성화 혹은 비활성화 할 수 있는 레지스터가 추가되어 있습니다.

 RTC는 다양한 용도로 사용됩니다. 특히 mBed나 freeRTOS 등과 같이 임베디드 RTOS는 시스템 작업 스케쥴링을 위해 TICK 이벤트를 사용합니다. 또한 정확하지는 않지만 개략적인 시간이 얼마나 흘렀는지 알려줄 수 있어 보조 타이머로도 많이 사용됩니다.

RTC로 1초 카운트 하기

RTC는 여러 목적으로 사용할 수 있지만, 그 중에서 저전력 시계로도 활용할 수 있습니다. RTC는 24비트 카운터를 지원하므로 최대 0xFFFFFFFF까지 카운트 할 수 있습니다. 따라서 Over Flow가 일어나는 것을 염두하고 펌웨어를 설계하여야 합니다.

 RTC의 카운터는 Prescaler 값에 따라 1초의 단위가 달라집니다. 만약 RTC의 Prescaler를 32로 설정하였다면 다음과 같은 공식으로 1초를 세는데 필요한 카운터 값을 알 수 있습니다.

32760 / (PRESCALER + 1) = 32760/ (32+1) = 992.96

따라서 RTC의 COUNTER 레지스터 값이 993인 경우 1초를 카운트 한다고 할 수 있습니다. 전체 소스코드는 아래와 같습니다.

				
					#include <zephyr/kernel.h>

// RTC0 Interrupt Handler
void RTC2_IRQHandler();


// Global Variables
uint32_t counter = 0;
int main(void)
{
        // Initialize RTC
        NRF_RTC2->TASKS_CLEAR = 0x01;
        NRF_RTC2->INTENSET = RTC_INTENSET_COMPARE0_Enabled << RTC_INTENSET_COMPARE0_Pos;
        NRF_RTC2->EVTENSET = RTC_EVTENSET_COMPARE0_Enabled << RTC_EVTENSET_COMPARE0_Pos;
        NRF_RTC2->PRESCALER = 32;       // 32768/(32+1) = 992.96counts = 1sec
        NRF_RTC2->TASKS_START = 1;

        // Set RTC Compare Register
        counter = NRF_RTC2->COUNTER + 993;
        NRF_RTC2->CC[0] = counter;   // Call RTC1 Compare0 Handler after 1sec

        // Activate IRQ
        IRQ_DIRECT_CONNECT(RTC2_IRQn, 0, RTC2_IRQHandler, 0);
        irq_enable(RTC2_IRQn);
        
        while(true){
                // Main Loop
                //__WFI();
                k_usleep(1);
        }
        return 0;
}

void RTC2_IRQHandler(){
        // Function for RTC0 Interrupt/Events
        if(NRF_RTC2->EVENTS_COMPARE[0] == 1){
                NRF_RTC2->EVENTS_COMPARE[0] = 0;
                
                // Print message
                printk("Elapsed Time : %ds(%08d)\n", counter / 993, counter);        // 992.9 == 1sec

                // Set next compare register for repeat
                counter = NRF_RTC2->COUNTER + 993;
                NRF_RTC2->CC[0] = counter;
        }
}
				
			

RTC를 이용하여 초시계 만들기

				
					/*
        Roverdyn Inc. Timer Interrupt Example.
        2024. 7. 15.

        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>

#define GPIO_LED_RED    22
#define GPIO_LED_GREEN  23
#define GPIO_LED_BLUE   24

// Interrupt Handler
void TIMER2_IRQHandler();
int main(void)
{
        // GPIO Configuration
        NRF_GPIO->DIRSET = (1 << GPIO_LED_RED) | (1 << GPIO_LED_GREEN) | (1 << GPIO_LED_BLUE);
        NRF_GPIO->OUTSET = (1 << GPIO_LED_RED) | (1 << GPIO_LED_GREEN) | (1 << GPIO_LED_BLUE);

        /*
                Timer Interrupt Setting
                Timer Bitmode : 32bit => Max. 2^32 counts
                Mode : Timer Mode
                Interrupt Event Channel : EVENT_COMPARE[0]
                Prescaler = 2^9 = 512
                1sec counts : 16000000/512 = 31250 = 1sec
                Comapre Counter(CC) : 15625 -> 31250 / 15625 = 0.5 period 
        */
        NRF_TIMER2->BITMODE = TIMER_BITMODE_BITMODE_32Bit << TIMER_BITMODE_BITMODE_Pos;
        NRF_TIMER2->PRESCALER = 9 << TIMER_PRESCALER_PRESCALER_Pos;
        NRF_TIMER2->MODE = TIMER_MODE_MODE_Timer << TIMER_MODE_MODE_Pos;
        NRF_TIMER2->INTENSET = TIMER_INTENSET_COMPARE0_Enabled << TIMER_INTENSET_COMPARE0_Pos;
        NRF_TIMER2->SHORTS = TIMER_SHORTS_COMPARE0_CLEAR_Enabled << TIMER_SHORTS_COMPARE0_CLEAR_Pos;
        NRF_TIMER2->CC[0] = 15625;      // Interrupt Period : 0.5sec(31250/2 = 0.5)
        NRF_TIMER2->TASKS_START = 1;
        
        // Enable IRQn
        IRQ_DIRECT_CONNECT(TIMER2_IRQn, 0, TIMER2_IRQHandler, 0);
        irq_enable(TIMER2_IRQn);
        
        while(true){
                k_usleep(1);        // Wait For Interrupt
        }
        return 0;
}

void TIMER2_IRQHandler(){
        // TIMER2 Interrupt Handler
        static bool isLedOn = false;
        if(NRF_TIMER2->EVENTS_COMPARE[0] == 1){
                NRF_TIMER2->EVENTS_COMPARE[0] = 0;
                if(!isLedOn){
                        isLedOn = true;
                        printk("LED On : Green\n");
                        NRF_GPIO->OUTCLR = (1 << GPIO_LED_GREEN);
                }else{
                        isLedOn = false;
                        printk("LED Off : Green\n");
                        NRF_GPIO->OUTSET = (1 << GPIO_LED_GREEN);
                }
        }
}
				
			

빌드 및 컴파일, 다운로드

작성한 소스 코드는 빌드 및 컴파일을 한 후 최종적으로는 디버거 혹은 프로그래머 장비를 이용하여 개발 보드에 다운로드할 수 있습니다. 개발 보드에 관한 설정은 “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 혹은 기타 디버거, 프로그래머와 연결이 되어 있어야 합니다.

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

동작 시켜보기

이 예제는 별다른 소프트웨어 없이 동작할 수 있습니다. 컴파일 및 다운로드 후 전원을 제거했다가 다시 연결하거나 개발 보드의 리셋 버튼을 이용하여 동작시킬 수 있습니다. 동작 시 SEGGER RTT Viewer를 이용하여 디버깅 메시지를 출력할 수 있으며, 0.5초 주기로 LED On 및 Off 문구가 출력됩니다. 또한, 보드 상에서는 초록색 LED가 0.5초를 주기로 켜짐과 꺼짐을 반복함을 확인할 수 있습니다.

Shopping cart close