Skip to content
Denna sida är ännu inte tillgänglig på svenska.
← Tillbaka till projekten

TinyML Arduino Posture Classifier/ Sida 1 av 4

Setup and Hardware

Setting up the Arduino Nano 33 BLE for TinyML. Exploring the board's specifications, Echo State Network architecture, and memory constraints for edge inference.

#tinyml#embedded#machine-learning#arduino#edge-computing#federated-learning

The Hardware

We used the Arduino Nano 33 BLE, which has:

  • Processor: ARM Cortex-M4 at 64MHz
  • RAM: 256KB
  • Flash: 1MB
  • IMU: LSM9DS1 (accelerometer, gyroscope, magnetometer)
  • Connectivity: Bluetooth 5.0 (BLE)
  • Storage: MbedOS KVStore for persistent data

The board is compact (45mm × 18mm) and can run on battery power, making it suitable for wearable posture classification.

The Echo State Network

We implemented an ESN with these parameters:

#define RESERVOIR_SIZE 20    // 20 reservoir neurons
#define OUTPUT_SIZE 3        // 3 classes: sitting, standing, moving
#define INPUT_SIZE 18        // 18 features (mean + std for 9 axes)
#define LEAKY 0.3            // Leaky integration parameter
#define LEARNING_RATE 0.01   // Online gradient descent learning rate

The ESN structure:

struct ESN {
  float reservoir[RESERVOIR_SIZE];                      // Current state
  float W_in[RESERVOIR_SIZE][INPUT_SIZE];               // Input weights (random, fixed)
  float W_res[RESERVOIR_SIZE][RESERVOIR_SIZE];          // Reservoir weights (random, fixed)
  float W_out[OUTPUT_SIZE][RESERVOIR_SIZE];             // Output weights (trainable)
};

Memory usage:

  • Reservoir state: 20 floats = 80 bytes
  • Input weights: 20 × 18 = 360 floats = 1,440 bytes
  • Reservoir weights: 20 × 20 = 400 floats = 1,600 bytes
  • Output weights: 3 × 20 = 60 floats = 240 bytes
  • Total model: ~3,360 bytes

This leaves plenty of room for the training buffers, IMU data, and runtime state within the 256KB limit.

Initialization

The ESN is initialized in esn.cpp:

void initESN() {
  srand(42);  // Fixed seed for reproducibility
  
  // Zero reservoir state
  for (uint8_t i = 0; i < RESERVOIR_SIZE; i++) {
    esn.reservoir[i] = 0.0f;
  }
  
  // Random input weights (-1 to 1)
  for (uint8_t i = 0; i < RESERVOIR_SIZE; i++) {
    for (uint8_t j = 0; j < INPUT_SIZE; j++) {
      esn.W_in[i][j] = ((float)rand() / RAND_MAX) * 2.0f - 1.0f;
    }
  }
  
  // Random reservoir weights (-0.5 to 0.5)
  for (uint8_t i = 0; i < RESERVOIR_SIZE; i++) {
    for (uint8_t j = 0; j < RESERVOIR_SIZE; j++) {
      esn.W_res[i][j] = ((float)rand() / RAND_MAX) - 0.5f;
    }
  }
  
  // Try to load persisted output weights from KVStore
  if (getKVPersistedWeights(esn.W_out))
    return;  // Successfully loaded
  
  // Otherwise, zero the output weights
  for (uint8_t i = 0; i < OUTPUT_SIZE; i++) {
    for (uint8_t j = 0; j < RESERVOIR_SIZE; j++) {
      esn.W_out[i][j] = 0.0f;
    }
  }
}

The fixed random seed (42) ensures the reservoir is identical across devices, which is important for federated learning: devices start with the same reservoir and only diverge in their output weights.

Communication Modes

At startup, the device waits for a button press to select the communication mode:

  • Short press: USB Serial mode
  • Long press: BLE mode

This is handled in button.cpp and tinyml-arduino-posture-classifier.ino:

void setup() {
    initLED();
    initButton();
    coms = getCommunicationMode();  // Wait for button press
    Coms.setBackend(coms);          // Initialize selected I/O
    initIMU();
    initESN();
}

Once the mode is selected, all commands (train, validate, infer, persist, share weights) work the same way regardless of whether they come over USB or BLE.

The State Machine

The main loop in engine.cpp implements a state machine that processes commands:

void runIteration(void) {
  SerialCommandType order = Coms.receive();
  
  switch (order) {
    case CMD_NONE:
      // Normal inference mode
      updateIMU();
      if (IMUwindowReady()) {
        FeatureVector state = computeFeatures();
        normalizeVector(state);
        updateReservoir(state);
      }
      return;
    
    case CMD_TRAIN:
      // Training mode
      collectBuffer(featureBuffer, &nSamples);
      Coms.getLabel(labelBuffer, nSamples);
      normalizeBuffer(featureBuffer, nSamples);
      trainOutputLayer(featureBuffer, labelBuffer, nSamples, LEARNING_RATE);
      // ... print predictions
      break;
    
    case CMD_VAL:
      // Evaluation mode
      evaluateLoop();
      break;
    
    // ... other commands
  }
}

The batch size for training is 8 samples:

#define BATCH_SIZE 8

This is a compromise between having enough data for meaningful gradient updates and fitting within the memory constraints.