Develop Real-Time Microcontroller-Based Applications Quickly Using MicroPython

By Jacob Beningo

Contributed By Digi-Key's North American Editors

Real-time embedded systems are becoming extremely complex, requiring an in-depth understanding not only of intricate 32-bit microcontrollers, as well as sensors, algorithms, Internet protocols, and widely varying end-user applications. With shorter development cycles and more features, development teams need to find ways to both accelerate design while being able to port their code to new products: an integrated and flexible development platform is needed.

There are several microcontroller-specific platforms that are available to help speed up the development process, but the problem with these solutions is that they tie developers to a single microcontroller provider. Porting software from one platform to the next can be time consuming and costly.

A unique and novel solution that is gaining wide acceptance and momentum is to couple the low-level microcontroller hardware with a high-level programming language such as Python. One such solution is MicroPython. This runs on several different microcontroller vendor parts and is open source, making it readily available for developers to use and customize for their own needs.

MicroPython.org describes it as a lean and efficient implementation of the Python 3 programming language that includes a small subset of the Python standard library optimized to run on microcontrollers and in constrained environments. MicroPython began as a kick-starter project that was not only successfully funded, but grew a large following and has now successfully been used in projects across multiple industries such as industrial and space-based systems.

Selecting the right microcontroller

MicroPython runs on several different microcontrollers and there are no major limitations to porting MicroPython to more microcontrollers provided that they have sufficient RAM, flash and processing power to run the interpreter. That said, there are several key requirements that a developer should look for in a microcontroller that will be used to run MicroPython:

  • at least 256 Kbytes of flash
  • at least 16 Kbytes of RAM
  • at least an 80 MHz CPU clock

These are general recommendations and developers can deviate from them based on their application needs, as well as how much time they want to spend customizing the MicroPython kernel. For example, MicroPython can be modified to use far less than 256 Kbytes of flash. These recommendations are to provide a developer with the best experience and to provide room for growth in their application code.

MicroPython has already been ported to several different microcontroller series that can be used as a great starting point for porting to a new platform or for selecting a microcontroller that is already supported. The MicroPython source code’s main directory is shown in Figure 1. From here, the reader can see several different supported microcontroller devices such as:

Image of example folder directory structure

Figure 1: Example folder directory structure showing the available microcontroller platforms that currently support MicroPython. These include ARM, CC3200, esp8266, Microchip PIC, and STM32. (Image source: Beningo Embedded Group)

Each folder listed in the root directory is a high level folder that contains the general drivers and support for that chip family. There may be several different development boards or processors supported within each folder. For example, the stmhal folder supports development boards such as STMicroelectronics’ STM32F429 Discovery Board and the STM32 IoT Discovery Node (STM32L), among several others such as Adafruit Industries’ STM32F405 pyboard. The ESP8266 folder contains support for Adafruit’s Huzzah break-out board for the ESP8266 and Feather Huzzah Stack Board.

The development boards that can run MicroPython are inexpensive, and it is a good idea for a developer to purchase several to feel out how much memory, storage space and processing power they need for an application. For example, a developer might start out using the STM32F405 pyboard and decide that in order to future-proof features and upgrades, they want to move to a STM32F429 in their final product. The STM32F429 has 2 Mbytes of flash, 256 Kbytes of RAM and special 0-wait-state RAM called CCM.

The MicroPython application code that a developer writes doesn’t necessarily have to be stored on the internal flash of the microcontroller. The MicroPython kernel needs to be on the microcontroller but the application code can be on an external storage medium, such as Panasonic's microSD 8 GB card. Using an external memory storage device to store the application code provides the opportunity to use a microcontroller with less memory and potentially save overall system costs.

Get up and running with MicroPython

MicroPython comes preinstalled on the Adafruit STM32F405 pyboard. However, any other development kit or custom hardware will require a developer to download the MicroPython source, build the source for the target board, and then flash the microcontroller with the software. Getting access to the MicroPython source code is easy since it is all hosted on GitHub. There are several steps that a developer will need to follow to setup the toolchain and get the environment configured to build MicroPython. In this example, we will build MicroPython for the STM32F429 Discovery board.

First, a developer will want to either create a Linux-based virtual machine or use a native Linux installation. Once Linux is available from a terminal, a developer will want to install the ARM compiler toolchain using the following command:

sudo apt-get install gcc-arm-none-eabi

 

If the Linux installation is fresh, the revision control system, git, may not be installed. Git can be installed from the terminal using the following command:

 

sudo apt-get install git

 

Once git is installed, the MicroPython source can be checked out of the repository by executing the following command in the terminal:

 

git clone https://github.com/micropython/micropython.git

The process may execute for a few minutes but a developer should see the sequence demonstrated (Figure 2).

Image of cloning the MicroPython repository to the local file system

Figure 2: Cloning the MicroPython repository to the local file system where a developer can then build MicroPython for their target board or customize the kernel for their own applications.  (Image source: Beningo Embedded Group)

Once the MicroPython source has been cloned to the local file system, they should change into that directory and then in the terminal perform a “cd stmhal”. The stmhal directory contains the makefile for MicroPython for the STM32 microcontrollers. There is also a “boards” folder that a developer can review that shows all the currently supported STM32 boards. From the terminal a developer can then build any board that is located in the “boards” folder. For example, a developer can build the STM32F4 Discovery board by typing the following command:

make BOARD=STM32F4DISC

MicroPython will take several minutes to build. While the build is in progress, a developer will want to install the device firmware update (DFU) tool that is used to program MicroPython over USB onto a microcontroller. The tool only needs to be installed once and can be done by typing the following command into a terminal:

sudo apt-get install dfu-util

When the MicroPython build is completed and the dfu-util has been installed, a developer is then ready to load MicroPython onto their microcontroller. The developer will need to first put their microcontroller into DFU bootloader mode. This can be done by setting the boot pins to load the internal bootloader on reset, rather than executing code from flash.

With the microcontroller in bootloader mode and connected to the host computer via USB, the dfu-util can be used to download MicroPython using the following command:

dfu-util -a 0 -d 0483:df11 -D build-STM32F4DISC/firmware.dfu

The dfu-util will use the dfu file that is output by the compilation process. The process will take several minutes since the microcontroller will be completely erased and reprogrammed. The process will look something very similar to the process shown (Figure 3). As soon as the tool completes, the boot jumpers should be adjusted to load from internal flash and then the microcontroller can be power cycled. MicroPython is now running on the target microcontroller.

Image of MicroPython being loaded onto a microcontroller using the dfu-util

Figure 3: MicroPython being loaded onto a microcontroller using the dfu-util. (Image source: Beningo Embedded Group)

Interfacing sensors and connected devices

The greatest advantage to using a high-level programming language such as MicroPython to develop real-time embedded software is that the software is agnostic to the underlying hardware. This means that a developer can develop a MicroPython script to run on a pyboard, and with little to no modifications, also run the script on the ESP8266 or the STM32F4 Discovery board. Let’s examine how a basic MicroPython script might look that interfaces a Bosch Sensortec BMP280 barometer and temperature sensor to an I2C bus and then transmits the data over a Bluetooth serial link using the Microchip Technology RN-42 Bluetooth module.

The BMP280 is an I2C based barometer and temperature sensor that has a default I2C slave address of decimal 119. The easiest way to interface it to the pyboard is to use DFRobot’s Gravity board, which provides a robust connector for easy access to power the device and access I2C. A developer can select either the I2C1 or I2C2 bus to connect the Gravity board. Once the boards are connected, the MicroPython script is straightforward.

First, a developer will import the I2C class from the pyb library. The pyb library provides access to microcontroller peripheral functions such as SPI, I2C and UART. Before any peripheral can be used, a developer must instantiate the peripheral class to create an object that can be used to control the peripheral. Once the peripheral class has been initialized, a developer can perform any other initializations such as verifying that devices are present before entering the primary application loop. The primary application code will then sample the sensor once per second. An example of how to do this is shown (Code Listing 1).

Copy

from pyb import I2C

 

GlobalTemp = 0.0

GlobalBarometer = 0.0

 

# Initialize and Instantiate I2C peripheral 2

I2C2 = I2C(2,I2C.MASTER, baudrate=100000)

 

while True:

            SensorSample()

            pyb.delay(1000)

 

def SensorSample():

            #Read the Temperature Data

            TempSample = I2C2.readfrom_mem(119, 0xFA,3)

 

            #Read the Pressure Data

            PressureSample = I2C2.readfrom_mem(119, 0xF7,3)

Code Listing 1: MicroPython script that initializes the I2C peripheral and communicates with DFRobot Gravity board to acquire temperature and barometer sensor data. (Code source: Beningo Embedded Group)

Sampling sensor data and doing nothing with it is not a terribly useful demonstration for how powerful MicroPython can be to a development team. Many development teams are being faced with technical challenges for connecting their sensor devices to the Internet or to a local sensor hub using Bluetooth.

An easy way to add Bluetooth functionality to a project is to use the RN-42. The RN-42 can be placed into a mode where the microcontroller simply sends the UART data that should be transmitted over Bluetooth, and the RN-42 handles the entire Bluetooth stack (Figure 4).

Image of pyboard running MicroPython connected to RN-42 Bluetooth module over UART

Figure 4: Connecting the pyboard running MicroPython to a RN-42 Bluetooth module over UART. (Image source: Beningo Embedded Group)

Once the Bluetooth board is connected, a developer can create a very simple script that takes the received sensor data and transmits it over Bluetooth to a mobile device that could then either save the data or forward it to the cloud for further analysis. The example script is shown (Code Listing 2). In this example, UART1 is configured for 115200 bps, 8-bit transmission, no parity and a single stop bit.

Copy

from pyb import uart

from pyb import I2C

 

GlobalTemp = 0.0

GlobalBarometer = 0.0

 

# Initialize and Instantiate I2C peripheral 2

I2C2 = I2C(2,I2C.MASTER, baudrate=100000)

 

# Configure Uart1 for communication

Uart1 = pyb.UART(1,115200)

Uart1.init(115200, bits=8, parity=None, stop=1)

 

while True:

            SampleSensor()

            pyb.delay(1000)

 

def SensorSample():

            #Read the Temperature Data

            TempSample = I2C2.readfrom_mem(119, 0xFA,3)

 

            #Read the Pressure Data

            PressureSample = I2C2.readfrom_mem(119, 0xF7,3)

 

            #Convert Sample data to string

            data = “#,temperature=”str(TempSample)+”,pressure”+str(PressureSample)+”,#,\n\r”

 

            #Write the data to Bluetooth

            Uart1.write(data)

Code Listing 2: Example MicroPython script initializes UART1 and communicates with an external device. (Code source: Beningo Embedded Group)

Not only is the Python application code easily ported to other hardware platforms, the application uses common libraries and functionality that has already been implemented to help developers accelerate their development. Creating the above application can be done in an hour or less, versus potentially a week or more for a developer starting at the lowest software levels and having to work their way up.

Tips and tricks for developing real-time software

Developing embedded applications using MicroPython is easy, but getting real-time performance from the system may not be as straightforward as one might think. While MicroPython offers huge advantages for simplifying and reusing code, getting predictable and consistent timing from the system can be challenging if a developer isn’t aware of a few interesting facts and libraries.

MicroPython includes a garbage collector that runs in the background and manages the heap and other memory resources. The garbage collector is non-deterministic, so developers expecting deterministic behavior can get themselves into trouble if the garbage collector starts executing in a time-critical section. There are several recommendations that developers should follow in order to make sure that this does not happen.

First, developers can import the garbage collection library, gc, and use the enable and disable methods to control when the garbage collector is enabled or disabled. A developer can disable garbage collection before a critical section and then enable it afterwards as shown (Code Listing 3).

Copy

import gc

 

gc.disable()

 

#My time critical code

 

gc.enable()

Code Listing 3: Disabling the MicroPython garbage collector prior to a time critical code section. (Code source: Beningo Embedded Group)

Second, a developer can also manually control the garbage collection process. When a developer creates and destroys objects, they allocate memory on the heap. The garbage collector runs and frees up unused space. Since it does so at an irregular interval, developers can use the collect method to periodically run garbage collection to ensure that the heap space doesn’t become overfilled with garbage. When this is done, garbage collection runs can go from 10 milliseconds down to less than a millisecond per run. Manually calling garbage collection also ensures that the developer’s application has control over the non-deterministic timed code. This allows them to decide when to run the garbage collection and ensures that their application has real-time performance.

There are several other best practices that developers can follow who are interested in writing real-time code. These include:

  • Using pre-allocated buffers for communication channels
  • Using the readinto method when using communication peripherals
  • Avoiding traditional Python documentation using ###
  • Minimizing object creation and destruction during run-time
  • Monitor application execution times

Developers interested in understanding more “best practices” can review MicroPython optimization documentation here.

Conclusion

MicroPython is an interesting platform for developers looking to implement real-time embedded applications that are agnostic to the underlying microcontroller hardware. Developers can write their high-level Python scripts using the standard libraries provided in MicroPython and run them on any supported microcontroller. This provides developers with many advantages, including:

  • Improved application reuse
  • Faster time to market
  • Decoupling the application from the hardware

MicroPython is not perfect for every application, but has been successful so far in industrial and space systems applications, along with rapid prototyping and proof-of-concepts. 

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 author

Jacob Beningo

Jacob Beningo is an embedded software consultant who currently works with clients in more than a dozen countries to dramatically transform their businesses by improving product quality, cost and time to market. He has published more than 200 articles on embedded software development techniques, is a sought-after speaker and technical trainer and holds three degrees which include a Masters of Engineering from the University of Michigan. Feel free to contact him at jacob@beningo.com, at his website www.beningo.com, and sign-up for his monthly Embedded Bytes Newsletter.

About this publisher

Digi-Key's North American Editors