Experimenting With Message Passing Software Modules for Arduino Programming

By Phil Kane

I've been fascinated by the idea of being able to build software applications in a way similar to how electronic circuits are built using ICs. That is, by connecting discrete software components together via clearly defined communication pathways.

I've begun to use the Arduino development platform to play around with ideas for implementing this type of component-based system. I'd like to see if it's possible to create useful Arduino-based applications this way. A library of reusable software components should promote faster prototyping and development. Also, Arduino was designed from the beginning to make it as easy as possible for those new to programming to get up to speed developing projects as quickly as possible. The ability to create useful projects from a collection of software black boxes without having to know much about the underlying code would be another step in that direction.

The following describes my first project, which involved creating a small set of very simple modules and a runtime system to host them. Regarding the latter, the Arduino run time environment – as simple as it is – turned out to be perfect for this first attempt. Also, for this project, I use functions, arrays and static variables (for keeping track of state within a module) rather than C++ classes.

Simple Software Module System Description

The diagram in Figure 1 shows two modules, A and B, connected by channels ab and ba. The channels are unidirectional. Channel ab carries data from module A to module B. It is connected to the output port of module A (OutPorta) and the input port of module B (InPortb). Likewise, channel ba carries data from module B to module A. As the diagram indicates, ports are connection points for channels.

Software Modules
Figure 1: Software Modules

Modules and Ports

Modules are designed to perform a single task (e.g., get a value from an analog pin, monitor the state of a digital input pin, perform a calculation, etc.) or a small group of simple tasks. They are implemented as functions. A module declaration looks like this:
void Mod1(int InPort1[],  ..., int InPortN[], int OutPortN[], ..., int InPortM)
{
           module code
}
The parameter field consists of a list of ports. Data is passed between module functions in the form of messages. Ports are entry and exit points for messages. As you can see from the previous code example, ports are implemented as one dimensional array declarations.

Messages

In this implementation, a message is a one dimensional array with space for a single data element of type int. The following declaration is used to create a message:

int msg[messageSize];

Where messageSize is a predefined constant.

Channels

Channels are one dimensional arrays of type int. In this implementation, they contain space for one message. The following declaration is used to create a channel:

int chan1[channelSize];

Where channelSize is a predefined constant.

Connecting Modules

To connect two modules, you associate a channel with an output port of the sending module and an input port of the receiving module. For example, to create a one way connection between mod1 and mod2 using channel m1_m2 you replace mod1 OutPort parameter and mod2 InPort parameter with mi_m2. To create a bidirectional connection between the two modules, use a second channel (say m2_m1) to replace the InPort parameter of mod1 and the OutPort parameter of mod2. The following code examples illustrate how to declare modules, create the connecting channels and connect the modules.

Declare the modules:

void mod1(InPort, Outport)
void mod2(InPort, OutPort)

Create the channels:
   int m1_m2[channelSize];  // connects output port of mod1 to input port of mod2
   int m2_m1[channelSize];  // connects output port of mod2 to input port of mod1

Then, in the execution loop (Arduino loop function), you "wire up" the modules by placing
the names of the connecting channels in the appropriate positions in the parameter list of
each module. For example:

void loop
{
     .
     .
     mod1(m2_m1, m1_m2);
     mod2(m1_m2, m2_m1);
     .
     .
}


The Runtime Environment

From the last code example in the previous section you may have already concluded (correctly) that an application is really just a sequence of function calls to modules listed in the main loop. Starting with the first module, the loop function calls each module in turn. A module performs its task (or a portion of it) and then returns execution to the loop function which calls the next module. After the last module executes, the process is repeated again. All of the action takes place in the modules.

Example Applications

The following examples illustrate how to create simple applications using a library of pre-existing components. There is no need to write any code to create instances of modules or channels. As you will see in the example descriptions, there is a set of macros to do this. Source code for the function that implements a module is generated each time the macro that instantiates that module is invoked. However, each function has a unique name – the name assigned to the module instance on invocation.

Build a Flashing LED

Parts List:

Qty.
Part Description Manufacturer Part Number Component Name
1
Arduino Uno R3 DIP Edition A000066
1
LED Uni-Color Green LG3330 LED1
1
Resistor Carbon Film 220kΩ CF1/2W221JRC R1
1
Resistor Carbon Film 10kΩ CF1/4W103JRC R2
1
SPST Switch EVQ-PAG04K S1

This is the Arduino version of the classic "hello world" program. It simply turns an LED on and off at the rate of approximately once per second. I've added an extra twist to this application by enabling you to turn the flasher on and off with a switch.

Flashing LED Figure 2: Flashing LED

As you can see in Figure 2, the project includes two software components, "Switch1" a digital input module and "Blinker", a square wave generator module.

The action of the circuit is as follows: The digital input module Switch1 monitors Arduino digital pin 5. When hardware switch S1 is open, the voltage at pin 5 is low, as a result, module Switch1 sends LOW values to the control port of module Blinker, essentially turning it off. When switch S1 is closed, the voltage at pin 5 goes high and module Switch1 sends HIGH values to Blinker, turning it on. Blinker then sends a stream of alternate HIGH and LOW values to Arduino digital pin 10, causing LED1 to flash on and off.

Flashing LED On Flashing LED On

Digital Input Module

The digital input module continuously reads a digital pin and sends the value (HIGH or LOW) to the output port. The module used in this example has internal debounce.

Digital Input Module Figure 3: Digital Input Module

You use the create DigitalInputModule macro command to create an instance of the digital input module. As shown in the code example below, you specify the name of the module and the number of the digital pin to associate with the module.

createDigitalInputModule(Switch, 5)

Square Wave Generator Module

The square wave generator is a digital output module. When it receives a HIGH at the control port (Cntrl) it generates a stream of HIGHs and LOWs spaced at approximately equal intervals. The interval between transitions is specified at the time the module is created. This stream continues until it receives a LOW at the control port.

Square Wave Generator Module Figure 4: Square Wave Generator Module

To create an instance of the square wave generator module, you use the createSquareWaveGenModule macro command. As shown in the following code example, you specify the name, the digital pin to associate with the module and the time interval (in milliseconds) between transitions.

createSquareWaveGenModule(Blinker, 10, 1000)

Creating and Connecting the Modules

Open up the Arduino IDE. Copy the code at the end of this article and paste it directly into the sketch window in the Arduino IDE. An alternative is to copy the code to a text file. Name the file something like "Modules.h". Create a folder with the same name ("Modules" for this example) and place the file in the folder. Locate your Arduino directory and navigate to the folder named "libraries." Place your folder in the Arduino "libraries" folder. Then, in the sketch window of the Arduino IDE, type the following line:

#include    “Modules.h”

To instantiate and connect the modules, type the following lines after the #include line (or after the code that you pasted into the sketch window, whichever applies).

// create the modules
createDigitalInputModule(Switch1, 5)
createSquareWaveGenModule(Blinker, 10, 1000)

// create the channel that connects Switch1 to Blinker
CreateChannel(sw1_blnkr)

// connect Switch1 to Blinker
BeginConnections
   Switch1(sw1_blnkr);
   Blinker(sw1_blnkr);
EndConnections

You can choose any names that you like for the channels. I try to give them names that will help me to remember which two modules they are intended to connect. For example, sw1_blnkr indicates that this channel connects the output port of Switch1 module to the control port of Blinker.

Compiling and Running the Application

Finally, in the Arduino IDE, select Upload from the File menu. When the preprocessor encounters the macros listed above, it will generate the source code for creating and connecting the modules (you won't see this).

After you wire up the hardware, connect the modules and compile the code, closing switch S1 should cause LED 1 to flash. When you open S1, the flashing will stop.

Build a Night Light

Parts List:

Qty.
Part Description Manufacturer Part Number Component Name
1
Arduino Uno R3 DIP Edition A000066
1
CdS Light Dependent Resistor CDS004-5003 LDR1
1
LED Uni-Color Green LG3330 LED1
1
Resistor Carbon Film 270kΩ CF1/4W274JRC R1
1
Resistor Carbon Film 220kΩ CF1/2W221JRC R2

Figure 5 shows a simple dark activated lamp (you can use it for a night light). The output from the voltage divider formed by resistor R1 and LDR1 increases as the light level decreases. Software component "Adc" is an Analog Input module that repeatedly reads the output from a 10-bit analog to digital converter connected to Arduino analog pin 1. The values are sent to the comparator module "GT512". If GT512 receives a value that's greater than 512 it sends a HIGH to the digital output module "LedDriver" which will turn on LED1 through digital pin 10. For values less than 512, "LedDriver" will receive a LOW and turn off LED1.

Night Light
Figure 5: Night Light

Analog Input Module

The analog input module gets the value from a specified Arduino analog input pin and sends the value to its output port.

Analog Input Module
Figure 6: Analog Input Module

You use the createAnalogInputModule macro command to create an instance of an analog input module.

createAnalogInputModule(ADCdc, 1)

As illustrated in the code example above, you specify the name and the analog pin number.

Comparator Module (GTc)

The GTc comparator module returns true if the input value is greater than an internal constant value. The constant is specified when the module is created. You use the createComparatorModule_GTc macro command to create an instance of the module.

createComparatorModule_GTc(GT512, 512)

Digital Output Module

The digital output module sets the digital output pin to the value (HIGH or LOW) contained in the input message. On module startup the pin is initialized to LOW.

Digital Output Module
Figure 7: Digital Output Module

Use the createDigitalOutputModule macro command to create an instance of this module.

createDigitalOutputModule(LedDriver, 10)

Creating and Connecting the Modules

Use the following macro commands to create and connect the modules for the night light.

// create the module instances
createAnalogInputModule(ADCdc, 1)
createComparatorModule_GTc(GT512, 512)
createDigitalOutputModule(LedDriver, 10)

//create the channels
CreateChannel(adc_gt512)
CreateChannel(gt512_led)

// connect the modules
BeginDCnConnections
   ADCdc(adc_gt512);
   GT512(adc_gt512, gt512_led);
   LedDriver(gt512_led);
EndConnections
The Complete Code

Below is the complete code for the examples described in this article.

// Message constants
const int messageSize = 3; // space for cmd field and data field containing 2 ints or 1 double
                           // can specify a different message size by changing this value
const int messageCmd = 0;  // command field (always element 0 of message array)
const int messageData = 1; // Start of data field. If messageSize > 2 then data field grows

const int invalidCmd = 0; // the module does not understand the command sent by the client
const int validData = 1;  // indicates the module is returning valid data to the client

// Channel constants
const int channelSize = messageSize + 1;
const int channelFlag = 0;  // check this field to see if the channel is/is not empty
const int channelCmd = 1;   // message command field
const int channelData = 2;  // start of message data

const int Empty = 0;
const int notEmpty = 1;

// Channel access functions

bool channelEmpty(int channel[])
{
  if (channel[channelFlag] == Empty) return true;
  // check the flag field to determine if channel is empty
  else return(false);
}

void readChannel(int channel[], int message[]) //retrieves the message from the channel
{
  message[messageCmd] = channel[channelCmd];
  for (int i = 0; i < (messageSize - 1); i++)  //loads the data field
  {
    message[messageData + i] = channel[channelData + i];
  }
  channel[channelFlag] = Empty;
  return;
}

void writeChannel(int message[], int channel[]) // writes the message to the channel
{
  channel[channelCmd] = message[messageCmd];
  for (int i = 0; i < (messageSize - 1); i++)  //loads the data field
  {
    channel[channelData + i] = message[messageData + i];
  }
  channel[channelFlag] = notEmpty;
  return;
}

//********** Modules **************************************************************

// aInx - Analog input pin controller module

#define createAnalogInputModule(name, pinNum) \
void name(int OutPort[]) \
{ \
   const int analogPin = pinNum; \
   int msg[messageSize]; \
   msg[messageData] = analogRead(analogPin); \
   writeChannel(msg, OutPort); \
   return; \
}


// dpInX_dbnc - digital input pin controller module with debounce
// returns the current state of digital pin X, eg dpIn3 reads pin 3

#define createDigitalInputModule(name, pinNum)  \
void name(int OutPort[]) \
{\
   const int dPin = pinNum; \
   const byte init = 0, running = 1, runningDebounce = 11, runningTestVal = 12; \
   static byte mode = init; \
   static int pinVal; \
   const unsigned long debounceTime = 30; \
   static unsigned long currentTime, startTime, elapsedTime; \
   int msg[messageSize]; \
   switch(mode) \
   { \
     case init: \
     pinMode(dPin, INPUT); \
     mode = running; \
     break; \
     case running:  \
     pinVal = digitalRead(dPin); \
     startTime = millis(); \
     mode = runningDebounce; \
     break; \
     case runningDebounce: \
     currentTime = millis(); \
     elapsedTime = currentTime - startTime; \
     if( elapsedTime >= debounceTime)mode = runningTestVal; \
     break; \
     case runningTestVal: \
     msg[messageData] = digitalRead(dPin); \
     if(msg[messageData] == pinVal)writeChannel(msg, OutPort); \
     mode = running; \
     break;   \
   } \
   return;   \
}


//dpOut
//Sets the digital output pin to the value (HIGH or LOW) contained
//in the input message. On module startup the pin is initialized
//to LOW.

#define createDigitalOutputModule(name, pinNum) \
void name(int InPort[]) \
{ \
  const byte init = 0, running = 1;  \
  const int dPin = pinNum; \
  static byte mode = init; \
  int msg[messageSize]; \
  switch(mode) \
  { \
     case init: \
     pinMode(dPin, OUTPUT); \
     digitalWrite(dPin, LOW); \
     mode = running; \
     break; \
     case running: \
     if(!channelEmpty(InPort)) \
      { \
        readChannel(InPort, msg); \
        digitalWrite(dPin, msg[messageData]); \
      } \
      break; \
  } \
  return; \
}


// SqW

// Square wave generator with a duty cycle of approximately 50%. Causes the output at a
// specified pin to alternate between HIGH and LOW at a rate determined by the time interval
// between changes in pin level.
//
// A HIGH control message starts the generator, a LOW control message stops it. On module
// startup the digital pin is initialized to LOW. When stopped, the pin level returns to LOW.
//
// Note: this module does not use delay(). It uses millis() to keep track of interval times.
// As a result the times will be approximate, and will be affected by the number of modules
// in the loop for a given application.
#define createSquareWaveGenModule(name, pin, interval) \
void name(int CntrlPort[]) \
{ \
   const int pinNum = pin; \
   const unsigned long delayTime = interval; \
   const byte init = 0, ready = 1, running = 2, runningStart = 21, runningDelay = 22; \
   static byte mode = init; \
   static int pinState = LOW; \
   static unsigned long startTime, currentTime; \
   int msg[messageSize]; \
   switch(mode) \
   { \
      case init: \
      pinMode(pinNum, OUTPUT); \
      digitalWrite(pinNum, LOW); \
      mode = ready; \
      break; \
      case ready: \
      if(!channelEmpty(CntrlPort)) \
      { \
        readChannel(CntrlPort, msg); \
        if(msg[messageData] == HIGH) mode = running; \
      } \
      break; \
      case running: \
      pinState = !pinState; \
      digitalWrite(pinNum, pinState); \
      startTime = millis(); \
      mode = runningDelay; \
      break; \
      case runningStart: \
      digitalWrite(pinNum, pinState); \
      mode = runningDelay;   \
      break; \
      case runningDelay: \
      if(!channelEmpty(CntrlPort)) \
      { \
        readChannel(CntrlPort, msg); \
        if(msg[messageData] == LOW) \
        { \
          digitalWrite(pinNum, LOW); \
          mode = ready; \
          break; \
        } \
      } \
      currentTime = millis(); \
      if( (currentTime - startTime) >= delayTime)mode=running; \
      break;      \
   }   \
   return; \
}



// GTc
// Comparator module. Returns true if input value is greater than internal constant value
// the constant is specified when module is created
#define createComparatorModule_GTc(name, constValue) \
void name(int InPort[], int OutPort[]) \
{ \
  const int val = constValue; \
  int msg[messageSize]; \
  if (!channelEmpty(InPort)) \
  { \
    readChannel(InPort, msg); \
    if (msg[messageData] > val ) \
    { \
      msg[messageData] = HIGH; \
    } \
    else \
    { \
      msg[messageData] = LOW; \
    } \
    writeChannel(msg, OutPort); \
  } \
  return; \
}


//**************************************************************************************


#define CreateChannel(name) int name[channelSize];

#define BeginConnections void setup(){} \
void loop() \
{

#define EndConnections }

//**************************************************************************************

For almost two decades, Phil Kane has been a technical writer in the software industry and occasionally authored articles for electronics enthusiast magazines. He has a bachelor's in Electronics Engineering Technology with a minor in Computer Science. Phil has had a life-long interest in science, electronics and space exploration. He enjoys designing and building electronic gadgets, and would very much like to see at least one of those gadgets on its way to the moon or Mars one day.

If you have an electronics story or project you'd like to share, please email [email protected].