Lecture 4. UART로 PC와 통신하기

강의목표

  • nRF52 개발 보드에 내장된 UARTE-USB Birdge를 사용하여  PC와 Serial 통신을 할 수 있다.
  •  UARTE 통신으로 PC에서 보드의 RGB LED를 켜고 끌 수 있다.
  • UARTE에 관한 다양한 설정과 핀 설정 등 사용법을 익힌다.

준비물

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

UARTE 통신 및 기본 설정

UARTE 통신은 Universal asynchronous receiver/transmitter with EasyDMA의 약자로 일반적인 UART 통신에 MCU의 동적 메모리에 손쉽게 접근할 수 있는 Nordic 사의 고유 기능인 EasyDMA를 추가하여 손쉽게 UARTE 통신 및 인터럽트 기반 통신을 수행할 수 있도록 구성한 것입니다. 실제 물리적인 동작은 기존의 UART와 동일합니다. 각각 RX, TX 선과 CTS, RTS 선을 이용하여 통신을 수행하며 Parity Bit, Flow Control 등의 기능 또한 활용할 수 있습니다. 

 nRF52 시리즈의 UARTE 기능은 기본적으로 HAL(Hardware Access Layer)를 이용하여 설정하고 사용할 수 있습니다. 위 이미지는 nRF52 시리즈에서 UARTE를 사용하는 간단한 방법에 대한 예시입니다. UARTE를 사용하기 위해서는 기본적으로 RX, TX 핀을 설정하고 각각의 데이터를 저장할 버퍼를 설정하며 통신 속도(Baudrate)를 설정하여 사용할 수 있습니다. 뿐만 아니라 TX와 RX에 관한 다양한 이벤트가 있으며 각 이벤트는 인터럽트와 연동되어 사용할 수 있습니다. UARTE 는 기본적으로 다른 IC 혹은 MCU 등과 연결되어서 사용할 수 있으며, 특히 UART to USB Bridge IC를 이용하면 컴퓨터의 COM 포트를 통하여 직접 컴퓨터와 통신할 수 있습니다.

Roverdyn의 nRF52 개발 보드는 버전에 따라 두 가지 버전의 UART to USB Bridge IC를 지원합니다. Revision 1의 경우 SiliconLabs 사의 CP2104 브릿지 IC를 내장하였으며 Revision 2의 경우 CH340BN 브릿지 IC를 내장하였습니다. 두 IC 모두 nRF52시리즈 MCU가 지원하는 최대 속도인 1Mbps 이상의 통신 속도를 지원하며 두 버전의 개발 보드 모두 동일한 RX, TX 핀을 사용하고 있습니다. 사용하는 핀은 다음과 같습니다.

  • RX핀 : P0.07
  • TX핀 : P0.08

개발 보드의 버전에 따라 브릿지 IC의 종류와 연결되는 핀은 달라질 수 있으니 반드시 데이터시트를 사전에 확인하시기 바랍니다.

UARTE 설정 및 사용하기

UARTE를 사용하기 위해서는 기본적인 설정을 해주어야 합니다. 다음은 필수로 설정하여야 하는 설정입니다.

  • NRF_UARTE0->BAUTEDATE : 통신 속도 설정
  • NRF_UARTE0->RXD.PTR : 수신 데이터 버퍼
  • NRF_UARTE0->TXD.PTR : 송신 데이터 버퍼
  • NRF_UARTE0->PSEL.RXD : 수신 핀 번호
  • NRF_UARTE0->PSEL.TXD : 송신 핀 번호
  • NRF_UARTE0->ENABLE : UARTE 기능 활성화

위 항목 중 Baudrate는 통신 속도를 설정할 수 있습니다. Baudrate의 범위는 9600bps ~ 1MBaud까지 지원합니다. RXD.PTR, TXD.PTR은 송수신 데이터 버퍼를 지정하며, 데이터를 저장할 버퍼의 포인터 주소를 지정하는 방법으로 사용할 수 있습니다. PSEL.RXD와 PSEL.TXD는 각각 Bridge IC와 연결된 RX핀과 TX핀의 번호를 입력하면 됩니다. 마지막으로 ENABLE은 해당 모듈을 활성화 할 것인지 결정하는 것으로서, 활성화 하기 위해서는 0x08 값을 넣어주면 됩니다.

만약 인터럽트 기능을 이용하고자 한다면 다음 항목을 설정하여야 합니다.

  • NRF_UARTE0->INTENSET
  • NRF_UARTE0->SHORTS

INTENSET은 사용하고자 하는 인터럽트 종류를 설정하는데 사용됩니다. 다양한 인터럽트 이벤트가 있으며 이 튜토리얼에서는 ENDTX, ENDRX, RXDRDY 이벤트의 인터럽트를 사용합니다. SHORTS는 말 그대로 지름길을 설정하는 레지스터로서,  ENDRX_STARTRX 혹은 ENDRX_STOPRX를 설정할 수 있습니다. 이 레지스터를 설정하게 되면 ENDRX 이벤트가 생성되면 자동으로 STARTRX 태스크를 수행하거나 STOPRX 태스크를 수행합니다.

또한 인터럽트는 Zephyr RTOS의 인스턴스를 활성화하여야 사용할 수 있으므로 다음 함수를 이용하여 인터럽트를 활성화 할 수 있습니다.

  • IRQ_DIRECT_CONNECT
  • irq_enable

IRQ_DIRECT_CONNECT 함수는 매개변수로 각각 인터럽트 인스턴스, 인터럽트 Priority, 리스너 함수, IRQ 설정 플래그를 입력하면 됩니다. 대개, 인터럽트 우선순위나 IRQ 설정 플래그는 0으로 설정하여 사용할 수 있습니다. irq_enable은 인터럽트 인스턴스를 활성화 시키는 함수이며, 이 튜토리얼에서는 UARTE0_UART0_IRQn 인스턴스를 사용하였습니다.

 

소스 코드

				
					/*
        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>

/* Pin definition */
#define GPIO_LED_RED            22
#define GPIO_LED_GREEN          23
#define GPIO_LED_BLUE           24
#define UARTE_PIN_RX            8
#define UARTE_PIN_TX            7

/* UARTE Buffer */
uint8_t rxBuffer[255] = {0}, txBuffer[255] = {0};

/* UARTE Functions */
void write(uint8_t *data, uint8_t length);
void UARTE_Listener();
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);

        // UARTE Configuration
        NRF_UARTE0->BAUDRATE = UARTE_BAUDRATE_BAUDRATE_Baud115200;
        NRF_UARTE0->PSEL.RXD = UARTE_PIN_RX;
        NRF_UARTE0->PSEL.TXD = UARTE_PIN_TX;
        NRF_UARTE0->TXD.PTR = (uint32_t)txBuffer;
        NRF_UARTE0->RXD.PTR = (uint32_t)rxBuffer;
        NRF_UARTE0->SHORTS = UARTE_SHORTS_ENDRX_STARTRX_Enabled << UARTE_SHORTS_ENDRX_STARTRX_Pos;
        NRF_UARTE0->INTENSET = UARTE_INTEN_RXDRDY_Enabled << UARTE_INTEN_RXDRDY_Pos;
        NRF_UARTE0->INTENSET = UARTE_INTEN_ENDRX_Enabled << UARTE_INTEN_ENDRX_Pos;
        NRF_UARTE0->INTENSET = UARTE_INTEN_ENDTX_Enabled << NFCT_INTEN_ENDTX_Pos;
        NRF_UARTE0->ENABLE = 0x08;
        NRF_UARTE0->RXD.MAXCNT = 255;
        NRF_UARTE0->TASKS_STARTRX = 1;
        
        // Enable IRQ Handler
        IRQ_DIRECT_CONNECT(UARTE0_UART0_IRQn, 0, UARTE_Listener, 0);
        irq_enable(UARTE0_UART0_IRQn);

        while(true){
                k_usleep(10);
        }
        return 0;
}

void write(uint8_t *data, uint8_t length){
        // Function for UARTE Write
        memcpy(txBuffer, data, length);
        NRF_UARTE0->TXD.MAXCNT = length;
        NRF_UARTE0->TASKS_STARTTX = 1;
}

void UARTE_Listener(){
        // Function for UARTE RXDRDY interrupt
        static uint8_t order = 0;
        if(NRF_UARTE0->EVENTS_ENDTX == 1){
                // Clear Tx Buffer
                NRF_UARTE0->EVENTS_ENDTX = 0;
                memset(txBuffer, 0x00, sizeof(uint8_t));
        }

        if(NRF_UARTE0->EVENTS_RXDRDY == 1){
                // Clear and Stop RxBuffer
                printk("RX data received : %c\r\n", rxBuffer[order]);
                write(&rxBuffer[order], 1);
                if(rxBuffer[order] == 0x0D){
                        NRF_UARTE0->TASKS_STOPRX = 1;
                }else{
                        order++;
                }
                NRF_UARTE0->EVENTS_RXDRDY = 0;
        }

        if(NRF_UARTE0->EVENTS_ENDRX == 1){
                // Clear and Stop RxBuffer
                printk("%d byte received.\r\n", strlen(rxBuffer));
                write(rxBuffer, strlen(rxBuffer));

                // Control LED
                if(strncmp(rxBuffer, "off", 3) == 0){
                        NRF_GPIO->OUTSET = (1 << GPIO_LED_RED) | (1 << GPIO_LED_GREEN) | (1 << GPIO_LED_BLUE);
                }else if(strncmp(rxBuffer, "red", 3) == 0){
                        NRF_GPIO->OUTCLR = 1 << GPIO_LED_RED;
                }else if(strncmp(rxBuffer, "green", 3) == 0){
                        NRF_GPIO->OUTCLR = 1 << GPIO_LED_GREEN;
                }else if(strncmp(rxBuffer, "blue", 3) == 0){
                        NRF_GPIO->OUTCLR = 1 << GPIO_LED_BLUE;
                }

                // Clear parameter
                order = 0;
                memset(rxBuffer, 0x00, sizeof(uint8_t));
                NRF_UARTE0->EVENTS_ENDRX = 0;
        }
}
				
			

빌드 및 컴파일, 다운로드

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

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

동작 시켜보기

이 예제를 실행하기 위해서는 먼저 PuTTY 프로그램을 이용하여 동작 시킬 수 있습니다. PuTTY는 공식 홈페이지에서 다운로드 할 수 있습니다. PuTTY를 실행시킨 후 “Connection type” 항목에서 “Serial”을 선택한 후 상단의 Serial Line에서 COM포트 번호와 Speed를 입력합니다. 연결되어 있는 COM포트 번호는 운영체제의 “장치관리자” 혹은 각 운영체제에 맞는 방법으로 확인할 수 있으며 사용하는 환경에 따라 포트 번호는 달라질 수 있습니다. 위 이미지에서는 COM4 포트에 개발 보드가 연결되어 있으며 통신 속도는 115200으로 설정하였습니다. 설정한 후 “Open” 버튼을 눌러 개발 보드와 연결합니다.

정상적으로 연결되었다면 위와 같이 소문자로 “green” 이라고 입력한 후 엔터 키를 누르면 개발 보드의 초록색 LED가 켜지는 것을 확인 할 수 있습니다. 마찬가지로 “off” 글자를 입력하면 모든 LED가 꺼지는 것을 확인할 수 있습니다. 이 튜토리얼은 백스페이스키를 지원하지 않으므로 백스페이스 키를 이용하여 입력 명령어를 수정할 수 없습니다.

 Red, Green, Blue LED를 각각 켜고 꺼서 3색 이외에도 조합된 색상을 얻을 수 있습니다.

Shopping cart close