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:
- Emulate something more powerful than an SMINI,
- Emulate more than one SMINI node on the same Arduino,
- 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:
- An SMINI which has 24 input lines and 48 output lines,
- An SUSIC which is configurable; it has 64 slots, and each slot can be either an input or output card. Each input and output card can address up to 32 lines.
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.:
- First output on the first node:
0001
, always shortened by JMRI to1
. System name isCL0001
(CMRI, Light, node 0, output number 001) - First output on the second node:
1001
. System name isCL1001
(CMRI, Light, node 1, output number 001) - 23rd output on the 4th node:
4023
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:
- Nodes – each node is an Arduino running Arduino C/MRI. Instead of a regular TTL USB/Serial connection, the serial input and output are routed to an RS485 bus. Each node has a unique address.
- Bus master – Whereas normally an Arduino would connect directly to the PC via USB, when running in a bus setup, it is the bus itself that needs to connect to the PC. This is done with an RS485-USB adapter.
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.
Comments