Part 0 - Creating a minimal build environment with Docker.

Part 0 - Creating a minimal build environment with Docker.

Hi folks!

After a few months, I'm back with a new series of articles. I will discuss tools and strategies for automated testing for STM32 microcontroller-based projects this time.

In this series, I plan to cover topics such as:

  • Creating a reproducible build environment with Docker for STM32 projects.
  • Using CMake to build applications and test code.
  • Creating a sample project for the STMicroelectronics NUCLEO-F446ZE board and building on a Docker image.
  • Setting up a CI/CD pipeline with GitHub Actions.
  • Automating tasks with Robot Framework.
  • Unit and integration testing with the Google Test Framework.
  • Using GMock to reduce hardware dependency.
  • Debugging strategies for STM32 microcontrollers.
  • ... and more to come!

In this initial article, I'll show you how to create a minimal Docker container to build an STM32 project. As new tools become necessary, I plan to improve this container article by article.

Throughout the series, I'll use the Linux Ubuntu 22.04 LTS distribution.

Check the current code result on the project's GitHub repository, tag part_0.

Let's begin!

Software development environment for STM32 microcontrollers

STMicroelectronics provides an extensive ecosystem of software development tools for STM32 microcontrollers. Most of these tools are free to use with ST products.

A suggested workflow for working with STM32 microcontrollers is a 4-step iterative development process, as shown in the image below:

STM32 iterative development process. source:

  1. Configuration: use STM32CubeMX, a graphical tool, to easily configure STM32 microcontrollers and generate corresponding initialization C code for peripheral drivers (source: https://www.st.com/en/development-tools/stm32cubemx.html).
  2. Development: use STM32CubeIDE, an integrated development environment based on Eclipse and the GNU C/C++ toolchain, to develop, compile, and debug your application solution. It also integrates features from other ecosystem tools, such as STM32CubeMX code generation (source: https://www.st.com/content/st_com/en/ecosystems/stm32cube-ecosystem.html).
  3. Programming: use the STM32CubeProgrammer to read, write, and check external devices and memories through various communication media (JTAG, SWD, UART, USB DFU, I2C, SPI, CAN, etc.) (source: https://www.st.com/en/development-tools/stm32cubeprog.html).
  4. Monitoring: use STM32CubeMonitor to tune and diagnose STM32 applications at runtime, reading and viewing variables in real-time (source: https://www.st.com/en/development-tools/stm32cubemonitor.html).

These tools are typically used in a desktop environment. However, the goal here is to create an environment that allows us to automate the build and test process, enabling the implementation of continuous integration processes integrated into our code repository.

After this introduction, let's create and test a minimal Docker image.

Install and test the Docker

We aim to use Docker to create a container that helps us build and test our project, locally or in an online repository like GitHub. Let's start by installing Docker on a local machine to achieve this.

It's easy to find tutorials online that describe how to install Docker on Ubuntu 22.04, like this one from Digital Ocean. Therefore, I will not repeat the steps here. Consult this or another tutorial to install Docker.

Assuming you have successfully installed Docker on your machine, let's test if you can run the Ubuntu 22.04 image in Docker by running the command:

$ docker run -it ubuntu:22.04        

After running the command in your console, you should see an output similar to this:

$ docker run -it ubuntu:22.04
Unable to find image 'ubuntu:22.04' locally
22.04: Pulling from library/ubuntu
a8b1c5f80c2d: Already exists 
Digest: sha256:a6d2b38300ce017add71440577d5b0a90460d0e57fd7aec21dd0d1b0761bbfb2
Status: Downloaded newer image for ubuntu:22.04
root@655be2fdf0cd:/#         

You may see the message Unable to find image 'ubuntu:22.04' locally. Since Docker couldn't find the image locally, it pulled the image from Docker Hub. Also note that the user is logged in as root, and the string 655be2fdf0cd is the container ID (this ID may differ on your machine).

Using Docker as root is not recommended, but let's leave that aside for now and address this later.

To exit the container, type exit in the container's console.

Revisiting the previous image and descriptions of the STM32 tools, we see that STM32CubeIDE provides the toolchain and performs the compilation process, among other things. But when considering creating an automated compilation and testing process, is this the appropriate tool to use? Let's find out.

Tools like STM32CubeMX and STM32CubeIDE are designed for user interaction and are not well-suited for automation. Therefore, ST provides another tool called STM32CubeCLT, a set of command-line development tools with code compilation, board programming, and debugging capabilities (source: https://www.st.com/en/development-tools/stm32cubeclt.html).

Wait! Did someone say command line development toolkit? This seems much better suited for automated use.

Creating a Docker image for STM32CubeCLT

First, we will create a project folder called stm32-testing-automation. Inside this folder, create another folder called docker, and inside it, create a file called Dockerfile.dev. This file is essentially a script that Docker can run to build a Docker image automatically.

Open the Dockerfile.dev file and type the following text:

# Description: Dockerfile for the development environment of the STM32 microcontroller.

# Use the official Ubuntu 22.04 image as the base image
FROM ubuntu:22.04

# Set the maintainer information
LABEL maintainer="Your Name <[email protected]>"

# Set environment variables to avoid interactive prompts during installation
ENV DEBIAN_FRONTEND=noninteractive

# Changes the default shell for the Docker build process to bash
SHELL ["/bin/bash", "-c"]

# Set the timezone to America/Fortaleza and update the symbolic link for the timezone
ENV TZ=America/Fortaleza
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

# Update and install base required packages
RUN apt-get update && apt-get install -y \
      build-essential git wget unzip python3 python3-pip \
      && apt-get clean && rm -rf /var/lib/apt/lists/*        

Here's a line-by-line explanation:

  1. FROM ubuntu:22.04: Specifies the base image to start from. In this case, it's Ubuntu version 22.04.
  2. LABEL maintainer="Your Name <[email protected]>": Adds metadata to the image indicating the maintainer.
  3. ENV DEBIAN_FRONTEND=noninteractive: Sets an environment variable to prevent interactive prompts during package installation.
  4. SHELL ["/bin/bash", "-c"]: Changes the default shell for the Docker build process to bash.
  5. ENV TZ=America/Fortaleza: Sets the timezone for the image.
  6. RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone: Updates the symbolic link for the local time to match the specified timezone. Feel free to change this to your time zone.
  7. RUN apt-get update && ... && rm -rf /var/lib/apt/lists/: Updates the package lists, installs several packages and then cleans up unnecessary files to keep the Docker image size small.

Next, download the STM32CubeCLT installation file manually. Unfortunately, we cannot include the download process in the dockerfile due to the need for license acceptance and user login.

Create your user account, accept the license, and download STM32CubeCLT 1.15.1. Unzip the file and copy it to the docker folder.

Now, add the instructions to install the package dependencies for STM32CubeCLT and copy the installation file to our image:

# Install the dependencies for STM32CubeCLT
RUN apt-get update && apt-get install -y \
      udev libusb-1.0-0-dev libncurses5 libncurses5-dev libpython2.7 libpython2.7-minimal ninja-build expect \
      && apt-get clean && rm -rf /var/lib/apt/lists/*

# Copy the STM32CubeCLT script installer
COPY st-stm32cubeclt_1.15.1_21094_20240412_1041_amd64.deb_bundle.sh /install_stm32cubeclt.sh        

If you try to install STM32CubeCLT on your local machine, you must accept the license during the installation process. This makes the Docker build process a little more complex because the installation process does not support a non-interactive mode.

We can utilize the Expect tool to mimic user interaction to solve the previous issue. Within the docker directory, create a new file named install_stm32cubeclt.exp and input the following:

#!/usr/bin/expect

set timeout -1

spawn /install_stm32cubeclt.sh

# Simulate reading the license agreement by sending "space" or "enter" to scroll down
expect {
    "More*" {
        send -- " \r"
        exp_continue
    }
    "I ACCEPT (y) / I DO NOT ACCEPT (N)" {
        send -- "y\r"
    }
}

expect eof        

The above Expect script starts the install_stm32cubeclt.sh script as a child process, and the expect block simulates user interaction based on the child process output. Let's check more details:

  • "More"*: if the script outputs a line containing "More*", it simulates pressing space or enter to scroll down by sending " \r" (a space and a carriage return). exp_continue tells Expect to continue processing the same expect block, handling multiple "More*" prompts.
  • "I ACCEPT (y) / I DO NOT ACCEPT (N)": if the script outputs this line (a prompt to accept a license agreement), it simulates pressing "y" and enter to accept the agreement by sending "y\r".

Finish the dockerfile by adding instructions to copy the Expect script and install STM32CubeCLT. Here is our Dockerfile.dev so far:

# Description: Dockerfile for the development environment of the STM32 microcontroller.

# Use the official Ubuntu 22.04 image as the base image
FROM ubuntu:22.04

# Set the maintainer information
LABEL maintainer="Charles Dias <[email protected]>"

# Set environment variables to avoid interactive prompts during installation
ENV DEBIAN_FRONTEND=noninteractive

# Changes the default shell for the Docker build process to bash
SHELL ["/bin/bash", "-c"]

# Set the timezone to America/Fortaleza and update the symbolic link for the timezone
ENV TZ=America/Fortaleza
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

# Update and install base required packages
RUN apt-get update && apt-get install -y \
      build-essential git wget unzip python3 python3-pip \
      && apt-get clean && rm -rf /var/lib/apt/lists/*

# Install the dependencies for STM32CubeCLT
RUN apt-get update && apt-get install -y \
      udev libusb-1.0-0-dev libncurses5 libncurses5-dev libpython2.7 libpython2.7-minimal ninja-build expect \
      && apt-get clean && rm -rf /var/lib/apt/lists/*

# Copy the STM32CubeCLT script installer
COPY st-stm32cubeclt_1.15.1_21094_20240412_1041_amd64.deb_bundle.sh /install_stm32cubeclt.sh

# Copy the STM32CubeCLT expect script
COPY install_stm32cubeclt.exp /install_stm32cubeclt.exp

# Install the STM32CubeCLT
RUN chmod +x /install_stm32cubeclt.sh /install_stm32cubeclt.exp && \
    /install_stm32cubeclt.exp        

Build and run the Dockerfile.dev image

On your local machine, open the console, navigate to the docker folder, and run the following command to build the image:

$ docker build -f Dockerfile.dev  -t <image-name>:<version> .        

Replace <image-name> and <version> with your desired image name and version number, and wait a few minutes for Docker to build the image.

Console output for docker build command.

When the image build process is finished, start the Docker image by running the command:

$ docker run -it --rm <image-name>:<version>        

Again, replace <image-name>:<version> with the image name and version used previously. For example:

$ docker run -it --rm charlesdias/stm32_dev_env:1.0        

Note: Notice that we are still logged in as the root user.

Once you are logged into the Docker image, run the following command:

# ls /opt/st/stm32cubeclt_1.15.1/        

After running the ls command, you can see the list of files and folders in the STM32CubeCLT installation directory. Among others, we have the CMake, GNU-tools-for-STM32, Ninja, and STLink-gdb-server folders.

Console output for

Let's try another command. This time, type:

# /opt/st/stm32cubeclt_1.15.1/GNU-tools-for-STM32/bin/arm-none-eabi-gcc --version        

and

# /opt/st/stm32cubeclt_1.15.1/CMake/bin/cmake --version        

to see the GCC and CMake versions, respectively. Below is the expected output for the two previous commands:

GCC and CMake version.

This confirms the correct installation of the STM32CubeCLT tool on a Docker image.

Create a user account on a Docker image

Wait a minute! What about the root user?

As I mentioned earlier, logging in as root on a Docker image is not good practice. Let's fix this by adding the following instructions at the end of the Dockerfile.

# Create a non-root user and set permissions
ENV USER_NAME=mcu
ENV PROJECT_DIR=/project

ARG host_uid=1000
ARG host_gid=1000
RUN groupadd -g $host_gid $USER_NAME && \
    useradd -g $host_gid -m -s /bin/bash -u $host_uid $USER_NAME && \
    mkdir -p $PROJECT_DIR && chown -R $USER_NAME:$USER_NAME $PROJECT_DIR

# Switch to the non-root user
USER $USER_NAME

# Set the working directory
WORKDIR $PROJECT_DIR        

Now, build the Docker image again, but using the command below:

$ docker build -f Dockerfile.dev --build-arg host_uid=$(id -u) --build-arg host_gid=$(id -g) -t <image-name>:<version> .        

These build arguments (host_id and host_gid) are then used to create a non-root user inside the Docker container with the same UID and GID as the host user.

Run the new Docker image. Which user are you logged in as now?

Check the current code result on the project's GitHub repository, tag part_0.

Conclusion

I hope you enjoyed this introduction!

In the next article, we'll see how to build an STM32 project using the Docker image created.

See you there!

References

Paulo Fonseca

Embedded Software Engineer

4 个月

Hey Charles Dias, M.Sc., Finally trying to implement something similar here at work. Something strange is happening in my case for the last step, the one where you created the non-root user. What i am seeing is: USER $USER_NAME is defaulting the mcu user to UID:GID 1000:1000. This kinda gets masked if your host id is 1000, but if it is not it looks like it will give troubles. As a workaround i modified that line to: USER ${host_uid}:${host_gid} Does anyone else sees this problem? Thanks Again

Dirk Jan ten Kate

Just crafting embedded software

9 个月

Impressive work, very well done! ??

回复
Paulo Fonseca

Embedded Software Engineer

9 个月

Parabéns Charles Dias, M.Sc. Great Article

回复
Viral Patel

Graduate Electronics Engineer at Sensoteq | UOL‘23

9 个月

Very helpful!??

Naveed Ahmed

IT Consultant at QuickOps | Embedded Software Engineer

9 个月

Series would be a wonderful experience.

要查看或添加评论,请登录

Charles Dias, M.Sc.的更多文章

社区洞察

其他会员也浏览了