16 Dec 2016

In this article, we’ll begin the Kinetis Deep Dive by setting up an embedded build and debug toolchain. Then, we can test the toolchain on an evaluation board through GDB.

I’m going to use the plural first person pronoun throughout this set of articles. “We” sounds much more inclusive than “I”, and ideally, if you’re reading these articles, then I hope that you will participate in this process. The cost of participation is not especially expensive. You will need to have access to a relatively modern computer and some familiarity with programming in C. There are plenty of great tutorials and study guides out there to bring you up to speed. In addition, you will need access to the evaluation board I’m using here. For the initial setup, I will be using the FRDM-K22F, which can be purchased for around $29.

Application

Before we begin the process of bootstrapping an embedded toolchain, it will be useful to consider the application to which we are working in this article. The primary focus of the initial articles is to explore the technology provided by the Kinetis parts and the Cortex-M4 platform. However, without a clear goal in mind, these articles could go on for quite a while with no practical outcome. So, to help scope these articles, I will describe a rather simple application – so simple as to be frivolous for the power of this chipset – and we will build firmware in support of this goal.

The application is a simple Pomodoro clock with an OLED display, a separate battery-backed RTC module, a Bluetooth Low Energy peripheral module, an audio amplifier, and a speaker. This, along with a few arcade style buttons, will be all of the peripherals with which our microcontroller board will interact. The goal is to create a simple clock that can perform the 25-minute and 5-minute countdown sequences that are part and parcel to this time management technique. Clearly, this project is not very practical – one could buy a smart phone app or a timer for far cheaper than the cost in parts here – but these peripherals and this application allow us to explore some real embedded concepts without digging into the complexities of a real embedded project. Specifically, this project lets us explore the three main communication protocols found in many real-life embedded applications: SPI, UART, and I2C. Also, because the Kinetis part we are using has support for digital audio, I’m including an I2S audio amplifier.

For the exact parts I am using, I picked a single popular electronics hobby supplier, Adafruit. Here are the components I selected. Note that we won’t be using these components until much later in this project, so if you do intend to follow along, you can wait to purchase these until I post articles building the drivers for each. Note that these components are in kit form, and will require some light soldering. However, they each fit into a standard breadboard to make testing with our evaluation board quite easy.

 

Debug Tool

Now that we have an application in mind, we can consider setting up an embedded toolchain. The evaluation board has built-in support for OpenSDA via bootloader firmware provided by Segger. The first step will be to download and install the Segger J-Link Software and Documentation Pack for your platform as well as the OpenSDA bootloader firmware for the FRDM_K22F. Note that you will need access to a Windows PC to load the bootloader firmware. However, the rest of this bootstrapping process will work on Windows, Linux, or Mac OS once the boot loader for the Freedom board has been loaded. Please follow the instructions provided in those links to set up both the bootloader for the FRDM-K22F board and the J-Link software.

Toolchain Build Script

Now that we have debugging enabled on the evaluation board, we will need an embedded build and debug toolchain that can target this board. The GNU Compiler Collection fits this bill nicely. We will need to bootstrap this compiler as a cross-compiler that targets the ARMv7 architecture using the ARM Embedded ABI. Although the Kinetis part we are targeting has an FPU, we will be using the soft float ABI to start since this simplifies some work in task switching. We can circle back and consider implementing hard float support later on.

The following instructions assume that you have access to a Unix-like environment. Windows users can install Cygwin or potentially use the Windows 10 Bash Shell. I have tested the following under Windows with Cygwin, but I have not tested this with any other Windows features. I have also tested this on Linux and Mac OS.

We are going to build an automated script that will verify that our build environment has the tools we need to build the toolchain, download the source tarballs we need to build this toolchain, verify that they downloaded completely, and then extract, configure, and install each component needed in this toolchain. There are a lot of steps, which is why we’re building a script. Don’t do by hand that which can be automated, especially if it will be repeated often. Furthermore, it is good practice to ensure that a process as important as setting up a build toolchain is repeatable. Setting up a script like this is crucial when setting up a toolchain that an embedded engineering team may use, so it’s good to get into the practice.

We will build this script from the bottom-up. This will be a bash script, which should exist in most Unix systems or that can be easily installed. We’ll start with the preamble and a few basic functions for checking that directories and executables exist. For those who want to cheat, the complete script and usage instructions can be found here.

#!/bin/bash

#check that a directory exists
check_directory_exists()
{
    local dir=$1
    local errormsg=$2

    if [ "" == "$dir" ]; then
        echo "$errormsg"
        exit 1
    fi

    if [ -d "$dir" ]; then
        echo "Verified that $dir exists."
    else
        echo "$errormsg"
        exit 1
    fi
}

#check for an executable
check_exe()
{
    if which $1 >/dev/null 2>&1; then
        echo $1 detected.
    else
        echo $1 not detected!  Install $1.
        exit 1
    fi
}

The first function, check_directory_exists, checks that that the directory provided in the first argument exists. The next function, check_exe, checks that an executable provided by the first argument exists. We will use the first function to ensure that an environment variable, ARM_TOOLCHAIN_DIR, is defined by the user of this script.

We want the build process in the script to be restartable. If an error occurs, or if we need to stop and restart the script, it’s useful if it can reason about what has already been done and start where it left off. The remaining functions will provide rudimentary restart support which should be good enough to ensure that this build process won’t be annoying if we have to tweak something in the middle. The first function, download_if_missing, will only download a tarball from a mirror if the tarball does not already exist in the current directory.

#download a file if it doesn't exist
download_if_missing()
{
    local url=$1
    local base="${url##*/}"

    if [ ! -f $base ]; then
        echo Downloading $base from $url.
        curl -# -L -O $url
    else
        echo $base already downloaded.
    fi
}

This function uses curl to perform the download. We use the truncated progress bar, follow links, and let curl decide the name of the output file using the url.

Once we download files, we need to verify that they downloaded completely, and have some basic assurances that we downloaded the right file. To do this, we use whichever utilities the developers of each tarball suggest. This isn’t a particularly secure way of handling this problem, but setting up a secure keychain and maintaining some of the other sorts of policies we’d need for vendor branching is a bit beyond this article. Suffice it to say that if you are looking for a pretty good way to ensure that files have downloaded correctly and you don’t suspect a malicious man-in-the-middle, this approach will work fine. If you are building a toolchain for something mission critical, research provenance techniques.

The first thing we want to do is grab a few public keys we will need to verify the signatures of source tarballs. The following function grabs a public key by identifier on a given key server, and saves them to the GPG key ring.

#get the given public key from the given server if missing
get_pubkey_if_missing()
{
    if gpg -k $1 > /dev/null 2>&1; then
        echo Public Key $1 found.
    else
        echo Fetching public key $1 from $2.
        if gpg --keyserver $2 --recv-keys $1; then
            echo "    Success."
        else
            echo "    Failure."
            exit 1
        fi
    fi
}

Next, we can verify the signature of a tarball with the verify_signature method. This method uses the signature provided in the first argument to verify the file provided in the second argument.

#verify the PGP signature of a given file
verify_signature()
{
    if gpg --quiet --verify $1 $2; then
        echo $2 verified.
    else
        echo "**** BAD SIGNATURE FOR $2.  ABORTING  ****"
        exit 1
    fi
}

If this process fails, both the tarball and the signature file should be deleted and the script should be run again. Most likely, the download failed.

Next, we can verify the MD5 hash of a file using verify_md5_ugly. As evidence by the name of the function, MD5 hashes are all but worthless. The algorithm suffers from major vulnerabilities, and it is possible to generate a file with an arbitrary hash in a surprisingly small amount of time. An MD5 hash is about as secure as a CRC. Unfortunately, many open-source projects still use MD5 hashes, even though they should know better.

#verify the MD5 checksum of a given file.  Ugly hack.  Why do people still use
#MD5???
verify_md5_ugly()
{
    local dgst=`openssl dgst -md5 -hex $2 | awk '{print $2}'`
    if [ "$1" == "$dgst" ]; then
        echo "$2 matches MD5 digest $1, for whatever that's worth."
    else
        echo "**** $2 DOES NOT MATCH MD5 DIGEST $1.  ABORTING  ****"
        exit 1
    fi
}

To round out our verification functions, the verify_sha512 function compares a file against the SHA-512 hash provided.

#verify the SHA512 checksum of a given file.
verify_sha512()
{
    local dgst=`openssl dgst -sha512 -hex $2 | awk '{print $2}'`
    if [ "$1" == "$dgst" ]; then
        echo "$2 matches SHA-512 digest $1."
    else
        echo $dgst
        echo "**** $2 DOES NOT MATCH SHA-512 DIGEST $1.  ABORTING  ****"
        exit 1
    fi
}

At this point, we have everything we need to download and verify tarballs. Once the tarballs are downloaded and verified, they need to be extracted, configured, built, optionally tested, and installed. For each of these steps, we will use dot files to track our progress. These dot files will be created after a given step completes successfully, and the next time the script is run, the existence of these dot files will allow the script to skip over work it has already performed.

The first function, extract_once, will extract a given tarball just once, and will enforce this extraction policy through the existence of a dot file that is created after the extraction successfully completes.

#extract a given file just once
extract_once()
{
    local tag=.${1}_extracted
    local args=x${2}f
    local archive=$3
    if [ ! -f $tag ]; then
        echo "Extracting $archive..."
        if tar $args $archive; then
            touch $tag
            echo "$archive extracted."
        else
            echo "Failure extracting $archive."
            exit 1
        fi
    else
        echo "$archive already extracted."
    fi
}

Next, configure_once runs the configure script provided with the tarball just once, enforcing this policy through the existence of a dot file that is created after the configure process successfully completes. This function takes four arguments: the tag name for the dot file, the workspace to configure, any additional configure options to pass to the configure script, and the name of the build directory to be used to build this package. This last argument is optional. If it exists, then the build directory will be created and configure will be run from that directory. If it does not exist, then configure will run from the package root directory.

#configure a given workspace just once
configure_once()
{
    local tag=.${1}_configured
    local workspace=$2
    local opts=$3
    local builddir=$4
    if [ ! -f $tag ]; then
        echo "Configuring $workspace..."

        #enter directory
        if [ "" != "$builddir" ]; then
            mkdir -p $workspace/$builddir
            pushd $workspace/$builddir
            local configcmd=../configure
        else
            pushd $workspace
            local configcmd=./configure
        fi

        if $configcmd --prefix=$ARM_TOOLCHAIN_DIR $opts; then
            #restore directory
            popd
            touch $tag
            echo "Configure succeeded."
        else
            echo "Failure configuring $workspace."
            exit 1
        fi
    else
        echo "$workspace already configured."
    fi
}

The build_once function builds a package just once, enforcing this policy through the existence of a dot file that is created after the build process successfully completes. This function takes four arguments, the tag to be used to create the dot file, the workspace name, the build directory, and the build target to pass to make. Both the build directory and build target arguments are optional. If the build directory is not specified, build_once will build from the package root. If the build target argument is not specified, then build_once will make the default build target.

Of note in this function is the existence of a MAKE_OPTS variable. This variable can be set by the user or passed as a variable assignment when this script is invoked to pass some options to make. For instance, to speed up the build process, it may not be a bad idea to set MAKE_OPTS to -j4 or -j8 depending upon the number of CPU cores you have. This will cut down the build time considerably.

#build a given workspace just once
build_once()
{
    local tag=.${1}_built
    local workspace=$2
    local builddir=$3
    local buildtarget=$4
    if [ ! -f $tag ]; then
        echo "Building $workspace..."

        #enter directory
        if [ "" != "$builddir" ]; then
            pushd $workspace/$builddir
        else
            pushd $workspace
        fi

        if make $MAKE_OPTS $buildtarget; then
            #restore directory
            popd
            touch $tag
            echo "Build succeeded."
        else
            echo "Failure building $workspace."
            exit 1
        fi
    else
        echo "$workspace already built."
    fi
}

In general, testing is critical for success. This is often true when building certain source packages as well. The multiprecision libraries on which gcc depends can be finicky on certain platforms. We definitely want to make sure that these libraries have built correctly, otherwise gcc may crash, or even worse, generate bad code. The test_once function runs the test suite of a newly built package. As before, this test suite is run just once, and the policy is enforced through the existence of a dot file that is created after the test suite runs successfully. The function takes four arguments, the tag to be used to create the dot file, the workspace name, the build target that kicks off the test suite, and the build directory. The build directory argument is optional. If not specified, then the test suite will be run from the package root.

#test a given workspace just once
test_once()
{
    local tag=.${1}_tested
    local workspace=$2
    local testcmd=$3
    local builddir=$4
    if [ ! -f $tag ]; then
        echo "Testing $workspace..."

        #enter directory
        if [ "" != "$builddir" ]; then
            pushd $workspace/$builddir
        else
            pushd $workspace
        fi

        if make $MAKE_OPTS $testcmd; then
            #restore directory
            popd
            touch $tag
            echo "Test succeeded."
        else
            echo "Failure testing $workspace."
            exit 1
        fi
    else
        echo "$workspace already tested."
    fi
}

Once a package has been extracted, configured, built, and tested, all that is left is to install it. The install_once function installs a package just once, using a dot file to enforce this policy. It takes four arguments, the tag name used to generate the dot file, the workspace, the optional build directory, and an optional install target name. If the build directory is not specified, installation is run from the package root. If the install target is not specified, then “install” is used as the target. The installation directory is specified by configure_once during the configuration step; each package is installed in ARM_TOOLCHAIN_DIR.

#install a given workspace just once
install_once()
{
    local tag=.${1}_installed
    local workspace=$2
    local builddir=$3
    local installtarget=$4
    if [ ! -f $tag ]; then
        echo "Installing $workspace..."

        #enter directory
        if [ "" != "$builddir" ]; then
            pushd $workspace/$builddir
        else
            pushd $workspace
        fi

        #set the install target
        if [ "" == "$installtarget" ]; then
            local installtarget=install
        fi

        if make $installtarget; then
            #restore directory
            popd
            touch $tag
            echo "Install succeeded."
        else
            echo "Failure installing $workspace."
            exit 1
        fi
    else
        echo "$workspace already installed."
    fi
}

That is all of the functions we need to create the build toolchain. Now, we can use these functions to run our script. First, we do a little sanity checking and environment setup. We want to make sure that the ARM_TOOLCHAIN_DIR environment variable is set and points to a real directory. We want to add this directory to the executable and library path so we can use tools after they have been built. Finally, we want to check that all tools required by this script have been installed. Using the functions we defined, this is short work.

#check that the environment variables we need have been set
check_directory_exists "$ARM_TOOLCHAIN_DIR" \
    "Please set ARM_TOOLCHAIN_DIR to a valid destination directory."

#override paths to use ARM_TOOLCHAIN_DIR
export DYLD_LIBRARY_PATH=$ARM_TOOLCHAIN_DIR/lib:$DYLD_LIBRARY_PATH
export LD_LIBRARY_PATH=$ARM_TOOLCHAIN_DIR/lib:$LD_LIBRARY_PATH
export PATH=$ARM_TOOLCHAIN_DIR/bin:$PATH

#check that we have the executables we need to run this script.
check_exe git
check_exe gcc
check_exe g++
check_exe curl
check_exe openssl
check_exe gpg
check_exe gzip
check_exe bzip2
check_exe xz
check_exe sed
check_exe libtool

Next, we want to download each of the required packages. We will also download public keys and signature files. Finally, we verify each of the packages to ensure that they were downloaded correctly.

#get GMP
get_pubkey_if_missing 28C67298 pgp.mit.edu
download_if_missing https://gmplib.org/download/gmp/gmp-6.1.1.tar.xz.sig
download_if_missing https://gmplib.org/download/gmp/gmp-6.1.1.tar.xz
verify_signature gmp-6.1.1.tar.xz.sig gmp-6.1.1.tar.xz

#get MPFR
get_pubkey_if_missing 980C197698C3739D pgp.mit.edu
download_if_missing http://www.mpfr.org/mpfr-current/mpfr-3.1.5.tar.xz.asc
download_if_missing http://www.mpfr.org/mpfr-current/mpfr-3.1.5.tar.xz
verify_signature mpfr-3.1.5.tar.xz.asc mpfr-3.1.5.tar.xz

#get MPC
get_pubkey_if_missing F7D5C9BF765C61E3 pgp.mit.edu
download_if_missing ftp://ftp.gnu.org/gnu/mpc/mpc-1.0.3.tar.gz.sig
download_if_missing ftp://ftp.gnu.org/gnu/mpc/mpc-1.0.3.tar.gz
verify_signature mpc-1.0.3.tar.gz.sig mpc-1.0.3.tar.gz

#get zlib
download_if_missing http://zlib.net/zlib-1.2.8.tar.gz
verify_md5_ugly 44d667c142d7cda120332623eab69f40 zlib-1.2.8.tar.gz

#get binutils
get_pubkey_if_missing 4AE55E93 pgp.mit.edu
download_if_missing https://ftp.gnu.org/gnu/binutils/binutils-2.27.tar.gz.sig
download_if_missing https://ftp.gnu.org/gnu/binutils/binutils-2.27.tar.gz
verify_signature binutils-2.27.tar.gz.sig binutils-2.27.tar.gz

#get gcc
get_pubkey_if_missing FC26A641 pgp.mit.edu
download_if_missing https://ftp.gnu.org/gnu/gcc/gcc-6.2.0/gcc-6.2.0.tar.bz2.sig
download_if_missing https://ftp.gnu.org/gnu/gcc/gcc-6.2.0/gcc-6.2.0.tar.bz2
verify_signature gcc-6.2.0.tar.bz2.sig gcc-6.2.0.tar.bz2

#get newlib
newlib_ck1="c60665e793dce2368a5baf23560beb50f641e1831854d702d1d7629fb6e9200c"
newlib_ck2="f814527f29796792a3d2dff81afee4255723df99ceb0732f99dd9580a17d2ac0"
download_if_missing ftp://sourceware.org/pub/newlib/newlib-2.4.0.tar.gz
verify_sha512 "$newlib_ck1$newlib_ck2"
newlib-2.4.0.tar.gz

#get gdb
get_pubkey_if_missing FF325CF3 pgp.mit.edu
download_if_missing https://ftp.gnu.org/gnu/gdb/gdb-7.12.tar.xz.sig
download_if_missing https://ftp.gnu.org/gnu/gdb/gdb-7.12.tar.xz
verify_signature gdb-7.12.tar.xz.sig gdb-7.12.tar.xz

The last step in the script is to extract and build each package in the right order. Using the functions we defined above, this is a very easy process.

#extract and build GMP
extract_once gmp J gmp-6.1.1.tar.xz
configure_once gmp `pwd`/gmp-6.1.1
build_once gmp `pwd`/gmp-6.1.1
test_once gmp `pwd`/gmp-6.1.1 check
install_once gmp `pwd`/gmp-6.1.1

#extract and build MPFR
cfg_mpfr1="--with-gmp=$ARM_TOOLCHAIN_DIR --disable-shared --enable-static"
extract_once mpfr J mpfr-3.1.5.tar.xz
configure_once mpfr `pwd`/mpfr-3.1.5 "$cfg_mpfr1"
build_once mpfr `pwd`/mpfr-3.1.5
test_once mpfr `pwd`/mpfr-3.1.5 check
install_once mpfr `pwd`/mpfr-3.1.5

#extract and build MPC
cfg_mpc1="--with-gmp=$ARM_TOOLCHAIN_DIR --with-mpfr=$ARM_TOOLCHAIN_DIR"
cfg_mpc2="--disable-shared --enable-static"
extract_once mpc z mpc-1.0.3.tar.gz
configure_once mpc `pwd`/mpc-1.0.3 "$cfg_mpc1 $cfg_mpc2"
build_once mpc `pwd`/mpc-1.0.3
test_once mpc `pwd`/mpc-1.0.3 check
install_once mpc `pwd`/mpc-1.0.3

#extract and build binutils
cfg_b1="--target=arm-none-eabi --with-cpu=cortex-m4 --with-mode=thumb"
cfg_b2="--enable-interwork --with-float=soft --enable-multilib"
cfg_b3="--disable-nls --disable-shared --enable-static"
extract_once binutils z binutils-2.27.tar.gz
configure_once binutils `pwd`/binutils-2.27 "$cfg_b1 $cfg_b2 $cfg_b3"
build_once binutils `pwd`/binutils-2.27
test_once binutils `pwd`/binutils-2.27 check
install_once binutils `pwd`/binutils-2.27

#extract and build gcc (bootstrap)
cfg_g1="--target=arm-none-eabi --with-cpu=cortex-m4 --with-float=soft"
cfg_g2="--with-mode=thumb --enable-interwork --enable-multilib"
cfg_g3="--with-system-zlib --with-newlib --without-headers --disable-shared"
cfg_g4="--disable-nls --with-gnu-as --with-gnu-ld"
cfg_g5="--with-gmp=$ARM_TOOLCHAIN_DIR --with-mpfr=$ARM_TOOLCHAIN_DIR"
cfg_g6="--with-mpc=$ARM_TOOLCHAIN_DIR --enable-languages=c"
extract_once gcc j gcc-6.2.0.tar.bz2
configure_once gcc_bootstrap `pwd`/gcc-6.2.0 \
    "$cfg_g1 $cfg_g2 $cfg_g3 $cfg_g4 $cfg_g5 $cfg_g6" bootstrap
build_once gcc_bootstrap `pwd`/gcc-6.2.0 bootstrap all-gcc
install_once gcc_bootstrap `pwd`/gcc-6.2.0 bootstrap install-gcc

#extract and build newlib
cfg_n1="--target=arm-none-eabi --with-cpu=cortex-m4 --with-float=soft"
cfg_n2="--with-mode=thumb --enable-interwork --enable-multilib --with-gnu-as"
cfg_n3="--with-gnu-ld --disable-nls --disable-newlib-supplied-syscalls"
cfg_n4="--enable-newlib-reent-small --disable-newlib-fvwrite-in-streamio"
cfg_n5="--disable-newlib-fseek-optimization --disable-newlib-wide-orient"
cfg_n6="--enable-newlib-nano-malloc --disable-newlib-unbuf-stream-opt"
cfg_n7="--enable-lite-exit --enable-newlib-global-atexit"
extract_once newlib z newlib-2.4.0.tar.gz
configure_once newlib `pwd`/newlib-2.4.0 \
    "$cfg_n1 $cfg_n2 $cfg_n3 $cfg_n4 $cfg_n5 $cfg_n6 $cfg_n7"
build_once newlib `pwd`/newlib-2.4.0
install_once newlib `pwd`/newlib-2.4.0

#build full GCC C/C++
cfg_g1="--target=arm-none-eabi --with-cpu=cortex-m4 --with-float=soft"
cfg_g2="--with-mode=thumb --enable-interwork --enable-multilib"
cfg_g3="--with-system-zlib --with-newlib --without-headers --disable-shared"
cfg_g4="--disable-nls --with-gnu-as --with-gnu-ld"
cfg_g5="--with-gmp=$ARM_TOOLCHAIN_DIR --with-mpfr=$ARM_TOOLCHAIN_DIR"
cfg_g6="--with-mpc=$ARM_TOOLCHAIN_DIR --enable-languages=c,c++"
extract_once gcc j gcc-6.2.0.tar.bz2
configure_once gcc_full `pwd`/gcc-6.2.0 \
    "$cfg_g1 $cfg_g2 $cfg_g3 $cfg_g4 $cfg_g5 $cfg_g6" full
build_once gcc_full `pwd`/gcc-6.2.0 full all
install_once gcc_full `pwd`/gcc-6.2.0 full install

#extract and build gdb
extract_once gdb J gdb-7.12.tar.xz
configure_once gdb `pwd`/gdb-7.12 "--target=arm-none-eabi"
build_once gdb `pwd`/gdb-7.12
install_once gdb `pwd`/gdb-7.12

That’s it. Combining all of these into a script gives us a bit of automation that can build the ARM toolchain for our Kinetis part. This script is easily modified to track new versions of GCC or other dependencies, and it can be easily adapted to other chipsets. If we want someone else to be able to build our embedded project, this script can come in handy for ensuring that the exact same toolchain is available to everyone.

As a final note, from this point on, we’ll need to update our build environment to pull in the new executables for this toolchain.

Testing the Toolchain

The last thing we’ll do in this article is test that the toolchain works. Note that this test will only work if the Segger tool was downloaded and the Segger OpenSDA firmware was loaded onto the evaluation board.

First, we need to build a linker control file that will allow us to create firmware images for the K22 microcontroller on the evaluation board. The linker control file defines where important segments of the code should go. The linker uses this file to build a firmware image that can be executed by the microcontroller. Among the details we have to get right are the sizes of the memory regions and their locations, such as RAM and code flash. This information can be discovered by reading the reference manual for the part on the evaluation board, which can be found here. Be sure to download and save this reference manual, as it is pretty much the bible for how we proceed with building the embedded OS and drivers.

The first thing we need to define in the linker control file is the entry point for our firmware image. We are using Newlib for our C runtime, so the entry point is predefined by Newlib as _start. We will also provide default values for the stack and heap sizes, and also provide the option of creating an interrupt vector table in RAM.

ENTRY(_start)

SZ_STACK                = DEFINED(__sz_stack__)        ? __sz_stack__ : 0x0400;
SZ_HEAP                 = DEFINED(__sz_heap__)         ? __sz_heap__  : 0x0400;
SZ_RAM_VECTOR_TABLE     = DEFINED(__sz_ram_vec_tab__)  ?       0x0400 : 0x0000;

By default, we provide a kilobyte of stack and heap, which is what we will use for the kernel’s stack and heap. This stack will also be used by the interrupts, so we want it to be sufficient enough in size to allow re-entry into the kernel by the interrupts later on.

Next, we need to define the memory areas for this MCU. According to the reference manual, the memory map looks something like this.

Address Range   Description
0x00000000 - 0x07FFFFFF     Program Flash / Constants
0x08000000 - 0x1BFFFFFF   FlexBus / Reserved
0x1C000000 - 0x1FFFFFFF   SRAM_L
0x20000000 - 0x200FFFFF   SRAM_H
 

The important things to notice in this memory map is the beginning of program flash, and the end of SRAM_L and the beginning of SRAM_H. In the Kinetis family of parts, the two halves of SRAM are anchored, but their sizes can vary. The end of SRAM_L is anchored at address 0x1FFFFFFF. The beginning of SRAM_H is anchored at 0x20000000. These two halves form a contiguous region, but that region is anchored at the middle. According to the reference manual, this part has 128KB of RAM, which is split evenly between these two regions. Therefore, with a little math, we know that SRAM_L begins at address 0x1FFF0000 and has a length of 64KB, or 0x00010000 bytes. We know that SRAM_H begins at address 0x20000000 and has a length of 0x00010000 bytes.

The interrupt vector, by default, starts at address 0x00000000. There are 256 entries in the vector table, each of which is a 32-bit, or four byte address. Therefore, the total size of the interrupt vector table is 1024 bytes or 0x400 bytes. Immediately after the vector table is the flash config, which is 16 bytes or 0x10 bytes in length. Then, the main program flash starts, at offset 0x410. The total flash size on this chip is 512KB, or 0x00080000 bytes. Subtracting the sizes of the interrupt vector table and the flash config, we get a length for the main program flash of 0x0007FBF0 bytes.

Putting all of this together, here is the memory map for our linker control file:

MEMORY
{
    mem_interrupts      (RX) : ORIGIN = 0x00000000, LENGTH = 0x00000400
    mem_flash_config    (RX) : ORIGIN = 0x00000400, LENGTH = 0x00000010
    mem_text            (RX) : ORIGIN = 0x00000410, LENGTH = 0x0007FBF0
    mem_data            (RW) : ORIGIN = 0x1FFF0000, LENGTH = 0x00010000
    mem_data_2          (RW) : ORIGIN = 0x20000000, LENGTH = 0x00010000
}

Object files place executable code, constants, and initialized data in different sections. The rest of the linker control file tells the linker where to place each of these sections in memory. We use the offsets provided in the memory map to organize these sections in a way that provides the linker with enough information to decide where each section goes.

SECTIONS
{

We’ll start with the interrupt vector table. We place all .isr_vector sections that are defined in object fils into the .interrupts section. We then place this section in the mem_interrupts memory region.

    .interrupts :
    {
        __VECTOR_TABLE = .;
        . = ALIGN(4);
        KEEP(*(.isr_vector))
        . = ALIGN(4);
    } > mem_interrupts

We also set the symbol, __VECTOR_TABLE to the location where this interrupt vector table is located. We use 32-bit alignment for each of the vectors, and we use a glob pattern to pull all .isr_vector sections into this section.

Next, we define a section for flash configuration data. This will be placed in the mem_flash_config memory region.

    .flash_config :
    {
        . = ALIGN(4);
        KEEP(*(.FlashConfig))
        . = ALIGN(4);
    } > mem_flash_config

Next, we define a section to hold executable code. This will be placed in the mem_text region. There are a lot of glob patterns here. The most important one is .text, but we throw in a few of the other common sections that may be encountered depending upon toolchains and languages used.

    .text :
    {
        . = ALIGN(4);
        *(.text)
        *(.text*)
        *(.rodata)
        *(.rodata*)
        *(.glue_7)
        *(.glue_7t)
        *(.eh_frame)
        KEEP (*(.init))
        KEEP (*(.fini))
        . = ALIGN(4);
    } > mem_text

Next, we provide some sections that deal with exception unwinding and stack traces. These may be useful later.

    .ARM.extab :
    {
        *(.ARM.extab* .gnu.linkonce.armextab.*)
    } > mem_text
    
    .ARM :
    {
        __exidx_start = .;
        *(.ARM.exidx*)
        __exidx_end = .;
    } > mem_text

The .ctors and .dtors sections glob together sections that are used by object constructors and destructors during program initialization and shutdown. We also include .preinit_array, .init_array, and .fini_array, which round out the initialization and shutdown function pointers needed to start up a C/C++ runtime environment.

    .ctors :
    {
        __CTOR_LIST__ = .;
        KEEP (*crtbegin.o(.ctors))
        KEEP (*crtbegin?.o(.ctors))
        KEEP (*(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors))
        KEEP (*(SORT(.ctors.*)))
        KEEP (*(.ctors))
        __CTOR_END__ = .;
    } > mem_text
    
    .dtors :
    {
        __DTOR_LIST__ = .;
        KEEP (*crtbegin.o(.dtors))
        KEEP (*crtbegin?.o(.dtors))
        KEEP (*(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors))
        KEEP (*(SORT(.dtors.*)))
        KEEP (*(.dtors))
        __DTOR_END__ = .;
    } > mem_text
    
    .preinit_array :
    {
        PROVIDE_HIDDEN (__preinit_array_start = .);
        KEEP (*(.preinit_array*))
        PROVIDE_HIDDEN (__preinit_array_end = .);
    } > mem_text
    
    .init_array :
    {
        PROVIDE_HIDDEN (__init_array_start = .);
        KEEP (*(SORT(.init_array.*)))
        KEEP (*(.init_array*))
        PROVIDE_HIDDEN (__init_array_end = .);
    } > mem_text
    
    .fini_array :
    {
        PROVIDE_HIDDEN (__fini_array_start = .);
        KEEP (*(SORT(.fini_array.*)))
        KEEP (*(.fini_array*))
        PROVIDE_HIDDEN (__fini_array_end = .);
    } > mem_text

That wraps up the text data. We can now create a symbol that represents the end of text data, __etext, and a symbol that represents the beginning of ROM data __DATA_ROM.

    __etext = .;
    __DATA_ROM = .;

If RAM interrupts are enabled, they will go at the beginning of mem_data. The .interrupts_ram section picks up any of these. We can then compute a few related symbols that can be used by the firmware to set up the RAM interrupt vector if defined.

    .interrupts_ram :
    {
        . = ALIGN(4);
        __VECTOR_RAM__ = .;
        __interrupts_ram_start__ = .;
        *(.interrupts_ram)
        . += SZ_RAM_VECTOR_TABLE;
        . = ALIGN(4);
        __interrupts_ram_end__ = .;
    } > mem_data
    
    __VECTOR_RAM = DEFINED(__sz_ram_vec_tab__)
        ? __VECTOR_RAM__
        : ORIGIN(mem_interrupts);
    __RAM_VECTOR_TABLE_SIZE_BYTES = DEFINED(__sz_ram_vec_tab__)
        ?  (__interrupts_ram_end__ - __interrupts_ram_start__)
        : 0x0;

Next, we define a section to hold all initialized data.

    .data : AT(__DATA_ROM)
    {
        . = ALIGN(4);
        __DATA_RAM = .;
        __data_start__ = .;
        *(.data)
        *(.data*)
        . = ALIGN(4);
        __data_end__ = .;
    } > mem_data

    __DATA_END = __DATA_ROM + (__data_end__ - __data_start__);
    text_end = ORIGIN(mem_text) + LENGTH(mem_text);
    ASSERT(__DATA_END <= text_end, "region mem_text overflowed with text and data")

The last assertion just checks that the text section doesn’t override the data section.

Next, we define a section to hold the .bss data, or uninitialized data.

    .bss :
    {
        . = ALIGN(4);
        __START_BSS = .;
        __bss_start__ = .;
        *(.bss)
        *(.bss*)
        *(COMMON)
        . = ALIGN(4);
        __bss_end__ = .;
        __END_BSS = .;
    } > mem_data

Finally, we place the heap and stack in the mem_data_2 region and make some simple assertions to make sure that our sizes and offsets are sane.

    .stack :
    {
        . = ALIGN(8);
        . += SZ_STACK;
    } > mem_data_2
    
    __StackTop = ORIGIN(mem_data_2) + LENGTH(mem_data_2);
    __StackLimit = __StackTop - SZ_STACK;
    PROVIDE(__stack = __StackTop);
    
    ASSERT(__StackLimit >= __HeapLimit,
        "region mem_data_2 overflowed with stack and heap")

To close out the linker control file, we null out the .ARM.attributes sections so these do not appear in the firmware image. These are still available for debugging purposes.

    .ARM.attributes 0 : { *(.ARM.attributes) }
}

We’ll name the linker control file, k22_board.ld.

 

Dummy Interrupt Vector Table

To start our example code, we will need to define a dummy interrupt vector table. Since we just want to test that our toolchain works, we are going to stub out this table. In subsequent articles, we’ll add interrupt handlers to this table and turn on interrupts.

    .syntax unified
    .arch armv7-m

    .section .isr_vector, "a"
    .align 2
    .global __isr_vector
__isr_vector:
    .long   __StackTop          /* Top of Stack */
    .long   _start              /* Reset handler -- start up the board */
    .long   0x0                 /* NMI Handler -- disabled */
    .long   0x0                 /* Hard Fault Handler -- disabled */
    .long   0x0                 /* MPU Fault Handler -- disabled */
    .long   0x0                 /* Bus Fault Handler -- disabled */
    .long   0x0                 /* Usage Fault Handler -- disabled */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* SVCall Handler -- disabled */
    .long   0x0                 /* Debug Monitor Handler -- disabled */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* PendSV Handler -- disabled */
    .long   0x0                 /* SysTick Handler -- disabled */

    /* External Interrupts */
    .long   0x0                 /* DMA0  Transfer Complete -- disabled */
    .long   0x0                 /* DMA1  Transfer Complete -- disabled */
    .long   0x0                 /* DMA2  Transfer Complete -- disabled */
    .long   0x0                 /* DMA3  Transfer Complete -- disabled */
    .long   0x0                 /* DMA4  Transfer Complete -- disabled */
    .long   0x0                 /* DMA5  Transfer Complete -- disabled */
    .long   0x0                 /* DMA6  Transfer Complete -- disabled */
    .long   0x0                 /* DMA7  Transfer Complete -- disabled */
    .long   0x0                 /* DMA8  Transfer Complete -- disabled */
    .long   0x0                 /* DMA9  Transfer Complete -- disabled */
    .long   0x0                 /* DMA10 Transfer Complete -- disabled */
    .long   0x0                 /* DMA11 Transfer Complete -- disabled */
    .long   0x0                 /* DMA12 Transfer Complete -- disabled */
    .long   0x0                 /* DMA13 Transfer Complete -- disabled */
    .long   0x0                 /* DMA14 Transfer Complete -- disabled */
    .long   0x0                 /* DMA15 Transfer Complete -- disabled */
    .long   0x0                 /* DMA Error Interrupt -- disabled */
    .long   0x0                 /* Normal Interrupt -- disabled */
    .long   0x0                 /* FTFA Cmd Complete Interrupt -- disabled */
    .long   0x0                 /* Read Collision Interrupt -- disabled */
    .long   0x0                 /* Low Voltage Warning -- disabled */
    .long   0x0                 /* Low Leakage Wakeup Unit -- disabled */
    .long   0x0                 /* WDOG Interrupt -- disabled */
    .long   0x0                 /* RNG Interrupt -- disabled */
    .long   0x0                 /* I2C0 Interrupt -- disabled */
    .long   0x0                 /* I2C1 Interrupt -- disabled */
    .long   0x0                 /* SPI0 Interrupt -- disabled */
    .long   0x0                 /* SPI1 Interrupt -- disabled */
    .long   0x0                 /* I2S0 transmit interrupt -- disabled */
    .long   0x0                 /* I2S0 receive interrupt -- disabled */
    .long   0x0                 /* LPUART0 status/error interrupt -- disabled */
    .long   0x0                 /* UART0 recv/xmit interrupt -- disabled */
    .long   0x0                 /* UART0 error interrupt -- disabled */
    .long   0x0                 /* UART1 recv/xmit interrupt -- disabled */
    .long   0x0                 /* UART1 error interrupt -- disabled */
    .long   0x0                 /* UART2 recv/xmit interrupt -- disabled */
    .long   0x0                 /* UART2 error interrupt -- disabled */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* ADC0 interrupt -- disabled */
    .long   0x0                 /* CMP0 interrupt -- disabled */
    .long   0x0                 /* CMP1 interrupt -- disabled */
    .long   0x0                 /* FTM0 interrupt -- disabled */
    .long   0x0                 /* FTM1 interrupt -- disabled */
    .long   0x0                 /* FTM2 interrupt -- disabled */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* RTC interrupt -- disabled */
    .long   0x0                 /* RTC seconds interrupt -- disabled */
    .long   0x0                 /* PIT0 interrupt -- disabled */
    .long   0x0                 /* PIT1 interrupt -- disabled */
    .long   0x0                 /* PIT2 interrupt -- disabled */
    .long   0x0                 /* PIT3 interrupt -- disabled */
    .long   0x0                 /* PDB0 interrupt -- disabled */
    .long   0x0                 /* USB0 interrupt -- disabled */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* DAC0 interrupt -- disabled */
    .long   0x0                 /* MCG interrupt -- disabled */
    .long   0x0                 /* LPTimer interrupt -- disabled */
    .long   0x0                 /* PortA interrupt -- disabled */
    .long   0x0                 /* PortB interrupt -- disabled */
    .long   0x0                 /* PortC interrupt -- disabled */
    .long   0x0                 /* PortD interrupt -- disabled */
    .long   0x0                 /* PortE interrupt -- disabled */
    .long   0x0                 /* Software interrupt -- disabled */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* FTM3 interrupt -- disabled */
    .long   0x0                 /* DAC1 interrupt -- disabled */
    .long   0x0                 /* ADC1 interrupt -- disabled */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Reserved */
    .long   0x0                 /* Interrupt 102 - disabled */
    .long   0x0                 /* Interrupt 103 - disabled */
    .long   0x0                 /* Interrupt 104 - disabled */
    .long   0x0                 /* Interrupt 105 - disabled */
    .long   0x0                 /* Interrupt 106 - disabled */
    .long   0x0                 /* Interrupt 107 - disabled */
    .long   0x0                 /* Interrupt 108 - disabled */
    .long   0x0                 /* Interrupt 109 - disabled */
    .long   0x0                 /* Interrupt 110 - disabled */
    .long   0x0                 /* Interrupt 111 - disabled */
    .long   0x0                 /* Interrupt 112 - disabled */
    .long   0x0                 /* Interrupt 113 - disabled */
    .long   0x0                 /* Interrupt 114 - disabled */
    .long   0x0                 /* Interrupt 115 - disabled */
    .long   0x0                 /* Interrupt 116 - disabled */
    .long   0x0                 /* Interrupt 117 - disabled */
    .long   0x0                 /* Interrupt 118 - disabled */
    .long   0x0                 /* Interrupt 119 - disabled */
    .long   0x0                 /* Interrupt 120 - disabled */
    .long   0x0                 /* Interrupt 121 - disabled */
    .long   0x0                 /* Interrupt 122 - disabled */
    .long   0x0                 /* Interrupt 123 - disabled */
    .long   0x0                 /* Interrupt 124 - disabled */
    .long   0x0                 /* Interrupt 125 - disabled */
    .long   0x0                 /* Interrupt 126 - disabled */
    .long   0x0                 /* Interrupt 127 - disabled */
    .long   0x0                 /* Interrupt 128 - disabled */
    .long   0x0                 /* Interrupt 129 - disabled */
    .long   0x0                 /* Interrupt 130 - disabled */
    .long   0x0                 /* Interrupt 131 - disabled */
    .long   0x0                 /* Interrupt 132 - disabled */
    .long   0x0                 /* Interrupt 133 - disabled */
    .long   0x0                 /* Interrupt 134 - disabled */
    .long   0x0                 /* Interrupt 135 - disabled */
    .long   0x0                 /* Interrupt 136 - disabled */
    .long   0x0                 /* Interrupt 137 - disabled */
    .long   0x0                 /* Interrupt 138 - disabled */
    .long   0x0                 /* Interrupt 139 - disabled */
    .long   0x0                 /* Interrupt 140 - disabled */
    .long   0x0                 /* Interrupt 141 - disabled */
    .long   0x0                 /* Interrupt 142 - disabled */
    .long   0x0                 /* Interrupt 143 - disabled */
    .long   0x0                 /* Interrupt 144 - disabled */
    .long   0x0                 /* Interrupt 145 - disabled */
    .long   0x0                 /* Interrupt 146 - disabled */
    .long   0x0                 /* Interrupt 147 - disabled */
    .long   0x0                 /* Interrupt 148 - disabled */
    .long   0x0                 /* Interrupt 149 - disabled */
    .long   0x0                 /* Interrupt 150 - disabled */
    .long   0x0                 /* Interrupt 151 - disabled */
    .long   0x0                 /* Interrupt 152 - disabled */
    .long   0x0                 /* Interrupt 153 - disabled */
    .long   0x0                 /* Interrupt 154 - disabled */
    .long   0x0                 /* Interrupt 155 - disabled */
    .long   0x0                 /* Interrupt 156 - disabled */
    .long   0x0                 /* Interrupt 157 - disabled */
    .long   0x0                 /* Interrupt 158 - disabled */
    .long   0x0                 /* Interrupt 159 - disabled */
    .long   0x0                 /* Interrupt 160 - disabled */
    .long   0x0                 /* Interrupt 161 - disabled */
    .long   0x0                 /* Interrupt 162 - disabled */
    .long   0x0                 /* Interrupt 163 - disabled */
    .long   0x0                 /* Interrupt 164 - disabled */
    .long   0x0                 /* Interrupt 165 - disabled */
    .long   0x0                 /* Interrupt 166 - disabled */
    .long   0x0                 /* Interrupt 167 - disabled */
    .long   0x0                 /* Interrupt 168 - disabled */
    .long   0x0                 /* Interrupt 169 - disabled */
    .long   0x0                 /* Interrupt 170 - disabled */
    .long   0x0                 /* Interrupt 171 - disabled */
    .long   0x0                 /* Interrupt 172 - disabled */
    .long   0x0                 /* Interrupt 173 - disabled */
    .long   0x0                 /* Interrupt 174 - disabled */
    .long   0x0                 /* Interrupt 175 - disabled */
    .long   0x0                 /* Interrupt 176 - disabled */
    .long   0x0                 /* Interrupt 177 - disabled */
    .long   0x0                 /* Interrupt 178 - disabled */
    .long   0x0                 /* Interrupt 179 - disabled */
    .long   0x0                 /* Interrupt 180 - disabled */
    .long   0x0                 /* Interrupt 181 - disabled */
    .long   0x0                 /* Interrupt 182 - disabled */
    .long   0x0                 /* Interrupt 183 - disabled */
    .long   0x0                 /* Interrupt 184 - disabled */
    .long   0x0                 /* Interrupt 185 - disabled */
    .long   0x0                 /* Interrupt 186 - disabled */
    .long   0x0                 /* Interrupt 187 - disabled */
    .long   0x0                 /* Interrupt 188 - disabled */
    .long   0x0                 /* Interrupt 189 - disabled */
    .long   0x0                 /* Interrupt 190 - disabled */
    .long   0x0                 /* Interrupt 191 - disabled */
    .long   0x0                 /* Interrupt 192 - disabled */
    .long   0x0                 /* Interrupt 193 - disabled */
    .long   0x0                 /* Interrupt 194 - disabled */
    .long   0x0                 /* Interrupt 195 - disabled */
    .long   0x0                 /* Interrupt 196 - disabled */
    .long   0x0                 /* Interrupt 197 - disabled */
    .long   0x0                 /* Interrupt 198 - disabled */
    .long   0x0                 /* Interrupt 199 - disabled */
    .long   0x0                 /* Interrupt 200 - disabled */
    .long   0x0                 /* Interrupt 201 - disabled */
    .long   0x0                 /* Interrupt 202 - disabled */
    .long   0x0                 /* Interrupt 203 - disabled */
    .long   0x0                 /* Interrupt 204 - disabled */
    .long   0x0                 /* Interrupt 205 - disabled */
    .long   0x0                 /* Interrupt 206 - disabled */
    .long   0x0                 /* Interrupt 207 - disabled */
    .long   0x0                 /* Interrupt 208 - disabled */
    .long   0x0                 /* Interrupt 209 - disabled */
    .long   0x0                 /* Interrupt 210 - disabled */
    .long   0x0                 /* Interrupt 211 - disabled */
    .long   0x0                 /* Interrupt 212 - disabled */
    .long   0x0                 /* Interrupt 213 - disabled */
    .long   0x0                 /* Interrupt 214 - disabled */
    .long   0x0                 /* Interrupt 215 - disabled */
    .long   0x0                 /* Interrupt 216 - disabled */
    .long   0x0                 /* Interrupt 217 - disabled */
    .long   0x0                 /* Interrupt 218 - disabled */
    .long   0x0                 /* Interrupt 219 - disabled */
    .long   0x0                 /* Interrupt 220 - disabled */
    .long   0x0                 /* Interrupt 221 - disabled */
    .long   0x0                 /* Interrupt 222 - disabled */
    .long   0x0                 /* Interrupt 223 - disabled */
    .long   0x0                 /* Interrupt 224 - disabled */
    .long   0x0                 /* Interrupt 225 - disabled */
    .long   0x0                 /* Interrupt 226 - disabled */
    .long   0x0                 /* Interrupt 227 - disabled */
    .long   0x0                 /* Interrupt 228 - disabled */
    .long   0x0                 /* Interrupt 229 - disabled */
    .long   0x0                 /* Interrupt 230 - disabled */
    .long   0x0                 /* Interrupt 231 - disabled */
    .long   0x0                 /* Interrupt 232 - disabled */
    .long   0x0                 /* Interrupt 233 - disabled */
    .long   0x0                 /* Interrupt 234 - disabled */
    .long   0x0                 /* Interrupt 235 - disabled */
    .long   0x0                 /* Interrupt 236 - disabled */
    .long   0x0                 /* Interrupt 237 - disabled */
    .long   0x0                 /* Interrupt 238 - disabled */
    .long   0x0                 /* Interrupt 239 - disabled */
    .long   0x0                 /* Interrupt 240 - disabled */
    .long   0x0                 /* Interrupt 241 - disabled */
    .long   0x0                 /* Interrupt 242 - disabled */
    .long   0x0                 /* Interrupt 243 - disabled */
    .long   0x0                 /* Interrupt 244 - disabled */
    .long   0x0                 /* Interrupt 245 - disabled */
    .long   0x0                 /* Interrupt 246 - disabled */
    .long   0x0                 /* Interrupt 247 - disabled */
    .long   0x0                 /* Interrupt 248 - disabled */
    .long   0x0                 /* Interrupt 249 - disabled */
    .long   0x0                 /* Interrupt 250 - disabled */
    .long   0x0                 /* Interrupt 251 - disabled */
    .long   0x0                 /* Interrupt 252 - disabled */
    .long   0x0                 /* Interrupt 253 - disabled */
    .long   0x0                 /* Interrupt 254 - disabled */
    .long   0xFFFFFFFF          /* Reserved for TRIM value */

    .size   __isr_vector, . - __isr_vector

    /* flash configuration */
    .section .FlashConfig, "a"
    .long 0xFFFFFFFF
    .long 0xFFFFFFFF
    .long 0xFFFFFFFF
    .long 0xFFFFFFFE

 

Example Program

With the linker control file completed and the dummy interrupt vector table defined, we can now build a simple example program to test that the build toolchain properly builds an image that we can load, and that the debugger allows us to step through the code. We’ll name this file, main.c.

    int z;

    int main()
    {
        z = 7;

        return 0;
    }

The other thing we will need to provide is a system exit method. When main exits, the _exit method is eventually called by the C runtime. Generally, this method is used to do any operating-system specific work necessary to return control of the system back to the operating system. In our case, we are running on bare metal, and there is no OS to which to return. Instead, we’ll write an _exit method that just enters an infinite loop. We’ll name this file, sysexit.c.

    void _exit(int status)
    {
        while(1);
    }

For simplicity sake, we’ll write a quick bash script for compiling our code. Later on, we’ll replace this with a GNU Make build script, but for now, we don’t need anything that elaborate. This script grabs all of the C source files in the current directory, and performs a single-shot compile using our new GCC compiler. We’ll call this script, build.sh.

#!/bin/sh -x

SOURCES=`ls *.c`

arm-none-eabi-gcc -mcpu=cortex-m4 -mfloat-abi=soft -mthumb -Wall -fno-common \
    -ffunction-sections -fdata-sections -ffreestanding -fno-builtin -mapcs \
    -std=c99 -O0 -gdwarf-2 -o test.elf intvecs.S $SOURCES -Tk22_board.ld \
    -Xlinker -Map=test.map -L ${ARM_TOOLCHAIN_DIR}/arm-none-eabi/lib

After running this script, two files should be created. test.elf is the ELF file for our image, and test.map is a linker map.

At this point, we can plug the evaluation board into the computer’s USB port, and start up the Segger GDB server. How this is started is dependent upon the installation. But, if running from the command-line, the startup should look something like this:

    JLinkGDBServer -device MK22FN512xxx12 -endian little -if SWD -select USB \
    -speed 1000

Next, in a separate terminal window, we’ll want to start up the ARM debugger built as part of the toolchain.

arm-none-eabi-gdb

From within GDB, we first want to load the firmware image file.

file test.elf

Now, we want to connect to the remote target.

target remote:2331

Now, we’ll want to halt the target CPU so we can load the firmware image.

monitor halt
load

At this point, the firmware image is loaded. We want to set a breakpoint in main just before the global variable z is set.

list main
break 5

Now, we’ll let the monitor run until it hits our breakpoint.

cont

To verify that our code is working, we will first print the value of z. It should be 0, since z is in the .bss section.

print z

Now, step over the instruction to set z.

next

Verify that z was set by printing z again.

print z

If z has been set to 7, then we have verified that the toolchain, the runtime library, and the debugger are all running correctly. That’s all for this article. Next time, we’ll begin building some task management functionality.