이번 개발일지 (3) 에서는 Nordic SDK의 nrfx_driver를 이용해 UART를 제어해 보도록 하겠습니다.
보통 이러한 튜토리얼은 SoC의 제작사에서 배포하는 예제 프로젝트를 복사해서 진행하며, Nordic SDK에서도 역시 많은 예제를 제공하여 SDK와 SoC의 사용법을 익힙니다. 또한 다양한 라이브러리를 제공하기 때문에 사용자는 이를 이용해 FIFO, Queue 등 기본적인 자료구조 혹은 Delay 같은 지연 방법은 SDK에 있는 라이브러리를 가져다 사용만 하면 됩니다.
그러나 이번 개발일지서는 SDK에서 제공하는 예제와 라이브러리를 사용하지 않고 아닌 SDK안의 드라이버만을 사용하여 주변장치를 제어해보도록 하겠습니다.
⚠️이번 개발일지에서는 예제 프로젝트를 복사하지 않지만 환경설정이 귀찮으시면 복사하시고 1장은 건너띄우셔도 됩니다.
Embedded Studio를 실행시켜 새로운 프로젝트를 생성해보겠습니다. File > New Project...
혹은 Ctrl + Shift + N
을 눌러 새 프로젝트 생성 윈도우를 띄웁니다. 여기서 🔗 nRF52832 개발일지 (0) 에서 다운로드 받았던 Nordic package에서 "A C/C++ executable for Nordic Semiconductor nRF." 를 실행시킵니다.
저는 프로젝트 이름을 DWM1001_UART 라고 지었습니다. Location에 있는 You'r Workspace는 자신의 Workspace로 지정해주시면 됩니다. 이제 Next를 눌러 세부사항을 정하겠습니다.
Target Processor는 nRF52832_xxAA 입니다. nRF52832_xxAB도 있지만 DWM1001에서는 xxAA 아키텍처를 사용하고있습니다. 두 모델은 RAM, ROM의 사이즈 차이가 있습니다.
Compiler는 기본적으로 Segger를 사용하지만 저는 gcc를 사용하겠습니다. 예제를 바로 복사하신 분들은 알겠지만 예제가 기본적으로 gcc Compiler를 사용하고 있습니다. 또 몇몇 라이브러리에서 gcc의 스탠다드 라이브러리를 호출하기도 합니다.
이제 Next 버튼을 눌러 프로젝트를 생성합시다.
Alt+Enter
혹은 Project Explorer 에서 Proejct 'DWM1001_UART'
를 우클릭해 option 윈도우를 띄우겠습니다.
Code > Preprocessor >
를 클릭한뒤, Configuration 옵션을 Debug에서 Common으로 바꿉니다.
이제,User Include Directories
를 클릭해 아래 PATH들을 추가해주세요.
$(ProjectDir)/nRF/Device/Include
$(nRF5_SDK_17)/components
$(nRF5_SDK_17)/components/boards
$(nRF5_SDK_17)/components/drivers_nrf/nrf_soc_nosd
$(nRF5_SDK_17)/components/libraries/experimental_section_vars
$(nRF5_SDK_17)/components/libraries/log
$(nRF5_SDK_17)/components/libraries/log/src
$(nRF5_SDK_17)/components/libraries/util
$(nRF5_SDK_17)/components/toolchain/cmsis/include
$(nRF5_SDK_17)/integration/nrfx
$(nRF5_SDK_17)/integration/nrfx/legacy
$(nRF5_SDK_17)/modules/nrfx
$(nRF5_SDK_17)/modules/nrfx/drivers/include
$(nRF5_SDK_17)/modules/nrfx/hal
$(nRF5_SDK_17)/modules/nrfx/mdk
.
맨 밑 .
은 "현재 디렉토리도 포함한다."라는 의미 이므로 포함해 주세요.
이번에는 Preprocessor Definitions
를 클릭해 DWM1001
을 이라는 매크로를 추가합니다. 이 매크로는 🔗 nRF52832 개발일지 (1) 에서 사용했습니다.
마지막으로 sdk_config.h
파일을 복사해 오겠습니다. 이 파일은 직접 작성하기에는 번거롭기 때문에 복사해 오겠습니다. $(nRF5_SDK_17)
매크로는 이번 🔗 nRF52832 개발일지 (0) 에서 미리 정의했습니다.
📁 $(SDK PATH)\examples\peripheral\uart\pca10040\blank\config
에서 복사합니다.
또한, sdk_config.h
편집은 CMSIS Configuration Wizard를 사용하여 편집하기 때문에 아래 링크를 가볍게 읽어보시길 권장합니다. ✌️
🔗 CMSIS Configuration Wizard Embedded Studio 적용 및 편집
이제 UART를 이용해 간단한 에코 프로그램 예제를 작성해 보겠습니다. UART를 이용해 에코 프로그램을 만드는 방법에는 크게 Busy Waiting(Polling) 과 Interrupt가 있습니다. 물론 DMA를 이용하는 방법도 있지만 Nordic에서는 DMA를 다른 인스턴스인 UARTE로써 사용하기 때문에 크게 두 가지라고 설명하겠습니다. 이번 예제는 Simple Test Code는 두 가지를 다 다루겠습니다.
코드작성에 앞서 sdk_config.h
파일을 잠깐만 손보겠습니다. 우리가 필요한 부분만 남기고 나머지는 지우겠습니다.
sdk_config.h
를 잘못 건드리면 에러가 나오니 저 처럼 쓸데없는부분 지우기를 좋아하는 분만 하셔도 됩니다. 🤣
빨간 밑줄이 간 부분은 우리가 필요없는 드라이브와 라이브러리 입니다. 저와 같은 sdk_config.h
를 제대로 복사해 오셨다면 아래에 작성된 라인을 지워주시면 됩니다.
51부터 ~ 265번라인 까지 , 483부터 ~ 614번라인 까지 모두 지웁니다.🗑️
⚠️ 라인넘버에 주의해서 지워주세요.
// <h> nRF_Drivers -> 49번 라인
...
// <e> NRFX_UART_ENABLED - nrfx_uart - UART peripheral driver -> 266번 라인
// <h> nRF_Libraries -> 483번 라인
...
// <h> nRF_Log -> 615번 라인
#### Busy Waiting Echo
📦 택배를 기다리기위해 문앞에서 아무것도 안하고 죽치고 앉아 있는 모습을 상상해 봅시다. 출근도 안하고 잠도 안자고 먹지도 않으며 택배를 기다립니다. 우리는 이러한 컨셉의 자원획득을 Busy Waiting 혹은 Polling 이라고 부릅니다. 🤣
우선 main.c
파일부터 살펴보도록 하겠습니다.
#include "nrfx_uart.h"
#include "boards.h"
nrfx_uart_t uart_inst = {NRF_UART0, 0};
char ch ;
int main(void) {
nrfx_uart_config_t uart_config = {UART_TX, UART_RX, 0, 0,
NULL, NRF_UART_HWFC_DISABLED,
NRF_UART_PARITY_EXCLUDED,
NRF_UART_BAUDRATE_115200, 7};
nrfx_uart_init(&uart_inst, &uart_config, NULL);
while(true) {
while(nrfx_uart_rx(&uart_inst, &ch, 1) != NRFX_SUCCESS);
while(nrfx_uart_tx(&uart_inst, &ch, 1) != NRFX_SUCCESS);
}
}
nrfx_uart_t uart_inst = {NRF_UART0, 0};
는 실제 UART 인스턴스를 만듭니다. nrfx_uart_t
타입의 실제 모습은 아래와 같습니다. NRF_UART0
는 레지스터의 주소를 담고 있고, 0
은 여러개의 UART 인스턴스 중 인덱스 번호 입니다.
이제 Busy Waiting이 어디서 구현되어 있는지 보겠습니다.
while(true) {
while(nrfx_uart_rx(&uart_inst, &ch, 1) != NRFX_SUCCESS);
while(nrfx_uart_tx(&uart_inst, &ch, 1) != NRFX_SUCCESS);
}
while(nrfx_uart_rx(&uart_inst, &ch, 1) != NRFX_SUCCESS)
구문은 UART를 통해 무엇인가 입력이 들어올때 까지 계속 무한정 기다리게 되어있습니다. 만약 무엇인가를 받았다면 nrfx_uart_rx() 함수에서는 NRFX_SUCCESS를 리턴하게 되어있고 while문을 탈출하게 됩니다. 또한 탈출 전에는 아무것도 하지 못합니다.
다음은 Interrupt 방식입니다. 인터럽트의 사전정의 부터 알아보도록 하겠습니다.
마이크로프로세서에서 인터럽트(interrupt, 문화어: 중단, 새치기)란 마이크로프로세서(CPU)가 프로그램을 실행하고 있을 때, 입출력 하드웨어 등의 장치에 예외상황이 발생하여 처리가 필요할 경우에 마이크로프로세서에게 알려 처리할 수 있도록 하는 것을 말한다.
출처: 위키백과 📚
위 에서 비유한 방법으로 다시 설명하자면, 우리가 택배를 기다릴때는 마냥 기다리지 않습니다. 우리는 출근도하고 밥도 먹고 잠도 자면서 택배를 기다립니다. 이렇게 일상생활을 하다 택배가 왔다는 문자가 오면 그때 택배를 수령합니다. 📦
이번에도 main.c
를 확인해 보겠습니다. 이전 코드와는 다른점이 몇가지 있습니다. 🤔
#include "nrfx_uart.h"
#include "boards.h"
nrfx_uart_t uart_inst = {NRF_UART0, 0};
char ch ;
void event_handler(nrfx_uart_event_t * p_event, void * p_context) {
switch(p_event->type){
case NRFX_UART_EVT_RX_DONE:
nrfx_uart_rx(&uart_inst, &ch, 1);
nrfx_uart_tx(&uart_inst, &ch, 1);
break;
case NRFX_UART_EVT_TX_DONE:
break;
case NRFX_UART_EVT_ERROR:
break;
}
}
int main(void) {
nrfx_uart_config_t uart_config = {UART_TX, UART_RX, 0, 0,
NULL, NRF_UART_HWFC_DISABLED,
NRF_UART_PARITY_EXCLUDED,
NRF_UART_BAUDRATE_115200, 7};
nrfx_uart_init(&uart_inst, &uart_config, event_handler);
nrf_uart_int_enable(NRF_UART0, NRF_UART_INT_MASK_RXDRDY | NRF_UART_INT_MASK_ERROR);
nrf_uart_event_clear(NRF_UART0, NRF_UART_EVENT_ERROR);
nrf_uart_event_clear(NRF_UART0, NRF_UART_EVENT_RXDRDY);
nrf_uart_task_trigger(NRF_UART0, NRF_UART_TASK_STARTRX);
while(true) {
; // Do nothing...
}
}
nrfx_uart_init(&uart_inst, &uart_config, event_handler)
초기화 부분에 evnet_handler() 라는 함수가 추가되었습니다. 이 함수는 Interrupt 이벤트가 발생했을때 이벤트를 처리해주는 콜백함수 입니다. 초기화 과정에 이러한 핸들러를 넣어주지 않는다면 nrfx_uart 드라이버는 Interrupt를 Enable 하지 않도록 설정되어 있습니다.
nrf_uart_int_enable(NRF_UART0, NRF_UART_INT_MASK_RXDRDY | NRF_UART_INT_MASK_ERROR);
nrf_uart_event_clear(NRF_UART0, NRF_UART_EVENT_ERROR);
nrf_uart_event_clear(NRF_UART0, NRF_UART_EVENT_RXDRDY);
nrf_uart_task_trigger(NRF_UART0, NRF_UART_TASK_STARTRX);
초기화 아래부분은 UART Interrupt 중 RX Interrupt를 Enable 시키기 위해서 추가해줬습니다. 참고로 저렇게 길게 쓰지 않고 nrfx_uart_rx() 함수를 호출하면 위 네 가지 함수를 호출해줍니다.
마지막으로 핸들러 내용을 살펴보겠습니다.
void event_handler(nrfx_uart_event_t * p_event, void * p_context) {
switch(p_event->type){
case NRFX_UART_EVT_RX_DONE:
nrfx_uart_rx(&uart_inst, &ch, 1);
nrfx_uart_tx(&uart_inst, &ch, 1);
break;
case NRFX_UART_EVT_TX_DONE:
break;
case NRFX_UART_EVT_ERROR:
break;
}
}
event_handler는 각각 어떤 이벤트가 발생했는지에 따라 처리하는 루틴이 다르게 구성되어 있습니다. RX 이벤트가 끝나면 RXD 레지스터에 있는 값을 ch
에 넣어 바로 nrfx_uart_tx()함수를 호출에 송신하게 되어있습니다.
Interrupt, BusyWaiting의 main.c
와 sdk_config.h
파일은 깃에 공유하도록 하겠습니다. 👏
sdk 를 어떻게 써야되나 고민이었는데 많이 도움되었습니다 !!