Driving many outputs with Arduino C/MRI

A fun little side project of mine is Arduino C/MRI, a library that lets you easily connect your Arduino projects up to the JMRI layout control software, by pretending to be a piece of C/MRI hardware. Hence the name.

Hello World

The basic "hello world" example is fairly straightforward, wiring up a JMRI light to a physical LED on the Arduino board.

#include <cmri.h>
CMRI cmri;

void setup() {
  Serial.begin(9600); // make sure this matches your speed set in JMRI
  pinMode(13, OUTPUT);
}

void loop() {
  // 1: main processing node of cmri library
  cmri.process();
  
  // 2: update output. Reads bit 0 and sets the LED to this
  digitalWrite(13, cmri.get_bit(0));
}

It's easy enough to extend this example to handle 5, 10, even 15 outputs... if you have an Arduino Mega, you could have a LED on every pin and have 48 LEDs driven by the one board. But what happens when you want to have 49 outputs?

Expansion options

By default, Arduino C/MRI emulates an SMINI node. The real SMINI is a small standalone board that talks directly to the computer, and handles 24 input lines, and 48 output lines. This number is fixed. Since it can't be changed, we have three options for expanding our inputs and outputs:

  1. Emulate something more powerful than an SMINI,
  2. Emulate more than one SMINI node on the same Arduino,
  3. Run multiple Arduinos.

Lets look at each of these options in turn.

1: Emulate something bigger than an SMINI

The simplest method of handling more than 48 outputs is to emulate something bigger. There are two basic forms of C/MRI hardware:

If we configure a maximal SUSIC with 64 slots, each one with a 32 bit output card, that gives up 2048 outputs! Configuring that many outputs will take a while; I settled on 8x 32 bit output cards to test with.

The code changes to support a SUSIC are fairly minor:

#include <CMRI.h>

CMRI cmri(0, 0, 256); // address 0, 0 inputs, 256 outputs

void setup() {
  Serial.begin(9600); // make sure this matches your speed set in JMRI
  pinMode(2, OUTPUT);
  pinMode(3, OUTPUT);
}

void loop() {
  // 1: main processing node of cmri library
  cmri.process();
  
  // 2: update output. Reads bit 0 of T packet and sets the LED to this
  digitalWrite(2, cmri.get_bit(0));
  digitalWrite(3, cmri.get_bit(255));
}

This will give you 256 output lines from a single Arduino. To test, first set up a SUSIC node with 8x 32 bit output control cards:

Then create a couple of lights (Tools > Tables > Lights), one at address 1, and the other at address 256 (JMRI has 1-based indexes for its inputs and outputs). Toggle each one and you can see the appropriate output LED lighting up.

This raises an interesting side question though: Arduino's don't have 256 pins, so how do we address that many pins? Luckily I have a tutorial on doing just that: Addressing many LEDs with a single Arduino.

2: Emulating more than one SMINI node on the same Arduino

This is an interesting case. C/MRI is bus-based, which means the protocol is built from the ground up to address multiple nodes. There is nothing in the protocol however which requires physically separate nodes, so with a few tweaks of code, we're able to quite easily make our single Arduino respond to multiple addresses.

#include <cmri.h>
CMRI cmri0(0); // first SMINI, 24 inputs, 48 outputs
CMRI cmri1(1); // second SMINI, another 24 inputs and another 48 outputs

void setup() {
  Serial.begin(9600); // make sure this matches your speed set in JMRI
  pinMode(2, OUTPUT);
  pinMode(3, OUTPUT);
}

char c;
void loop() {
  // 1: main processing node of cmri library
  while (Serial.available() > 0)
  {
    c = Serial.read();
    cmri0.process_char(c);
    cmri1.process_char(c);
  }
  //cmri1.process();
  
  // 2: update outputs.
  digitalWrite(2, cmri0.get_bit(0));
  digitalWrite(3, cmri1.get_bit(0));
}

So what is going on here? First off, we now have two CMRI objects in our code. The first, cmri0 is set to address 0. The second, cmri1 is set to address 1. That means we now have two independent CMRI objects, one responding to address = 0, and the other responding to address = 1.

Then in the main loop we differ a little bit. Normally we would just call jmri.process() which would handle reading in and processing the serial data. However that won't work with more than one node, as the first node clears the serial buffer as it reads it, so the second node will never get any input. To solve this, we handle reading the serial data ourselves. First we read in the serial data, then each node in turn processes the same piece of data.

To set this up in JMRI, you need to add a second SMINI node with address = 1. This can be done in the JMRI preferences window by opening the 'Configure C/MRI nodes' window, entering 1 as the node address, and then clicking 'Add Node' to add the configured node.

Then when setting up outputs (e.g. in PanelPro, Tools > Tables > Lights > Add...) you can enter the extended address for the nodes. E.g.:

This can be verified in the CMRI > List Assignments window:

Here we can see that node 1 has the first output assigned.

Then in our code it is simply a matter of mapping CMRI outputs to digital outputs on our Arduino:

  digitalWrite(2, cmri0.get_bit(0));
  digitalWrite(3, cmri1.get_bit(0));

3: Running multiple Arduinos

Emulating multiple nodes inside of one Arduino is one possible approach to expanding your outputs. But what if you need your nodes to be physically separate? Maybe they are on different modules, or there is a very long distance between them which makes running dozens of wires less practical? In these cases it may make more sense to have multiple physical Arduino nodes, each emulating their own C/MRI node.

The obvious approach to setting this up would be to add multiple C/MRI nodes in JMRI, each on its own serial port. Unfortunately while it is possible to set this up in the options, it does not currently work (as of JMRI 3.5.3).

Therefore the only option if you wish to do this is to create a bus arrangement that is almost identical to the real C/MRI system. It consists of two parts:

Since the C/MRI protocol is a "don't ask, don't tell" type protocol, no node should speak unless specifically asked to, i.e. it is a polled protocol. That means that access control is taken care of by JMRI. It will either be broadcasting output control packets onto the bus, or it will be asking a specific node to transmit an input status packet. While the latter is happening no other packets are transmitted over the bus, ensuring no collisions between different nodes and packets.

Such a set up will be the focus of a future article.

In summary

We now know of three different approaches to controlling more than just the default 48 output channels. Each has their pros and cons. By combining approaches 1 and 2, we are able to address many dozens of nodes, each with 2048 digital lines. And, if we wanted to, we could combine this with approach number 3 and place the nodes on a bus, letting us spread SMINIs and SUSICs all over our railway.

Newer Older

Comments

Wednesday, Jun 11 2014, 6:59 PM Keith (from k_forster@hotmail.com) says...
Hi Adam, this is brilliant, its exaclty what i was looking for, just started to play around with this, im using the arduinos for occupancy detection. Im using reed switches embedded under the track and magnets in the fuel tanks of locos. i have put 22uF caps across the reeds to delay the closed circuit time so that the arduino has enough time to react (if the trains are going fast enough the arduino would not capture the switch) but all works fine now will be getting to grips with the scripts in jmri for full automation. Thanks again!
Sunday, Feb 9 2020, 5:13 PM Tom (from chalabi1@yahoo.com) says...
Adam. Super article and just what I needed. I'm building a layout w/ 6000 feet of track and over 320 turnouts. My problem was the CMRI limit of 48 outputs per node. Plan to use Neopixels for the 1000+ signals, but the node output limit would require many additional Ardrinos. With your code, I can now map the all the signals to the same Arduino via a additional Nodes. Hence the same Arduino that is driving my turnouts (via a servo driver board) and powering the frogs (via relays) can also run all the signals. Thanks so much! Tom