John Bradnam
Published © GPL3+

Laser Tachometer

A small hand-held tachometer that uses a laser to show how fast an object is rotating in revolutions per minute (rpm).

IntermediateFull instructions provided8 hours1,113
Laser Tachometer

Things used in this project

Hardware components

Microchip ATtiny1614 microproceesor
×1
TP4056 Battery Charging module
×1
Battery, 3.7 V
Battery, 3.7 V
120mA/Hr
×1
Tactile Switch, Top Actuated
Tactile Switch, Top Actuated
6mm shafts with button tops
×2
128x32 I2C OLED display module
×1
5mW Laser Diode
×1
KY-008 Laser and sensor kit
×1
Passive Components
0805 Resistors - 1x33ohm, 1x0ohm, 1x10Kohm; 0805 capacitors - 1x0.1uF
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

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

Story

Read more

Custom parts and enclosures

STL Files

STL files for the slicing software

Schematics

Schematic

PCB

Eagle Files

Schematic and PCB in Eagle format

Code

TachometerV1.ino

C/C++
// Laser Tachometer V1
// Based on a design by Elite Worm (https://www.hackster.io/Elite_Worm/build-a-laser-tachometer-c5a320)
//
// 
// V1: jlb (jbrad2089@gmail.com)
//  - Wrote software for a ATtiny1614 and 128x32 I2C OLED display
//

/**
 * ATTiny1614 Pins mapped to Ardunio Pins
 *
 *             +--------+
 *         VCC + 1   14 + GND
 * (SS)  0 PA4 + 2   13 + PA3 10 (SCK)
 *       1 PA5 + 3   12 + PA2 9  (MISO)
 * (DAC) 2 PA6 + 4   11 + PA1 8  (MOSI)
 *       3 PA7 + 5   10 + PA0 11 (UPDI)
 * (RXD) 4 PB3 + 6    9 + PB0 7  (SCL)
 * (TXD) 5 PB2 + 7    8 + PB1 6  (SDA)
 *             +--------+
 */

#include <OneWire.h>
#include <U8g2lib.h>
#include <avr/sleep.h>

#define MEASURE_BTN 0   //PA4 D0
#define ON_BTN 1        //PA5 D1
#define SENSOR 9        //PA2 D9
#define LASER 10        //PA3 D10

U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

volatile uint32_t pulseStartTime = 0;   //Time revolution started
volatile uint32_t pulseDuration = 0;    //Duration of revolution

char textBuffer[20];                    //Used for text measurement

//Used to store a running average
#define AVG_SIZE 128
#define AVG_MASK 0x7F
uint32_t runningTotal = 0;
uint16_t runningCount = 0;
uint8_t runningHead = 0; 
uint8_t runningTail = 0;
uint32_t runningValues[AVG_SIZE];

#define PULSE_TIMEOUT 5000
#define UPDATE_TIME 300
uint32_t updateTimeout;

//-----------------------------------------------------------------------------
//Initialise Hardware
void setup() 
{
  //Serial.begin(9600);

  pinMode(LASER, OUTPUT);
  digitalWrite(LASER, LOW);
  pinMode(SENSOR, INPUT);
  attachInterrupt(digitalPinToInterrupt(SENSOR), SensorInterrupt, RISING);
  
  //ON button Interrupt will wake up from sleep mode
  pinMode(ON_BTN, INPUT_PULLUP);
  pinMode(MEASURE_BTN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(ON_BTN),SwitchInterrupt,CHANGE);

  u8g2.begin();
  u8g2.clearBuffer();         // clear the internal memory
  u8g2.sendBuffer();          // transfer internal memory to the display
  
  systemSleep();  
}

//-----------------------------------------------------------------------------
//Handle pin change interrupt when ON button is pressed
// - This will wake up the microprocessor
// - Code will continue after the sleep_mode() command in the systemSleep() function.
void SwitchInterrupt()
{
}

//-----------------------------------------------------------------------------
//Handle pin change interrupt when ON button is pressed
// - This will trigger on the rising edge of the sensor
void SensorInterrupt()
{
  
  pulseDuration = micros() - pulseStartTime;
  pulseStartTime = micros();
}

//--------------------------------------------------------------------
// Show splash screen

void showSplashScreen()
{
  //Splash screen
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_helvB12_tr); // helvetica bold
  u8g2.drawStr(20,14,"ATtiny1614");   // x,y,text
  u8g2.drawStr(15,29,"Laser Tacho");
  u8g2.sendBuffer();                // transfer internal memory to the display
  delay(3000);

  strcpy(textBuffer, "Ready");
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_logisoso20_tr); // helvetica bold
  int width = u8g2.getStrWidth(textBuffer);
  u8g2.drawStr((128-width)/2,26,textBuffer);
  u8g2.sendBuffer();                // transfer internal memory to the display
}

//-----------------------------------------------------------------------------
//Main Program Loop
void loop() 
{
  uint32_t started;
  uint32_t duration;
  uint32_t rpm;
  
  while (digitalRead(ON_BTN) == HIGH)
  {
    if (digitalRead(MEASURE_BTN) == LOW)
    {
      //Switch on laser
      digitalWrite(LASER, HIGH);
      delay(10);
      
      //Start reading the sensor
      noInterrupts();
      pulseDuration = 0;          //Wait for at least one revolution
      pulseStartTime = micros();  //If no intertupts, we can test this for a timeout
      interrupts();

      //Clear out any old running totals
      runningTotal = 0;
      runningCount = 0;
      runningHead = 0; 
      runningTail = 0;
      
      while (digitalRead(MEASURE_BTN) == LOW)
      {
        noInterrupts();
        started = pulseStartTime;
        duration = pulseDuration;
        interrupts();

        if (duration == 0 && (millis() - started) > PULSE_TIMEOUT)
        {
          //No pulse so we aren't measuring anything
          rpm = 0;
        }
        else if (duration > 0)
        {
          //Get the RPM and add to running average
          rpm = (60000000 / duration);
          if (runningCount == AVG_SIZE)
          {
            //Remove oldest value from running total
            runningTotal = runningTotal - runningValues[runningTail];
            runningTail = (runningTail + 1) & AVG_MASK;
            runningCount--;
          }
          //Add new value to running total
          runningValues[runningHead] = rpm;
          runningHead = (runningHead + 1) & AVG_MASK;
          runningTotal = runningTotal + rpm;
          runningCount++;
          rpm = runningTotal / runningCount;
        }

        //Display running average
        if (millis() > updateTimeout)
        {
          displayNumber(rpm);
          updateTimeout = millis() + UPDATE_TIME;
        }
      }
      
      //Switch off laser
      digitalWrite(LASER, LOW);
    }
    delay(100);
  }
  
  //Wait until ON button is released
  while (digitalRead(ON_BTN) == LOW)
  {
    delay(100);
  }
  //Shut down unit
  systemSleep();
}

//--------------------------------------------------------------------
// display current RPM
void displayNumber(uint32_t  number)
{
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_logisoso20_tr); // helvetica bold
  sprintf(textBuffer,"%5lu",number);
  int twidth = u8g2.getStrWidth(textBuffer);
  u8g2.setFont(u8g2_font_helvB12_tr); // helvetica bold
  strcpy(textBuffer," rpm ");
  int rwidth = u8g2.getStrWidth(textBuffer);
  int tx = max(128 - (twidth + rwidth), 0);
  u8g2.drawStr(tx + twidth,30,textBuffer);
  sprintf(textBuffer,"%5lu",number);
  u8g2.setFont(u8g2_font_logisoso20_tr); // helvetica bold
  u8g2.drawStr(tx,30,textBuffer);
  u8g2.sendBuffer();                // transfer internal memory to the display
}

//-----------------------------------------------------------------------------
//Shut down OLED and put ATtiny to sleep
//Will wake up when LEFT button is pressed
void systemSleep() 
{
  interrupts();
  u8g2.clearBuffer();         // clear the internal memory
  u8g2.sendBuffer();          // transfer internal memory to the display
  u8g2.setPowerSave(true);
  //cbi(ADCSRA,ADEN);                   // switch Analog to Digitalconverter OFF
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);  // sleep mode is set here
  sleep_enable();
  sleep_mode();                         // System actually sleeps here
  sleep_disable();                      // System continues execution here when watchdog timed out
  //sbi(ADCSRA,ADEN);                   // switch Analog to Digitalconverter ON
  u8g2.setPowerSave(false);
  //Wait until on button is released
  while (digitalRead(ON_BTN) == LOW)
  {
    delay(100);
  }
  showSplashScreen();
  updateTimeout = 0;
}

Credits

John Bradnam

John Bradnam

141 projects β€’ 167 followers
Thanks to Elite Worm.

Comments