Using Docker to cross-compile embedded software

Are you tired of managing the installation of different cross-development toolchains on the same machine, fixing issues when your compiler does not work after a host OS upgrade or having to deal with the same toolchain being installed in heterogeneous environments?

Docker fixes some of these issues by providing a light-weight virtualization layer that isolates the cross-development toolchains from the host OS, allows the easier coexistence of different tools in the same machine, and facilitates their management and deployability.

We have been facing these problems while developing software for an Infineon XMC4800 microcontroller on a Linux host, and have improved our process by using a Docker cross-compilation container with the following features:

  • Docker container based on Ubuntu 18.04 LTS
  • GNU ARM toolchain
  • Infineon XMC libraries for XMC4800
  • Segger JLink tool for target flashing and debugging
  • Container compiles code from the host invocation directory
  • Use of ccache to speed up subsequent compilations

This is our resulting Dockerfile:

# Root image built from LTS ubuntu in Docker Hub.
FROM ubuntu:18.04

MAINTAINER Juan Solano "jsm@jsolano.com"

# Update this variable to force a refresh of all base images and make
# sure subsequent commands do not use old cache versions.
ENV REFRESHED_AT 2018-11-26

ARG USERNAME="docker"
ARG USERGROUP="dckrgroup"
ARG DEBIAN_FRONTEND=noninteractive
# These can be overriden with a command line option when the image is
# built, e.g. --build-arg UID=$(id -u) --build-arg GID=$(id -g).
ARG UID=1000
ARG GID=1000
ARG GCC_ARM_TOOLCHAIN_VER="gcc-arm-none-eabi-7-2018-q2-update"
ARG GCC_ARM_TOOLCHAIN_URL="https://developer.arm.com/-/media/Files/downloads/gnu-rm/7-2018q2/"$GCC_ARM_TOOLCHAIN_VER-linux.tar.bz2
ARG XMC_LIB_VER="XMC_Peripheral_Library_v2.1.18"
ARG XMC_LIB_URL="http://dave.infineon.com/Libraries/XMCLib/"$XMC_LIB_VER.zip
ARG JLINK_VER="JLink_Linux_V634g_x86_64"

# Set up the compiler path and other container environment variables.
ENV PATH $PATH:/home/$USERNAME/opt/$GCC_ARM_TOOLCHAIN_VER/bin
ENV GCC_ARM_TOOLCHAIN_VER $GCC_ARM_TOOLCHAIN_VER
ENV GCC_COLORS="error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01"
ENV USB_SCRIPT="usbdev_allow.sh"
ENV TZ=Europe/Berlin

RUN apt-get update -q \
    && apt-get install --no-install-recommends -y apt-utils \
    && apt-get install --no-install-recommends -y vim make sudo \
       tzdata libncurses5 ca-certificates unzip bzip2 libtool ccache \
       usbutils libusb-1.0-0-dev libusb-dev \
    && rm -rf /var/lib/apt/lists/*

# Set timezone and standard user.
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
    && echo $TZ > /etc/timezone \
    && groupadd --gid $GID $USERGROUP \
    && useradd -m -u $UID -g $GID -o -s /bin/bash $USERNAME \
    && echo "root:root" | chpasswd \
    && echo "$USERNAME:$USERNAME" | chpasswd \
    && usermod -a -G 20 $USERNAME \
    && adduser $USERNAME sudo \
    && echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers

# Set up a build tools directory.
RUN mkdir -p /home/$USERNAME/opt
WORKDIR /home/$USERNAME/opt
RUN chown $USERNAME /home/$USERNAME/opt \
    && cd /home/$USERNAME/opt

# Install JLink as root, before changing to standard user.
COPY $JLINK_VER.deb /home/$USERNAME/opt
RUN dpkg -i $JLINK_VER.deb \
    && rm $JLINK_VER.deb
COPY $USB_SCRIPT /home/$USERNAME/opt
RUN chmod +x /home/$USERNAME/opt/$USER_SCRIPT

# Further operations as standard user.
USER $USERNAME

# Install the XMC library.
COPY $XMC_LIB_VER.zip /home/$USERNAME/opt
RUN unzip $XMC_LIB_VER.zip \
    && rm $XMC_LIB_VER.zip

# Install the ARM cross-compilation toolchain.
COPY $GCC_ARM_TOOLCHAIN_VER-linux.tar.bz2 /home/$USERNAME/opt
RUN bunzip2 $GCC_ARM_TOOLCHAIN_VER-linux.tar.bz2 \
    && tar xvf $GCC_ARM_TOOLCHAIN_VER-linux.tar \
    && rm $GCC_ARM_TOOLCHAIN_VER-linux.tar

# Required so that ccache files are kept in shared work directory.
RUN cd /usr/lib/ccache \
    && sudo ln -s ../../bin/ccache arm-none-eabi-gcc
ENV PATH /usr/lib/ccache:$PATH

# Create a directory for our project and setup a shared work directory.
RUN mkdir -p /home/$USERNAME/project
WORKDIR /home/$USERNAME/project
VOLUME /home/$USERNAME/project
RUN cd /home/$USERNAME/project \
    && mkdir -p $HOME/.ccache \
    && echo "cache_dir = $HOME/project/.ccache" >> \
       $HOME/.ccache/ccache.conf

Initially we added wget commands to the Dockerfile, so that the tools were directly downloaded before usage, but we have later decided to keep a local copy of our tools to speed up the Docker image creation. After creating the Docker image, compiling is just a matter of going to the directory where our source code lives and executing our make alias, which can be defined like e.g.:

alias xmcmake='docker run --rm -it --device=/dev/bus/usb --volume=$(pwd):/home/docker/project docker-arm-xmc make'

This starts a container based on the previously created docker-arm-xmc image, allowing access to the JLink usb port from inside the container, and executes the make command. After the make command is executed, the container exits and we can see our compiled binaries as well as a directory with the .ccache artifacts which will be used the next time the make command is invoked.

In subsequent posts, I will delve into additional development steps that can be realized with the help of this container. I hope you find this useful.

2 thoughts on “Using Docker to cross-compile embedded software”

  1. Can you please post the contents of usbdev_allow.sh, or elaborate on the steps that let the docker container and the JLink play nice together?

    Reply

Leave a Comment