diff --git a/CMakeLists.txt b/CMakeLists.txt index 4de34fb..30aec08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,20 @@ cmake_minimum_required(VERSION 3.20.0) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) -project(blinky) +project(vesiscan) -target_sources(app PRIVATE src/main.c) +target_include_directories(app PRIVATE + src + src/ble + src/drivers/battery + src/drivers/led +) + +target_sources(app PRIVATE + src/main.c + src/parser.c + src/power_control.c + src/ble/ble_service.c + src/drivers/battery/battery_adc.c + src/drivers/led/led_control.c +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..b17e7d9 --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +# VesiScan BASIC - Zephyr (nRF Connect SDK) + +nRF52840 기반 VesiScan BASIC 펌웨어의 Zephyr 포팅 프로젝트입니다. + +## 현재 구현 상태 + +| Stage | 내용 | 상태 | +|-------|------|------| +| 1 | GPIO + 전원 래치 + 버튼 상태머신 | 완료 | +| 1 | LED 제어 (green/orange 패턴) | 완료 | +| 2 | BLE 스택 + NUS (advertising, 연결, 재연결) | 완료 | +| 3 | 배터리 ADC (AIN2, 12bit, 저전압 자동 OFF) | 진행중 | +| 3 | 커맨드 파서 (msn? 구현) | 진행중 | +| - | Flash 저장소 (NVS) | 미구현 | +| - | 온도 센서 (TMP235, AIN3) | 미구현 | +| - | IMU (ICM42670P, I2C) | 미구현 | +| - | 피에조 + ADC121S051 | 미구현 | +| - | BLE 보안 (본딩, 패스키) | 미구현 | + +## 프로젝트 구조 + +``` +blinky/ +├── CMakeLists.txt # 빌드 설정 (소스 파일 등록) +├── prj.conf # Kconfig 설정 (기능 ON/OFF) +├── boards/ +│ └── nrf52840dk_nrf52840.overlay # Devicetree Overlay (핀 매핑) +└── src/ + ├── main.c # 메인 진입점 + 부팅 시퀀스 + 버튼 상태머신 + ├── main.h # 전역 상수/변수/타입 정의 + ├── debug_print.h # 디버그 출력 매크로 (RTT) + ├── parser.c / .h # BLE 커맨드 파서 (TAG+DATA+CRC16) + ├── power_control.c / .h # 전원 시퀀스 상태머신 + ├── ble/ + │ └── ble_service.c / .h # BLE NUS 서비스 (advertising, 연결, TX/RX) + └── drivers/ + ├── battery/ + │ └── battery_adc.c / .h # 배터리 전압 ADC (AIN2, 12bit) + └── led/ + └── led_control.c / .h # LED 패턴 제어 (k_timer 기반) +``` + +## 빌드 방법 + +### 일반 빌드 +소스 파일(.c/.h)만 변경한 경우. + +### Pristine Build +아래 파일을 변경한 경우 반드시 Pristine Build: +- `prj.conf` (Kconfig 설정) +- `boards/*.overlay` (Devicetree 핀 매핑) +- `CMakeLists.txt` (소스/include 경로 추가) + +nRF Connect for VS Code → Build 패널 → **Pristine Build** 또는 `build` 폴더 삭제 후 빌드. + +## 핀 매핑 (Devicetree Overlay) + +| 이름 | 핀 | 방향 | 역할 | +|------|-----|------|------| +| `PWR_HOLD` | P0.08 | Output, Active HIGH | 전원 래치 | +| `BUTTON_CHECK` | P1.08 | Input, Pull-up, Active LOW | 전원 버튼 | +| `LED_BLE` | P0.12 | Output, Active LOW | 초록 LED | +| `FUNCTION_LED` | P0.29 | Output, Active LOW | 주황 LED | +| ADC AIN2 | P0.04 | Analog Input | 배터리 전압 분압 | + +> UART0은 P0.08 충돌 방지를 위해 비활성화 (`status = "disabled"`) + +## 전원 버튼 동작 + +| 동작 | 조건 | 결과 | +|------|------|------| +| 부팅 (OFF→ON) | 2초 이상 홀드 | 전원 래치 + LED 깜빡임 + BLE advertising | +| 무시 | 부팅 후 2초 미만 릴리스 | 전원 OFF (래치 안 됨) | +| 전원 OFF (ON→OFF) | 버튼 놓은 뒤 다시 2초 홀드 | advertising 중지 + LED OFF + 3초 후 전원 차단 | + +## BLE 커맨드 프로토콜 + +### 패킷 포맷 +``` +[TAG 4바이트] [DATA N바이트] [CRC16 2바이트] +``` +- TAG: ASCII 4글자 (예: `msn?`, `mfv?`) +- DATA: uint16 little-endian 값들 +- CRC16: CRC-CCITT (초기값 0xFFFF) +- 응답 TAG: 첫 글자 `m` → `r` (예: `msn?` → `rsn:`) + +### 구현된 커맨드 + +| 커맨드 | 응답 | 기능 | +|--------|------|------| +| `msn?` | `rsn:` + uint16 mV | 배터리 전압 측정 | + +### 전체 커맨드 목록 (구현 예정) + +| # | 내용 | 명령 | 응답 | 비고 | +|---|------|------|------|------| +| 1 | 전원 OFF | `msq?` | `rsq:` | | +| 2 | 재부팅 | `mss?` | `rss:` | | +| 3 | 본딩 삭제 + 재부팅 | `msr?` | `rsr:` | | +| 4 | LED 상태 | `mls?` | - | | +| 5 | HW Version 읽기 | `mrh?` | `rrh:` | FDS | +| 6 | HW Version 쓰기 | `mwh?` | `rwh:` | FDS | +| 7 | Serial Number 읽기 | `mrs?` | `rrs:` | FDS | +| 8 | Serial Number 쓰기 | `mws?` | `rws:` | FDS | +| 9 | FW Version 읽기 | `mfv?` | `rfv:` | | +| 10 | Passkey 읽기 | `mqz?` | `rqz:` | FDS | +| 11 | Passkey 쓰기 | `mpz?` | `rpz:` | FDS | +| 12 | 배터리 측정 | `msn?` | `rsn:` | 구현 완료 | +| 13 | IMU 단발 측정 | `msp?` | `rsp:` | | +| 14 | 온도 측정 | `mso?` | `rso:` | | +| 15 | 단일 채널 측정 | `mec?` | `reb:` → `raa:` | 테스트용 | +| 16 | 모든 채널(6) 측정 | `maa?` | `reb:`(6개) → `raa:` | | +| 17 | 전체 측정 | `mbb?` | `rbb:` → `reb:`(6개) → `raa:` | 배터리+IMU+온도+피에조 | +| 18 | 측정 파라미터 읽기 | `mcf?` | `rcf:` | FDS | +| 19 | 측정 파라미터 쓰기 | `mcs?` | `rcs:` | FDS | + +## 디버그 로그 (RTT) + +- **VS Code**: nRF Connect 패널 → Connected Devices → 보드 선택 → 터미널 아이콘 +- **J-Link RTT Viewer**: Connection: USB, Target Device: NRF52840_XXAA + +### 로그 태그 +| 태그 | 내용 | +|------|------| +| `[BTN]` | 버튼 이벤트 (2s 홀드, 릴리스) | +| `[PWR]` | 전원 래치 ON/OFF | +| `[BOOT]` | 부팅 시퀀스 완료 | +| `[BLE]` | BLE 연결/해제/advertising/파라미터 | +| `[BLE RX]` | NUS 수신 데이터 (hex dump) | +| `[NUS TX]` | NUS 송신 | +| `[CMD]` | 커맨드 파서 처리 결과 | +| `[BATT]` | 배터리 ADC 측정값 | +| `[SYS]` | 슬립 진입 | + +## prj.conf 주요 설정 + +| 설정 | 값 | 역할 | +|------|-----|------| +| `CONFIG_GPIO` | y | GPIO 드라이버 | +| `CONFIG_LOG` | y | 로깅 시스템 | +| `CONFIG_LOG_BACKEND_RTT` | y | RTT 로그 출력 | +| `CONFIG_SERIAL` | n | UART 비활성화 (P0.08 충돌 방지) | +| `CONFIG_BT` | y | BLE 스택 | +| `CONFIG_BT_NUS` | y | Nordic UART Service | +| `CONFIG_BT_L2CAP_TX_MTU` | 247 | MTU 크기 | +| `CONFIG_BT_CTLR_TX_PWR_PLUS_8` | y | TX power +8dBm | +| `CONFIG_ADC` | y | ADC 드라이버 (배터리, 온도) | diff --git a/README.rst b/README.rst deleted file mode 100644 index ec23fe5..0000000 --- a/README.rst +++ /dev/null @@ -1,97 +0,0 @@ -.. zephyr:code-sample:: blinky - :name: Blinky - :relevant-api: gpio_interface - - Blink an LED forever using the GPIO API. - -Overview -******** - -The Blinky sample blinks an LED forever using the :ref:`GPIO API `. - -The source code shows how to: - -#. Get a pin specification from the :ref:`devicetree ` as a - :c:struct:`gpio_dt_spec` -#. Configure the GPIO pin as an output -#. Toggle the pin forever - -See :zephyr:code-sample:`pwm-blinky` for a similar sample that uses the PWM API instead. - -.. _blinky-sample-requirements: - -Requirements -************ - -Your board must: - -#. Have an LED connected via a GPIO pin (these are called "User LEDs" on many of - Zephyr's :ref:`boards`). -#. Have the LED configured using the ``led0`` devicetree alias. - -Building and Running -******************** - -Build and flash Blinky as follows, changing ``reel_board`` for your board: - -.. zephyr-app-commands:: - :zephyr-app: samples/basic/blinky - :board: reel_board - :goals: build flash - :compact: - -After flashing, the LED starts to blink and messages with the current LED state -are printed on the console. If a runtime error occurs, the sample exits without -printing to the console. - -Build errors -************ - -You will see a build error at the source code line defining the ``struct -gpio_dt_spec led`` variable if you try to build Blinky for an unsupported -board. - -On GCC-based toolchains, the error looks like this: - -.. code-block:: none - - error: '__device_dts_ord_DT_N_ALIAS_led_P_gpios_IDX_0_PH_ORD' undeclared here (not in a function) - -Adding board support -******************** - -To add support for your board, add something like this to your devicetree: - -.. code-block:: DTS - - / { - aliases { - led0 = &myled0; - }; - - leds { - compatible = "gpio-leds"; - myled0: led_0 { - gpios = <&gpio0 13 GPIO_ACTIVE_LOW>; - }; - }; - }; - -The above sets your board's ``led0`` alias to use pin 13 on GPIO controller -``gpio0``. The pin flags :c:macro:`GPIO_ACTIVE_HIGH` mean the LED is on when -the pin is set to its high state, and off when the pin is in its low state. - -Tips: - -- See :dtcompatible:`gpio-leds` for more information on defining GPIO-based LEDs - in devicetree. - -- If you're not sure what to do, check the devicetrees for supported boards which - use the same SoC as your target. See :ref:`get-devicetree-outputs` for details. - -- See :zephyr_file:`include/zephyr/dt-bindings/gpio/gpio.h` for the flags you can use - in devicetree. - -- If the LED is built in to your board hardware, the alias should be defined in - your :ref:`BOARD.dts file `. Otherwise, you can - define one in a :ref:`devicetree overlay `. diff --git a/boards/nrf52840dk_nrf52840.overlay b/boards/nrf52840dk_nrf52840.overlay new file mode 100644 index 0000000..1c0fae4 --- /dev/null +++ b/boards/nrf52840dk_nrf52840.overlay @@ -0,0 +1,90 @@ +/* + * VesiScan BASIC - Custom board overlay + * 프로젝트에서 사용하는 하드웨어 핀 정의 + * + * Pin mapping: + * POWER_HOLD: P0.08 (output, active high - latches power) + * POWER_BTN: P1.08 (input, active low - pressed = LOW) + * LED_BLE: P0.12 (output, active low) + * LED_ORANGE: P0.29 (output, active low) + * BATT_ADC: P0.04 (AIN2, battery voltage measurement) + */ + +#include +#include + +/* P0.08이 DK 기본 UART0 RX로 잡혀있어서 GPIO 출력 충돌 → 비활성화 */ +&uart0 +{ + status = "disabled"; +}; + +/* 배터리 ADC 채널 설정 (AIN2 = P0.04) */ +&adc +{ + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + + /* 배터리 전압 측정 채널 + * 1/6 gain + 0.6V 내부 레퍼런스 → 풀스케일 3.6V + * 12bit 해상도, 4X 오버샘플링, 40us 샘플링 */ + channel@2 + { + reg = <2>; + zephyr,gain = "ADC_GAIN_1_6"; + zephyr,reference = "ADC_REF_INTERNAL"; + zephyr,acquisition-time = ; + zephyr,input-positive = ; + zephyr,resolution = <12>; + zephyr,oversampling = <2>; /* 2^2 = 4X */ + }; +}; + +/* P0.04(AIN2)가 DK Arduino 헤더에 GPIO로 잡혀있어서 SAADC 충돌 → 비활성화 */ +&arduino_adc +{ + status = "disabled"; +}; + +/ { + /* ADC 채널을 코드에서 참조하기 위한 zephyr,user 노드 */ + zephyr,user + { + io-channels = <&adc 2>; + }; + + led + { + compatible = "gpio-leds"; + + LED_BLE: LED_BLE + { + gpios = <&gpio0 12 GPIO_ACTIVE_LOW>; + label = "Green LED"; + }; + + FUNCTION_LED: FUNCTION_LED + { + gpios = <&gpio0 29 GPIO_ACTIVE_LOW>; + label = "Orange LED"; + }; + }; + + pin + { + compatible = "gpio-keys"; + + PWR_HOLD: PWR_HOLD + { + gpios = <&gpio0 8 GPIO_ACTIVE_HIGH>; + label = "Power Hold Latch"; + }; + + BUTTON_CHECK: BUTTON_CHECK + { + gpios = <&gpio1 8 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + label = "Power Button"; + }; + }; +}; diff --git a/boards/nrf54h20dk_nrf54h20_cpuppr.overlay b/boards/nrf54h20dk_nrf54h20_cpuppr.overlay deleted file mode 100644 index cbc7b67..0000000 --- a/boards/nrf54h20dk_nrf54h20_cpuppr.overlay +++ /dev/null @@ -1,23 +0,0 @@ -/* SPDX-License-Identifier: Apache-2.0 */ - -/ { - aliases { - led0 = &led0; - }; - - leds { - compatible = "gpio-leds"; - led0: led_0 { - gpios = <&gpio9 0 GPIO_ACTIVE_HIGH>; - label = "Green LED 0"; - }; - }; -}; - -&gpiote130 { - status = "okay"; -}; - -&gpio9 { - status = "okay"; -}; diff --git a/boards/nrf54l15dk_nrf54l15_cpuapp_hpf_gpio.overlay b/boards/nrf54l15dk_nrf54l15_cpuapp_hpf_gpio.overlay deleted file mode 100644 index bd1ceb2..0000000 --- a/boards/nrf54l15dk_nrf54l15_cpuapp_hpf_gpio.overlay +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2024 Nordic Semiconductor ASA - * - * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause - */ - -&led0 { - gpios = <&hpf_gpio 9 GPIO_ACTIVE_HIGH>; -}; diff --git a/curcuit/VesiScan-Basic_curcuit.pdf b/curcuit/VesiScan-Basic_curcuit.pdf new file mode 100644 index 0000000..8d8766d Binary files /dev/null and b/curcuit/VesiScan-Basic_curcuit.pdf differ diff --git a/prj.conf b/prj.conf index 91c3c15..d351866 100644 --- a/prj.conf +++ b/prj.conf @@ -1 +1,52 @@ +# VesiScan BASIC - Zephyr configuration +# Stage 1: GPIO + Power Control + LED + Debug Logging +# Stage 2: BLE (NUS) + CONFIG_GPIO=y + +# Logging via RTT +CONFIG_LOG=y +CONFIG_LOG_MODE_DEFERRED=y +CONFIG_LOG_BACKEND_RTT=y +CONFIG_LOG_BACKEND_UART=n +CONFIG_USE_SEGGER_RTT=y +CONFIG_PRINTK=y +CONFIG_CONSOLE=y +CONFIG_RTT_CONSOLE=y +CONFIG_SERIAL=n +CONFIG_UART_CONSOLE=n + +# BLE stack +CONFIG_BT=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_DEVICE_NAME="VB026030000" +CONFIG_BT_DEVICE_NAME_DYNAMIC=y +CONFIG_BT_DEVICE_NAME_MAX=16 + +# BLE NUS (Nordic UART Service) +CONFIG_BT_NUS=y + +# GATT / MTU +CONFIG_BT_GATT_CLIENT=y +CONFIG_BT_L2CAP_TX_MTU=247 +CONFIG_BT_BUF_ACL_TX_SIZE=251 +CONFIG_BT_BUF_ACL_RX_SIZE=251 + +# Connection parameters +CONFIG_BT_PERIPHERAL_PREF_MIN_INT=24 +CONFIG_BT_PERIPHERAL_PREF_MAX_INT=24 +CONFIG_BT_PERIPHERAL_PREF_LATENCY=0 +CONFIG_BT_PERIPHERAL_PREF_TIMEOUT=400 + +# TX power +CONFIG_BT_CTLR_TX_PWR_PLUS_8=y + +# Security (dev mode - disabled for now) +CONFIG_BT_SMP=n + +# ADC (battery, temperature) +CONFIG_ADC=y + +# System +CONFIG_HEAP_MEM_POOL_SIZE=2048 +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 diff --git a/sample.yaml b/sample.yaml deleted file mode 100644 index de71191..0000000 --- a/sample.yaml +++ /dev/null @@ -1,12 +0,0 @@ -sample: - name: Blinky Sample -tests: - sample.basic.blinky: - tags: - - LED - - gpio - filter: dt_enabled_alias_with_parent_compat("led0", "gpio-leds") - depends_on: gpio - harness: led - integration_platforms: - - frdm_k64f diff --git a/src/ble/ble_service.c b/src/ble/ble_service.c new file mode 100644 index 0000000..3dbaf84 --- /dev/null +++ b/src/ble/ble_service.c @@ -0,0 +1,229 @@ +/******************************************************************************* + * @file ble_service.c + * @brief BLE NUS service module (Zephyr port) + * + * Original VesiScan-Basic BLE implementation ported to Zephyr/NCS: + * - NUS (Nordic UART Service) for data exchange + * - Advertising: 40ms interval, 3-min timeout + * - Connection: 30ms interval, 0 latency, 4s supervision timeout + * - TX power +8 dBm, 2M PHY preferred + * - Dev mode: no security / Production mode: bonding + passkey (TODO) + ******************************************************************************/ +#include +#include +#include +#include +#include +#include +#include + +#include "ble_service.h" +#include "main.h" +#include "debug_print.h" +#include "led_control.h" + +LOG_MODULE_REGISTER(ble_svc, LOG_LEVEL_INF); + +/*============================================================================== + * Module variables + *============================================================================*/ +static struct bt_conn *current_conn; +static ble_data_rx_cb_t rx_callback; + +/* BLE API는 ISR/콜백에서 직접 호출하면 안전하지 않을 수 있으므로 k_work 사용 */ +static struct k_work adv_restart_work; + +static void adv_restart_handler(struct k_work *work) +{ + ARG_UNUSED(work); + ble_advertising_start(); + led_set_state(LED_STATE_ADVERTISING); +} + +/*============================================================================== + * Advertising data + *============================================================================*/ +static const struct bt_data ad[] = +{ + BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), + BT_DATA(BT_DATA_NAME_COMPLETE, SERIAL_NUMBER, sizeof(SERIAL_NUMBER) - 1), +}; + +static const struct bt_data sd[] = +{ + BT_DATA_BYTES(BT_DATA_UUID128_ALL, BT_UUID_NUS_VAL), +}; + +/*============================================================================== + * Connection callbacks + *============================================================================*/ +static void connected(struct bt_conn *conn, uint8_t err) +{ + if (err) + { + DBG_PRINTF("[BLE] Connect failed (err %d) -> re-adv\r\n", err); + k_work_submit(&adv_restart_work); + return; + } + + current_conn = bt_conn_ref(conn); + ble_connection_st = true; + + /* 연결 정보 출력 */ + char addr_str[BT_ADDR_LE_STR_LEN]; + bt_addr_le_to_str(bt_conn_get_dst(conn), addr_str, sizeof(addr_str)); + DBG_PRINTF("[BLE] Peer: %s\r\n", addr_str); + + /* Connection parameters (30ms interval) */ + struct bt_le_conn_param conn_param = + { + .interval_min = BLE_MIN_CONN_INTERVAL, + .interval_max = BLE_MAX_CONN_INTERVAL, + .latency = BLE_SLAVE_LATENCY, + .timeout = BLE_CONN_SUP_TIMEOUT, + }; + bt_conn_le_param_update(conn, &conn_param); + + led_set_state(LED_STATE_OFF); + DBG_PRINTF("[BLE] Connected\r\n"); +} + +static void disconnected(struct bt_conn *conn, uint8_t reason) +{ + if (current_conn) + { + bt_conn_unref(current_conn); + current_conn = NULL; + } + + ble_connection_st = false; + DBG_PRINTF("[BLE] Disconnected (reason 0x%02x)\r\n", reason); + + /* 워크큐에서 advertising 재시작 */ + k_work_submit(&adv_restart_work); +} + +static void le_param_updated(struct bt_conn *conn, uint16_t interval, + uint16_t latency, uint16_t timeout) +{ + DBG_PRINTF("[BLE] Params: interval=%d latency=%d timeout=%d\r\n", + interval, latency, timeout); +} + +BT_CONN_CB_DEFINE(conn_callbacks) = +{ + .connected = connected, + .disconnected = disconnected, + .le_param_updated = le_param_updated, +}; + +/*============================================================================== + * NUS callbacks + *============================================================================*/ +static void nus_received(struct bt_conn *conn, const uint8_t *data, uint16_t len) +{ + if (rx_callback) + { + rx_callback(data, len); + } +} + +static void nus_sent(struct bt_conn *conn) +{ + DBG_PRINTF("[NUS TX] complete\r\n"); +} + +static struct bt_nus_cb nus_cb = +{ + .received = nus_received, + .sent = nus_sent, +}; + +/*============================================================================== + * Public functions + *============================================================================*/ +int ble_service_init(ble_data_rx_cb_t rx_cb) +{ + int err; + + rx_callback = rx_cb; + + k_work_init(&adv_restart_work, adv_restart_handler); + + /* Enable BLE stack */ + err = bt_enable(NULL); + if (err) + { + DBG_PRINTF("[BLE] bt_enable failed (err %d)\r\n", err); + return err; + } + DBG_PRINTF("[BLE] Stack enabled\r\n"); + + /* Initialize NUS */ + err = bt_nus_init(&nus_cb); + if (err) + { + DBG_PRINTF("[BLE] NUS init failed (err %d)\r\n", err); + return err; + } + DBG_PRINTF("[BLE] NUS initialized\r\n"); + + return 0; +} + +int ble_advertising_start(void) +{ + int err; + + struct bt_le_adv_param adv_param = BT_LE_ADV_PARAM_INIT( + BT_LE_ADV_OPT_CONN, + APP_ADV_INTERVAL, /* min interval */ + APP_ADV_INTERVAL + 16, /* max interval (slight window) */ + NULL /* undirected */ + ); + + err = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd)); + if (err) + { + DBG_PRINTF("[BLE] Adv start failed (err %d)\r\n", err); + return err; + } + + DBG_PRINTF("[BLE] Advertising started\r\n"); + return 0; +} + +int ble_advertising_stop(void) +{ + int err = bt_le_adv_stop(); + if (err) + { + DBG_PRINTF("[BLE] Adv stop failed (err %d)\r\n", err); + return err; + } + + DBG_PRINTF("[BLE] Advertising stopped\r\n"); + return 0; +} + +int ble_data_send(const uint8_t *data, uint16_t len) +{ + if (!current_conn) + { + DBG_PRINTF("[NUS TX] not connected\r\n"); + return -ENOTCONN; + } + + DBG_PRINTF("[NUS TX] %d bytes\r\n", len); + int err = bt_nus_send(current_conn, data, len); + if (err) + { + DBG_PRINTF("[NUS TX] failed (err %d)\r\n", err); + } + return err; +} + +bool ble_is_connected(void) +{ + return (current_conn != NULL); +} diff --git a/src/ble/ble_service.h b/src/ble/ble_service.h new file mode 100644 index 0000000..4712d07 --- /dev/null +++ b/src/ble/ble_service.h @@ -0,0 +1,44 @@ +/******************************************************************************* + * @file ble_service.h + * @brief BLE NUS service module (Zephyr port) + * + * Nordic UART Service (NUS) based BLE communication. + * Original: SoftDevice S140 + ble_nus SDK module + * Ported: Zephyr BLE + NCS bt_nus + ******************************************************************************/ +#ifndef BLE_SERVICE_H__ +#define BLE_SERVICE_H__ + +#include +#include + +/*============================================================================== + * BLE configuration + *============================================================================*/ +#define BLE_DEV_MODE 1 /* 1: Dev (no security), 0: Production */ + +#define APP_ADV_INTERVAL 64 /* 64 x 0.625ms = 40ms */ +#define APP_ADV_DURATION 18000 /* 18000 x 10ms = 180s (3 min) */ + +#define BLE_MIN_CONN_INTERVAL 24 /* 30ms / 1.25ms = 24 */ +#define BLE_MAX_CONN_INTERVAL 24 /* 30ms / 1.25ms = 24 */ +#define BLE_SLAVE_LATENCY 0 +#define BLE_CONN_SUP_TIMEOUT 400 /* 4000ms / 10ms = 400 */ + +#define BLE_TX_POWER 8 /* +8 dBm */ + +/*============================================================================== + * Callback types + *============================================================================*/ +typedef void (*ble_data_rx_cb_t)(const uint8_t *data, uint16_t len); + +/*============================================================================== + * Public functions + *============================================================================*/ +int ble_service_init(ble_data_rx_cb_t rx_cb); +int ble_advertising_start(void); +int ble_advertising_stop(void); +int ble_data_send(const uint8_t *data, uint16_t len); +bool ble_is_connected(void); + +#endif /* BLE_SERVICE_H__ */ diff --git a/src/debug_print.h b/src/debug_print.h new file mode 100644 index 0000000..1cea58c --- /dev/null +++ b/src/debug_print.h @@ -0,0 +1,23 @@ +/******************************************************************************* + * @file debug_print.h + * @brief Debug output macros (Zephyr LOG backend) + ******************************************************************************/ +#ifndef DEBUG_PRINT_H +#define DEBUG_PRINT_H + +#include + +/* + * DBG_PRINTF maps to printk for RTT/UART console output. + * For module-level logging use LOG_INF/LOG_DBG etc. + */ +#define ENABLE_PRINTF 1 + +#if ENABLE_PRINTF + #include + #define DBG_PRINTF(...) printk(__VA_ARGS__) +#else + #define DBG_PRINTF(...) +#endif + +#endif /* DEBUG_PRINT_H */ diff --git a/src/drivers/battery/battery_adc.c b/src/drivers/battery/battery_adc.c new file mode 100644 index 0000000..798adbe --- /dev/null +++ b/src/drivers/battery/battery_adc.c @@ -0,0 +1,196 @@ +/******************************************************************************* + * @file battery_adc.c + * @brief Battery voltage ADC measurement (Zephyr devicetree 기반) + * + * ADC 채널 설정은 디바이스트리 overlay에서 관리: + * - AIN2 (P0.04), Single-ended + * - 12-bit 해상도, 1/6 gain, 4X oversample, 40us acq time + * + * 변환 공식: mV = (ADC × 600 / 4095) × 6 × 1.42 + * → 프로젝트 고유 분압비(1.42x) 때문에 adc_raw_to_millivolts_dt() 사용 불가 + * + * 동작 모드: + * - 단독 측정 (msn? 커맨드) → battery_read_mv() 호출 + * - 주기 모니터링 (60초) → 저전압 10회 연속 시 자동 전원 OFF + * - info4 모드 (mbb?) → info_batt에 저장 + ******************************************************************************/ +#include +#include +#include +#include + +#include "battery_adc.h" +#include "debug_print.h" +#include "main.h" + +/*============================================================================== + * ADC 디바이스트리 설정 + *============================================================================*/ +#define ZEPHYR_USER_NODE DT_PATH(zephyr_user) + +/* 디바이스트리에서 ADC 채널 스펙 가져오기 (gain, ref, acq time, input, resolution, oversampling) */ +static const struct adc_dt_spec battery_adc = ADC_DT_SPEC_GET(ZEPHYR_USER_NODE); + +static int16_t adc_buffer; + +/*============================================================================== + * Module variables + *============================================================================*/ +volatile bool battery_saadc_done = false; +volatile uint16_t info_batt = 0; + +static struct k_timer battery_timer; +static struct k_work battery_work; +static uint8_t low_battery_cnt = 0; + +#define BATTERY_MONITOR_INTERVAL_MS 60000 /* 60초 주기 */ + +/*============================================================================== + * 전압 변환 + *============================================================================*/ + +/** @brief ADC raw → mV 변환 + * 공식: (raw × 600 / 4095) × 6 × 1.42 + * 정수 연산: raw × 600 × 6 × 142 / (4095 × 100) */ + +static int adc_raw_to_mv(int16_t raw) +{ + if (raw < 0) + { + raw = 0; + } + /* 오버플로 방지: 600 × 6 × 142 = 511,200 → raw 최대 4095 → 4095 × 511200 = ~2B → int32 OK */ + int32_t mv = ((int32_t)raw * 600 * 6 * 142) / (4095 * 100); + return (int)mv; +} + +/*============================================================================== + * Public functions + *============================================================================*/ + +void battery_adc_init(void) +{ + /* ADC 디바이스 준비 상태 확인 */ + if (!adc_is_ready_dt(&battery_adc)) + { + DBG_PRINTF("[BATT] ADC device not ready\r\n"); + return; + } + + /* 디바이스트리 설정으로 채널 구성 */ + int err = adc_channel_setup_dt(&battery_adc); + if (err) + { + DBG_PRINTF("[BATT] Channel setup failed (err %d)\r\n", err); + return; + } + + DBG_PRINTF("[BATT] ADC init OK (DT-based, ch=%d, res=%d, os=%d)\r\n", + battery_adc.channel_id, + battery_adc.resolution, + battery_adc.oversampling); +} + +int battery_read_mv(void) +{ + /* 매번 채널 재설정 (다른 센서와 SAADC 공유 시 필요) */ + int err = adc_channel_setup_dt(&battery_adc); + if (err) + { + DBG_PRINTF("[BATT] Channel setup failed (err %d)\r\n", err); + return -1; + } + + /* 디바이스트리에서 시퀀스 초기화 (channels, resolution, oversampling) */ + struct adc_sequence seq = {0}; + + err = adc_sequence_init_dt(&battery_adc, &seq); + if (err) + { + DBG_PRINTF("[BATT] Sequence init failed (err %d)\r\n", err); + return -1; + } + + seq.buffer = &adc_buffer; + seq.buffer_size = sizeof(adc_buffer); + + /* ADC 읽기 */ + err = adc_read_dt(&battery_adc, &seq); + if (err) + { + DBG_PRINTF("[BATT] Read failed (err %d)\r\n", err); + return -1; + } + + int16_t raw = adc_buffer; + + int mv = adc_raw_to_mv(raw); + //DBG_PRINTF("[BATT] raw=%d mv=%d\r\n", raw, mv); + + battery_saadc_done = true; + return mv; +} + +/*============================================================================== + * 주기 모니터링 타이머 + *============================================================================*/ + +/** @brief k_work 핸들러 — 스레드 컨텍스트에서 ADC 읽기 (adc_read는 블로킹이라 ISR 불가) */ + +static void battery_work_handler(struct k_work *work) +{ + ARG_UNUSED(work); + + if (processing) + { + return; + } + + int batt = battery_read_mv(); + + if (batt < 0) + { + return; + } + + /* 배터리 저전압 */ + if (batt <= LOW_BATTERY_VOLTAGE) + { + low_battery_cnt++; + DBG_PRINTF("[BATT] LOW! cnt=%d mv=%d\r\n", low_battery_cnt, batt); + + if (low_battery_cnt >= 10) + { + low_battery_cnt = 0; + DBG_PRINTF("[BATT] 10x low -> Power OFF\r\n"); + sleep_mode_enter(); + } + } + else + { + low_battery_cnt = 0; + } +} + +/* 타이머 ISR → k_work 예약 */ +static void battery_timer_handler(struct k_timer *timer) +{ + ARG_UNUSED(timer); + k_work_submit(&battery_work); +} + +void battery_timer_init(void) +{ + k_work_init(&battery_work, battery_work_handler); + k_timer_init(&battery_timer, battery_timer_handler, NULL); +} + +void battery_timer_start(void) +{ + k_timer_start(&battery_timer, K_MSEC(BATTERY_MONITOR_INTERVAL_MS), K_MSEC(BATTERY_MONITOR_INTERVAL_MS)); +} + +void battery_timer_stop(void) +{ + k_timer_stop(&battery_timer); +} diff --git a/src/drivers/battery/battery_adc.h b/src/drivers/battery/battery_adc.h new file mode 100644 index 0000000..79ba97d --- /dev/null +++ b/src/drivers/battery/battery_adc.h @@ -0,0 +1,28 @@ +/******************************************************************************* + * @file battery_adc.h + * @brief Battery voltage ADC measurement (Zephyr port) + * + * AIN2 채널, 12-bit, 1/6 gain, 4X oversample + * 저전압(3500mV) 10회 연속 감지 시 자동 전원 OFF + ******************************************************************************/ +#ifndef BATTERY_ADC_H__ +#define BATTERY_ADC_H__ + +#include +#include + +#define LOW_BATTERY_VOLTAGE 3500 /* 저전압 임계값 (mV) */ + +/* 배터리 측정 완료 플래그 (all_sensors 대기용) */ +extern volatile bool battery_saadc_done; + +/* info4 모드에서 배터리 전압 저장 (mV) */ +extern volatile uint16_t info_batt; + +void battery_adc_init(void); +int battery_read_mv(void); +void battery_timer_start(void); +void battery_timer_stop(void); +void battery_timer_init(void); + +#endif /* BATTERY_ADC_H__ */ diff --git a/src/drivers/led/led_control.c b/src/drivers/led/led_control.c new file mode 100644 index 0000000..e2ea12b --- /dev/null +++ b/src/drivers/led/led_control.c @@ -0,0 +1,246 @@ +/******************************************************************************* + * @file led_control.c + * @brief LED direct control driver (Zephyr port) + * + * k_timer based 2-color LED (green/orange) pattern control + * Simple on/off states use immediate GPIO, complex patterns use state machine + ******************************************************************************/ +#include +#include +#include +#include "led_control.h" +#include "debug_print.h" + +/*============================================================================== + * Devicetree LED specs + *============================================================================*/ +#define LED_BLE_NODE DT_NODELABEL(led_ble) +#define FUNCTION_LED_NODE DT_NODELABEL(function_led) + +static const struct gpio_dt_spec led_ble = GPIO_DT_SPEC_GET(LED_BLE_NODE, gpios); +static const struct gpio_dt_spec function_led = GPIO_DT_SPEC_GET(FUNCTION_LED_NODE, gpios); + +/*============================================================================== + * Color constants + *============================================================================*/ +#define COLOR_NONE 0 +#define COLOR_GREEN 1 +#define COLOR_ORANGE 2 + +/*============================================================================== + * Error pattern constants (State 7) + *============================================================================*/ +#define ERROR_BLINK_ON_MS 166 +#define ERROR_BLINK_OFF_MS 166 +#define ERROR_BLINK_COUNT 3 +#define ERROR_PAUSE_MS 1000 + +/*============================================================================== + * Pattern table + *============================================================================*/ +typedef struct { + uint32_t on_ms; + uint32_t off_ms; + uint8_t color; + bool repeat; +} led_pattern_t; + +static const led_pattern_t m_patterns[LED_STATE_COUNT] = { + [LED_STATE_OFF] = { 0, 0, COLOR_NONE, false }, + [LED_STATE_POWER_ON] = { 2000, 0, COLOR_GREEN, false }, + [LED_STATE_POWER_OFF] = { 2000, 0, COLOR_GREEN, false }, + [LED_STATE_ADVERTISING] = { 500, 500, COLOR_GREEN, true }, + [LED_STATE_DETACH_WARNING] = { 1000, 3000, COLOR_GREEN, true }, + [LED_STATE_ALIGN_SEARCHING] = { 1000, 1000, COLOR_ORANGE, true }, + [LED_STATE_ALIGN_COMPLETE] = { 3000, 1000, COLOR_GREEN, true }, + [LED_STATE_ERROR] = { 0, 0, COLOR_ORANGE, true }, +}; + +/*============================================================================== + * Module variables + *============================================================================*/ +static struct k_timer m_led_timer; + +static led_state_t m_current_state = LED_STATE_OFF; +static bool m_phase_on; + +/* Error pattern state machine */ +static uint8_t m_error_blink_cnt; +static uint8_t m_error_phase; + +/*============================================================================== + * GPIO helpers + *============================================================================*/ +static inline void led_ble_on(void) { gpio_pin_set_dt(&led_ble, 1); } +static inline void led_ble_off(void) { gpio_pin_set_dt(&led_ble, 0); } +static inline void function_led_on(void) { gpio_pin_set_dt(&function_led, 1); } +static inline void function_led_off(void) { gpio_pin_set_dt(&function_led, 0); } + +static void led_all_off(void) +{ + led_ble_off(); + function_led_off(); +} + +static void led_color_on(uint8_t color) +{ + led_all_off(); + if (color == COLOR_GREEN) led_ble_on(); + else if (color == COLOR_ORANGE) function_led_on(); +} + +/*============================================================================== + * Timer helper + *============================================================================*/ +static void timer_start_ms(uint32_t ms) +{ + if (ms == 0) return; + k_timer_start(&m_led_timer, K_MSEC(ms), K_NO_WAIT); +} + +/*============================================================================== + * Error pattern state machine + *============================================================================*/ +static void error_pattern_start(void) +{ + m_error_blink_cnt = 0; + m_error_phase = 0; + led_color_on(COLOR_ORANGE); + timer_start_ms(ERROR_BLINK_ON_MS); +} + +static void error_pattern_tick(void) +{ + switch (m_error_phase) + { + case 0: /* ON period done -> OFF */ + led_all_off(); + m_error_phase = 1; + timer_start_ms(ERROR_BLINK_OFF_MS); + break; + case 1: /* OFF period done */ + m_error_blink_cnt++; + if (m_error_blink_cnt < ERROR_BLINK_COUNT) { + m_error_phase = 0; + led_color_on(COLOR_ORANGE); + timer_start_ms(ERROR_BLINK_ON_MS); + } else { + m_error_phase = 2; + timer_start_ms(ERROR_PAUSE_MS); + } + break; + case 2: /* Pause done -> restart */ + m_error_blink_cnt = 0; + m_error_phase = 0; + led_color_on(COLOR_ORANGE); + timer_start_ms(ERROR_BLINK_ON_MS); + break; + default: + break; + } +} + +/*============================================================================== + * Timer callback + *============================================================================*/ +static void led_timer_handler(struct k_timer *timer) +{ + ARG_UNUSED(timer); + + if (m_current_state == LED_STATE_ERROR) + { + error_pattern_tick(); + return; + } + + const led_pattern_t *p = &m_patterns[m_current_state]; + + if (m_phase_on) + { + /* ON -> OFF transition */ + led_all_off(); + m_phase_on = false; + + if (p->off_ms > 0) + { + timer_start_ms(p->off_ms); + } + else if (!p->repeat) + { + if (m_current_state == LED_STATE_POWER_OFF) { + led_all_off(); + m_current_state = LED_STATE_OFF; + } + /* POWER_ON: stays lit, no timer */ + } + } + else + { + /* OFF -> ON transition */ + if (p->repeat) + { + led_color_on(p->color); + m_phase_on = true; + timer_start_ms(p->on_ms); + } + } +} + +/*============================================================================== + * Public functions + *============================================================================*/ + +void led_init(void) +{ + gpio_pin_configure_dt(&led_ble, GPIO_OUTPUT_INACTIVE); + gpio_pin_configure_dt(&function_led, GPIO_OUTPUT_INACTIVE); + led_all_off(); + + k_timer_init(&m_led_timer, led_timer_handler, NULL); + + m_current_state = LED_STATE_OFF; +} + +void led_set_state(led_state_t state) +{ + if (state >= LED_STATE_COUNT) return; + + + k_timer_stop(&m_led_timer); + led_all_off(); + + m_current_state = state; + m_phase_on = false; + + const led_pattern_t *p = &m_patterns[state]; + + switch (state) { + case LED_STATE_OFF: + break; + case LED_STATE_ERROR: + error_pattern_start(); + break; + default: + led_color_on(p->color); + m_phase_on = true; + + if (p->on_ms > 0) + { + timer_start_ms(p->on_ms); + } + break; + } +} + +led_state_t led_get_state(void) +{ + return m_current_state; +} + +void led_ble_solid(void) +{ + k_timer_stop(&m_led_timer); + led_all_off(); + led_ble_on(); + m_current_state = LED_STATE_OFF; /* no pattern running */ +} diff --git a/src/drivers/led/led_control.h b/src/drivers/led/led_control.h new file mode 100644 index 0000000..20090cb --- /dev/null +++ b/src/drivers/led/led_control.h @@ -0,0 +1,34 @@ +/******************************************************************************* + * @file led_control.h + * @brief LED direct control driver (Zephyr port) + * + * Green (P0.12) + Orange (P0.29) 2-color LED with k_timer based patterns + ******************************************************************************/ +#ifndef LED_CONTROL_H__ +#define LED_CONTROL_H__ + +#include +#include + +/*============================================================================== + * LED state enumeration + *============================================================================*/ +typedef enum +{ + LED_STATE_OFF = 0, + LED_STATE_POWER_ON, /* 1: Green ON 2s -> stay on */ + LED_STATE_POWER_OFF, /* 2: Green ON 2s -> OFF */ + LED_STATE_ADVERTISING, /* 3: Green blink 500ms/500ms */ + LED_STATE_DETACH_WARNING, /* 4: Green 1s on / 3s off */ + LED_STATE_ALIGN_SEARCHING, /* 5: Orange blink 1s/1s */ + LED_STATE_ALIGN_COMPLETE, /* 6: Green 3s on / 1s off */ + LED_STATE_ERROR, /* 7: Orange 3Hz x3 / 1s off */ + LED_STATE_COUNT +} led_state_t; + +void led_init(void); +void led_set_state(led_state_t state); +led_state_t led_get_state(void); +void led_ble_solid(void); + +#endif /* LED_CONTROL_H__ */ diff --git a/src/main.c b/src/main.c index 4cab496..0da0a2d 100644 --- a/src/main.c +++ b/src/main.c @@ -1,48 +1,340 @@ -/* - * Copyright (c) 2016 Intel Corporation +/******************************************************************************* + * @file main.c + * @brief VesiScan BASIC - Zephyr 포팅 (Stage 1: GPIO + 전원 + LED + BLE) * - * SPDX-License-Identifier: Apache-2.0 - */ - -#include + * 부팅 시퀀스: + * Phase 1: 전원 유지 핀, GPIO, 타이머, LED 초기화 + * Phase 2: BLE 스택 + NUS 초기화, advertising 시작 + * Phase 3+: (이후 단계: 센서 등) + * + * 전원 버튼 상태머신 (5ms 폴링, k_timer): + * [OFF→ON] 버튼 2초 이상 유지 → P0.08 래치 ON + 녹색 LED 깜빡임 + * 2초 미만 놓으면 전원 OFF (래치 안 됨) + * [ON→OFF] 버튼 2초 이상 유지 → sleep_mode_enter() → 전원 차단 + ******************************************************************************/ #include #include +#include -/* 1000 msec = 1 sec */ -#define SLEEP_TIME_MS 1000 +#include "main.h" +#include "debug_print.h" +#include "power_control.h" +#include "led_control.h" +#include "ble_service.h" +#include "battery_adc.h" +#include "parser.h" -/* The devicetree node identifier for the "led0" alias. */ -#define LED0_NODE DT_ALIAS(led0) +LOG_MODULE_REGISTER(vesiscan, LOG_LEVEL_INF); + +/*============================================================================== + * Devicetree GPOP Settings + *============================================================================*/ +#define POWER_HOLD_NODE DT_NODELABEL(pwr_hold) // 전원 래치 +#define POWER_BTN_NODE DT_NODELABEL(button_check) // 전원 버튼 + +static const struct gpio_dt_spec power_hold = GPIO_DT_SPEC_GET(POWER_HOLD_NODE, gpios); +static const struct gpio_dt_spec power_btn = GPIO_DT_SPEC_GET(POWER_BTN_NODE, gpios); + +/*============================================================================== + * Timer + *============================================================================*/ +static struct k_timer m_power_on_delay_timer; /* 전원 버튼 폴링용 (5ms 싱글샷) */ +static struct k_timer m_power_off_delay_timer; /* 전원 OFF 지연용 (3초 후 전원 차단) */ + +/*============================================================================== + * 전역 변수 + *============================================================================*/ +volatile bool ble_connection_st = false; /* BLE 연결 상태 */ +volatile bool processing = false; /* 센서 데이터 처리 중 플래그 */ +bool bond_data_delete = true; /* 본딩 데이터 삭제 요청 */ +uint32_t m_life_cycle = 0; /* 디바이스 수명 카운터 */ +uint8_t m_reset_status = 1; /* 리셋 상태 코드 */ +char SERIAL_NO[SERIAL_NO_LENGTH]; /* 시리얼 번호 */ +char HW_NO[HW_NO_LENGTH]; /* 하드웨어 번호 */ +char m_static_passkey[PASSKEY_LENGTH]; /* BLE 정적 패스키 */ + +static uint16_t cnt_s; /* 전원 버튼 폴링 카운터 (5ms 단위) */ +static bool device_on = false; /* 디바이스 전원 상태 (래치 완료 여부) */ +static bool boot_btn_released = false; /* 부팅 후 버튼 놓았는지 여부 */ /* - * A build error on this line means your board is unsupported. - * See the sample documentation for information on how to fix this. + * BLE advertising 제어용 워크 아이템 + * + * BLE HCI 명령(bt_le_adv_start/stop 등)은 내부적으로 세마포어 대기가 필요하므로 + * 인터럽트 컨텍스트(타이머 콜백 등)에서 직접 호출하면 커널 oops가 발생한다. + * (증상: "Controller unresponsive, command opcode 0x2006 timeout") + * + * k_work를 사용하면 시스템 워크큐 스레드에서 실행되므로 안전하게 BLE API 호출 가능. + * main_s() 타이머 콜백에서 k_work_submit()으로 예약하여 사용한다. */ -static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios); +static struct k_work adv_start_work; +static struct k_work adv_stop_work; +/* 시스템 워크큐에서 BLE advertising 시작 */ +static void adv_start_work_handler(struct k_work *work) +{ + ARG_UNUSED(work); + ble_advertising_start(); +} + +/* 시스템 워크큐에서 BLE advertising 중지 */ +static void adv_stop_work_handler(struct k_work *work) +{ + ARG_UNUSED(work); + ble_advertising_stop(); +} + +/*============================================================================== + * BLE RX 콜백 + *============================================================================*/ +static void ble_rx_handler(const uint8_t *data, uint16_t len) +{ + DBG_PRINTF("[BLE RX] %d bytes: ", len); + for (uint16_t i = 0; i < len && i < 32; i++) + { + DBG_PRINTF("%02X ", data[i]); + } + if (len > 32) + { + DBG_PRINTF("..."); + } + DBG_PRINTF("\r\n"); + dr_parser(data, len); +} + +/*============================================================================== + * 전원 유지 (POWER_HOLD) 제어 + *============================================================================*/ + +/* 전원 유지 핀(P0.08) 초기화 - 아직 래치하지 않음 + * 버튼을 물리적으로 누르고 있는 동안만 전원 유지됨 + * 2초 후 main_s()에서 래치(HIGH) 설정 */ +static void power_hold_init(void) +{ + gpio_pin_configure_dt(&power_hold, GPIO_OUTPUT_INACTIVE); +} + +/* 전원 ON/OFF 제어 - P0.08 핀으로 물리적 전원 래치/해제 */ +static void power_control_handler(on_off_cont_t device_power_st) +{ + if (device_power_st == OFF) { + gpio_pin_set_dt(&power_hold, 0); /* P0.08 LOW → 전원 래치 해제 → 전원 차단 */ + DBG_PRINTF("[PWR] OFF\r\n"); + } else { + gpio_pin_set_dt(&power_hold, 1); /* P0.08 HIGH → 전원 유지 */ + DBG_PRINTF("[PWR] ON\r\n"); + } +} + +/*============================================================================== + * GPIO 초기화 + *============================================================================*/ +static void gpio_init(void) +{ + gpio_pin_configure_dt(&power_btn, GPIO_INPUT); /* 전원 버튼(P1.08) 입력 설정 */ + DBG_PRINTF("[GPIO] OK (BTN=%d)\r\n", gpio_pin_get_dt(&power_btn)); + +} + +/*============================================================================== + * 기본 설정값 로드 + *============================================================================*/ +static void load_default_config(void) +{ + memset(SERIAL_NO, 0, SERIAL_NO_LENGTH); + memcpy(SERIAL_NO, SERIAL_NUMBER, strlen(SERIAL_NUMBER)); + + memset(m_static_passkey, 0, PASSKEY_LENGTH); + memcpy(m_static_passkey, DEFAULT_PASSKEY, PASSKEY_LENGTH); + + m_reset_status = 1; + bond_data_delete = true; + + DBG_PRINTF("[CFG] Default (S/N=%s)\r\n", SERIAL_NO); +} + +/*============================================================================== + * 전원 OFF 타임아웃 콜백 + * LED 표시 후 3초(POWER_OFF_DELAY) 경과 시 호출되어 물리적 전원 차단 + *============================================================================*/ +static void t_power_off_timeout_handler(struct k_timer *timer) +{ + ARG_UNUSED(timer); + DBG_PRINTF("[PWR] Off timeout\r\n"); + led_set_state(LED_STATE_OFF); + power_control_handler(OFF); +} + +/*============================================================================== + * 슬립 모드 / 전원 OFF + * LED로 사용자에게 알린 후 3초 뒤 전원 차단 + *============================================================================*/ +void sleep_mode_enter(void) +{ + led_set_state(LED_STATE_POWER_ON); + DBG_PRINTF("[SYS] Sleep\r\n"); + k_timer_start(&m_power_off_delay_timer, K_MSEC(POWER_OFF_DELAY), K_NO_WAIT); +} + +void device_power_off(void) +{ + led_set_state(LED_STATE_POWER_OFF); + k_timer_start(&m_power_off_delay_timer, K_MSEC(POWER_OFF_DELAY), K_NO_WAIT); +} + +/*============================================================================== + * 전원 버튼 상태머신 (5ms 폴링) + * + * [부팅 시퀀스] (device_on == false) + * 1. 버튼 누름 → 전원 공급 → MCU 부팅 → 5ms마다 폴링 시작 + * 2. cnt_s < 400 (2초 미만)에서 놓으면 → 전원 OFF (래치 안 됨) + * 3. cnt_s == 400 (2초) 도달 → P0.08 래치 ON + 녹색 LED 깜빡임 + * 4. 버튼 놓으면 → 깜빡임 유지, 폴링 계속 + * + * [전원 OFF 시퀀스] (device_on == true) + * 1. 버튼 2초 이상 누름 → sleep_mode_enter() → 전원 차단 + * 2. 버튼 2초 미만 놓으면 → 카운터 리셋, 무시 + *============================================================================*/ +#define BOOT_THRESHOLD 400 /* 5ms x 400 = 2초 */ + +static void main_s(struct k_timer *timer) +{ + ARG_UNUSED(timer); + + bool button_pressed = (gpio_pin_get_dt(&power_btn) == 1); + + if (!device_on) /* ── 부팅 시퀀스 (OFF → ON) ── */ + { + if (!button_pressed) /* 버튼 놓음 → 2초 미만이면 전원 OFF */ + { + DBG_PRINTF("[BTN] Short press (%d) -> OFF\r\n", cnt_s); + power_control_handler(OFF); + cnt_s = 0; + } + else /* 버튼 계속 누르고 있음 */ + { + cnt_s++; + + if (cnt_s == BOOT_THRESHOLD) /* 2초 도달: 래치 + 부팅 완료 */ + { + device_on = true; + cnt_s = 0; /* 카운터 리셋: 안 하면 다음 틱에서 ON→OFF 분기가 + * cnt_s >= 400 조건을 즉시 만족하여 전원 OFF됨 */ + power_control_handler(ON); + led_set_state(LED_STATE_ADVERTISING); + k_work_submit(&adv_start_work); + battery_timer_start(); + m_reset_status = 1; + DBG_PRINTF("[BTN] 2s -> Power latched, LED blink\r\n"); + DBG_PRINTF("[BOOT] Complete, device ON\r\n"); + DBG_PRINTF("[DEV] device_on=%d\r\n", device_on); + } + } + } + else /* ── 전원 OFF 시퀀스 (ON → OFF) ── */ + { + if (!boot_btn_released) /* 부팅 시 눌렀던 버튼을 아직 안 놓음 → 대기 */ + { + if (!button_pressed) + { + boot_btn_released = true; + DBG_PRINTF("[BTN] Boot button released\r\n"); + } + } + else if (button_pressed) /* 버튼 새로 누르고 있음 */ + { + cnt_s++; + + if (cnt_s >= BOOT_THRESHOLD) /* 2초 이상 → 전원 OFF */ + { + DBG_PRINTF("[BTN] 2s long press -> Power OFF\r\n"); + battery_timer_stop(); + k_work_submit(&adv_stop_work); + device_on = false; + boot_btn_released = false; + cnt_s = 0; + sleep_mode_enter(); + return; + } + } + else /* 버튼 놓음 → 카운터 리셋 */ + { + if (cnt_s > 0) + { + DBG_PRINTF("[BTN] Short press (%d) -> ignored\r\n", cnt_s); + } + cnt_s = 0; + } + } + + k_timer_start(&m_power_on_delay_timer, K_MSEC(POWER_ON_DELAY), K_NO_WAIT); +} + +/*============================================================================== + * 타이머 초기화 / 시작 + *============================================================================*/ +static void timers_init(void) +{ + k_timer_init(&m_power_on_delay_timer, main_s, NULL); + k_timer_init(&m_power_off_delay_timer, t_power_off_timeout_handler, NULL); + k_work_init(&adv_start_work, adv_start_work_handler); + k_work_init(&adv_stop_work, adv_stop_work_handler); + power_timer_init(); +} + +/* 전원 버튼 폴링 시작 (5ms 후 main_s 콜백) */ +static void timers_start(void) +{ + k_timer_start(&m_power_on_delay_timer, K_MSEC(POWER_ON_DELAY), K_NO_WAIT); +} + +/*============================================================================== + * 메인 함수 + *============================================================================*/ int main(void) { - int ret; - bool led_state = true; + /*── Phase 1: 하드웨어 기본 초기화 ──*/ + power_hold_init(); + cnt_s = 0; - if (!gpio_is_ready_dt(&led)) { - return 0; - } + DBG_PRINTF("\r\n========================================\r\n"); + DBG_PRINTF(" VesiScan BASIC %s (Zephyr)\r\n", FIRMWARE_VERSION); + DBG_PRINTF("========================================\r\n"); - ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE); - if (ret < 0) { - return 0; - } + DBG_PRINTF("[1] HW Init\r\n"); + gpio_init(); + timers_init(); + load_default_config(); + led_init(); + battery_adc_init(); + battery_timer_init(); + DBG_PRINTF(" gpio/timer/config/led/batt OK\r\n"); - while (1) { - ret = gpio_pin_toggle_dt(&led); - if (ret < 0) { - return 0; - } + /*── Phase 2: BLE 스택 + NUS ──*/ + DBG_PRINTF("[2] BLE Init\r\n"); + if (ble_service_init(ble_rx_handler) == 0) + { + DBG_PRINTF(" ble/nus OK\r\n"); + } + else + { + DBG_PRINTF(" ble FAIL\r\n"); + } - led_state = !led_state; - printf("LED state: %s\n", led_state ? "ON" : "OFF"); - k_msleep(SLEEP_TIME_MS); - } - return 0; + /*── Phase 3: FDS/NVS (TODO) ──*/ + /*── Phase 4: 애플리케이션 (TODO) ──*/ + + DBG_PRINTF("\r\n========================================\r\n"); + DBG_PRINTF(" READY [%s]\r\n", SERIAL_NO); + DBG_PRINTF("========================================\r\n\r\n"); + + /* 전원 버튼 상태머신 시작 (부팅 시 버튼이 눌려있는 상태) */ + timers_start(); + + /* 메인 루프 - idle */ + for (;;) { + k_msleep(100); + } + + return 0; } diff --git a/src/main.h b/src/main.h new file mode 100644 index 0000000..03dd488 --- /dev/null +++ b/src/main.h @@ -0,0 +1,77 @@ +/******************************************************************************* + * @file main.h + * @brief VesiScan BASIC - Zephyr port main header + ******************************************************************************/ + +#ifndef MAIN_H__ +#define MAIN_H__ + +#include +#include +#include +#include +#include + +/*============================================================================== + * Firmware identification + *============================================================================*/ +#define FIRMWARE_VERSION "VBTFW0102" +#define HARDWARE_VERSION "VB0HW0000" +#define SERIAL_NUMBER "VB026030000" +#define DEFAULT_PASSKEY "123456" + +/*============================================================================== + * Data length constants + *============================================================================*/ +#define SERIAL_NO_LENGTH 12 +#define HW_NO_LENGTH 12 +#define PASSKEY_LENGTH 6 + +/*============================================================================== + * Enumerations + *============================================================================*/ + +typedef enum +{ + OFF = 0, + ON = 1 +} on_off_cont_t; + +typedef enum +{ + CMD_BLE = 0, + CMD_UART = 1 +} which_cmd_t; + +typedef enum +{ + BLE_DISCONNECTED_ST = 0, + BLE_CONNECTED_ST = 1 +} ble_status_t; + +/*============================================================================== + * Timing constants + *============================================================================*/ +#define POWER_ON_DELAY 5 /* Power button poll interval (ms) */ +#define POWER_OFF_DELAY 3000 /* LED display before power off (ms) */ + +/*============================================================================== + * Function declarations + *============================================================================*/ + +void sleep_mode_enter(void); +void device_power_off(void); + +/*============================================================================== + * Global variables (extern) + *============================================================================*/ +extern volatile bool ble_connection_st; +extern volatile bool processing; +extern char SERIAL_NO[SERIAL_NO_LENGTH]; +extern char HW_NO[HW_NO_LENGTH]; +extern char m_static_passkey[PASSKEY_LENGTH]; +extern bool bond_data_delete; +extern uint32_t m_life_cycle; +extern uint8_t m_reset_status; + +#endif /* MAIN_H__ */ diff --git a/src/parser.c b/src/parser.c new file mode 100644 index 0000000..7435627 --- /dev/null +++ b/src/parser.c @@ -0,0 +1,171 @@ +/******************************************************************************* + * @file parser.c + * @brief BLE command parser (Zephyr port) + * + * 원본: pc_firm/parser.c + * 패킷 포맷: [TAG 4B] [DATA NB] [CRC16 2B] + * + * 현재 구현된 커맨드: msn? (배터리 측정), mls? (LED 상태 설정) + ******************************************************************************/ +#include +#include + +#include "parser.h" +#include "main.h" +#include "debug_print.h" +#include "ble_service.h" +#include "battery_adc.h" +#include "led_control.h" + +/*============================================================================== + * CRC16 (CRC-CCITT, Nordic SDK 호환) + *============================================================================*/ +static uint16_t dr_crc16_compute(const uint8_t *p_data, uint32_t size) +{ + uint16_t crc = 0xFFFF; + + for (uint32_t i = 0; i < size; i++) + { + crc = (uint8_t)(crc >> 8) | (crc << 8); + crc ^= p_data[i]; + crc ^= (uint8_t)(crc & 0xFF) >> 4; + crc ^= (crc << 8) << 4; + crc ^= ((crc & 0xFF) << 4) << 1; + } + return crc; +} + +/*============================================================================== + * 응답 패킷 전송 + *============================================================================*/ + +/* TAG(4B) + uint16 값(2B) + CRC16(2B) = 8바이트 전송 */ +static void send_response_u16(const char *tag, uint16_t value) +{ + uint8_t buf[8]; + + buf[0] = tag[0]; + buf[1] = tag[1]; + buf[2] = tag[2]; + buf[3] = tag[3]; + buf[4] = (uint8_t)(value >> 8); + buf[5] = (uint8_t)(value & 0xFF); + + uint16_t crc = dr_crc16_compute(buf, 6); + buf[6] = (uint8_t)(crc & 0xFF); + buf[7] = (uint8_t)(crc >> 8); + + ble_data_send(buf, 8); +} + +/*============================================================================== + * 커맨드 핸들러 + *============================================================================*/ + +/* msn? → 배터리 전압 측정 → rsn: + mV */ + +static int cmd_msn(const uint8_t *data, uint8_t data_len) +{ + int mv = battery_read_mv(); + if (mv < 0) + { + mv = 0; + } + + send_response_u16("rsn:", (uint16_t)mv); + DBG_PRINTF("[CMD] msn -> %d mV\r\n", mv); + return 1; +} + +/* mls? → LED 상태 변경 → rls: + state echo back + * 파라미터: [state(2B LE)] — led_state_t enum 값 + * 0=OFF, 4=DETACH_WARNING, 5=ALIGN_SEARCHING, 6=ALIGN_COMPLETE, 7=ERROR + * 에러 응답: 0xFFFF=파라미터 없음, 0xFFFE=범위 초과 */ +static int cmd_mls(const uint8_t *data, uint8_t data_len) +{ + /* 파라미터 부족 → 에러 코드 0xFFFF 에코 */ + if (data_len < 2) + { + send_response_u16("rls:", 0xFFFF); + DBG_PRINTF("[CMD] mls: no data\r\n"); + return 1; + } + + /* 리틀엔디안으로 읽기 (앱이 LE로 전송) */ + uint16_t state = (uint16_t)(data[0]) | ((uint16_t)(data[1]) << 8); + + /* 범위 초과 → 에러 코드 0xFFFE 에코 */ + if (state > LED_STATE_ERROR) + { + send_response_u16("rls:", 0xFFFE); + DBG_PRINTF("[CMD] mls: invalid state %d\r\n", state); + return 1; + } + + led_set_state((led_state_t)state); + send_response_u16("rls:", state); + DBG_PRINTF("[CMD] mls -> LED state=%d\r\n", state); + return 1; +} + +/*============================================================================== + * 커맨드 테이블 + *============================================================================*/ +typedef struct +{ + char tag[5]; + int (*handler)(const uint8_t *data, uint8_t data_len); +} cmd_entry_t; + +static const cmd_entry_t cmd_table[] = +{ + { "msn?", cmd_msn }, + { "mls?", cmd_mls }, +}; + +#define CMD_TABLE_SIZE (sizeof(cmd_table) / sizeof(cmd_table[0])) + +/*============================================================================== + * 파서 엔트리 + *============================================================================*/ +int dr_parser(const uint8_t *buf, uint16_t len) +{ + /* 최소 4바이트 TAG 필요 */ + if (len < 4) + { + DBG_PRINTF("[CMD] Too short (%d)\r\n", len); + return -1; + } + + /* CRC16 검증 (6바이트 이상이면 마지막 2바이트가 CRC) */ + if (len >= 6) + { + uint16_t calc_crc = dr_crc16_compute(buf, len - 2); + uint16_t recv_crc = (uint16_t)(buf[len - 2]) | ((uint16_t)(buf[len - 1]) << 8); + + if (calc_crc != recv_crc) + { + DBG_PRINTF("[CMD] CRC fail (calc=0x%04X recv=0x%04X)\r\n", calc_crc, recv_crc); + return -1; + } + } + + /* TAG 추출 (4바이트) */ + char tag[5] = { buf[0], buf[1], buf[2], buf[3], '\0' }; + + /* 데이터 부분 (TAG 이후, CRC 이전) */ + const uint8_t *data = buf + 4; + uint8_t data_len = (len >= 6) ? (len - 4 - 2) : (len - 4); + + /* 테이블 검색 + 디스패치 */ + for (int i = 0; i < CMD_TABLE_SIZE; i++) + { + if (memcmp(tag, cmd_table[i].tag, 4) == 0) + { + return cmd_table[i].handler(data, data_len); + } + } + + DBG_PRINTF("[CMD] Unknown: %s\r\n", tag); + return 0; +} diff --git a/src/parser.h b/src/parser.h new file mode 100644 index 0000000..1c6f6d2 --- /dev/null +++ b/src/parser.h @@ -0,0 +1,14 @@ +/******************************************************************************* + * @file parser.h + * @brief BLE command parser (Zephyr port) + * + * 4바이트 TAG + DATA + CRC16 패킷 파싱 및 디스패치 + ******************************************************************************/ +#ifndef CMD_PARSER_H__ +#define CMD_PARSER_H__ + +#include + +int dr_parser(const uint8_t *buf, uint16_t len); + +#endif /* CMD_PARSER_H__ */ diff --git a/src/power_control.c b/src/power_control.c new file mode 100644 index 0000000..c8ea3a4 --- /dev/null +++ b/src/power_control.c @@ -0,0 +1,83 @@ +/******************************************************************************* + * @file power_control.c + * @brief Device power sequence control (Zephyr port) + * + * Power-up sequence state machine with k_timer (single-shot 20ms intervals). + ******************************************************************************/ +#include +#include "main.h" +#include "power_control.h" +#include "debug_print.h" + +#define POWER_LOOP_INTERVAL 20 /* ms */ + +static struct k_timer m_power_timer; +static uint8_t p_order; +static bool lock_check = false; + +extern volatile bool processing; + +static void power_loop_expiry(struct k_timer *timer) +{ + power_loop(timer); +} + +int device_sleep_mode(void) +{ + k_msleep(2); + DBG_PRINTF("Device_Sleep_Mode OK!\r\n"); + k_msleep(10); + processing = false; + return 0; +} + +int device_activated(void) +{ + p_order = 0; + lock_check = true; + power_timer_start(); + return 0; +} + +void power_loop(struct k_timer *timer) +{ + power_timer_stop(); + + /* Sensor init not needed - imu_read_direct() handles it per measurement */ + p_order = 2; + + if (p_order < 2) + { + p_order++; + power_timer_start(); + } + else + { + DBG_PRINTF("[PWR] Device Activated OK!\r\n"); + } +} + +int device_reactivated(void) +{ + /* sw_i2c_init_once() will be added in Stage 3 */ + k_msleep(10); + lock_check = true; + p_order = 0; + power_timer_start(); + return 0; +} + +void power_timer_start(void) +{ + k_timer_start(&m_power_timer, K_MSEC(POWER_LOOP_INTERVAL), K_NO_WAIT); +} + +void power_timer_stop(void) +{ + k_timer_stop(&m_power_timer); +} + +void power_timer_init(void) +{ + k_timer_init(&m_power_timer, power_loop_expiry, NULL); +} diff --git a/src/power_control.h b/src/power_control.h new file mode 100644 index 0000000..616f9c4 --- /dev/null +++ b/src/power_control.h @@ -0,0 +1,18 @@ +/******************************************************************************* + * @file power_control.h + * @brief Device power sequence control (Zephyr port) + ******************************************************************************/ +#ifndef POWER_CONTROL_H__ +#define POWER_CONTROL_H__ + +#include "main.h" + +int device_sleep_mode(void); +int device_activated(void); +int device_reactivated(void); +void power_loop(struct k_timer *timer); +void power_timer_start(void); +void power_timer_stop(void); +void power_timer_init(void); + +#endif /* POWER_CONTROL_H__ */