9. Asynchrony and blocking


Thus far, we have used the delay() function to handle the timing of events, such as turning an LED on or off or writing data via a serial connection. The problem with using delay() is that it works by blocking. This means that while delay() is running the microprocessor cannot do any other tasks. In the previous exercise, we waited 100 milliseconds before sending data. During that time, the ATmega328 could have performed about 2 million instructions. That is a lot of wasted computational resources to enable blocking! (The chip in your computer is more than 1000 times faster, so blocking on your computer wastes even more resources. We will address asynchrony on the Python side in later lessons.)

Asynchrony

Asynchrony is an important concept in the design of biodevices. It is best explained by example.

Say you make a device that acquires data from a sensor and sends those data to your computer via USB. Meanwhile, your device should listen for a signal coming from the computer that will ask it to stop sending data. Say you wrote a function that listens to the USB channel for a signal from the computer. Upon receipt of the signal, it stops sending data over USB. The problem is that if you run the listener function and it is blocking, your device cannot do any other operations, including processing and sending the sensor data.

Similarly, if you are using delay() to time the sending of data over USB, you are blocking and you could miss the signal from the computer requesting that data stop being sent.

So, it is important that the listening and sending happen asynchronously. While waiting for the signal from the computer, the microprocessor is free to do other tasks. Similarly, while the microprocessor is waiting to send data over USB, it should be able to do other tasks.

A function that can run asynchronous returns so that the program can proceed and then completes its tasks when possible.

Serial.print(), Serial.println(), and Serial.write() are asynchronous

Writing data over USB is asynchronous. In most cases, Serial.print(), Serial.println(), and Serial.write() will almost immediately return and the microcontroller will continue to the next lines of your sketch. Meanwhile, the data is sent over USB while the microcontroller is busy with other tasks. These functions do this by putting the bytes that need to be transfered into an output buffer, which is then processed by the USB interface chip and sent to your computer.

The exception to this behavior is when the output buffer is full. In this case, the microcontroller has no place to dump the bytes that need to be written, so it has to wait (and block) until the buffer is free.


Thinking exercise 3: Other asynchronous functions

Think back to some previous exercises.

a) Does tone() operate asynchronously?

b) Does analogWrite() operate asynchronously?

The answer to this exercise is at the bottom of this lesson.


Avoiding delay()

As a rule of thumb, you should avoid using delay() because it is blocking. The function is really convenient when you are just learning or debugging with occasional serial output, but should not really be used beyond that.

How do we go about avoiding delay? Again, this is best learned by example.


Follow-along exercise 5: Serial output without delay

Consider the same setup you had for the follow-along exercise on serial communication, the schematic of which is shown below.

potentiometer schematic

We wish to write voltage values via USB, but without using the blocking function delay(). To do this, we manually keep track of the amount of time that has passed sine the last reset. The millis() function returns this time in milliseconds. The number returned by millis() is an unsigned long, so it is important to declare any variable storing its output as such. Similarly, the function micros() returns the amount of time since the last reset in units of microseconds. Note, though, that because the Arduino Uno board operates at about 16 MHz, the resolution of micros() is four µs.

So, we use the millis() function to capture when we last sent data. We then use the millis() function again to get the current time. When the difference between the current time and the last time we sent data is at least as large as the time we want between sending data, we then go ahead and send the data. We also set the last time that data was sent to the current time, and then continue. This is implemented in the code below.

// Which pin we will read from
const int sensorPin = A0;

// How often to write the result to serial in milliseconds
const long reportInterval = 100;

// Baud rate (must be long)
const long baudRate = 115200;

// Global variable to keep track of the last writeout
unsigned long timeLastWrite;


void setup() {
  Serial.begin(baudRate);

  // Initialize timing
  timeLastWrite = millis();
}


void loop() {
  // Grab the current time
  unsigned long currTime = millis();

  if (currTime - timeLastWrite >= reportInterval) {
    // Record time of last write
    timeLastWrite = currTime;

    // Use ADC to get 10-bit integer sensor value
    int sensorVal = analogRead(sensorPin);

    // Convert to voltage
    float voltage = sensorVal / 1023.0 * 5.0;

    // Write the voltage out to two decimal places
    Serial.println(String(voltage, 2));
  }
}

Note that I am careful with types, making sure to use longs and unsigned longs where appropriate. Note also that it is important that timeLastWrite is a global variable. Remember that loop() must not take any arguments. We therefore cannot pass timeLastWrite as a static long, for example. Since it needs to retain its value each time loop() is called, which happens over and over again, we have to have timeLastWrite as a global variable.

Go ahead and run the code, checking the Serial Monitor, to make sure it runs ok.


Do-it-yourself exercise 4: K.I.T.T. scanner bar

Now that you have some experience coding Arduino and putting elements together on bread boards, you are ready to try exercises that are a bit more involved.

In the hit 1982 television show Knight Rider, the main character Michael Knight (played by the incomparable David Hasselhoff) drives the most awesome car ever, a modified Trans-Am called K.I.T.T., pronounced “kit.” If you watch the show’s opening credits, you can see that an oscillating scanner bar on the front of the car features heavily in its awesomeness. You can see a close-up of the scanner bar here. Note that this scanner bar is from later in the series and features eight lights, while the one in the original pilot and opening credits features six.

For this exercise, you will make an LED array that lights up like K.I.T.T.’s scanner bar in the opening credits of the series. To this end, you will line up six LEDs (do not forget to connect them to 220 Ω resistors.) You can vary their light intensity using pulse width modulation. Do not use delays, and try to get the pattern of lighting as similar as possible to K.I.T.T.’s from the videos.


Answer to the asychronous functions exercise

a) Yes, tone() operates asynchronously. It sends a square wave with a 50% duty ratio at a given frequency on a given pin, and continues to do so even while the microcontroller performs other tasks. It therefore does not block.

b) Yes, analogWrite() operates asynchronously. It sends square waves with a given duty ratio at a frequency of 490 Hz (980 Hz for pins 5 and 6), and continues to do so even while the microcontroller performs other tasks. It therefore does not block.