Thursday, December 13, 2012

The start of an Asynchronous Device Framework

Since my last post I refactored my digital input and output controllers and added an analog input controller. I've decided for now to call this my Asynchronous Device Framework, or ADF for short. The name, as it implies, is using the Quantum Leaps QP software framework to carve out slices of time for each of the input and output device controllers. The devices are grouped by functionality, so for example, simple digital output devices are controlled by a Digital Output Device Controller (DigitalODC) class. I have a Digital Input device controller and an Analog Input device controller. The controller for the logic is called MyController; it receives events from the various input devices and issues event commands to the output devices. One nice feature of the Quantum framework is the ability for each task, in this case a controller, to issue inter-process communications (IPC) to other controllers. This message framework makes it easy for an input controller to send sensor data to the logic controller asynchronously. This of course moves the task of polling a sensor to a process running in an independent "thread". This means no logic is ever blocked waiting for input, it is all event driven.


In the above picture, all of these devices are running asynchronously: 3 LED's, a PIR, a switch, a vibrator, a temperature sensor and a light sensor. When the state of any of the digital input modules is changed, that state is sent to the logic controller. The analog temperature sensor is sending readings once every 5 seconds and the light sensor every second.

The code to "program" the inputs and outputs is done in the logic controller (MyController). For example, the digital output controller exposes a simple add method:

  // pre-program the operation LED to constantly blink
  // at 600 ms on and 600ms off
  DigitalODC_Add(OPERATION_LED, 600, DOD_MODE_CYCLE);

  // pre-program the Warning LED to flash once for 1/2 second
  DigitalODC_Add(WARNING_LED, 500, DOD_MODE_ONESHOT);

  // pre-program the vibration sensor to buzz for 1 second
  DigitalODC_Add(VIBRATION, 1000, DOD_MODE_ONESHOT);

  // pre-program the GO LED in the off state
  // this control might be somewhat useless
  // unless its mode can change.
  DigitalODC_Add(GO_LED, 0, DOD_MODE_OFF);

The first argument of the add method is the digital i/o pin number; the second value is an on/off cycle rate in milliseconds or the duration if its mode is one shot, and the final argument is the Digital Output Device mode, currently one of:

// Digital Output Device Mode
enum DOD_MODE
{
    DOD_MODE_OFF = 1, // direct controlled and is off
    DOD_MODE_ON,     
// direct controlled and is on
    DOD_MODE_CYCLE,   // cycle continuously
    DOD_MODE_ONESHOT  // only on for duration of the cycle time
};


So the operation LED blinks continuously for 600ms on and 600ms off. The warning LED, when triggered will flash for 500ms, and the vibrator module will run for 1 second. The GO_LED is under direct control of MyController, that is, it is explicitly commanded to be either on or off.

As for the inputs, they are programmed as well:

  // setup the passive infrared sensor as an input
  DigitalIDC_Add(PIR_SENSOR); 

  // setup the switch (with pullup!) as an input
  // probably need to add input-with-pull up option as well
  DigitalIDC_Add(SWITCH);

  // add an analog light sensor input and have it
  // send status every second
  AnalogIDC_Add(LIGHT_SENSOR, 1000);

  // add an analog light sensor input and have it
  // send status every 5 seconds seconds
  AnalogIDC_Add(TEMPERATURE_SENSOR, 5000);

The Digital Input Device Controller, at this time, will only send a state change, so it is only necessary to add the PIR sensor and the slide SWITCH. However, the analog inputs are instructed to send data updates every 1000ms and 5000ms respectively.

The MyController logic for the PIR sensor looks like this:

case DID_STATE_CHANGED_SIG:
   if((CAST_TO_DDEventArg->pinNumber == PIR_SENSOR) &&
      (CAST_TO_DDEventArg->state == HIGH))
   {
      DigitalODC_Set(WARNING_LED, HIGH); // fire warning LED and
      if(switchState)    // if the switch is on then
          DigitalODC_Set(VIBRATION, HIGH);  // vibrate
   }
   else if(CAST_TO_DDEventArg->pinNumber == SWITCH) // if switch
   {
      switchState = CAST_TO_DDEventArg->state; // when changed
      DigitalODC_Set(GO_LED,CAST_TO_DDEventArg->state); // LED changes
   }
   break;

To understand what is happening beyond this logic, you need to understand the IPC (inter-process communications) mechanism. When MyController gets a slice of time, it may or may not have a message waiting for it. If it does it comes in the form of an enumerated signal, in this case, DID_STATE_CHANGED_SIG. To further clarify, I defined this signal in my framework, and programmed the Digital Input Device to publish this event message if one of its monitored inputs changed state. In this case, we can have two digital inputs, one from the PIR and one from the SWITCH, so we need to figure out which one is related to this event. The event message arrives with a pointer to a class object, in this case I've programmed the digital input device controller to send a Digital Device Event Argument (DDEventArg). It has two members, a pin number and a state. So if the message is DID_STATE_CHANGED_SIG AND the pin number is the PIR sensor AND its state has gone positive, something happened. In this case it sends an "on" signal to the warning led, which was pre-programmed to flash on for only 500 ms. If the switch state happens to be "on", then the vibrator module is sent an "on" signal and it vibrates for the pre-programmed 1 second.

If on the other hand, the incoming message is DID_STATE_CHANGED_SIG and the pin number is SWITCH, then we just capture that state in the switchState variable and make the GO_LED track this state.

I created the DigitalODC_Set method to hide the details of sending the IPC message to the Output Device Controller. Here is what it looks like:

// set a binary output device's state
void DigitalODC_Set(uint8_t pinNumber, uint8_t state)
{
    DDEventArg *ea = Q_NEW(DDEventArg, SET_DOD_STATE);
    ea->pinNumber = pinNumber;
    ea->state = state;
    QF::PUBLISH(ea, SET_BOD_STATE);
}

This method takes the pin number and state of the device to be triggered and wraps that up in a Q_NEW event argument class object then Publishes that as a SET_DOD_STATE message. Because the DOD controller has subscribed to this message, when it gets its time slice it will receive the message and set the output accordingly. See my previous posts for more information on quantum platform's IPC mechanism.

To kick things off in the ino file I've encapsulated the start ups in each of the class definition files, so the ino startup looks like this:

    uint8_t priority = 1;

    DigitalODC_Start( ++priority );
    DigitalIDC_Start( ++priority );
    AnalogIDC_Start ( ++priority );

    MyController_Start( ++priority );

This code segment does not show the standard QP initialization calls.

I've designed the controllers to be able to accept responsibility for as many devices as reasonable. By that I mean that I can't add 200 LEDs to the DOD controller, the code would fall over. In that case I would write a MultiLED controller so MyController would send messages to that controller. This means there needs to be some good decisions about the granularity of control because of messaging issues and response latency.

Each controller uses a simple linked list of devices to control/monitor. Because the devices are not removed during runtime, there is no need to worry about memory fragmentation issues that would become problematic with dynamic lists. So in effect, the controllers can support any number of virtual devices as long as there is a pin or pins for them to run on.

Memory requirements so far are about 9K for ROM and an SRAM monitor shows about 1.2K free out of 2K at runtime. Also note there are no huge libraries loaded at this time and I have no idea how much code squeeze this might cause. If necessary I might have to move to a Mega for large complicated asynchronous projects.

I use recursion to walk the linked list of devices and I'm not sure if that may put too much stress on the stack. I've run this code overnight at 300 ticks per second. It did not crash and I saw no RAM creep. I don't have a logic analyzer so I don't know what the real response is like. If time ticks are 3 ms each, it would take up to 3 ms to detect an input change, 3 ms for MyController to send a message to an output controller and 3 ms to change it. That means a theoretical response of up to 9 ms, perhaps more. For faster responses I would need to move to the pre-emptive mode in QP and that brings in a whole lot of issues and greatly complicates the code. Note that I have not explored the possibility of directly invoking the destination controller through a FIFO post that is built into the QP framework. This could speed up response if it becomes necessary.

I plan to add an SPI interface to a 3 axis accelerometer that I have. If this framework can support some additional stress like SPI communications and maybe concurrent serial I/O, then it might be a worthwhile framework to which I could base some applications.

Anyhow, for now, I'm not planning to upload the code until I get it cleaned up a bit. I'll make it available here for anyone that cares to use/evaluate it.

More later ...

No comments:

Post a Comment