This is a continuation of High Density DCC Block Detection. In this post I’ll talk about putting all the pieces together to create a unified controller for module 1, lower level of the L&NC.
But first, lets finish out the block detection system.
Modifying Current Sensing Procedures.
I’ve talked extensively about current sensing and block occupancy detection throughout the blog. I started with ACS712 sensors because, in part, they work with both DC and DCC. Their flaw, if you will, is that the sensor produces so much noise that its hard to discern a reading below 20 mA (officially, you can’t); and impossible under 12 mA. That matters because 1) today’s DCC decoders typically consume 5 mA when idling with all functions off; so to detect an idle DCC locomotive, you have to be able to detect down to 5 mA; and 2) You can take advantage of the fact that DCC track power is always on, and use 10k resistor wheels to make rolling stock detectable. To do that successfully you have to be able to sense 1 mA of current flow at N scale voltage.
Since the L&NC is a DCC layout I ended up going with Current Transformers, a DCC only solution that, with the right ADC, can detect current flow under 1 mA. With my 14 bit Mayhew Labs Extended ADC Shield, I’ve successfully detected current flow just under 500 µA. The Root Mean Square algorithm for detecting current has been effective with both sensor types, but with CT sensors you can get an accurate read with fewer samples than you would need with the ACS712.
Note that you can successfully read CT sensors with the Arduino built-in ADC; I’ll be doing that in the next module and will share any tweaks needed to get satisfactory detection.
As previously mentioned, the Mayhew Labs Shield and the software library that goes with it is optimized to sequentially read the ports. With this ADC, the ports have to be configured immediately before being read. So, the read function supplied in the library does just that: reads one port and prepares the next. The upshot of that is when you start a read cycle you have to do a throw away read to setup your first port. Hence, reading all the ports you can in a single pass saves time and steps.
The code to do a single read pass in DIFFERENTIAL BIPOLAR mode on three channels:
// throw away read to prepare channel pair 0-1 extendedADCShield.analogReadConfigNext(0, DIFFERENTIAL, BIPOLAR, RANGE5V); // read prepared and prepare next channel pairs; 2-3, 4-5, 6-7 (unused) raw[0] = extendedADCShield.analogReadConfigNext(2, DIFFERENTIAL, BIPOLAR, RANGE5V) - blocks[sensor_map[row][0]].aqv; raw[1] = extendedADCShield.analogReadConfigNext(4, DIFFERENTIAL, BIPOLAR, RANGE5V) - blocks[sensor_map[row][1]].aqv; raw[2] = extendedADCShield.analogReadConfigNext(6, DIFFERENTIAL, BIPOLAR, RANGE5V) - blocks[sensor_map[row][2]].aqv;
Each command executes in about 5μs, so the read completes in 20μs; doing it this way limits throw away reads and the time they waste.
I created a special version of my current reading function for the Mayhew Labs Extended ADC Shield. The algorithm is identical to the current sensing algorithm I’ve previously posted, except that it works on three inputs at once. The full code (available on the github site) is specific to my situation but it does demonstrate how to adapt the base current sensing method I have posted to different hardware setups.
void readSensorRow(int row){ extern BLOCK_DEF blocks[]; float accumulator[3]; float raw[3]; float delta, rms; unsigned int count; unsigned long curMicros, prevMicros = micros() - sampleInterval; // set the address lines to access the row digitalWrite(pinA, addrBits[row].A); digitalWrite(pinB, addrBits[row].B); digitalWrite(pinC, addrBits[row].C); count = 0; for(int j = 0; j < 3; j++){ accumulator[j] = 0; } while(count < numSamples){ curMicros = micros(); if (curMicros - prevMicros >= sampleInterval) { prevMicros = curMicros; // prepare channel 0 extendedADCShield.analogReadConfigNext(0, DIFFERENTIAL, BIPOLAR, RANGE5V); // read/prepare subsequent 3 channel pairs; 0-1, 2-3, 4-5 raw[0] = extendedADCShield.analogReadConfigNext(2, DIFFERENTIAL, BIPOLAR, RANGE5V) - blocks[sensor_map[row][0]].aqv; raw[1] = extendedADCShield.analogReadConfigNext(4, DIFFERENTIAL, BIPOLAR, RANGE5V) - blocks[sensor_map[row][1]].aqv; raw[2] = extendedADCShield.analogReadConfigNext(6, DIFFERENTIAL, BIPOLAR, RANGE5V) - blocks[sensor_map[row][2]].aqv; for(int j = 0; j < 3; j++){ raw[j] /= SENSITIVITY; // convert to amperes } for(int j = 0; j < 3; j++){ accumulator[j] += (raw[j] * raw[j]); // sum the squares } ++count; } } for(int j = 0; j < 3; j++){ //https://en.wikipedia.org/wiki/Root_mean_square rms = sqrt((float)accumulator[j] / (float) numSamples); delta = abs(blocks[sensor_map[row][j]].aqc - rms); blocks[sensor_map[row][j]].occ = delta > DETECTION_THRESHOLD; } }
The “DETECTION_THRESHOLD” is .0006 (approx 600 µA); the “SENSITIVITY” factor is 3.3. The “sensor_map” associates specific sensors on a “row” of sensors with specific blocks. If you set up the sensors specifically so they will read in block numeric order then you wouldn’t need a “map.” In this case, the sensors are arranged according to the proximity of feeders, not numeric block order.
Constructing an Integrated Controller
So, with working Block Occupancy Detection, 9 turnouts and a few signals installed it was time to create the “controller” sketch for this module/level. In my experience to date, putting all the track functions together on a single controller is the most efficient way to ensure that all the pieces interact in a timely and correct manner. So, starting with an UNO, I built a controller stack with the UNO, an Ethernet shield, the Mayhew Labs Sheild and a connection board (a simple prototyping shield with screw terminals installed at all the pin locations, making connections easy and secure).
In essence, the controller sketch is an improved version (including my turnout class, and a signals class I’m developing) of the test loop sketch; the main difference being that there is more of everything. I compiled the sketch and had about 600 bytes of local memory left. That was pretty much the same as what I got on the test loop, so I thought it would work OK.
At first it seemed to work, but then became erratic and wouldn’t boot consistently. Uh oh. I’ve seen that sort of thing before: Memory problems! Clearly, the increased number of objects (turnouts, signals & blocks) was swamping memory resources at run-time. I’ve always known that 600 bytes remaining for local variables is right on the edge of what you can get away with, and my sketch was jumping the edge and into the void!
Time to Go MEGA
I frequently see people use MEGA’s because of all the additional pins it comes with. Since I use various pin multiplication methods, I don’t find that aspect of the MEGA compelling. But when you need more memory for code or data, the MEGA is unbeatable. When I compile the controller sketch for the MEGA, there are 6700 bytes of local memory remaining. Plenty!
Needless to say, everything works reliably now. I’ve tested it by letting it run for hours undisturbed, then running a train to see if things work. Perfect.
Given that there can be a lot of “pieces” to controlling trackage, its likely that I’ll need a couple more MEGA’s before I’m done. I’ll use UNO’s and NANO’s mostly except key places where the amount of track/turnouts/signals requires a more robust solution.
I’m really done with the knock-offs and am making the effort to buy genuine. Arduino prices are down a little, which helps. This genuine MEGA cost me a little over $30 (its up a little today; prices do fluctuate some) on Amazon which I think is fine for what it is and what I’m able to get out of it (current official list is $38). You still can’t beat the overall cost of the system; off-the shelf “model railroading” equivalents (BOD systems, signals, etc) will run double or more for the amount of track, turnouts and signals involved.
The Challenges of Stacking
Even with a MEGA, stacking shields can present problems because each shield needs to monopolize at least one pin (for SS [Slave Select] on the SPI interface), which means that pin cannot be used for anything else. Further, some shields come with “extras” that require an additional dedicated pin. The Ethernet shields I use, for example, include a MicroSD Card reader; so in addition to taking over pin 10 for the network SS, pin 4 is required for the MicroSD and becomes unusable for anything else. Some shields give you some ability to modify pin assignments. The Mayhew Labs Extended ADC shield also wants to use pin 10 for its SS, but you can change that and a couple of other pin assignments. Sometimes the change is made by closing a solder jumper. Other times you need a jumper wire to relocate a pin assignment. The Mayhew Labs shield requires a little of both but is fairly adaptable as a consequence.
Pin usage issues is one reason why the Adafruit 16 Channel PWM Driver for the turnouts is so important here: it uses the I2C wire interface, totally separate from SPI and the regular pins, so it runs happily along side the other devices without consuming pins. If you look at a recent UNO or MEGA, on the digital pin side after D13 there are a couple of additional slots labeled SCL and SCA; some shields (like the Ethernet shield) don’t even have pins in those locations (see the image of the Controller Stack, above). Consequently, I have to jumper the pins from the Mayhew Labs shield to the connection shield on top of the stack.
This leaves all the regular analog ports and several regular digital/pwm pins available for other uses, such as connecting to the DuinoNode network that controls lights and animation. On the Mega, there are dozens of remaining pins, both digital and analog should I ever need them.
Timing Issues
My “round robin” sensor reading function for this module reads one “row” of three sensors; there are 8 rows total, so the function is called for each row.
Why did I do it this way? Well, as previously discussed, it takes time to perform Block Occupancy Detection. With the Mayhew Labs shield and a sketch that is optimized for sequential reads, I’ve got the process of reading all 24 blocks down to about 1/2 second.
That’s great all things considered. But unfortunately, it dramatically slows down turnout motion because that big block of busy prevents running the turnout.update() method frequently enough. The first time I ran it without any changes it took an average of 30 seconds to move a turnout. Way too long.
So, I did two things to adjust and speed up turnout motion. 1) I added a movement increment option to the turnout class, so double or triple moves are possible if needed; and 2) I broke the block occupancy detection process down to one row at a time interleaved with turnout updates. The key loop() code looks like this:
for(int i = 0; i < 8; i++){ readSensorRow(i); for(int j = 0; j < NUM_TURNOUTS; j++){ turnouts[j]->update(currentMillis); } currentMillis = millis(); }
Turnout motion is satisfactory now
Coming Soon
I know quite a few people are interested in the turntable. While I’ve talked about the mechanism, I haven’t gotten into programming and control, so that’s next. That will probably be the last major post exclusively focused the lower level of module 1 as I move onto the adjacent modules and completing the lower level. I’m way over due for a video, too, and I think there’s enough working now to justify a little video tour.
Until then, Happy Railroading.
Hi Robin!
You are a great inspiration, and your postings are very informative. You have done an amazing job on block detection. I have a question regarding CT sensors and one of the calibration routines, adcRead. One of the input parameters is pin. In the routine you have a variable called next (set to 6). I don’t understand the code as in the initial throw away read you use pin, but in the real read you use 6. Why not use pin in both reads?
Kind Regards, Lars
Hi Lars,
I assume you are talking about this code:
float adcRead(int bank, int pin) {
int next = 6;
digitalWrite(pinA, addrBits[bank].A);
digitalWrite(pinB, addrBits[bank].B);
digitalWrite(pinC, addrBits[bank].C);
// setup the pin
extendedADCShield.analogReadConfigNext(pin, DIFFERENTIAL, BIPOLAR, RANGE5V);
// read it
float reading = extendedADCShield.analogReadConfigNext(next, DIFFERENTIAL, BIPOLAR, RANGE5V);
return reading;
}
The driver for the Mayhew Labs shield has a quirk where the pin parameter for the read routine is used to advance the internal pin pointer AFTER the “current” pin is read. The current pin is set in the prior line, which is the throw away read using the “pin” parameter — that line doesn’t actually read the “pin” — the next line with the “next” parameter is actually reading “pin”. In this function, we’re only reading one pin, not multiple pines in sequence. Here the “next” pin is a throwaway to satisfy the driver.
This code is only valid for the Mayhew Labs ADC Board. As far as I know, that board is no longer available.
Best, Rob
Hi Robin,
Thank you for the clarification. I’m very grateful to for finding out how the board works with its Quirks.
I have built your setup and it is working as expected.
I tried to buy more boards, but couldn’t select the board, and now I know why.
Thanks again!
Lars