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!
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:
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.
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.
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:
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 |
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:

The PIC16F690
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.