Here is a design of model train detector and speedometer I'm working on.
ABSTRACT
- Microprocessor module: Arduino UNO R3
- Detecting trains by photo-reflectors, which lighten IR LED and monitor the change of current caused by IR reflection.
- 74151 Logic ICs for channel selection. Arduino 8 pins for sensor input, and 3 pins for channel selection output. 8 x 2^3 = 64 sensors supported.
- Repeated reflection test to improve the reliability.
- Speedometer capability by calculating the reflection interval.
- Transmitting the packet to the PC is with the help of LocoShield.
DETECTOR
REQUIREMENTS
- Max. Train Speed: 130km/h(actual speed) / 150(model scale) = 240mm/s
- Length of Reflection Sheet beneath Trains: 4mm
- Max. # of sensors: 64
SPECS
- Min. Reflection Sheet Passing Duration: 4mm / 240mm/s = 16ms
- IR Pulse Length: 0.5ms for each ON/OFF
- Reflection Test Repeats: 10 times. Each test increases/decreases the counter between 0 to 10 to avoid jittering.
- Detection Time: 0.5ms x 10 = 5ms < 16ms (requirements fulfilled)
DESIGN
- Starts scanning sensors 0.2ms after LED switches ON/OFF.
- Sends out LocoNet packets while waiting for the scanning time.
- Takes 0.3ms (experimented) to scan all the sensor.
- I(L)~0.40mA (experimented).
- IC=(Vc-VCEsat)/R=0.47mA, large enough for 74HC input.
- IBmin>IC/hFE=0.011mA to saturate TR.
- (I(L)-IBmin)*R>VBEsat to drive TR, hence R>2.13k
- When LED gets turned ON, TR is likely to be OFF. The larger R increases IB, which leads to TR's faster turning ON.
- When LED gets turned OFF, TR will be eventually turned OFF after a short while. To make Tr<100us, R<10k. Actual Tr is faster due to the remaining TR activity.
CIRCUIT
BOARDS
LED Driver and Sensor Amplifier Board x 2
SPEEDOMETER
REQUIREMENTS
- Max. acceleration: 10km/h/s (actual), 18.5mm/s/s (N scaled)
- Max. length of reflection sheet: 20mm
- Current thresholds of sensors vary due to the anomalies of electronic components.
- Delay of digital output after crossing the threshold: 5ms
- Occasional missed sensing allowed. Erroneous sensed values not allowed.
DESIGN
- Reflection sheet: 4mm IR-reflecting white area separated by 8mm IR-absorbing black gap. The black gap is long enough to decrease the sensor current under the threshold when the sheet passes at the maximum velocity.
- Records the latest 4 L/H switching times.
- When switching to H, velocities, V1 and V2, are estimated: V1=12mm/BW, V2=12mm/WB
- At max. acceleration from velocity 0, passing duration for 12mm, T: 18.5*T^2/2=12mm, T=1.14s
- Max. difference between V1 and V2: 18.5mm/s/s * T = 21.09mm/s
- If |V1 - V2| > 22mm/s, the estimation is considered unreliable, hence the result is ignored.
- If B > BW * ((8mm+R)/12mm) or B > WB * ((8mm+R)/12mm), it is likely B does not correspond to the 8mm gap but another black area, hence the result is ignored. Relaxing term R=3.
- Estimated velocity: (V1 + V2) / 2
CODE
Draft code.
#define LCDDEBUG 0
#define STAYON 0
#include <LocoNet.h>
#if LCDDEBUG
#include <LiquidCrystal.h>
static LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
#endif
static const byte SENSOR_PIN_COUNT = 8;
static volatile byte *const sensorDDR = &DDRD;
static volatile byte *const sensorPIN = &PIND;
static volatile byte *const sensorPORT = &PORTD;
static volatile byte *const irDDR = &DDRC;
static volatile byte *const irPORT = &PORTC;
static const byte irBit = 0;
static volatile byte *const errorDDR = &DDRC;
static volatile byte *const errorPIN = &PINC;
static volatile byte *const errorPORT = &PORTC;
static const byte errorBit = 1;
static volatile byte *const channelDDR = &DDRB;
static volatile byte *const channelPORT = &PORTB;
static const byte selA = 2;
static const byte selB = 3;
static const byte selC = 4;
static const byte ledPin = 13;
static const byte grayCode[8] = { 0, 1, 3, 2, 6, 7, 5, 4 };
static const byte grayBits[8] = { selC, selA, selB, selA, selC, selA, selB, selA };
static const byte grayState[8] = { LOW, HIGH, HIGH, LOW, HIGH, HIGH, LOW, LOW };
static const unsigned int PULSE_DURATION = 200;
static const byte TEST_TIMES = 10;
static const byte ENTER_SENT_MASK = 0x80;
static const byte PASSING_MASK = 0x40;
static const byte COUNT_MASK = 0x3F;
static const int BLINK_QUEUE_SIZE = 4;
static byte irState = LOW;
static byte correctCounts[SENSOR_PIN_COUNT * 8];
static unsigned long blinkTimes[SENSOR_PIN_COUNT * 8 * BLINK_QUEUE_SIZE] = { 0 };
static byte blinkIndexes[SENSOR_PIN_COUNT * 8] = { 0 };
static byte sensorToSend = 0;
static boolean inError = false;
void setup() {
LocoNet.init(9);
cli();
*sensorDDR = 0x00; // input
*sensorPORT = 0x00; // disables pull-up
*irDDR |= _BV(irBit); // output
*irPORT &= ~_BV(irBit); // LOW
*errorDDR &= ~_BV(errorBit); // input
*errorPORT &= ~_BV(errorBit); // disables pull-up
*channelDDR |= _BV(selA) | _BV(selB) | _BV(selC); // output
*channelPORT &= ~(_BV(selA) | _BV(selB)); // LOW
*channelPORT |= _BV(selC); // HIGH
sei();
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);
#if LCDDEBUG
lcd.begin(16, 2);
lcd.print(TEST_TIMES);
#endif
for (byte sensor = 0; sensor < SENSOR_PIN_COUNT * 8; ++sensor) {
correctCounts[sensor] = TEST_TIMES;
}
}
void loop() {
// check error
byte error = (*errorPIN) & _BV(errorBit);
if ((error != 0) && !inError) {
// turn off IR LED
irState = LOW;
cli();
*irPORT &= ~_BV(irBit);
sei();
// power off rail
lnMsg msg;
msg.data[0] = OPC_GPOFF;
LocoNet.send(&msg);
}
inError = (error != 0);
if (inError) {
return;
}
// switch IR
#if STAYON
irState = HIGH;
cli();
*irPORT |= _BV(irBit);
sei();
#else
irState ^= HIGH;
cli();
*irPORT ^= _BV(irBit);
sei();
#endif
// send LocoNet packet
unsigned long start = micros();
unsigned long elapsed = 0;
for (byte i = 0, sensor = sensorToSend; i < SENSOR_PIN_COUNT * 8; ++i, ++sensor) {
if (sensor == SENSOR_PIN_COUNT * 8) {
sensor = 0;
}
elapsed = micros() - start;
if (elapsed >= PULSE_DURATION) {
sensorToSend = sensor;
break;
}
byte count = correctCounts[sensor] & COUNT_MASK;
byte sent = correctCounts[sensor] & ENTER_SENT_MASK;
if (count == 0 && !sent) {
// send the Enter packet when repeated test are passed and the packet has not yet been sent.
int address = (int)sensor + 1;
byte sw1 = (address - 1) >> 1;
byte sw2 = (((address - 1) >> 8) & 0x0F) | OPC_SW_REP_INPUTS | (((address - 1) & 0x1) ? OPC_INPUT_REP_SW : 0);
LocoNet.send(OPC_INPUT_REP, sw1, sw2 | OPC_INPUT_REP_HI);
// do not send the Enter packet again
correctCounts[sensor] |= ENTER_SENT_MASK;
} else if (count >= TEST_TIMES && sent) {
// sent the Leave packet when the Enter packet has been sent and the tests are failed.
int address = (int)sensor + 1;
byte sw1 = (address - 1) >> 1;
byte sw2 = (((address - 1) >> 8) & 0x0F) | OPC_SW_REP_INPUTS | (((address - 1) & 0x1) ? OPC_INPUT_REP_SW : 0);
LocoNet.send(OPC_INPUT_REP, sw1, sw2 | 0);
// do not send the Leave packet again
correctCounts[sensor] &= ~ENTER_SENT_MASK;
// calculate the speed
byte index = blinkIndexes[sensor];
unsigned long *queue = blinkTimes + sensor * BLINK_QUEUE_SIZE;
unsigned long whiteblack = queue[(index + 2) % BLINK_QUEUE_SIZE] - queue[(index) % BLINK_QUEUE_SIZE];
unsigned long blackwhite = queue[(index + 3) % BLINK_QUEUE_SIZE] - queue[(index + 1) % BLINK_QUEUE_SIZE];
unsigned long black = queue[(index + 2) % BLINK_QUEUE_SIZE] - queue[(index + 1) % BLINK_QUEUE_SIZE];
unsigned int v2mmPerSec = 12UL * 1000000UL / whiteblack;
unsigned int v1mmPerSec = 12UL * 1000000UL / blackwhite;
#if LCDDEBUG
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(whiteblack / 1000UL);
// lcd.print(v2mmPerSec);
lcd.setCursor(8, 0);
lcd.print(black / 1000UL);
lcd.setCursor(0, 1);
lcd.print(blackwhite / 1000UL);
// lcd.print(v1mmPerSec);*/
#endif
if (abs((int)(v1mmPerSec - v2mmPerSec)) < 22 && black <= min(blackwhite, whiteblack) * 11UL / 12UL) {
unsigned int v = (v1mmPerSec + v2mmPerSec) / 2;
LocoNet.send(0xBE, (v >> 7) & 0x7F, v & 0x7F);
#if LCDDEBUG
lcd.setCursor(8, 1);
lcd.print(v * 150 * 3600UL / 1000000UL);
#endif
}
}
digitalWrite(ledPin, count == 0 ? HIGH : LOW);
}
// wait for pulse length
if (PULSE_DURATION > elapsed) {
delayMicroseconds(PULSE_DURATION - elapsed);
}
// test the reflection
for (byte gray = 0; gray < 8; ++gray) {
cli();
*channelPORT ^= _BV(grayBits[gray]); // switch the channel
sei();
byte channel = grayCode[gray];
byte sensorValue = *sensorPIN;
for (byte pin = 0; pin < SENSOR_PIN_COUNT; ++pin) {
byte sensor = channel + pin * 8;
byte count = correctCounts[sensor];
if (((sensorValue & _BV(pin)) != 0) != irState) {
if ((count & COUNT_MASK) > 0) {
count = (count & ~COUNT_MASK) | ((count & COUNT_MASK) - 1);
if (count == 0) {
if (!(count & PASSING_MASK)) {
count |= PASSING_MASK;
blinkTimes[sensor * BLINK_QUEUE_SIZE + blinkIndexes[sensor]] = micros();
blinkIndexes[sensor] = (blinkIndexes[sensor] + 1) % BLINK_QUEUE_SIZE;
}
}
}
} else {
if ((count & COUNT_MASK) < TEST_TIMES) {
// even when not reflected, each one of two tests is likely to be passed
count = (count & ~COUNT_MASK) | ((count & COUNT_MASK) + 2);
if (count >= TEST_TIMES) {
if (count & PASSING_MASK) {
count &= ~PASSING_MASK;
blinkTimes[sensor * BLINK_QUEUE_SIZE + blinkIndexes[sensor]] = micros();
blinkIndexes[sensor] = (blinkIndexes[sensor] + 1) % BLINK_QUEUE_SIZE;
}
}
}
}
correctCounts[sensor] = count;
}
}
}
void printFixedDigits(unsigned long x) {
#if LCDDEBUG
lcd.setCursor(0, 1);
lcd.print(x / 10000000 % 10);
lcd.print(x / 1000000 % 10);
lcd.print(x / 100000 % 10);
lcd.print(x / 10000 % 10);
lcd.print(x / 1000 % 10);
lcd.print(x / 100 % 10);
lcd.print(x / 10 % 10);
lcd.print(x / 1 % 10);
#endif
}