Introduction
The Raspberry Pi is a great little device, probably used by most to serve as a media center running XBMC. I had one laying around for exactly this purpose, but I never forced myself to set the whole thing up properly. Then it dawned on me that I could experiment with the GPIO pins as well. This article describes my latest project, which aims at letting two Pi's communicate (chat) to eachother through the GPIO pins: a text message is entered in Pi 1 which sends it through the GPIO pins to another Pi, where it is then shown on screen.
I wanted to design a protocol that allows only for 1 bit to be sent at a time. In such a protocol, the most natural type of encoding is some sort of Morse-code, but my program allows for any policy that can be used to encode a string to a bitstream (and decode it of course). I could of course have gone for 8 bits at a time, which would have allowed to send one ASCII character each time, but I chose not to for 2 reasons:
- I only have 1 Pi so when testing, I have to let it talk to itself. This means that I need both an input and output for every bit I want to send. This alone is 16 pins out of the 17 GPIO pins my Pi Model B provides. As will become clear in the remainder of this article, I need a few more for synchronization. It just won't fit...
- What's the challenge in sending entire characters at a time? Morse is way cooler!
Background
Most GPIO tutorials use the Python library to control the pins. While this is perfectly fine and all, I have chosen to build my application in C++ using the WiringPi library for C and C++. This is mainly because I'm much more familiar with C++ than with Python, which means I actually dare to show off the C++ code. I'd probably be too afraid to do this with any Python code I write... The downside of writing a program in C or C++ for the Pi is that you have to compile it, which can take pretty long on the 800Mhz ARM chip.
On my Pi, I'm using a minimal Debian (Raspbian) image that I downloaded here. I then upgraded to Jessie to be able to use C++11 to its full extent in GCC 4.9 (Wheezy comes with 4.6).
The result will be encapsulated in a class called PiChat
, but I won't share the entire interface right away. This would only distract from the interesting stuff. The snippets below will be (parts of) member functions of this class, hence the PiChat::
prefix.
Communication Protocol
The first thing I had to design, was some protocol that could send bits from one Pi to another. The challenge in designing this protocol is that you can never assume that both Pi's are at the same point in code. As with every multi-processor application, we need to be able to synchronize. Synchronization turned out to be the major issue, but let's keep things simple for now. Just assume that we have some sync()
function that guarantees that both devices are synchronized when it returns. We can now think of what our send- and listen-functions should look like. In the following, MLI (Message Line In) is the name of the pin that receives the bits, whereas MLO (Message Line Out) is the one that sends them out.
Sending
The pre-code in the snippet below will make sure that there is a std::vector<bool>
available (let's call it code
), containing the encoded message in the form of a bitstream. All we need to do is send out the bits one by one:
void PiChat::send(string const &str)
{
write(SRO, 1); for (bool bit: code)
{
write(MLO, bit); sync(); sync(); }
write(SRO, 0); sync();
}
The first line sets the SRO (Send Receive Out) channel to high. This channel is connected to the SRI (Send Receive In) channel on the other end, and serves to notify the other device that we want to send some data. The peculiar thing about this piece of code is the double synchronization. The first sync is to make sure that the sender has set the output correctly, whereas the second is to ensure that the receiver has read the input.
Listening
Keep in mind that the previous snippet is executed simultanuously with this one, triggered by a signal on SRI. An empty std::vector<bool>
will serve to store the code, which will be decoded in the post-code.
std::string PiChat::listen()
{
while (true)
{
sync();
bool bit = read(MLI); code.push_back(bit); if (!read(SRI)) break;
sync(); }
}
The sharp-eyed reader will now notice the sync-discrepancy in both snippets. The catch is that, when the sender breaks out of the loop, the receiver will have started the next iteration and is waiting in the first sync. Therefore, there has to be a sync outside the send-loop. When the receiver now checks the SRI, it will discover that the sender has stopped sending: the code can be decrypted now!
Synchronization (1)
Time to reveal the guts of sync()
, which has been a black box up to this point. To make it work, we need additional channels to communicate, which we call the sync-lines. Each device has two sync-lines: one for input and one for output, let's call them SLI (Sync Line In) and SLO (Sync Line Out). What follows is my first implementation, which did not work! However, I think this is the best way to explain why I did what I eventually did, so bear with me.
The idea is to set a value on SLO, and wait for SLI to show the same value. This value alternates between 0 and 1 to make sure that subsequent syncs are distinguishable. Simple enough, right? Here it is:
void PiChat::sync()
{
static bool syncVal = 0;
syncVal = !syncVal;
write(SLO, syncVal);
if (!wait(SLI, syncVal))
throw Exception<TimeOut>("connection timed out");
}
The static variable syncVal
is remembered on subsequent calls, alternating between 0 and 1 before being written to SLO. When the value is written to the output, the input SRI is monitored in the wait()
function which returns as soon as SRI takes on the value equal to syncVal
. Actually, wait()
takes a third argument that specifies time-out interval in seconds, which defaults to 2. An exception is thrown when wait()
fails.
The implementation of wait()
is straightforward, and uses the traditional C timing function gettimeofday()
. Yes, I know, C++11 has a fantastic <chrono>
facility to do this the modern way. For now, the implementation looks like this:
bool PiChat::wait(int pin, int val, int timeout)
{
timeval t0, t1, diff;
gettimeofday(&t0, NULL);
while (read(pin) != val)
{
if (timeout > 0)
{
gettimeofday(&t1, NULL);
timersub(&t1, &t0, &diff);
if (diff.tv_sec >= timeout)
return false;
}
}
return true;
}
Synchronization (2)
As I already mentioned, the above does not work! The reason for this lies in the subsequent syncs. If you paid attention, you'd know that the sync-values alternate, so a device might do something like this:
- SLI is in state 0
- ...
- Set SLO to 1, wait for SLI to become 1
- ...
- Set SLO to 0, wait for SLI to become 0
- ...
The effect I observed was that one of the devices was so fast in setting SLO first to 1 and then back to 0, that the other device didn't even notice the change. It got stuck in step 3, waiting for SLI to become 0 when in fact it already had. I think this might be due to the fact that I'm testing all this on a single Pi, forcing it to hyperthread both applications. But to be sure, I wanted to overcome this problem without building in delays. The solution I found was an additional sync-channel.
In stead of two sync-channels (SLI, SLO) per device, I know have 4: SLI1, SLO1, SLI2 and SLO2. By not only alternating sync-values, but also sync-channels, it is guaranteed that every change is observed by both parties. The implementation has now changed to the following:
void PiChat::sync()
{
static int lineSelect = 0;
static bool syncVal[2] = {0, 0};
static Pin const syncLineIn[2] = {SLI1, SLI2};
static Pin const syncLineOut[2] = {SLO1, SLO2};
lineSelect = !lineSelect; int s = (syncVal[lineSelect] = !syncVal[lineSelect]);
write(syncLineOut[lineSelect], s);
if (!wait(syncLineIn[lineSelect], s))
throw Exception<TimeOut>("connection timed out");
}
Parsing Pins and Read/Write
You might have noticed the other two black boxes in the code-snippets above: read()
and write()
. They take pin-numbers like SLI and MLO, which have been mapped onto the real pin-numbers. Actually, they have been mapped onto the WiringPi pin-numbering convention, which you can read all about right here. The read()
and write()
don't do anything but apply the mapping:
void PiChat::write(Pin pin, bool value)
{
digitalWrite(d_pins[pin], value);
}
bool PiChat::read(Pin pin)
{
return digitalRead(d_pins[pin]);
}
The values inside d_pins
(d_
for datamember, like Stroustrup's m_
) are of course not hardcoded! They have been read from a pin-file at run-time. This way you can set up your Pi whichever way you want and specify the details in a file. This means that the file needs to be parsed, which I do with my little Parser
class, which does nothing but read lines, look for the =
-sign and try to extract the strings left and right of the equality sign to some type (it's a class template, so it can be any type as long as operator>>(std::istream, T)
is defined for it). In the case of our pin-file, the result is a std::map<std::string, int>
. The strings of the map are then compared to a hardcoded set of strings that represent our channels ("SRI", "SRO", etc. (these are strings)). The final result is a std::vector<int>
, that can be indexed by members of the Pin-enumeration (SRI, SRO etc. (these are enum constants)). I won't bother with the details but you are free to ask for them.
A pin-file might look like this:
SRO = 8
SLO1 = 9
SLO2 = 12
MLO = 7
SRI = 0
SLI1 = 2
SLI2 = 13
MLI = 3
Morse Code
Because the protocol allows for just one bit at a time, the already optimized Morse-code seems pretty reasonable. The PiChat
class-interface however accepts any policy, derived from the abstract class EncoderBase
, that provides an encode()
and decode()
member to convert a std::string
to std::vector<bool>
and vice versa.
In my implementation of the morse-encoder class (aptly named MorseEncoder
), I used the standard morse-signals, where a dot (dit) is represented by a single 1, and a dash (dah) by two 1's. A dit (1) and dah (11) are refered to as units. Units are separated by a single 0, and multiple units make up a character, separated by double 0's. Multiple characters make up words, which are separated by triple 0's. For example, the message "SOS SOS" would in morse be: "...---... ...---...". And encoded in 0's and 1's:
101010011011011001010100010101001101101100101010
To keep things simple, I only used the 26 letters of the alphabet plus 10 digits. This can of course be arbitrarily extended to whichever character set you need. Again, I will not discuss the implementation of MorseEncoder here, but its implementation is available in the archive and I'm happy to answer any questions regarding it!
Tying it Together
I guess we are about ready to finalize the project, and set up a prompt that waits for a user to input some text that he/she wants to send. However, it should also listen for incoming messages...
This calls for threads, which have been standardized in C++11. We only need 2 threads:
void PiChat::prompt()
{
d_running = true;
thread sendThread(&PiChat::waitForUserInput, this);
thread receiveThread(&PiChat::waitForMessage, this);
sendThread.join();
receiveThread.join();
}
As you can see, the first thread (sendThread
) waits for user input, which it will then try to send. The other thread (receiveThread
) waits for messages from the other side. A little complication that could arise is when both users try to send a message at the same time. This is actually handled in the precode of the send()
and listen()
functions, which I omitted. A flag that is shared between these threads is set to indicate whether a message is being sent/received. If this is the case, the other thread will simply wait until the message-line is free again.
Finally, the code for both threads:
void PiChat::waitForUserInput()
{
while (true)
{
try
{
cout << ">> " << flush;
string message;
getline(cin, message);
if (cin.eof())
break;
else if (message.empty())
continue;
send(message);
}
catch (Exception<TimeOut> const &e)
{
throw; }
catch (exception const &e)
{
error(e.what());
}
}
cout << endl;
d_running = false;
}
void PiChat::waitForMessage()
{
while (d_running)
{
while (d_running && !read(SRI)) {}
if (d_running)
{
try
{
string message = listen();
cout << "\n\t\t" << message << "\n>> " << flush;
}
catch (Exception<TimeOut> const &e)
{
throw; }
catch (exception const &e)
{
error(e.what());
}
}
}
}
The entire code is uploaded and available in the archive! I've compiled with both GCC 4.8 and 4.9. I know that there are issues in 4.6 because some C++11 syntactical features haven't been implemented yet in that version. A makefile is included to make life easy for you. Also, don't forget that you need to install the WiringPi library first!
Request
I think I'll be able to test my program on 2 Pi's pretty soon, but if anyone is going to try this out in real life by hooking up two Pi's, please let me know if it works! I know it works on just the one, and I'm pretty confident it will on two separate ones, but you never know until you know, you know?
Thanks for reading!
History
August 20, 2014: First draft
August 22 2014: Fixed some typos, uploaded images to codeproject