들어가며 — 손가락 하나로 점수를 올리는 일
이 시리즈는 라켓 스포츠(테니스, 배드민턴, 스쿼시, 탁구)의 점수를 손가락 제스처로 기록하는 BLE 링 디바이스 FingerScore를 직접 만들어 보는 학습 여정입니다. 1편에서는 전체 시스템 구성과 부품 선정을, 2편에서는 PCB와 전원 설계를 다뤘습니다. 이번 3편은 그 모든 하드웨어에 생명을 불어넣는 부분, 바로 MCU 펌웨어와 제스처 입력 처리입니다.
겉보기에 FingerScore가 하는 일은 단순합니다. 사용자가 득점하면 링을 낀 손가락을 까딱하거나 버튼을 누르고, 점수가 1점 올라가며, 그 값을 BLE로 휴대폰에 보냅니다. 그런데 이 단순한 동작을 코인 셀 배터리 하나로 몇 주씩 버티게 하려면, 펌웨어가 대부분의 시간을 자고 있어야 합니다. "필요할 때만 깨어나는" 이벤트 기반 설계가 이 글의 핵심 주제입니다.
이 글은 임베디드를 처음 접하는 분도 따라올 수 있도록 개념부터 차근차근 설명하지만, 코드는 실제로 컴파일되어 동작하는 수준을 목표로 합니다. 모든 C 코드는 코드 블록 안에 담겨 있으니, 그대로 옮겨 적으며 읽어도 됩니다.
MCU 펌웨어의 큰 그림
MCU(Micro Controller Unit) 펌웨어는 대부분 비슷한 골격을 가집니다. 전원이 들어오면 한 번 실행되는 초기화 단계, 그 뒤로 영원히 도는 메인 루프, 그리고 외부 사건에 반응하는 인터럽트입니다.
전원 ON
|
v
[ 초기화 ] <- 클럭, GPIO, 타이머, BLE 스택, 센서 설정
|
v
[ 메인 루프 ] ---> 할 일이 없으면 [ 슬립 ] 으로
^ |
| v
+------ [ 인터럽트로 깨어남 ] <----+
(버튼 눌림 / IMU 움직임 / 타이머)
전통적인 아두이노 스케치는 `loop()` 함수가 쉬지 않고 도는 폴링(polling) 방식입니다. 이는 배우기 쉽지만 CPU가 계속 깨어 있어 전류를 많이 먹습니다. FingerScore처럼 배터리 수명이 중요한 기기에서는, 메인 루프가 할 일이 없으면 곧장 슬립으로 들어가고 인터럽트가 깨워줄 때만 일하는 구조를 씁니다.
폴링 vs 인터럽트 vs 이벤트 기반
| 방식 | 동작 | 전류 | 반응성 | 적합한 곳 |
| --- | --- | --- | --- | --- |
| 폴링 | 루프가 계속 상태를 확인 | 높음 | 빠름 | 단순 프로토타입 |
| 인터럽트 | 사건 발생 시 핸들러 호출 | 중간 | 매우 빠름 | 버튼, 센서 입력 |
| 이벤트 기반 슬립 | 평소엔 슬립, 인터럽트로 깨어남 | 매우 낮음 | 빠름 | 배터리 웨어러블 |
FingerScore는 세 번째 방식을 택합니다. 평소에는 깊은 슬립(deep sleep)에 머물다가, 버튼 눌림이나 IMU의 움직임 감지 신호가 GPIO 인터럽트로 들어오면 깨어나 처리하고 다시 잠듭니다.
펌웨어 골격 코드
먼저 가장 위 단계의 골격을 봅시다. 이 예제는 ARM Cortex-M 계열(예: Nordic nRF52)을 가정한 의사 코드에 가까운 C이지만, 구조 자체는 어떤 MCU에서도 통용됩니다.
#include <stdint.h>
#include <stdbool.h>
/* 전역 이벤트 플래그. 인터럽트가 set 하고, 메인 루프가 처리한다. */
volatile bool g_button_event = false;
volatile bool g_motion_event = false;
static void system_init(void);
static void enter_low_power_sleep(void);
static void handle_score_increment(void);
int main(void)
{
system_init(); /* 클럭, GPIO, 타이머, BLE, IMU 초기화 */
for (;;) { /* 영원히 도는 메인 루프 */
if (g_button_event) {
g_button_event = false;
handle_score_increment();
}
if (g_motion_event) {
g_motion_event = false;
handle_score_increment();
}
/* 처리할 이벤트가 없으면 슬립으로 진입.
인터럽트가 들어오면 이 줄 다음부터 다시 실행된다. */
enter_low_power_sleep();
}
return 0; /* 실제로는 도달하지 않는다 */
}
핵심은 `volatile` 키워드입니다. 인터럽트 핸들러와 메인 루프가 같은 변수를 공유할 때, 컴파일러가 그 값을 레지스터에 캐싱해 두고 메모리를 다시 읽지 않는 최적화를 막아 줍니다. 이걸 빠뜨리면 "버튼을 눌렀는데 메인 루프가 영원히 알아채지 못하는" 악명 높은 버그가 생깁니다.
system_init의 역할
static void system_init(void)
{
clock_init(); /* 저전력 클럭 소스 선택 (예: 32.768kHz) */
gpio_init(); /* 버튼 핀을 입력 + 풀업, IMU 인터럽트 핀 설정 */
timer_init(); /* 디바운스용 타이머 */
ble_stack_init(); /* BLE 스택 활성화, 광고 파라미터 설정 */
imu_init(); /* 가속도계 임계값, 모션 인터럽트 활성화 */
/* 전원이 막 들어왔을 때 표시등을 잠깐 켰다 끄는 부팅 신호 */
led_blink_once();
}
각 `_init()` 함수의 세부 구현은 MCU SDK마다 다르지만, 순서에는 이유가 있습니다. 클럭을 가장 먼저 잡아야 타이머와 통신 주변장치가 정확한 시간 기준을 가집니다. GPIO와 인터럽트는 BLE 스택보다 먼저 설정해 두는 편이 안전합니다.
GPIO 인터럽트로 버튼 감지하기
버튼은 가장 직관적인 입력입니다. 누르면 핀의 전압이 바뀌고, 그 변화(에지, edge)를 MCU가 감지해 인터럽트를 발생시킵니다. FingerScore의 버튼은 한쪽이 GPIO 핀, 다른 쪽이 GND에 연결되며, 핀 내부 풀업 저항으로 평소에는 HIGH(1)를 유지하다가 누르면 LOW(0)로 떨어집니다. 이를 '폴링 다운'이 아니라 풀업 + 액티브 로우 구성이라고 합니다.
VCC
|
[내부 풀업 저항]
|
+---- GPIO2 (MCU 입력 핀) --- 평소 HIGH
|
[버튼]
|
GND <- 누르면 GPIO2 가 LOW 로 떨어짐 (falling edge)
falling edge에서 인터럽트가 발생하도록 설정하면, 버튼을 누르는 순간 핸들러가 호출됩니다.
/* GPIO 인터럽트 핸들러.
하드웨어가 falling edge를 감지하면 이 함수가 자동 호출된다. */
void GPIO_IRQHandler(void)
{
if (gpio_interrupt_pending(BUTTON_PIN)) {
gpio_clear_interrupt(BUTTON_PIN); /* 인터럽트 플래그 클리어 (필수) */
/* 핸들러 안에서는 무거운 일을 하지 않는다.
플래그만 세우고 빠르게 빠져나간다. */
g_button_event = true;
}
if (gpio_interrupt_pending(IMU_INT_PIN)) {
gpio_clear_interrupt(IMU_INT_PIN);
g_motion_event = true;
}
}
인터럽트 핸들러(ISR, Interrupt Service Routine) 작성의 황금률은 "짧고 빠르게"입니다. ISR 안에서 BLE 전송 같은 무거운 작업을 하면, 그 사이 다른 인터럽트가 막히고 시스템이 불안정해집니다. 그래서 ISR은 플래그만 세우고, 실제 처리는 메인 루프로 넘깁니다. 이 패턴을 흔히 'deferred work' 또는 'bottom-half'라고 부릅니다.
또 하나 잊기 쉬운 점은 `gpio_clear_interrupt()`로 인터럽트 플래그를 비워 주는 일입니다. 이걸 빼먹으면 같은 인터럽트가 끝없이 재호출되어 시스템이 멈춘 것처럼 보입니다.
디바운싱 — 한 번 눌렀는데 여러 번 세는 문제
물리 버튼에는 누구나 한 번쯤 당하는 함정이 있습니다. 사람 눈에는 한 번 눌린 것 같지만, 금속 접점이 닿는 순간 미세하게 튕기며 수 밀리초 동안 HIGH와 LOW를 여러 번 오갑니다. 이를 채터링(chattering) 또는 바운스(bounce)라고 합니다. 디바운싱을 하지 않으면 한 번의 클릭이 점수를 2점, 3점씩 올려 버립니다.
이상적인 버튼: ----+________________
|
실제 버튼: ----+_|‾|_|‾|________ <- 짧게 여러 번 튐
<-- 5~20ms -->
해결책은 두 가지입니다. 하드웨어 디바운스(RC 필터, 슈미트 트리거)와 소프트웨어 디바운스입니다. FingerScore는 부품을 줄이기 위해 소프트웨어 디바운스를 씁니다. 가장 흔한 방식은 "마지막 입력 이후 일정 시간(예: 30ms)이 지나지 않았으면 무시"하는 것입니다.
#include <stdint.h>
#include <stdbool.h>
#define DEBOUNCE_MS 30u
extern uint32_t millis(void); /* 부팅 후 경과 밀리초 반환 */
static uint32_t s_last_press_ms = 0;
/* 메인 루프에서 호출되는 디바운스 판정 함수.
유효한 새 입력이면 true, 채터링이면 false 를 반환한다. */
static bool button_debounced_accept(void)
{
uint32_t now = millis();
if ((now - s_last_press_ms) < DEBOUNCE_MS) {
return false; /* 너무 빨리 다시 들어옴 -> 채터링으로 간주, 무시 */
}
s_last_press_ms = now;
return true; /* 유효한 입력 */
}
`millis()`는 부팅 이후 경과한 밀리초를 돌려주는 함수로, 보통 타이머 인터럽트로 1ms마다 증가하는 카운터를 둡니다. `now - s_last_press_ms` 같은 부호 없는 정수 뺄셈은 카운터가 한 바퀴 돌아 오버플로(wrap-around)되어도 올바르게 동작하는 좋은 성질이 있어, 임베디드에서 자주 쓰는 관용구입니다.
메인 루프의 버튼 처리는 이렇게 디바운스를 끼워 넣습니다.
if (g_button_event) {
g_button_event = false;
if (button_debounced_accept()) {
handle_score_increment(); /* 유효한 클릭만 점수 반영 */
}
}
시간 기반 vs 안정화 기반 디바운스
| 방식 | 원리 | 장점 | 단점 |
| --- | --- | --- | --- |
| 시간 기반 | 일정 시간 내 재입력 무시 | 구현 간단 | 빠른 더블클릭 놓칠 수 있음 |
| 안정화(샘플링) | N회 연속 같은 값일 때만 확정 | 노이즈에 강함 | 타이머 폴링 필요 |
웨어러블 점수기처럼 빠른 더블클릭이 중요하지 않은 경우엔 시간 기반으로 충분합니다. 반대로 노이즈가 심한 환경이라면 일정 주기로 핀을 여러 번 읽어 연속으로 같은 값일 때만 인정하는 안정화 방식이 더 견고합니다.
IMU로 손가락 제스처 인식하기
버튼이 가장 확실하지만, FingerScore의 진짜 매력은 버튼 없이 손가락을 까딱하는 제스처로 점수를 올리는 것입니다. 이를 위해 IMU(Inertial Measurement Unit) 센서를 씁니다. IMU는 보통 3축 가속도계(accelerometer)와 3축 자이로스코프(gyroscope)를 한 칩에 담고 있습니다.
- 가속도계: 중력을 포함한 선형 가속도(어느 방향으로 얼마나 빠르게 움직이는가)를 측정합니다. 정지 상태에서도 중력 1g를 읽습니다.
- 자이로스코프: 각속도(얼마나 빠르게 회전하는가)를 측정합니다.
손가락을 빠르게 까딱하는 동작은 짧고 강한 가속도 변화로 나타납니다. 가장 단순한 인식법은 "전체 가속도 크기가 임계값을 넘으면 제스처로 본다"입니다. 세 축의 값을 제곱합의 제곱근으로 합치면 방향과 무관한 크기를 얻습니다.
크기 = sqrt(ax^2 + ay^2 + az^2)
정지: 약 1.0 g (중력만)
가벼운 흔들림: 1.2 ~ 1.5 g
의도적 까딱: 2.0 g 이상 <- 이 지점을 임계값으로
단순 임계값 상태 머신
단순히 "임계값 한 번 넘음 = 제스처"로 처리하면, 한 번의 동작이 여러 번 잡히거나 일상적인 떨림까지 점수로 들어갑니다. 그래서 상태 머신(state machine)을 둡니다. 정지 상태(IDLE)에서 큰 가속도를 만나면 '동작 중(ACTIVE)'으로 들어가고, 다시 조용해지면(임계값 아래로 일정 시간 유지) 한 번의 제스처로 확정한 뒤 IDLE로 돌아옵니다.
[IDLE] --(크기 > HIGH_TH)--> [ACTIVE]
^ |
| (크기 < LOW_TH 가 |
| QUIET_MS 동안 유지) |
+----- 제스처 1회 확정 <-------+
HIGH 임계값과 LOW 임계값을 다르게 두는 것을 히스테리시스(hysteresis)라고 합니다. 임계값 하나만 쓰면 경계 근처에서 신호가 들락거릴 때 상태가 마구 토글되는데, 두 임계값 사이에 '여유 구간'을 두면 안정적으로 한 번만 잡힙니다.
#include <stdint.h>
#include <stdbool.h>
#include <math.h>
#define HIGH_TH_G 2.0f /* 제스처 시작으로 보는 가속도 크기 */
#define LOW_TH_G 1.3f /* 동작이 끝났다고 보는 가속도 크기 */
#define QUIET_MS 120u /* LOW 아래로 이만큼 유지되면 동작 종료 */
typedef enum {
GESTURE_IDLE = 0,
GESTURE_ACTIVE
} gesture_state_t;
static gesture_state_t s_state = GESTURE_IDLE;
static uint32_t s_quiet_start_ms = 0;
extern uint32_t millis(void);
/* 가속도 3축(g 단위)을 받아 제스처 1회가 완성되면 true 반환 */
static bool gesture_update(float ax, float ay, float az)
{
float magnitude = sqrtf(ax * ax + ay * ay + az * az);
uint32_t now = millis();
switch (s_state) {
case GESTURE_IDLE:
if (magnitude > HIGH_TH_G) {
s_state = GESTURE_ACTIVE; /* 동작 시작 감지 */
}
return false;
case GESTURE_ACTIVE:
if (magnitude > LOW_TH_G) {
/* 아직 움직이는 중 -> 조용해진 타이머 리셋 */
s_quiet_start_ms = now;
return false;
}
/* LOW 아래로 떨어진 상태. 충분히 오래 조용했나? */
if ((now - s_quiet_start_ms) >= QUIET_MS) {
s_state = GESTURE_IDLE; /* 제스처 1회 완성, 다시 대기 */
return true;
}
return false;
default:
s_state = GESTURE_IDLE;
return false;
}
}
이 함수는 메인 루프에서 IMU 데이터가 새로 들어올 때마다 호출됩니다. 반환값이 `true`이면 의도된 제스처 한 번으로 보고 점수를 올립니다. 임계값과 `QUIET_MS`는 실제로 손에 끼고 흔들어 보며 조정해야 하는 값입니다. 처음부터 완벽한 숫자는 없으니, 로깅으로 실제 가속도 크기를 찍어 보며 튜닝하는 과정이 반드시 필요합니다.
IMU의 모션 웨이크업 기능
많은 저전력 IMU(예: ST LSM6DSO, Bosch BMI270)는 칩 내부에서 "일정 가속도 이상이 감지되면 인터럽트 핀을 토글"하는 하드웨어 기능을 내장합니다. 이를 활용하면 MCU는 깊은 슬립에 머물다가 의미 있는 움직임이 있을 때만 깨어납니다. 즉 MCU가 직접 가속도를 폴링하지 않아도 되어 전력을 크게 아낍니다. `imu_init()`에서 이 임계값과 인터럽트를 설정해 두는 이유입니다.
터치/플렉스 센서라는 대안
IMU 제스처가 부담스럽다면 더 단순한 입력 방식도 있습니다.
| 센서 | 원리 | 장점 | 단점 |
| --- | --- | --- | --- |
| 택트 버튼 | 기계식 접점 | 확실한 클릭감, 저렴 | 채터링, 마모 |
| 정전식 터치 | 손가락 정전용량 변화 | 움직이는 부품 없음 | 땀/장갑에 민감 |
| 플렉스 센서 | 휘면 저항 변화 | 손가락 구부림 감지 | 부피, 내구성 |
정전식 터치는 많은 MCU가 내장 주변장치로 지원해 핀 하나로 구현할 수 있고, 움직이는 부품이 없어 내구성이 좋습니다. 다만 땀이 많거나 장갑을 낀 상태에선 인식이 흔들릴 수 있습니다. 플렉스 센서는 손가락을 구부리는 동작 자체를 직접 읽을 수 있지만 링 폼팩터에 넣기엔 부피가 큽니다. FingerScore 1차 프로토타입은 택트 버튼으로 동작을 검증하고, 2차에서 IMU 제스처를 더하는 단계적 접근을 권합니다.
이벤트에서 점수 증가로
입력을 어떻게 받든, 결국 "한 번의 유효 입력 = 점수 +1 = BLE 전송"으로 수렴합니다. 이 흐름을 한 함수로 모아 둡니다.
#include <stdint.h>
static uint16_t s_score = 0;
extern void ble_send_score(uint16_t score);
extern void led_blink_once(void);
/* 유효한 입력(버튼 또는 제스처)이 확정되었을 때 호출된다. */
static void handle_score_increment(void)
{
s_score++; /* 점수 1 증가 */
led_blink_once(); /* 사용자에게 입력 인식을 피드백 */
ble_send_score(s_score); /* 현재 점수를 휴대폰으로 전송 */
}
여기서 BLE 전송은 ISR이 아니라 메인 루프 문맥에서 일어난다는 점이 중요합니다. 앞서 ISR에서는 플래그만 세웠기 때문에, 실제 무거운 무선 전송은 안전한 메인 루프에서 수행됩니다. 점수를 0으로 되돌리는 리셋이나, 한쪽이 한 게임을 이겼을 때의 처리 등은 다음 단계의 로직이지만, 가장 작은 단위는 이 한 줄짜리 증가입니다.
전체를 메인 루프에 합치면 다음과 같습니다.
for (;;) {
if (g_button_event) {
g_button_event = false;
if (button_debounced_accept()) {
handle_score_increment();
}
}
if (g_motion_event) {
g_motion_event = false;
float ax, ay, az;
imu_read_accel(&ax, &ay, &az); /* 최신 가속도 읽기 */
if (gesture_update(ax, ay, az)) {
handle_score_increment();
}
}
enter_low_power_sleep(); /* 할 일 끝, 다시 슬립 */
}
저전력 설계 — 잠드는 기술
웨어러블에서 전력은 곧 사용성입니다. 매일 충전해야 하는 점수 링은 아무도 끼지 않습니다. 저전력 설계의 핵심은 "CPU가 깨어 있는 시간을 최소화"하는 것입니다.
활성(active): 수 mA ~ 십수 mA (CPU + 무선 동작)
유휴(idle): 수백 uA
딥 슬립(deep): 1 uA 안팎 <- 평소 머무는 상태
숫자 단위에 주목하세요. 딥 슬립의 마이크로암페어(uA)는 활성의 밀리암페어(mA)보다 천 배가량 작습니다. 하루 중 깨어 있는 시간이 1퍼센트만 되어도 배터리 수명은 극적으로 달라집니다.
`enter_low_power_sleep()`의 내부는 MCU마다 다르지만 개념은 같습니다. CPU 코어를 멈추고, 필요 없는 주변장치 클럭을 끄되, 인터럽트를 받는 회로만 살려 둡니다.
static void enter_low_power_sleep(void)
{
/* 처리 대기 중인 이벤트가 있으면 자지 않는다 (경쟁 조건 방지). */
if (g_button_event || g_motion_event) {
return;
}
disable_unused_peripherals(); /* 불필요한 클럭/전원 차단 */
/* WFI = Wait For Interrupt. CPU를 멈추고 인터럽트를 기다린다.
인터럽트가 들어오면 이 줄 다음부터 다시 깨어난다. */
__asm volatile ("wfi");
}
`WFI`(Wait For Interrupt)는 ARM Cortex-M의 명령어로, CPU를 저전력 상태로 두고 인터럽트가 올 때까지 멈춥니다. 진입 직전에 다시 한 번 이벤트 플래그를 확인하는 이유는 경쟁 조건(race condition) 때문입니다. 플래그 확인과 슬립 진입 사이에 인터럽트가 끼어들면, 자칫 깨어날 이유를 놓치고 영원히 잠들 수 있습니다. 이 확인이 그 틈을 막아 줍니다.
펌웨어 개발 환경 갖추기
코드를 쓰는 것만큼이나 중요한 게 환경입니다. 처음 임베디드를 시작할 때 가장 막막한 부분이기도 합니다.
- SDK / 툴체인: MCU 제조사가 제공하는 SDK를 씁니다. Nordic nRF52라면 nRF Connect SDK(Zephyr 기반), Espressif ESP32라면 ESP-IDF가 표준입니다. 컴파일러는 보통 GCC(arm-none-eabi-gcc)입니다.
- 디버거 / 프로그래머: 코드를 칩에 굽고 단계 실행하려면 하드웨어 디버거가 필요합니다. SEGGER J-Link, ST-Link, 또는 보드에 내장된 디버그 프로브를 씁니다. GDB로 중단점(breakpoint)을 걸고 변수를 들여다봅니다.
- 로깅: 임베디드의 `printf`는 보통 UART(시리얼)나 RTT(Real-Time Transfer)로 PC에 메시지를 보냅니다. "여기까지 실행됐다"는 한 줄 로그가 디버깅의 절반입니다. 단, 로그 출력 자체가 전류를 먹으므로 양산 펌웨어에서는 줄이거나 끕니다.
RTOS를 쓸까, 베어메탈로 갈까
| 방식 | 특징 | 적합한 경우 |
| --- | --- | --- |
| 베어메탈 | RTOS 없이 메인 루프 + 인터럽트 | 작은 펌웨어, 학습 단계 |
| RTOS(FreeRTOS 등) | 태스크 스케줄링, 동기화 도구 | 동시 작업 많고 복잡할 때 |
FingerScore의 1차 펌웨어 정도면 베어메탈(메인 루프 + 인터럽트)로 충분합니다. 다만 BLE 스택이 내부적으로 RTOS를 요구하는 SDK(예: Zephyr)에서는 자연스럽게 태스크 위에서 작성하게 됩니다. 둘 중 무엇이든, 이 글에서 다룬 인터럽트·디바운스·상태 머신 개념은 그대로 적용됩니다.
테스트 — 손으로 흔들어 보기 전에
펌웨어 테스트는 PC 소프트웨어보다 까다롭습니다. 칩 위에서 도는 코드를 어떻게 검증할까요.
- 단위 테스트: 디바운스 함수나 제스처 상태 머신처럼 하드웨어와 무관한 순수 로직은 PC에서 테스트할 수 있습니다. `millis()` 같은 함수를 가짜(mock) 함수로 바꿔 시간을 마음대로 조작하면, "30ms 안에 두 번 들어온 입력은 한 번만 인정되는가"를 자동으로 검증할 수 있습니다.
- HIL(Hardware-in-the-Loop): 실제 하드웨어를 테스트 장비에 물려, 신호를 흘려보내고 결과를 확인하는 방식입니다. 예를 들어 버튼 핀에 정해진 펄스를 인가하고 BLE로 점수가 올바르게 나가는지 자동 측정합니다. 본격적인 양산 단계에서 도입합니다.
학습 단계에서는 순수 로직의 단위 테스트만 갖춰도 큰 도움이 됩니다. 상태 머신의 경계값(임계값 바로 위/아래, 조용한 시간 직전/직후)을 표로 정리해 테스트 케이스로 만들면, 손으로 흔들어 보기 전에 논리 오류를 거의 다 잡을 수 있습니다.
흔한 함정들
임베디드 입문자가 자주 빠지는 함정을 모았습니다.
- 스위치 채터링: 디바운싱을 빠뜨리면 한 번 클릭이 여러 점수로 잡힙니다. 가장 흔하고 가장 먼저 의심할 문제입니다.
- 인터럽트 플래그 미클리어: ISR에서 플래그를 비우지 않으면 같은 인터럽트가 끝없이 재발생해 시스템이 멈춘 듯 보입니다.
- `volatile` 누락: 인터럽트와 메인 루프가 공유하는 변수에 `volatile`을 빠뜨리면, 최적화 때문에 이벤트를 영영 놓칩니다.
- 누설 전류(current leakage): 입력 핀을 풀업/풀다운 없이 떠 있는(floating) 상태로 두면, 핀 전압이 떠다니며 미세한 전류가 새고 슬립 전류가 치솟습니다. 안 쓰는 핀도 명확한 상태로 묶어 둬야 합니다.
- ISR에서 무거운 작업: 핸들러 안에서 BLE 전송이나 긴 연산을 하면 다른 인터럽트가 막혀 시스템 반응성이 무너집니다. 플래그만 세우고 빠져나오세요.
- 슬립 직전 경쟁 조건: 이벤트 확인과 슬립 진입 사이의 틈에서 인터럽트를 놓치면 영원히 잠듭니다. 진입 직전 재확인으로 막습니다.
마치며
이번 글에서는 FingerScore의 두뇌인 MCU 펌웨어를 다뤘습니다. 초기화·메인 루프·인터럽트·슬립이라는 골격, GPIO 인터럽트로 버튼을 감지하는 법, 채터링을 잡는 소프트웨어 디바운스, IMU 임계값과 히스테리시스를 활용한 제스처 상태 머신, 그리고 이벤트에서 점수 증가로 이어지는 흐름까지, 컴파일 가능한 수준의 C 코드와 함께 살펴봤습니다. 핵심 철학은 단 하나, "평소엔 자고, 의미 있는 사건에만 깨어난다"는 이벤트 기반 저전력 설계입니다.
다음 4편에서는 여기서 만든 점수 값을 실제로 휴대폰까지 보내는 BLE 통신을 깊이 다룹니다. GATT 서비스와 캐릭터리스틱 설계, 광고와 연결 파라미터, 그리고 무선 구간에서의 전력 최적화까지, 펌웨어가 바깥 세상과 대화하는 방법을 이어서 풀어 가겠습니다.
참고 자료
- Nordic Semiconductor 개발자 문서: https://docs.nordicsemi.com
- Espressif ESP-IDF 프로그래밍 가이드: https://docs.espressif.com
- FreeRTOS 공식 문서: https://www.freertos.org
- Memfault Interrupt 임베디드 블로그: https://interrupt.memfault.com
- Adafruit Learn(센서/임베디드 튜토리얼): https://learn.adafruit.com
- ST LSM6DSO IMU 데이터시트: https://www.st.com/resource/en/datasheet/lsm6dso.pdf
- Bosch BMI270 IMU 제품 페이지: https://www.bosch-sensortec.com/products/motion-sensors/imus/bmi270/
현재 단락 (1/255)
이 시리즈는 라켓 스포츠(테니스, 배드민턴, 스쿼시, 탁구)의 점수를 손가락 제스처로 기록하는 BLE 링 디바이스 FingerScore를 직접 만들어 보는 학습 여정입니다. 1편에...