Hardware components | ||||||
![]() |
| × | 1 | |||
![]() |
| × | 1 | |||
![]() |
| × | 1 | |||
Software apps and online services | ||||||
![]() |
| |||||
Hand tools and fabrication machines | ||||||
![]() |
| |||||
![]() |
|
As a man with many hobbies including electronics and airguns I decided to use an old Arduino board laying around and make something useful. I always wanted to know what the real speed of my airguns was but never had a real chronograph, soI decided to make one using the mentioned Arduino board.
And here is the finished product:
The design is based on photodiodes, each triggering an LM339 comparator. There are two photodiodes (cheap chinese photo diodes) on both in and out sensors and they work as Inclusive OR logic. The photodiodes are illuminated with 4 IR leds each (8 in total) creating kind of 4x2 beam mesh. When there is a difference in the light received by the photodiode/s a high signal is sent to the input pins on the Arduino.
Hardest part is adjusting the sensitivity of the sensors to get reliable results. It is done by adjusting the potentiometer for each photodiode. It took some trial and error but once set you don't need to touch it again. So far it works perfectly every time if the shot is not way off center.
Check it out and feel free to ask for help if you want to build something similar.
Chronograph v2.0
ArduinoMenu functionality has been added for setig up pellet weight needed for energy calculation.
TO DO, add additional functionalities, such as average speed based on no. of shots etc.
Chronograph
Version 2.00
Sketch provides basic chronograph functionality by measuring time between trigerring both sensors and displaying calculated m/s and fps.
Menu functionality has been added for setig up pellet weight needed for energy calculation.
TO DO, add additional functionalities, such as average speed based on no. of shots etc.
Thanks to Phil Grant, polepole for providing parts of the code
Petar Janevski 27/04/2020
*/
#include <EEPROM.h>
//#include <EEPROMAnything.h>
#include <avr/sleep.h>
#include <LiquidCrystal.h>
#define Trip_in_1 8 //set light Trigger 1 to pin 8
#define Trip_in_2 9 //set light Trigger 2 to pin 9
#define UP 14 //Set the menu up button to input 14
#define DOWN 15 //Set the menu down button to input 15
#define OK_BTN 16 //Set menu button OK to input 16
#define WAIT_STATE 0
#define MONITOR_STATE 1
#define OUTPUT_STATE 2
#define MENU_STATE 3
unsigned long Trigger_1 = 0;
unsigned long Trigger_2 = 0;
//volatile bool adcDone;
//volatile boolean is_timeout = false;
volatile unsigned int current_state = WAIT_STATE;
//unsigned long my_timeout = 5000000;
//char my_oldSREG; //to hold status register while ints are disabled
unsigned long SensDist = 100000; //Distance between sensors in meters*1000000 (328084)
float pWeight; //Pellet weight in grains
int weightTens; //first two digits of pWeight
int weightDec; //Decimal part of Pellet weight
float mps = 0; //storage for meter per sec value
float fps = 0; //storage for feet per sec value
float joules = 0; //storage for feet per sec value
unsigned long duration = 0; // time between triger 1 and 2
//boolean firstShot = false; //flag to detect that a shot has been fired. remove adj. pellet weight
unsigned long timeout = 0; //timer to return to main screen after measurement
const int menuTimeout1 = 20000; // time of inactivity to return to main screen after measurement
/*
MENU REQUIRED GLOBAL VARIABLES & DEFINITIONS
*/
#define MOVECURSOR 1 // constants for indicating whether cursor should be redrawn
#define MOVELIST 2 // constants for indicating whether cursor should be redrawn
byte totalRows = 2; // total rows of LCD
byte totalCols = 16; // total columns of LCD
unsigned long timeoutTime = 0; // this is set and compared to millis to see when the user last did something.
const int menuTimeout = 5000; // time to timeout in a menu when user doesn't do anything.
volatile unsigned int menu_state;
unsigned long lastButtonPressed; // this is when the last button was pressed. It's used to debounce.
const int debounceTime = 300; //this is the debounce and hold delay.
// initialize the LCD library with the numbers of the interface pins
LiquidCrystal lcd(5, 6, 7, 10, 11, 12);
void setup () {
Serial.begin(9600);
Serial.println("Chrono v2.00");
//Retreive pellet weight
weightTens = EEPROM.read(0);
weightDec = EEPROM.read(1);
pWeight = weightTens + float(weightDec)/100;
// EEPROM_readAnything(0, Config);
Serial.print("EEPROM 0 = ");
Serial.println(EEPROM.read(0));
Serial.print("EEPROM 1/100 = ");
Serial.println(float(EEPROM.read(1))/100);
Serial.print("pWeight = ");
Serial.println(pWeight);
Serial.print("weightTens = ");
Serial.println(weightTens);
Serial.print("weightDec = ");
Serial.println(weightDec);
pinMode(Trip_in_1, INPUT); //declare pins in use
pinMode(Trip_in_2, INPUT);
pinMode(UP, INPUT);
pinMode(DOWN, INPUT);
pinMode(OK_BTN, INPUT);
digitalWrite(UP, HIGH); //turn on pullup resistor for buttons
digitalWrite(DOWN, HIGH);
digitalWrite(OK_BTN, HIGH);
lcd.begin(16, 2); //2 line 16 character LCD
lcd.clear();
lcd.setCursor(5, 0); //Draw splash screen
lcd.print("Chrono");
lcd.setCursor(4, 1);
lcd.print("Ver 2.00");
delay (3000); //show splash screen for 5 seconds
defaultScreen();
reset_variables();
}
void loop () {
switch (current_state) {
case WAIT_STATE:
if(digitalRead(Trip_in_1)==LOW) {
Trigger_1 = micros();
current_state = MONITOR_STATE;
}
if (digitalRead(OK_BTN)==LOW) {
basicMenu();
delay(200);
Serial.println("back in main loop");
defaultScreen();
}
if (timeout != 0 && timeout<millis()){ // user hasn't done anything in awhile
defaultScreen();
timeout = 0;
}
break;
case MONITOR_STATE:
while(digitalRead(Trip_in_2)==HIGH); //loop until the Trigger goes LOW && !is_timeout
Trigger_2 = micros();
//Serial.println("Detected exit");
current_state = OUTPUT_STATE;
break;
case OUTPUT_STATE:
output_serial_info();
reset_variables();
timeout = millis() + menuTimeout1;
current_state = WAIT_STATE;
break;
}
}
void output_serial_info() {
//firstShot = true; //First shot has been fired so clear pellet weight adj. message
lcd.clear();
mps = float(SensDist)/(Trigger_2 - Trigger_1);
mps = constrain(mps,0,999); //limit mps tp plus value upto 999
fps = mps*3.28084;
joules = ((pWeight*0.00006479)*(mps*mps))/2;
//Send data to LCD
if (mps<999.9) { //Check for large no.
Serial.print("m/s: ");
Serial.println(int(mps));
Serial.print("fps: ");
Serial.println(int(fps));
lcd.setCursor(0, 0);
lcd.print("m/s:");
lcd.print(int(mps));
lcd.setCursor(0, 1);
lcd.print("fps:");
lcd.print(int(fps));
lcd.setCursor(9, 0);
lcd.print("joules:");
lcd.setCursor(11, 1);
lcd.print(joules);
}
}
void reset_variables() {
Trigger_1 = 0;
Trigger_2 = 0;
}
/*
MENU ROUTINE
*/
void basicMenu(){
byte topItemDisplayed = 0; // stores menu item displayed at top of LCD screen
byte cursorPosition = 0; // where cursor is on screen, from 0 --> totalRows.
// redraw = 0 - don't redraw
// redraw = 1 - redraw cursor
// redraw = 2 - redraw list
byte redraw = MOVELIST; // triggers whether menu is redrawn after cursor move.
byte i=0; // temp variable for loops.
byte totalMenuItems = 0; //a while loop below will set this to the # of menu items.
// Put the menu items here. Remember, the first item will have a 'position' of 0.
const char* menuItems[]={
"Set Pweight",
"Exit",
"",
};
while (menuItems[totalMenuItems] != ""){
totalMenuItems++; // count how many items are in list.
}
totalMenuItems--; //subtract 1 so we know total items in array.
lcd.clear(); // clear the screen so we can paint the menu.
boolean stillSelecting = true; // set because user is still selecting.
boolean set_pweight = true;
boolean enter_menu = true;
timeoutTime = millis() + menuTimeout; // set initial timeout limit.
do { // loop while waiting for user to select.
/*
IF YOU WANT OTHER CODE GOING ON IN THE BACKGROUND
WHILE WAITING FOR THE USER TO DO SOMETHING, PUT IT HERE
*/
switch(read_buttons()) { // analyze button pressed response. Default is 0.
case 1: // EQUIVALENT OF 'UP' BUTTON PUSHED
timeoutTime = millis()+menuTimeout; // reset timeout timer
// if cursor is at top and menu is NOT at top
// move menu up one.
if(cursorPosition == 0 && topItemDisplayed > 0) // Cursor is at top of LCD, and there are higher menu items still to be displayed.
{
topItemDisplayed--; // move top menu item displayed up one.
redraw = MOVELIST; // redraw the entire menu
}
// if cursor not at top, move it up one.
if(cursorPosition>0)
{
cursorPosition--; // move cursor up one.
redraw = MOVECURSOR; // redraw just cursor.
}
break;
case 2: // EQUIVALENT OF 'DOWN' BUTTON PUSHED
timeoutTime = millis()+menuTimeout; // reset timeout timer
// this sees if there are menu items below the bottom of the LCD screen & sees if cursor is at bottom of LCD
if((topItemDisplayed + (totalRows-1)) < totalMenuItems && cursorPosition == (totalRows-1))
{
topItemDisplayed++; // move menu down one
redraw = MOVELIST; // redraw entire menu
}
if(cursorPosition<(totalRows-1)) // cursor is not at bottom of LCD, so move it down one.
{
cursorPosition++; // move cursor down one
redraw = MOVECURSOR; // redraw just cursor.
}
break;
case 4: // EQUIVALENT TO 'SELECT' OR 'OKAY' BEING PUSHED
timeoutTime = millis()+menuTimeout; // reset timeout timer
switch(topItemDisplayed + cursorPosition){ // adding these values together = where on menuItems cursor is.
// put code to be run when specific item is selected in place of the Serial.print filler.
// the Serial.print code can be removed, but DO NOT change the case & break structure.
// (Obviously, you should have as many case instances as you do menu items.)
case 0: // menu item 1 selected
Serial.print("Menu item ");
Serial.print(topItemDisplayed + cursorPosition);
Serial.print(" selected - ");
Serial.println(menuItems[topItemDisplayed + cursorPosition]);
if(enter_menu == false){
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Pellet weight:");
lcd.setCursor(1,1);
lcd.print(pWeight);
Serial.println(pWeight);
do{
switch(read_buttons()){
case 1:
pWeight = pWeight + 0.01 ;
weightDec = (pWeight - int(pWeight))*100;
lcd.setCursor(1,1);
lcd.print(pWeight);
break;
case 2:
pWeight = pWeight - 0.01 ;
weightDec = (pWeight - int(pWeight))*100;
lcd.setCursor(1,1);
lcd.print(pWeight);
break;
case 4:
delay(200);
EEPROM.write(0,int(pWeight));
EEPROM.write(1,weightDec);
set_pweight = false;
break;
}
}while (set_pweight == true);
}
// to have nested menus,copy this function(i.e. all of basicMenu) into a new function
enter_menu = false;
break;
case 1: // menu item 2 selected
Serial.print("Menu item ");
Serial.print(topItemDisplayed + cursorPosition);
Serial.print(" selected - ");
Serial.println(menuItems[topItemDisplayed + cursorPosition]);
stillSelecting = false;
Serial.println("exit");
// add as many "case #:" as items you have. You could put
// line separators in menuList and leave out the
// corresponding case, which would mean that nothing
// would be triggered when user selected the line separator.
break;
}
case 8: // button was pushed for long time. This corresponds to "Back" or "Cancel" being pushed.
// stillSelecting = false;
// Serial.println("Button held for a long time");
break;
}
if(stillSelecting == true){
switch(redraw){ // checks if menu should be redrawn at all.
case MOVECURSOR: // Only the cursor needs to be moved.
redraw = false; // reset flag.
if (cursorPosition > totalMenuItems) // keeps cursor from moving beyond menu items.
cursorPosition = totalMenuItems;
for(i = 0; i < (totalRows); i++){ // loop through all of the lines on the LCD
lcd.setCursor(0,i);
lcd.print(" "); // and erase the previously displayed cursor
lcd.setCursor((totalCols-1), i);
lcd.print(" ");
}
lcd.setCursor(0,cursorPosition); // go to LCD line where new cursor should be & display it.
lcd.print(">");
lcd.setCursor((totalCols-1), cursorPosition);
lcd.print("<");
break; // MOVECURSOR break.
case MOVELIST: // the entire menu needs to be redrawn
redraw=MOVECURSOR; // redraw cursor after clearing LCD and printing menu.
lcd.clear(); // clear screen so it can be repainted.
if(totalMenuItems>((totalRows-1))){ // if there are more menu items than LCD rows, then cycle through menu items.
for (i = 0; i < (totalRows); i++){
lcd.setCursor(1,i);
lcd.print(menuItems[topItemDisplayed + i]);
}
}
else{ // if menu has less items than LCD rows, display all available menu items.
for (i = 0; i < totalMenuItems+1; i++){
lcd.setCursor(1,i);
lcd.print(menuItems[topItemDisplayed + i]);
}
}
break; // MOVELIST break
}
if (timeoutTime<millis()){ // user hasn't done anything in awhile
stillSelecting = false; // tell loop to bail out.
defaultScreen();
/*
in my main code, I had a function that
displayed a default screen on the LCD, so
I would put that function here, and it would
bail out to the default screen.
defaultScreen();
*/
}
}
}
while (stillSelecting == true); //
Serial.println("EXIT");
}
void defaultScreen(){
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Ready");
lcd.setCursor(7,1);
lcd.print("OK > menu");
}
int read_buttons(){ // you may need to swap "void" with "int" or "byte"
byte returndata = 0;
int buttonState;
// *** REMEMBER to declare buttonUp, buttonDown, buttonSelect, & buttonCancel pins
if ((lastButtonPressed + debounceTime) < millis()){ // see if it's time to check the buttons again
// read Up button
buttonState = digitalRead(UP);
if (buttonState == LOW){
returndata = returndata + 1;
lastButtonPressed = millis();
}
// read Down button
buttonState = digitalRead(DOWN);
if (buttonState == LOW){
returndata = returndata + 2;
lastButtonPressed = millis();
}
// read Select button
buttonState = digitalRead(OK_BTN);
if (buttonState == LOW){
returndata = returndata + 4;
lastButtonPressed = millis();
}
// read Cancel button
// buttonState = digitalRead(buttonCancel);
// if (buttonState == LOW){
// returndata = returndata + 8;
// lastButtonPressed = millis();
// }
}
return returndata; // this spits back to the function that calls it the variable returndata.
}
Comments
Please log in or sign up to comment.