shady_bob
Published © MIT

Chainsaw Edu Model

3D printed IoT model of working chainsaw in scale for educational purposes. Tactile feedback, moving elements, sound, led illumination.

AdvancedFull instructions provided8 hours46
Chainsaw Edu Model

Things used in this project

Hardware components

M5Stack M5Stamp C3
×1
I2S audio DAC AMP module
×1
Step-down module 1.0 V - 17 V HW-187 MINI-360
×1
DC motor with 1:48 gear 3-6V with double sided shaft
×1
Transistor N-MOSFET IRLZ44N
×2
Limit switch mini with roller - WK625
×1
Electrolytic capacitor 220uF/25V or 35V
×2
Ceramic condenser 100nF / 50V THT
×2
Schottky diode 1N5822 3A / 40V
×2
LED RGB 5mm matte common cathode
×1
Resistors (33Ω, 68Ω, 100Ω) from set of CF THT 1/4W resistors
×3
Mini Vibration Motor 10x2,7mm - 3V
×1
USB Type-C Female Connector
×1
White polypropylene string 15 kg 1.5 mm x 50 m
×1

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

blade_guide_1_ZLyvP0QjT8.stl

Sketchfab still processing.

blade_guide_2_nltRgUUVmI.stl

Sketchfab still processing.

button_enclosure_ZTYZg8tedX.stl

Sketchfab still processing.

dc_motor_stand_n3tIwI4V6o.stl

Sketchfab still processing.

drive_wheel_LzZfELPegS.stl

Sketchfab still processing.

electronic_base_plate_4C4RF6Igoc.stl

Sketchfab still processing.

handle_8mtISyTbQI.stl

Sketchfab still processing.

left_cover_6DO4xBoh34.stl

Sketchfab still processing.

main_body_dvvf5LRaH2.stl

Sketchfab still processing.

right_cover_ICpXrrR2ve.stl

Sketchfab still processing.

Schematics

connection diagram

Code

main.cpp

C/C++
platformio project
#include <Arduino.h>
#include <FS.h>
#include <LittleFS.h>
#include <driver/i2s.h>
#include <Adafruit_NeoPixel.h>

//────────────────────────────────────────────────────────────────────────────
// 1) PIN / CHANNEL DEFINITIONS
//────────────────────────────────────────────────────────────────────────────

//— Button (active LOW) —
#define BUTTON_PIN    5   // external pushbutton on GPIO5

//— DC Motor (via MOSFET, PWM) —
const int   DC_MOTOR_PIN     = 1;    // GPIO1 → DC motor MOSFET gate
const int   DC_MOTOR_CHANNEL = 0;    // LEDC channel for DC motor
const int   PWM_FREQ_HZ      = 200;  // 200 Hz PWM for motors
const int   PWM_RESOLUTION   = 8;    // 8-bit resolution (0…255)
const int   PWM_MIN          = 40;   // minimum duty cycle
const int   PWM_MAX          = 50;   // maximum duty cycle
const int   PWM_STEP         = 5;    // step for ramping
const int   PWM_DELAY_MS     = 500;  // ramp step every 500 ms

//— Vibration Motor (ON/OFF, active HIGH) —
const int   VIB_MOTOR_PIN    = 0;    // GPIO0 → vibration motor

//— NeoPixel (onboard RGB) —
#define LED_PIN      2
#define NUM_LEDS     1
Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);

//— External RGB LED (three separate LEDC channels) —
#define EXT_RED_PIN    4
#define EXT_GREEN_PIN  21
#define EXT_BLUE_PIN   20

const int EXT_RED_CH    = 1;
const int EXT_GREEN_CH  = 2;
const int EXT_BLUE_CH   = 3;

//— I²S (audio) PIN CONFIGURATION —
#define I2S_NUM         I2S_NUM_0
#define I2S_BCK_IO      10  // BCLK
#define I2S_WS_IO       7  // LRCK
#define I2S_DO_IO       6  // DIN

//— WAV file paths in LittleFS —
const char *SOUND_START    = "/chainsaw-start.wav";
const char *SOUND_IDLE     = "/chainsaw-idle_on.wav";
const char *SOUND_PUSH     = "/chainsaw-button_push.wav";
const char *SOUND_CUTTING  = "/chainsaw-cutting_v2.wav";

//— Volume (0.0 … 1.0) —
const float VOLUME_IDLE    = 0.3f;  // ~2%
const float VOLUME_PUSH    = 0.3f;
const float VOLUME_CUT     = 0.3f;

//────────────────────────────────────────────────────────────────────────────
// 2) STATE MACHINE DEFINITION

//────────────────────────────────────────────────────────────────────────────
enum ChainsawState {
  IDLE,
  STARTING,
  RUNNING
};
volatile ChainsawState chainsawState = IDLE;
volatile bool        stateChanged   = false;

//────────────────────────────────────────────────────────────────────────────
// 3) FORWARD DECLARATIONS OF TASKS & ISR
//────────────────────────────────────────────────────────────────────────────
void IRAM_ATTR handleButtonISR();
void audioTask(void *pvParameters);
void motorTask(void *pvParameters);
void vibTask(void *pvParameters);
void ledTask(void *pvParameters);
void setupI2S();

//────────────────────────────────────────────────────────────────────────────
// 4) HELPER: Set external RGB LED via LEDC
//────────────────────────────────────────────────────────────────────────────
void setExternalRGB(uint8_t r, uint8_t g, uint8_t b) {
  ledcWrite(EXT_RED_CH,   r);
  ledcWrite(EXT_GREEN_CH, g);
  ledcWrite(EXT_BLUE_CH,  b);
}

//────────────────────────────────────────────────────────────────────────────
// 5) SETUP()
//────────────────────────────────────────────────────────────────────────────
void setup() {
  Serial.begin(115200);
  delay(100);

  //—— Mount LittleFS for WAV files ——
  if (!LittleFS.begin()) {
    Serial.println("❌ LittleFS Mount Failed");
    while (true) { delay(10); }
  }

  //—— Initialize I²S for 16 kHz, 16-bit WAV playback ——
  setupI2S();

  //—— Button as INPUT_PULLUP + attach interrupt ——
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), handleButtonISR, CHANGE);

  //—— Onboard NeoPixel ——
  strip.begin();
  strip.show();

  //—— DC motor PWM channel setup ——
  ledcSetup(DC_MOTOR_CHANNEL, PWM_FREQ_HZ, PWM_RESOLUTION);
  ledcAttachPin(DC_MOTOR_PIN, DC_MOTOR_CHANNEL);
  ledcWrite(DC_MOTOR_CHANNEL, 0);

  //—— Vibration motor ——
  pinMode(VIB_MOTOR_PIN, OUTPUT);
  digitalWrite(VIB_MOTOR_PIN, LOW);

  //—— External RGB LED PWM channels ——
  ledcSetup(EXT_RED_CH,   PWM_FREQ_HZ, PWM_RESOLUTION);
  ledcAttachPin(EXT_RED_PIN,   EXT_RED_CH);
  ledcSetup(EXT_GREEN_CH, PWM_FREQ_HZ, PWM_RESOLUTION);
  ledcAttachPin(EXT_GREEN_PIN, EXT_GREEN_CH);
  ledcSetup(EXT_BLUE_CH,  PWM_FREQ_HZ, PWM_RESOLUTION);
  ledcAttachPin(EXT_BLUE_PIN,  EXT_BLUE_CH);
  setExternalRGB(0, 0, 0);

  //—— Create FreeRTOS tasks ——
  xTaskCreatePinnedToCore(audioTask, "AudioTask", 4096, NULL, 2, NULL, 0);
  xTaskCreatePinnedToCore(motorTask, "MotorTask", 4096, NULL, 1, NULL, 0);
  xTaskCreatePinnedToCore(vibTask,   "VibTask",   2048, NULL, 1, NULL, 0);
  xTaskCreatePinnedToCore(ledTask,   "LEDTask",   2048, NULL, 1, NULL, 0);

  // Delete the default loop task; everything runs in RTOS tasks now.
  vTaskDelete(NULL);
}

//────────────────────────────────────────────────────────────────────────────
// 6) I²S INITIALIZATION (16 kHz, 16-bit, stereo but files can be mono)
//────────────────────────────────────────────────────────────────────────────
void setupI2S() {
  i2s_config_t i2s_config = {
    .mode                 = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
    .sample_rate          = 16000,
    .bits_per_sample      = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format       = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = I2S_COMM_FORMAT_I2S_MSB,
    .intr_alloc_flags     = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count        = 8,
    .dma_buf_len          = 64,
    .use_apll             = false,
    .tx_desc_auto_clear   = true
  };

  i2s_pin_config_t pin_config = {
    .bck_io_num   = I2S_BCK_IO,
    .ws_io_num    = I2S_WS_IO,
    .data_out_num = I2S_DO_IO,
    .data_in_num  = I2S_PIN_NO_CHANGE
  };

  i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM, &pin_config);
}

//────────────────────────────────────────────────────────────────────────────
// 7) BUTTON ISR
//────────────────────────────────────────────────────────────────────────────
void IRAM_ATTR handleButtonISR() {
  bool isPressed = (digitalRead(BUTTON_PIN) == LOW);

  if (isPressed) {
    if (chainsawState == IDLE) {
      chainsawState = STARTING;
      stateChanged   = true;
    }
  } else {
    if (chainsawState == RUNNING || chainsawState == STARTING) {
      chainsawState = IDLE;
      stateChanged   = true;
    }
  }
}

//────────────────────────────────────────────────────────────────────────────
// 8) AUDIO TASK
//────────────────────────────────────────────────────────────────────────────
void audioTask(void *pvParameters) {
  (void)pvParameters;
  for (;;) {
    const char *filePath = nullptr;
    float       volume   = 0.0f;

    switch (chainsawState) {
      case STARTING:
        filePath = SOUND_PUSH;
        volume   = VOLUME_PUSH;
        break;
      case RUNNING:
        filePath = SOUND_CUTTING;
        volume   = VOLUME_CUT;
        break;
      case IDLE:
      default:
        filePath = SOUND_IDLE;
        volume   = VOLUME_IDLE;
        break;
    }

    File f = LittleFS.open(filePath, "r");
    if (!f) {
      Serial.printf("❌ Failed to open %s\n", filePath);
      vTaskDelay(pdMS_TO_TICKS(500));
      continue;
    }

    // Skip WAV header
    uint8_t header[44];
    if (f.read(header, 44) < 44) {
      Serial.printf("❌ Invalid WAV header: %s\n", filePath);
      f.close();
      vTaskDelay(pdMS_TO_TICKS(500));
      continue;
    }

    static uint8_t buf[512];
    while (true) {
      // If STARTING finished playing once, switch to RUNNING:
      if (chainsawState == STARTING && filePath == SOUND_PUSH && !f.available()) {
        chainsawState = RUNNING;
        stateChanged   = true;
        break;
      }
      // If user left IDLE but we were still streaming idle.wav, break:
      if (chainsawState != IDLE && filePath == SOUND_IDLE && !f.available()) {
        break;
      }
      // If user released button during PUSH or CUT, break:
      if (chainsawState == IDLE && filePath != SOUND_IDLE) {
        break;
      }
      // If running finished one loop of cutting.wav, reopen to loop again:
      if (chainsawState == RUNNING && filePath == SOUND_CUTTING && !f.available()) {
        f.close();
        f = LittleFS.open(SOUND_CUTTING, "r");
        if (!f) {
          Serial.println("❌ Failed to re-open cutting.wav");
          break;
        }
        f.read(header, 44);
        continue;
      }
      if (!f.available()) {
        break;
      }

      size_t chunk = min<size_t>(sizeof(buf), f.available());
      size_t r     = f.read(buf, chunk);
      if (r == 0) break;

      // Volume scale (16-bit PCM)
      for (size_t i = 0; i + 1 < r; i += 2) {
        int16_t sample = (int16_t)(buf[i] | (buf[i + 1] << 8));
        sample = (int16_t)(sample * volume);
        buf[i]     = (uint8_t)(sample & 0xFF);
        buf[i + 1] = (uint8_t)((sample >> 8) & 0xFF);
      }

      size_t written = 0;
      i2s_write(I2S_NUM, buf, r, &written, portMAX_DELAY);

      // Break out immediately if state changed:
      if (chainsawState == IDLE && filePath != SOUND_IDLE) {
        break;
      }
      if (chainsawState == RUNNING && filePath == SOUND_IDLE) {
        break;
      }
      if (chainsawState == STARTING && filePath != SOUND_PUSH) {
        break;
      }
    }

    f.close();
    vTaskDelay(pdMS_TO_TICKS(10));
  }
}

//────────────────────────────────────────────────────────────────────────────
// 9) MOTOR TASK (DC motor ramp up/down)
//────────────────────────────────────────────────────────────────────────────
void motorTask(void *pvParameters) {
  (void)pvParameters;
  int  pwmValue   = PWM_MIN;
  bool increasing = true;

  for (;;) {
    if (chainsawState == RUNNING) {
      ledcWrite(DC_MOTOR_CHANNEL, pwmValue);

      if (increasing) {
        pwmValue += PWM_STEP;
        if (pwmValue >= PWM_MAX) {
          pwmValue   = PWM_MAX;
          increasing = false;
        }
      } else {
        pwmValue -= PWM_STEP;
        if (pwmValue <= PWM_MIN) {
          pwmValue   = PWM_MIN;
          increasing = true;
        }
      }

      vTaskDelay(pdMS_TO_TICKS(PWM_DELAY_MS));
    }
    else {
      ledcWrite(DC_MOTOR_CHANNEL, 0);
      vTaskDelay(pdMS_TO_TICKS(10));
    }
  }
}

//────────────────────────────────────────────────────────────────────────────
// 10) VIBRATION TASK
//────────────────────────────────────────────────────────────────────────────
void vibTask(void *pvParameters) {
  (void)pvParameters;
  bool vibOn = false;

  for (;;) {
    if (chainsawState == IDLE) {
      vibOn = !vibOn;
      digitalWrite(VIB_MOTOR_PIN, vibOn ? HIGH : LOW);
      vTaskDelay(pdMS_TO_TICKS(250)); // 250 ms → 2 Hz
    }
    else if (chainsawState == RUNNING) {
      digitalWrite(VIB_MOTOR_PIN, HIGH);
      vTaskDelay(pdMS_TO_TICKS(10));
    }
    else {
      // STARTING: keep it OFF until RUNNING begins
      digitalWrite(VIB_MOTOR_PIN, LOW);
      vTaskDelay(pdMS_TO_TICKS(10));
    }
  }
}

//────────────────────────────────────────────────────────────────────────────
// 11) LED TASK (NeoPixel + external RGB)
//────────────────────────────────────────────────────────────────────────────
void ledTask(void *pvParameters) {
  (void)pvParameters;

  for (;;) {
    switch (chainsawState) {
      case STARTING:
        // Solid RED
        strip.setPixelColor(0, strip.Color(255, 0, 0));
        strip.show();
        setExternalRGB(255, 0, 0);
        break;

      case RUNNING:
        // Solid BLUE
        strip.setPixelColor(0, strip.Color(0, 0, 255));
        strip.show();
        setExternalRGB(0, 0, 255);
        break;

      case IDLE:
      default:
        // Solid GREEN
        strip.setPixelColor(0, strip.Color(0, 255, 0));
        strip.show();
        setExternalRGB(0, 255, 0);
        break;
    }
    vTaskDelay(pdMS_TO_TICKS(100));
  }
}

//────────────────────────────────────────────────────────────────────────────
// 12) loop() is never used (deleted in setup)
//────────────────────────────────────────────────────────────────────────────
void loop() {
  // not used
}

platformio.ini

Plain text
platformio project
; PlatformIO Project Configuration File
;
;   Build options: build flags, source filter
;   Upload options: custom upload port, speed and extra flags
;   Library options: dependencies, extra library storages
;   Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html

[env:m5stamp_c3]
platform = espressif32
board = esp32-c3-devkitm-1 
framework = arduino
monitor_speed = 115200
board_build.filesystem = littlefs
board_build.partitions = partitions.csv
lib_deps =
  bodmer/TFT_eSPI
    adafruit/Adafruit NeoPixel @ ^1.11.0
  https://github.com/espressif/arduino-esp32.git#2.0.11
build_flags =
  -DARDUINO_ARCH_ESP32
  -DNEOPIXEL_PIN=2      

partitions.csv

Plain text
partitions table for sound files to upload to processor
# Name,   Type, SubType, Offset,  Size
nvs,      data, nvs,     0x9000,  0x5000
otadata,  data, ota,     0xe000,  0x2000
app0,     app,  ota_0,   0x10000, 0x140000
spiffs,   data, spiffs,  0x150000,0x2B0000

Credits

shady_bob
1 project • 0 followers
Ex-student of LUT university with initial protfolio of IoT projects as well as software-only projects

Comments