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.
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.
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!
One thought on “Adding Signals to the Test Loop, Part 2”