Tidtagningsmodul till luftgevärsskytte
Postat: 22 maj 2017, 16:23:01
Efter åratal av inaktivitet och paus i många projekt har jag nyligen genomfört ett lite mindre projekt som precis avslutats. Tänkte jag kunde dela det här och få lite idéer på vidareutveckling eller andra tips inför framtiden.
Modulen är till för att kunna mäta tiden för att skjuta ner fällmål typ skidskytte. 5 st mindre mål ska träffas och när alla har fällts stoppas tiden. I skytteföreningen brukar skyttekvällar avslutas med att tävla om bästa tid samtidigt som Survivors låt "Eye of the tiger" spelas för att öka stressnivån lite.
Fällmålen återställs med att dra i ett snöre, då åker en båge upp och fäller tillbaka målen. Sensorerna (tungelement) monterades på bågen och magneter limmades på varje enskilt litet mål. Så när alla tungelementen leder så kortsluts signalen till jord.
Använde en Arduino nano, en extern oscillator som bättre tidsreferens, en vanlig 16x2 LCD jag hade liggandes och lite fina knappar från farnell. Knapparnas inbyggda LED signalerar att modulen förväntar sig att man ska trycka på den specifika knappen, de två knappar som är märkta "Skytt X" är parallellkopplade med fällmålens sensorer.
"Företagsloggan" Sten hård vara med fyrkantvågen med ringningar är en ordlek då mitt smeknamn är Sten. Märker mina projekt med den för skoj skull och företaget finns inte (ännu i alla fall).
Oscillatorn har tyvärr inte riktigt tillräckligt hög noggrannhet för det som behövs (endast 100 ppm) för en upplösning på hundradelar (maxtiden är 10 minuter, 9:59.99). Helt enkelt för att skärmen inte har plats för fler tecken och mer behövs oftast inte. Trodde i min enfald att det fanns 5V, 4 MHz oscillatorer med ca 10-20 ppm i samma kapsel ( dip-8 ), men inte hittat nån...
Har använt PIC tidigare men tänkte det skulle bli lätt att bara slänga ihop något med en Arduino, det kom tillbaka och bet mig rätt ordentligt. I Arduino nano är tydligen inte BOR-spänningen anpassad för 5V matning utan 3,3V. Det betydde att ibland när man slog till strömmen så hamnade spänningen (under kort tid) under gränsen för definierat beteende hos processorn men inte under BOR-spänningen så allt hängde sig bara. Efter mycket om och men lyckades jag programmera om bootloadern och då skriva om Fuse-bitsen till en BOR-spänning på 4,3V.
Hårdvaran (och mjukvaran till viss del) är förberedd för att kunna mata ut tiden till en stor 7-seg LED display. En där varje siffra får ett 8-bit seriell till parallell shiftregister typ 74HC595. Därav att SPI hårdvaran i AVR:en skickar ut en del data, displayen är dock inte byggd ännu.
Lådan gjordes i PLA med en 3d-printer, frontpanelen gjordes i svart plexi med en laserskärare. Panelens text och logga "graverades" med låg effekt på lasern samtidigt som den skars ut med hög effekt. Sen m.h.a. vit akrylfärg och en pappersservett geggades det "graverade" igen med färgen så att det blev vitt och tydligt. Resultat blev inte perfekt med det blev definitivt tillräckligt snyggt.
Anledningen till att jag använde D-Sub 9 kontakter var att det fanns tillgängligt gratis för mig. Alla kablar internt i lådan är kontakterade med stiftlister så att det är möjligt att serva enkelt när något går sönder.
Modulen är till för att kunna mäta tiden för att skjuta ner fällmål typ skidskytte. 5 st mindre mål ska träffas och när alla har fällts stoppas tiden. I skytteföreningen brukar skyttekvällar avslutas med att tävla om bästa tid samtidigt som Survivors låt "Eye of the tiger" spelas för att öka stressnivån lite.
Fällmålen återställs med att dra i ett snöre, då åker en båge upp och fäller tillbaka målen. Sensorerna (tungelement) monterades på bågen och magneter limmades på varje enskilt litet mål. Så när alla tungelementen leder så kortsluts signalen till jord.
Använde en Arduino nano, en extern oscillator som bättre tidsreferens, en vanlig 16x2 LCD jag hade liggandes och lite fina knappar från farnell. Knapparnas inbyggda LED signalerar att modulen förväntar sig att man ska trycka på den specifika knappen, de två knappar som är märkta "Skytt X" är parallellkopplade med fällmålens sensorer.
"Företagsloggan" Sten hård vara med fyrkantvågen med ringningar är en ordlek då mitt smeknamn är Sten. Märker mina projekt med den för skoj skull och företaget finns inte (ännu i alla fall).
Oscillatorn har tyvärr inte riktigt tillräckligt hög noggrannhet för det som behövs (endast 100 ppm) för en upplösning på hundradelar (maxtiden är 10 minuter, 9:59.99). Helt enkelt för att skärmen inte har plats för fler tecken och mer behövs oftast inte. Trodde i min enfald att det fanns 5V, 4 MHz oscillatorer med ca 10-20 ppm i samma kapsel ( dip-8 ), men inte hittat nån...
Har använt PIC tidigare men tänkte det skulle bli lätt att bara slänga ihop något med en Arduino, det kom tillbaka och bet mig rätt ordentligt. I Arduino nano är tydligen inte BOR-spänningen anpassad för 5V matning utan 3,3V. Det betydde att ibland när man slog till strömmen så hamnade spänningen (under kort tid) under gränsen för definierat beteende hos processorn men inte under BOR-spänningen så allt hängde sig bara. Efter mycket om och men lyckades jag programmera om bootloadern och då skriva om Fuse-bitsen till en BOR-spänning på 4,3V.
Hårdvaran (och mjukvaran till viss del) är förberedd för att kunna mata ut tiden till en stor 7-seg LED display. En där varje siffra får ett 8-bit seriell till parallell shiftregister typ 74HC595. Därav att SPI hårdvaran i AVR:en skickar ut en del data, displayen är dock inte byggd ännu.
Lådan gjordes i PLA med en 3d-printer, frontpanelen gjordes i svart plexi med en laserskärare. Panelens text och logga "graverades" med låg effekt på lasern samtidigt som den skars ut med hög effekt. Sen m.h.a. vit akrylfärg och en pappersservett geggades det "graverade" igen med färgen så att det blev vitt och tydligt. Resultat blev inte perfekt med det blev definitivt tillräckligt snyggt.
Anledningen till att jag använde D-Sub 9 kontakter var att det fanns tillgängligt gratis för mig. Alla kablar internt i lådan är kontakterade med stiftlister så att det är möjligt att serva enkelt när något går sönder.
Kod: Markera allt
// Tidtagare för "eye of the tiger" hos Chalmers skytteförening
// 2 fällmål
// Skrivet av: Sven "Sten" Åkersten
// PORTx set output value here if conf as output
// DDRx configure input/output, 0 = input, 1 = output
// PINx port input register, read here if conf as input
#include <avr/io.h>
#include <avr/interrupt.h>
#include <LiquidCrystal.h>
#include <avr/wdt.h>
#define STATE_INIT 0b00000000
#define STATE_CLEAR 0b00000111
#define STATE_WAIT_TO_START 0b00000001
#define STATE_RUNNING_BOTH 0b00000110
#define STATE_RUNNING_PL1 0b00000010
#define STATE_RUNNING_PL2 0b00000100
#define MAIN_BUTTON (0b00000001)
#define PLAYER_1_BUTTON (0b00000010)
#define PLAYER_2_BUTTON (0b00000100)
#define MAIN_LED (0b00001000)
#define PLAYER_1_LED (0b00010000)
#define PLAYER_2_LED (0b00100000)
#define BUTTON_PORT PINC
#define LED_PORT PORTC
#define MAXIMUM_TIME (300000)
#define DEBOUNCE_TIME 150 //testat till 150
#define EXT_OSC 0b00100000
#define SHIFT_REGISTER DDRB
#define LATCH_REGISTER DDRD
#define SHIFT_PORT PORTB
#define LATCH_PORT PORTD
#define DATA (1<<PB3) //MOSI (SI)
#define LATCH (1<<PD4) //
#define CLOCK (1<<PB5) //SCK (SCK)
LiquidCrystal lcd(2,3,7,8,9,10);
volatile unsigned long int timer_tick_vola = 0; //timer value from interrupt
volatile unsigned long int last_tick_1_vola = 0; //time at last player 1 trigged from interrupt
volatile unsigned long int last_tick_2_vola = 0; //time at last player 2 trigged from interrupt
volatile unsigned short int holdoff = MAIN_BUTTON; // holdoff for buttons for interrupt
unsigned short int system_state = STATE_INIT; // current system state
unsigned short int debounce_timer_holdoff = 0b00000000; // holdoff for statemachine, timer or internal state
unsigned long int actual_tick_1 = MAXIMUM_TIME; //last player 1 time, non volatile
unsigned long int actual_tick_2 = MAXIMUM_TIME; //last player 2 time, non volatile
unsigned long int time_to_display_1 = MAXIMUM_TIME; //time to display player 1 time, non volatile
unsigned long int time_to_display_2 = MAXIMUM_TIME; //time to display player 2, non volatile
unsigned long int start_tick_1 = 0; //start time for player 1, non volatile
unsigned long int start_tick_2 = 0; //start time for player 2, non volatile
unsigned long int timer_tick = 0; //timer value copy to use in state machine, non volatile
unsigned long int last_tick_1 = 0; //last time player 1 copy to use in state machine, non volatile
unsigned long int last_tick_2 = 0; //last time player 2 copy to use in state machine, non volatile
void setup()
{
delay(1000);
// initialize pinsz
DDRC = ~(PLAYER_1_BUTTON | PLAYER_2_BUTTON | MAIN_BUTTON); // inputs
DDRC |= (PLAYER_1_LED | PLAYER_2_LED | MAIN_LED); // output
PORTC = (PLAYER_1_BUTTON | PLAYER_2_BUTTON | MAIN_BUTTON); // all inputs with internal pullup
DDRB = 0b11111111; // all output,
//initialize SPI module and pins
SHIFT_REGISTER |= (DATA | CLOCK); //Set control pins as outputs
LATCH_REGISTER = (LATCH | 0b11111111) & ~EXT_OSC ;
SHIFT_PORT &= ~(DATA | CLOCK); //Set control pins low
SPCR = (1<<SPE) | (1<<MSTR); //Start SPI as Master
//Pull LATCH low (Important: this is necessary to start the SPI transfer!)
LATCH_PORT &= ~LATCH;
// initialize Timer1
cli(); // disable global interrupts
TCCR1A = 0; // set entire TCCR1A register to 0
DDRD = 0b11011111; // T1 as input
TCCR1B = 0b00001111; // turn on CTC mode and set external clock
//TCCR1B = 0b00001001; // turn on CTC mode and set prescaler as 1
// set compare match register to desired timer count:
OCR1A = 7999; // calculated to 7999 for 2 ms
//OCR1A = 31999; // calculated to 31999 for 2 ms
// enable timer compare interrupt:
TIMSK1 |= (1 << OCIE1A);
sei(); // enable global interrupts
wdt_enable(WDTO_4S);
lcd.begin(16,2);
lcd.print("Skytt 1 Skytt 2");
// Start serial com.
Serial.begin(9600);
}
void loop() {
// statemachine that runs over and over again
wdt_reset();
if (system_state == STATE_INIT) // inital state
{
//print_time_serial(1,actual_tick_1,actual_tick_2); // display last time
time_to_display_1 = actual_tick_1;
time_to_display_2 = actual_tick_2;
LED_PORT |= (PLAYER_1_LED | PLAYER_2_LED); // turn on pla 1 and 2 led, to wait for them to be pressed
LED_PORT &= ~MAIN_LED; // turn off main button led
if(holdoff & PLAYER_1_BUTTON) // if player 1 is pressed
{
debounce_timer_holdoff |= PLAYER_1_BUTTON;
// clear pla 1 led
LED_PORT &= ~PLAYER_1_LED;
}
if(holdoff & PLAYER_2_BUTTON) // if player 2 is pressed
{
debounce_timer_holdoff |= PLAYER_2_BUTTON;
// clear pla 1 led
LED_PORT &= ~PLAYER_2_LED;
}
if( (debounce_timer_holdoff & PLAYER_1_BUTTON) && (debounce_timer_holdoff & PLAYER_2_BUTTON) ) // if both player have been pressed, go to next state
{
debounce_timer_holdoff &= ~(PLAYER_1_BUTTON | PLAYER_2_BUTTON);
system_state = STATE_CLEAR;
}
}
if(system_state == STATE_CLEAR)
{
//print_time_serial(2,actual_tick_1,actual_tick_2); // display last time
time_to_display_1 = actual_tick_1;
time_to_display_2 = actual_tick_2;
holdoff &= ~(PLAYER_1_BUTTON | PLAYER_2_BUTTON | MAIN_BUTTON); // clear all buttons
delay(50); // wait to see if any button is depressed so it will set it's corresponding holdoff
system_state = STATE_WAIT_TO_START; //change state
debounce_timer_holdoff |= MAIN_BUTTON; //set main button state machine holdoff
}
if (system_state == STATE_WAIT_TO_START)
{
if( !( holdoff & (PLAYER_1_BUTTON | PLAYER_2_BUTTON) ) ) // if any player button has been pressed since cleared in last state, return to that state. Makes sure player buttons is low (targets reset)
{
if(holdoff & MAIN_BUTTON & ~debounce_timer_holdoff) // if statemachine holdoff is cleared and main button been pressed, start time and change state. If main button is clear, clear main button holdoff to enable start else go back to clearing state
{
LED_PORT &= ~MAIN_LED; //clear main button led
cli();
start_tick_1 = last_tick_1_vola;
start_tick_2 = last_tick_2_vola;
sei();
system_state = STATE_RUNNING_BOTH;
}
else if (~holdoff & MAIN_BUTTON)
{
time_to_display_1 = 0;
time_to_display_2 = 0;
//print_time_serial(3,0,0); // print zeros, system ready to start
LED_PORT |= MAIN_LED; //turn on main button led
debounce_timer_holdoff &= ~MAIN_BUTTON;
}
else
{
//print_time_serial(3,actual_tick_1,actual_tick_2); // display last measured time again, system NOT ready to start
time_to_display_1 = actual_tick_1;
time_to_display_2 = actual_tick_2;
LED_PORT &= ~MAIN_LED; //clear main button led
system_state = STATE_CLEAR;
}
}
else
{
LED_PORT &= ~MAIN_LED; //clear main button led
system_state = STATE_CLEAR;
}
}
cli();
timer_tick = timer_tick_vola; // copy current timer tick from interrupt
sei();
if(system_state == STATE_RUNNING_PL1 || system_state == STATE_RUNNING_BOTH ) // if player time is running, recalculate time and display.
{
LED_PORT &= ~MAIN_LED; //clear main button led, turn on pl 1
LED_PORT |= PLAYER_1_LED;
if( (actual_tick_1 = (timer_tick - start_tick_1)) < DEBOUNCE_TIME) // if time since start less then minimum time, set mimimum time
{
actual_tick_1 = 0;
}
else // if time since start greater then minimum time
{
time_to_display_1 = actual_tick_1 - DEBOUNCE_TIME;
//print_time_serial(41,actual_tick_1,actual_tick_2); //display time pla 1
}
if(holdoff & PLAYER_1_BUTTON) // if player 1 holdoff
{
cli();
last_tick_1 = last_tick_1_vola;
sei();
if( timer_tick - last_tick_1 > DEBOUNCE_TIME ) // Wait until debounce time have passed
{
if( ~BUTTON_PORT & PLAYER_1_BUTTON) // if button is still pressed, Calculate finish time and clear running state of that player, when both player been clear state return to init
{
LED_PORT &= ~PLAYER_1_LED; //clear pla 1 led
debounce_timer_holdoff |= MAIN_BUTTON;
time_to_display_1 = actual_tick_1 = last_tick_1 - start_tick_1;
system_state &= ~STATE_RUNNING_PL1;
}
else // else it was a button bounce, clear holdoff and do nothing
{
holdoff &= ~PLAYER_1_BUTTON;
}
}
}
else if(actual_tick_1 > MAXIMUM_TIME) // if time since start greater then maximum time and button not been pressed, terminate
{
LED_PORT &= ~PLAYER_1_LED; //clear pla 1 led
debounce_timer_holdoff |= MAIN_BUTTON;
time_to_display_1 = actual_tick_1 = MAXIMUM_TIME;
system_state &= ~STATE_RUNNING_PL1;
}
}
if(system_state == STATE_RUNNING_PL2 || system_state == STATE_RUNNING_BOTH ) // if player time is running, recalculate time and display.
{
LED_PORT &= ~MAIN_LED; //clear main button led, turn on pl 2
LED_PORT |= PLAYER_2_LED;
if( (actual_tick_2 = (timer_tick - start_tick_2)) < DEBOUNCE_TIME) // if time since start less then minimum time, set mimimum time
{
time_to_display_2 = 0;
}
else // if time since start greater then minimum time
{
//print_time_serial(42,actual_tick_1,actual_tick_2); //display time pl 2
time_to_display_2 = actual_tick_2 - DEBOUNCE_TIME;
}
if(holdoff & PLAYER_2_BUTTON) // if player 2 holdoff
{
cli();
last_tick_2 = last_tick_2_vola;
sei();
if( timer_tick - last_tick_2 > DEBOUNCE_TIME ) // Wait until debounce time have passed
{
if( ~BUTTON_PORT & PLAYER_2_BUTTON) // if button is still pressed, Calculate finish time and clear running state of that player, when both player been clear state return to init
{
LED_PORT &= ~PLAYER_2_LED; //clear pla 2 led
debounce_timer_holdoff |= MAIN_BUTTON;
time_to_display_2 = actual_tick_2 = last_tick_2 - start_tick_2;
system_state &= ~STATE_RUNNING_PL2;
}
else // else it was a button bounce, clear holdoff and do nothing
{
holdoff &= ~PLAYER_2_BUTTON;
}
}
}
else if(actual_tick_2 > MAXIMUM_TIME) // if time since start greater then maximum time and button not been pressed, terminate
{
LED_PORT &= ~PLAYER_2_LED; //clear pla 2 led
debounce_timer_holdoff |= MAIN_BUTTON;
time_to_display_2 = actual_tick_2 = MAXIMUM_TIME;
system_state &= ~STATE_RUNNING_PL2;
}
}
print_time_lcd(time_to_display_1,time_to_display_2);
print_time_serial(1,time_to_display_1,time_to_display_2);
}
void output_byte_spi(unsigned short data)
{
//Shift in some data
SPDR = data;
//Wait for SPI process to finish
while(!(SPSR & (1<<SPIF))) ;
//Latch into output latch
LATCH_PORT |= LATCH;
delayMicroseconds(1);
LATCH_PORT &= ~LATCH;
delayMicroseconds(1);
}
void send_message_spi(char* data,unsigned short message_length)
{
for(int i = 0; i < message_length ; i++)
{
//Shift in some data
SPDR = data[i];
//Wait for SPI process to finish
while(!(SPSR & (1<<SPIF))) ;
}
//Latch into output latch
LATCH_PORT |= LATCH;
delayMicroseconds(1);
LATCH_PORT &= ~LATCH;
delayMicroseconds(1);
}
void print_time_lcd(unsigned long time1, unsigned long time2)
{
unsigned int t1_mm = time1/30000;
unsigned int t2_mm = time2/30000;;
unsigned int t1_ss = (time1/500)%60;
unsigned int t2_ss = (time2/500)%60;
unsigned short int t1_cc = (time1/5)%100;
unsigned short int t2_cc = (time2/5)%100;
char lcd_char_time[16] ={'-',':','-','-','.','-','-',' ',' ','-',':','-','-','.','-','-'};
if(time1 != MAXIMUM_TIME)
{
lcd_char_time[3] = t1_ss%10 + 48;
lcd_char_time[2] = (t1_ss/10)%10 + 48;
lcd_char_time[0] = t1_mm + 48;
lcd_char_time[6] = t1_cc%10 + 48;
lcd_char_time[5] = (t1_cc/10)%10 + 48;
}
if(time2 != MAXIMUM_TIME)
{
lcd_char_time[12] = t2_ss%10 + 48;
lcd_char_time[11] = (t2_ss/10)%10 + 48;
lcd_char_time[9] = t2_mm + 48;
lcd_char_time[15] = t2_cc%10 + 48;
lcd_char_time[14] = (t2_cc/10)%10 + 48;
}
lcd.setCursor(0, 1);
for(int i = 0; i < 16 ; i++)
lcd.print(lcd_char_time[i]);
send_message_spi(lcd_char_time,16);
}
void print_time_serial(unsigned short state, unsigned long time1, unsigned long time2) //debug printing of current state, volatile holdoff and the last measured time for each player
{
unsigned int t1_ss = time1/500;
unsigned int t2_ss = time2/500;
unsigned short int t1_cc = (time1/5)%100;
unsigned short int t2_cc = (time2/5)%100;
// calculate time as seconds and hundreds of seconds before sending on serial port
Serial.print("State: ");
Serial.print(state);
Serial.print(" Holdoff: ");
Serial.print(holdoff);
Serial.print(" Time 1: ");
Serial.print(t1_ss);
Serial.print(".");
if(t1_cc < 10)
{
Serial.print("0");
}
Serial.print(t1_cc);
Serial.print(" Time 1: ");
Serial.print(t2_ss);
Serial.print(".");
if(t2_cc < 10)
{
Serial.print("0");
}
Serial.println(t2_cc);
}
ISR(TIMER1_COMPA_vect) // interrupt runs once for every timer tick. If button holdoff is clear and button is pressed, store current time in volatile variable of corresponding player
{
unsigned short int new_button_state;
timer_tick_vola++; //increment main timer, one tick (2 ms) have passed
new_button_state = (~BUTTON_PORT & ~holdoff) & (MAIN_BUTTON | PLAYER_1_BUTTON | PLAYER_2_BUTTON); // if a button is pressed and holdoff is clear set high button state, buttons is active low
if(MAIN_BUTTON & new_button_state) // store both player time if start (main) button is pressed. The start time ie.
{
holdoff |= MAIN_BUTTON;
last_tick_1_vola = timer_tick_vola;
last_tick_2_vola = timer_tick_vola;
}
if(PLAYER_1_BUTTON & new_button_state) // store player time if player 1 button is pressed. The finish time for that player ie.
{
holdoff |= PLAYER_1_BUTTON;
last_tick_1_vola = timer_tick_vola;
}
if(PLAYER_2_BUTTON & new_button_state) // store player time if player 2 button is pressed. The finish time for that player ie.
{
holdoff |= PLAYER_2_BUTTON;
last_tick_2_vola = timer_tick_vola;
}
}