TinyML Arduino Posture Classifier/ Page 1 of 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.
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.