Model train detector/speedometer with Arduino and photo-reflectors - Arduinoとフォトリフレクタで鉄道模型列車検知・速度検出

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](http://www.scuba.net/wiki/index.php/LocoShield).

![speedometer.jpg](https://qiita-image-store.s3.amazonaws.com/0/12670/ad845a05-74f1-0bec-bab4-e5773e9ca48a.jpeg)

# 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

![detector-circuit.png](https://qiita-image-store.s3.amazonaws.com/0/12670/741b2ad8-cf81-3d9d-9ec2-d7d7a49f5a0d.png)


## BOARDS

Sensor Unit x 64
![detector-sensor-board.png](https://qiita-image-store.s3.amazonaws.com/0/12670/18f7de57-0ae7-b168-3d6f-a94daac7295a.png)

LED Driver and Sensor Amplifier Board x 2
![detector-board.png](https://qiita-image-store.s3.amazonaws.com/0/12670/7ed96ab2-41bb-9387-b80b-35cfd2b8dadd.png)

LocoNet Board x 1
![detector-loconet-board.png](https://qiita-image-store.s3.amazonaws.com/0/12670/4e8d21e6-8a11-8ede-16b5-9d6cbce60e56.png)

# SPEEDOMETER

![speedometer-design.png](https://qiita-image-store.s3.amazonaws.com/0/12670/289b58aa-a58c-940b-488e-b291247f02e0.png)

## 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.

```c
#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
}
```