Background Processing in the Arduino

While the Arduino is a slick little environment, it lacks one thing: a background processing paradigm. When you read an analog sensor, delay, or wait for the serial port to finish transmitting data, you effectively waste time. This is fine if you are doing basically simple control tasks, but if you want to do something a little more complex, you need to restructure your code to do multiple things simultaneously.

Well, in computers, old things often become new again. Back in the late 1980s and early 1990s most computers ran some form of Microsoft DOS. Oddly enough, the Arduino processor has basic similarities to the old Intel 8088 and the single task environment is similar as well. The tricks we did in DOS translate easily to the Arduinio.

One of the more popular techniques was to break down tasks into their various states and create a state machine. The state machine can be called periodically to do a little bit of processing each time and return. Each successive step in the state machine progresses the overall process. Its like a thread, but no thread context is needed.

A prime candidate for a state machine algorithm is a common ultrasonic range finder device (Like the Parallax ping or similar devices). Before we get ahead of ourselves, let's fix the Arduino code first. Just as you have "setup()" and "loop()" functions, you will also define a yield() function.

void yield()
{
}

In the Arduino code, you will add "void yield(void);" to "Arduino.h" right after the definitions of "setup()" and "loop()."

void yield(void);

In the file "HardwareSerial.cpp" modify the "flush()" method to include the yield function.

void HardwareSerial::flush()
{
  while (_tx_buffer->head != _tx_buffer->tail)
    yield();
}

In the wiring_analog.c, modify the analogRead() function:

        // ADSC is cleared when the conversion finishes
        while (bit_is_set(ADCSRA, ADSC))
                yield() ;

Lastly, in the "wiring.c" file, modify the delay function:

void delay(unsigned long ms)
{
        uint16_t start = (uint16_t)micros();

        while (ms > 0) {
                if (((uint16_t)micros() - start) >= 1000) {
                        ms--;
                        start += 1000;
                }
                yield();
        }
}

Now that this is done, rebuild everything. Once complete, you will now get call backs on the yield() function when you delay, read the analog sensor, or call flush on the serial object.

With the yield callback in place, lets look at a well suited state machine task. The ultrasonic sensor class. Except for some setup and object management code, the meat of the sensor looks like this:

/*
 * This comprises the "state machine" for the sensor.
 * After a ping is sent, this will step through the
 * states until complete.
 * T0   T1     T2   T3    T4              complete (back to T0)
 * v    v      v    v     v                 v
 *      --------          -------------------
 * _____|      |__________|                 |_____
 *
 * idle | ping |  settle  |  wait for echo  | idle
 * 
 */ 
int UltrasonicSensor::step()
{
	unsigned long t;
	m_now = micros();

	switch(m_state)
	{
		case SENSOR_STATE_T0:		// Start, initiate a ping
			m_state = SENSOR_STATE_T1;
			pinMode(m_pin, OUTPUT);
			digitalWrite(m_pin, 1);
			m_start = micros();
			m_state_time = 0;
			break;
		case SENSOR_STATE_T1:		// We've started the ping, now time it
			if(longDiff(m_start, m_now) > 5)
			{ 	// Stop ping, now wait to settle
				digitalWrite(m_pin, 0);
				pinMode(m_pin, INPUT);
				changeState(SENSOR_STATE_T2);
			}
			break;
		case SENSOR_STATE_T2:		// Wait for sensor to settle
			if(longDiff(m_state_time, m_now) > 150)
				changeState(SENSOR_STATE_T3);

		case SENSOR_STATE_T3:		// Waiting for rise on leading edge
			if(digitalRead(m_pin))
				changeState(SENSOR_STATE_T4);
			else if(longDiff(m_start, m_now) > 500)
				timeout();
			break;
		case SENSOR_STATE_T4:		// Waiting for dropping edge of signal
			if(!digitalRead(m_pin))
			{
				m_value = usec2mm(m_state_time, m_now);
				changeState(SENSOR_STATE_T0);
			}
			else if(longDiff(m_start, m_now) > 21000)
				timeout();
			break;
		default:
			timeout();
			break;
	}
	return m_state;
}

all that is needed now is to modify the yield() function to call into the ultrasonic sensor object.

UltrasonicSensor us;

void yield()
{
    us.step();
}

Now, in your main thread, periodically place a yield() call if you are doing any long processing, but besides that, the ultrasonic sensor will take care of itself.

Now, obviously, one ultrasonic sensor is hardly enough, but it doesn't take too much imagination to think how to do more, and besides, that's a different post for another day.

One thing that should be recommended is the KISS principal (Keep It Simple Stupid). The Arduino is a small computer and a simple environment, know when enough is enough and don't get too sophisticated. An easily understandable solution that works well enough is often better and is usually more robust than a more complex one.