Scalextric Lap Counter

1. Introduction

After getting some Micro Scalextric for Christmas and shortly after acquiring a vast mount of second-hand track from eBay I decided I needed a lap counter so I could race against the clock with opponents. I didn’t just want to see who could do 10 laps first – I wanted to time the laps to a reasonable degree of accuracy to see who was fastest. Building something to do that shouldn’t be too difficult I thought, and now here is my project page for my Scalextric Lap Counter.

The lap counter is based around a PIC Microprocessor, a couple of IR LEDs and IR Photodiodes, and an LCD character display. The basic theory of operation is that the PIC will be running a timer and each time the IR light beam is broken by a car crossing the finish line the lap time will be recorded and displayed in the LCD.

2. Circuit

The circuit for the lap counter is actually fairly simple. There are 2 IR LEDs (one for each track) that are constantly powered and 2 IR Photodiodes to detect the light. When the IR light is detected (beam not broken) the voltage going to the input pin on the PIC is 5v. When the beam is broken the voltage drops to 0v (or thereabouts). I’ve also added a couple of push buttons to let the user reset the individual lap times and counter for each track. The last bit of the circuit is the LCD display which uses up all the spare pins on the PIC!

Scalextric lap counter circuit diagram

Circuit diagram of the Scalextric lap counter (click to enlarge)

For clarity, I have coloured the connections going to the PIC:

  • BLUE – LCD command lines
  • GREEN – LCD data lines
  • RED – IR Photodiodes
  • PINK – Push buttons

The table below shows the parts list for the Scalextric Lap Counter:

Table 1: Parts list
Description Supplier Order Code Price Quantity Total Price
Total Price £21.12
Side Looking IR TX Maplin CH10 £0.44 2 £0.88
Side Looking IR RX Maplin CH11 £0.44 2 £0.88
16×2 White on Blue LCD (Batron BTHQ21605VSS-SMN-LED WHITE) Farnell 5004998 £16.00 1 £16.00
PIC16F690 Farnell 1103406 £2.08 1 £2.08
20MHz Crystal Farnell 9712879 £0.74 1 £0.74
15pF Ceramic Capacitor Farnell 9411666 £0.03 2 £0.06
100R 0.75w Metal-Film Resistor Farnell 9497340 £0.06 2 £0.12
220R 0.75w Metal-Film Resistor Farnell 9497552 £0.06 1 £0.06
4K7 0.75w Metal-Film Resistor Farnell 9497781 £0.06 1 £0.06
15K 0.75w Metal-Film Resistor Farnell 9497439 £0.06 4 £0.24

Below is a photo of the Scalextric Lap Counter built and fully working on prototype board.

Scalextric Lap Counter - Breadboard

In the next few sections I’ll describe how the LCD works nad the code running on the PIC Microprocessor.

3. LCD Display

The LCD display I chose to use is a 16×2 character display. These displays have a number of control lines and eight data lines in order to display text on the screen.

Scalextric Lap Counter LCD 1Scalextric Lap Counter LCD 2

From what I can tell it looks like these LCD character displays all have a pretty similar interface:

  • Control Lines
    • Register Select – “High” when sending text, “Low” when sending instructions
    • Read/Write Signal – “High” when reading data from the display, “Low” when writing data to the display
    • Enable – Clocks data and instructions into the display (done on high->low falling edge)
  • Data Lines
    • DB0 – Least significant bit
    • DB1
    • DB2
    • DB3
    • DB4
    • DB5
    • DB6
    • DB7 – Most significant bit

Once the display is initialised sending text to the the screen is pretty straight forward. You simply set the R/S line “high” as you’re sending text, set the R/W line “low” as you’re writing to the display, and set the Enable line “high”. You then send the ASCII code for the character you want to display by setting the 8 data lines accordingly (e.g. to send “A” the ASCII code is 65 which in binary is 1000001), and finally set the Enable line “low” to clock the data into the display. The display should then show the character you just sent and advance the cursor along ready for you to send the next character. Easy!

You’ll notice that I said “once the display is initialised” just now. This is the bit that caught be out for quite a while. I was sure I had connected the LCD screen up properly and I was sure I was sending text correctly but nothing was being displayed. After some extensive googling I found out that when the screen is powered up the display and cursor are turned off. Therefore you need send an instruction to the LCD display to get it to turn the display on and set the cursor to be visble (or not if you wish). After more Googling I came across an instruction set that seems to be pretty standard across these LCD character displays:

Table 2: LCD instruction set
Command Data
  DB7 DB6 DB5 DB4 DB3 DB2 DB1 DB0
Clear Display 0 0 0 0 0 0 0 1
Entry Mode Set 0 0 0 0 0 1 I/D S
Display Control 0 0 0 0 1 D C B
Cursor/Display Shift 0 0 0 1 S/C R/L x x
Function Set 0 0 1 DL N F x x
Table 3: LCD instruction set
Bit Name Meaning
I/D 0 = Decrement cursor position 1 = Increment cursor position
S 0 = No display shift 1 = Display shift
D 0 = Display off 1 = Display on
C 0 = Cursor off 1 = Cursor on
B 0 = Cursor blink off 1 = Cursor blink on
S/C 0 = Move cursor 1 = Shift display
R/L 0 = Shift left 1 = Shift right
DL 0 = 4-bit interface 1 = 8-bit interface
N 0 = 1/8 or 1/11 Duty (1 line) 1 = 1/16 Duty (2 lines)
F 0 = 5×7 Dots 1 = 5×10 Dots

Once I’d figured out how to initialise the display, move the cursor around and display text, it was time to write an LCD library for the PIC Microprocessor that I could use in my Lap Counter code as well as future projects.

Now I’m sure most hobbyists who have used PIC Microprocessors will have done so using assembly language, and although this project is simple enough that I could have used assembly language to write the program code I chose to use a free C compiler because it’s just that much easier and quicker to write the code, plus it’s easier to debug.

The C code for my LCD library is shown below. This basically consists of an init method to initialise the LCD screen, and some methods to move the cursor, clear the screen, and send text wither by sending a string or individual characters at a time. Notice that when I am toggling the LCD control lines I have used a FOR loop to insert a delay between the instructions. This is because the LCD screen is relatively slow compared to the speed of the PIC and if I simply toggled all the data and control lines with out any delays the LCD screen wouldn’t be able to keep up. The minimum time delays should be given in a timing diagram in the datasheet. Because I was lazy and wanted to get something going as soon as possible I haven’t bothered to work out the exact delay loops required for each command, so instead I’m doing using a generic FOR loop. Apparently the LCD display has a READY flag which you can read from the display so the screen can tell you when it’s ready for more data after sending it instructions such as clear screen, etc. Again, I haven’t bothered to do this, instead opting for a more simple FOR loop to generate a delay.

#define LCD_RS          RA0             // register select (1=data, 0=instruction)
#define LCD_RW          RA1             // read/write select (1=read, 0=write)
#define LCD_ENABLE      RA2             // Start signal for read/write

#define LCD_DATA        PORTC

#define LCD_DELAY       250
int lcdDelay=0;

void LCDinit(void) {
	LCDdelayLong();
	LCDputCmd(0b00111000);          // configure display for 8-bit interface and 2 lines
	LCDdelay();
	LCDputCmd(0b00000001);          // clear the screen
	LCDdelay();
	LCDputCmd(0b00001100);          // turn display on and disable cursor blink
	LCDdelay();
}

void LCDputCmd(unsigned char cmd) {
	LCD_RS = 0;
	LCD_RW = 0;
	LCD_ENABLE = 1;
	LCDdelay();
	LCD_DATA = cmd;
	LCDdelay();
	LCD_ENABLE = 0;
	LCDdelay();
}

void LCDputChar(unsigned char ch) {
	LCD_RS = 1;
	LCD_RW = 0;
	LCD_ENABLE = 1;
	LCDdelay();
	LCD_DATA = ch;
	LCDdelay();
	LCD_ENABLE = 0;
	LCDdelay();
}

void LCDclear(void) {
	LCDputCmd(0b00000001);
	for (lcdDelay=0; lcdDelay}

void LCDsetCursor(unsigned char row, unsigned char col) {
	if (row==1) {
		col += 0x40;
	}
	LCDputCmd((0b1000 << 4) + col);
	for (lcdDelay=0; lcdDelay}

void LCDputLine(unsigned char line, unsigned char *str, unsigned char len) {
	LCDsetCursor(line,0);
	while(*str) {
		LCDputChar(*str++);
	}
}

void LCDdelay(void) {
	for (lcdDelay=0; lcdDelay}

void LCDdelayLong(void) {
	for (lcdDelay=0; lcdDelay}

Listing 1: LCD Library

4. PIC Microprocessor

A PIC is basically a small specialised computer that has a variety of connectivity to the outside world (IO Pins, ADCs, PWM, etc). Computer programs can then be written to run on the PIC to make it do whatever we want. The PIC is where the magic takes place in the Scalextric Lap Counter – it is responsible for running a timer to time the laps, detecting the cars breaking the IR beam so we know when a car has completed a lap, and it also talks to the LCD display so we can see our lap times.

PICs come in all shapes and sizes and with all kinds of neat features such as ADCs, PWM controllers, UARTs, etc. For the lap counter project all that is really needed is a PIC with lots IO pins to communicate with the LCD screen, and a timer. I ended up using the PIC16F690 as it has just the right number of pins (18 GPIO pins in total) and is pretty cheap. The PIC16F690 also has lots of neat features, none of which are actually needed for the lap counter:

PIC16F690

The PIC16F690

Table 4: PIC16F690 Specifications
CPU 20 MHz
Flash 8 kbytes, 256 EEPROM
RAM 256 bytes
Clock 8-MHz on-chip oscillator
Timers low power with gate control;four-channel PWM
Analog 12-channel, 10-bit ADC;dual comparators with set-reset latch
Serial SPP with I2C and SPI support; serial port with LIN support
GPIO 18
Power 9 µA at 32 kHz to 2.4 mA at 20 MHz
Debug JTAG/OnCE
Package 20-pin PDIP, SOIC, SSOP, and 4- by 4-mm QFN

The basic operation of the code running on the PIC is this: First all the IO pins on the PIC are initialised after which the LCD is initialised. A ‘splash’ screen is then shown for a couple of seconds. After that the internal timer is started and interrupts are enabled for the timer overflowing and for the state of the lap counter pins changing. The main section of the program then sits in a loop updating the screen with the lap counts and times. With the PIC running at 20MHz and the TMR0 prescaler set to 1:256, the timer overflows causing an interrupt every 13.1072 milliseconds. In the interrupt handler the timer variables for each track are incremented every time the timer overflows. The individual track timer variables are 32-bit unsigned longs meaning the the maximum lap time the code can cater for is about 15 minutes which should be long enough for most cases. The interrupt handler is also run if either of the IR light beams are broken. In here we check which track it was that caused the interrupt, increment that tracks lap counter and store the lap time. We then reset the individual track timer variable to 0 and set a flag to ignore further interrupts for that tracks light sensor for the next 1.5 seconds to prevent “bouncing” of the sensor (yes, it does occur even with a light break beam).

#include
#include "Lcd.h"

/**
 * TODO:
 *   Start timer when car goes through gantry for the first time (i.e. lapcounter = 0)
 *
 * BUGS:
 *   Doesnt always record fastest lap even though the time shown briefly is faster than the current best
 *   Also, occasionally slower lap time are taken as being the fastest even when they are slower than the current best
 *
 * @author uk_dave
 * @version 1.0, Sunday 12th March 2006
 */

#define processor = 16F690
__CONFIG(0x3fd2);

#define Track1Sensor       RA0
#define Track2Sensor       RA1

#define CURRENT_TIME       1
#define BEST_TIME          2
unsigned char displayMode = BEST_TIME;

unsigned char Track1Triggered = 0;          // Whether track 1 has been triggered (used for debounce)
unsigned char Track1Counter = 0;            // Number of laps for track 1
unsigned int  Track1BestLapTime = 65535;    //
unsigned int  Track1LastLapTime = 0;        //
unsigned int  Track1TMR0intr = 0;           // Value of TMR0 for track 1

unsigned char Track2Triggered = 0;          // Whether track 2 has been triggered (used for debounce)
unsigned char Track2Counter = 0;            // Number of laps for track 2
unsigned int  Track2BestLapTime = 65535;    //
unsigned int  Track2LastLapTime = 0;        //
unsigned int  Track2TMR0intr = 0;           // Value of TMR0 for track 2

unsigned char debounceCounter = 0;          // counter used in the TMR0 interrupt for debouncing
bit redraw = 0;                             // Flag to determine whether the LCD screen needs updating
unsigned char dump = 0;                     // PORTB is dumped here in the ISR

void init(void) {
	IOCA = 0b00000000;      // Disable interrupt-on-change for Port A
	IOCB = 0b01010000;      // Enable interrupt-on-change for RB4 and RB6
	INTCON = 0b00101000;    // Enable TMR0 interrupts and IOC interrupts ************* (GIE will be enabled later!!) **************
	OPTION = 0b10000111;    // Assign prescaler to TMR0 and set prescaler to 1:256 which will cause an interrupt every 13.1072 milliseconds

	TRISA = 0b00000000;     // Set Port A all output
	TRISB = 0b01010000;     // Set Port B all output except for RB4 and RB6 (track sensors)
	TRISC = 0b00000000;     // Set Port C all output

	ANSEL = 0b00000000;     // Disable ADC so we can use pins as regular IO
	ANSELH = 0b00000000;    // Disable ADC so we can use pins as regular IO
	CM1CON0 = 0b00000000;   // Disable comparator 1
	CM2CON0 = 0b00000000;   // Disable comparator 2
	SSPCON = 0b00010000;    // Disable UART

	PORTA = 0b00000000;     // Clear Port A
	PORTB = 0b00000000;     // Clear Port B
	PORTC = 0b00000000;     // Clear Port C
}

void printTime(unsigned int t) {
	unsigned long time = t * 13L;
	unsigned char time_ms = (time % 1000);
	unsigned char time_sec = ((time/1000) % 60);
	unsigned char time_min = ((time/60000) % 60);
	LCDputChar(((time_min / 10) % 10) + 48);
	LCDputChar((time_min % 10) + 48);
	LCDputChar(':');
	LCDputChar(((time_sec / 10) % 10) + 48));
	LCDputChar((time_sec % 10) + 48);
	LCDputChar('.');
	LCDputChar(((time_ms / 100) % 10) + 48);
	LCDputChar(((time_ms % 10) % 10) + 48);
}

void main() {
	unsigned char i = 0;
	unsigned char j = 0;
	unsigned char k = 0;

	init();

	LCDsetup();

	LCDputLine(0, "   Scalextric   ", 16);
	LCDputLine(1, "  Lap  Counter  ", 16);
	for (i=0; i	LCDclear();
	redraw=1;
	Track1TMR0intr=0;
	Track2TMR0intr=0;
	GIE=1;

	while(1) {
		if (redraw == 1) {
			redraw = 1;

			LCDsetCursor(0, 0);
			LCDputChar('T');
			LCDputChar('1');
			LCDputChar(':');
			LCDputChar(' ');
			LCDputChar(((Track1Counter / 10) % 10) + 48);
			LCDputChar((Track1Counter % 10) + 48);
			LCDputChar(' ');
			LCDputChar(' ');
			if (Track1Triggered==1) {
				printTime(Track1LastLapTime);
			} else {
				if (displayMode == CURRENT_TIME) {
					printTime(Track1TMR0intr);
				} else {
					printTime(Track1BestLapTime);
				}
			}

			LCDsetCursor(1, 0);
			LCDputChar('T');
			LCDputChar('2');
			LCDputChar(':');
			LCDputChar(' ');
			LCDputChar(((Track2Counter / 10) % 10) + 48);
			LCDputChar((Track2Counter % 10) + 48);
			LCDputChar(' ');
			LCDputChar(' ');
			if (Track2Triggered==1) {
				printTime(Track2LastLapTime);
			} else {
				if (displayMode == CURRENT_TIME) {
					printTime(Track2TMR0intr);
				} else {
					printTime(Track2BestLapTime);
				}
			}
		}
	}
}

static void interrupt isr(void) {
	dump = PORTB;

	if (RABIF == 1) {
		if ((dump & 0b00010000)==0b00010000 && Track1Triggered==0) {
			debounceCounter = 0;
			Track1Triggered = 1;
			Track1Counter++;
			Track1LastLapTime = Track1TMR0intr;
			if (Track1LastLapTime < Track1BestLapTime) {
				Track1BestLapTime = Track1LastLapTime;
			}
			Track1TMR0intr = 0;
			redraw = 1;
			RB7=1;
		}
		if ((dump & 0b01000000)==0b01000000 && Track2Triggered==0) {
			debounceCounter = 0;
			Track2Triggered = 1;
			Track2Counter++;
			Track2LastLapTime = Track2TMR0intr;
			if (Track2LastLapTime < Track2BestLapTime) {
				Track2BestLapTime = Track2LastLapTime;
			}
			Track2TMR0intr = 0;
			redraw = 1;
			RB7=1;
		}
		RABIF = 0;
	}

	if (T0IF == 1) {
		if (debounceCounter==100) {
			Track1Triggered = 0;
			Track2Triggered = 0;
			debounceCounter = 0;
			RB7=0;
		} else {
			debounceCounter++;
		}
		Track1TMR0intr++;
		Track2TMR0intr++;
		T0IF = 0;
	}
}

Listing 2: Lap Counter Code

5. Track

The images below show a protoype gantry made from a piece of cardboard used to hold the IR detectors above the track. Two small holes had to be drilled in the slot of track for each of the IR LEDs to shine through, and then the IR Photodides are positioned directly above on the cardboard gantry.

Scalextric Lap Counter Gantry 1Scalextric Lap Counter Gantry 2