diff --git a/Documentation/firmware/README.md b/Documentation/firmware/README.md index 30dd71d..7d4c5d6 100644 --- a/Documentation/firmware/README.md +++ b/Documentation/firmware/README.md @@ -1,22 +1,70 @@ -# Overview +# Firmware Documentation -This firmware runs on the Nordic Semiconductor nRF5340 dual-core SoC and provides multi-modal biosignal acquisition with Bluetooth Low Energy (BLE) streaming. The system supports synchronized acquisition from multiple sensors: +This firmware runs on the **Nordic Semiconductor nRF5340** dual-core SoC and provides multi-modal biosignal acquisition with Bluetooth Low Energy (BLE) streaming. It is built on the **Zephyr RTOS** using the **nRF Connect SDK** and the custom **SENSEI SDK** board support package. -- **EEG/EMG**: 16 channels via dual ADS1298 AFE (500 Hz) -- **IMU**: LIS2DUXS12 3-axis accelerometer (400 Hz) -- **PPG**: MAX86150 sensor -- **Microphone**: PDM digital microphone (16 kHz) +## Supported Sensors -> [!CAUTION] -> The PPG sensor has currently NOT being tested +| Sensor | Device | Channels | Sample Rate | Module | +|--------|--------|----------|-------------|--------| +| EEG | 2x ADS1298 | 16 | 500 Hz | `sensors/eeg` | +| EMG | 2x ADS1298 | 16 | 500 Hz | `sensors/emg` | +| IMU | LIS2DUXS12 | 3-axis accel | 400 Hz | `sensors/imu` | +| Microphone | PDM digital mic | 1 (mono) | 16 kHz | `sensors/mic` | +| PPG | MAX86150 | 2 (Red/IR) | ~100 Hz | `sensors/ppg` | -## Getting Started +> **Note**: The PPG sensor has not been tested yet. PPG data, when active, is multiplexed into the ExG (i.e. EEG/EMG) BLE packet by replacing the last two ADS1298 channels. -To get started with the firmware, please refer to the [Getting Started Guide](./getting_started.md). +## Key Features -## Authors +- Multi-threaded sensor acquisition with dedicated threads per sensor +- Barrier-synchronized simultaneous start across all active sensors +- BLE streaming via Nordic UART Service (NUS) at 2M PHY, 251-byte MTU +- Inter-board GPIO synchronization for multi-device setups (Not tested) +- PMIC-based power management with battery monitoring -- Philipp Schilk (schilkp@ethz.ch), ETH Zurich -- Philip Wiese (wiesep@iis.ee.ethz.ch), ETH Zurich -- Sebastian Frey (sefrey@iis.ee.ethz.ch), ETH Zurich -- Giovanni Pollo (giovanni.pollo@polito.it), Politecnico di Torino \ No newline at end of file +## Documentation Index + +### Getting Started +- [Getting Started Guide](./getting_started.md) - Build, flash, and run the firmware + +### Architecture +- [Architecture Overview](./architecture.md) - System architecture, threading model, and data flow + +### Modules +- [BLE Module](./ble_module.md) - BLE connectivity, NUS service, connection management, and streaming +- [BLE Protocol](./ble_protocol.md) - BLE command protocol specification (command codes and packet formats) +- [Sensor Modules](./sensor_modules.md) - EEG, EMG, IMU, Microphone, and PPG acquisition threads +- [BSP Module](./bsp_module.md) - Board support package (power management, battery, system status) +- [Core Module](./core_module.md) - Core utilities (synchronization, I2C helpers, board sync) + +### Reference +- [Configuration](./configuration.md) - Build configuration, Kconfig options, device tree overlays +- [Data Formats](./data_formats.md) - BLE packet formats for all sensor types + +## Source Code Layout + +``` +Firmware/ +├── src_NRF/ # Main firmware source +│ ├── main.c # Entry point, initialization sequence +│ ├── CMakeLists.txt # Build configuration +│ ├── prj.conf # Zephyr project configuration +│ ├── Kconfig # Custom Kconfig options +│ ├── nrf5340_senseiv1_cpuapp.overlay # Device tree overlay +│ ├── pm_static.yml # Flash partition layout +│ ├── afe/ # ADS1298 AFE driver +│ ├── ble/ # BLE stack and application layer +│ ├── bsp/ # Board support (power, battery) +│ ├── core/ # Core utilities (sync, I2C) +│ ├── sensors/ # Sensor acquisition modules +│ │ ├── eeg/ # EEG streaming +│ │ ├── emg/ # EMG streaming +│ │ ├── imu/ # IMU (LIS2DUXS12) driver +│ │ ├── mic/ # PDM microphone +│ │ └── ppg/ # PPG (MAX86150) driver +│ └── child_image/ # Network core and bootloader configs +├── custom_dts/ # Custom device tree bindings for ADS1298 +└── custom_shields/ # Shield definitions for ExG and PPG boards + ├── SENSEI_ExGShield/ + └── SENSEI_PPGShield/ +``` diff --git a/Documentation/firmware/architecture.md b/Documentation/firmware/architecture.md new file mode 100644 index 0000000..de0416a --- /dev/null +++ b/Documentation/firmware/architecture.md @@ -0,0 +1,54 @@ +# Architecture Overview + +The BioGAP firmware is a multi-threaded sensor acquisition and BLE streaming system running on the **nRF5340** SoC, built on **Zephyr RTOS**. + +## Dual-Core Architecture + +The nRF5340 is a dual-core processor: + +- **Application Core** — Runs the main firmware: sensor drivers, BLE application layer, and power management +- **Network Core** — Runs the BLE radio controller transparently; the application communicates with it via Zephyr's HCI IPC driver + +## Initialization + +On boot, the firmware initializes hardware in this order: + +1. **Power** — Configure PMIC (MAX77654) power rails and enable battery charging +2. **Console** — Enable USB CDC ACM serial console +3. **AFE** — Configure ADS1298 SPI bus and Data Ready (DRDY) interrupt +4. **GAP9** — Power on the GAP9 co-processor +5. **BLE** — Start the BLE stack +6. **Sensors** — Initialize IMU (I2C), microphone (PDM), and AFE (EEG or EMG) +7. **Board Sync** — Configure inter-board GPIO sync (if enabled) + +After initialization, the main thread sleeps and monitors the soft-reset button. + +## Threading Model + +Each sensor and subsystem runs in its own Zephyr thread. This allows sensors to operate independently and in parallel: + +- **Sensor threads** — One per active sensor (EEG/EMG, IMU, Microphone). Each waits for a start signal, then continuously acquires data and enqueues BLE packets. +- **BLE send thread** — Dequeues packets from all sensors and transmits them over BLE. +- **BLE receive thread** — Processes incoming BLE commands (start/stop streaming, query status, etc.). +- **Battery thread** — Periodically reads battery status from the PMIC. + +## Data Flow + +### Sensor Streaming + +Each sensor thread: +1. Waits for a start command (semaphore from BLE receive thread) +2. Powers up the sensor and configures it +3. Participates in barrier synchronization (if multi-sensor start) +4. Loops: reads samples, fills a BLE packet, enqueues it for transmission +5. On stop command: powers down and returns to idle + +## Barrier Synchronization + +When multiple sensors are started together, a barrier ensures they all begin at the same instant: + +1. The barrier is set up for N subsystems +2. Each sensor thread registers and blocks at the barrier +3. When all N have arrived, they are all released simultaneously + +This guarantees temporal alignment across different sensor modalities. diff --git a/Documentation/firmware/ble_module.md b/Documentation/firmware/ble_module.md new file mode 100644 index 0000000..e160486 --- /dev/null +++ b/Documentation/firmware/ble_module.md @@ -0,0 +1,66 @@ +# BLE Module + +The BLE module (`src_NRF/ble/`) manages all Bluetooth Low Energy connectivity using the **Nordic UART Service (NUS)** for bidirectional data transfer. It is optimized for high-throughput sensor data streaming while supporting a command/response protocol for device control. + +## File Overview + +| File | Purpose | +|------|---------| +| `bluetooth.c/h` | BLE stack init, NUS service, connection callbacks, PHY/MTU negotiation, statistics | +| `ble_appl.c/h` | Application layer: send/receive threads, message queues, command dispatcher | +| `ble_commands.h` | Protocol command codes (see [BLE Protocol](./ble_protocol.md)) | + +## Threading + +### Send Thread (`ble_send_tid`) + +- **Stack**: 2048 bytes +- Loops on `k_msgq_get(&send_msgq, ...)` with 100ms timeout +- Calls `send_data_ble()` → `bt_nus_send()` for each packet +- Tracks packet statistics by header byte + +### Receive Thread (`ble_receive_tid`) + +- **Stack**: 2048 bytes +- Waits on `ble_data_received` semaphore (signaled by NUS RX callback) +- Reads from `receive_msgq` and dispatches commands via `process_ble_rx_data()` + +### Write Thread (`ble_write_thread_id`) + +- **Stack**: 1024 bytes +- Waits for BLE initialization (`ble_init_ok` semaphore) +- Used for connection establishment monitoring + +## Message Queues + +### Send Queue (`send_msgq`) + +``` +K_MSGQ_DEFINE(send_msgq, sizeof(ble_packet_t), 64, 4) +``` + +- 64 entries of `ble_packet_t` (variable-size, max 244 bytes data) +- 4-byte alignment +- Filled by sensor streaming threads, drained by BLE send thread + +### Receive Queue (`receive_msgq`) + +``` +K_MSGQ_DEFINE(receive_msgq, BLE_PCKT_RECEIVE_SIZE, 16, 1) +``` + +- 16 entries of 234 bytes each +- Filled by NUS RX callback, drained by BLE receive thread + +## Public API + +| Function | Description | +|----------|-------------| +| `send_data_ble(data, len)` | Send raw bytes over NUS (TX notify) | +| `add_data_to_send_buffer(pkt, len)` | Enqueue a packet for async BLE transmission | +| `get_ble_eeg_packets_sent()` | Get count of sent EEG packets | +| `get_ble_imu_packets_sent()` | Get count of sent IMU packets | +| `get_ble_mic_packets_sent()` | Get count of sent MIC packets | +| `get_ble_other_packets_sent()` | Get count of other sent packets | +| `get_ble_packets_failed()` | Get count of failed transmissions | +| `send_battery_status()` | Request battery status transmission | \ No newline at end of file diff --git a/Documentation/firmware/ble_protocol.md b/Documentation/firmware/ble_protocol.md new file mode 100644 index 0000000..b17aee2 --- /dev/null +++ b/Documentation/firmware/ble_protocol.md @@ -0,0 +1,82 @@ +# BLE Protocol + +The BioGAP firmware uses a custom command protocol over the Nordic UART Service (NUS) for bidirectional communication. Commands are sent from the BLE peer to the device, and responses are sent back as NUS notifications. + +## Command Codes + +| Code | Name | Description | +|------|------|-------------| +| 12 | `SET_DEVICE_SETTINGS` | Configure device settings | +| 13 | `GET_DEVICE_SETTINGS` | Read device settings | +| 14 | `REQUEST_HARDWARE_VERSION` | Returns hardware version (major='2', minor='b') | +| 15 | `GET_BOARD_STATE` | Returns current board state | +| 17 | `REQUEST_BATTERY_STATE` | Returns battery information (7 bytes) | +| 18 | `START_EEG_STREAMING` | Start EEG acquisition and BLE streaming | +| 19 | `STOP_EEG_STREAMING` | Stop EEG acquisition | +| 20 | `SET_BOARD_STATE` | Set operating mode (Nordic/GAP9) | +| 21 | `RESET_BOARD` | Reset the board | +| 24 | `GO_TO_SLEEP` | Enter low-power sleep mode | +| 26 | `START_MIC_STREAMING` | Start PDM microphone capture | +| 27 | `STOP_MIC_STREAMING` | Stop microphone | +| 31 | `START_STREAMING_ALL` | Start all sensors with barrier synchronization | +| 32 | `STOP_STREAMING_ALL` | Stop all active sensors | +| 33 | `START_IMU_STREAMING` | Start IMU (accelerometer) streaming | +| 34 | `STOP_IMU_STREAMING` | Stop IMU streaming | +| 35 | `START_EEG_MIC_STREAMING` | Combined EEG + microphone streaming | +| 37 | `START_EMG_STREAMING` | Start EMG acquisition and BLE streaming | +| 38 | `STOP_EMG_STREAMING` | Stop EMG acquisition | + +## Command Flow Examples + +### Start EEG Streaming + +``` +Peer → Device: [18] (START_EEG_STREAMING) +Device → Peer: [0x55][cnt][ts][data...][tr] (EEG data packets @ 500 Hz) +... +Peer → Device: [19] (STOP_EEG_STREAMING) +Device → Peer: (streaming stops) +``` + +### Start All Sensors (Synchronized) + +``` +Peer → Device: [31] (START_STREAMING_ALL) + ↓ sync barrier: all sensors start simultaneously +Device → Peer: [0x55][...] (EEG data @ 500 Hz) +Device → Peer: [0x56][...] (IMU data @ 400 Hz) +Device → Peer: [0xAA][...] (MIC data @ 16 kHz) +... +Peer → Device: [32] (STOP_STREAMING_ALL) +``` + +### Query Battery + +``` +Peer → Device: [17] (REQUEST_BATTERY_STATE) +Device → Peer: [17][charging][rsv][pwr][soc][vbat][temp] +``` + +### Combined EEG + Microphone + +``` +Peer → Device: [35] (START_EEG_MIC_STREAMING) + ↓ sync barrier: EEG + MIC start simultaneously +Device → Peer: [0x55][...] (EEG data) +Device → Peer: [0xAA][...] (MIC data) +... +Peer → Device: [19] (STOP_EEG_STREAMING) +Peer → Device: [27] (STOP_MIC_STREAMING) +``` + +## Data Packet Headers + +Streaming data packets are identified by their first byte (header): + +| Header | Sensor | Trailer | See | +|--------|--------|---------|-----| +| `0x55` | EEG/EMG (EXG) | `0xAA` | [Data Formats](./data_formats.md) | +| `0x56` | IMU | `0x57` | [Data Formats](./data_formats.md) | +| `0xAA` | Microphone | `0x55` | [Data Formats](./data_formats.md) | + +See [Data Formats](./data_formats.md) for detailed packet structure documentation. diff --git a/Documentation/firmware/bsp_module.md b/Documentation/firmware/bsp_module.md new file mode 100644 index 0000000..ad735f4 --- /dev/null +++ b/Documentation/firmware/bsp_module.md @@ -0,0 +1,86 @@ +# BSP Module (Board Support Package) + +The BSP module (`src_NRF/bsp/`) provides board-level hardware management including power supply control, battery monitoring, and system status aggregation. + +## Submodules + +``` +bsp/ +├── pwr_bsp.c/h # Board-level power initialization and control +├── battery/ +│ ├── battery.c # Battery monitoring thread +│ └── battery.h # Battery status structure +├── power/ +│ ├── power.c # ADS1298 power rail control +│ └── power.h # Power API +└── system_status/ + ├── system_status.c # System info aggregator + └── system_status.h # Status API +``` + +## Power Management (`pwr_bsp`) + +### Key Functions + +| Function | Description | +|----------|-------------| +| `pwr_init()` | Initialize PMIC I2C communication | +| `pwr_bsp_start()` | Configure all PMIC rails (SBB0-2, LDO0-1) | +| `pwr_charge_enable()` | Enable battery charging (285mA input, 90mA fast-charge, 4.2V CV) | +| `pwr_bsp_soft_rst_cb()` | Soft-reset button GPIO interrupt handler | +| `gap9_pwr(on)` | Power on/off the GAP9 co-processor via I2C | + + + +## ADS1298 Power Control (`power`) + +The ADS1298 analog supply voltage depends on the electrode configuration: + +### Unipolar Mode (EEG) + +Used for EEG with common reference electrodes. + +``` +power_ads_on_unipolar() → LDO1 set to 3.0V +power_ads_off_unipolar() → LDO1 disabled +``` + +### Bipolar Mode (EMG) + +Used for EMG with differential electrode pairs. + +``` +power_ads_on_bipolar() → LDO1 set to 1.5V, SBB1 set to 2.7V +power_ads_off_bipolar() → LDO1 disabled, SBB1 disabled +``` + +## Battery Monitoring (`battery`) + +### Battery Thread + +- **Stack**: 1024 bytes, **Priority**: 6 +- **Poll interval**: 5 seconds +- Runs continuously, reading PMIC battery gas gauge data + +### Battery Status Structure + +```c +typedef struct { + uint8_t soc_percent; // State of charge (0-100%) + uint16_t voltage_mv; // Battery voltage in millivolts + bool is_charging; // Whether battery is charging + uint16_t power_mw; // Power consumption in milliwatts + const char *power_source; // "USB/External" or "Battery" +} battery_status_t; +``` + +### Key Functions + +| Function | Description | +|----------|-------------| +| `battery_update_thread()` | Main thread loop, polls PMIC every 5 seconds | +| `get_battery_status()` | Returns current `battery_status_t` | + +### Behavior During Streaming + +Battery reads are **skipped** during active sensor streaming to avoid I2C/SPI interference. The battery data is only read when sensors are idle. \ No newline at end of file diff --git a/Documentation/firmware/configuration.md b/Documentation/firmware/configuration.md new file mode 100644 index 0000000..fcdd3af --- /dev/null +++ b/Documentation/firmware/configuration.md @@ -0,0 +1,35 @@ +# Configuration + +This document covers the most important build-time configuration options for the BioGAP firmware added during the refactor. + +## Build System + +### Environment Variable + +| Variable | Description | +|----------|-------------| +| `SENSEI_SDK_ROOT` | Path to the SENSEI SDK root directory (required) | + +## Kconfig Options + +Custom Kconfig options are defined in the firmware's `Kconfig` file: + +### Sensor Selection + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `CONFIG_SENSOR_EEG` | bool | y | Enable EEG mode (ADS1298 unipolar power) | +| `CONFIG_SENSOR_EMG` | bool | n | Enable EMG mode (ADS1298 bipolar power) | + +> These two options are mutually exclusive. Only one can be enabled at a time. + +### Board Synchronization + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `CONFIG_BOARD_SYNC_ROLE_STANDALONE` | bool | y | Single device, no sync | +| `CONFIG_BOARD_SYNC_ROLE_PRIMARY` | bool | n | Primary board in multi-device setup | +| `CONFIG_BOARD_SYNC_ROLE_SECONDARY` | bool | n | Secondary board in multi-device setup | +| `CONFIG_BOARD_ID` | int | 1 | Unique board identifier (1-255) | +| `CONFIG_BOARD_SYNC_PERIODIC_MS` | int | 1000 | Periodic sync pulse interval (ms) | + diff --git a/Documentation/firmware/core_module.md b/Documentation/firmware/core_module.md new file mode 100644 index 0000000..2e5d37f --- /dev/null +++ b/Documentation/firmware/core_module.md @@ -0,0 +1,102 @@ +# Core Module + +The core module (`src_NRF/core/`) provides shared utilities used across the firmware: common definitions, I2C helpers, barrier synchronization, and inter-board GPIO synchronization. + +## Barrier Synchronization (`sync_streaming`) + +Provides a counting barrier to ensure multiple sensor subsystems start acquisition simultaneously. This is critical for temporal alignment of multi-modal data. + +### API + +| Function | Description | +|----------|-------------| +| `sync_begin(count)` | Initialize barrier for `count` subsystems | +| `sync_wait(subsystem_id, timeout_ms)` | Register and block until all subsystems arrive | +| `sync_reset()` | Clean up barrier state | + +### Subsystem Identifiers + +| ID | Name | Bitmask | +|----|------|---------| +| 0 | `SYNC_SUBSYSTEM_EXG` | bit 0 | +| 1 | `SYNC_SUBSYSTEM_MIC` | bit 1 | +| 2 | `SYNC_SUBSYSTEM_IMU` | bit 2 | + +### Implementation + +The barrier uses atomic operations for thread-safe registration: +1. `sync_begin(N)` sets the expected count and resets the arrival counter +2. Each `sync_wait()` atomically increments the arrival counter and sets its bitmask bit +3. The last arriving subsystem (count == N) gives the semaphore (N-1) times to unblock all others +4. Uses `atomic_t sync_ready_count` and `atomic_t sync_ready_mask` for lock-free coordination + +### Timeout Handling + +If a subsystem does not arrive within `timeout_ms`, the barrier is released and the waiting subsystem enters an error state, powers down its sensor, and returns to idle. This prevents deadlocks if one sensor fails to initialize. + +### Usage with Board Sync + +The barrier integrates with inter-board synchronization: +- In **PRIMARY** mode: the primary board's sync GPIO is asserted when the barrier completes +- In **SECONDARY** mode: the secondary board waits for the primary's GPIO signal at the barrier before proceeding + +## Inter-Board Synchronization (`board_sync`) + +For multi-device setups, hardware GPIO synchronization aligns sampling across multiple BioGAP boards. This is essential for synchronized multi-subject recordings or high-channel-count configurations. + +> **Note**: This feature has not been tested yet and is currently theoretical. The implementation is based on standard GPIO interrupt handling and periodic pulse generation for drift correction. + +### Sync Roles (Kconfig) + +| Role | Kconfig | Description | +|------|---------|-------------| +| STANDALONE | `CONFIG_BOARD_SYNC_ROLE_STANDALONE` | No sync, single device (default) | +| PRIMARY | `CONFIG_BOARD_SYNC_ROLE_PRIMARY` | Outputs sync signal on GPIO | +| SECONDARY | `CONFIG_BOARD_SYNC_ROLE_SECONDARY` | Waits for PRIMARY's GPIO signal | + +### Board ID + +Each board has a unique ID (1-255) set via `CONFIG_BOARD_ID`. The board ID is embedded in every BLE packet for source identification. + +### Synchronization Mechanism + + +``` +PRIMARY Board SECONDARY Board +───────────── ─────────────── +sync_barrier completes ──► assert GPIO ──► GPIO ISR fires + │ +periodic timer ───────► pulse GPIO ─────────► drift correction + │ + sync_sem given + (proceed with acquisition) +``` + +### PRIMARY Behavior + +1. When barrier completes, assert sync GPIO output +2. Periodic timer (configured via `CONFIG_BOARD_SYNC_PERIODIC_MS`, default 1000ms) pulses the GPIO for drift correction +3. Pulse counter incremented each time and embedded in BLE packets + +### SECONDARY Behavior + +1. At barrier, wait for `sync_sem` (given by GPIO ISR) +2. On receiving signal, proceed with acquisition +3. Periodic pulses from PRIMARY are counted for drift correction + +### Key Functions + +| Function | Description | +|----------|-------------| +| `board_sync_init()` | Configure sync GPIO based on role | +| `board_sync_signal()` | (PRIMARY) Assert sync GPIO | +| `board_sync_wait(timeout_ms)` | (SECONDARY) Wait for sync signal | +| `board_sync_get_pulse_count()` | Get current pulse counter | + +### Pulse Counter + +The pulse counter (`sync_pulse_count`) is embedded in the BLE packet metadata (byte 208 of EXG packets). This allows post-hoc temporal alignment of data from multiple boards. + +### GPIO Configuration + +The sync GPIO is defined in the device tree overlay (`nrf5340_senseiv1_cpuapp.overlay`), defaulting to P0.05 (commented out by default, must be enabled for multi-board setups). diff --git a/Documentation/firmware/data_formats.md b/Documentation/firmware/data_formats.md new file mode 100644 index 0000000..a4b435d --- /dev/null +++ b/Documentation/firmware/data_formats.md @@ -0,0 +1,155 @@ +# Data Formats + +This document specifies the BLE packet formats for all sensor data types transmitted over the Nordic UART Service (NUS). + +## Common Packet Structure + +All sensor data packets follow a common pattern: + +``` +[Header] [Metadata] [Payload] [Trailer] +``` + +- **Header** (1 byte): Identifies the packet type +- **Metadata**: Counter and timestamp +- **Payload**: Sensor-specific data +- **Trailer** (1 byte): Frame end marker + +## EXG Packet (EEG/EMG) + +**Header**: `0x55` | **Trailer**: `0xAA` | **Size**: 211 bytes + +``` +Offset Size Field Description +────── ──── ────────────────── ─────────────────────────────────────── +0 1 header 0x55 +1-2 2 packet_counter uint16_t, little-endian, wraps at 65535 +3-6 4 timestamp uint32_t, microseconds, little-endian +7-206 200 samples[4] 4 samples x 50 bytes each (see below) +207 1 board_id CONFIG_BOARD_ID (1-255) +208 1 sync_pulse_count Inter-board sync pulse counter +209 1 reserved 0x00 +210 1 trailer 0xAA +``` + +### Sample Structure (50 bytes each) + +Each sample within the EXG packet: + +``` +Offset Size Field Description +────── ──── ────────────────── ─────────────────────────────────────── +0-23 24 ads_a_data[8] ADS1298_A: 8 channels x 3 bytes (24-bit) +24-47 24 ads_b_data[8] ADS1298_B: 8 channels x 3 bytes (24-bit) +48 1 counter_extra Additional counter +49 1 reserved 0x00 +``` + +### PPG Multiplexing + +When `PPG_ACTIVE` is defined, the last 2 ADS1298 channels in each sample are replaced: + +``` +ADS1298_A channel 7 (bytes 21-23) → PPG Red LED (3 bytes) +ADS1298_A channel 8 (bytes 24-26) → PPG IR LED (3 bytes) + (or equivalently, ADS1298_B channel 7-8 depending on mapping) +``` + +### Packet Counter + +The packet counter (`packet_counter`) is a monotonically increasing uint16_t that wraps at 65535. It can be used to detect lost packets on the receiving end by checking for gaps in the sequence. + + +## IMU Packet + +**Header**: `0x56` | **Trailer**: `0x57` | **Size**: 127 bytes + +``` +Offset Size Field Description +────── ──── ────────────────── ─────────────────────────────────────── +0 1 header 0x56 +1 1 packet_counter uint8_t, wraps at 255 +2-5 4 timestamp uint32_t, microseconds, little-endian +6-125 120 samples[20] 20 samples x 6 bytes each (see below) +126 1 trailer 0x57 +``` + +### IMU Sample Structure (6 bytes each) + +Each acceleration sample: + +``` +Offset Size Field Description +────── ──── ────── ─────────────────────────────────────── +0-1 2 x int16_t, big-endian, X-axis acceleration +2-3 2 y int16_t, big-endian, Y-axis acceleration +4-5 2 z int16_t, big-endian, Z-axis acceleration +``` + + +## Microphone Packet + +**Header**: `0xAA` | **Trailer**: `0x55` | **Size**: 136 bytes + +``` +Offset Size Field Description +────── ──── ────────────────── ─────────────────────────────────────── +0 1 header 0xAA +1-2 2 packet_counter uint16_t, little-endian, wraps at 65535 +3-6 4 timestamp uint32_t, microseconds, little-endian +7-134 128 samples[64] 64 samples x 2 bytes each (16-bit PCM) +135 1 trailer 0x55 +``` + +### Audio Sample Format + +Each sample is a 16-bit signed integer (little-endian): + +``` +Byte 0: LSB +Byte 1: MSB +``` + +This is standard 16-bit PCM audio at 16 kHz, mono channel. + +## Battery Status Packet + +**Size**: 7 bytes (sent as response to command, not a streaming packet) + +``` +Offset Size Field Description +────── ──── ────────────────── ─────────────────────────────────────── +0 1 command_code 17 (REQUEST_BATTERY_STATE) +1 1 is_charging 0 = not charging, 1 = charging +2 1 reserved 0x00 +3 1 power_mw Power consumption in mW (truncated to uint8) +4 1 soc_percent State of charge (0-100%) +5 1 voltage_mv Battery voltage in mV (truncated to uint8) +6 1 temperature_celsius Die temperature from IMU sensor +``` + +Note that `power_mw` and `voltage_mv` are truncated to uint8, so values above 255 are clipped. For full-precision readings, the raw PMIC registers must be queried directly. + +--- + +## Packet Identification Summary + +| Header | Trailer | Type | Size | Sensor | Packet Rate | +|--------|---------|------|------|--------|-------------| +| `0x55` | `0xAA` | EXG | 211 bytes | EEG/EMG (ADS1298) | 62.5 Hz | +| `0x56` | `0x57` | IMU | 127 bytes | LIS2DUXS12 | 20 Hz | +| `0xAA` | `0x55` | MIC | 136 bytes | PDM Microphone | 250 Hz | + +## Timestamp + +All packet types include a 32-bit microsecond timestamp (`k_cyc_to_us(k_cycle_get_32())`). The timestamp represents the time when the last sample in the packet was captured. At 32-bit width, the timestamp wraps every ~4295 seconds (~71.6 minutes). + +## Receiving and Parsing + +To parse the BLE data stream: + +1. Read the first byte to determine packet type +2. Read the expected number of bytes for that packet type +3. Verify the trailer byte matches the expected value +4. Extract metadata (counter, timestamp) +5. Process payload samples \ No newline at end of file diff --git a/Documentation/firmware/getting_started.md b/Documentation/firmware/getting_started.md index f23774e..1edddc3 100644 --- a/Documentation/firmware/getting_started.md +++ b/Documentation/firmware/getting_started.md @@ -1,149 +1,139 @@ -This document provides instruction on how to get started with the firmware, including building, flashing, and running the application on the nRF5340. +# Getting Started -# Cloning the SENSEI-SDK +This guide covers how to set up the development environment, clone the required repositories, build the firmware, and flash it onto the BioGAP mainboard. -In order to build the firmware, you first need to clone the SENSEI-SDK repository, which contains the necessary Zephyr board support package and other dependencies. +## Prerequisites -```bash -git clone https://github.com/pulp-bio/sensei-sdk.git -``` - -Then move to the `sensei-sdk` folder: +- **nRF Connect SDK** (NCS) v2.x with Zephyr RTOS +- **Visual Studio Code** with the nRF Connect for VS Code extension (recommended) +- **SEGGER J-Link** debugger (or the BioGAP Debug Board) +- **Python 3.10+** with west build tool +- Git -```bash -cd sensei-sdk -``` +## Step 1: Clone the SENSEI-SDK -Now you need to update the submodules to clone all the necessary third-party dependencies: +The firmware depends on the SENSEI-SDK, which provides the Zephyr board support package for the custom `nrf5340_senseiv1` board and all third-party dependencies. ```bash +git clone https://github.com/pulp-bio/sensei-sdk.git +cd sensei-sdk git submodule update --init --recursive ``` -# Cloning the BioGAP Repository -Now you need to clone the BioGAP repository, which contains the firmware source code and the custom modifications for the SENSEI-SDK. +Set the `SENSEI_SDK_ROOT` environment variable to point to this directory: ```bash -git clone https://github.com/pulp-bio/BioGAP.git +export SENSEI_SDK_ROOT=/path/to/sensei-sdk ``` -Then move to the `BioGAP` folder: +## Step 2: Clone the BioGAP Repository ```bash -cd BioGAP +git clone https://github.com/pulp-bio/BioGAP.git ``` -# Adapting the SENSEI-SDK for BioGAP - -The cloned SENSEI-SDK is not ready to be used for the BioGAP firmware. For this reason, inside this repository, under `Firmware/` you will find two folders: +## Step 3: Integrate Custom Files into the SENSEI-SDK -- `custom_dts`: Contains a custom file for the Analog Front-End (AFE) -- `custom_shields`: Contains the custom shield definitions for the ExG (EEG/EMG) and PPG sensors. +The BioGAP firmware requires custom device tree bindings and shield definitions that are not part of the base SENSEI-SDK. These are provided in the `Firmware/` directory. -You need to copy the content of these two folders into the corresponding folders in the `sensei-sdk` repository. +### Copy Custom Device Tree Bindings -First move to the BioGAP directory: +The ADS1298 AFE binding must be added to the SDK: ```bash -cd BioGAP +cp -r Firmware/custom_dts/* $SENSEI_SDK_ROOT/NRF/dts/bindings ``` +### Copy Custom Shield Definitions + +The ExG and PPG shield overlays must be added to the SDK: + ```bash -cp -r Firmware/custom_dts/* ~/sensei-sdk/NRF/dts/bindings -cp -r Firmware/custom_shields/* ~/sensei-sdk/NRF/boards/shields +cp -r Firmware/custom_shields/* $SENSEI_SDK_ROOT/NRF/boards/shields ``` -This will copy the custom device tree source files and the custom shield definitions into the SENSEI-SDK, allowing you to build the firmware for the BioGAP hardware. +## Step 4: Modify the SENSEI-SDK Device Tree -Additionally you have to do some modification to the `sensei-sdk/NRF/boards/arm/nrf5340_senseiv1/nrf5340_senseiv1_cpuapp.dts`. +The base SENSEI-SDK device tree file needs modifications to support the BioGAP hardware. Edit the file: -First you need to comment the alias of the UART. The following snippet of code: ``` - aliases { - i2ca = &i2c0; - i2cb = &i2c1; - uartgap = &uart_gap; - }; +$SENSEI_SDK_ROOT/NRF/boards/arm/nrf5340_senseiv1/nrf5340_senseiv1_cpuapp.dts ``` -Should become: -``` - aliases { - i2ca = &i2c0; - i2cb = &i2c1; - // uartgap = &uart_gap; - }; -``` +### 4a. Comment out the UART GAP alias -Then, just under the modification you need to add the following lines: +Change the `aliases` block from: -``` - buttons{ - gpio_lis2duxs12_int1: gpio_lis2duxs12_int1 { - gpios = <&gpio0 23 GPIO_ACTIVE_HIGH>; - label = "LIS2DUXS12_INT"; - }; - gpio_soft_rst: gpio_soft_rst { - gpios = <&gpio0 26 GPIO_ACTIVE_HIGH>; - label = "BUTTON_SOFT_INT"; - }; - }; +```dts +aliases { + i2ca = &i2c0; + i2cb = &i2c1; + uartgap = &uart_gap; +}; ``` -So the final code should look something like this: +To: -``` - aliases { - i2ca = &i2c0; - i2cb = &i2c1; - // uartgap = &uart_gap; - }; - buttons{ - gpio_lis2duxs12_int1: gpio_lis2duxs12_int1 { - gpios = <&gpio0 23 GPIO_ACTIVE_HIGH>; - label = "LIS2DUXS12_INT"; - }; - gpio_soft_rst: gpio_soft_rst { - gpios = <&gpio0 26 GPIO_ACTIVE_HIGH>; - label = "BUTTON_SOFT_INT"; - }; - }; +```dts +aliases { + i2ca = &i2c0; + i2cb = &i2c1; + // uartgap = &uart_gap; +}; ``` -Then you need to comment out the following lines: +### 4b. Add GPIO button definitions -``` - uart_gap_default: uart0_default { - group1 { - psels = , - ; - }; - }; - - uart_gap_sleep: uart0_sleep { - group1 { - psels = , - ; - low-power-enable; - }; - }; +Add the following block immediately after the `aliases` block: + +```dts +buttons { + gpio_lis2duxs12_int1: gpio_lis2duxs12_int1 { + gpios = <&gpio0 23 GPIO_ACTIVE_HIGH>; + label = "LIS2DUXS12_INT"; + }; + gpio_soft_rst: gpio_soft_rst { + gpios = <&gpio0 26 GPIO_ACTIVE_HIGH>; + label = "BUTTON_SOFT_INT"; + }; +}; ``` -Finally, you also need to comment out these lines: +### 4c. Comment out the UART3 pin control -``` -uart_gap: &uart3{ - status = "okay"; - current-speed = <115200>; +Comment out the `uart_gap_default` and `uart_gap_sleep` pin control groups: - pinctrl-0 = <&uart_gap_default>; - pinctrl-1 = <&uart_gap_sleep>; - pinctrl-names = "default", "sleep"; -}; +```dts +// uart_gap_default: uart0_default { +// group1 { +// psels = , +// ; +// }; +// }; +// +// uart_gap_sleep: uart0_sleep { +// group1 { +// psels = , +// ; +// low-power-enable; +// }; +// }; ``` -After these modifications, the SENSEI-SDK should be ready to be used for building the BioGAP firmware. -# Building the Firmware +### 4d. Comment out the UART3 node definition + +Comment out the `uart_gap` node at the bottom of the file: + +```dts +// uart_gap: &uart3 { +// status = "okay"; +// current-speed = <115200>; +// pinctrl-0 = <&uart_gap_default>; +// pinctrl-1 = <&uart_gap_sleep>; +// pinctrl-names = "default", "sleep"; +// }; +``` -Once everything is set up, you can build the firmware. The instruction are exactly the same as the one provided in the [SENSEI-SDK repository](https://github.com/pulp-bio/sensei-sdk). +## Step 5: Build the Firmware +Once everything is set up, you can build the firmware. The instruction are exactly the same as the one provided in the [SENSEI-SDK repository](https://github.com/pulp-bio/sensei-sdk). \ No newline at end of file diff --git a/Documentation/firmware/sensor_modules.md b/Documentation/firmware/sensor_modules.md new file mode 100644 index 0000000..cb67661 --- /dev/null +++ b/Documentation/firmware/sensor_modules.md @@ -0,0 +1,233 @@ +# Sensor Modules + +The sensor modules (`src_NRF/sensors/`) each run in a dedicated Zephyr thread and handle acquisition from a specific sensor type. All modules follow a similar pattern: wait on a start semaphore, power up the sensor, acquire data in a loop, construct BLE packets, and enqueue them for transmission. + +## EEG Module (`sensors/eeg/`) + +### Files + +| File | Purpose | +|------|---------| +| `eeg_appl.c` | EEG streaming thread and ADS1298 control flow | +| `eeg_appl.h` | EEG configuration, states, packet format constants | + +### Configuration + +| Parameter | Value | +|-----------|-------| +| Sensor | 2x ADS1298 (16 channels) | +| Sample rate | 500 Hz | +| Samples per packet | 4 | +| Packet size | 211 bytes | +| Thread stack | 2048 bytes | +| Power mode | Unipolar (3.0V LDO) | + +### State Machine + +``` +EEG_STATE_IDLE → EEG_STATE_STARTING → EEG_STATE_STREAMING → EEG_STATE_STOPPING → EEG_STATE_IDLE +``` + +### Acquisition Flow + +1. Wait on `eeg_start_sem` (given by BLE command handler) +2. Power on ADS1298 analog rail: `power_ads_on_unipolar()` +3. First run only: verify ADS1298 ID (expects `0xD2`) +4. Initialize ADS1298 registers via `ads_init()` +5. Participate in sync barrier: `sync_wait(SYNC_SUBSYSTEM_EXG, timeout)` +6. Start conversion: `ads_start()` +7. Streaming loop: call `process_ads_data()` which: + - Waits for DRDY interrupt + - Reads 27 bytes from each ADS1298 + - Constructs BLE packet (4 samples per packet) + - Enqueues via `add_data_to_send_buffer()` +8. On stop command: `ads_stop()` → power off → return to IDLE + +### Configuration Structure + +```c +typedef struct { + uint8_t sample_rate; // ADS1298 ODR index (default: 6) + uint8_t ads_mode; // ADS1298 mode (default: 0) + uint8_t channel_2_func; // Channel 2 function (default: 2) + uint8_t channel_4_func; // Channel 4 function (default: 4) + uint8_t gain; // PGA gain setting (default: 0) +} eeg_config_t; +``` + +### Kconfig + +- `CONFIG_SENSOR_EEG=y` - Enable EEG mode (mutually exclusive with EMG) + +--- + +## EMG Module (`sensors/emg/`) + +### Files + +| File | Purpose | +|------|---------| +| `emg_appl.c` | EMG streaming thread (mirrors EEG module) | +| `emg_appl.h` | EMG configuration, states, packet format constants | + +The EMG module is architecturally identical to the EEG module with two key differences: + +1. **Power configuration**: Uses bipolar mode (1.5V LDO + 2.7V SBB1) via `power_ads_on_bipolar()` +2. **Kconfig**: `CONFIG_SENSOR_EMG=y` (mutually exclusive with `CONFIG_SENSOR_EEG`) + +All other parameters (sample rate, packet format, thread configuration) are the same as EEG. + +### Kconfig + +- `CONFIG_SENSOR_EMG=y` - Enable EMG mode (mutually exclusive with EEG) + +--- + +## IMU Module (`sensors/imu/`) + +### Files + +| File | Purpose | +|------|---------| +| `imu_appl.c` | IMU streaming thread (400 Hz, 20 samples per packet) | +| `imu_appl.h` | IMU states, packet format constants | +| `lis2duxs12_sensor.c/h` | Low-level LIS2DUXS12 driver (I2C, DRDY interrupt) | +| `driver/lis2duxs12_reg.c/h` | ST platform-independent register driver (auto-generated) | + +### Configuration + +| Parameter | Value | +|-----------|-------| +| Sensor | LIS2DUXS12 (ST) | +| Interface | I2C (address 0x19) | +| Sample rate | 400 Hz | +| Samples per packet | 20 | +| Packet size | 127 bytes | +| Thread stack | 2048 bytes | + +### DRDY Handling + +The LIS2DUXS12 asserts INT1 (GPIO P0.23) when new acceleration data is available: +1. GPIO interrupt fires → `lis2duxs12_drdy_handler()` +2. Gives `lis2duxs12_drdy_sem` semaphore +3. IMU streaming thread waits on semaphore, then reads 6 bytes (X, Y, Z as int16_t) + +### Acquisition Flow + +1. Wait on `imu_start_sem` +2. Initialize LIS2DUXS12: set output data rate, range, bandwidth +3. Configure DRDY interrupt on INT1 +4. Participate in sync barrier (if multi-sensor start) +5. Streaming loop: + - Wait for `lis2duxs12_drdy_sem` + - Read X, Y, Z acceleration (3 x int16_t = 6 bytes) + - Fill packet buffer (20 samples) + - When full, enqueue via `add_data_to_send_buffer()` +6. On stop: disable sensor, return to IDLE + +### State Machine + +``` +IMU_STATE_IDLE → IMU_STATE_STARTING → IMU_STATE_STREAMING → IMU_STATE_STOPPING → IMU_STATE_IDLE +``` + +--- + +## Microphone Module (`sensors/mic/`) + +### Files + +| File | Purpose | +|------|---------| +| `mic_appl.c` | PDM microphone streaming thread | +| `mic_appl.h` | Mic states, packet format constants | + +### Configuration + +| Parameter | Value | +|-----------|-------| +| Sensor | PDM digital microphone | +| Interface | DMIC peripheral (NRFX PDM driver) | +| Sample rate | 16 kHz | +| Bit width | 16-bit | +| Channel | Mono (left channel) | +| Samples per packet | 64 | +| Packet size | 136 bytes | +| Thread stack | 2048 bytes | +| Memory slab | 8 blocks of 128 bytes each | + +### PDM Configuration (Device Tree Overlay) + +| Parameter | Value | +|-----------|-------| +| CLK pin | P0.04 | +| DIN pin | P0.12 | +| ACLK source | 12.288 MHz | + +### Acquisition Flow + +1. Wait on `mic_start_sem` +2. Configure PDM peripheral via `pdm_configure()` +3. Start DMIC with `dmic_start()` +4. Participate in sync barrier (if multi-sensor start) +5. Streaming loop: + - `dmic_read()` blocks until audio block available from memory slab + - Copy 128 bytes (64 samples x 2 bytes) into BLE packet + - Enqueue via `add_data_to_send_buffer()` + - Release audio block back to memory slab +6. On stop: `dmic_stop()`, return to IDLE + +### Buffer Management + +Audio blocks are managed via a Zephyr memory slab: + +```c +K_MEM_SLAB_DEFINE_STATIC(mic_mem_slab, MAX_BLOCK_SIZE, 8, 4) +``` + +- 8 blocks of `MAX_BLOCK_SIZE` bytes (128 bytes each = 4ms of audio at 16 kHz) +- Blocks are acquired by the DMIC driver and released by the streaming thread after copying + +### State Machine + +``` +MIC_STATE_IDLE → MIC_STATE_STARTING → MIC_STATE_STREAMING → MIC_STATE_STOPPING → MIC_STATE_IDLE +``` + +--- + +## PPG Module (`sensors/ppg/`) + +### Files + +| File | Purpose | +|------|---------| +| `ppg_appl.c` | MAX86150 PPG driver (I2C) | +| `ppg_appl.h` | MAX86150 register definitions, circular buffer | + +### Configuration + +| Parameter | Value | +|-----------|-------| +| Sensor | MAX86150 (Maxim) | +| Interface | I2C (address 0x5E) | +| LEDs | Red + IR | +| Sample rate | ~100 SPS | +| ADC range | 32768 nA | +| LED amplitude | 0x25 (Red and IR) | +| FIFO depth | 32 entries | + +> **Note**: The PPG module is not fully integrated. The PPG thread is commented out. PPG data is currently read on-demand and multiplexed into the EXG BLE packet when `PPG_ACTIVE` is defined. + +### Data Buffer + +PPG samples are stored in a circular buffer: + +```c +typedef struct Record { + uint32_t red[40]; // Red LED samples (19-bit values) + uint32_t IR[40]; // IR LED samples (19-bit values) + uint16_t head; + uint16_t tail; +} sense_struct; +``` \ No newline at end of file