LoginSignup
0
0

More than 5 years have passed since last update.

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

Last updated at Posted at 2013-12-25

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.

speedometer.jpg

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

BOARDS

Sensor Unit x 64
detector-sensor-board.png

LED Driver and Sensor Amplifier Board x 2
detector-board.png

LocoNet Board x 1
detector-loconet-board.png

SPEEDOMETER

speedometer-design.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.

#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
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0