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.