The Raspberry Pi 3 has ability to rapid fire pulses out of its GPIO. It's surprising how fast! In other words, it can handle a lot of tasks.
If the only thing the Pi is expected to do is spin a stepper or servo motor really fast, this is good news. Theoretically, the Pi can send pulses to the motor driver faster than most any drive could accept them. The problems start when the Pi is expected to control and monitor numerous devices, all while maintaining exact timing. There just isn’t time to do it all exactly when you want it done.
The solution to the problem is to offload some tasks to another device. That is exactly what we did here with a Raspberry Pi 3 and two Arduino UNO’s (Figure 1 & 2). The Pi and the UNOs are connected via an i2c bus. The Pi functions as the bus master and the UNOs function as slave nodes. With an arrangement like this, each slave node is told what to do with a uniquely addressed packet of data. They handle control of the motors long after the Pi has sent them their instructions, leaving it free to do something else. If the Pi was handling everything itself, it would need to use clever algorithms and more advanced coding methods like timer interrupts to simultaneously control the two motors.
A logic level converter allows the 5-volt UNOs to safely communicate with the 3.3-volt Raspberry Pi 3. Also, notice the two pull-up resistors on the SCL and SCK lines of the i2c bus. There are several articles on the internet that investigate how the value of these resistors effect the rise time an i2c signal. 10KΩ is the value we had at hand and over the short distances involved and the relatively slow speed of 400Kbps, it worked perfectly. You may want to research this topic for your own projects.
Figure 1: The wiring diagram.
Figure 2: Raspberry Pi 3 connected to Arduino UNOs via an I2c bus. Note the red logic level converter board on the breadboard. It is required for bidirectional communications between the 5v UNO and the 3.3v Raspberry Pi.
The code explained a bit.
(Code is attached to this post, link at the bottom)
A short length of Python code runs on the Raspberry Pi. This code relies on the smbus library to automates the transmission of data out onto the i2c bus. The function bus.write_i2c_block_data(address, cmd byte, array) takes three arguments. The address argument is the address of the slave node you want to transmit to. The cmd byte is a special reserved byte that always gets sent first. The array argument is where you put the main bulk of the data you want to send, up to 32 bytes. It will be tx’d immediately after the cmd byte is sent.
Each UNO slave uses the wire.h library for handling of the i2c interface. You will see a lot of print commands sending data to the serial-monitor. These can be removed without consequence. They are used for keeping an eye on what the code is doing.
Every few seconds the python code addresses each slave node in turn and gives it instructions. The first bit of the cmd byte to indicate the direction of motor rotation (Figure 3). The remaining bit are unused although, they can be used to communicate anything you want. The 4 bytes in each of the arrays communicate the number of steps for the motor to take and the time to wait between them.
Figure 3: The first bit of the cmd byte indicates the whether the UNO should spin the motor clockwise or counter-clockwise. The other bits aren’t used for anything and are ignored by the code.
Data is stored in a byte array because the i2c bus transmits data a byte at a time. An unsigned byte can only store values from 0 – 255. To get around this bottleneck, we treat two concurrent array elements like they are the high and low byte of a 16-bit integer value (Figure 4). These bytes are sent out and then recombined into an integer variable at the slave node using this clever bit of code from the C51 compiler site. It allows the access and manipulation of the high and low bytes of an integer variable with a couple of macros. You can see the macro definitions and how it appears in the Arduino sketch in Figure 5. They perform this trick by creating pointers to the bytes within an integer value.
There are other ways to do this but this is a very quick way of accomplishing the task.
Figure 4: Integer values are split up into a byte array.
Figure 5: These macros from the C51 site allow us to manipulate the high and low bytes within an integer variable.
You aren’t obligated at all to use any of the hardware or the libraries presented here. You don’t even have to use i2c, another serial protocol can be used. This is a concept that can be applied across systems and platforms. Also, our example uses Arduino UNOs, but there is no reason that a smaller board like an Adafruit Trinket or an Arduino Mini couldn’t be used in its place. A robot with an UNO at each motor might look a little silly.
For truly exact timing, a real-time clock could be added to the i2c bus. The master and slave nodes can then read the exact time and use that data to precisely time events.
The protocol we devised for this example of using the cmd byte in conjunction with 4 more bytes from an array to communicate step count, time between steps and direction of rotation can be modified to meet your requirements. Each of your slave nodes could be doing far more than spinning a motor and as such, may require 20 or more bytes to effectively communicate instructions. Whatever form that protocol takes is up to you. Do not feel obligated to do exactly as we did. This article is only meant to demonstrate a technique as simply as possible and to give the reader a solid starting point.
Finally, our example uses two slave nodes but there is no reason why more couldn’t be added. A lot more in fact. 127 nodes at the minimum and with some adjustment to the addressing scheme, there is no reason you couldn’t put 1000 devices on the bus. Not that you’d need that many. If you do, post a comment. We all are going to want to know what the heck you are up to.
Explanation of video:
What you see here are the two UNO i2c nodes controlling their respective motors after they have received instructions on how to do so from the Master node. Those instructions where sent, received and decoded before the motors even started to move. If the raspberry Pi was left to control these motors, it would be occupied with that task for the duration of each motor movement. By offloading the job of controlling the motors, the Pi has more resources to dedicated to something else, like running a GUI.
Have a story tip? Message me at: cabe(at)element14(dot)com