# Hello MLC Activity Recognition
LSM6DSOX Machine Learning Core (MLC) activity recognition demo for CUBE32.
## Overview
This demo uses the **on-sensor decision tree engine** in the LSM6DSOX IMU to
classify real-time motion into 5 activities with **zero CPU overhead**:
| MLC Output | Activity | UI Color |
|------------|-------------|-----------|
| 0x00 | Stationary | Blue-grey |
| 0x01 | Walking | Green |
| 0x04 | Jogging | Orange |
| 0x08 | Biking | Blue |
| 0x0C | Driving | Purple |
> **Note:** MLC output values are non-sequential (0, 1, 4, 8, 12). The helper
> function `lsm6dsox_activity_is_known()` validates output codes.
The MLC processes accelerometer data at 26 Hz in a sliding window, computing
statistical features (mean, variance, energy, peak-to-peak) and running them
through a decision tree — all in hardware.
## UI Features (LVGL, adapts to the screen size)
The layout is computed from the display size at start-up (the log shows `Screen WxH, canvas WxH`):
| Screen | Chart canvas |
|--------|--------------|
| 240×240 | 220×60 |
| 320×320 | 300×140 |
The canvas spans the full width (10 px margins) and all the height between the duration timer and the legend; the history bars and the accelerometer waveform stretch with it, and the legend columns are spread over the screen width.
- **Activity icon + label** — large center display of current classification
- **Stability timer** — how long the current activity has persisted
- **History timeline** — color-coded scrolling bar of recent activities
- **Accel waveform** — real-time acceleration magnitude mini-graph
- **Legend** — color key for all 5 activity classes
## Prerequisites
Enable the following in `idf.py menuconfig`:
1. **CUBE32 Board Configuration → IMU Configuration → Enable IMU (LSM6DSO)**
2. **CUBE32 Board Configuration → IMU Configuration → Enable Machine Learning Core (MLC)**
3. **CUBE32 Board Configuration → Display Configuration → Enable LVGL**
4. **Component config → LVGL → Font usage**: Enable Montserrat 10, 14, 18, 24
5. **CUBE32 Application Selection → Hello MLC Activity Recognition Example**
Optional:
6. **CUBE32 Board Configuration → MLC diagnostic logging** — enables verbose
register dumps at startup (register values, program memory readback, etc.)
## Build & Flash
```bash
source ~/esp/v5.5.1/esp-idf/export.sh 2>/dev/null
idf.py set-target esp32s3
idf.py menuconfig # Enable options above
idf.py build flash monitor
```
## How It Works
1. `cube32_init()` initializes the board and IMU with default settings
2. `load_mlc_config()` calls `imu.loadMLC()` with the UCF data (773 register
writes), which:
- Iterates each `{address, data}` pair, writing to the sensor over I2C
- Adds 100 µs delays after program-memory writes (register 0x09) since the
I2C bus at 400 kHz is faster than the sensor's internal memory write cycle
- Adds 300 µs delays after page-switch writes (register 0x01)
- Reconfigures the accelerometer to ±4 g @ 26 Hz
- Programs the decision tree into MLC hardware memory
- Enables the MLC engine
3. After a 5-second warmup (for the sliding window to fill), the MLC
continuously classifies motion in hardware
4. The demo polls `MLC0_SRC` on the embedded function page for classification
results at 5 Hz via an LVGL timer
## Console Output
The demo prints activity status to the serial console at 1 Hz:
```
Activity: Stationary |mag|=1.00g Duration: 5s MLC0_SRC=0x00 MLC_STATUS=0x01
Activity: Walking |mag|=1.23g Duration: 2s MLC0_SRC=0x01 MLC_STATUS=0x01
Activity: Jogging |mag|=1.85g Duration: 3s MLC0_SRC=0x04 MLC_STATUS=0x01
```
---
## How to Create and Import an MLC Decision Tree
This section explains the full pipeline for converting a decision tree model
into a UCF configuration that runs on the LSM6DSOX MLC hardware.
### Toolchain Overview
```
Training Data (.csv)
│
▼
┌──────────────────┐
│ ST Unico GUI / │ ← Desktop tool from ST
│ MEMS Studio │
└───────┬──────────┘
│ generates
▼
UCF file (.ucf) ← Register-write sequence (text)
│
▼
┌──────────────────┐
│ Convert to C │ ← Python script or manual
│ header (.h) │
└───────┬──────────┘
│
▼
ucf_line_t array ← Compiled into firmware
│
▼
┌──────────────────┐
│ imu.loadMLC() │ ← Written to sensor over I2C at boot
└──────────────────┘
```
### Step 1: Collect Training Data
Record accelerometer data for each activity class you want to detect. Use the
LSM6DSOX evaluation board or the CUBE32 itself to log raw data to CSV:
- Sample at the target ODR (typically 26 Hz for activity recognition)
- Collect at least 30–60 seconds per class
- Include varied conditions (pocket, hand, desk, etc.)
### Step 2: Design the Decision Tree in Unico GUI / MEMS Studio
ST provides two desktop tools for MLC configuration:
- **Unico GUI** — legacy tool, well-documented
([download](https://www.st.com/en/development-tools/unico-gui.html))
- **MEMS Studio** — newer replacement
([download](https://www.st.com/en/development-tools/mems-studio.html))
Within the tool:
1. Select the **LSM6DSOX** device
2. Open the **MLC** tab
3. Configure the **accelerometer** settings (ODR, full-scale)
4. Define **features** for each window (mean, variance, energy, peak-to-peak,
min, max, etc.)
5. Import your training CSV data and assign class labels
6. The tool trains a decision tree and shows accuracy metrics
7. Click **Generate UCF** → save as `.ucf` file
> **Tip:** ST provides pre-built UCF examples for common use cases at
> [github.com/STMicroelectronics/STMems_Machine_Learning_Core](https://github.com/STMicroelectronics/STMems_Machine_Learning_Core).
> The activity recognition UCF used in this demo comes from the
> `lsm6dsox/activity_recognition_for_mobile` example in that repository.
### Step 3: Convert the UCF File to a C Header
A `.ucf` file is a text file with lines like:
```
Ac 10 00
Ac 11 00
Ac 01 80
Ac 04 00
...
```
Each line is: `Ac <register_address_hex> <data_hex>`.
Convert this to a C header containing a `ucf_line_t` array:
```python
#!/usr/bin/env python3
"""Convert a .ucf text file to a C header with ucf_line_t array."""
import sys, re
def convert_ucf(ucf_path, header_path, array_name):
entries = []
with open(ucf_path) as f:
for line in f:
m = re.match(r'Ac\s+([0-9A-Fa-f]{2})\s+([0-9A-Fa-f]{2})', line.strip())
if m:
entries.append((m.group(1).upper(), m.group(2).upper()))
guard = array_name.upper() + "_H"
with open(header_path, "w") as out:
out.write(f"#ifndef {guard}\n#define {guard}\n\n")
out.write("#ifndef MEMS_UCF_SHARED_TYPES\n")
out.write("#define MEMS_UCF_SHARED_TYPES\n")
out.write("typedef struct { uint8_t address; uint8_t data; } ucf_line_t;\n")
out.write("#endif\n\n")
out.write(f"static const ucf_line_t {array_name}[] = {{\n")
for addr, data in entries:
out.write(f" {{0x{addr}, 0x{data}}},\n")
out.write("};\n\n")
out.write(f"static const size_t {array_name}_size =\n")
out.write(f" sizeof({array_name}) / sizeof(ucf_line_t);\n\n")
out.write(f"#endif /* {guard} */\n")
print(f"Converted {len(entries)} entries → {header_path}")
if __name__ == "__main__":
convert_ucf(sys.argv[1], sys.argv[2], sys.argv[3])
```
Usage:
```bash
python3 convert_ucf.py my_model.ucf my_model.h my_model_ucf
```
### Step 4: Add the Header to the CUBE32 Project
Place the generated header in the MLC include directory:
```
components/cube32/include/drivers/imu/mlc/my_model.h
```
### Step 5: Include and Load in Your Application
```cpp
#include "drivers/imu/mlc/my_model.h"
// In app_main or init function:
cube32::IMU &imu = cube32::IMU::instance();
cube32_result_t ret = imu.loadMLC(my_model_ucf, my_model_ucf_size);
if (ret != CUBE32_OK) {
ESP_LOGE(TAG, "MLC load failed");
}
```
### Step 6: Read MLC Output
```cpp
uint8_t mlc_out = 0;
imu.readMLCOutput(0, mlc_out); // tree_index = 0
// mlc_out now contains the class label from the decision tree
```
### Important Notes
- **Do not modify the UCF data.** The register writes must be played back
verbatim in the exact order generated by Unico/MEMS Studio. The sequence
includes page switches, embedded function configuration, and program memory
writes that depend on ordering.
- **Output class values are defined by the UCF.** They are typically
non-sequential integers assigned during training. Add helper functions
(like `lsm6dsox_activity_label()`) to map them to human-readable strings.
- **The MLC needs a warmup period** after loading. The sliding window must
fill before the first valid classification. At 26 Hz with a 1-second window,
allow ~5 seconds for stable output.
- **I2C bus speed matters.** At 400 kHz the bus can outpace the sensor's
internal program-memory write cycle. The CUBE32 driver adds micro-second
delays automatically in `loadMLC()` to handle this.
- **Up to 8 decision trees** can run simultaneously on the LSM6DSOX MLC.
Read tree N output with `imu.readMLCOutput(N, result)` or all 8 at once
with `imu.readMLCOutputAll(results)`.
## File Structure
```
hello_mlc_activity/
├── main.cpp # Demo application
├── README.md # This file
└── (UCF header is at)
components/cube32/include/drivers/imu/mlc/
└── lsm6dsox_activity_recognition.h # 773-entry UCF + helpers
```
## References
- [STMems_Machine_Learning_Core](https://github.com/STMicroelectronics/STMems_Machine_Learning_Core) — ST's official MLC examples and UCF files
- [LSM6DSOX Datasheet](https://www.st.com/resource/en/datasheet/lsm6dsox.pdf) — Register map and MLC description
- [AN5259](https://www.st.com/resource/en/application_note/an5259-lsm6dsox-machine-learning-core-stmicroelectronics.pdf) — ST Application Note: LSM6DSOX Machine Learning Core
- [Unico GUI](https://www.st.com/en/development-tools/unico-gui.html) — GUI tool for MLC configuration
To create a project from this example, run:
idf.py create-project-from-example "cube32esp/cube32_bsp=0.0.17:hello_mlc_activity"