Working with Arduino* sketches on Intel® Galileo, or Intel® Edison you might run into situations where you would want to add some functionality coming from the underlying Yocto* Linux OS running underneath. Which brings us directly to the subject of this blog: How to efficiently communicate between those two worlds.Let's start to define some criteria we will try to meet in the following
Criteria
- no communication on disk (SD card, eMMC) in order to reduce disk wearing and to improve performance
- communication triggered by events, i.e. in particular we don't want to periodically check a status but to get informed by an event and idle otherwise
Inter Process Communication (IPC) on Linux
An Arduino* sketch running on Intel® Galileo, or Intel® Edison is in fact just a Linux process running in parallel to other Linux processes. As there is a full grown Linux running on the boards we can also use standard means for inter process communication (IPC) between the Arduino* process and native process(es). There are various IPC methods on Linux. One out of those "memory mapped IPC". In essence it means that IPC processes share the same memory. Which means any modification from any process sharing that memory region will be at once visible to all the other processes. It also matches our first criteria to not write communication data on disk but operate only on memory.
Mutexes and condition variables
Having a shared memory leads to several questions like:
- how to make sure only one process is operating on the shared data at a given time? (synchronization)
- how to notify the other process(es) once data has been modified? (notification)
In the following we will look into those 2 questions one after the other. We will make use of "mutexes" and "condition variables" which are both contained in the POSIX threads (Pthreads) library available on Linux.
Synchronization - Mutex
A mutual exclusion (mutex) is a standard concept provided by probably any modern multitasking OS. In this blog we won't describe concepts and details here but only give high level information on POSIX threads (Pthreads) mutexes. For more information you should consult textbooks on operating systems (e.g. Tanenbaum, Woodhull: Operating Systems 3rd ed. Pearson 2006) or search online.
As the name says the Pthreads library focuses mainly on threads programming. However, it also offers powerful commands applicable to processes. Moreover the Arduino* IDE for Intel® Galileo, and Intel® Edison resp supports pthreads (i.e. links to the pthreads library) out of the box thus providing an easy integration. Hence, it seems a natural selection to use Pthreads for our purpose.
Typically a mutex makes sure a certain critical region of code is only accessed by one thread. As we are dealing with processes here we will use mutexes in a way that only one process can proceed a pthread_mutex_lock request in the code. Any other process will be set to sleep by the OS until pthread_mutex_unlock is called on the mutex after which the OS will wake all other processes requesting pthread_mutex_lock.
In pseudo code:
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
This has to be done in the same way for locking write as well as for locking read accesses as otherwise a read could access "half updated" data. In the next section we come to a further Pthreads concept which provides notification on data changes
Notification - Condition variable
Similar to the mutex concept where a process trying to access an already locked mutex Pthreads provides the concept of condition variables. The condition variable allows a thread, or in our case a process to request to be put to sleep until waked up through the variable. This is done by the function pthread_cond_wait.
Mutex and condition variable combined yield following pseudo code:
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond_variable, &mutex);
pthread_mutex_unlock(&mutex);
The other process need to unlock the mutex and signal the change by calling pthread_cond_signal. This will wake up the sleeping process.
For further details pls check textbooks, or online tutorials resp on Pthreads. In the next section we will come to a sample implementation.
Implementation
some explanations:
Note
The sample code below is provided under the MIT license.
There are three files below:
- mmap.ino: sketch to be put onto Arduino* IDE
- mmap.cpp: native process sending data
- mmap.h: header file - same file to be used on Arduino* IDE as well as Linux native
I.E. on the Arduino* side you should have a folder which contains "mmap.ino" and "mmap.h" in your Arduino* sketches directory. On the Linux native side you should have a folder containing "mmap.cpp", and "mmap.h".
In order to run the sketch just open the "mmap" sketch in the Arduino* IDE and upload it to the right board (Intel® Galileo Gen 1, Intel® Galileo Gen 2, or Intel® Edison respectively). On the native side the Intel IoT devkit on https://software.intel.com/iot comes with a cross compiler, or alternatively the Yocto* Linux coming with Intel® Edison as well as the SD card Yocto* image on https://software.intel.com/iot or Intel® Galiileo come with a C++ compiler pre-installed. Using the pre-installed compiler you would need to run
g++ mmap.cpp -lpthread -o mmap
in the folder you put mmap.cpp and mmap.h. It will generate a binary you can execute as follows
./mmap {0,1}{0,1}
where "{0,1}" stands for either 0 or 1. I.E. "./mmap 00" would switch both LEDs off, whereas "./mmap 11" would switch both LEDs on. Some additional output to the serial monitor [CTRL+SHIFT+M] on the Arduino* IDE, and to the console you start the Linux native process show in addition the data which has been set.
mmap.ino
#include "mmap.h"
using namespace std;
struct mmapData* p_mmapData;
int led8 = 8;
int led13 = 13;
void exitError(const char* errMsg) {
string s_cmd("echo 'error: ");
s_cmd = s_cmd + errMsg + " - exiting' > /dev/ttyGS0";
system(s_cmd.c_str());
exit(EXIT_FAILURE);
}
void setup() {
int fd_mmapFile;
fd_mmapFile = open(mmapFilePath, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
if (fd_mmapFile == -1) exitError("couldn't open mmap file");
if (ftruncate(fd_mmapFile, sizeof(struct mmapData)) == -1) exitError("couldn' modify mmap file");
p_mmapData = static_cast<struct mmapData*>(mmap(NULL, sizeof(struct mmapData), PROT_READ | PROT_WRITE, MAP_SHARED, fd_mmapFile, 0));
if (p_mmapData == MAP_FAILED) exitError("couldn't mmap");
pthread_mutexattr_t mutexattr;
if (pthread_mutexattr_init(&mutexattr) == -1) exitError("pthread_mutexattr_init");
if (pthread_mutexattr_setrobust(&mutexattr, PTHREAD_MUTEX_ROBUST) == -1) exitError("pthread_mutexattr_setrobust");
if (pthread_mutexattr_setpshared(&mutexattr, PTHREAD_PROCESS_SHARED) == -1) exitError("pthread_mutexattr_setpshared");
if (pthread_mutex_init(&(p_mmapData->mutex), &mutexattr) == -1) exitError("pthread_mutex_init");
pthread_condattr_t condattr;
if (pthread_condattr_init(&condattr) == -1) exitError("pthread_condattr_init");
if (pthread_condattr_setpshared(&condattr, PTHREAD_PROCESS_SHARED) == -1) exitError("pthread_condattr_setpshared");
if (pthread_cond_init(&(p_mmapData->cond), &condattr) == -1) exitError("pthread_mutex_init");
pinMode(led8, OUTPUT);
pinMode(led13, OUTPUT);
}
void loop() {
if (pthread_mutex_lock(&(p_mmapData->mutex)) != 0) exitError("pthread_mutex_lock");
if (pthread_cond_wait(&(p_mmapData->cond), &(p_mmapData->mutex)) != 0) exitError("pthread_cond_wait");
if (p_mmapData->led8_on) {
system("echo 8:1 > /dev/ttyGS0");
digitalWrite(led8, HIGH);
}
else {
system("echo 8:0 > /dev/ttyGS0");
digitalWrite(led8, LOW);
}
if (p_mmapData->led13_on) {
system("echo 13:1 > /dev/ttyGS0");
digitalWrite(led13, HIGH);
}
else {
system("echo 13:0 > /dev/ttyGS0");
digitalWrite(led13, LOW);
}
if (pthread_mutex_unlock(&(p_mmapData->mutex)) != 0) exitError("pthread_mutex_unlock");
}
mmap.cpp
#include "mmap.h"
void exitError(const char* errMsg) {
perror(errMsg);
exit(EXIT_FAILURE);
}
using namespace std;
int main(int argc, char** argv) {
struct mmapData* p_mmapData; int fd_mmapFile;
fd_mmapFile = open(mmapFilePath, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
if (fd_mmapFile == -1) exitError("fd error; check errno for details");
p_mmapData = static_cast<struct mmapData*>(mmap(NULL, sizeof(struct mmapData), PROT_READ | PROT_WRITE, MAP_SHARED, fd_mmapFile, 0));
if (p_mmapData == MAP_FAILED) exitError("mmap error");
if (pthread_mutex_lock(&(p_mmapData->mutex)) != 0) exitError("pthread_mutex_lock");
if (argc == 1) {
cout << "8:0" << endl;
cout << "13:0" << endl;
p_mmapData->led8_on = false;
p_mmapData->led13_on = false;
}
else if (argc > 1) {
int binNr = atol(argv[1]);
if (binNr >= 10) {
cout << "8:1" << endl;
p_mmapData->led8_on = true;
}
else {
cout << "8:0" << endl;
p_mmapData->led8_on = false;
}
binNr %= 10;
if (binNr == 1) {
cout << "13:1" << endl;
p_mmapData->led13_on = true;
}
else {
cout << "13:0" << endl;
p_mmapData->led13_on = false;
}
}
if (pthread_mutex_unlock(&(p_mmapData->mutex)) != 0) exitError("pthread_mutex_unlock");
if (pthread_cond_signal(&(p_mmapData->cond)) != 0) exitError("pthread_cond_signal");
}
mmap.h
#ifndef MMAP_HPP
#define MMAP_HPP
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <iostream>
#include <string.h>
#include <stdlib.h>
#include <cstdio>
#include <pthread.h>
static const char* mmapFilePath = "/tmp/arduino";
struct mmapData {
bool led8_on; bool led13_on; pthread_mutex_t mutex;
pthread_cond_t cond;
};
#endif