nRF52832는 내부적으로 UART를 1개 가지고 있습니다. 그리고 이 UART는 사용자가 GPIO핀을 이용해 마치 아두이노의 software serial
처럼 TX, RX, RTS, CTS를 설정 할 수 있습니다. nRF52832의 UART는 EasyDMA를 이용하여 UARTE(UART + EasyDMA)를 이용할 수 있으나, UART와 UARTE는 자원을 공유하기 때문에 동시에 사용할 수 없습니다. 두 peripheral 및 관련 레지스터를 간단히 살펴보겠습니다.
nRF52832 UART가 지원하는 내용과 주요 레지스터 목록입니다.
여기서 INTENSET
은 인터럽트를 enable 해주는 레지스터이며, INTENCLR
레지스터는 인터럽트를 disable 하는 레지스터입니다. nRF52832 인터럽트 enable/disable 세팅은 한 개의 레지스터만을 비트를 1/0로 세팅해 조정하는 것이 아니라 개별 레지스터를 사용하는 것을 알 수 있습니다.
위에서 언급했듯, UARTE는 Universal asynchronous receiver/transmitter with EASY DMA의 약자입니다. 1Mbps baudrate 까지 지원하며, RAM에 직접 접근해 데이터를 전송할 수 있습니다.
nRF52832 UARTE가 지원하는 내용과 주요 레지스터 목록입니다.
관련 레지스터표 내용을 보면 RAM에 접근하기 위한 Pointer를 저장하는 레지스터 RXD.PTR
, TXD.PTR
및 buffer의 최댓값을 저장하는 RXD.MAXCNT
, TXD.MAXCNT
몇 바이트까지 송신/수신 되었는지를 세는 RXD.AMOUNT
, RXD.AMOUNT
등의 레지스터가 추가된 것을 확인 할 수 있습니다.
Nordic의 MCU에는 Event, Task, Shortcuts 라는 개념이 있습니다. Event와 Task, Shortcuts에 관해 자세히 설명하려면 PPI 같은 Nordic MCU의 특징을 설명해야 함으로 나중에 따로 포스팅 하겠습니다. 대신 UART와 UARTE의 레지스터 목록을 보시면 EVENTS_
, TASKS_
로 시작하는 레지스터를 부가 설명하기 위해 Event와 Task에 대해 간략히 알아보겠습니다.
Event : Event는 주변장치의 상태 변화(예: GPIO의 1 ➔ 0)과 같은 이벤트를 다른 주변장치 및 CPU에 알리는데 사용됩니다. UART에서는 EVENTS_
로 시작하는 레지스터의 특정 비트에 1이 세팅되면 인터럽트 신호가 발생 됩니다. MSP430 같은 MCU에서의 flag와 비슷합니다.
Task : Task는 주변장치가 특정 동작을 수행하게 하는데 이용됩니다. 실제 코드에 UART 인스턴스를 생성하고 Configuration을 설정한 뒤, 인터럽트만 enable만 하면 Tx/Rx를 수행하지 않습니다. Task 관련 레지스터를 조작해야 비로소 UART가 동작합니다.
이제 Nordic의 SDK를 분석해 보겠습니다. Nordic SDK는 여러 매크로를 사용해 약간 지저분한 면이 있습니다. 또한 각 주변장치에 관련된 드라이버 파일들도 일관성이 없습니다. (의도된 것인지, 의도치 않은 것인지는 모르겠습니다만...)
우선 1장에서 만들었던 예제의 main. cmain.c 파일을 열어 상단에 include 파일을 보겠습니다.
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "app_uart.h"
#include "app_error.h"
#include "nrf_delay.h"
#include "nrf.h"
#include "bsp.h"
#if defined (UART_PRESENT)
#include "nrf_uart.h"
#endif
#if defined (UARTE_PRESENT)
#include "nrf_uarte.h"
#endif
#if defined (UART_PRESENT)
와 #if defined (UARTE_PRESENT)
는 nrf52832_peripherals.h
에 정의되어있습니다. 이 헤더파일은 nrf52832에 존재하는 주변장치를 매크로로 표현한 것입니다. 안에 내용을보면 _PRESENT
로 끝나는 매크로는 존재 여부를 나타낸 것이고, 그 바로 아래 _COUNT
는 주변장치의 개수를 정의한 것입니다.
main.c
의 APP_UART_FIFO_INIT();
함수내부를 타고 들어가보면 app_uart_init()
를 호출하고 nrf_drv_uart_init()
, nrfx_uart_init()
순서로 호출하는 것을 확인할 수 있습니다. 각각의 함수가 정의된 파일경로는 아래와 같습니다.
📁component/libraries/app_uart
:app_uart_init()
📁integration/nrf/legacy
:nrf_drv_uart_init()
📁modules/nrfx/drivers
:nrfx_uart_init()
Nordic SDK의 Library에서 nrf_drv_module을 호출하고 nrf_drv_module은 다시 nrfx 모듈을 호출하도록 구성되어 있으며 이를 표현하면 아래와 같습니다.
굳이 이런 식으로 호출 하는 이유는 하위 호환 때문으로 생각됩니다. nRF52832 개발일지 (0) 에서 말했듯 구버전 SDK에서는 드라이버 파일 앞에 nrf_drv_
같은 접두어를 붙이는 규칙이 있었는데 모듈이 신버전으로 바뀌면서 nrfx_
로 규칙이 바꾸었습니다.
라이브러리와 드라이브파일의 중간인 구버전 드라이브파일 내용을 자세히 살펴봅시다. nrf_drv_uart.c
에 내용 일부를 가져왔습니다.
#include "nrf_drv_uart.h"
...
static void uart_evt_handler(nrfx_uart_event_t const * p_event,
void * p_context)
{
uint32_t inst_idx = (uint32_t)p_context;
nrf_drv_uart_event_t event =
{
.type = (nrf_drv_uart_evt_type_t)p_event->type,
.data =
{
.error =
{
.rxtx =
{
.p_data = p_event->data.error.rxtx.p_data,
.bytes = p_event->data.error.rxtx.bytes,
},
.error_mask = p_event->data.error.error_mask,
}
}
};
m_handlers[inst_idx](&event, m_contexts[inst_idx]);
}
#endif // defined(NRF_DRV_UART_WITH_UART)
ret_code_t nrf_drv_uart_init(nrf_drv_uart_t const * p_instance,
nrf_drv_uart_config_t const * p_config,
nrf_uart_event_handler_t event_handler)
{
uint32_t inst_idx = p_instance->inst_idx;
m_handlers[inst_idx] = event_handler;
m_contexts[inst_idx] = p_config->p_context;
...
nrf_drv_uart_config_t config = *p_config;
config.p_context = (void *)inst_idx;
ret_code_t result = 0;
if (NRF_DRV_UART_USE_UARTE)
{
result = nrfx_uarte_init(&p_instance->uarte,
(nrfx_uarte_config_t const *)&config,
event_handler ? uarte_evt_handler : NULL);
}
else if (NRF_DRV_UART_USE_UART)
{
result = nrfx_uart_init(&p_instance->uart,
(nrfx_uart_config_t const *)&config,
event_handler ? uart_evt_handler : NULL);
}
return result;
}
위 내용처럼 이벤트 핸들러의 인자인 nrfx_uart_event_t
자료형을 p_evnent
및 p_context
를 nrf_drv_uart_event_t
자료형으로 바꾸는 역할만 합니다. 또한 초기화 과정을 담당하는 nrf_drv_uart_init()
함수내부에서도 NRF_DRV_UART_USE_UARTE
, NRF_DRV_UART_USE_UART
매크로에 따라 nrfx_uarte_init()
을 실행할 것인지 혹은 nrfx_uart_init()
을 실행할지 나뉜것 외에는 크게 이벤트 핸들러 처럼 크게 다른 내용이 없습니다.
nrf_drv_uart.h
파일엔 두 가지 중요한 매크로가 있습니다. 이 매크로들은 최상단에 정의되어 있습니다.
#include <nrfx.h>
#if defined(UARTE_PRESENT) && NRFX_CHECK(NRFX_UARTE_ENABLED)
#define NRF_DRV_UART_WITH_UARTE
#endif
#if defined(UART_PRESENT) && NRFX_CHECK(NRFX_UART_ENABLED)
#define NRF_DRV_UART_WITH_UART
#endif
상단 #if defined(UARTE_PRESENT) && NRFX_CHECK(NRFX_UARTE_ENABLED)
의미는 아래와 같습니다.
MCU에 UARTE 주변장치가 존재하고 UARTE가 enable 되어있으면 NRF_DRV_UART_WITH_UARTE를 정의한다.
참고로 NRFX_UARTE_ENABLED
매크로는 sdk_config.h
에 정의되어있습니다. 이는 다음에 더 자세히 살펴보겠습니다.
여기서 정의하는 NRF_DRV_UART_WITH_UARTE
와 NRF_DRV_UART_WITH_UART
매크로는 아래 nrf_drv_uart.h
에 정의된 여러 매크로 함수에서 중요하게 사용됩니다. 일부 예시는 아래와 같습니다.
__STATIC_INLINE
uint32_t nrf_drv_uart_task_address_get(nrf_drv_uart_t const * p_instance,
nrf_uart_task_t task)
{
uint32_t result = 0;
if (NRF_DRV_UART_USE_UARTE)
{
result = nrfx_uarte_task_address_get(&p_instance->uarte,
(nrf_uarte_task_t)task);
}
else if (NRF_DRV_UART_USE_UART)
{
result = nrfx_uart_task_address_get(&p_instance->uart, task);
}
return result;
}
__STATIC_INLINE
uint32_t nrf_drv_uart_event_address_get(nrf_drv_uart_t const * p_instance,
nrf_uart_event_t event)
{
uint32_t result = 0;
if (NRF_DRV_UART_USE_UARTE)
{
result = nrfx_uarte_event_address_get(&p_instance->uarte,
(nrf_uarte_event_t)event);
}
else if (NRF_DRV_UART_USE_UART)
{
result = nrfx_uart_event_address_get(&p_instance->uart, event);
}
return result;
}
보이는 것 처럼 두 매크로를 이용해 nrfx_uarte.c
에 있는 함수를 호출하는지 혹은 nrfx_uart.c
에 있는 함수를 호출하는지 구분 합니다.
nrfx_uart.c
에는 실제 여러 중요한 함수들이 구현되어 있습니다. init()
함수 interrupt enable()
,rx()
, tx()
, irq_handler()
등과 같은 UART가 갖춰야할 기본적인 동작들이 정의 되어 있습니다 . 여기 nrfx_uart.c
에서 가장 눈에 들어오는 내용은 바로 uart_control_block_t
구조체 입니다.
typedef struct
{
void * p_context;
nrfx_uart_event_handler_t handler;
uint8_t const * p_tx_buffer;
uint8_t * p_rx_buffer;
uint8_t * p_rx_secondary_buffer;
volatile size_t tx_buffer_length;
size_t rx_buffer_length;
size_t rx_secondary_buffer_length;
volatile size_t tx_counter;
volatile size_t rx_counter;
volatile bool tx_abort;
bool rx_enabled;
nrfx_drv_state_t state;
} uart_control_block_t;
static uart_control_block_t m_cb[NRFX_UART_ENABLED_COUNT];
uart_control_block_t
구조체는 실제 UART의 인스턴스가 가지고 있어야 할 내용으로 구성된 구조체 입니다. (만약 c 파일이 아닌 cpp 였다면 class로 구성했겠죠?) 안의 내용을 살펴보면 그 밑부터 살펴보면 이벤트 핸들러 입니다. 특정 이벤트가 발생하면 처리할 함수가 들어갑니다. 다음은 tx_buffer 와 rx_buffer의 주소를 담을 포인터 변수 Rx의 두번째 버퍼 위치 각 버퍼의 길이 같은 여러 중요한 요소가 있습니다. (처음 p_context의 의미는 아직도 파악이 안됩니다...)