TinyML Cookbook

By Gian Marco Iodice
    What do you get with a Packt Subscription?

  • Instant access to this title and 7,500+ eBooks & Videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Chapter 2: Prototyping with Microcontrollers

About this book

This book explores TinyML, a fast-growing field at the unique intersection of machine learning and embedded systems to make AI ubiquitous with extremely low-powered devices such as microcontrollers.

The TinyML Cookbook starts with a practical introduction to this multidisciplinary field to get you up to speed with some of the fundamentals for deploying intelligent applications on Arduino Nano 33 BLE Sense and Raspberry Pi Pico. As you progress, you’ll tackle various problems that you may encounter while prototyping microcontrollers, such as controlling the LED state with GPIO and a push-button, supplying power to microcontrollers with batteries, and more. Next, you’ll cover recipes relating to temperature, humidity, and the three “V” sensors (Voice, Vision, and Vibration) to gain the necessary skills to implement end-to-end smart applications in different scenarios. Later, you’ll learn best practices for building tiny models for memory-constrained microcontrollers. Finally, you’ll explore two of the most recent technologies, microTVM and microNPU that will help you step up your TinyML game.

By the end of this book, you’ll be well-versed with best practices and machine learning frameworks to develop ML apps easily on microcontrollers and have a clear understanding of the key aspects to consider during the development phase.

Publication date:
April 2022
Publisher
Packt
Pages
344
ISBN
9781801814973

 

Chapter 2: Prototyping with Microcontrollers

Deploying machine learning (ML) applications on microcontrollers is cool because what we develop doesn't just live within our computer's brain. Instead, it can animate many things around us. Therefore, before diving into the ML world, let's take a glance at how to build basic applications on microcontrollers from a software and hardware perspective.

In this chapter, we will deal with code-debugging and present how to transmit data to the Arduino serial monitor. Next, we will discover how to program GPIO peripherals with the Arm Mbed API and use the solderless breadboard to connect external components such as LEDs and push-buttons. At the end of the chapter, we will see how to power the Arduino Nano and Raspberry Pi Pico with batteries.

The aim of this chapter is to cover the relevant microcontroller programming basics for the following topics in this book.

In this chapter, we're going to cover the following recipes:

  • Code debugging 101
  • Implementing an LED status indicator on the breadboard
  • Controlling an external LED with the GPIO
  • Turning an LED on and off with a ush-button
  • Using interrupts to read the push-button state
  • Powering microcontrollers with batteries
 

Technical requirements

To complete all the practical recipes of this chapter, we will need the following:

  • An Arduino Nano 33 BLE Sense board
  • A Raspberry Pi Pico board
  • A micro-USB cable
  • 1 x half-size solderless breadboard (30 rows and 10 columns)
  • 1 x red LED
  • 1 x 220 Ω resistor
  • 1 x 3 AA battery holder (Raspberry Pi Pico only)
  • 1 x 4 AA battery holder (Arduino Nano only)
  • 4 x AA batteries
  • 1 x push-button
  • 5 x jumper wires
  • Laptop/PC with either Ubuntu 18.04+ or Windows 10 on x86-64

The source code and additional material are available in the Chapter02 folder on the GitHub repository (https://github.com/PacktPublishing/TinyML-Cookbook/tree/main/Chapter02).

 

Code debugging 101

Code debugging is a fundamental process of software development to uncover errors in code.

This recipe will show how to perform print debugging on an Arduino Nano and Raspberry Pi Pico by transmitting the following strings to the serial terminal:

  • Initialization completed: Once we have completed the initialization of the serial port
  • Executed: After every 2 seconds

The following Arduino sketch contains the code referred to in this recipe:

  • 01_printf.ino:

https://github.com/PacktPublishing/TinyML-Cookbook/blob/main/Chapter02/ArduinoSketches/01_printf.ino

Getting ready

All programs are prone to bugs, and print debugging is a basic process that prints statements on the output terminal to give insight into the program execution, as shown in the following example:

int func (int func_type, int a) {
  int ret_val = 0;
  switch(func_type){
    case 0:
      printf("FUNC0\n");
      ret_val = func0(a)
      break;
    default:
      printf("FUNC1\n");
      ret_val = func1(a);
  }
  return ret_val;
}

To get ready with this first recipe, we only need to know how the microcontroller can send messages on the serial terminal.

The Arduino programming language offers a similar function to printf(), the Serial.print() function.

This function can send characters, numbers, or even binary data from the microcontroller board to our computer through the serial port, commonly called UART or USART. You can refer to https://www.arduino.cc/reference/en/language/functions/communication/serial/print/ for the complete list of input arguments.

How to do it...

Note

The code reported in this recipe is valid for both the Arduino Nano and Raspberry Pi Pico. The Arduino IDE, in fact, will compile the code accordingly with the selected platform in the device drop-down menu.

Open the Arduino IDE and create a new empty project by clicking on Sketchbook from the leftmost menu (EDITOR) and then click on NEW SKETCH, as shown in the following figure:

Figure 2.1 – Click on the NEW SKETCH button to create a new project

Figure 2.1 – Click on the NEW SKETCH button to create a new project

As we saw in Chapter 1, Getting Started with TinyML, all sketches require a file containing the setup() and loop() functions.

The following steps will show what to write in these functions to implement our print debugging recipe:

  1. Initialize the UART baud rate in the setup() function and wait until the peripheral is open:
    void setup() {
      Serial.begin(9600);
      while (!Serial);

In contrast to the standard C library printf function, the Serial.print() function requires initialization before transmitting data. Therefore, we initialize the peripheral with the Arduino Serial.begin() function, which only requires the baud rate as an input argument. The baud rate is the data transmission rate in bits per second, and it is set to 9600 bps.

However, we can't use the peripheral immediately after the initialization because we should wait until it is ready to transmit. So, we use while(!Serial) to wait until the serial communication is open.

  1. Print Initialization completed after Serial.begin() in the setup() function:
      Serial.print("Initialization completed\n");
    }

We transmit the string Initialization completed with Serial.print("Initialization completed\n") to report the completion of the initialization.

  1. Print Executed every 2 seconds in the loop() function:
    void loop() {
      delay(2000);
      Serial.print("Executed\n");
    }      

Since the loop() function is called iteratively, we use the Arduino's delay() function to pause the program execution for 2 seconds. delay() accepts the amount of time in milliseconds (1 s = 1000 ms) as an input argument.

Now, make sure the device is plugged into your computer through the micro-USB cable.

If the device is recognized, we can open the serial monitor by clicking on Monitor from the Editor menu. From there, we will see any data transmitted by the microcontroller through the UART peripheral. However, before any communication starts, ensure the serial monitor uses the same baud rate as the microcontroller peripheral (9600), as shown in the following figure:

Figure 2.2 – The serial monitor must use the same baud rate as the UART's peripheral

Figure 2.2 – The serial monitor must use the same baud rate as the UART's peripheral

With the serial monitor open, we can click on the arrow near the device drop-down menu to compile and upload the program to the target platform. Once the sketch has been uploaded, the serial monitor will receive the Initialization completed and Executed messages, as shown in the following screenshot:

Figure 2.3 – Expected output on the serial monitor

Figure 2.3 – Expected output on the serial monitor

As we can see from the serial monitor output, Initialization completed is printed once because the setup() function is just called when starting the program.

There's more

Print debugging is a simple debugging approach, but it has significant disadvantages with the increase of software complexity, such as the following:

  • Needing to re-compile and flash the board every time we add or move Serial.print().
  • Serial.print() costs in terms of program memory footprint.
  • We could make mistakes reporting the information (for example, using print to report an unsigned int variable that is actually signed).

We will not cover more advanced debugging in this book, but we recommend looking at serial wire debug (SWD) debuggers (https://developer.arm.com/architectures/cpu-architecture/debug-visibility-and-trace/coresight-architecture/serial-wire-debug) to make this process less painful. SWD is an Arm debug protocol for almost all Arm Cortex processors that you can use to flash the microcontroller, step through the code, add breakpoints, and so on with only two wires.

 

Implementing an LED status indicator on the breadboard

We have the chance to interact with the world around us with microcontrollers. For example, we can get data from sensors or perform physical actions, such as turning on and off an LED or moving an actuator.

In this recipe, we will learn how to connect external components with the microcontroller by building the following electronic circuit on the breadboard:

Figure 2.4 – LED power status indicator circuit

Figure 2.4 – LED power status indicator circuit

The preceding circuit uses a red LED to indicate whether the microcontroller is plugged into the power.

Getting ready

When connecting external components to the microcontroller, we mean physically joining two or more metal connectors together. Although we could solder these connectors, it is not usual for prototyping because it is not quick and straightforward.

Therefore, this Getting ready section aims to present a solderless alternative to connect our components effortlessly.

Making contacts directly with the microcontroller's pins can be extremely hard for the tiny space between each pin. For example, considering the RP2040 microcontroller, the pin space is roughly 0.5 mm since the chip size is 7x7 mm. Therefore, it would be practically impossible to connect any of our components safely since most terminals have a wire diameter of ~1 mm.

For this reason, our platforms provide alternative points of contact with wider spacing on the board. These contact points on the Arduino Nano and Raspberry Pi Pico are the two rows of pre-drilled holes located at the platform's edge.

The simplest way to know the correspondence between these contacts and the microcontroller pins is to refer to the datasheet of the microcontroller boards. Hardware vendors usually provide the pinout diagram to note the pins' arrangement and functionality.

For example, the following list reports the links to the Arduino Nano and Raspberry Pi Pico pinout diagrams:

  • Arduino Nano:

https://content.arduino.cc/assets/Pinout-NANOsense_latest.pdf

  • Rasberry Pi Pico:

https://datasheets.raspberrypi.org/pico/Pico-R3-A4-Pinout.pdf

On top of these pre-drilled holes, which often come with a 2.54 mm spacing, we can solder a header to insert and connect the electronic components easily.

The header can be either a male (pin header) or a female connector (socket header), as shown in the following figure:

Figure 2.5 – Male header versus female header

Figure 2.5 – Male header versus female header (image from https://en.wikipedia.org/wiki/Pin_header)

Important Note

We recommend buying devices with pre-soldered male headers if you are not familiar with soldering or just want a ready-to-go solution.

As we have seen, the boards provide a way to connect the external components with the microcontroller. However, how can we attach other electrical elements to build a complete electronic circuit?

Prototyping on a breadboard

The breadboard is a solderless prototyping platform to build circuits by pushing the device's pins in a rectangular grid of metal holes:

Figure 2.6 – Solderless breadboard

Figure 2.6 – Solderless breadboard

As shown in the previous figure, breadboards provide two connecting areas for our components:

  • Bus rails are usually located on both sides of the breadboard and consist of two columns of holes identified with the symbols + and as shown in the following diagram:
Figure 2.7 – Bus rails labeled with + and - on both sides of the breadboard

Figure 2.7 – Bus rails labeled with + and - on both sides of the breadboard

All the holes of the same column are internally connected. Therefore, we will have the same voltage through all its columns when applying a voltage to whatever hole.

Since bus rails are beneficial for having reference voltages for our circuits, we should never apply different voltages on the same bus column.

  • Terminal strips are located in the central area of the breadboard and join only the holes of the same row so that the following occurs:
    • Holes on the same row have the same voltage.
    • Holes on the same column might have a different voltage.

However, since we typically have a notch running parallel in the middle of the breadboard, we have two different terminal strips per row, as shown in the following figure:

Figure 2.8 – Terminal strips are located in the central area of the breadboard

Figure 2.8 – Terminal strips are located in the central area of the breadboard

We can place several devices on the breadboard and connect them through jumper wires.

Note

The size of a breadboard is defined by the number of rows and columns in the terminal area. In our case, we will always refer to a half-sized breadboard with 30 rows and 10 columns.

How to do it...

Before building any circuits, unplug the micro-USB cable from the microcontroller board to remove the possibility of unintentionally damaging any components.

Once we have disconnected the board from the power, follow the following steps to build the circuit to turn the LED on when the platform is plugged into the power:

  1. Put the microcontroller board on the breadboard:
Figure 2.9 – Vertically mount the microcontroller board between the left and right terminal strips

Figure 2.9 – Vertically mount the microcontroller board between the left and right terminal strips

Since we have a notch running parallel, it is safe to put the platforms in this way because the left and right pin headers touch two different terminal strips.

  1. Use two jumper wires to connect the 3.3 V and GND pins of the microcontroller board with the + and - bus rails:
Figure 2.10 – Use the jumper wires to connect the 3.3 V and GND to the + and - bus rails

Figure 2.10 – Use the jumper wires to connect the 3.3 V and GND to the + and - bus rails

It is important to note that all holes of the bus rails will have 3.3 V and GND, respectively, only when the microcontroller is connected to the power.

  1. Insert the LED pins on two terminal strips:
Figure 2.11 – Insert the LED on the breadboard

Figure 2.11 – Insert the LED on the breadboard

In the preceding figure, we insert the longer LED terminal in (H, 24) and the shorter one in (H, 25). Do not invert the longer and shorter terminals because then the LED won't turn on.

  1. Place the 220 Ω resistor in series with the LED:
Figure 2.12 – Place the resistor in series with the LED

Figure 2.12 – Place the resistor in series with the LED

The color bands of the resistor can be determined through the Digikey web tool (https://www.digikey.com/en/resources/conversion-calculators/conversion-calculator-resistor-color-code). For example, a 220Ω resistor with five or six bands is encoded with the following colors:

  • First band: red (2)
  • Second band: red (2)
  • Third band: black (0)
  • Fourth band: black (1)

As reported in the circuit presented at the beginning of this recipe, one terminal of the resistor should touch the shorter LED pin. In our case, we insert one terminal in (H, 25). The remaining terminal of the resistor goes in whichever unused terminal strip. In our case, we insert this terminal in (H, 28).

  1. Close the circuit by connecting the + bus rail (3.3 V) to the longer LED pin and the - bus rail (GND) to the resistor terminal:
Figure 2.13 – Close the circuit by connecting 3.3 V and GND

Figure 2.13 – Close the circuit by connecting 3.3 V and GND

The previous figure shows how to connect the two remaining jumper wires used to close the circuit. One jumper wire connects the + bus rail with the longer LED terminal (H, 24) while the other one connects the - bus rail with the resistor (H, 28).

Now, the LED should emit light whenever you plug the microcontroller into the power with the micro-USB cable.

 

Controlling an external LED with the GPIO

Nowadays, LEDs are everywhere, particularly in our houses, because they use less energy than older lights for the same luminous intensity. However, the LEDs considered for our experiments are not light bulbs but through-hole LEDs for rapid prototyping on the breadboard.

In this recipe, we will discover how to build a basic circuit with an external LED and program the GPIO peripheral to control its light.

The following Arduino sketch contains the code referred to in this recipe:

  • 03_gpio_out.ino:

https://github.com/PacktPublishing/TinyML-Cookbook/blob/main/Chapter02/ArduinoSketches/03_gpio_out.ino

Getting ready

To implement this recipe, we need to know how the LED works and how to program the microcontroller GPIO peripheral in output mode.

LED stands for Light-Emitting Diode and is a semiconductor component that emits light when the current flows through it.

A through-hole LED is made of the following:

  • A head of transparent material from where the light comes. The head can be of different diameters, but typically comes in 3mm, 5mm, and 10mm sizes.
  • Two legs (leads) of different lengths to identify the positive (anode) from the negative (cathode) terminal. The anode is the longer lead.

The following diagram shows the basic structure of a through-hole LED and its symbolic representation in an electronic circuit.

Figure 2.14 – LED with symbolic representation

Figure 2.14 – LED with symbolic representation

As mentioned, the LED emits light when the current flows through it. However, in contrast to the resistors, the current flows only in one direction, specifically from the anode to the cathode. This current is commonly called forward current (If).

The brightness of the LED is proportional to If, so the higher it is, the brighter it will appear.

The LED has a maximum operating current that we must not exceed to avoid breaking it instantly. For standard through-hole 5 mm LEDs, the maximum current is typically 20 mA, so values between 4 mA and 15 mA should be enough to see the LED emitting the light.

To allow the current to flow, we need to apply a specific voltage to the terminals' LED, called forward voltage (Vf). We define the Vf as:

We report the typical Vf range for some LED colors in the following table:

Figure 2.15 – Typical LED forward voltage

Figure 2.15 – Typical LED forward voltage

From the preceding table, we can observe the following about the forward voltage range:

  • It depends on the color.
  • It is narrow and less than the typical 3.3 V required to power a microcontroller in most cases.

From these observations, three questions come into mind:

  • First, how can we apply the forward voltage on the LED terminals since we typically only have 3.3 V from the microcontroller?
  • What happens if we apply a voltage lower than the minimum Vf?
  • What happens if we apply a voltage higher than the maximum Vf?

The answers rely on the following physical relationship between the voltage and current of the LED:

Figure 2.16 – Voltage-current (VI) characteristic of LED

Figure 2.16 – Voltage-current (VI) characteristic of LED

From the previous chart where the x and y axes report the voltage and current, we can deduce the following:

  • If we applied a voltage much lower than Vf to the LED, the LED would not turn on because the current would be low.
  • If we applied a voltage much higher than Vf on the LED, the LED would be damaged because the current would exceed the 20 mA limit.

Therefore, fixing the voltage at the required operating Vf is crucial to ensure that the device works and is not damaged.

The solution is simple and only requires a resistor in series with the LED, as shown in the following figure:

Figure 2.17 – The resistor in series with the LED limits the current

Figure 2.17 – The resistor in series with the LED limits the current

At this point, it should be clear why we included the resistor in the circuit of the previous recipe. Since the LED has a fixed voltage drop when it emits the light (Vf), the resistor limits the current at the value we want, such as 4 mA–15 mA. Therefore, having the LED current in the acceptable range means that the Vf does not fall out of the expected operating range.

We can calculate the resistor's value using the following formula:

Where:

  • Vf is the forward voltage.
  • If is the forward current.
  • R is the resistance.

The forward voltage/current and LED brightness information is generally available in the LED datasheet.

Now, let's see how we can control the status of this device with the GPIO peripheral.

Introducing the GPIO peripheral

General-purpose input/output (GPIO) is the most common and versatile peripheral on microcontrollers.

As the name suggests, GPIO does not have a fixed functionality. Instead, its primary function is to provide (output) or read (input) digital signals (1 or 0) through the external pins, commonly called either GPIO, IO, or GP.

A microcontroller can integrate several GPIO peripherals, where each one can control a dedicated pin of the integrated chip.

GPIO has similar behavior to std::cout and std::cin of the C++ iostream library but with the difference that it writes and reads fixed voltages rather than characters.

The commonly applied voltages for the logical 1 and 0 levels are as follows:

Figure 2.18 – Relation between logical levels and voltages

Figure 2.18 – Relation between logical levels and voltages

The LED blinking is a typical example of configuring the GPIO peripheral in output mode to supply either 3.3 V (1) or 0 V (0) programmatically.

There are two ways to connect the LED with the GPIO pin, and the direction of the current makes them different. The first way is current sourcing, where the current flows out of the microcontroller board. To do so, we need to do the following:

  • Connect the LED anode to the GPIO pin.
  • Connect the LED cathode to the resistor in the series.
  • Connect the remaining resistor terminal to GND.

The following circuit shows how to drive an LED with a current sourcing circuit:

Figure 2.19 – Current sourcing. The current goes out of the microcontroller board

Figure 2.19 – Current sourcing. The current goes out of the microcontroller board

From the preceding circuit, we can observe that the GPIO pin should supply the logical level 1 to turn on the LED.

The second and opposite way is current sinking, where the current flows into the microcontroller board. In this case, we need to do the following:

  • Connect the LED cathode to the GPIO pin.
  • Connect the LED anode to the resistor in series.
  • Connect the remaining resistor terminal to 3.3 V.

As we can observe from the following circuit, the GPIO pin should supply the logical level 0 to turn on the LED:

Figure 2.20 – Current sinking. The current goes into the microcontroller board

Figure 2.20 – Current sinking. The current goes into the microcontroller board

Whatever solution we adopt, it is essential to keep in mind that the pin has limits on the maximum current, which can be different depending on its direction. For example, the Arduino Nano has a maximum output current of 15 mA and a maximum input current of 5 mA. So, when designing the circuit to drive the LED, we should always consider these limitations for correctly operating and not damaging the device.

How to do it...

Disconnect the microcontroller boards from the power and keep the LED and resistor on the breadboard as in the previous recipe. However, unplug all the jumper wires except the one connected to the - bus rail (GND). The following diagram shows what you should have on the breadboard:

Figure 2.21 – We keep the microcontroller board, LED, and resistor from the Implementing an LED status indicator on the breadboard recipe

Figure 2.21 – We keep the microcontroller board, LED, and resistor from the Implementing an LED status indicator on the breadboard recipe

Since the LED cathode is connected to the terminal resistor, the LED will be driven by a current sourcing circuit.

The following steps will show how to control the LED light through the GPIO peripheral:

  1. Choose the GPIO pin to drive the LED. The following table reports our choice:
Figure 2.22 – GPIO pin selected for driving the LED

Figure 2.22 – GPIO pin selected for driving the LED

  1. Connect the LED anode to the GPIO pin with a jumper wire:
Figure 2.23 – Connect the LED anode to the GPIO pin

Figure 2.23 – Connect the LED anode to the GPIO pin

On the Arduino Nano, we use a jumper wire to connect (J, 6) with (J, 24). On the Raspberry Pi Pico, we use a jumper wire to connect (J, 12) with (J, 24).

  1. Connect the terminal resistor to GND:
Figure 2.24 – Connect the resistor to GND

Figure 2.24 – Connect the resistor to GND

On both the Arduino Nano and Raspberry Pi Pico, we connect (J, 28) with the - bus rail.

The 220Ω resistor imposes an LED current of ~5 mA, which is below the maximum 20 mA LED current and below the maximum output GPIO current, as reported in the following table:

Figure 2.25 – Max GPIO current (sourcing) on the Arduino Nano and Raspberry Pi Pico

Figure 2.25 – Max GPIO current (sourcing) on the Arduino Nano and Raspberry Pi Pico

Once the circuit is ready, we can focus on the GPIO programming.

  1. Open the Arduino IDE and create a new sketch. Declare and initialize a global mbed::DigitalOut object with the pin name used for driving the LED.

For the Arduino Nano, we have the following:

mbed::DigitalOut led(p23); 

And this for the Raspberry Pi Pico:

mbed::DigitalOut led(p22); 

Mbed, or rather Mbed OS (https://os.mbed.com/), is a real-time operating system (RTOS) specifically for Arm Cortex-M processors, which offers functionalities typical of a canonical OS and drivers to control microcontroller peripherals. All programs on the Arduino Nano 33 BLE Sense board and Raspberry Pi Pico are built on top of this tiny operating system. In this recipe, we use the mbed::DigitalOutput object (https://os.mbed.com/docs/mbed-os/v6.15/apis/digitalout.html) from Mbed OS to interface with the GPIO peripheral in output mode. The peripheral initialization requires the GPIO pin (PinName) connected to the LED. PinName always starts with the letter p, followed by the pin number.

On the Arduino Nano, the pin number is obtained from the y number reported in the pin label P<x>.<y>. Therefore, PinName is p23.

On the Raspberry Pi Pico, the pin number is obtained from the y number reported in the label GPy. Therefore, PinName is p22.

  1. Set led to 1 for turning on the LED in the loop() function:
    void loop() {
      led = 1;
    }

Compile the sketch and upload the program to the microcontroller.

 

Turning an LED on and off with a push-button

In contrast to a PC where the keyboard, mouse, or even a touchscreen facilitates human interactions with the software applications, a physical button represents the easiest way for a user to interact with a microcontroller.

This recipe will teach us how to program the GPIO to read the status of a push-button (pushed or released) to control the LED light.

The following Arduino sketch contains the code referred to in this recipe:

  • 04_gpio_in_out.ino:

https://github.com/PacktPublishing/TinyML-Cookbook/blob/main/Chapter02/ArduinoSketches/04_gpio_in_out.ino

Getting ready

To get ready for this recipe, we need to know how this device works and program the GPIO peripheral in input mode.

The push-button is a type of button used with microcontrollers, and it has boolean behavior since its state can either be pushed (true) or released (false).

From an electronics point of view, a push-button is a device that makes (a.k.a. short) or breaks (a.k.a. open) the connection between two wires. When we press the button, we connect the wires through a mechanical system, allowing the current to flow. However, it is not like a standard light switch that keeps the wires connected when released. When we don't apply pressure to the button, the wires disconnect, and the current stops flowing.

Although this device has four metal legs, it is a two-terminal device because the contacts on the opposite side (1, 4 and 2, 3) are connected, as shown in the following figure:

Figure 2.26 – Push-button representation

Figure 2.26 – Push-button representation

When building a circuit with this component, the legs on the same side (1,2 or 4,3 in the preceding figure) are responsible for connecting two points. These two points will have the same voltage when the push-button is pressed.

The state of a push-button can be read with the GPIO peripheral in input mode. When configuring the GPIO in input mode, the peripheral reads the applied voltage on the pin to infer the logical level. From this value, we can guess whether the button is pressed. 

In the following diagram, the voltage on the GPIO pin is GND when we press the button. However, what is the voltage when the button is released?

Figure 2.27 – What is the voltage on the GPIO pin when we release the push-button?

Figure 2.27 – What is the voltage on the GPIO pin when we release the push-button?

Although the pin could only assume two logical levels, this could not be true in some input mode circumstances. A third logical level called floating (or high impedance) could occur if we do not take circuit precautions. When the floating state occurs, the pin's logical level is undefined because the voltage fluctuates between 3.3 V and GND. Since the voltage is not constant, we cannot know whether the push-button is pressed. To prevent this problem, we must include a resistor in our circuit to always have a well-defined logical level under all conditions.

Depending on what logical level we want in the pushed state, the resistor can be as follows:

  • Pull-up: The resistor connects the GPIO pin to the 3.3 V. Thus, the GPIO pin reads LOW in the pushed state and HIGH in the released state.
  • Pull-down: The resistor connects the GPIO pin to GND in contrast to the pull-up configuration. Thus, the GPIO pin reads the logical level HIGH in the pushed state and LOW in the released state.

The following diagram shows the difference between the pull-up and pull-down configurations:

Figure 2.28 – Pull-up versus pull-down configurations

Figure 2.28 – Pull-up versus pull-down configurations

Typically, a 10 K resistor should be okay for both cases. However, most microcontrollers offer an internal and programmable pull-up resistor so the external one is often not needed.

How to do it...

Keep all the components on the breadboard. The following steps will show what to change in the previous sketch to control the LED status with the push-button:

  1. Choose the GPIO pin for reading the push-button state. The following table reports our choice.
Figure 2.29 – GPIO pin used to read the push-button state

Figure 2.29 – GPIO pin used to read the push-button state

  1. Mount the push-button between the breadboard's left and right terminal strips:
Figure 2.30 – The push-button is mounted between the terminal strips 21 and 23

Figure 2.30 – The push-button is mounted between the terminal strips 21 and 23

As we can observe from the preceding diagram, we use terminal strips not employed by other devices.

  1. Connect the push-button to the GPIO pin and GND:
Figure 2.31 – The push-button is only connected to the GPIO pin and GND

Figure 2.31 – The push-button is only connected to the GPIO pin and GND

The floating state will not occur because we use the microcontroller pull-up resistor.

  1. Open the sketch developed in the previous recipe. Declare and initialize a global mbed::DigitalIn object with the pin name used for the push-button.

For the Arduino Nano:

mbed::DigitalIn button(p30);

And this for the Raspberry Pi Pico:

mbed::DigitalIn button(p10);

mbed::DigitalIn (https://os.mbed.com/docs/mbed-os/v6.15/apis/digitalin.html) is used to interface with the GPIO peripheral in input mode. The initialization only requires the GPIO pin (PinName) connected to the push-button.

  1. Set the button mode to PullUp in the setup() function:
    void setup() {
      button.mode(PullUp);
    }

The preceding code enables the microcontroller's internal pull-up resistor.

  1. Turn on the LED when the push-button is LOW (0) in the loop() function:
    void loop() {
      led = !button;
    }

We just need to set the led object to the opposite value returned by button to light up the LED when the push-button is pressed.

Compile the sketch and upload the program to the microcontroller.

Tip

When the push-button is pressed, the switch could generate spurious logical-level transitions due to the mechanical nature of the component. This issue is called button bouncing because the switch response bounces between HIGH and LOW for a short time. You may consider adopting a switch debouncing algorithm (for example, https://os.mbed.com/teams/TVZ-Mechatronics-Team/wiki/Timers-interrupts-and-tasks) to prevent the generation of multiple transitions.

 

Using interrupts to read the push-button state

The previous recipe explained how to read digital signals with the GPIO peripheral. However, the proposed solution is inefficient because the CPU wastes cycles waiting for the button to be pressed while it could do something else in the meantime. Furthermore, this could be a scenario where we would keep the CPU in low-power mode when there is nothing else to do.

This recipe will teach us how to read the push-button state efficiently by using the interrupts on the Arduino Nano.

The following Arduino sketch contains the code referred to in this recipe:

  • 05_gpio_interrupt.ino:

https://github.com/PacktPublishing/TinyML-Cookbook/blob/main/Chapter02/ArduinoSketches/05_gpio_interrupt.ino

Getting ready

Let's prepare this recipe by learning what an interrupt is and what Mbed OS API we can use to read the push-button efficiently.

An interrupt is a signal that temporarily pauses the main program to respond to an event with a dedicated function, called an interrupt handler or interrupt service routine (ISR). Once the ISR ends the execution, the processor resumes the main program from the point it was left at, as shown in the following diagram:

Figure 2.32 – Interrupt pauses the main program temporarily

Figure 2.32 – Interrupt pauses the main program temporarily

The interrupt is a powerful mechanism to save energy because the CPU could enter the sleep state and wait for an event before starting the computation.

A microcontroller has several interrupt sources, and for each one, we can program a dedicated ISR.

Although the ISR is a function, there are limitations to its implementation:

  • It does not have input arguments.
  • It does not return a value. Therefore, we need to use global values to report status changes.
  • It must be short to not steal too much time from the main program. We want to remind you that the ISR is not a thread since the processor can only resume the computation when the ISR finishes.

For GPIO peripherals in input mode, we can use the mbed::InterruptIn (https://os.mbed.com/docs/mbed-os/v6.15/apis/interruptin.html) object to trigger an event whenever the logical level on the pin changes:

Figure 2.33 – Rising interrupt versus falling interrupt

Figure 2.33 – Rising interrupt versus falling interrupt

As we can observe from the preceding diagram, mbed::InterruptIn can trigger interrupts when the logical level on the pin goes from LOW to HIGH (rising interrupts) or HIGH to LOW (falling interrupt).

How to do it...

Open the sketch built in the previous recipe and follow these steps to turn on and off the LED with the GPIO interrupt:

  1. Define and initialize the mbed::InterruptIn object with the PinName of the GPIO pin connected to the push-button.

For the Arduino Nano:

mbed::InterruptIn button(p30);

For the Raspberry Pi Pico:

mbed::InterruptIn button(p10);

The mbed::DigitalIn object is not required anymore since mbed::InterruptIn also controls the interface with the GPIO peripheral in input mode.

  1. Write an ISR for handling the interrupt request on the rising edge (LOW to HIGH) of the input signal:
    void rise_ISR() {
      led = 0;
    }

The LED is turned off when the preceding ISR is called (led = 0).

Next, write an ISR for handling the interrupt request on the falling edge (HIGH to LOW) of the input signal:

void fall_ISR() {
  led = 1;
}

The LED switches on when the preceding ISR is called (led = 1).

  1. Initialize button in the setup() function:
    void setup() {
      button.mode(PullUp);
      button.rise(&rise_ISR);
      button.fall(&fall_ISR);
    }

We configure the mbed::InterruptIn object by doing the following:

  • Enabling the internal pull-up resistor (button.mode(PullUp))
  • Attaching the ISR function to call when the rising interrupt occurs (button.rise(&rise_ISR))
  • Attaching the ISR function to call when the falling interrupt occurs (button.fall(&fall_ISR))
  1. Replace the code in the loop() function with delay(4000):
    void loop() {
      delay(4000);
    }

In theory, we could leave the loop() function empty. However, we recommend calling delay() when nothing has to be done because it can put the system in low-power mode.

Compile the sketch and upload the program to the microcontroller.

 

Powering microcontrollers with batteries

For many TinyML applications, batteries could be the only power source for our microcontrollers.

In this final recipe, we will learn how to power microcontrollers with AA batteries.

The following Colab notebook contains the code referred to in this recipe:

  • 06_estimate_battery_life.ipynb:

https://github.com/PacktPublishing/TinyML-Cookbook/blob/main/Chapter02/ColabNotebooks/06_estimate_battery_life.ipynb

Getting started

Microcontrollers don't have a built-in battery, so we need to supply an external one to make the device work when it is not connected through the micro-USB cable.

To get ready for this recipe, we need to know what types of batteries we need and how we can use them correctly to supply power.

Batteries are sources of electric power and have a limited energy capacity. The energy capacity (or battery capacity) quantifies the energy stored and is measured in milli-ampere-hour (mAh). Therefore, a higher mAh implies a longer battery life.

The following table reports some commercial batteries that find applicability with microcontrollers:

Figure 2.34 – Suitable commercial batteries for microcontrollers

Figure 2.34 – Suitable commercial batteries for microcontrollers

The battery selection depends on the required microcontroller voltage and other factors such as energy capacity, form factor, and operating temperature.

As we can observe from the preceding table, the AA battery provides a higher capacity, but it supplies 1.5 V, typically insufficient for microcontrollers.

Therefore, how can we power microcontrollers with AA batteries?

In the following subsections, we will show standard techniques to either increase the supplied voltage or the energy capacity.

Increasing the output voltage by connecting batteries in series

When connecting batteries in series, the positive terminal of one battery is connected to the negative terminal of the other one, as shown in the following figure:

Figure 2.35 – Batteries in series

Figure 2.35 – Batteries in series

Important Note

This approach will not extend the battery capacity but just the supplied voltage.

The new supplied voltage () is as follows:

Where N is the number of connected batteries in series.

For example, since one AA battery supplies 1.5 V for 2400 mAh, we could connect two AA batteries in series to produce 3.0 V for the same energy capacity.

However, if the battery capacity is not enough for our application, how can we increase it?

Increasing the energy capacity by connecting batteries in parallel

When connecting batteries in parallel, the positive terminals of the batteries are tied together with one wire. The same applies to the negative terminals, which are joined together as shown in the following figure:

Figure 2.36 – Batteries in parallel

Figure 2.36 – Batteries in parallel

Important Note

This approach will not increase the output voltage but just the battery capacity.

The new battery capacity () is as follows:

Where N is the number of connected batteries in parallel.

For example, since one AA battery has a battery capacity of 2400 mAh, we could connect two AA batteries in parallel to increase the battery capacity by two times.

Now that we know how to connect multiple batteries together to get the desired output voltage and energy capacity, let's see how we can use them to power the microcontrollers.

Connecting batteries to the microcontroller board

Microcontrollers have dedicated pins for supplying power through external energy sources, such as batteries. These pins have voltage limits, commonly reported in the datasheet.

On the Arduino Nano, the external power source is supplied through the Vin pin. The Vin input voltage can range from 5 V–21 V.

On the Raspberry Pi Pico, the external power source is supplied through the VSYS pin. The VSYS input voltage can range from 1.8 V – 5.5 V.

On both platforms, the onboard voltage regulator will convert the supplied voltage to 3.3 V.

How to do it...

Disconnect the Arduino Nano and Raspberry Pi Pico from the micro-USB and keep all the components on the breadboard.

The battery holder considered for this recipe connects the AA batteries in series. We recommend not inserting the batteries in the battery holder yet. The batteries should only be inserted when the electric circuit is completed.

The following steps will show how to power the Arduino Nano and Raspberry Pi Pico with batteries:

  1. Connect the positive (red) and negative (black) wires of the battery holder to the + and bus rails respectively:
Figure 2.37 – Connect the battery holder to the bus rails

Figure 2.37 – Connect the battery holder to the bus rails

  1. The Arduino Nano and Raspberry Pi Pico have different voltage limits for the external power source. Therefore, we cannot use the same number of AA batteries on both platforms. In fact, three AA batteries are enough for the Raspberry Pi Pico but not for the Arduino Nano. In contrast, four AA batteries are enough for the Arduino Nano but beyond the voltage limit on the Raspberry Pi Pico. For this reason, we use a 4 x AA battery holder for the Arduino Nano to supply 6 V and a 3 x AA battery holder for the Raspberry Pi Pico to supply 4.5 V.
  2. Connect the external power source to the microcontroller board, as shown in the following diagram:
Figure 2.38 – Connect the bus rails to the microcontroller power pin and GND

Figure 2.38 – Connect the bus rails to the microcontroller power pin and GND

As you can observe from the preceding figure, VIN (Arduino Nano) and VSYS (Raspberry Pi Pico) are connected to the positive battery holder terminal through the + bus rail.

  1. Insert the batteries in the battery holder:
    • 4 x AA batteries for the Arduino Nano
    • 3 x AA batteries for the Raspberry Pi Pico

The LED application should now work again.

However, one thing we might be curious about is how can we evaluate the lifetime of a battery-powered application?

There's more

Once we have chosen the battery for the microcontroller, we can estimate its lifetime with the following formula:

Where:

Figure 2.39 – Physical quantities of the battery lifetime estimate formula

Figure 2.39 – Physical quantities of the battery lifetime estimate formula

The following Python code calculates the battery life in hours and days:

battery_cap_mah = 2400
i_load_ma = 1.5
 
battery_life_hours = battery_cap_mah / i_load_ma
battery_life_days = battery_life_hours / 24
 
print("Battery life:", battery_life_hours,"hours,", battery_life_days, "days")

The preceding code estimates the battery life for the case when the battery capacity (battery_cap_mah) is 2400 mAh, and the load current (i_load_ma) is 1.5 mA.

The expected output is as follows:

Figure 2.40 – Expected output from the battery life estimator

Figure 2.40 – Expected output from the battery life estimator

Although the formula above is an estimation and valid under ideal conditions, it is enough to understand how long the system could last. A better model could include other factors such as battery self-discharge and temperature.

About the Author

  • Gian Marco Iodice

    Gian Marco Iodice is team and tech lead in the Machine Learning Group at Arm, who co-created the Arm Compute Library in 2017. The Arm Compute Library is currently the most performant library for ML on Arm, and it's deployed on billions of devices worldwide – from servers to smartphones.

    Gian Marco holds an MSc degree, with honors, in electronic engineering from the University of Pisa (Italy) and has several years of experience developing ML and computer vision algorithms on edge devices. Now, he's leading the ML performance optimization on Arm Mali GPUs.

    In 2020, Gian Marco cofounded the TinyML UK meetup group to encourage knowledge-sharing, educate, and inspire the next generation of ML developers on tiny and power-efficient devices.

    Browse publications by this author
TinyML Cookbook
Unlock this book and the full library FREE for 7 days
Start now