8.12.3.9. Fan Blade Anomaly Detection — On-Device Training Example
8.12.3.9.1. Table of Contents
8.12.3.9.2. 1. Overview
This is a complete working example that demonstrates on-device training for anomaly detection on the F29H85x microcontroller. The application trains an autoencoder model directly on the device using embedded data, recalculates the anomaly detection threshold, and evaluates the model — all without any PC involvement after deployment.
What this example demonstrates:
Golden vector validation (verify artifacts are loaded correctly)
Pre-training inference (baseline — shows the model hasn’t learned yet despite coincidental accuracy)
On-device training with early stopping (model learns from scratch or fine-tunes)
Automatic threshold recalculation using validation data
Post-training inference
Result from the example run: The model trains from completely untrained weights (0 PC-side epochs). While pre-training accuracy happened to be 100% on this small test set by coincidence (the initial threshold landed in the right spot), the model had not actually learned anything — normal reconstruction errors were ~5800. After on-device training, normal errors dropped to ~10 and the separation between normal and anomaly improved from 1.18× to 8.6×, producing a genuinely robust detector.
8.12.3.9.2.1. Prerequisites
Hardware: F29H85x LaunchPad
Software: F29H85x SDK, Code Composer Studio (CCS), C29 CGT Tools
Artifacts generated via ModelZoo with
ondevice_training: True(see Running ModelZoo for ODT)Conceptual background: Overview
8.12.3.9.2.2. Project location
tinyml-sdk/c29/ai/examples/fan_blade_anomalydetection_ondevice_learning/
8.12.3.9.3. 2. Project Structure
8.12.3.9.3.1. SDK layout (before CCS import)
fan_blade_anomalydetection_ondevice_learning/
├── application_main.c # Main application code
└── f29h85x/
├── artifacts/ # All auto-generated model files
└── CCS/
└── *.projectspec # CCS project definition
8.12.3.9.3.2. CCS project layout (after import)
After importing the project into CCS, the project links in shared SDK libraries and organizes files into these key directories:
fan_blade_anomalydetection_ondevice_learning_f29h85x/
│
├── artifacts/ # Auto-generated by ModelZoo pipeline
│ ├── mod.a # Compiled frozen model (static library)
│ ├── tvmgen_default.h # Frozen model inference API
│ ├── trainable_model_config.h/.c # Trainable layer architecture + weights
│ ├── ondevice_training_data.h/.c # Embedded train/val/test datasets
│ ├── user_input_config.h # Feature extraction config + initial threshold
│ └── test_vector.c # Golden vectors for validation
│
├── feature_extract/ # Linked from SDK
│ ├── feature_extract.h/.c # Feature extraction implementation
│ └── feature_extract_c29.c # C29-specific optimizations
│
├── ondevice_training/ # Linked from SDK
│ ├── anomaly_detection_odt.h/.c # Task library (state machine, training loop)
│ └── ondevice_training_lib.h/.c # Core library (forward, backward, SGD)
│
├── libs/ # Linked from SDK — DSP libraries
│ └── cfft_f32_* # FFT library for feature extraction
│
├── application_main.c # Main application (this walkthrough)
└── *.cmd / *.syscfg # Linker scripts + SysConfig
8.12.3.9.3.3. Key files by role
Role |
Files |
Source |
|---|---|---|
Application |
|
You write/modify this |
Model artifacts |
|
Auto-generated by ModelZoo |
ODT libraries |
|
Linked from SDK (shared) |
Feature extraction |
|
Linked from SDK (shared) |
DSP libraries |
|
Linked from SDK (shared) |
8.12.3.9.4. 3. Application Flow
The application runs through six stages sequentially:
Stage 1: Initialization
│
▼
Stage 2: Golden Vector Validation
│
▼
Stage 3: Pre-Training Inference (baseline)
│
▼
Stage 4: On-Device Training (debugger-triggered)
│
▼
Stage 5: Threshold Recalculation
│
▼
Stage 6: Post-Training Inference (evaluation)
8.12.3.9.4.1. Stage 1: Initialization
The application initializes the hardware, dataset iterators, hyperparameters, the anomaly detection model, and the feature extraction pipeline.
Board and data setup:
Device_init();
Device_initGPIO();
Board_init();
init_dataset_iterators();
Hyperparameter configuration:
ad_model_config.learning_rate = 0.0001;
ad_model_config.patience = 10;
ad_model_config.min_improvement_delta = 0.5;
ad_model_config.ema_alpha = 0.5;
ad_model_config.batches_per_epoch = 10;
ad_model_config.initial_threshold = RECONSTRUCTION_ERROR_THRESHOLD;
ad_model_config.threshold_samples = 500;
ad_model_config.threshold_percentile = 0.99;
ad_model_config.gaussian_std_multiplier = 4;
ad_model_config.use_percentile_threshold = 1;
ad_model_config.inference_window_size = 1;
ad_model_config.stats_window_size = 100;
Model initialization:
AnomalyDetection_Init(&ad_model_ctx, &ad_model_config);
This validates all hyperparameters, initializes the core training library (ODT_Init), sets up layer pointers and buffers, and starts in MODE_INFERENCE.
Console output:
========================================
ANOMALY DETECTION MODEL INITIALIZATION
========================================
Learning rate(multiplied by 10^5): 10
Batches per epoch: 10, total samples per epoch 10
Train batch size: 1
Val batch size: 1
Patience: 10
Threshold samples: 500
========================================
ODT INITIALIZATION
========================================
Layer 0: Linear(48→24)
Layer 1: ReLU(24)
Layer 2: Linear(24→12)
...
Layer 10: Linear(24→48)
8.12.3.9.4.2. Stage 2: Golden Vector Validation
A known test input is processed through the complete pipeline (feature extraction → frozen model → trainable model) and the output is compared against a pre-computed golden reference.
// Feature extraction
FE_initFeatureExtract(fe_handle);
FE_runFeatureExtract(fe_handle);
// Frozen model
tvmgen_default_run(&inputs, &outputs);
// Trainable model forward pass
AnomalyDetection_ForwardPass(&ad_model_ctx, frozen_output, model_input, all_model_output, &reconstruction_error);
// Compare against golden output
for (output_index = 0; output_index < FE_NN_OUT_SIZE; output_index++) {
float difference = fabs(golden_output[output_index] - all_model_output[output_index]);
if (difference > 1e-3) error++;
}
Console output:
Loss for test vector: 5820.863770
Golden vectors matched: 48 not matched: 0
All 48 output elements match within tolerance, confirming that mod.a, trainable_model_config, and feature extraction are all loaded and working correctly.
8.12.3.9.4.3. Stage 3: Pre-Training Inference
All 12 test samples are run through the full pipeline using the initial threshold from PC-side training. This establishes a baseline before on-device training.
test_iter.current_index = 0;
for (i = 0; i < NUM_TEST_SAMPLES; i++) {
process_one_sample(&test_iter);
AnomalyDetection_ProcessInferenceFrame(&ad_model_ctx, frozen_output, model_input, &result);
// Log result
}
Console output:
=== Pre-training Inference on Test Data ===
Sample 0: error=7471.3379 threshold=6430.9824 predicted=ANOMALY label=ANOMALY OK
Sample 1: error=11115.2373 threshold=6430.9824 predicted=ANOMALY label=ANOMALY OK
Sample 2: error=5879.2905 threshold=6430.9824 predicted=NORMAL label=NORMAL OK
Sample 3: error=6949.8081 threshold=6430.9824 predicted=ANOMALY label=ANOMALY OK
Sample 4: error=5703.4355 threshold=6430.9824 predicted=NORMAL label=NORMAL OK
Sample 5: error=5691.7251 threshold=6430.9824 predicted=NORMAL label=NORMAL OK
Sample 6: error=10856.5381 threshold=6430.9824 predicted=ANOMALY label=ANOMALY OK
Sample 7: error=5873.1714 threshold=6430.9824 predicted=NORMAL label=NORMAL OK
Sample 8: error=8936.2441 threshold=6430.9824 predicted=ANOMALY label=ANOMALY OK
Sample 9: error=5829.2065 threshold=6430.9824 predicted=NORMAL label=NORMAL OK
Sample 10: error=8369.9053 threshold=6430.9824 predicted=ANOMALY label=ANOMALY OK
Sample 11: error=5727.6235 threshold=6430.9824 predicted=NORMAL label=NORMAL OK
Pre-training accuracy: 12/12 = 100.0%
Note: The 100% pre-training accuracy is misleading — the model has not been trained at all (0 PC-side epochs). The autoencoder has not learned to reconstruct normal data, which is evident from the extremely high reconstruction errors: normal samples produce errors around 5700–5900, meaning the reconstruction is essentially garbage. The 100% accuracy is purely coincidental — the initial threshold (6430.98, exported from the PC-side pipeline) happens to land between the normal and anomaly error ranges on this small test set. The tiny margin between normal errors (~5900) and the threshold (~6430) means even slight data variation could flip predictions. This is not a usable model — on-device training is needed to actually learn meaningful reconstructions.
8.12.3.9.4.4. Stage 4: On-Device Training
The application waits for the user to trigger training via the CCS debugger.
printf("Set start_training=1 in debugger to begin training...\n");
while (!start_training) {}
How to trigger: In CCS, open the Variables view , add start_training, and change its value from 0 to 1.
Once triggered, the training loop runs:
AnomalyDetection_StartTraining(&ad_model_ctx);
while (current_mode == MODE_TRAINING) {
if (ad_model_ctx.current_phase == PHASE_TRAIN) {
process_one_sample(&train_iter); // Feed training data
} else {
process_one_sample(&val_iter); // Feed validation data
}
AnomalyDetection_ProcessTrainingFrame(&ad_model_ctx, frozen_output, model_input);
AnomalyDetection_GetMode(&ad_model_ctx, ¤t_mode);
}
The library automatically:
Alternates between training and validation phases each epoch
Applies EMA smoothing to validation loss
Saves best weights when validation improves
Triggers early stopping after
patienceepochs without improvement
Console output (abbreviated):
=== Training Started ===
========================================
STARTING ON-DEVICE TRAINING
========================================
Epoch 0: Train Loss = 5571
Epoch 0: Val Loss = 6109 (raw), 6109 (smoothed) Best model, weights saved!
Epoch 1: Train Loss = 5547
Epoch 1: Val Loss = 5818 (raw), 5963 (smoothed) Best model, weights saved!
...
Epoch 93: Train Loss = 3132
Epoch 93: Val Loss = 1827 (raw), 3192 (smoothed) Best model, weights saved!
Epoch 94: Train Loss = 765
Epoch 94: Val Loss = 126 (raw), 1659 (smoothed) Best model, weights saved!
Epoch 95: Train Loss = 68
Epoch 95: Val Loss = 43 (raw), 851 (smoothed) Best model, weights saved!
...
Epoch 111: Val Loss = 7 (raw), 10 (smoothed) Best model, weights saved!
...
Epoch 121: Val Loss = 13 (raw), 16 (smoothed) No improvement (10/10)
========================================
EARLY STOPPING TRIGGERED
========================================
Best val loss: 10 (epoch 111)
Training Summary:
epochs trained: 121
Best val loss: 10
Quit mode: Early Stopped
8.12.3.9.4.5. Stage 5: Threshold Recalculation
After training, the model’s reconstruction errors are very different, so the threshold must be recalculated. The library automatically transitioned to MODE_THRESHOLD_CALC after training stopped.
val_iter.current_index = 0;
while (current_mode == MODE_THRESHOLD_CALC) {
process_one_sample(&val_iter);
AnomalyDetection_ProcessThresholdFrame(&ad_model_ctx, frozen_output, model_input);
AnomalyDetection_GetMode(&ad_model_ctx, ¤t_mode);
}
The 6 validation samples are cycled as we can store limited data in flash. In real world this comes from a sensor collecting live data.
Console output:
=== Threshold Calculation ===
Threshold calculation progress: 200/500
Threshold calculation progress: 400/500
========================================
THRESHOLD CALCULATION
========================================
Samples collected: 500
Threshold Calculation Results:
Mean error: 14
Std error: 6
Gaussian (mean + 4*std): 39
95th percentile: 25
99th percentile: 25
Configured percentile (0): 25
Method used: Percentile
FINAL THRESHOLD: 25
The threshold dropped from 6430.98 (pre-training) to 25.77 (post-training) — a ~250× reduction, reflecting the dramatically lower reconstruction errors on normal data.
8.12.3.9.4.6. Stage 6: Post-Training Inference
The same 12 test samples are evaluated with the trained model and new threshold.
Console output:
=== Post-training Inference on Test Data ===
Sample 0: error=692.4103 threshold=25.7727 predicted=ANOMALY label=ANOMALY OK
Sample 1: error=1001.3439 threshold=25.7727 predicted=ANOMALY label=ANOMALY OK
Sample 2: error=10.0213 threshold=25.7727 predicted=NORMAL label=NORMAL OK
Sample 3: error=131.5526 threshold=25.7727 predicted=ANOMALY label=ANOMALY OK
Sample 4: error=15.1960 threshold=25.7727 predicted=NORMAL label=NORMAL OK
Sample 5: error=7.4827 threshold=25.7727 predicted=NORMAL label=NORMAL OK
Sample 6: error=922.0417 threshold=25.7727 predicted=ANOMALY label=ANOMALY OK
Sample 7: error=9.4103 threshold=25.7727 predicted=NORMAL label=NORMAL OK
Sample 8: error=404.9972 threshold=25.7727 predicted=ANOMALY label=ANOMALY OK
Sample 9: error=12.5238 threshold=25.7727 predicted=NORMAL label=NORMAL OK
Sample 10: error=308.8339 threshold=25.7727 predicted=ANOMALY label=ANOMALY OK
Sample 11: error=11.2795 threshold=25.7727 predicted=NORMAL label=NORMAL OK
Post-training accuracy: 12/12 = 100.0%
8.12.3.9.4.7. Before vs. After Training Comparison
Metric |
Pre-Training |
Post-Training |
Change |
|---|---|---|---|
Accuracy |
12/12 (100%) |
12/12 (100%) |
Same |
Threshold |
6430.98 |
25.77 |
~250× lower |
Normal error range |
5691 – 5879 |
7.5 – 15.2 |
~400× lower |
Anomaly error range |
6949 – 11115 |
131 – 1001 |
~10× lower |
Separation ratio (min anomaly / max normal) |
6949 / 5879 = 1.18× |
131 / 15.2 = 8.6× |
Much clearer separation |
While both pre- and post-training accuracy are 100%, the quality of the model is fundamentally different. Before training, the autoencoder hadn’t learned anything — normal reconstruction errors (~5800) were nearly as high as anomaly errors (~7000–11000), and the 100% accuracy was coincidental. After training, the model has genuinely learned what “normal” looks like: normal errors dropped to ~10, while anomaly errors remain high (~130–1000). The separation ratio improved from 1.18× to 8.6×, meaning the detector is now robust to noise and threshold sensitivity rather than relying on a lucky threshold placement.
8.12.3.9.5. 4. Key Code Patterns
8.12.3.9.5.1. 4.1 process_one_sample()
This function processes one raw sample from the dataset through feature extraction and the frozen model, producing frozen_output ready for the trainable model.
void process_one_sample(DatasetIterator_t* iter) {
// For each frame in num_frame_concat:
// 1. Extract raw sensor data for all channels from the sample
// 2. Shift feature history (for temporal context)
// 3. Run feature extraction (FE_runFeatureExtract)
// After all frames processed:
// 4. Run frozen model (tvmgen_default_run)
// 5. Advance iterator (wraps around at end)
}
Why this complexity? Raw sensor data has multiple channels (X, Y, Z) and potentially multiple frames for temporal context. The feature extraction pipeline processes one frame at a time, maintaining a history buffer for concatenation. This function encapsulates that multi-frame, multi-channel processing.
8.12.3.9.5.2. 4.2 DatasetIterator_t
A simple struct for cycling through embedded datasets:
typedef struct {
const float (*inputs)[RAW_INPUT_SIZE]; // Pointer to 2D array
const int16_t* labels; // Pointer to labels
uint16_t num_samples; // Total samples
uint16_t current_index; // Current position (wraps)
} DatasetIterator_t;
Three iterators are used — train_iter, val_iter, test_iter — each pointing to the corresponding arrays in ondevice_training_data.c.
8.12.3.9.5.3. 4.3 Training Loop Pattern
The training loop checks the library’s internal phase to decide which data to feed:
while (current_mode == MODE_TRAINING) {
if (ad_model_ctx.current_phase == PHASE_TRAIN) {
process_one_sample(&train_iter); // Training data
} else {
process_one_sample(&val_iter); // Validation data
}
AnomalyDetection_ProcessTrainingFrame(&ad_model_ctx, frozen_output, model_input);
AnomalyDetection_GetMode(&ad_model_ctx, ¤t_mode);
}
The library handles everything internally — epoch counting, phase switching, batch management, early stopping. The application just feeds the right data and checks when training is done.
8.12.3.9.5.4. 4.4 Debugger Variable Trigger
volatile uint16_t start_training = 0;
// ...
while (!start_training) {}
The volatile keyword prevents the compiler from optimizing away the loop. In CCS, use Variables view to change start_training to 1 while the application is running.
8.12.3.9.6. 5. Hyperparameter Reference
Parameter |
Value |
Set In |
What It Controls |
|---|---|---|---|
|
0.0001 |
|
Weight update step size |
|
10 |
|
Epochs without improvement before early stop |
|
0.5 |
|
Minimum loss decrease to count as improvement |
|
0.5 |
|
Validation loss smoothing (higher = more reactive) |
|
10 |
|
Training samples per epoch (× batch_size) |
|
500 |
|
Normal samples for threshold calculation |
|
0.99 |
|
Percentile for threshold (if percentile method) |
|
4 |
|
Std devs above mean (if Gaussian method) |
|
1 |
|
1 = percentile, 0 = Gaussian |
|
1 |
|
Majority voting window (1 = no voting) |
|
100 |
|
Frames per statistics report |
|
6430.98 |
|
Pre-training threshold from ModelMaker |
|
1 |
|
Samples per weight update (compile-time) |
|
1 |
|
Samples per validation evaluation (compile-time) |
8.12.3.9.7. 6. How to Adapt for Your Own Data
To use this example with a different dataset or sensor:
Generate new artifacts via ModelZoo with your dataset and YAML configuration (see Running ModelZoo for ODT)
Replace artifact files in the
artifacts/directory:mod.a— new frozen modeltvmgen_default.h— new frozen model APItrainable_model_config.h/.c— new trainable architecture and weightsondevice_training_data.h/.c— new embedded datasetuser_input_config.h— new feature extraction configtest_vector.c— new golden vectors
Adjust hyperparameters in
main()if needed:Different data may need different
learning_rate(start with 0.0001, adjust if training diverges or converges too slowly)Adjust
patiencebased on expected convergence timeAdjust
threshold_samplesbased on available validation data
Modify
process_one_sample()if your sensor pipeline differs (different number of channels, different frame structure, etc.)Rebuild the CCS project and flash the device
8.12.3.9.8. 7. Further Reading
Document |
Description |
|---|---|
What is on-device training, architecture, features |
|
Generate artifacts for this example |
|
What each artifact file contains |
|
API reference for the library used in this example |
|
Core training primitives |