Juggling Arduino Sensors With cocoOS

Can you have your cake and eat it too? With Arduino yes you can — but only if you eat very small crumbs, one at a time.

Here’s a problem that I see often in Arduino devices for IoT: The Arduino device is connected to multiple sensors (temperature, light, motion, …) but it responds slowly when the sensor data changes.

Why? Think of the cake. It has lots of goodness inside – like an Arduino device with many sensors, each sensor ready to stream out plenty of good useful data.

But a microcontroller like Arduino Uno is terrible at streaming out huge volumes of sensor data (or cake) in realtime. The solution is to aggregate the sensor data over time into small chunks for more efficient processing. And switch quickly from one sensor to the next, so that no sensor is starving from lack of processing time (and cake).

In this article we’ll explore a solution for juggling multiple Arduino sensors, using an open source library for cooperative processing: cocoOS. All code in this article may be found on GitHub:


Let’s Build A Multitasking Arduino Device

Suppose we are building an Arduino device for environment monitoring that’s represented above, using the following components:

  1. Arduino Uno
  2. Temperature Sensor (BME280 I2C)
  3. Humidity Sensor (BME280 I2C)
  4. Network Transceiver for sending the data to an IoT network (like Sigfox)

We would like to monitor the temperature and humidity sensor values independently, and send a network message if any of the values exceed a certain threshold:

    IF temperature > 27.0
OR humidity > 60.0
THEN send message

To do this on Arduino we’ll normally use a loop like this: (I have omitted the actual checking of values)

What’s wrong with this? A couple of issues…

  1. Everything runs sequentially: Reading of sensors, checking of sensor values, sending of messages
  2. We can’t check the sensors again until the message sending has been completed. Which may take a few seconds, depending on the network.
  3. We are forced to poll the temperature and humidity sensors at the same rate. If we decide that temperature is more important than humidity, it’s not possible to check the temperature 20 times a minute and the humidity 10 times a minute.
  4. Actually with the above code we can’t even be sure how many times a minute we are polling each sensor, because it depends on the temperature polling delay + humidity polling delay + sending delay + … We need a proper timer here!

From https://github.com/lupyuen/send_altitude_cocoos/blob/master/main.cpp

Now Let’s Build It With cocoOS

There is hope! Check out the code above from send_altitude_cocoos

  1. The polling of each sensor runs in its own Sensor Task. Each task can be managed independently, with its own code and data.
  2. The code sets the polling interval to 500 milliseconds for each sensor — the sensor is checked every 500 milliseconds, regardless of other running tasks (ideally). We could have set a different polling interval for each sensor, so it’s possible to check the temperature more often than humidity.
  3. Each Sensor Task may be assigned a Task Priority. In the above code the temperature task has higher priority than the humidity task. Another task for displaying sensor values (not shown above) runs on the lowest priority. The Display Task will never disrupt a Sensor Task that’s busy reading a sensor. Very sensible right?

This is made possible with the excellent cocoOS Library by Peter Eckstrand. It uses timers and cooperative processing to make sure that the Sensor Tasks all play nice with each other. Without requiring you to write complicated Arduino coroutines that simulate multitasking.

Yet the cocoOS library is lightweight enough to run on the Arduino Uno. I have tested up to 4 Sensor Tasks and 1 Display Task (total 5 Tasks) in my send_altitude_cocoos code.

    task_create(sensor_task, tempContext, 10, ...

Creating a new Sensor Task is really that easy, using the task_create function from cocoOS. This line of code will start a new task that runs the sensor_task function. tempContext is the Sensor Context for the temperature sensor, which includes the Sensor object for fetching the sensor value. Let’s take a peek at the sensor_task()function…


From https://github.com/lupyuen/send_altitude_cocoos/blob/master/sensor.cpp

The Sensor Task Loop

Above is the sensor_task() function that every Sensor Task runs. It’s an endless loop that…

  1. Reads a sensor (specified by the Sensor Context in context)
  2. Idles a while (poll_interval) for other tasks to run
  3. And repeats.

Function sensor_task() has this structure:

Summarised from https://github.com/lupyuen/send_altitude_cocoos/blob/master/sensor.cpp

Every task in cocoOS must call task_open() ... task_close() Even if the task runs an endless loop.

Some resources in Arduino are not meant to be used by multiple tasks concurrently. The code here uses the temperature and humidity sensors from a single BME280 module that runs on a single I2C Bus on the Arduino.

Since the BME280 module and the I2C Bus may not be used concurrently by 2 tasks, we use a Semaphore to lock the module/bus and ensure exclusive access.

sem_wait() … sem_signal() are the Semaphore functions provided by cocoOS for locking resources. More about Semaphores later.

The Sensor objects contain code for reading the sensors — take a look at temp_sensor.cpp and humid_sensor.cpp. The code for each sensor now lives in its own source file. No more messy interleaving of sensor code!


From https://github.com/lupyuen/send_altitude_cocoos/blob/master/sensor.cpp

Sending Messages To Tasks

In any multitasking system, the tasks need to communicate in order to perform a function together. To display sensor data in the Sensor Task, we send the sensor data to another task, the Display Task, so that we may continue the sensor processing without waiting for the Display Task to complete.

With cocoOS, we use the msg_post() function to send a message. It takes two parameters: the unique ID of the task that will receive the message (i.e. display_task_id) and the message to be sent (i.e. a DisplayMsg object).

From https://github.com/lupyuen/send_altitude_cocoos/blob/master/display.cpp

To receive a message, just use the msg_receive() function provided by cocoOS. The code above shows the task loop that the Display Task runs to receive messages and display them.


What’s A Semaphore?

That’s an actual Semaphore above. Really! Ask a Computer Scientist if you wish to know why an algorithm for locking and unlocking access to shared resources is named after a person waving flags (which explains why nobody enjoys Computer Science).

You see the spirals in the vending machines that regulate the dispensing of paid merchandise? A Semaphore algorithm works just like a spiral in one of the merchandise slots.

In each slot, at any time, only ONE packet of chips stays in front, ready to be dispensed. When that packet is dispensed, the packet behind is pushed to the front for dispensing next.

That’s exactly like a bunch of sensors awaiting processing. The Arduino Uno can only process one sensor at a time (just like having one packet of chips in front). All other sensors will have to queue up and wait for their turn.

Now if you drop a huge bag of coins really quickly into the vending machine, you’ll get a non-stop supply of chips. That’s the same effect we want when switching quickly between sensors – generating an endless stream of data processed from multiple sensors.

From main.cpp and sensor.cpp in https://github.com/lupyuen/send_altitude_cocoos

Remember that our sensors reside on the same BME280 module, connected on a single I2C bus? That’s why we need a Semaphore to regulate the two sensors (sensor = bag of chips) and ensure that only one sensor (bag of chips) is accessed via BME280/I2C at any time. The code above shows how you can use Semaphores in cocoOS:

  • sem_counting_create() creates a Semaphore with 2 parameters: how many tasks are allowed to queue for the Semaphore (maxCount), and how many tasks the Semaphore should release and run concurrently (initValue, which is usually 1).
  • sem_wait() waits for the Semaphore to be available and locks it. Just like the vending machine: the bags in the queue wait for their turn to shift to the front, one at a time.
  • sem_signal() releases the Semaphore and makes it available for the next task. Just like the vending machine: one spin of the spiral moves the next bag of chips to the front.

How do we know that the Semaphore works?

If you look at the sample log, you’ll observe that all 4 Sensor Tasks are politely queueing up for their turn at the Semaphore.

Once a task has locked the Semaphore, no other task is allowed to poll the sensor.


Compiling The Arduino Code

The code in this article comes from a fully functioning application for the Arduino Uno named send_altitude_cocoos— it’s in the link above. It assumes that you have a BME280 module connected to the Uno via the I2C Bus, but you may comment out the BME280 code and use the simulated values instead (search for Simulated sensor in the files *_sensor.cpp)

The code above compiles under the Arduino IDE, Arduino Web Editor, and Visual Studio Code with the PlatformIO extension installed. You’ll need to install the following libraries:

  • Time: Download the Timelibrary by Michael Margolis from the Arduino Library Manager
  • BME280: Download the BME280 library by Tyler Glenn from the Arduino Library Manager
  • cocoOS_5.0.1: Download from http://www.cocoos.net/download.html, unzip and move all files in inc and src to top level. Zip up and add to Arduino IDE or Arduino Web Editor as a library.

Normal Arduino sketches have a setup() and a loop() function. Ours is different — it starts at the main() function defined in main.cpp. Because our sketch defines the main() function, the setup() and loop() functions will be ignored.

Edit the file platform.hand uncomment the following line so that you’re using the right features for this article…

#define CONFIG_ARTICLE1  //  Uncomment to support Article #1

Check that the other #define CONFIG_ARTICLE... lines are commented out.

Arduino IDE
Arduino IDE

To open send_altitude_cocoos in Arduino IDE:

  1. Browse to https://github.com/lupyuen/send_altitude_cocoos
  2. Click Clone Or Download then Download Zip
  3. Unzip the downloaded file
  4. Look for the send_altitude_cocoos_master folder. If you see another send_altitude_cocoos_master folder inside that folder, use the inner folder.
  5. Rename the folder to send_altitude_cocoos
  6. Open the folder. Double-click the Arduino sketch send_altitude_cocoos.ino
Arduino Web Editor
Arduino Web Editor showing the source tree, source window and compiler log

The Arduino Web Editor is a browser-based development tool that stores and compiles your code in the cloud. It talks to your Arduino device using a driver that’s installed on your PC.

To load the send_altitude_cocoos code directly into Arduino Web Editor, just click the link below:

https://create.arduino.cc/editor/lupyuen/9a440293-242e-42f4-a386-6540c938e048/preview

If you haven’t tried Arduino Web Editor, click the link and test it out! I highly recommend it for beginners. My IoT students seem to have fewer problems with the Arduino Web Editor than with the Arduino IDE (e.g. unable to detect Arduino on USB, incorrect file extensions).

The link above contains a snapshot of the send_altitude_cocoos code, not linked to GitHub. If you wish to use the latest code from GitHub:

  1. Browse to https://github.com/lupyuen/send_altitude_cocoos
  2. Click Clone Or Download then Download Zip
  3. Unzip the downloaded file
  4. Look for the send_altitude_cocoos_master folder. If you see another send_altitude_cocoos_master folder inside that folder, use the inner folder.
  5. Rename the folder to send_altitude_cocoos
  6. Zip the renamed folder into send_altitude_cocoos.zip
  7. In Arduino Web Editor, click Sketchbook in the navigation bar and upload send_altitude_cocoos.zip
Visual Studio Code with PlatformIO
Visual Studio Code + PlatformIO showing the source tree, multiple source windows and Serial Monitor

Visual Studio Code with PlatformIO is a client-based IDE like the Arduino IDE. But the auto-completion and code navigation features make complex Arduino programming so much simpler.

I recommend Visual Studio Code with PlatformIO (the free version) if you’re doing serious Arduino programming with many modules. I used it to develop the code for send_altitude_cocoos.

PlatformIO requires the send_altitude_cocoos code to be located in the src subfolder. Follow the instructions below to create the src subfolder and the symbolic links to the source files (see “Create Source File Links for PlatformIO”):

https://github.com/lupyuen/send_altitude_cocoos/blob/master/README.md

The cocoOS library folder should be copied into the lib subfolder.

Within the cocoOS folder, create an src subfolder. All cocoOS source files (*.h, *.c) should be moved into the src subfolder.


What’s Next?

You have now learnt the core cocoOS functions for building a program that juggles multiple sensors on a constrained device like the Arduino Uno. You’ll notice that we haven’t actually sent any sensor data to the cloud yet.

That will be explained in my next article — we’ll use cocoOS to send sensor data to the Sigfox network while processing new incoming sensor data. Then we’ll unleash the full power of multitasking with cocoOS! Check out the article here…

If you prefer to run cocoOS on the more commercial STM32 Blue Pill hardware platform, check this article…


Bonus Tip: Conserving Dynamic Memory By Using Flash Memory

Memory is extremely scarce on the Arduino Uno. Here are some tricks I used to get my multitasking application to run on the device…

Sketch uses 16732 bytes (51%) of program storage space. Maximum is 32256 bytes.
Global variables use 1454 bytes (70%) of dynamic memory, leaving 594 bytes for local variables. Maximum is 2048 bytes.

When compiling the Arduino sketch, look for the message above. On PlatformIO, look for this:

It shows…

  • The Arduino Uno has 32 KB of onboard Flash Memory (program storage space)
  • 51% of the Flash Memory is currently used
  • The Uno has 2 KB of onboard Dynamic Memory (i.e. RAM)
  • 70% of the Dynamic Memory is currently used

After Arduino programs have been compiled, the resulting machine code and data are copied to Flash Memory. The code and data are remembered permanently in Flash Memory even when you power off the Arduino.

When you power on the Arduino, the code begins execution using Dynamic Memory. When you allocate objects (on the heap) or call functions (using the stack), you consume Dynamic Memory.

Therefore Dynamic Memory is very important – if you run out of Dynamic Memory while allocating objects or calling functions, your Arduino will silently crash and reboot.

There’s a trick to move some data out of Dynamic Memory and into Flash Memory…

Serial.print("This uses Dynamic Memory");
Serial.print(F("This uses Flash Memory"));

See the difference? The F() function tells the compiler to use Flash Memory instead of Dynamic Memory for storing the string. This works only with strings that don’t change.

Unfortunately the F() trick only works with Serial.print() and a few other functions. To write your own functions that support F(), use C++ function overloading with __FlashStringHelper like this:

Adapted from https://github.com/lupyuen/send_altitude_cocoos/blob/master/display.cpp

Which explains why you see plenty of code like debug(F(…)) in send_altitude_cocooos.