Block Occupancy Detection for DC and DCC, Part 4

Through the first three parts of this series (links to part 1, part 2 and part 3) I’ve experimented with and refined to a degree the use of ACS712 current sensors for block occupancy detection. The system works well in DC mode, lighting up whenever a running DC train enters or is powered up in a block; program logic “remembers” block states when power is off for control reasons. My DC locos typically start drawing 30 – 50  mA when the lamps come on.

In DCC mode, ultra low current detection becomes an issue and the off-the-shelf ACS712 sensors don’t meet every need. My BLI DCC/Sound Locomotive draws enough power to be detected with the current system in idle mode, all sound and lights off. However, some standard DCC decoders draw so little current in idle mode that they are not consistently detectable. Further, constant track power in DCC invites us to find a way to sense any object drawing some minimal increment of current — such as 1.1 mA for a 10KΩ resistor wheelset on an 11 volt (in N scale) feed. There are a number of obstacles to achieving 1 mA  sensitivity with ACS712 sensors.

The Challenges

First, there is a question of how low a current the ACS712 can resolve. Theoretically, because the chip responds in a ratiometric way to current input, any amount of current will provoke a proportional output response. In reality, the output of the chip is a little noisy, and the noise masks the low current response.

Allegro MicroSystems says that the best resolution with the optimum filter capacitor is 20 mA. There is some general agreement among internet cognoscenti that with some amplification of the signal from the ACS712, greater sensitivity should be possible.

Second, the 10 bit ADC built into most Arduino boards has a maximum resolution of 4.88 mV (5 volts / 1024 steps), limiting the minimum detectable current to 26 mA (4.88 mV resolution / 185 mV/A sensitivity). Even the 12 bit ADC found in the Due and Zero boards is not quite close enough, with a maximum resolution of 1.22 mV (5 volts / 4096 steps), resulting in minimum detectable current of 6.6 mA.

A Better Analog-to-Digital Converter

Of the two problems, this is by far the easier problem to solve by using an external ADC with a 14 or 16 bit resolution. A 14 bit ADC has a maximum resolution of .305 mV, which is a good resolution for consistently detecting current at the 1.6 mA level or higher; a 16 bit ADC would provide 76 µV resolution and current detection in the mid to high µA range.  For those wanting to detect a single resistor wheelset, 16-bit is the way to go.

For my purposes and goals, I’m going to see how it goes with 14 bits so I’m not pushing resolution past the point of usefulness at this stage. My thinking is that 1.6 mA should be sufficient to detect a pair of resistor wheels. Two 10kΩ resistors in parallel have a resulting resistance of 5kΩ, which at 11 volts will draw 2.2 mA (@16 volts, 3.2 mA). Assuming I can overcome the noise problem to the point where 14 bits of resolution provides accurate current detection, the final step would be to go to 16 bits.

I looked at several ADC products and settled on the Mayhew Labs Extended ADC Shield, which is also available at Amazon. I chose this for several reasons: it supports 8 inputs, comes in a shield format and (perhaps most importantly) offers enough bandwidth to be able to sample multiple sensors at a reasonable rate.

Mayhew provides a code library for reading a variety of sensor types through the shield. It is fairly straight forward to use, and the shield is a very good quality piece of gear. I have found other ADC boards, but none with the unique capabilities of this one.

Mayhew Labs Extended ADC Shield

Sparkfun Low Current Sensor

Sparkfun sells a breakout board as a low current sensor using the ACS712 sensor chip plus an operational amplifier to amplify the signal. This is the only commercial solution of its type I’m aware of, so I bought one to check out and test. The schematic for the board can be seen here.

SparkFun Low Current Sensor Breakout – ACS712. This image is from an older version; different trimpots are used in the current version.

Although the circuit is promising, this board is really an experimental item and not suitable for production use on a model railroad. Part of the problem is that it does not provide — and is not drilled for — standard screw terminals for attaching the input current source, adding additional challenges to deployment. On the input side (I+ / I-) it is drilled for a pair of header pins and two larger holes with contact pads. The control side (VCC, OUT, GND) is drilled for standard headers.

Secondly, because this is an experimental board, it has two trim-pots for configuring the Op-amp.  Sounds great, but in practice this setup doesn’t work well. Add sketchy instructions for setting and using the board and I think most will find this board impractical for large scale use, though an interesting experimental tool.

Learning By Failing

So when the board arrived, I soldered on some header pins and hooked up the sensor to an UNO with the Mayhew ADC Shield, then attempted to configure and use the board. Here are the setup instructions in their entirety:

“To calibrate, first set the output offset to the desired level (with zero current on the sense lines, read output with a DVM). Then with a known current input (a 100mA limited supply works well for this), set the output deflection with the gain pot. Sensitivity is then calculated as (Vref – Vdeflect)/(current input).”

The “output offset” is (presumably, though not literally stated) adjusted with the vref trim-pot.  But what “level” is “desirable” for this application?  The second instruction is even weirder, since you are not going to short out a power supply by directly attaching it to the sensor without a load, and its the load that determines the current flow. I’m sure they meant to say something like “supply a load with a known current draw.”

I tinkered fruitlessly with the trim-pots for hours, testing various theories about how it should work and getting nothing intelligible out of the sensor. The gain pot is straightforward enough, resulting obvious changes to readings as the trim-pot is adjusted. Its the vref adjustment that mystified me and seemed to have no logic. I  did notice that when setting it there was a point where the output would suddenly, but only briefly, drop to O. That zero point is so finely specific that it is essentially impossible to set the trim-pot at that level; it always ends up off one way or another. That was a clue, but I did not yet understand.

Back to School

I found a nice tutorial about Op-amp circuits and, after a while started to understand the circuit and comprehend the problem. If you don’t understand Op-amps (who does among those of us not trained in electrical engineering?), check out the tutorial before reading further here.

The Op-amp circuit Sparkfun uses is a “voltage subtractor,” the intent of which is to  subtract the input voltage from the reference voltage and amplify the difference. This is the right choice—I think—but the implementation is wrong, at least for my purposes.

Unique ACS712 Properties

Many sensors that one might use with an Arduino are straight forward linear output devices where the output voltage ranges from 0 to 5volts in proportion to an input (light, temperature, etc.).

What makes the ACS712 different is that it senses both polarity and current of the measured input.  The “quiescent voltage” we measure when calibrating a sensor is the sensor’s “zero” point: ideally VCC/2 — midway between 0 and VCC (nominally 5 volts).  When the polarity is one way the measured current is represented by a value above VCC/2; when the polarity is the opposite, the sensor produces a value under VCC/2.

That means that we are only interested in the offset between the sensor output and its quiescent value. The software calibration routine I’ve been using seeks to get the most accurate average mid-point value possible to enhance the accuracy of the offset measurement. Amplifying that offset should be the route to gaining sensitivity.

Shouldn’t vRef be VCC / 2?

It may well be that the vref adjustment has value when measuring DC current with a fixed polarity. However, I’ve seen no math or theory in support of that proposition. For measuring DCC current, which is an 8kHz alternating polarity wave form, there is only one correct reference voltage for comparison to the ACS712 output: VCC/2.  So, Sparkfun had the right idea using a trim-pot as a voltage divider circuit to generate the reference voltage, but the trim-pot inhibits setting vref to VCC/2 because it is too imprecise to set the two resistor legs to exactly the same value.

What Now?

The Sparkfun sensor experiment was a bust in that it did not produce a usable result.  But it did help me research and think about what it is I need an operational amplifier to do in order to successfully amplify the ACS712 signal. Sometimes we learn more from failure than we do from success.

The next step is the set up an Op-amp with a fixed reference voltage of VCC/2, then continue experimenting from there.  I have the parts. In a few weeks, if time permits (between work and our ailing Beagle, time is tight right now), I should have some sort of result.

Adding Signals to the Test Loop, Part 2

Using shift register/darlington driver nodes for attaching lighting (and other devices) to the layout is easily justified by the conservation of precious Arduino pins. But that’s not necessarily the most important benefit of this approach.

Duino node for station lighting.

Duino nodes for station signal and lighting on the Test Loop.

The principal advantage of organizing lighting control into chains of shift registers—with or without Darlington Drivers to sink instead of source current (at this level it doesn’t matter what the shift register is attached to)—is to create an addressable architecture for lighting and animation.

What do I mean?

An addressable architecture is a system where individual objects can be identified by an “address” that provides sufficient information, in proper context, to allow the object to be accessed (my definition). An “address” should be a direct access mechanism and should not require conversion to a different form in order to use it. However, an address does not necessarily have to be complete if the context within which it is used has “knowledge” of the missing address information.

By that definition a pin number is a valid address. But, since we want to control more objects that we have pins, we need to think on a larger scale.

Arrays

Going beyond pin numbers, an address could be an offset (index) into an array. This works for objects attached to a shift register because you need to have memory set aside to represent state of the shift register; the values in memory are used to generate the stream of bits that sets the shift register state.

An 8-bit shift register could be represented in memory as an array of 8 bytes, either as plain 8-bit values or as boolean values (boolean is the better idea), occupying one byte for each bit on the register:

byte MyRegister[8];
or
bool MyRegister[8];

Having done that, every object on the shift register can be referred to by its position in the MyRegister array, a number from 0 to 7.  When you want to know or change the state of a particular bit on the shift register, you access the bit via its array index, making the index an address.

What about the Pins?

I am cheating in a sense. I’m relying on the fact that the first shift register in a chain has to be attached to three pins, you need to know which pin is which, and we can embed that information in the method that pushes bits out to the shift register. With that housekeeping squared away, all we need to do to turn shift register outputs on or off is maintain the array representing the shift register in memory, using the array indices as addresses for specific objects.

Here is an example method (derived from the Arduino Shift-Out tutorials, and used in the Roundhouse to run stall lamps) that writes to shift registers, using a static byte array to hold the register state information:

const int clockPIN = 3;
const int latchPIN = 4;
const int dataPIN = 5;
void registerWrite(int output, int state) {
  static byte outputStates[8];
  byte bitsToSend = 0;

  // update the outputStates array
  outputStates[output] = state;
  
 // Set bits according to the outputStates Array
 for(int i = 0; i < 8; i++){
     bitWrite(bitsToSend, i, outputStates[i]);
 } 
 // turn off the output while shifting bits
 digitalWrite(latchPIN, LOW);
 // shift the bits out
 shiftOut(dataPIN, clockPIN, MSBFIRST, bitsToSend);
 // turn on the output
 digitalWrite(latchPIN, HIGH);
}

This is what I mean by proper context. Here the context is a known “base address” for the shift register that never changes: the LatchPin, ClockPin and DataPin used to access it. Once context is established with a method that knows the “base address” and writes to the shift register, we no longer need to worry about that aspect of the problem and can supply the unique part of the address — the position of the output we wish to change — as an argument to the registerWrite method. In this instance, the address supplied to the method should be considered a relative address, because it is relative to the static portion of the address stored in the method code.

This simple approach works great for a shift register or two, but gets unwieldy when trying to manage the state of multiple shift register outputs with a single array. Further, memory is a precious resource and using a byte of memory to represent the state of a single bit on a shift register is inefficient at best. For a layout with hundreds of controllable lights for signals, crossings, street lights, building lighting and whatever else you can think of, something more sophisticated is called for.

The Data

So lets start with a better way to hold the values of the 8 bits of a shift register; I call shift registers “nodes”:

typedef struct nodeState {
  byte pins;
};

The 8 bits constituting a single byte of memory represent the 8 bits of a node. This is a big improvement in both memory efficiency and execution efficiency, since the pins byte can be sent directly to the target register without any processing.

Next we need a type to hold all information relevant to a chain of nodes:

typedef struct nodeGroup {
  byte clockPin;
  byte latchPin;
  byte dataPin;
  byte numNodes;
  nodeState *nodes;
};

Now we have all the data we need to handle a chain of shift register nodes in one place. Notice how the nodeState type is used. Once instantiated,  the *nodes element will point to an array of bytes, one for each node.

Notice something else: the door is now open to run multiple, distinct chains of nodes easily by creating a nodeGroup data instance for each chain. This confers even more freedom to structure resources for the convenience and logic of the sketch.

What’s The Address?

So that is how the nodes are attached and accessed. Now we’re ready for an address form that will work with this architecture:

typedef struct nodeAddress { 
  byte chain;
  byte node;
  byte pin;
};

With all node data organized into nodeGroup structures, the nodeAddress type provides what amounts to an absolute address to any resource defined in this way. An absolute address contains all necessary parts of an address, and is processed by methods without having any hidden address elements embedded in the method code.

Station and Signal 5

Practical Application

The test loop has one chain of four nodes.  For that sketch, a single node group is instantiated this way:

nodeState ndata0[] = {0,0,0,0};
nodeGroup nodeGroups[] = {{7, 6, 5, 4, ndata0}};

Here I’ve defined an array of nodeGroups with one member. The node group uses pins 5, 6 and 7 on the Uno, and has four member nodes. An array of nodeState structures, one for each node, is defined as ndata0[], and incorporated into the nodeGroup via the ndata0 pointer.

Signals on the test loop are defined by a different data structure that includes this element

nodeAddress addr;

capturing the base address of a particular signal. When the sketch is ready to set the state of a signal, it passes the nodeAddress to the node setting method, like this:

nodeSet(signal.addr, nodeBits);

All manipulation of the nodes can be accomplished with 4 methods:

void nodeWrite(struct nodeAddress addr, byte state) {
  // write the state to the pin bit defined by nodeAddress addr
  bitWrite(nodeGroups[addr.chain].nodes[addr.node].pins, addr.pin, state);
  nodeRefresh();
}
void nodeSet(struct nodeAddress addr, byte state) {
  // set the pins element with a byte value
  nodeGroups[addr.chain].nodes[addr.node].pins = state;
  nodeRefresh();
}
byte nodeGet(struct nodeAddress addr){
  // get the pins element for a node
  return nodeGroups[addr.chain].nodes[addr.node].pins;
}
void nodeRefresh(){
  // Shift out current node data, one node group at a time
  for(int i = (NODE_GROUPS - 1); i>=0; i--) {
    // shift all bits out in MSB order (last node first):
    // Prepare to shift by turning off the output
    digitalWrite(nodeGroups[i].latchPin, LOW);
    // for each node in the group
    for(int j = (nodeGroups[i].numNodes - 1); j>=0; j--) {
      shiftOut(nodeGroups[i].dataPin, nodeGroups[i].clockPin, MSBFIRST, nodeGroups[i].nodes[j].pins);
    }
    // turn on the output to activate
    digitalWrite(nodeGroups[i].latchPin, HIGH);
  }
}

The nodeSet method used by the signal system—primarily because a signal requires an output for each aspect (red, green, yellow)—sets the pins byte for a random node in one operation.

NodeWrite sets a single output bit on any random node.

NodeGet retrieves the current pin byte for a random node. The signal system uses this method to retrieve the bits on a signal node, before modifying the values and applying them with nodeSet.

NodeRefresh writes the pins bytes to the nodes.

There is, of course, more going on with the signals on the test loop. Deciding what aspect a signal should show at any given time is a matter of logic and data. In the next post in this series I’ll do a deep dive into my first take on making signals operational within an Arduino environment.

Until then, happy railroading!