Subscribe via RSS
12May/105

Multiplexing ‘Photodetectors’ to detect train occupancy.

Right, I wasn't impressed whilst using the Sharp distance detectors and so went back to the age-old method of light-detection between the sleepers. As this is N Scale, I didn't want the standard, large and bulky Light Dependent Resistors and went for these smaller 'Photodetectors' found on eBay from a Taiwanese reseller.

Single detector

These were chosen based on the fact that they have a flat lens/front and are clear. They fit nicely between sleepers of Tomix FineTrack and, since I'd already laid and ballasted my main loop, could be retrofitted by drilling up and through the base.

Track wired up Rear of detector Detectors between sleepers

Detector between sleepers

Now, since I bought these in bulk, I started going crazy and sticking them everywhere I could. The goal was to put one everywhere that would become a good trigger-point for automation. I started with all of my stabling areas and put one at the start, middle and end of the sidings. I would use the 'trigger' from these to know when to slow to an engine to 50% throttle, 25% throttle and then stop. I then also put some in the tunnel entrances, station/platforms and also where signals should probably be (around points.)

Detector installed and ballasted

Another installed detector

It started dawning on me that I would require one analog input pin on my Arduino per photodetector. This would've gotten very expensive very quickly, but then I remembered that there was a simple tutorial on multiplexing analog inputs on the Arduino Playground (based on the 4051 IC). This IC would save me a lot of time and resources: with a little more wiring it could potentially give me 64 analog inputs for a total of 6 digital pins and one analog.

Here's the basic idea of wiring up a single 4051.

Basic Multiplexer with Photodetectors

Here's how you can use multiple 4051s and reduce pin consumption:

Advanced Multiplexer with Photodetectors

Notes on the options in the above image:

  • Option 1: Take the wires in the first rectangle and wire them to one analog pin and three digital. This will give you a total of 8 detector inputs.
  • Option 2: Take the 8 analog wires and put them into analog input pins. You then also need to connect up the 3 digital pins. For all inputs you'll only ever need 3 digital pins. But for the analog pins you'll need 1 pin for each 8 inputs. (i.e. 8:1, 16:2, 24:3, 32:4, etc... there is no upper limit, as long as you have the analog inputs.)
  • Option 3: Take the single analog pin and then the 6 digital pins. This will give you a total of 64 inputs and will use more digital than analog pins.

As you can see, you can interface with a lot of analog detectors, based on what pins you have available. As you may be aware, analog inputs are more 'expensive' on the Arduino than digital outputs as there are less available.

The process to control the above circuit is to set the digital pins to the desired address and then read the analog pin(s). You then need to set the next address and read the same pin (depending on your setup.) As changing through a lot of inputs and reading can take time, you need to be careful how many detectors you end up implementing. I have no exact numbers; but reading 64 inputs can easily be done in under a second. The goal is to make sure that a train does not pass a detector before it has been read!

So we have our detectors installed and circuitry built; we could now write software to manage it all. The basic idea was to read the value, adjust the min/max of that single detector and then check if it exceeded a threshold. Since these detectors required light to function, they would be effected by the amount of ambient light in the room and therefore the code would need to be smart enough to work out what was 'covered' (i.e. vehicle blocking light) and what was 'open'.

This code was also noted in my previous post where I used the Sharp detectors. These detectors produced a lot of noise and had to be filtered so that my code wouldn't simply trigger when a high/low value broke a threshold.

Here is the basic idea for reading one detector:

 read value of detector 
 if (detector value is greater than recorded maximum) then record new maximum value
 if (detector value is lower than recorded minimum) then record new minimum value
 if (either min or max has changed)
  then update range of this detector [max - min]
  adjust threshold [max - (range*0.25)]
 end if
 if (detector value is greater than threshold) then
  report that this detector is 'active'
 else
  report that this detector is 'inactive'
 end if

Right, so the above concept uses a 25% threshold below the maximum-read-value to see if the value read from the detector is 'active'. It is also constantly updating it's valid reading range so that it can adapt to the environmental changes. The main issue with this concept is that if the environment drastically changes (lights are turned on/off, curtains opened/closed, etc...) then this code would not adapt, as it never has a chance to 'retract' the limits. Therefore the following adjustment needs to be made:

 store the last 32 values of detector in circular array
 read value of detector and push last into array, popping off the first value
 find the lowest value in the array and store as the minimum extremity
 find the highest value in the array and store as the maximum extremity
 if (either min or max has changed)
  then update range of this detector [max - min]
  calculate the average from the last 32 read values
  adjust threshold [average + (max-min*0.10)]
 end if
 if (detector value is greater than this threshold) then
  report that this detector is 'active'
 else
  report that this detector is 'inactive'
 end if

Here you can now see that we only care about the last 32 read values (instead of the max and min since the code was running.) We are also using a new threshold calculation: 10% above the average of the last 32 values. This therefore means that we will receive an active notification if the value increases 10% above the 'stable' value of which we have been observing.

Of course, we are always able to introduce new issues; the above code, if run at processor-speed will read 32 values in under a second and, dependent on environment changes, may well not be able to cope. We therefore need to only test the detector at a specific interval (your mileage (kilometre'age) may vary!) of say, 100ms. This then means that the 32 values are taken over the course of 3.2 seconds. If this doesn't suit, then you can also increase the buffer size or decrease the polling delay.

But I bet you haven't seen the main issue? If a vehicle is stationary on the detector for too long then the range will/should drop to zero and therefore the detector will always be 'active'.
Wait, that would be correct? Wouldn't it?
It would, but it would also then report active for a certain time span until the range had expanded again once the vehicle had moved on. Note this can also be simulated by a long train traversing the detector and blocking the light (even with intermittent gaps of light) for a long period of time.

To prevent this? Adjust the polling delay and the buffer size...

Another good trick for limiting environmental effects is to add lights/LEDs to your layout around the detectors to ensure they always have a good source of UV. That way, when those curtains close, the ranges of your detectors wont drop too low.

What's next... well, what do you want to do with all this new information? You need to read it, pass it to the methods we've described above to filter the data and then act on it. Since we're multiplexing, we need to first tell our 4051 IC(s) which input we want to read and then read it. The following classes operate the multiplexers and detectors:

class DetectorCollection {
	private:
		struct Detector {
			int dValues[32];
			int dMax;
			int dMin;
			int dRange;
			int dAverageValue;
			int dCurrentValue;
			int dThreshold;
			int dCurrentIndex;
			bool dFullArray;
			int dAnalogPin;
			int dBitIndex;
			bool dIsActive;
		} detectors[32];
		int numDetectors;
		int digPins[3];
	public:
        DetectorCollection(int _digPin1, int _digPin2, int _digPin3);
        bool AddDetector(int _aPin, int _bit);
        void UpdateDetector(int detector);
        void UpdateAllDetectors();
        bool IsActive(int detector);
        void DebugInformation(int detector);
        int GetCurrentValue(int detector);
};

DetectorCollection::DetectorCollection(int _digPin1, int _digPin2, int _digPin3) {
	numDetectors = 0;
	digPins[0] = _digPin1;
	digPins[1] = _digPin2;
	digPins[2] = _digPin3;
}

bool DetectorCollection::AddDetector(int _aPin, int _bit) {
	//initialise a detector. the array contains "zero'd" detectors
	//by default
	if (numDetectors < 32) {
		detectors[numDetectors].dAnalogPin = _aPin;
		detectors[numDetectors].dBitIndex = _bit;
		for (int idx = 0; idx < 32; idx++)
			detectors[numDetectors].dValues[idx] = 0;
		detectors[numDetectors].dMax = 0;
		detectors[numDetectors].dMin = 999;
		detectors[numDetectors].dRange = 0;
		detectors[numDetectors].dAverageValue = 0;
		detectors[numDetectors].dThreshold = 0;
		detectors[numDetectors].dCurrentIndex = 0;
		detectors[numDetectors].dFullArray = false;
		detectors[numDetectors].dIsActive = false;
		numDetectors++;
		return true;
	} else return false;
}

void DetectorCollection::UpdateDetector(int detector) {
	//set digital pins      
    for (int pin = 0; pin < 3; pin++)
      digitalWrite(digPins[pin], 
        ((detectors[detector].dBitIndex >> abs(pin-2)) & 0x01) == true ? HIGH : LOW);


	//read analog pin.
	detectors[detector].dCurrentValue = 
        analogRead(detectors[detector].dAnalogPin);
	detectors[detector].dValues[detectors[detector].dCurrentIndex] =
        detectors[detector].dCurrentValue;
		

	//find the lowest and highest values in the array and store as
	//the minimum and maximum extremities.
	int tempVal, newValue = 0;
	bool extremitiesChanged = false;
	for (int idx = 0; idx < 32; idx++) {
		tempVal = detectors[detector].dValues[idx];
		if (tempVal < detectors[detector].dMin || detectors[detector].dMin == 0) {
			detectors[detector].dMin = tempVal;
			extremitiesChanged = true;
		}
		if (tempVal > detectors[detector].dMax) {
			detectors[detector].dMax = tempVal;
			extremitiesChanged = true;
		}
		//used for average calculated below.
		newValue += tempVal;
	}

		//update range of this detector [max - min]
		detectors[detector].dRange =
		    detectors[detector].dMax - detectors[detector].dMin;
		if (newValue > 0) {
			if (detectors[detector].dFullArray) 
                          detectors[detector].dAverageValue = newValue / 32;
			else detectors[detector].dAverageValue = 
                          newValue / (detectors[detector].dCurrentIndex + 1);
			//adjust threshold [average + (max-min*0.10)]
			detectors[detector].dThreshold =
                          detectors[detector].dAverageValue + 
                          (detectors[detector].dRange * 0.35);
			//adjust active flag:
			detectors[detector].dIsActive = 
                          (detectors[detector].dCurrentValue > 
                           detectors[detector].dThreshold);
		}

	//finally update the next location to store the next incoming value...
	//we're using a circular buffer, so just point to the start of the
	//array instead of shifting everything along.
	detectors[detector].dCurrentIndex++;
	if (detectors[detector].dCurrentIndex >= 32) {
		detectors[detector].dCurrentIndex = 0;
		//for calculating the average, we need to know once 
                //we have a full buffer. Once it's full we will always 
                //have a full set of NUM_READINGS values, otherwise
		//we only have as many as dCurrentIndex
		detectors[detector].dFullArray = true;
	}
}

void DetectorCollection::UpdateAllDetectors() {
  for (int d = 0; d < numDetectors; d++) UpdateDetector(d); 
}

bool DetectorCollection::IsActive(int detector) {
  return detectors[detector].dIsActive;
}

int DetectorCollection::GetCurrentValue(int detector) {
  return detectors[detector].dCurrentValue;
}

void DetectorCollection::DebugInformation(int detector) {
  Serial.print("Detector: "); 
  Serial.print(detector);  
  Serial.print(", APin: ");
  Serial.print(detectors[detector].dAnalogPin);
  Serial.print(", DBit: ");
  Serial.print(detectors[detector].dBitIndex);
  Serial.print(", Min: ");
  Serial.print(detectors[detector].dMin);  
  Serial.print(", Max: ");
  Serial.print(detectors[detector].dMax);  
  Serial.print(", Range: ");
  Serial.print(detectors[detector].dRange);  
  Serial.print(", Threshold: ");
  Serial.print(detectors[detector].dThreshold);  
  Serial.print(", Average: ");
  Serial.print(detectors[detector].dAverageValue);  
  Serial.print(", Current: ");
  Serial.print(detectors[detector].dCurrentValue);  
  Serial.print(", FullArray: ");
  Serial.print(detectors[detector].dFullArray);
  Serial.print(", CurrentIndex: ");
  Serial.println(detectors[detector].dCurrentIndex);
  /*for (int idx = 0; idx < 32; idx++) {
    Serial.print("|");
    if (idx == detectors[detector].dCurrentIndex) Serial.print("*");
    Serial.print(detectors[detector].dValues[idx]);
  }
  Serial.println("|");*/
}

And now, use it in your main program. Note I've created custom characters for the Arduino Liquid Crystal library via this website.

#define multiplexerPinBitA  40
#define multiplexerPinBitB  41
#define multiplexerPinBitC  42

#define pwmPin  			2
#define dirPin1 			3
#define dirPin2 			4

#define lcdRSPin			30
#define lcdENPin			31
#define lcdD4Pin			32
#define lcdD5Pin			33
#define lcdD6Pin			34
#define lcdD7Pin			35

#include <LiquidCrystal.h>
LiquidCrystal lcd(lcdRSPin, lcdENPin, lcdD4Pin, lcdD5Pin, lcdD6Pin, lcdD7Pin);

//cool hack! create characters for the LiquidCrystal Library!
//see here: http://icontexto.com/charactercreator/
byte trainCharFrontOn[8] = 
{B11111,B10001,B10001,B11111,B10101,B11111,B01010,B11111};
byte emptyChar[8] = 
{B00000,B00000,B00000,B00000,B00000,B00000,B11111,B10101};

DetectorCollection dCol = DetectorCollection(multiplexerPinBitA, 
    multiplexerPinBitB, multiplexerPinBitC);

void setup() {
  Serial.begin(9600);
  pinMode(multiplexerPinBitA, OUTPUT);
  pinMode(multiplexerPinBitB, OUTPUT);
  pinMode(multiplexerPinBitC, OUTPUT);

  for (int d = 0; d < 24; d++) {
    //analogpin is 0, 1, 2 [so DIV 8].
    //(where 0 is detectors 1-8, 1 is 9-16 and 2 is 17-24)
    //bit is the 0-7 on that analog pin [so MOD 8].
    dCol.AddDetector(d/8, d%8);
  }

  //start a train: direction
  digitalWrite(dirPin2, HIGH);
  digitalWrite(dirPin1, LOW);
  //speed (out of 255 [where ~50 is stopped])
  analogWrite(pwmPin, 85);

  lcd.createChar(0, emptyChar);
  lcd.createChar(1, trainCharFrontOn);
  lcd.begin(16, 2);
}

void loop() {
  //output to the LCD (16x2) the status of all the detectors:
  lcd.clear();
  lcd.setCursor(0, 0);	//top left

  for (int d = 0; d < 16; d++) {
    dCol.UpdateDetector(d);
    lcd.write(dCol.IsActive(d) ? 1 : 0);
  }
  lcd.setCursor(8, 0);
  for (int d = 16; d < 25; d++) {
    dCol.UpdateDetector(d);
    lcd.write(dCol.IsActive(d) ? 1 : 0);
  }
  
  //we still have 8 characters to draw other stuff... no idea what yet though.
  delay(333);
}

And then the detectors in action. Note it was at night time and I'm surprised the result was this good!

LCD showing train location

4May/102

Properly reading values from a Sharp GP2D12

Right, my efforts to read an IR Voltage until now have been flawed. It seems that my method of just plugging analogue inputs into my Arduino and expecting a clean reading was pointless. 'Noise' is a huge factor when reading analogue inputs (let alone correct pull-up/down resistors and grounding!) and dirty power supplies + PWM generation circuits really do kill any analogue data floating around.

Before I go into the actual sensors, read the Analogue Input Pin tutorial for the Arduino and also some sample code to read range of Sharp sensor.

So, how do you sort all these noise issues this out? Capacitors!
Based on the references below... you're either to put a ~22uf Capacitor between Vcc and GND or a 4.7uf Capacitor between Vout and GND. Firstly, here's the references:

Arduino + Sharp Sensors:

Other links on the Sharp sensors:

From all this information above I tinkered further with the sensors; but then proceeded to give up. The readings were much more stable, but I simply couldn't get them to do what I wanted as their values would drop off when the distance between the vehicle and sensor was less than 10cm.

Following is the code I used. Note that it does some trickery with caching the last 20 values and averaging them... I'll explain this all further in my next post.

//LIBRARY
#define NUM_INDEXES 20
 
class DistanceDetector {
 private:
  int min_valid;
  int max_valid;
  int analogPinNumber;
  int latest_max_value;
  int latest_min_value;
  int latest_time_updated;Here is the code I used anyway
  int lastIndex;
  bool full;
  void CalcAverage();
  
 public: 
  int lastReadValues[NUM_INDEXES];
  int sensorValue;
  DistanceDetector(int _analogPinNumber);
  void UpdateDetector();
};
  
  
DistanceDetector::DistanceDetector(int _analogPinNumber) {
 min_valid = 100;
 max_valid = 600;
 lastIndex = 0;
 analogPinNumber = _analogPinNumber;
 full = false;
 for (int idx = 0; idx < NUM_INDEXES; idx++) lastReadValues[idx] = 0;
}
 
void DistanceDetector::UpdateDetector()
{
 int latest_value = analogRead(analogPinNumber);
 if (latest_value >= min_valid && latest_value <= max_valid) {
  lastReadValues[lastIndex] = latest_value;
  CalcAverage();
  lastIndex++;
  if (lastIndex > NUM_INDEXES) {
   lastIndex = 0;
   full = true;
  }
 }
}
 
void DistanceDetector::CalcAverage() {
 sensorValue = 0;
 for (int idx = 0; idx < NUM_INDEXES; idx++) sensorValue += lastReadValues[idx];
 if (sensorValue > 0) {
  if (full) {
   sensorValue /= NUM_INDEXES;
  } else {
   sensorValue /= (lastIndex+1);
  }
 }
}
 
 
//USAGE:
DistanceDetector d1(SENSOR_PIN);
 
void setup() {
 //nothing required, as the constructor takes the pin number.
}
void loop() {
 d1.UpdateDetector(); //update the sensor.
 Serial.println(d1.sensorValue); //print out the value.
}

Conclusion

I still should have bought the GP2D120! But either way, I've decided to go with standard LDR/IR optics in the track base. Sadly, I'm over attempting the distance detection. My next post will cover a less-visible method for occupancy detection in the sleepers.