Bare-Metal Integration

This section describes how to integrate a state machine with a cycle based or event driven execution semantic on any microcontroller, so on any embedded device. The integration can be structured in three parts, which results the main part of your program.

  • Initialization – called once
  • While loop – calling the state machine until it’s final
  • Final – called if the state machine is final
According to the used State Machine, there are different parts, which must be taken into account:
  • In Events
  • Timed Events
  • Out Events

Main Function

The main function could be realized as following: First, the hardware will be initialized. Then the timers will be taken into account. After this the state machine will be initialized, the out event observers will be initialized and subscribed, and finally the state machine will be entered.
At this point the state machine will be called as long as the state machine is not final. Note: If there is no final state defined the behavior is equal to a while(true) loop. At first all in events will be raised. After this the timer will be updated, which internally raises time events. The behavior is similar to in events. Then the runCycle will be called, which handles the logical execution of the state machine. This is only needed if the execution semantics is cycle-based and must only be executed based on the elapsed time, e.g. 200 ms for a CycleBased(200) state machine. The out events are observed and callbacks are called if the out events are raised, while the state machine is executed. The last step, if wanted, is setting the microcontroller into a sleep mode.

void main() {
	// Initialization
	
	hardwareInit();
	
	#ifdef TIME_EVENTS
		timerInit();
	#endif
	
	statemachine_init();
	
	#ifdef OUT_EVENTS
		subscribe_observers();
	#endif
	
	statemachine_enter();

	// While loop
	while(!statemachine_isFinal()) {
		#ifdef IN_EVENTS
			handleInEvents();
		#endif
		
		#ifdef TIME_EVENTS
			handleTimer();
		#endif
		
		#ifdef CYCLE_BASED
		if(elapsedTime >= cyclePeriod) {
			elapsedTime = 0;
			statemachine_runCycle();
		}
		#endif
		
		#ifdef SLEEP_MODE
			goToSleep()
			// wait for Interrupt
		#endif
	}
}

Handle In Events

For now on, there are two different ways of how the events should interact on the embedded system. One design pattern is polling. The other one is the usage of interrupts.

A design using polling cyclically updates the status of the inputs and raises the respective event.

void handleInEvents() {
	if(readGPIO(1)) {
		statemachine_raise_inEvent1();
	}
	if(readGPIO(2)) {
		statemachine_raise_inEvent2();
	}
}

As already described in the Interrupt section, a design using interrupts recommends bool flags as storage for the event.

void handleInEvents() {
	if(inEvent1) {
		statemachine_raise_inEvent1();
		inEvent1 = false;
	}
	if(inEvent2) {
		statemachine_raise_inEvent2(inEvent2Value);
		inEvent2 = false;
	}
}

Sensor1_ISR{
	inEvent1 = true;
}

Sensor2_ISR{
	inEvent2 = true;
	inEvent2Value = readSensor2();
}

Handle Timer

Handling the timer is more comprehensive. Each time event of the state machine must be updated and handled because in detail they are nothing else than an in event. Therefore, a timer service is provided in the examples. This timer service must be updated with the elapsed time since its last call, which leads to the next implementation point: Determining the time.
On a pure bare-metal implementation there is no such thing as elapsed time. Everything depends on cycles.

Imagine a microcontroller running with a clock of 16MHz. Running one instruction needs 1/16MHz = 62.5ns. A delay() function 100000 would need 6,25 ms. A first approach updating the timer could be:

void main() {
	while(true) {
		delay(100000);
		updateTimer(6,25);
		runCycle();
	}
}

But using this implementations has two big drawbacks:

  • the delay function is blocking
  • the execution time of the updateTimer and runCycle functions are not taken into account

A much more precise design is using timer interrupts, which can generate a periodic interrupt every x cycles:

void handleTimer() {
	if(updateTimerFlag) {
		updateTimer(TIMER_TICK_MS);
		updateTimerFlag = false;
	}
}

#define TIMER_TICK_MS 32 // 32 ms for example
Timer_ISR{ 
	updateTimerFlag = true;
}

Some microcontrollers, like the Arduino, support functions like millis(), which are in fact using a similar method, but storing the elapsed time since starting the device. This time can be used to determine the elapsed time between two cycles in a loop:

void loop() {
	current_millis = millis();
	updateTimer(current_millis - last_cycle_time);
	statemachine_runCycle();
	last_cycle_time = current_millis;
}

For prototyping, this implementation can be used, but it should be considered that the timer will overflow after approximately 50 days. That’s why we highly recommend a implementation with interrupts as described before.

Handle Out Events

Out events raised by the state machine can be checked by using the state machine’s API. The default case is using observables, which must be initialized and registered before the state machine is entered. If an out event gets raised, the registered callback of the observer will be called. Thus, out events are handled while executing the state machine’s runCycle. They can be used to wire up different actuators. Mapping values, e.g. integers, to the out events is also possible.

void handleOutEvents() {
	if(statemachine_israised_outEvent1()) {
		controlActuator1();
	}
	if(statemachine_israised_outEvent2()) {
		controlActuator2(statemachine_get_outEvent2_value());
	}
}

void on_outEvent1(StateMachine* handle) {
	controlActuator1();
}

void on_outEvent2(StateMachine* handle, sc_integer value) {
	controlActuator2(value);
}

void subscribe_observers(StateMachine *handle, sc_single_subscription_observer *outEvent1Observer, sc_single_subscription_observer_sc_integer *outEvent2Observer) {
	sc_single_subscription_observer_init(outEvent1Observer, handle, (sc_observer_next_fp) on_outEvent1);
	sc_single_subscription_observer_subscribe(outEvent1Observer, &handle->iface.outEvent1);

	sc_single_subscription_observer_sc_integer_init(outEvent2Observer, handle, (sc_observer_next_fp) on_outEvent2);
	sc_single_subscription_observer_sc_integer_subscribe(outEvent2Observer, &handle->iface.outEvent2);
}

Arduino Bare-Metal Integration

There are two examples for a bare-metal integration using interrupts and polling for Arduino and can be used for both execution semantics: @CycleBased and @EventDriven. They have been tested with an Arduino Uno (ATmega328p) and an Arduino Mega (ATmega2560):

Add them to YAKINDU Statechart Tool with the example wizard:
File -> New -> Example... -> YAKINDU Statechart Examples -> Embedded Systems Integration Guide -> Arduino – Bare-Metal Interrupts/Polling ©