Daisy-chaining SPI devices
SPI, or Serial Peripheral Interface, is a 3-wire interface standard which can transfer data at high speeds with a simple protocol, much simpler than I2C. The signals have in the past been called MOSI (Master Out Slave In), MISO (Master In Slave Out), and SCK or SCLK (Serial Clock), plus a fourth signal named SS (Slave Select). At the time I first wrote this, the electronics industry was phasing out the use of the terms master and slave for this interface. Arduino has now standardized on replacing master and slave with controller and peripheral respectively. As a result, MISO is now CIPO, and MOSI is now COPI. The signal formerly known as Slave Select (SS) is now Chip Select (CS) as well.
CS (Chip Select) is an active-low signal which activates which peripheral is to be controlled during a data transfer. To indicate this is an active-low signal, it will be written here as CS*.
In a standard SPI configuration where one controller talks to multiple peripherals, the COPI, CIPO, and SCK signals are bussed to all the devices. Each device though requires a dedicated CS* signal so that only one peripheral at a time responds to the other three signals. This means that the number of available pins which can serve as CS* signals limits how many SPI peripherals the controller can talk to. Figure 1 shows three peripherals, labeled as nodes, which are connected to a controller. COPI carries data from the controller to the peripherals, while CIPO flows from the peripherals back to the controller. When the clock signal SCK is toggling, data bits are transferred across COPI and CIPO at the same time. As you can see, using three nodes requires three separate CS* signals from the controller.

The code running on each processor sets whether its SPI interface will act as a controller or a node. This will determine the directions of the SPI signals for each device.
Signal | Controller | Node |
COPI | output | input |
CIPO | input | output |
SCK | output | input |
CS* | output | input |
For my track sensor network, I am using an alternative design, where the peripherals are daisy-chained, making them look like a single peripheral to the controller. Most SPI devices transfer 8 bits at a time; daisy-chaining three 8-bit devices will operate as if they are a single 24-bit device. Figure 2 shows how this is done. You will notice that I only need one CS* signal which is connected to all of the nodes.

If you follow the flow of data from the controller’s COPI (D11, an output), you will see it feed into Node 2’s COPI (D11 again, but an input). As bits are clocked into Node 2’s COPI, earlier bits are transferred from Node 2’s CIPO (D12, an output) to Node 1’s COPI (D11 again), and from Node 1’s CIPO to Node 0’s COPI. The bits stored in Node 0 then pass back to the controller’s CIPO. So for the controller, COPI is an output, while CIPO is an input. For the three nodes, the reverse applies.
So if you transfer 3 bytes (24 bits) of data from the controller, the first 8 pass through Node 2, then Node 1, finally ending up in Node 0. The next 8 bits will pass through Node 2 and stop in Node 1. The final 8 bits will only make it as far as Node 2. Meanwhile, all 24 bits that were stored in the three nodes will be clocked into the controller.
Using the daisy-chained configuration, I can connect (almost) as many nodes to one controller as I’d like. In practice, due to limitations in fanning out the bussed signals from the controller (CS* and SCK), and in keeping the number of nodes manageable for the software, I will be limiting each controller to 16 nodes.
I2C over servo cable
I2C was designed to work over relatively short distances, limited by the capacitance between the conductors in the cable. I am planning to use I2C to connect modules for several of the systems on the project, such as the switch machine controllers, the banks of relays in the block switching matrix, and trackside signals. Since some of these systems will have their modules spread out, I need to test a few different forms of I2C cabling to see what works where.
For example, on the bus connecting the switch machine controller modules I am considering using standard white/red/black servo wire for the I2C bus. Because the controller modules will receive their 5 volt power independently of the bus, the servo cable only needs to carry the SDA (data), SCL (clock), and ground signals. To keep the capacitance between the SDA/SCL wires and the ground balanced, the center wire (red) will be the ground wire. I’ve arbitrarily decided to make the white wire SDA and the black wire SCL. So far this has worked out fine in testing over 2 meters (about 6.6 ft) of servo wire. If I have problems with driving longer runs of servo wire, I have some I2C bus extender chips (P82B715) which will increase the distances I can run the bus between modules.
Because the block switching controller modules will all reside in a pair of rack-mounted units, the distances between modules will be short enough that I won’t need the bus extenders there.
UDP network communications
Most network communications over Ethernet cable is done using one of two protocols, TCP and UDP. TCP provides message acknowledgements and error-correction, while UDP has neither unless the application using UDP provides them. You can find a detailed comparison of the two protocols here. The bottom line is that UDP is more suited to real-time communications, at the expense of communications reliability.
For a steady stream of data, say from a sensor or to a display device, timing is usually more important than completeness of the data samples. An example of this is my throttle system. Sometimes though, it’s important that every message (datagram in UDP terms) be received and processed correctly. For example, dropping a command intended to position a turnout, or to switch a track block to another throttle’s control, would be very bad news for operations. For this type of application, you need either the reliable communications of TCP with its real-time performance issues, or a protocol layered on UDP to provide reliability and performance. A draft standard for a protocol named Reliable UDP or RUDP exists, but it’s more complex than I want to implement on Arduinos, so (in true Maker fashion 😁) I have rolled my own. For now I will call it UDP-E, or UDP with Echo.
NOTE:
The following section has been updated to describe an improved design of my UDP-E protocol. The main changes are:
- Messages are now divided into two sections: a 6-byte header followed by one or more bytes of payload data.
- The signature is now the first 2 bytes of the header.
- The sequence number is now the next 2 bytes following the signature in the header.
- Following the sequence number in the header is a 16-bit (2 byte) checksum which is used to verify correctness of the header in received messages.
The UDP-E protocol, like UDP in general, can be used to carry different types of messages, using the signature to distinguish the message types. The sequence will usually increment with each message transmitted, except when a message must be repeated because the sender failed to receive an echoed copy of the message before a timeout period expired. This is an improvement over using UDP alone, which has no built-in mechanism for a sender to detect that a message did not arrive at the receiver.
The checksum is computed using ones’ complement arithmetic by adding the other header fields as 16-bit values, and setting the checksum field to the negative of the sum. The checksum is verified by adding all three header fields (signature, sequence, checksum), again using ones’ complement addition. A correct sum will be 16 bits of all 1’s (1111 1111 1111 1111
in binary). If the sum is anything else, then there is one or more bits in the header that are wrong, and the message will be discarded.
I have been writing a test implementation of UDP-E where 10-byte payload data is sent from a Raspberry Pi 400 over an Ethernet connection to an Arduino UNO microcontroller with an Ethernet interface installed on it. Both devices connect to each other with Ethernet cables and a network switch in between.
The table below shows the format for the messages being transmitted by the test program, written in C++, which runs on the Raspberry Pi 400. I am also testing a Python version of the program, since I can more easily create graphical user interfaces (GUIs) for applications in Python than C++ by using the tkinter Python package.
byte offsets | field name | description |
0-1 | signature | 16-bit value to identify message type |
2-3 | sequence | 0 to 65,535 ( 0000 0000 0000 0000 to 1111 1111 1111 1111 in binary) |
4-5 | checksum | 16-bit negative sum of above fields using ones’ complement math |
6 and up | payload | specific to each message type, 10 bytes in test program |
Each time the Arduino receives a message (datagram in UDP speak) from the central processor (Pi 400), it first echoes the exact message back to the central processor before acting upon it. In an actual application, the payload would contain a command for the Arduino to act upon. The command is executed only if all of the following conditions are met.
- The message is the correct length (16 bytes for test version).
- The signature field is correct (base-16 value
424C
for this test). - The ones’ complement sum of all three header fields is a 16-bit value of all one-bits (in base 16,
FFFF
).
Meanwhile, the central processor listens for the echoed message after it transmits each one. If no echo is received after a preset wait time, the same message (with the same sequence byte) is sent again. This process repeats up to some preset number of times until the exact same message that was sent is received back. If the expected echoed message is still not received after, say, five retries, this means there’s a hardware or software issue that must be resolved.
There may be applications where I will need to get data from the Arduino instead of just sending commands to it. That could be done in either of two ways: (1) reverse which device (Arduino or Pi) sends the original messages and which sends the echoed messages, or (2) have the Pi send the message which requests the data from the Arduino, and have the Arduino embed the returned data in the payload portion of the echoed message. The payload portion of the echoed message would not match that of the sent message, but the headers would match.
I will most likely go with option 2 for most applications, polling for the data by the Pi, because option 1 would force the Arduino to deal with sending retries when needed, which can interfere with its real-time operations.
When I get a chance, I plan to upload to my GitHub repository copies of both the controller code running on the Raspberry Pi (C++ and Python versions), and the Arduino code which receives and echoes back the messages sent by the controller.