Better BSP

Embedded systems don't have to suck

Anatomy of a Bootable ISO

I recently tried to make a bootable ISO using Buildroot, and encountered some difficulty. In that process, I realized that I don’t actually know what all goes into a bootable Linux ISO. So this is my collection of notes that I made while trying to answer that question.

Starting out, the ISOLINUX documentation is going to be my guide, along with a Core Linux ISO. I was originally going to use an Ubuntu ISO, but I had a hard time getting the Ubuntu ISO to boot in qemu without having a graphic display attached to it (the box I’m running this all on is headless).

Loopback mount of the ISO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
aja042@tellurium:~$ sudo mkdir /mnt/corelinux
[sudo] password for aja042:

aja042@tellurium:~$ sudo mount -o loop ~/Downloads/Core-current.iso /mnt/corelinux/
mount: /dev/loop1 is write-protected, mounting read-only

aja042@tellurium:~$ ls /mnt/corelinux/
boot

aja042@tellurium:~$ ls /mnt/corelinux/boot/
core.gz  isolinux  vmlinuz

aja042@tellurium:~$ ls /mnt/corelinux/boot/isolinux/
boot.cat  boot.msg  f2  f3  f4  isolinux.bin  isolinux.cfg

This is a nice simple looking ISO.

What’s in the isolinux.cfg?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
aja042@tellurium:~$ cat /mnt/corelinux/boot/isolinux/isolinux.cfg
display boot.msg
default microcore
label microcore
        kernel /boot/vmlinuz
        initrd /boot/core.gz
        append loglevel=3
                        
label mc
        kernel /boot/vmlinuz
        append initrd=/boot/core.gz loglevel=3
implicit 0
prompt 1
timeout 300
F1 boot.msg
F2 f2
F3 f3
F4 f4

And booting it is pretty simple too:

1
$ qemu-system-x86_64 -curses -boot d -cdrom ~/Downloads/Core-current.iso -m 512

Note that I’m using the -curses option because I don’t have a display on this machine. Drop that if you’re running from desktop Linux. If you do run with -curses, note that you can press ESC 2 to get into the QEMU monitor mode and type quit to exit.

What else is on here?

1
2
aja042@tellurium:/mnt/corelinux/boot$ file vmlinuz
vmlinuz: Linux kernel x86 boot executable bzImage, version 4.2.9-tinycore (tc@box) #1999 SMP Mon Jan 18 19:42:12 UTC 2016, RO-rootFS, swap_dev 0x3, Normal VGA

So that’s a standard bzImage kernel, renamed to vmlinuz for some reason. Good enough!

1
2
aja042@tellurium:/mnt/corelinux/boot$ file core.gz
core.gz: gzip compressed data, was "core.cpio", last modified: Mon Jul  4 08:08:02 2016, max compression, from Unix

That’s the initrd referenced above. Together, those are the only real files that the isolinux.cfg references.

Can I repack this?

A good test to see if I get how an image like this works is to figure out how to recreate it from the files. One important thing here is that this is a bootable ISO:

1
2
aja042@tellurium:/mnt/corelinux/boot/isolinux$ file ~/Downloads/Core-current.iso
/home/aja042/Downloads/Core-current.iso: ISO 9660 CD-ROM filesystem data 'Core' (bootable)

Which is likely an extra wrinkle.

Going off the instructions, you can get a binary version of syslinux from kernel.org. I’m going to use 6.03.

For a little bit of an added challenge, I’m going to make the directory layout different - I’m just going to stick all the files in the root of the new ISO.

1
2
3
aja042@tellurium:~/workspace/bootable-iso$ mkdir cd_root
aja042@tellurium:~/workspace/bootable-iso$ cd cd_root/
aja042@tellurium:~/workspace/bootable-iso/cd_root$ cp /mnt/corelinux/boot/{vmlinuz,core.gz} .

Next, the instructions say to copy isolinux.bin and ldlinux.c32:

1
2
aja042@tellurium:~/workspace/bootable-iso/cd_root$ cp ../syslinux-6.03/bios/core/isolinux.bin .
aja042@tellurium:~/workspace/bootable-iso/cd_root$ cp ../syslinux-6.03/bios/com32/elflink/ldlinux/ldlinux.c32 .

And now, I’m supposed to create isolinux.cfg. Here’s what I put in it:

1
2
3
4
5
6
7
aja042@tellurium:~/workspace/bootable-iso$ cat cd_root/isolinux.cfg
default microcore
label microcore
        kernel /vmlinuz
        initrd /core.gz
        append loglevel=3
                       

One thing that isn’t clear to me in all of this is how isolinux knows where to find these files. Anyway, I’ll keep following the instructions for now. Next up is creating the actual ISO:

1
2
3
4
5
6
7
8
9
10
aja042@tellurium:~/workspace/bootable-iso$ mkisofs -o output.iso -b isolinux.bin -c boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table cd_root/
I: -input-charset not specified, using utf-8 (detected in locale settings)
Size of boot image is 4 sectors -> No emulation
 91.43% done, estimate finish Fri Mar  3 20:22:49 2017
 Total translation table size: 2048
 Total rockridge attributes bytes: 0
 Total directory bytes: 0
 Path table size(bytes): 10
 Max brk space used 0
 5483 extents written (10 MB)

Alright, that seemed to work. Let’s try booting it!

1
aja042@tellurium:~/workspace/bootable-iso$ qemu-system-x86_64 -curses -boot d -cdrom output.iso -m 512

This boots it successfully!

What do the -b and -c flags do in mkisofs?

The man page for mkisofs is actually called genisoimage, for some reason. Doing mkisofs --help reveals this.

-b eltorito_boot_image 

  Specifies the path and filename of the boot image to be used when
  making an El Torito bootable CD for x86 PCs. The pathname must be
  relative to the source path specified to genisoimage.  This option is
  required to make an El Torito bootable CD.  The boot image must be
  exactly 1200 kB, 1440 kB or 2880 kB, and genisoimage will use this
  size when creating the output ISO9660 filesystem.  The PC BIOS will
  use the image to emulate a floppy disk, so the first 512-byte sector
  should contain PC boot code.  This will work, for example, if the boot
  image is a LILO-based boot floppy.

  If the boot image is not an image of a floppy, you need to add either
  -hard-disk-boot or -no-emul-boot.  If the system should not boot off
  the emulated disk, use -no-boot.

So this is useful! It says that the argument passed to -b is going to be used as the boot image. So what the heck is El Torito? It is, apparently, a standard for how PCs should search for boot code on a CD-ROM. If you’re really curious, you can have a look at the specification from Intel and Phoenix. Section 5.3 describes “No Emulation Booting”, which goes into some awesome x86 history describing interrupts that you can use to retrieve more boot information.

One thing that the standard mentions is the “boot catalog”. The man page confirms that -c is used to specify the filename to be used for the boot catalog. What is the boot catalog? According to section 2.0, it’s a “collection of [0x]20-byte entries, packed [0x]40 entries to the sector”. Section 2.5 explains that these contain a Header ID, a platform ID (x86/PowerPC/Mac), an ID string, a checksum, and two key bytes. These also have a bit more information about what kind of boot media it is, how many sectors should be loaded, etc. I’m personally quite happy that mkisofs makes this for me and I don’t really have to think much about what’s going on here.

So how does isolinux know where to find isolinux.cfg?

The file txt/syslinux.cfg.txt in the syslinux source directory explains the answer to this:

*ISOLINUX* (before 4.02) used the configuration filename of
isolinux.cfg, searching  /boot/isolinux (starting 2.00), then /isolinux
and /.  As of 4.02, *ISOLINUX* will search for isolinux.cfg then
syslinux.cfg in /boot/isolinux before searching for the same files in
/isolinux, /boot/syslinux, /syslinux, and /.

So, basically, I got lucky that it searches for it in the root, since I didn’t read those docs ahead of time.

Conclusion

Looks like the process of making a bootable Linux ISO is pretty straight-forward. You take the isolinux.bin and make an isolinux.cfg, put them in the right place, and tell mkisofs where to find them. Awesome! Now I’m going to go back to figuring out why the ISO I’m making using buildroot isn’t booting right.

Buildroot-ing an OS for a PC Engines APU2

What is an APU2? It’s a pretty awesome 64-bit single-board computer from PC Engines. I have one here that I want to turn into a router, and figured it’d be a good process to turn into a tutorial.

The Fast Way

Want to see the completed project and build it yourself? Check out the github repo and use the bootable-iso branch. You should be able to run make in that branch and get a completed ISO out at output/images/rootfs.iso9660. This is a hybrid ISO, so you can write it to either a CD (not all that useful) or a USB key (very useful!).

The Step-by-Step Way (Starting From Scratch)

I went through a lot of experimenting to get this process nailed down. The bootable-iso repository linked to above has a series of commits that are forked off of the 2017.02 tag that match up with these steps.

Get a copy of the buildroot-2017.02 source.

You can either grab this as a tarball or from Github. If you’re going to use git, make sure you’re starting from the 2017.02 tag, if you want to be sure that the following steps work. Using something newer or older? Let me know if it works!

Start with qemu_x86_64_defconfig.

This is a pre-made configuration that targets 64-bit x86 emulation in qemu. If you want to see what’s in the file, it’s stored in configs/qemu_x86_64_defconfig. We want to tell buildroot to use this configuration:

1
aja042@tellurium:~/workspace/buildroot/buildroot$ make qemu_x86_64_defconfig

Do a build

To do a build with buildroot, you just run make. This takes will take the configured settings, use them to download all of the necessary sources, build them, and finally generate a kernel and root filesystem for you.

1
aja042@tellurium:~/workspace/buildroot/buildroot$ make

This is going to take a while. Maybe go make a coffee…

The build filesystem images go into output/images. Once the build is done, we can check out what we’ve got:

1
2
aja042@tellurium:~/workspace/buildroot/buildroot$ ls output/images/
bzImage  rootfs.ext2

In the output, we’ve got a kernel and a root filesystem, but no ISO.

Configure buildroot to build an ISO

To change the types of filesystem images we want to build, we use the buildroot menu-based configuration.

1
aja042@tellurium:~/workspace/buildroot/buildroot$ make menuconfig

This pops up the top-level menu:

From here, we go into Bootloaders and enable ISOLINUX (this is one of the options for a bootable-ISO-compatible bootloader, and probably the simplest):

Next, we go back to the top-level menu and choose Filesystem images and enable “ISO image”. While you’re here, make sure you’ve checked off Build hybrid image; a “hybrid image” is an ISO that can also be written to a USB key, which will be useful when we go to run code on the APU2.

Exit the menu system and re-run make to re-build with the new options. This should be way quicker than the first build, because it will re-use as much of the previous work as it can.

1
2
3
4
aja042@tellurium:~/workspace/buildroot/buildroot$ make
...
aja042@tellurium:~/workspace/buildroot/buildroot$ ls output/images/
bzImage  rootfs.cpio  rootfs.ext2  rootfs.iso9660  syslinux

Hooray! We’ve got an ISO!

Run the built ISO in QEMU

QEMU is a pretty cool virtualization tool. It simulates an entire PC, it’s free, and it’s easy to use from the command-line. You could probably use VirtualBox, or VMware, or whatever else to do this step too. One of the great perks of QEMU is that it can emulate a text console to the target machine over SSH; handy, because I’m doing all of this work on a headless Linux box.

1
2
aja042@tellurium:~/workspace/buildroot/buildroot$ qemu-system-x86_64 -curses -boot d -cdrom output
/images/rootfs.iso9660 -m 512                                                                     

Unfortunately, the default kernel settings provided with the qemu_x86_64_defconfig use a QEMU-emulated video card, and no matter how many different kernel command-line options I tried, I couldn’t figure out how to turn this off.

This is all it shows:

If you’re running the curses version of QEMU and need to quit, you can press Alt-2 or ESC 2 to switch to the QEMU monitor console and type quit. If you go to the monitor console and want to go back to the console, press Alt-1.

Disable the QEMU video drivers in the Linux kernel

Buildroot provides the menuconfig for configuring what buildroot is going build, and it also provides linux-menuconfig for configuring which options are going to be built into the Linux kernel. We’ll use that to find the video drivers and disable them.

1
aja042@tellurium:~/workspace/buildroot/buildroot$ make linux-menuconfig

In the Device Drivers, Graphics Drivers menu, you want to make sure that QXL virtual GPU, DRM support for bochs..., and Virtio GPU driver are all disabled.

Now, we’ll rebuild and try qemu again:

1
2
3
aja042@tellurium:~/workspace/buildroot/buildroot$ make
...
aja042@tellurium:~/workspace/buildroot/buildroot$ qemu-system-x86_64 -curses -boot d -cdrom output/images/rootfs.iso9660 -m 512                                                                    

You can log in as root with no password now and poke around in our super minimal Linux distribution:

When you’re ready to quit, press Alt-2 and type quit.

Saving our configuration

So far, we’ve modified both the buildroot configuration and the Linux kernel configuration. Let’s export those configurations so that we can get them back easily. First, we’ll change the name of the kernel configuration in the main buildroot menuconfig.

1
2
3
aja042@tellurium:~/workspace/buildroot/buildroot$ make menuconfig

Kernel -> Configuration file path -> board/qemu/x86_64/apu2-linux-4.9.config

Exit out of the menuconfig, and tell buildroot to save the current kernel config:

1
aja042@tellurium:~/workspace/buildroot/buildroot$ make linux-update-defconfig

This should create our apu2-linux-4.9.config for us:

1
2
aja042@tellurium:~/workspace/buildroot/buildroot$ ls board/qemu/x86_64/
apu2-linux-4.9.config  linux-4.9.config  readme.txt

Now that the kernel config is there, we can save the main buildroot configuration. Since this has already been configured early on, we want to override the BR2_DEFCONFIG variable with make to save to a new one:

1
2
3
4
aja042@tellurium:~/workspace/buildroot/buildroot$ grep DEFCONFIG .config
BR2_DEFCONFIG="/home/aja042/workspace/buildroot/buildroot/configs/qemu_x86_64_defconfig"

aja042@tellurium:~/workspace/buildroot/buildroot$ make savedefconfig BR2_DEFCONFIG=configs/apu2_x86_64_defconfig

Now, we can do make apu2_x86_64_defconfig to configure our system using our own configuration instead of the qemu_x86_64_defconfig we started with.

Getting it to work on serial

Right now, we’ve got the system booting successfully and displaying everything on a text console. This is a good start, but the APU2 doesn’t have a display at all; it all just runs over the serial port.

In the command-line/curses version of QEMU, you can look at the serial port by pressing Alt-3. If you do this on our existing image, you’ll see that there isn’t anything interesting there yet.

There’s three things we need to configure for serial:

  • The ISOLINUX bootloader
  • The kernel console
  • The kernel login prompt

The bootloader configuration lives in fs/iso9660/isolinux.cfg. Looking at the docs tells us that we need to add a serial directive as the first line of the configuration file. At the same time, we’ll add a console= directive to the append line (which configures the kernel command-line).

After editing, it should look like this:

1
2
3
4
5
6
7
aja042@tellurium:~/workspace/buildroot/buildroot$ cat fs/iso9660/isolinux.cfg
serial 0 115200
default 1
label 1
      kernel __KERNEL_PATH__
      initrd __INITRD_PATH__
      append root=/dev/sr0 console=ttyS0,115200n8

Next, we’ll tell Buildroot to use the serial port for a login prompt. To change this setting, you run make menuconfig, and navigate to System configuration, Run a getty (login prompt) after boot, and change the TTY Port to ttyS0.

Now, rebuild your ISO and run QEMU:

1
2
3
aja042@tellurium:~/workspace/buildroot/buildroot$ make
...
aja042@tellurium:~/workspace/buildroot/buildroot$ qemu-system-x86_64 -curses -boot d -cdrom output/images/rootfs.iso9660 -m 512

When you re-run QEMU, you will no longer get all of the kernel output on the main console; it should just show a bit of boot stuff:

If you press Alt-3, you will switch to the serial console and should see all of the kernel stuff you were expecting to see, along with a login prompt.

As usual, to quit QEMU, press Alt-2 and type quit.

The last step here is to save our modified buildroot configuration, so that we don’t lose our changes to the default TTY. This time we don’t have to specify a BR2_DEFCONFIG, because we’ve already told it to use our apu2_x86_64_defconfig.

1
aja042@tellurium:~/workspace/buildroot/buildroot$ make savedefconfig

Copying the ISO onto a USB Key

Following the instructions in the Syslinux documentation, we should be able to use dd to copy the ISO image onto a USB key. In the command below, replace xxxxx with the path to your USB key. MAKE SURE YOU DON’T SCREW THIS UP!. If you put the wrong device here, you could overwrite the start of your hard drive with this ISO instead, and that’s likely not what you want…

I like to use dmesg to check which drive I just plugged in to the machine:

1
2
3
4
5
6
7
8
9
10
11
12
aja042@tellurium:~/workspace/buildroot/buildroot$ dmesg
[2067820.465956] usb-storage 3-10:1.0: USB Mass Storage device detected
[2067820.466252] scsi host19: usb-storage 3-10:1.0
[2067821.628122] scsi 19:0:0:0: Direct-Access     PNY      USB 2.0 FD       1100 PQ: 0 ANSI: 4
[2067821.628670] sd 19:0:0:0: Attached scsi generic sg2 type 0
[2067821.629397] sd 19:0:0:0: [sdb] 31195648 512-byte logical blocks: (16.0 GB/14.9 GiB)
[2067821.629965] sd 19:0:0:0: [sdb] Write Protect is off
[2067821.629967] sd 19:0:0:0: [sdb] Mode Sense: 43 00 00 00
[2067821.630513] sd 19:0:0:0: [sdb] No Caching mode page found
[2067821.630515] sd 19:0:0:0: [sdb] Assuming drive cache: write through
[2067821.633139]  sdb: sdb1
[2067821.634613] sd 19:0:0:0: [sdb] Attached SCSI removable disk

That looks like the one. So I’m going to replace xxxxx with sdb, but you need to make sure you’ve got that right.

1
2
3
4
5
aja042@tellurium:~/workspace/buildroot/buildroot$ sudo dd if=output/images/rootfs.iso9660 of=/dev/xxxxx
[sudo] password for aja042:
12288+0 records in
12288+0 records out
6291456 bytes (6.3 MB, 6.0 MiB) copied, 1.07977 s, 5.8 MB/s

Giving the APU2 boot a shot!

My USB-serial adapter showed up as /dev/ttyUSB0 when I plugged it in. Yours might show up as /dev/ttyUSB[x] or /dev/ttyACM[x] or something else entirely. Replace as needed in the minicom command.

Minicom is my preferred serial terminal emulator. Before powering on the APU2, get the serial cable all hooked up and get minicom started:

1
aja042@tellurium:~/workspace/buildroot/buildroot$ sudo minicom -o -D /dev/ttyUSB0

Minicom should start up and have a blank window:

Plug the power in on the APU2 and watch stuff start to scroll by. When you get to the point where it’s asking which device to boot off of, choose the number that corresponds with your USB key:

And TADA! It’s booted!

Unfortunately, it doesn’t know anything about the Ethernet controllers on the APU2:

To quit minicom, press Ctrl-a z q and say yes to Leave without reset?

Rebuild the kernel with network driver support

We’re back to doing kernel configuration: make linux-menuconfig. In here, we want to go to Device Drivers, Network device support, Ethernet driver support, scroll down to Intel devices. The APU2 spec page calls these NICs “i210AT / i211AT”, but a bit of google searching reveals that these are covered by the Intel(R) 82575/82576 PCI-Express Gigabit Ethernet support driver. Enable that.

Now, rebuild your ISO and reflash your USB key. Once again, make sure you’re writing to the correct USB device!

1
2
3
4
5
6
aja042@tellurium:~/workspace/buildroot/buildroot$ make
...
aja042@tellurium:~/workspace/buildroot/buildroot$ sudo dd if=output/images/rootfs.iso9660 of=/dev/xxxxx
12288+0 records in
12288+0 records out
6291456 bytes (6.3 MB, 6.0 MiB) copied, 4.0793 s, 1.5 MB/s

And try booting it again with minicom watching. Once it’s booted, verify that the NICs are detected:

Hooray! The last thing to do is to save the changed kernel config - we want to be able to reproduce this setup later!

1
aja042@tellurium:~/workspace/buildroot/buildroot$ make linux-update-defconfig

With that, I feel like this is a lot of content. There’ll be a part 2 coming soon where this gets configured as a router.

Autoconf/Automake Quick Reference

Autoconf has always been a bit of a mystery to me. While working through the MailCore 2/libetpan stuff, I once again got reacquainted with Autotools and had to dive a bit deeper. John Calcote’s Autotools is an awesome reference that I found on O'Reilly Safari today.

This is mostly a set of rapid fire notes that I will probably refer back to in the future for my own reference.

Tools

  • Automake - takes Makefile.am files and turns them into Makefile.in files.

  • Autoconf - takes configure.ac, aclocal.m4, acsite.m4 and uses them to generate the configure script. Configure script takes .in files and outputs them with the .in stripped (e.g. Makefile.in -> Makefile)

So that’s the general flow of things here. Automake goes from .am to .in, Autoconf takes .ac and .m4 to generate the configure script (and config.h.in). Finally, ./configure takes the .in files and generates the output with the .in removed (Makefile.in -> Makefile, config.h.in -> config.h).

RANT: Why Don't You Use Vendor-Provided Tools and Examples?

I’ve had an aversion to vendor tools for a long time. I’m going to pick on Freescale here, but that’s just because I’m currently waiting for their sample code to finish downloading. All 660MB of sample code and… other stuff? My previous post was pointed squarely at ST. I love the hardware these companies put out, I just can’t handle their software or the code that they write. I’ll take emacs over Eclipse any day of the week, and most vendors these days just rebrand Eclipse. Good for lots of people, just not good for me.

Anyway, on to an example. Here’s some Freescale example code for the FRDM-KL25Z. It says this should be the content of your main() function. I’ve fixed the formatting for them a little bit.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
volatile int delay;

// Configure board specific pin muxing
hardware_init();

// Initialize the UART terminal
dbg_uart_init();

PRINTF("\r\nRunning the myProject project.\n");

// Enable GPIO port for LED1
LED1_EN;

for (;;)
{
  LED1_ON;

  delay = 5000000;
  while(delay--);

  LED1_OFF;

  delay = 5000000;
  while(delay--);
}

Ok, so this starts out well. They have init functions for the board and the debug UART. Nitpick: you can use vowels, debug is just find, you don’t need to shorten that to dbg.

Next, we see PRINTF. The fact that the function name is in all caps suggests to me that it’s some weird version of printf, perhaps implemented as some kind of macro so that it outputs on the debug UART. Either way, it’s a bit scary. Also, they can’t seem to decide on whether to use Windows- or Unix-style newlines; the first one is Windows-style, the second is Unix-style.

The next line is where I start getting grumpier. LED1_EN; is not a function call. It looks to me like a side-effect free statement. (just like x; is valid, but pointless C code). But, judging by the name, this affects a GPIO register!

The body of the for-loop continues on the same way. The LED1_ON and LED1_OFF macros are gross, and the delay loops are pretty ridiculous.

I’ve ranted before about this, and I’ll do it again here: Sample code is documentation, and possibly the only documentation your users are ever going to read. By giving this as the very first example of what the code can look like, you’re teaching new developers to write garbage.

And that’s why I set up real toolchains that just use standard C libraries and some very light (and idiomatic!) macros to do embedded development.

A Nice STM32F042 Toolchain

Note: This is a snapshot of the README.md file on the git repository for this project. Over the next few weeks, I plan on writing a series of blog posts dissecting the different parts of it and explaining how it all fits together.

You can find the repository here: https://github.com/tonyarkles/stm32f042-morse-example

If you’re looking at using an STM32F042K6 Nucleo board, this is a decent alternative to using mbed, if you feel like rolling up your sleeves a bit and getting dirty.


What is this?

This is a sample project for the STM32F042K6 Nucleo board that I’m happy with. It has the following features:

  • CMake for builds
  • crosstool-ng for building a toolchain
  • Unity for unit testing on the host
  • OpenOCD and gdb for running code on the target

How do I get started?

Toolchain

First, you need to have crosstool-ng installed. I’m using version 1.22.0. The instructions are pretty straight-forward. Once you’ve got it installed, don’t worry about the rest of their instructions (i.e. stop after the export PATH line).

Once it’s installed, you can use the configuration file in the crosstool/ directory in this repository to build a compiler and newlib (libc) for the microcontroller. To do this, you first tell crosstool-ng to use the configuration provided, and then you tell it to build it:

1
2
stm32f042-morse-example/crosstool$ DEFCONFIG=stm32f0-defconfig ct-ng defconfig
stm32f042-morse-example/crosstool$ ct-ng build

This will take a while. On my i7 desktop, it took about 16 minutes.

This will install the toolchain into ~/x-tools/arm-stm32f0-eabi.

OpenOCD

The crosstool-ng toolchain comes with an ARM-compatible gdb, but without OpenOCD, there’s no way to connect to the target board.

I was lazy here and just installed it through apt: apt install -y openocd

To test that it’s working, plug your Nucleo board into a USB port and run this command. You should get similar output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ sudo openocd -f /usr/share/openocd/scripts/board/st_nucleo_f0.cfg
Open On-Chip Debugger 0.9.0 (2015-09-02-10:42)
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
adapter speed: 1000 kHz
adapter_nsrst_delay: 100
none separate
srst_only separate srst_nogate srst_open_drain connect_deassert_srst
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : clock speed 950 kHz
Info : STLINK v2 JTAG v25 API v2 SWIM v14 VID 0x0483 PID 0x374B
Info : using stlink api v2
Info : Target voltage: 3.243541
Info : stm32f0x.cpu: hardware has 4 breakpoints, 2 watchpoints

CMake

CMake is a build system. The two-second intro is that instead of having a Makefile, you have files called CMakeLists.txt that are used to generate Makefiles (and other things, in different environments).

Here, too, I was lazy and just installed it via apt: sudo apt install -y cmake

Building and testing on the host

You should now have all the pieces in place necessary to do both host-unit-test builds and target-binary builds!

First, let’s do a host build. For CMake projects, you generally make a new directory to do the build in, which keeps all of the build objects out of your source tree (makes for really easy cleanup!)

To do a host build, make a directory called build/ in the source directory. The .gitignore file is already set up to ignore this directory. In there, you run CMake to process the CMakeLists.txt files and generate a Makefile. Then you run make.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
stm32f042-morse-example$ mkdir build
stm32f042-morse-example$ cd build
stm32f042-morse-example/build$ cmake ..
-- The C compiler identification is GNU 5.4.0
-- The ASM compiler identification is GNU
-- Found assembler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/aja042/workspace/crosstool/stm32f042-morse-example/build
stm32f042-morse-example/build$ make
... lots of output ...

Once this has finished, there’s a unit test runner stored in build/test/testrunner. Executing this should result in output like this:

1
2
3
4
5
6
7
stm32f042-morse-example/build$ test/testrunner
Unity test run 1 of 1
..............

-----------------------
14 Tests 0 Failures 0 Ignored
OK

Building and flashing onto the microcontroller

Now that you can see that the unit tests are passing, it’s time to try running the code on the microcontroller!

We’re going to make a new build directory for this; this one will contain all of the ARM-compiled pieces instead of the x86-compiled pieces from above. The process here is pretty similar, but we’re going to pass an additional parameter to CMake to tell it what to use for a cross-compilation toolchain. We’ll also tell CMake to do a Debug build, so that we can easily see what’s going on in GDB.

1
2
3
4
stm32f042-morse-example$ mkdir build-target
stm32f042-morse-example$ cd build-target
stm32f042-morse-example/build-target$ cmake -DCMAKE_TOOLCHAIN_FILE=../toolchain-stm32f042.cmake -DCMAKE_BUILD_TYPE=Debug ..
stm32f042-morse-example/build-target$ make

The output from this is going to be target/target.elf. This file contains all of the code that’s going to go onto the microcontroller, plus additional things like debugging symbols.

To do these steps, make sure that you’ve got OpenOCD running (see above). We’re going to use gdb to connect to OpenOCD and upload the firmware. I’ll talk about what is happening with each step as we go along.

Start the ARM version of gdb:

1
stm32f042-morse-example/build-target$ ~/x-tools/arm-stm32f0-eabi/bin/arm-stm32f0-eabi-gdb

Tell GDB to connect to OpenOCD. OpenOCD by defaults listens on port 4444 for general commands, and port 3333 for GDB commands:

1
2
3
(gdb) target remote localhost:3333
Remote debugging using localhost:3333
0x00000000 in ?? ()

Tell GDB which file has the firmware:

1
2
3
4
(gdb) file target/target.elf
A program is being debugged already.
Are you sure you want to change the file? (y or n) y
Reading symbols from target/target.elf...done.

And tell GDB to tell OpenOCD to flash that firmware onto the chip:

1
2
3
4
5
6
7
(gdb) load
Loading section .isr_vector, size 0xc4 lma 0x8000000
Loading section .ARM.exidx, size 0x8 lma 0x80000c4
Loading section .text, size 0xbcc lma 0x80000cc
Loading section .data, size 0x4c8 lma 0x8000c98
Start address 0x8000364, load size 4448
Transfer rate: 13 KB/sec, 1112 bytes/write.

Now tell GDB to tell OpenOCD to reset the chip and hold it in the halt state:

1
2
3
4
(gdb) monitor reset halt
target state: halted
target halted due to debug-request, current mode: Thread
xPSR: 0xc1000000 pc: 0x08000364 msp: 0x20001800

At this point, the firmware’s loaded on the chip. To get it to run, all we need to do is issue the continue command:

1
(gdb) continue

And if you look over at the board, the LED should now be blinking out “HELLO WORLD” in Morse Code.