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:


–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);
      prevMicros += sampleInterval;
  // 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:

    {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};


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);
  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.aqc = determineCQ(, 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.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;
    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.


23 thoughts on “Block Occupancy Detection for DC and DCC, Part 3”

  1. Hi there,

    I have been trying to get this to work but i am struggling, do you have a link to the full sketch with the improvements you have made in this part, i tried the one from part 1 but noticed that it missed the few improvements and im not sure where exactly to put them in the code.



    • Hi Matt,

      I do have, and am happy to share, the full sketch. It does contain other code for the signals and communications that might confuse the issue. If you want to wade through that, I’ll email the file package to you.

      If you let me know what sort of issues you are running into, I’m happy to try to help.



      • Hi Robin,

        Weve been trying to get it working with the sensors, they are detected, but unfortunately the bouncing is such that it makes it very hard to detect a loco on the track, or sometimes a false positive is given, so thinking of putting your queue system in place but increasing it to say 5 pending, therefore itll make 100% sure the reading is correct before updating the status, also wondering if the changes youve made in this part will help.

        If you can email me the full sketch would be much appreciated, im sure i can strip out the signal and communications stuff from it just to get the latest code for the occupancy detectors.



      • Hello Robin,

        I am too interested in your full detection package. I am currently assembling the current sensor boards. Thanks again for your contributions!

  2. Hi Robin,

    I stumbled across your blog when google searching on how to use an Allegro current sensing chip; came to the same conclusion as you in avoiding using transformer/coils etc for current sensing. I’m currently building/designing a DCC accessory decoder for a level crossing in N. So far I’ve managed to power an ATTiny 25 from a DCC track, which has been programmed to blink 4 level crossing leds alternately. The next step I plan to do is build a block detection circuit with the ACS712, obviously I’ll need a different micro controller for it since the ATTiny25 is limited.

    I have to say I’m impressed with what you have achieved so far.

    Quick question for you, what is the lowest current your setup can detect? Ideally I want to be able to detect wagons/coaches fitted with a 10kohm resistor in order to keep current draw low; ~1mA+ on a track voltage of 12V.

    Cheers Dimitrios

    • Hi Dimitrios,

      So glad you found the blog!

      If you play with two or three of the sensor boards like I’ve been using, you’ll find that the effective sensitivity varies from board to board. On my test loop, the four block sensors that were all bought at the same time are sensitive in the 1 – 1.4 mA range. The fifth sensor, which I bought at a different time and use as the track master sensor, is sensitive to about 2.2 mA. Effective sensitivity is really a noise management problem; noise, and the mathematical values needed to filter it, is what varies between boards.

      Looking at the ACS712 documentation, I’ve come to the conclusion that the thing that is varying from board to board (or, more realistically, lot to lot of boards) is the filter capacitor, for which the documentation gives a value and then goes on to say it may be varied for different applications. I think I can confidently say that there is no standard for that with the off-the-shelf boards; and nobody is producing an ACS712 board optimized for our 8 kHz DCC waveform. One of my projects for the coming months is to get some ACS712 chips and try different circuit configurations to see if I can get consistent DCC sensitivity under 1 mA.

      I haven’t tested resistor wheels yet so I’m curious how things go for you.

      Best, Robin

      • Hi Robin,

        Thank you for the reply. I had a look at the ACS712 datasheet in more detail after posting here and curiously it claims on page 9 the lowest value of current that can be measured is: noise level (mV)/sensitivty (mV/A) i.e. for the 5V version the figure comes to 21/185= 113mA. Maybe my maths or understanding is completely off here but your figures seem to be a lot better. Noise is a real killer here, it all depends on how clean the power is to the chip and board layout i.e. grounding, bypassing etc.

        I’ll order a couple of these chips from one tape reel and build my own breakout board and hook it up to see what I get from experimenting.

        Will be following your progress with great interest.

        Cheers Dimitrios

        • Hi Dimitrios,

          That noise number is, as far as I can tell, dependent on the cap chosen for the filter. I think that noise number is for a 47nf filter; 1 nF is specified for everything else so I’m darned if I know what that really means.

          Sensitivity is expressed in mV/A, but no minimum increment is noted. That makes sense for an analog sensor — so the real issue may be limits in the ability to read the sensor. If we could read its output at the microvolt level we might get much greater sensitivity.

          I went back to reading more myself and I think I’m onto a possible solution for sensitivity using an external ADC with a higher resolution. I’m going to order some sample boards this weekend and see what I can accomplish.



          • Hi Robin,

            Noticed an error in my previous post not to confuse anyone else reading this; I should have put +/- 5A version not 5V.

            Resolution as you point out is the problem. I understand that Sparkfun have made a breakout board using the ACS712 with an additional op amp to extend the output range with a gain of 47, order number SEN-08883. Personally for me having to play around with a pot to calibrate is a practice best to be avoided.

            A higher resolution ADC maybe the answer here since the Arduino (Due or Zero) are limited to 12-bit.

            On another note relating to using capacitors I become aware sometime ago of the issue of using ceramic capacitors whose capacitance can vary due to dc bias. There have been many cases reported whereby people just plonked the cheapest branded/unbranded ceramic capacitor into their designs well within their published ratings and found things not working as expected, completely unaware that the capacitance in operation could change considerably depending on the voltage. Sadly capacitor manufacturers, even well known brands, do not provide a dc bias characteristic graph on their datasheets and one has to dig the information out from the manufacturer’s website.

            Cheers Dimitrios

  3. Hi Robin
    I’ve been exploring various block occupancy methods over the last few months and came across your great thread. I’ve loaded up the script in part 1 and changed the occupancy threshold 0.010 and now works with an old Lima 00 class 26 and my newer Bachmann class 08. What I’m not sure about is 1. My wiring, I have the acs712 in series with the DCC controller. I couldn’t see a wiring diagram. 2. How can I get the occupancy to work when the train is in the block but stationary?

    Thanks James

    • Hi James,

      If you are able to detect a running loco, then you have the wiring right. I’m assuming your locos are DCC without Sound. My DCC sound loco triggers detection at idle with all sound and lights off. However, I have collected information from a number of modelers using DCC without sound suggesting that the idle draw of a standard DCC decoder is not detectable, at least without further refinements to the system.

      What you are running into is a sensitivity limit that is the result of two factors that come into play: 1) the ACS712 chip calls for an external filter capacitor to manage bandwidth and noise; whatever they put on the cheap boards from China, its not optimum for low current applications. Its also possible that the ACS712 design simply requires additional external resources to achieve greater sensitivity; 2) the limited resolution of the 10-bit ADC built into most Arduinos (a couple of the newer, more advanced boards have 12-bit ADC’s) is an impediment to greater sensitivity.

      I have ordered an ACS712 sensor board specifically designed to sense low current; you can find it at Sparkfun (see Dimitrios’ comment above). If it works, it may lead me to a way to modify the off-the-shelf sensors to achieve greater sensitivity. The working principle of the board appears to be to suppress the signal coming from the ACs712 with resistors, then amplify the result, leading to less noise and greater sensitivity.

      I’m also working with a couple of external ADC boards with higher resolution (14 – 16 bits). The problem with the built-in ADC on most boards that at 10-bits, resolution is limited to 4.88 mA (5 amps / 1024), so a more sensitive ACS712 board is useless without a higher resolution ADC. The calibration and measurement procedures I’ve used do seem to help suppress noise and improve sensitivity; but not enough to deal with resistor wheelsets or idling DCC decoders with no sound.

      I’m expecting to conduct experiments in the coming weeks to try to clarify the problem and chart a workable, affordable solution. I’ll blog about the results as I go along.

      Best, Robin

  4. Thanks Robin, I will watch with interest how you get on as the approach is affordable and quick to put together. With my little test setup I am now going to set up another Arduino loaded with CMRI and link that to JMRI. The plan is to get the CMRI board to act as sensor and listen for a output from your block ardunio. I should the be able to cobtrol and automate stuff. Keep up the great work.

  5. Hi Robin, Good news. I got the Arduino running your scripts to talk to the Arduino running Micheal Adams arduinoCMRI library. I used a digitalWrite in your script when the train entered the block. JMRI sensor lit up on the laptop, and out when I exited. I struggled a bit with the wiring but have drawn it all down and happy to email over if you want? I ran a ground between both Arduino’s not sure if that is good practice?
    I’ve not tried my sound loco yet to see if it detects without moving but will do and also watch the progress you make in this area for dcc loco’s without sound.
    I might think about combining the cmri.h library and code into your script and do away with the second arduino although having them separate has been good for fault finding.
    I think the solution you have come up with is great. The ACS712 boards are cheap along with the Arduino and also easy to get hold of on auction sites. It’s far better than spending hours soldering diodes or coils to make one occupancy detector. The layout I’m building will need about 25 so this seems a great way forward.
    In my test setup I’m only using one ACS712 but I’m going to get a few more and scale up a bit. I will probably need to think of my design a little better or could end up in a wiring mess!
    Thanks for all the help.

  6. I just stumbled upon your Block Occupancy papers with regards to Arduino and signals and see that you offer to send your complete sketch.

    I am a pre-novice at Arduino and sophisticated electronics and would appreciate it if you could send me the complete sketch and a wiring diagram for the system.

    Harry DeLoach
    408 Maplemere Lane
    Okatie, SC 29909

Leave a Reply

Your email address will not be published. Required fields are marked *