NU40 DK, BLE UART 서비스 구현 예제
이번 예제는 nRF52 계열 보드(NU40 DK)에서 Adafruit Bluefruit 라이브러리를 사용하여 BLE UART 통신 서비스를 구현하는 예제입니다.
스마트폰 같은 Central 장치에서 “1” 또는 “0”을 전송하면 Peripheral(보드)가 이를 수신하여 NU40 DK의 LED1(13번 핀)을 켜고 끄는 간단한 원격 제어 프로그램입니다.
시연
NU40 DK 보드에서 블루투스 인식이 안될 때는 s1, s2버튼을 동시에 누른채로 전원버튼을 눌러주면 블루투스가 잡힙니다
먼저 보드에 펌웨어 코드가 제대로 Flash 되지 않은 상태에서는 BARAM DFU라는 이름으로 nRF Connect 앱에서 잡힙니다.
제대로 코드를 굽게 된다면, nRF Connect 앱에서 코드에서 설정한 이름으로 디바이스를 찾을 수 있게 되는데

UART TX에서 아래 화살표를 눌러준 상태에서, UART RX에서 위쪽 화살표를 누르면

위와 같은 창이 뜨는데, 여기서 숫자 0또는 1을 보낼 것이므로 UTF8 에서 1을 보내보면?
꺼져있던 LED1번이 켜지는 것을 볼 수 있고, 반대로 0을 전송하면 LED가 꺼지게 됩니다.
Vs code 환경 설정
먼저 펌웨어 코드를 Build, Flash 하기 위해 vs code에서 아래 5개 확장 프로그램을 install 해줍니다.

그리고 SDK 설치해줍니다. 저는 v2.9.0 했어요
nRF Connect 확장프로그램을 열어주고
create a new application클릭Copy a sample클릭- 설치한 SDK 선택
Bluetooth LE UART service예제 선택Configuration설정

좌하단의 Add Configuration 누른 후, 우측에서 설치한 SDK 버전과 Toolchain 설정, 보드 nRF52840 설정 후, 빌드 진행
빌드가 완료되면 좌측 하단에 Flash 버튼으로 코드를 구워줍니다.

이렇게 빌드까지 완료했다면, 해당 예제를 기반으로 수정한 코드는 아래와 같습니다.
일부 참고해서 수정하셔도 되고, 전체 복붙하셔서 다시 빌드 후 플래시 하셔도 됩니다.
코드의 큰 전체 구조는
- 초기화(Setup)
- 이벤트 처리(Callback)
- 데이터 처리(Loop)
3가지로 나뉩니다.
전체 코드
#include <bluefruit.h>
#include <Adafruit_LittleFS.h>
#include <InternalFileSystem.h>
// BLE Service
BLEDfu bledfu; // OTA DFU service
BLEDis bledis; // device information
BLEUart bleuart; // uart over ble
BLEBas blebas; // battery
// callback invoked when central connects
void connect_callback(uint16_t conn_handle)
{
// Get the reference to current connection
BLEConnection* connection = Bluefruit.Connection(conn_handle);
char central_name[32] = { 0 };
connection->getPeerName(central_name, sizeof(central_name));
Serial.print("Connected to ");
Serial.println(central_name);
}
/**
* Callback invoked when a connection is dropped
* @param conn_handle connection where this event happens
* @param reason is a BLE_HCI_STATUS_CODE which can be found in ble_hci.h
*/
void disconnect_callback(uint16_t conn_handle, uint8_t reason)
{
(void) conn_handle;
(void) reason;
Serial.println();
Serial.print("Disconnected, reason = 0x"); Serial.println(reason, HEX);
}
// --- BLE 이벤트를 처리할 중앙 핸들러 ---
void ble_event_callback(ble_evt_t* event)
{
// 이벤트가 발생한 커넥션 핸들(식별자)
uint16_t conn_handle = event->evt.gap_evt.conn_handle;
// 이벤트의 종류(ID)에 따라 분기
switch (event->header.evt_id)
{
case BLE_GAP_EVT_CONNECTED:
{
// 연결 이벤트가 발생하면, 기존 connect_callback을 호출
connect_callback(conn_handle);
break;
}
case BLE_GAP_EVT_DISCONNECTED:
{
// 연결 해제 이벤트가 발생하면, 기존 disconnect_callback을 호출
uint8_t reason = event->evt.gap_evt.params.disconnected.reason;
disconnect_callback(conn_handle, reason);
break;
}
default:
break;
}
}
void setup() {
// (옵션) USB 시리얼 모니터를 사용한다면 초기화
Serial.begin(115200);
// LED 핀(13번)을 출력 모드로 설정하고 끈 상태로 시작
pinMode(13, OUTPUT);
digitalWrite(13, LOW);
// BLE 스택 초기화
Bluefruit.begin();
// (옵션) nRF Connect 앱에서 보일 장치 이름 설정
Bluefruit.setName("NU40_LED_Test");
// BLE UART 서비스 시작
bleuart.begin();
Bluefruit.setEventCallback(ble_event_callback);
// --- 광고 시작 함수 호출 ---
startAdv();
}
void startAdv(void)
{
// Advertising packet
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
// Include bleuart 128-bit uuid
Bluefruit.Advertising.addService(bleuart);
// Secondary Scan Response packet (optional)
// Since there is no room for 'Name' in Advertising packet
Bluefruit.ScanResponse.addName();
/* Start Advertising
* - Enable auto advertising if disconnected
* - Interval: fast mode = 20 ms, slow mode = 152.5 ms
* - Timeout for fast mode is 30 seconds
* - Start(timeout) with timeout = 0 will advertise forever (until connected)
*
* For recommended advertising interval
* https://developer.apple.com/library/content/qa/qa1931/_index.html
*/
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
}
void loop() {
// BLE UART를 통해 데이터가 수신되었는지 확인
if (bleuart.available()) {
// 수신된 데이터를 1바이트 읽음
char c = (char)bleuart.read();
if (c == '1') {
// '1'을 받으면 LED 켬
digitalWrite(13, HIGH); // LED_BUILTIN 대신 13번 핀 명시
// 스마트폰으로 "성공" 응답을 보냄
bleuart.print("Received 1: LED ON");
} else if (c == '0') {
// '0'을 받으면 LED 끔
digitalWrite(13, LOW); // LED_BUILTIN 대신 13번 핀 명시
// 스마트폰으로 "성공" 응답을 보냄
bleuart.print("Received 0: LED OFF");
} else {
// '1'이나 '0'이 아닌 다른 것을 받으면
// 받은 문자를 그대로 응답으로 보냄
bleuart.print("Received unknown char: ");
bleuart.write(c);
}
}
}
이 코드의 동작 방식은 두 가지가 혼합되어 있습니다.
이벤트 기반 (Event-Driven) 구조 : BLE 연결, 연결 끊김, 보안 설정 등 BLE 스택(SoftDevice)에서 발생하는 주요 상태 변경은 ‘이벤트’로 처리 setEventCallback(ble_event_callback)를 통해 이러한 이벤트가 발생할 때마다 ble_event_callback 함수가 자동으로 호출
폴링 (Polling) 구조 : 실제 데이터(문자 ‘1’, ‘0’)가 수신되었는지 확인하는 작업은 loop() 함수 내에서 bleuart.available()을 지속적으로 확인(폴링) 하는 방식으로 이루어진다.
이 두 구조가 맞물려, 평소에는 loop()를 돌며 데이터 수신을 기다리다가, 스마트폰이 연결되거나 끊어지는 ‘사건’이 발생하면 ble_event_callback이 끼어들어 해당 상태를 처리합니다.
1. 헤더 파일 및 전역 객체
#include <bluefruit.h>
// ... (다른 헤더)
// BLE Service
BLEDfu bledfu; // OTA DFU service
BLEDis bledis; // device information
BLEUart bleuart; // uart over ble
BLEBas blebas; // battery
-
#include <bluefruit.h>: Adafruit Bluefruit nRF52 라이브러리의 모든 핵심 기능을 가져옵니다. BLE 통신에 필요한 모든 기능 보유 -
BLEUart bleuart;: 이 코드의 핵심! BLE를 통해 가상의 시리얼 포트(UART) 통신을 할 수 있게 해주는 ‘서비스’ 객체. 스마트폰과 텍스트 데이터를 주고받는 통로 역할을 함 -
BLEDfu,BLEDis,BLEBas: 각각 펌웨어 업데이트(DFU), 장치 정보(DIS), 배터리 상태(BAS)를 위한 표준 BLE 서비스입니다. 이 코드에서는 선언만 해두고 아직 기능은 사용하지 않았음
2. setup() 함수 (초기화 및 설정)
setup()은 보드에 전원이 켜질 때 단 한 번 실행됨
void setup() {
Serial.begin(115200); // 1. USB 시리얼 모니터 켜기 (디버깅용)
pinMode(13, OUTPUT); // 2. 13번 LED 핀을 출력으로 설정
digitalWrite(13, LOW); // 3. LED를 끈 상태로 시작
Bluefruit.begin(); // 4. BLE 스택(SoftDevice) 초기화
Bluefruit.setName("NU40_LED_Test"); // 5. BLE 장치 이름 설정
bleuart.begin(); // 6. BLE UART 서비스 시작
// 7. BLE 이벤트 핸들러 등록
Bluefruit.setEventCallback(ble_event_callback);
// 8. BLE 광고(Advertising) 시작
startAdv();
}
-
Bluefruit.begin(): nRF52 칩의 BLE 라디오를 켜고 Nordic의 SoftDevice(BLE 프로토콜 스택)를 활성화한다 -
bleuart.begin(): bleuart 객체를 활성화 -
Bluefruit.setEventCallback(ble_event_callback): 앞으로 BLE와 관련한 모든 이벤트(연결, 끊김 등)가 발생하면 ble_event_callback 함수를 호출하게 설정 -
startAdv(): 아래에서 설명할 advertising을 시작하라는 명령
3. startAdv() 함수(광고)
주변에 패킷 전송
void startAdv(void)
{
// ... (광고 플래그, TxPower 설정)
// 1. 광고 패킷에 'BLE UART 서비스 UUID' 포함
Bluefruit.Advertising.addService(bleuart);
// 2. 스캔 응답 패킷에 '장치 이름' 포함
Bluefruit.ScanResponse.addName();
// 3. 연결이 끊어지면 자동으로 다시 광고 시작
Bluefruit.Advertising.restartOnDisconnect(true);
// ... (광고 주기 설정)
// 4. 광고 시작 (0 = 무한정)
Bluefruit.Advertising.start(0);
}
-
Bluefruit.Advertising.addService(bleuart): 스마트폰이 BLE 장치를 검색할 때, UART 서비스가 있다고 인식할 수 있게 해줌. 이게 없으면 스마트폰이 이 보드에 연결하더라도 UART 통신을 할 수 없다. -
Bluefruit.Advertising.restartOnDisconnect(true): 사용 편의성을 위한 중요한 설정입니다. 스마트폰과 연결이 끊어졌을 때, 다시 startAdv()를 호출하지 않아도 라이브러리가 알아서 광고를 다시 시작함.
4. BLE 이벤트 콜백(상태 변경 처리)
void ble_event_callback(ble_evt_t* event)
{
uint16_t conn_handle = event->evt.gap_evt.conn_handle;
// 발생한 이벤트의 종류(ID)에 따라 분기
switch (event->header.evt_id)
{
case BLE_GAP_EVT_CONNECTED: // 1. 연결 이벤트 발생 시
connect_callback(conn_handle);
break;
case BLE_GAP_EVT_DISCONNECTED: // 2. 연결 해제 이벤트 발생 시
...
disconnect_callback(conn_handle, reason);
break;
...
}
}
-
이 함수는 BLE 스택에서 이벤트가 발생할 때마다 자동으로 호출됩니다
-
switch문은 이벤트의 종류(ID)를 확인 -
BLE_GAP_EVT_CONNECTED: 스마트폰이 보드에 성공적으로 연결되었을 때 발생 이때connect_callback을 호출하여 USB 시리얼 모니터에 “Connected to…” 메시지를 출력 -
BLE_GAP_EVT_DISCONNECTED: 연결이 끊어졌을 때 발생. 이때disconnect_callback을 호출하여 “Disconnected…” 메시지를 출력
loop() 함수(실제 데이터 처리)
setup()이 끝난 후, loop() 함수는 무한히 반복 실행됩니다. 이 코드는 loop()에서 데이터 수신을 감시(폴링) 합니다.
void loop() {
// 1. BLE UART 버퍼에 수신된 데이터가 있는지 확인
if (bleuart.available()) {
// 2. 데이터가 있다면 1바이트 읽기
char c = (char)bleuart.read();
if (c == '1') {
// 3. '1'이면 LED 켜기
digitalWrite(13, HIGH);
// 4. 스마트폰으로 응답 전송
bleuart.print("Received 1: LED ON");
} else if (c == '0') {
// 3. '0'이면 LED 끄기
digitalWrite(13, LOW);
// 4. 스마트폰으로 응답 전송
bleuart.print("Received 0: LED OFF");
}
...
}
}
-
bleuart.available(): 스마트폰이 bleuart 서비스를 통해 데이터를 전송하면, 해당 데이터는 bleuart 객체 내부의 수신 버퍼에 쌓음available()은 이 버퍼에 읽을 데이터가 1바이트라도 있는지 확인합니다 -
bleuart.read(): 버퍼에서 데이터 1바이트를 꺼내 c 변수에 저장 -
digitalWrite(...): c의 값에 따라 실제 하드웨어(13번 핀 LED)를 제어 -
bleuart.print(...): 양방향 통신. 데이터를 받기만 하는 것이 아니라,bleuart의print함수를 이용해 스마트폰(Central)으로 다시 데이터를 전송. 스마트폰 앱의 수신 창에 문자열이 나타남.
다음글은 C# MAUI 기반의 실제 어플을 만들어서 BLE 통신을 해보도록 하겠습니다.
댓글남기기