Block Occupancy Detection for DC and DCC, Part 1

Block occupancy detection is central to advanced layout control. Without it systems like control panels, signals or animation that depend on knowing the position of your trains can’t function. If my concept of building a layout around a network of Arduino boards and off-the-shelf peripherals is going to work, it has to be able to do block occupancy detection.

A Control Panel made with JMRI

A Control Panel made with JMRI PanelPro

As it turns out, block occupancy detection is possible and economical (in terms of equipment cost) with an Arduino and some sensors. In this post I’ll talk about and demonstrate what I’ve learned so far about the electronics of block occupancy detection.


Block occupancy detection can be accomplished a number of ways. The simplest methods involve installing trigger points that detect a passing train using optical or magnetic sensors. These methods generally only work with moving trains, and can’t detect stationary objects located on the tracks between trigger points.

Detecting Current Flow

Enter current sensing, the block detection method most closely tied to the electronic function of the layout. The idea behind sensing current for block occupancy detection is simple enough: current should only flow across the rails in a block that has a locomotive or other device (such as rolling stock with lights or a sound car decoder) on it.  If current flows, the block is occupied; if not, it is (or should be) clear.

How Current is Sensed

Digitrax BDL168 Detection Unit

Digitrax BDL168 Detection Unit

 

Current sensing is generally done indirectly, to avoid drawing on the current (more of a problem in high voltage applications than for a model railroader) and to enable sensors to operate at a different voltage/current from that being sensed. Early commercial block detection systems used  induction coils to sense current passing through wires wrapped around the coil. Newer systems tend to be hybrid or solid-state, reducing size and power consumption. The majority are optimized for DCC and few systems claim to work in DC.

RRCirKits Detection Coils

RRCirKits Detection Coils

To put it another way, there is no block occupancy sensor device on the market with a generic analog or digital output that could be directly read by a general purpose computing device, such as an Arduino. For that matter, I could not identify a single BO system that could easily interconnect with a system from another manufacturer. There aren’t any real standards in this area.

Current Sensing, the Arduino Way

The Allegro ACS712 chip is a leading solid state current sensing solution for power feeds up to 30 amps.  Its works using a phenomena called the “hall effect” to indirectly detect current flow through a silicon chip. The output of the ACS712 connects to an analog port on an Arduino board (or any other microcontroller/microcomputer that supports analog sensing). Connected to an Arduino, a call to analogRead() gives you the output of the sensor.

ACS712 Board

ACS712 Sensor Board

As is usually the case, the chip requires a few external components to function properly; primarily connectors and a filter capacitor/resistor combination to fine tune the sensitivity of the device.  As it happens there is a robust market for simple ACS712 sensing boards, resulting in prices under $2 per sensor if you buy directly from a Chinese source on Ebay, or Alibaba. Even on Amazon.com, sensors can be purchased for about $7 each with Prime 2-day shipping, which makes them slightly cheaper per block than sensors for the most cost-effective systems specifically marketed for DCC.

The only issue I’ve seen with these open market sensors is that there is no specific standard for the filter capacitor. Accordingly, different batches of these boards may have a slightly different sensitivity which would have to be accounted for in the software. As you’ll see, that is not nearly as big a deal as it sounds.

For now I think the optimal strategy for anyone trying to install a block detection system with these components, is to buy all the ones you will need (plus a few extra) in a single batch from a single source. At $2 a pop from China, it makes sense to buy in bulk. In my experience that will guarantee they are all tuned the same way.

DC and DCC Ready

It’s more important for model railroaders that the sensor functions equally well with alternating and direct current. The only difference in the way the sensors work as a system in DC versus DCC comes from the fact that in DC track power is used to control trains; an occupied block may have no power at all for control reasons. That could throw a block occupancy detection system off.

Any current sensing system for DC has to account this; as should any DCC friendly system that wants to preserve block occupancy information between track power outages (such as caused by a short circuit on the track).  We’ll get to solving that problem in the next post.

Plug and Read . . . Not!

I should tell you that if you plug in a sensor and try to determine current state with a single reading…. well, you’re going to be disappointed.  Individual readings seem to jump around a lot although you can discern trends in either direction.

Alternating current presents the additional problem of sensing a current flow that reverses polarity; at any given moment the power could be anywhere in its cycle, including 0 volts at polarity transition. For AC, multiple samples taken at a frequency that is greater than the frequency of the power cycle is required. Something like 8 – 12 samples per cycle will allow proper detection of AC current. Multi-sampling helps with DC too.

First, Calibration

Different sensor chips will produce slightly different analog readings under the same conditions. Any differences in filter capacitors between boards will also produce different readings, as will any +/- variation from 5 volts supplied as Vcc to the chip. To deal with these differences and actual power conditions, we have to calibrate each sensor on startup.

What we need to know is the output of each sensor when there is no current flow (no load); we’ll call this condition adc-zero. The difference between sensor output and adc-zero represents the current sensed by the device.

Getting an accurate number for adc-zero requires averaging a large number of quick readings — 5000 seems to be optimal — like this (we call adc-zero the “quiescent voltage” output):

long VQ = 0;
//read 5000 samples to stabilize value
for (int i=0; i<5000; i++) {
   VQ += analogRead(PIN);
   delay(1);
 }
 VQ /= 5000;
 return int(VQ);

With that number calculated for each sensor, we can get  consistent readings across multiple sensors.

Sampling Current

Reading current consistently requires a multi-sample approach over a defined time span. For this demonstration we’ll accumulate 250 samples taken over a 100 ms span on each read cycle.

Alternating current drives you to calculate the Root Mean Square of the data to filter polarity shifts and get a very stable mean current value you can work with.  Not only does this work well with A/C, it also is equally helpful in getting stable readings of DC current because the output of the sensor varies over time [for the hardcore math and theory, see the Wikipedia page].

Its an easy formula to translate into a computer algorithm, using a function like this :

float readCurrent(int PIN, float adc_zero)
{
  float currentAcc = 0;
  unsigned int count = 0;
  unsigned long prevMicros = micros() - sampleInterval ;
  while (count < numSamples)
  {
    if (micros() - prevMicros >= sampleInterval)
    {
      float adc_raw = analogRead(PIN) - adc_zero; // Electical offset voltage
      adc_raw /= SENSITIVITY; // convert to amperes
      // accumulate sum of the squares of converted raw data
      currentAcc += (adc_raw * adc_raw); 
      ++count;
      prevMicros += sampleInterval;
    }
  }
  // calculate the root mean square of the data set
  float rms = sqrt((float)currentAcc / (float)numSamples);
  return rms;
}

Finally, we have to use the rms values to determine whether or not a block is occupied. For the purposes of this demonstration, I’m using a hard rms threshold of .0259 (the adc-zero rms is about .021 on the sensor used for the demonstration); I’ll adopt a different approach when I scale up to a multi-block arrangement.

To further smooth the sketch response, it averages each reading with the previous one. Here is the key code that determines if a block is occupied:

  prev_current = current; // save last reading
  current = readCurrent(currentPin, adc_zero);
  avg_current = (current + prev_current)/2;
  occupied = avg_current > occupancy_threshold;

So lets put the whole journey to this point together in a video demonstration. This demonstrates current sensing in a DC context:

Here’s the sketch in its entirety:

// Advanced Sensing with ASC712
// Using Calibration to find sensor 0
// then sensing by compiling multiple readings to obtain an RMS (Root Mean Square).
// Derived from these forum posts: https://forum.arduino.cc/index.php?topic=179541.0
// NOTE: Forum posters did not account for device sensitivity
///////////////////////////////////////////////////////////////////////////////////
const int currentPin = 0;
// Sampling Parameters
const unsigned long sampleTime = 100000UL; // sample over 100ms
const unsigned long numSamples = 250UL; // the number of samples divides sampleTime exactly, 
                                        // but low enough for the ADC to keep up
const unsigned long sampleInterval = sampleTime/numSamples;  // the sampling interval
                                         //  must be longer than then ADC conversion time
#define SENSITIVITY 185  // from ACS712 data sheet for 5A version, in mV/A
int adc_zero;  // variable to hold calibrated sensor quiescent voltage

boolean occupied = false;
float occupancy_threshold = .0259;

float current = 0;
float prev_current;
void setup()
{
  Serial.begin(9600);
  Serial.println("\nACS712 Current Sensing Basic Demonstration\nMultiple Readings converted to RMS at 1 second intervals\nValues for quiescent output are determined by Calibration.\n\nCalibrating the sensor:\n");
  adc_zero = determineVQ(currentPin); //Quiescent output voltage - the average voltage ACS712 shows with no load (0 A)
  delay(1000);
}

void loop(){
  float avg_current;
  prev_current = current;
  current = readCurrent(currentPin, adc_zero);
  avg_current = (current + prev_current)/2;
  occupied = avg_current > occupancy_threshold;
  
  Serial.print("Current Sensed:");
  Serial.print(current * 1000 ,1);
  Serial.print(" mA\t\t");
  Serial.print("The block is ");
  if(occupied){
    Serial.println("occupied");
  } else {
    Serial.println("not occupied");
  }
  
  delay(1000);
}

int determineVQ(int PIN) {
  Serial.print("estimating avg. quiscent voltage:");
  long VQ = 0;
  //read 5000 samples to stabilize value
  for (int i=0; i<5000; i++) {
    VQ += analogRead(PIN);
    delay(1);//depends on sampling (on filter capacitor), can be 1/80000 (80kHz) max.
  }
  VQ /= 5000;
  Serial.print(map(VQ, 0, 1023, 0, 5000));
  Serial.println(" mV");
  return int(VQ);
}
float readCurrent(int PIN, float adc_zero)
{
  float currentAcc = 0;
  unsigned int count = 0;
  unsigned long prevMicros = micros() - sampleInterval ;
  while (count < numSamples)
  {
    if (micros() - prevMicros >= sampleInterval)
    {
      float adc_raw = analogRead(PIN) - adc_zero; // Electical offset voltage
      adc_raw /= SENSITIVITY; // convert to amperes
      currentAcc += (adc_raw * adc_raw);
      ++count;
      prevMicros += sampleInterval;
    }
  }
  //https://en.wikipedia.org/wiki/Root_mean_square
  float rms = sqrt((float)currentAcc / (float)numSamples);
  return rms;
}

Next installment, we go to the next level: multiple blocks and adding signals to the mix. And I’ll demonstrate everything in both DC and DCC.