Simplify ARM® Cortex®-M0+-Based IoT Embedded Design with CircuitPython Dev Boards

Contributed By Digi-Key's North American Editors

Many embedded applications use advanced MCUs but require only basic hardware control capabilities, not the “hard real-time” requirements of advanced embedded designs. Yet too often, developers and makers get immersed in the details of hardware design, C/C++ programming, and real-time operating systems. Fortunately, there is an easier way.

This article will show a more accessible approach using a pair of tiny development boards from Adafruit Industries, which combine an embedded design variant of the Python programming language with a sophisticated 32-bit MCU based on the ARM Cortex-M0+ processor.

Advanced MCU simplifies design

Advanced MCUs help simplify hardware design by integrating a full complement of analog and digital peripherals with powerful processor cores. For example, the Microchip Technology ATSAMD21G18 MCU combines an ARM Cortex-M0+ core, 256 Kbytes of flash, 32 Kbytes of SRAM, advanced control subsystems, and a host of peripherals, all in a 10 x 10 mm thin profile quad flat pack (TQFP) package (Figure 1).

Diagram of Microchip Technology SAM D21 MCU family

Figure 1: Based on the ultra-low-power ARM® Cortex®-M0+, the Microchip Technology SAM D21 MCU family members all provide a comprehensive set of functional blocks and peripherals, differing only in the specific amount of memory and peripheral channels. (Image source: Microchip Technology)

Along with 32 GPIOs, the ATSAMD21G18 MCU’s peripheral set includes multiple advanced serial communication (SERCOM) channels, waveform output channels, a multi-channel 12-bit analog-to-digital converter (ADC), analog comparators, and a 10-bit digital-to-analog converter (DAC).

Design challenges

This class of advanced MCU eliminates the need for developers to spend time finding and interfacing external peripherals, but it still imposes strict requirements on how the MCU itself is designed into a system. For example, in integrating multiple types of circuits, the ATSAMD21G18 MCU’s design supplies power through a corresponding separate set of domains. As a result, developers must deal with multiple power and ground pins for the processor core VDDCORE, its internal regulator (VDDIN), peripherals (VDDIO), and analog blocks (VDDANA) (Figure 2).

During design, developers need to follow specific recommendations for supplying power, connecting to ground, and selecting and placing decoupling capacitors – nothing unusual for an experienced engineer, but a potential pitfall for developers new to embedded MCU hardware design.

Image of Microchip Technology ATSAMD21G18 MCU

Figure 2: The Microchip Technology ATSAMD21G18 MCU uses multiple power domains for supplying its diverse analog and digital blocks – and requires care in supplying power to those domains. (Image source: Microchip Technology)

Similarly, software development with these devices can seem overwhelming. Typically, new embedded systems developers find themselves caught up in the details associated with learning C/C++ from embedded development materials geared more for applications with hard real-time requirements. These applications typically have critical timing requirements for interrupt latency and deterministic response. However, many emerging sensor designs for the Internet of things (IoT) need (or can easily tolerate) much more relaxed requirements for data acquisition or actuator operation.

Simplified embedded development

Designed to eliminate these hardware and software hurdles for embedded developers, a set of development boards from Adafruit offer a particularly effective solution across a range of application requirements. Based on the ATSAMD21G18 MCU, the Adafruit Metro M0 Express and Feather M0 Express each offer a complete embedded system including serial interfaces (USB, SPI, I2C, and UART), pulse-width modulation (PWM), interrupt input, as well as multiple analog IOs and GPIOs. The boards differ only in size and number of GPIOs: the 2.8" x 2.1" x 0.28" Metro M0 Express provides 25 GPIOs, while the slightly smaller (2.0" x 0.9" x 0.28") Feather M0 Express offers 20 GPIOs.

As with most advanced MCUs, the SAM D21 MCU family provides far more peripheral channels than physical pins, but provides a pin mapping feature to allocate peripheral functionality to specific hardware pins. As a result, even with their small size, each board uses shared pins to deliver the full range of the MCU’s extensive peripheral capabilities (Figure 3).

Diagram of Adafruit Feather M0 Express development board

Figure 3: Adafruit takes advantage of pin multiplexing to provide a generous subset of the ATSAMD21G18’s peripheral functionality in its tiny Feather M0 Express development board. (Image source: Adafruit)

For developers, however, these details are transparent. Adafruit already provides the specific configuration for each board in specific modules in its open source software package (Listing 1).

STATIC const mp_rom_map_elem_t board_global_dict_table[] = {

    { MP_ROM_QSTR(MP_QSTR_A0), MP_ROM_PTR(&pin_PA02) },

    { MP_ROM_QSTR(MP_QSTR_A1), MP_ROM_PTR(&pin_PB08) },

    { MP_ROM_QSTR(MP_QSTR_A2), MP_ROM_PTR(&pin_PB09) },

    { MP_ROM_QSTR(MP_QSTR_A3), MP_ROM_PTR(&pin_PA04) },

    { MP_ROM_QSTR(MP_QSTR_A4), MP_ROM_PTR(&pin_PA05) },

    { MP_ROM_QSTR(MP_QSTR_A5), MP_ROM_PTR(&pin_PB02) },

    { MP_ROM_QSTR(MP_QSTR_SCK), MP_ROM_PTR(&pin_PB11) },



    { MP_ROM_QSTR(MP_QSTR_D0), MP_ROM_PTR(&pin_PA11) },

    { MP_ROM_QSTR(MP_QSTR_RX), MP_ROM_PTR(&pin_PA11) },

    { MP_ROM_QSTR(MP_QSTR_D1), MP_ROM_PTR(&pin_PA10) },

    { MP_ROM_QSTR(MP_QSTR_TX), MP_ROM_PTR(&pin_PA10) },

    { MP_ROM_QSTR(MP_QSTR_SDA), MP_ROM_PTR(&pin_PA22) },

    { MP_ROM_QSTR(MP_QSTR_SCL), MP_ROM_PTR(&pin_PA23) },

    { MP_ROM_QSTR(MP_QSTR_D5), MP_ROM_PTR(&pin_PA15) },

    { MP_ROM_QSTR(MP_QSTR_D6), MP_ROM_PTR(&pin_PA20) },

    { MP_ROM_QSTR(MP_QSTR_D9), MP_ROM_PTR(&pin_PA07) },

    { MP_ROM_QSTR(MP_QSTR_D10), MP_ROM_PTR(&pin_PA18) },

    { MP_ROM_QSTR(MP_QSTR_D11), MP_ROM_PTR(&pin_PA16) },

    { MP_ROM_QSTR(MP_QSTR_D12), MP_ROM_PTR(&pin_PA19) },

    { MP_ROM_QSTR(MP_QSTR_D13), MP_ROM_PTR(&pin_PA17) },



MP_DEFINE_CONST_DICT(board_module_globals, board_global_dict_table);

Listing 1: The Adafruit open source CircuitPython library abstracts hardware details, using board-specific pin maps such as the map shown here for the Feather M0 Express board. (Code source: Adafruit)

To begin development, users can plug the boards into their USB port and use the built-in USB bootloader with the Arduino IDE. For an even simpler introduction to embedded software design, developers can use built-in features to easily load CircuitPython onto their boards and begin building their embedded applications.

Simplified development with CircuitPython

Intended to help ease the embedded development learning curve, CircuitPython actually derives its features from MicroPython, a more direct descendant of Python. Python has emerged as a popular language through its simple, clear syntax and wide availability of support modules. Yet, its code footprint proves too large to be practical for embedded systems.

MicroPython cuts away some of the bulkier features of Python, resulting in a version able to satisfy the logistical constraints of embedded systems, while retaining the core features of the language. In creating CircuitPython, Adafruit has taken that a step further, eliminating modules deemed unnecessary for new embedded systems programmers.

Adafruit’s stated objective with CircuitPython is to provide a language well suited to education, allowing developers to gain proficiency with embedded design without getting bogged down in low-level development details. One of the desirable features CircuitPython gains from its Python heritage is its interpretative nature, which allows developers to interactively explore the interfaces of external modules. For example, a fundamental module in CircuitPython is the board module – a board-specific module that provides access to the associated board’s I/O pins. Developers can launch CircuitPython from the console, import the board module, and immediately view the supported pin names (Listing 2).

>>> import board

>>> dir(board)

['A0', 'SPEAKER', 'A1', 'A2', 'A3', 'A4', 'SCL', 'A5', 'SDA', 'A6', 'RX',

'A7', 'TX', 'LIGHT', 'A8', 'TEMPERATURE', 'A9', 'BUTTON_A', 'D4', 'BUTTON_B',

'D5', 'SLIDE_SWITCH', 'D7', 'NEOPIXEL', 'D8', 'D13', 'REMOTEIN', 'IR_RX',




Listing 2: At the interpreter console prompt (>>), programmers can import the board module and enter dir(board) to view the pin names provided in that board-specific module. (Code source: Adafruit)

The board module provides the connection to underlying hardware, providing a simple way to access the Metro M0 Express and Feather M0 Express board’s pin. For example, the A0 analog pin is referenced simply as board.A0. On the other hand, specific hardware functionality resides in modules such as analogio for analog; digitalio for digital; busio for I2C, SPI, and UART; and pulseio for PWM and other pulse-based protocols, among others. Thus, reading the A0 analog input in CircuitPython is as simple as importing the associated modules and reading the value of an associated device instance (Listing 3).

import board

import analogio


def adc_to_voltage(val):

   return val / 65535 * 3.3


adc = analogio.AnalogIn(board.A0)

pinA0voltage = adc_to_voltage(adc.value)

Listing 3: As with Python, CircuitPython provides a number of high-level modules that developers import into their own code; unlike Python, CircuitPython also provides modules that let programmers perform hardware-level operations such as reading the value (adc.value) at an ADC input pin (board.A0). (Code source: Adafruit)

Developers can easily extend their hardware functionality through direct access to the analog or digital IO pins. For example, they could explore analog output by breadboarding an LED to the board’s A0 connection (Figure 4) and using the analogio module to cause the LED to flash on and off (Listing 4).

Image of Metro M0 Express board's A0 analog output

Figure 4: Developers can quickly prototype external hardware by connecting breadboarded circuits, such as an LED with current-limiting resistor, to the Metro M0 Express board's A0 analog output, which brings out the MCU’s DAC. (Image source: Adafruit)

import board

import analogio

led = analogio.AnalogOut(board.A0)


while True:

    led.value = 65535   # max brightness

    time.sleep(0.5)     # stay on for 1/2 sec

    led.value = 0       # off

    time.sleep(0.5)     # stay off for 1/2 sec

Listing 4: For their breadboarded circuit shown in Figure 4, developers use the CircuitPython analogio module, creating an Analogout class instance (led) bound to the board’s A0 pin and modifying its value property to control LED brightness. (Code source: Adafruit)

Most modern “smart” sensors and actuators provide I2C or SPI interfaces for reading, writing, and monitoring the peripheral device. Although the developer can easily connect the device to either board’s SPI or I2C interfaces, the software interface could require additional effort.

To minimize this effort, Adafruit offers CircuitPython modules for a number of popular devices such as the Silicon Labs SI7021 temperature/humidity sensor. As with the analog I/O module, the SI7021 CircuitPython module lets programmers access the sensor simply using an instance of the corresponding class object after defining the required I2C interface object (Listing 5).

import adafruit_si7021


from busio import I2C

from board import SCL, SDA


# create the I2C interface object

i2c = I2C(SCL, SDA) 


# and use it to instantiate the sensor object

sensor = adafruit_si7021.SI7021(i2c)


# and perform the sensor measurements

current_temperature = sensor.temperature

current_relative_humidity = sensor.relative_humidity

Listing 5: The Adafruit open-source software repository offers CircuitPython modules that simplify access to add-on hardware functionality such as temperature and humidity measurement using the Silicon Labs SI7021 sensor. (Code source: Adafruit)

Although intended primarily as a learning platform, the combination of the Adafruit boards and CircuitPython open source library could be used to create rather sophisticated IoT devices and other embedded designs. At the same time, developers need to recognize that an interpreted language such as MicroPython/CircuitPython has significant limitations in its ability to address hard real-time requirements. Nevertheless, for many embedded applications, this learning platform offers a solid base for expansion.

To add hardware functionality, developers can stack available Adafruit FeatherWing daughter cards on the Feather M0 Express board, or even use the FeatherWing Proto prototyping board to add their own circuits. To add support for the additional hardware functionality in CircuitPython, developers face creating custom software to add the required underlying drivers. Yet, even that effort is minimized through the combination of the open-source library and the nature of Python itself.

By examining the open-source library, programmers can study key design patterns used to implement hardware support. For example, Adafruit’s SI7021 module demonstrates an appropriately “Pythonic” class structure including constructors and helper functions (Listing 6). By following this approach, developers can add their own hardware with minimal effort.

from micropython import const

import ustruct

import sys


from adafruit_bus_device.i2c_device import I2CDevice



HUMIDITY = const(0xf5)

TEMPERATURE = const(0xf3)

_RESET = const(0xfe)

_READ_USER1 = const(0xe7)

_USER1_VAL = const(0x3a)



def _crc(data):

    crc = 0

    for byte in data:

        crc ^= byte

        for i in range(8):

            if crc & 0x80:

                crc <<= 1

                crc ^= 0x131


                crc <<= 1

    return crc



class SI7021:


    A driver for the SI7021 temperature and humidity sensor.



    def __init__(self, i2c, address=0x40):

        self.i2c_device = I2CDevice(i2c, address)


        self._measurement = 0


    def init(self):


        # Make sure the USER1 settings are correct.

        while True:

            # While restarting, the sensor doesn't respond to reads or writes.


                data = bytearray([_READ_USER1])

                with self.i2c_device as i2c:

                    i2c.write(data, stop=False)


                value = data[0]

            except OSError as e:

                if e.args[0] not in ('I2C bus error', 19): # errno 19 ENODEV




        if value != _USER1_VAL:

            raise RuntimeError("bad USER1 register (%x!=%x)" % (

                value, _USER1_VAL))


    def _command(self, command):

        with self.i2c_device as i2c:

            i2c.write(ustruct.pack('B', command))


    def _data(self):

        data = bytearray(3)

        data[0] = 0xff

        while True:

            # While busy, the sensor doesn't respond to reads.


                with self.i2c_device as i2c:


            except OSError as e:

                if e.args[0] not in ('I2C bus error', 19): # errno 19 ENODEV



                if data[0] != 0xff: # Check if read succeeded.


        value, checksum = ustruct.unpack('>HB', data)

        if checksum != _crc(data[:2]):

            raise ValueError("CRC mismatch")

        return value


    def reset(self):




    def relative_humidity(self):

        """The measured relative humidity in percents."""


        value = self._data()

        self._measurement = 0

        return value * 125 / 65536 - 6



    def temperature(self):

        """The measured temperature in degrees Celcius."""


        value = self._data()

        self._measurement = 0

        return value * 175.72 / 65536 - 46.85


    def start_measurement(self, what):


        Starts a measurement.


        Starts a measurement of either ``HUMIDITY`` or ``TEMPERATURE``

        depending on the ``what`` argument. Returns immediately, and the

        result of the measurement can be retrieved with the

        ``temperature`` and ``relative_humidity`` properties. This way it

        will take much less time.


        This can be useful if you want to start the measurement, but don't

        want the call to block until the measurement is ready -- for instance,

        when you are doing other things at the same time.


        if what not in (HUMIDITY, TEMPERATURE):

            raise ValueError()

        if not self._measurement:


        elif self._measurement != what:

            raise RuntimeError("other measurement in progress")

        self._measurement = what

Listing 6: For adding custom hardware to their CircuitPython applications, developers can turn to open-source software such as the Adafruit CircuitPython driver for the SiLabs si7021, which demonstrates key design patterns for designing a sensor hardware class (SI7021) with implicit (__init__) and explicit (init) constructors and accessing the hardware itself across the serial bus (in this case, the I2C bus). (Code source: Adafruit)

Other modules, particularly in the library’s hardware abstraction layer (HAL), offer lower-level C language services and hooks for implementing access to physical hardware. After completing the custom module, developers can take advantage of available step-by-step recipes that describe use of specific hooks built into Python, MicroPython, and CircuitPython to add custom C and Python code to the environment. Although the enhancement process ends at this point for desktop or server Python environments, embedded environments require the additional step of updating the board’s firmware with the enhanced code images.

Adafruit supplies the boards with a built-in bootloader that automatically loads USB Flashing Format (UF2) images. Developers trigger the bootloader process by pressing the board’s RESET button twice, which causes a new “boot” removable drive to appear in the user’s host file system. Developers need only drag and drop the UF2 image from their host system to the removable drive that represents the board (Figure 5). This is the same process used to load CircuitPython initially. In this case, developers simply drop the UF2 image built with their custom code. The bootloader proceeds automatically to flash the board with the new image.

Image of Adafruit simplifies flashing by supplying the boards with a bootloader

Figure 5: Adafruit simplifies flashing by supplying the boards with a bootloader that, when launched by pressing the board’s RESET button, causes a BOOT removable drive to appear in their file system (MAC OS shown here) onto which developers simply drop their new UF2 image. (Image source: Adafruit)


For developers looking to gain experience in embedded design, the tools and techniques needed to address “hard” real-time requirements are simply overkill. At the same time, developers expect ready access to sophisticated 32-bit MCUs able to provide extensive analog and digital IO functionality.

Adafruit’s open-source CircuitPython package provides a simpler development environment able to meet these simpler requirements. By combining CircuitPython with Adafruit’s Metro M0 Express or Feather M0 Express development boards, new developers can quickly gain experience with embedded systems while more experienced developers can rapidly build embedded application prototypes.

Together, CircuitPython and Adafruit development boards provide an accessible, yet powerful, platform for embedded applications development.

Disclaimer: The opinions, beliefs, and viewpoints expressed by the various authors and/or forum participants on this website do not necessarily reflect the opinions, beliefs, and viewpoints of Digi-Key Electronics or official policies of Digi-Key Electronics.

About this publisher

Digi-Key's North American Editors