Block Occupancy Detection for DC and DCC, Part 3

In Part 1 of this series I demonstrated the basics of current sensing with ACS712 sensors and an Arduino microcontroller. In Part 2, I demonstrated the test loop wired for four blocks, occupancy detection and signals.

In this post I’ll put on my programmer’s hat and talk about the code behind block occupancy detection on the test loop in Part 2. Some is an improved version of code used in Block Occupancy Detection for DC and DCC, Part 1.  The rest is new; and I’m scaling from a single to multiple detectors. I’ve also changed the way I calculate the occupancy threshold so that it is calibrated for each sensor.

ACS712 Basics

Current Sensors and Block Feeder Connections.

ACS712 Current Sensors and Block Feeder Connections on the Test Loop.

The basic concepts for working with an ACS712 sensor board are discussed in  Block Occupancy Detection for DC and DCC, Part 1, but lets review them here:

–The ACS712 sensor senses current flow regardless of polarity up to its rated maximum (5, 20 or 30 amps). That means that it can be used with both DC and AC current. DCC track power is an AC type wave form alternating polarity at about 8 kHz. Raw sensor readings are noisy, requiring some mathematical processing to be useful.

–For maximum accuracy sensors have to be calibrated at start up. This is a software process by which you determine the value produced by each sensor when there is no current present, the average quiescent voltage also called adc_zero. In addition, I now calculate the average quiescent current reading  (AQC) at adc_zero; this value is used to calculate the occupancy detection threshold as explained below.

–The current detected by a sensor is represented by the difference between the reading and adc_zero for that sensor, converted to milliamps by the sensitivity factor for the version of the chip in use. Here’s the formula:

CURRENT (in mA) = (RAW_VALUE  – ADC_ZERO) / SENSITIVITY

–Accurately measuring an AC waveform requires multiple readings over a defined sampling period, using a sampling interval that is shorter than the frequency of the current you are trying to measure in order to sample the entire cycle. The Root Mean Square of the data set is calculated to arrive at an rms current reading.

Determining Occupancy

The heart of the occupancy detection system is a function to read current from a sensor and return an rms current value. This function is intended to be called once for each sensor during the detection cycle, so the target sensor’s pin and adc_zero are passed as arguments.

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 = (float) analogRead(PIN) - adc_zero;
      // convert to amperes
      adc_raw /= SENSITIVITY;
      // accumulate the sum of the squares of the readings
      currentAcc += (adc_raw * adc_raw);
      ++count;
      prevMicros += sampleInterval;
    }
  }
  //https://en.wikipedia.org/wiki/Root_mean_square
  // square root of the sum of the squares / number of samples
  float rms = sqrt((float)currentAcc / (float)numSamples);
  return rms;
}

During the detection cycle, readCurrent() is called by another function that determines if the block is occupied by comparing the rms current reading to a threshold value, returning either true or false.

bool isOccupied(int block, float adc_zero, float threshold) {
  float current = readCurrent(block, adc_zero);
  return (current > threshold);
}

Sampling Parameters

Here are the key sampling parameters I’m currently using.

// constant variables for block occupancy detection

// sample over 58 ms; 100 times the nominal pulse width for binary 1 in DCC
// sample time is expressed in microseconds
const unsigned long sampleTime = 58000UL;

// number of samples taken during a read cycle 
const unsigned long numSamples = 200UL;

// ADC conversion time is about 50µs (+/-)
// the sampling interval-here 290µs-should be longer than then ADC conversion time 
const unsigned long sampleInterval = sampleTime / numSamples;

I originally started with 100 ms sample time, then later realized I could shorten it, which helped compensate for the time required for two step detection as described below. I’ve only just begun to play with the variables and see how time efficient I can make the detection process.

Managing Block Data

I should back up a bit and explain my data handling strategy.  I represent each block with this data structure:

typedef struct BLOCK_DEF {
  int pin;
  int aqv;
  float aqc;
  bool occ;
  bool chg_pending;
};

Each block is a sensor, so the first element is its pin assignment, and the next two elements (aqv & aqc) of the structure are the calibration data for the sensor calculated at startup.  The fourth element, occ is the current occupancy state of the block (true if occupied, otherwise false). The last element indicates whether a change in occupancy state has been detected but not finalized (true or false).

The test loop sketch defines an array of BLOCK_DEF structures for the four blocks, initializing the array with the pin numbers and starting values for the remaining elements; An additional BLOCK_DEF structure is defined and initialized for for the master current sensor:

BLOCK_DEF blocks[NUM_BLOCKS] = {
    {0, 0, 0, false, false},
    {1, 0,0, false, false},
    {2, 0, 0, false, false},
    {3, 0, 0, false, false}}; 
    
BLOCK_DEF master = {4, 0, 0, false, false};

Calibration

My calibration routine has evolved to require two functions. The first is the same as in the original demonstration; the second function is new. Remember that track power must be OFF during calibration.

int determineVQ(int PIN) {
  float VQ = 0;
  //read a large number of samples to stabilize value
  for (int i = 0; i < CALIBRATION_READS; i++) {
    VQ += analogRead(PIN);
    delayMicroseconds(100);
  }
  VQ /= CALIBRATION_READS;
  return int(VQ);
}

float determineCQ(int pin, float aqv) {
  float CQ = 0;
  // set reps so the total actual analog reads == CALIBRATION_READS
  int reps = (CALIBRATION_READS / numSamples);
  for (int i = 0; i < reps; i++) {
    CQ += readCurrent(pin, aqv);
  }
  CQ /= reps;
  return CQ;
}

The first function determines the quiescent voltage, adc_zero, which is saved in the aqv element of the BLOCK_DEF structure. With adc_zero calculated, the second function determines the average quiescent current reading at adc_zer0, which is saved in the aqc element of the BLOCK_DEF data structure.

Here’s the calibration procedure from setup():

  master.aqv = determineVQ(master.pin);
  master.aqc = determineCQ(master.pin, master.aqv);
  
  for (i = 0; i < NUM_BLOCKS; i++) { // for each block 
    blocks[i].aqv = determineVQ(blocks[i].pin);
    blocks[i].aqc = determineCQ(blocks[i].pin, blocks[i].aqv);
  }

Why the second step? Because even though there is no current to be sensed, the sensor still produces a small reading. The sensors on the test loop produce an AQC reading from 9 to 24 mA. By capturing the average quiescent current reading, the detection threshold can be automatically adjusted for each sensor.

Instead of using a hard current threshold (like .0259, about 26 mA, as I did in the first demo), I now use a multiplier that I apply to the AQC reading for each sensor to set the detection threshold for that sensor.

Detection sensitivity is controlled by the multiplier. The multiplier I used during the demo was 1.5, which produces satisfactory results on the test loop, but is insensitive to low power devices.

Set the multiplier too low, and block states will flicker because of the variance in sensor readings. On the test loop I’m inhibited because of the Atlas bumper light and its draw on track power on block 3. I’m able to bring the multiplier down to 1.095; below that block 3 flickers mercilessly.  But the other blocks do not, so I think even lower multipliers and higher sensitivities are possible. I’ll find out on the L&NC.

The Detection Cycle

A detection cycle occurs on every iteration of the main loop. The first step is to check the master power. If the master power is on, then each block is checked in turn. If a block’s occupancy state is changed during the cycle, a notification is sent out to “subscribers,” other devices that have asked to receive block occupancy data. That is how my control panel gets updated.

  power_state = isOccupied(master.pin, master.aqv, 
           master.aqc * DETECTION_MULTIPLIER, master.chg_pending);
  if(power_state != master.occ) {
    if(master.chg_pending) {
      master.occ = power_state;
      master.chg_pending = false;
    } else {
      master.chg_pending = true;
    }
  } else {
    master.chg_pending = false;
  }
  if(master.occ){
    for (int i = 0; i < NUM_BLOCKS; i++) {
      block_state = isOccupied(blocks[i].pin, blocks[i].aqv, 
               blocks[i].aqc * DETECTION_MULTIPLIER, false);
      if (block_state != blocks[i].occ) { // if occupancy state has changed
      // if a pending change has been previously detected
      if(blocks[i].chg_pending && !master.chg_pending) {
           blocks[i].occ = block_state;
           blocks[i].chg_pending = false;
           String id = String(i);
           notifySubscribers(1, id, String(block_state));
         } else { // initial change detection
           blocks[i].chg_pending = true;
         }
       } else {
         blocks[i].chg_pending = false;       
       }
    } 
  }

As stable as the current sensing system is now, I have found that when a locomotive is crossing a block boundary and drawing from two blocks you can get some jitter causing the newly occupied block’s state to flicker.

My current (pardon the pun) solution is to implement a system rule requiring 2 consecutive readings to confirm a block state change.  So, the first time a state change is detected, the block (or the master) is marked pending. On the next cycle, if the state change is detected again, the state change is accepted. Otherwise the pending flag is reset. The additional time lag in detection this creates is unnoticeable, and it completely eliminates detection jitter.

What’s Next?

Dual Searchlight Signals, Scratch Made with BLMA Signal Heads.

Dual Searchlight Signals, Scratch Made with BLMA Signal Heads.

The whole point of block occupancy detection is to put that data to use in some way. The most basic use for it is to run signals, which I used on the test loop as a way of showing occupancy detection in action. That can be done through JMRI, or as part of the layout’s intrinsic Arduino processes which is they way I’m approaching it.

I think of signals as the simplest form of animation a layout can and should have. As you can see from the demo, signals done the Arduino way are equally functional in a DC or DCC environment. It simply doesn’t matter how you run your trains: if you want signals then you should have them!

But, as always, there are unique complications that arise when doing things the Arduino way, not the least of which is the finite number of pins available to run lights or other external devices.

Here’s a hint: running signals the Arduino way works best when the signals are tied together in a physical and logical chain.

That, fellow railroaders, is the subject for another post.

 

L&NC Construction Underway

Lassen & North Coast RailroadConstruction of the Lassen & North Coast Railroad began in earnest with the construction of the three box frames for the layout.

The L&NC is intended to be a portable layout consisting of three modules that will individually fit into the back of a small to mid-sized vehicle, including crating material. All three modules should fit into a larger vehicle, such as an SUV or Van. To do this, I’m building a foundation that—for lack of a better term—I call a “box frame.”

Most benchwork is done in the L-girder style if the layout is permanent or in a box style if portable or modular.  I’m building a variation on the box theme, so that it can be both portable and include two levels. Each module consists of an upper and lower frame, joined together at four corner posts. Both top and bottom frames are topped with a piece of 1/4″ plywood.  This approach should help keep weight down while making the structure rigid.

The trick for this layout, of course, is to get three two-level modules to fit together securely so they can be taken apart for work or for transport, then put back together again reliably. One level is easy enough, but two levels?

Building A Stable Foundation

After cutting, milling and drilling all the frame members, assembly begins.

After cutting, milling and drilling all the frame members, assembly begins.

I decided that the way to pull this off was to prepare the necessary lumber for all modules at the same time, then put everything together at once to guarantee fit. I chose poplar from my local Lowes instead of pine. While more expensive, poplar more than makes up for it with its straight grain and hardwood strength. For joinery I decided on lap joints; more glue surface and thus more strength than simple butt joints, but easy to do. I like to follow Norm Abram’s methods and pin all joints with brads during glue-up.

My small collection of shop tools made this part easy. I cut all the pieces to length on the miter saw, using stop blocks to cut multiple pieces of the same size.  Most frame members are made from 1 x 2’s; I used 2 x 2 poplar for the corner posts. Then I taped frame members of the same size and type together (see the image above) and, with a dado blade in the table saw, cut the lap joints at the ends, and dadoes for cross members, in the frame members.  Then, at a small bench-top drill press, I cut holes in pieces intended to be used as internal cross members to facilitate wiring and other needs.

The last piece of preparation was on frame members of adjoining modules; these connect to each other when the layout is assembled. Preparation of these pieces required the installation of an alignment system that guarantees the layout comes back together consistently.

Aligning Modules

Installing McMaster-Carr alignment pins & liners

Installing McMaster-Carr alignment pins & liners

I use alignment pins from McMaster-Carr for aligning adjoining modules, choosing the 3/8″ diameter pins and matching liners. The fit of the pin into the liner is precise, and will bind if you attempt to force them together at the wrong angle. On the other hand, the rounded ends of the pins help guide insertion which, if the angle is correct, is smooth as silk.

I configured four pairs (two pair for each module interface) of frame members to butt against each other with the pins for alignment. Each pair was taped together, I marked center points and then drilled and countersunk on the drill press. Actually, I countersunk first on both sides of the taped pair, then drilled the smaller through-bore. I use Forstner bits for this kind of thing, because of ease of centering and the clean bores those bits produce.

Installing corner posts to join the top and bottom frames.

Installing corner posts to join the top and bottom frames.

After gluing up the top and bottom frames of the first module, I set the corner posts that were dadoed to fit into the frame corners.  As you can see, the posts form short legs for the module in addition to supporting the top frame.

For the top joint, I created an ersatz mortise and tenon joint to hold the top when installed. To do that I set the top frame on top of the posts, then glued blocks to the frame around the “tenon” to form the mortise. The mortise and tenon joint created is snug; This joint is intended to come apart, so I eased the top edges of the tenons to make assembly/disassembly easier.

Module #1 fully assembled.

Module #1 (the “eastern” module) fully assembled.

Here is the fully assembled first module.The near end abuts the next module; the matching member for the lower box is mated with the assembled module to test fit.

Test fitting the three module frames.

Test fitting the three module frames.

Next the middle and right hand modules were built, then the whole thing was put together to test fit and the alignment system.  Thanks to the McMaster-Carr alignment pins, and careful assembly making sure everything was aligned correctly every step of the way, the frames came together correctly the first time! I can’t emphasis enough how important it is to take your time, constantly testing and retesting fit of key components, when doing this kind of framing.

At this point the western (or right hand) module is only partially framed. When I put together the helix intended for that section, I found I needed to make a few modifications to the upper frame to make it work.  I’ll get into that in a subsequent post.

Drilling for and Setting Retention Bolts.

Drilling for and Setting Retention Bolts.

In the meantime, the last step in basic construction was to create a fastening system to hold the modules together when assembled. NTrak and other modular systems generally rely on C clamps.  On this layout, given its design intention to sit on top of a bar (or tables), C clamps are a bit awkward. So I chose 3″ machine screws that connect with 4 prong tee nuts (often used in furniture making).  After clamping each connecting pair of posts together, I drilled the hole then set the tee nut on one side to connect with the bolt inserted from the other side.

The modules fit together well enough that it is not generally necessary to use the bolts to pull modules together. The bolts with the alignment pins make a solid connection that can be assembled and disassembled easily.

With the basic frames complete I topped the eastern (left hand) and center modules with plywood. I also put 1/4″ plywood atop the lower frame of the western module. The upper frame of the western module will need more work to accept a helix.  That project, and general build up of the layout are ahead.

With much to do, I’ll pick this up again in a subsequent post.


 

Block Occupancy Detection for DC and DCC, Part 2

In Block Occupancy Detection for DC and DCC I demonstrated the use of an ACS712 sensor and an Arduino Uno to sense current draw across the rails, the basis for most block occupancy detection systems. Assuming that one has also equipped some rolling stock with resistor wheel-sets, lighting or sound decoders, so that the entire length of a train is detectable, the theory is that current draw indicates that a block is occupied — and lack of current draw indicates that a block is clear.

In reality, it may not be that simple if you have devices drawing from track power other than locomotives and rolling stock. Any background current draw on rail power can interfere with the current sensing model.

Rail Powered Accessories

The Test Loop

The Test Loop

Keen eyes will notice that the test loop has a rail powered light on the Atlas track bumper at the end of the siding. I laid the track on the test loop long before I understood block occupancy detection.  I won’t do that again as I build the L&NC.

The problem is that the bumper light draws from the track itself and thus presents a constant load on that block. The draw of the lamp forces a slightly higher threshold for determining occupancy than would otherwise be appropriate.  Static loads on monitored blocks greatly complicate current sensing.

The best practice where current sensing is used to detect block occupancy is to feed accessories from a separate power bus. To use track power (as you might with a stationary decoder running a turnout motor), draw from outside monitored blocks—from a separate branch or sub-bus—to avoid confusing the occupancy detection system.

Solving the Continuity Problem.

While current sensing works well on a DC layout, as shown in the first video demonstration, the use of current as a control mechanism means that block occupancy is undetectable at times because of normal operation. Similarly, any transient loss of DCC track power (as with a short) will throw the occupancy detection system off for the same reasons. I think of this as a continuity problem.

The solution in both cases is to monitor the track bus with an extra sensor and lock the state of the system whenever the track bus is off. The simplest algorithm, puts it a slightly different way:

if track current is flowing then check blocks and update else maintain current state

E.g. ( in C++, where “master.on” represents the master current status):

if(master.on == true) {
  for ( i = 0, i < NUMBER_BLOCKS; i++) {
    // check block sensor i and update block status
etc......

For the cost of a sensor and a few lines of code the block occupancy system will now maintain its state correctly whenever there is a total loss of track power.

Wiring for Detection

Preparing the test loop for the Arduino & ACS712 sensor based occupancy detection system was a bit of a chore, primarily because current sensing requires some rethinking of the standard approach to layout wiring.

Wiring. Block sensors were added in Phase 2. Phase 3 signals and lighting in progress.

Test Loop Wiring. Block sensors were added in Phase 2. Phase 3 signals and lighting were in progress when this picture was taken.

The test loop represents my first crack at it. Needless to say, there is a list of things that will be done differently on the L&NC.

Routing Buses

The loop has a bus structure that, except for the lack of the DCC command bus (I connect my throttle directly to the booster when it is attached to the test loop), is the prototype for the L&NC.

Test Loop Connection Panel

Test Loop Connection Panel

The bus structure begins with the connection points on one end of the layout, mounted on a connection panel made from .080 styrene sheet. Every module on the L&NC will have one of these, with matching cables at the opposite side where it adjoins the next module.

Starting from the top the first two connection groups are polarized power connections made with color coded Anderson Powerpole connectors. A “Recommended Practice” per the Ntrak Manual, these things are a reliable solution for creating polarized power connections.  Some parts are available from Amazon; I find that Powerwerx.com has the best selection of PowerPole parts and supplies. The 15 amp gear should be sufficient for most N Scale layouts (an N Scale layout requiring more than that would be something to see!). Kudos to Ntrak for finding and adopting these connectors. BTW: the crimper is expensive but indispensable if one is going to make a bunch of Powerpole connectors.

The top group is the main DC power bus in three voltages: 12 (yellow), 5 (red) and 3.3 (blue) plus ground (black). This was also where I attached a label with the loop’s assigned IP address.

The next group down is the track bus (red & black); my DC power pack and my DCC booster can connect to the loop here with a matching plug.

The next two connections are an RJ45 port for Ethernet and a USB connection to the UNO. Both are  assemblies I got from Adafruit Industries. On the L&NC, the USB port(s) for attached Arduino boards will be located in an accessible place, away from the other connection groups (which will generally be hidden).

Connection Panel Bus Connections

Panel Bus Connections

Color coded power buses are routed along the edge to a central barrier strip where feeders take off to supply power where needed. On the L&NC I’ll choose different, unique colors for the track bus and feeders to reserve black for system ground and red for system Vcc (+5 volts DC).

Suitcase connectors are really popular these days for connecting wires without soldering. If you are not going to do occupancy detection then directly connecting feeders to bus wires with suitcase connectors is a clean solution. However, once you start down the road I’m traveling–whether your solution is off-the-shelf or homemade–suitcase connectors are not helpful. Instead, I find that PC board mounted screw terminals make easy wiring connections that are reliable and easily changed when necessary. When dealing with magnet wire, which you have to use in N scale if you want to run wires through scale light poles, conduits, etc., top-of-the-line PC board terminal systems are the ONLY reliable solution (the cheap versions will drive you crazy with intermittent connectivity).

ACS712 Board

ACS712 Board

The ACS712 sensor boards come with screw terminals for the monitored load, plus 3 pin headers that take standard 3 wire servo connectors for connecting to system power and the microcontroller. Wires that connect to the central barrier strip have crimped terminals.  I mostly solder at the PC board level and, less often, to make special cables (I solder track connections too, but that is a different issue).

Power Distribution on the Test Loop

Power Distribution on the Test Loop. The board in the upper right corner is for distributing 5 volt power to the signal system via PC board terminals.

On an L&NC module, the bus will continue from the central distributions point out to the edge configured to connect to the next module. One big change in module construction on the L&NC will make wiring easier: the cross frame members will be pre-drilled with large holes. That “little” gouge you see where wires cross the frame member at the bottom of the picture taught me that lesson.

On the test loop the 12 volt bus supplies the attached UNO with its Ethernet shield; the 5 volt bus supplies all sensors and actuators attached to the UNO and is generally considered system Vcc. The 3.3 volt bus is not currently being used on the test loop (but would be great for low current lighting or animations). All share a common ground (really important!). NB: I use modified Computer power supplies that  produce all three voltages at once with a common ground. Multiple DC power supplies can be combined to the same effect; just tie their grounds together to create the common ground.

Even on the test loop, the 5 volt bus is used so much that I need to create a distribution system to get power where needed. The lesson for the L&NC is  I will need to have distribution nodes for the whole power bundle in a few strategic locations on each module. Obviously, I need to do some more detailed wiring planning in advance so I know where everything has to go.

Routing Feeders

Current sensing encourages a nodal distribution system for track power. Instead of attaching feeders to the nearest track bus, the feeders are routed to an area where the sensors have been mounted. You could mount individual sensors at the points where feeders descend from the tracks above. In some situations that may work better; in other situations, aggregating the sensors in groups is the easier and more effective method.

Current Sensors and Block Feeder Connections.

Current Sensors and Block Feeder Connections.

ACS712 sensors read only one of the two rail conductors. On the test loop sensors are attached to the red rail.

I tried to complete the rail feeder connection with a board (with terminals) across from the sensors for the black rail connection points. I also ended up using the same board to centralize the power and data connections for the sensors and the servo for the turnout. In the future I will not mix track / control system connection points in this way.

Block Occupancy Detection in Action

Here is demonstration of the progress so far, with block occupancy detection active on all 4 blocks of the test loop. The signals are programmed to respond to block occupancy and the state of the turnout (the turnout actually has three unique states: “aligned main”, “aligned divergent”, and “in motion”), allowing you to see the system responding as trains move along the track. You’ll see it work in both DC and DCC — the layout works whether a power pack or a DCC booster is plugged into the track bus port. I have yet to see a single commercial occupancy detection solution that moves as easily between DC and DCC as this one does. I added a station with internal lights (connected to the same nodal system that supports the signals) to give just a little hint of the capabilities that will be unlocked on the L&NC.

In future installments, I’ll dig into the occupancy detect code again to give more insight into how it works. I’ll also go into the details of how I’ve implemented signalling on the test loop, including some special (and inexpensive) gear I’ve designed to allow a single microcontroller to run up to 255 nodes of signals and other kinds of lighting or animation.