SCXML on an ATMega328

This page describes how to use SCXML to model the control flow for an embedded micro-controller. We will transform an SCXML state-chart onto ANSI-C, provide some scaffolding and deploy it onto the Arduino Nano. Regardless of the target platform, the approach is the same if you like to use the ANSI-C transformation for just any other platform.

Hardware Platform

For a project of mine, I used a solar-powered Arduino Nano to control four aquarium pumps to water my balcony plants when their soil is dry. There are some peculiarities that justify to use a state-chart to model the controller's behavior, namely:

  1. There is only enough power for one pump at a time
  2. Pumps should only run if there is sufficient sunlight
  3. The pump related to the sensor with the driest soil below a given threshold should run
  4. If a pump runs, it ought to run for a given minimum amount of time

The whole system is powered via a 5W solar module, which is connected via a 12V DCDC converter to another 12V5V DCDC converter, allowing to operate the actuators with 12V and the Arduino with 5V. On the 12V potential, there are some gold caps worth ~150mF to stabilize the potential and provide some kick-off current for a pump to start.

Sensors

In order to measure the soil's moisture, I originally bought those cheap moisture sensors that will measure conductivity between two zinc plated conductors. The problem with these is that they corrode pretty quickly. You can alleviate the problem somewhat by measuring only once every N minutes, but still I disliked the approach.

That's why I went for capacitive sensors with some nice aluminum plates that I can either drop into the water reservoir of my balcony's flower beds or bury at the edge of a bed. Moist soil will cause a higher capacity and we can measure it very easily with two digital pins on the ATMega328: Just set the first pin to HIGH and measure the time it takes for a second pin, connected to the same potential to read HIGH. The longer it took to achieve the HIGH potential on the second pin, the higher the capacity, the more moist the soil.

There is another sensor as a simple voltage divider with 1M Ohm and 100K Ohm to measure the voltage at the solar module. This will allow us only to water plants when the sun is actually shining and with sufficient strength.

Actuators

I connected four 3W aquarium pumps via relais that I supply with 12V from the solar module and control via analog outputs from the Arduino as most digital outputs are taken by the capacitive sensors for the soil's moisture. The pumps are submerged in a 60l tank of water that I have to refill occasionally.

Control Logic

Initially, I developed the system as a pet project but soon realized that I could utilize a state-chart to, more formally, adhere to the requirements above. Here is the SCXML document I wrote:

1 <scxml datamodel="native" initial="dark" version="1.0">
2  <!-- we provide the datamodel inline in the scaffolding -->
3  <script><![CDATA[
4  pinMode(LED, OUTPUT);
5  for (char i = 0; i < 4; ++i) {
6  pinMode(pump[i], OUTPUT);
7  digitalWrite(pump[i], PUMP_OFF);
8  bed[i].set_CS_AutocaL_Millis(0xFFFFFFFF);
9  }
10  ]]></script>
11 
12  <!-- it is too dark to water flowers -->
13  <state id="dark">
14  <transition event="light" cond="_event->data.light > LIGHT_THRES" target="light" />
15  <onentry>
16  <script><![CDATA[
17  for (char i = 0; i < 4; ++i) {
18  digitalWrite(pump[i], PUMP_OFF);
19  }
20  ]]></script>
21  </onentry>
22  </state>
23 
24  <!-- start to take measurements and activate single pumps if too dry -->
25  <state id="light">
26  <transition event="light" cond="_event->data.light &lt; LIGHT_THRES" target="dark" />
27 
28  <!-- delivers events for all the capsense measurements -->
29  <invoke type="capsense" id="cap" />
30 
31  <state id="idle">
32  <transition event="pump" cond="soil[0] &lt; 0 &amp;&amp;
33  soil[0] &lt;= soil[1] &amp;&amp;
34  soil[0] &lt;= soil[2] &amp;&amp;
35  soil[0] &lt;= soil[3]" target="pump1" />
36  <transition event="pump" cond="soil[1] &lt; 0 &amp;&amp;
37  soil[1] &lt;= soil[0] &amp;&amp;
38  soil[1] &lt;= soil[2] &amp;&amp;
39  soil[1] &lt;= soil[3]" target="pump2" />
40  <transition event="pump" cond="soil[2] &lt; 0 &amp;&amp;
41  soil[2] &lt;= soil[0] &amp;&amp;
42  soil[2] &lt;= soil[1] &amp;&amp;
43  soil[2] &lt;= soil[3]" target="pump3" />
44  <transition event="pump" cond="soil[3] &lt; 0 &amp;&amp;
45  soil[3] &lt;= soil[0] &amp;&amp;
46  soil[3] &lt;= soil[1] &amp;&amp;
47  soil[3] &lt;= soil[2]" target="pump4" />
48  </state>
49 
50  <state id="pumping">
51  <transition event="idle" target="idle" />
52  <onentry>
53  <send delay="8000ms" event="idle" />
54  </onentry>
55 
56  <state id="pump1">
57  <invoke type="pump" id="1" />
58  </state>
59  <state id="pump2">
60  <invoke type="pump" id="2" />
61  </state>
62  <state id="pump3">
63  <invoke type="pump" id="3" />
64  </state>
65  <state id="pump4">
66  <invoke type="pump" id="4" />
67  </state>
68  </state>
69  </state>
70 </scxml>

There are a few noteworthy things in the SCXML document above:

  • Datamodel attribute is `native and no datamodel element is given
    • This will cause the transpilation process to include all datamodel statements and expressions as is in the generated source, without passing them to a user-supplied callback for interpretation.
    • Using this approach, it is no longer possible to interpret the document with the browser.
  • We can use identifiers and functions available on the Arduino platform
    • As all expressions and statements will be inserted verbatim into the generated ANSI-C file, and will merely compile it as any other file when we deploy on the Arduino.
  • Content of script elements is in CDATA sections while expressions in conditions are escaped
    • We are using the DOMLSSerializer to create a textual representation of the XML DOM and I could not convince it to unescape XML entities when writing the stream.
  • We assume a pump and a capsense invoker
    • Indeed, we will not write proper invokers but just mock them in the scaffolding. The important thing is that we get the lifetime management from the SCXML semantics.

When we transform this document via uscxml-transform -tc -i WaterPump.scxml -o stateMachine.c we will arrive at a stateMachine.c file which conforms to the control flow modeled in the SCXML file. Now, we will need to write the scaffolding that connects the callbacks and starts the machine. The complete scaffolding for the generated state machine is given below:

1 // Resources:
2 // https://www.avrprogrammers.com/howto/atmega328-power
3 // https://github.com/PaulStoffregen/CapacitiveSensor
4 // https://de.wikipedia.org/wiki/Faustformelverfahren_%28Automatisierungstechnik%29
5 // http://brettbeauregard.com/blog/2011/04/improving-the-beginners-pid-introduction/
6 // google://SCC3 for conformal coating
7 
8 #include <LowPower.h>
9 #include <CapacitiveSensor.h>
10 
11 
12 #define USCXML_NO_HISTORY
13 
14 #define LED 13 // LED pin on the Arduino Nano
15 #define LIGHT A7 // 1:10 voltage divider soldered into the solar panel
16 #define LIGHT_THRES 300 // do not actuate beneath this brightness
17 #define MEASURE_INTERVAL SLEEP_1S // time between cycles
18 #define DARK_SLEEP_CYCLES 1
19 
20 #define PUMP_ON LOW // Setting an output to LOW will trigger the relais
21 #define PUMP_OFF HIGH
22 
23 #define ROLLOFF 0.8 // exponential smoothing for sensor readings
24 
25 float soil[4] = { 0, 0, 0, 0 }; // smoothed sensor readings from the capacitive sensors
26 int pump[4] = { A0, A1, A2, A3 }; // we abuse analog pins as digital output
27 int activePump = -1;
28 
29 int thrs[4] = { 1400, 1400, 1400, 1400 }; // start pumping below these values
30 
31 CapacitiveSensor bed[4] = { // Pins where the capacitive sensors are connected
32  CapacitiveSensor(3, 2),
33  CapacitiveSensor(5, 4),
34  CapacitiveSensor(7, 6),
35  CapacitiveSensor(9, 8)
36 };
37 char readCapSense = 0; // Whether the capsense invoker is active
38 
39 struct data_t {
40  int light;
41 };
42 struct event_t {
43  const char* name;
44  struct data_t data;
45 };
46 
47 // the various events
48 long pumpRemain = 0;
49 struct event_t _eventIdle = {
50  name: "idle"
51 };
52 struct event_t _eventLight = {
53  name: "light"
54 };
55 struct event_t _eventPump = {
56  name: "pump"
57 };
58 struct event_t* _event;
59 
60 #include "stateMachine.c"
61 
62 uscxml_ctx ctx;
63 
64 /* state chart is invoking something */
65 static int invoke(const uscxml_ctx* ctx,
66  const uscxml_state* s,
67  const uscxml_elem_invoke* invocation,
68  unsigned char uninvoke) {
69  if (strcmp(invocation->type, "pump") == 0) {
70  int pumpId = atoi(invocation->id);
71  digitalWrite(pump[pumpId], uninvoke == 0 ? PUMP_ON : PUMP_OFF);
72  } else if (strcmp(invocation->type, "capsense") == 0) {
73  readCapSense = uninvoke;
74  }
75 }
76 
77 /* is the event matching */
78 static int matched(const uscxml_ctx* ctx,
79  const uscxml_transition* transition,
80  const void* event) {
81  // we ignore most event name matching rules here
82  return strcmp(transition->event, ((const struct event_t*)event)->name) == 0;
83 }
84 
85 static int send(const uscxml_ctx* ctx, const uscxml_elem_send* send) {
86  if (send->delay > 0)
87  pumpRemain = send->delay;
88  return USCXML_ERR_OK;
89 }
90 
91 static void* dequeueExternal(const uscxml_ctx* ctx) {
92  // we will only call step when we have an event
93  void* tmp = _event;
94  _event = NULL;
95  return tmp;
96 }
97 
98 static bool isInState(const char* stateId) {
99  for (size_t i = 0; i < ctx.machine->nr_states; i++) {
100  if (ctx.machine->states[i].name &&
101  strcmp(ctx.machine->states[i].name, stateId) == 0 &&
102  BIT_HAS(i, ctx.config)) {
103  return true;
104  }
105  }
106 
107  return false;
108 }
109 
110 void setup() {
111  // initilize the state chart
112  memset(&ctx, 0, sizeof(uscxml_ctx));
113  ctx.machine = &USCXML_MACHINE;
114  ctx.invoke = invoke;
115  ctx.is_matched = matched;
116  ctx.dequeue_external = dequeueExternal;
117  ctx.exec_content_send = send;
118 
119  int err = USCXML_ERR_OK;
120 
121  // run until first stable config
122  while((err = uscxml_step(&ctx)) != USCXML_ERR_IDLE) {}
123 }
124 
125 
126 void loop() {
127  digitalWrite(LED, HIGH);
128 
129  int err = USCXML_ERR_OK;
130 
131  if (readCapSense) {
132  // capsense invoker is active
133  for (int i = 0; i < 4; ++i) {
134  int cap = bed[i].capacitiveSensor(50);
135  if (cap > 0) {
136  soil[i] = ROLLOFF * soil[i] + (1 - ROLLOFF) * (cap - thrs[i]);
137  }
138  }
139  }
140 
141  _eventLight.data.light = analogRead(LIGHT);
142  _event = &_eventLight;
143  while((err = uscxml_step(&ctx)) != USCXML_ERR_IDLE) {}
144 
145  if (isInState("dark")) {
146  LowPower.powerDown(MEASURE_INTERVAL, ADC_OFF, BOD_OFF);
147  return;
148  }
149 
150  if (isInState("light")) {
151  if (false) {
152  } else if (isInState("pumping")) {
153  // is time elapsed already?
154  if (pumpRemain == 0) {
155  _event = &_eventIdle;
156  while((err = uscxml_step(&ctx)) != USCXML_ERR_IDLE) {}
157  }
158  } else if (isInState("idle")) {
159  // check is we need to pump
160  _event = &_eventPump;
161  while((err = uscxml_step(&ctx)) != USCXML_ERR_IDLE) {}
162  }
163  }
164 
165  pumpRemain = (pumpRemain >= 8000) ? pumpRemain - 8000 : 0;
166 
167  digitalWrite(LED, LOW);
168  LowPower.powerDown(MEASURE_INTERVAL, ADC_OFF, BOD_OFF);
169 
170 }
All information pertaining to a element.
Definition: stateMachine.c:250
All information pertaining to any state element.
Definition: stateMachine.c:234
All information pertaining to a element.
Definition: stateMachine.c:315
Represents an instance of a state-chart at runtime/.
Definition: stateMachine.c:335
All information pertaining to an element.
Definition: stateMachine.c:295

To integrate the scaffolding and the control logic from the state-chart, the generated stateMachine.c is merely included into the compilation unit. To compile it all, I am using PlatformIO IDE as it is more convenient to work with multifile projects as apposed to the Arduino IDE, but both will work. It is important, not to compile stateMachine.c as a distinct compilation unit, but only as part of the scaffolding. If you have any problems to exclude it from the build process, you may always rename it into something without a .c extension.

The scaffolding is rather minimal and somewhat unorthodox as I tried to get away without using malloc for dynamic memory allocations, but keep everything on the stack. Let's walk through the individual lines:

  • First we include the header files for LowPower and CapacitiveSensor. With PlatformIO, you will have to copy them into your lib directory along with their respective implementations.
  • Then we declare some macros that define constant values. Noteworthy is the USCXML_NO_HISTORY macro which causes the generated ANSI-C to drop a block of code for processing history elements, which we do not use.
  • Afterwards, we declare and define variables. We could have them as part of a datamodel element in the original SCXML document, but in my opinion, defining them here makes their existence more explicit.
  • It is noteworthy that we do enumerate all the events we are going to pass and will not implement an actual queue of events. A proper queue would require malloc and there will never be more than one event to consider per microstep.
  • In line 62, we include the generated ANSI-C stateMachine and define a context, as it is required to represent the state of a state-chart.
  • Then we define the callbacks that we later connect to the state-chart's context:
    • invoke will be called whenever an invocation is to be started or stopped at the end of a macro-step. This is where we merely remember whether we are supposed to start a pump (type pump) or deliver sensor readings from the capacitive sensors (type capsense).
    • matched is called to determine whether a given transition's event descriptor is matched by a given event and a concept explained in more detail in the SCXML recommendation. In this implementation, we ignore the finer points of event descriptor matching and only match, when the event's name is a literal match for the transition's event. attribute
    • send: When we start a pump, we are sending a delayed event to ourselves which we will have to deliver back into the state-chart after the given time in milliseconds passed. We just remember the current delay in pumpRemain and, subsequently, decrease it until we reach the timeout and have to deliver it.
    • dequeueExternal: Whenever the interpreter is in a stable configuration, it makes an attempt to dequeue an external event, which will cause this callback to be triggered. If we return an event, it will be processed by triggering transitions and a change in the configuration, if we return NULL, the state-chart will IDLE.
    • isInState is not formally a callback to be registered by the context but very useful to dispatch upon the configuration of the state chart later.
  • In setup, we initialize the state of the platform after the reset button is pressed or the power came back on. I.e. we connect the callbacks and initialize the state chart by proceeding to the first IDLE interpreter state.
  • In loop we process one cycle of the controller. We turn the LED on to indicate that we are processing and read the capacitive sensors if the invoker is active. Then we read the amount of light that arrives at the solar module via the voltage divider connected to LIGHT and transition accordingly. Afterwards we check if it is time to send the eventual idle event to turn of the pumps or check if we ought to activate a pump.

One thing that helped me when developing the scaffolding was to thing about the configuration the state chart would eventually be in and observe the various events it would react to. Then to make sure that these events would be delivered when they are relevant.

Resources

The resources required when deploying this program on the ATMega328 are given as follows:

Program:   10534 bytes (32.1% Full)
(.text + .data + .bootloader)

Data:       1636 bytes (79.9% Full)
(.data + .bss + .noinit)

There are still quite some possibilities to reduce these resources some more if we are pressed on space:

  • Event and invoker names can be enumerated
  • We can drop some unused callbacks from the uscxm_ctx struct
  • We can remove most fields of the uscxml_elem_send struct