Once a toolchain is setup to compile code for ARM processors you're ready to program the Dash's CPU with new code. In this section I'll describe how to download, compile, and flash example code to control some functions of the Dash like its LED and a serial UART.
If your hardware experience is mostly with platforms like Arduino this is a great opportunity to learn how those platforms work at a lower level. With Arduino you don't need to worry about setting up a toolchain, writing code to directly access the CPU, etc. as that's all taken care of for you by Arduino's libraries and IDE. However the trade-off is that you can only write code that works on chips that Arduino supports, and you can only use features that Arduino has exposed for you. By writing your own code to control a chip 'bare-metal' you have much more flexibility, but you have to do more work yourself.
For the Dash's STM32F205RG6 CPU the definitive source for how everything on the chip works is the STM32F205xx reference manual. This document describes all of the functions of the CPU, but be warned it's almost 1500 pages long! Instead of trying to read the entire reference manual start by skimming the STM32F205xx datasheet which is a high-level summary of the chip's features. Then lookup specific parts of the chip in the reference manual when you want to understand how they work.
Although the reference manual is all you technically need to start programming the CPU, you'll be much more productive using a library that implements some of the tedious and boilerplate code to access the hardware. One option is STMicro's STM32CubeF2 HAL (hardware abstraction layer), and another is the libopencm3 library. For these examples I've chosen to use the libopencm3 library because it's mature, easy to setup, and open source. There's no right or wrong option when choosing a library--experiment with using different hardware libraries to find one that supports the features you need and makes you the most productive.
Examples
To download the example code first make sure you are running the toolchain VM and have connected to it with SSH. It will be easiest to download the code to the /vagrant folder in the VM so that it can be edited from your host operating system with any text editor (I recommend Atom as a good cross-platform programmer's text editor).
Run the following commands to clone the example code to the /vagrant folder:
cd /vagrant git clone --recursive https://github.com/adafruit/dash-examples cd dash-examples
Make sure to specify the --recursive option so that the libopencm3 library (which is a git submodule) is downloaded too.
After the code is downloaded you'll first need to build the libopencm3 library which the examples use. Run the following command from inside the dash-examples root folder:
make
You should see text pass by as libopencm3 and the examples are compiled. Once the compilation finishes you can navigate to each example's subdirectory and run its Makefile to build and program the example code.
Make sure to compile libopencm3 as described above before running and compiling the examples! If you don't compile libopencm3 then the examples will fail to compile with an error like 'no target for xxxx.elf'!
The examples are available inside the examples subdirectory of the root and include:
- examples/blink - A basic example to rotate through blinking the red, green, and blue LED for a second each.
- examples/pwm - A more advanced example that uses timers on the CPU to generate a PWM signal that can light the LED to any RGB color. The example will cycle through all possible color hues.
- examples/uart - Example of sending data out USART6 TX (pin PC7) using printf calls. This is useful for printing debug messages and other data from the Dash. Note that receiving data on the RX pin isn't yet in the example.
To run an example first make sure the Dash and STLink V2 programmer are connected to the computer running the VM. Then navigate inside the example's folder and run the following command (to run the blink example):
cd examples/blink make stlink-flash
The stlink-flash target of the makefile will use the st-flash utility to program the Dash. You should see an output that looks something like:
FLASH blink.bin 2015-08-09T03:42:38 INFO src/stlink-common.c: Loading device parameters.... 2015-08-09T03:42:38 INFO src/stlink-common.c: Device connected is: F2 device, id 0x20036411 2015-08-09T03:42:38 INFO src/stlink-common.c: SRAM size: 0x20000 bytes (128 KiB), Flash: 0x100000 bytes (1024 KiB) in pages of 131072 bytes 2015-08-09T03:42:38 INFO src/stlink-common.c: Attempting to write 916 (0x394) bytes to stm32 address: 134217728 (0x8000000) EraseFlash - Sector:0x0 Size:0x4000 Flash page at addr: 0x08000000 erased 2015-08-09T03:42:39 INFO src/stlink-common.c: Finished erasing 1 pages of 16384 (0x4000) bytes 2015-08-09T03:42:39 INFO src/stlink-common.c: Starting Flash write for F2/F4 2015-08-09T03:42:39 INFO src/stlink-common.c: Successfully loaded flash loader in sram size: 916 2015-08-09T03:42:39 INFO src/stlink-common.c: Starting verification of write complete 2015-08-09T03:42:39 INFO src/stlink-common.c: Flash written and verified! jolly good!
Immediately after finishing you should see the Dash's LED start blinking through red, green, blue colors. Woo hoo, you've reprogrammed the Dash!
If you receive an error make sure you've compiled libopencm3 in the example root directory, and that the STLink V2 is plugged in to the computer. Also check the STLink V2 is visible to the VM using the lsusb command. If the STLink V2 isn't visible to the VM make sure VirtualBox's extension pack is installed and try running VirtualBox's GUI program to view the ARM-toolchain-vagrant VM's settings (look in the USB settings to see if the STLink V2 can manually be added to the VM).
For the uart example you'll want to connect the Dash to a serial to USB cable so you can see the UART output on your computer. You'll need a wire soldered to the Dash's PC6 USART6 TX pad and then connected to the serial to USB cable's RX pin. In addition connect the serial to USB cable's ground pin to the Dash's ground. Finally open the serial port with 115200 baud, 8 data bits, no parity, and 1 stop bit in a tool like PuTTY (Windows) or screen (Linux & Mac OSX). Every second you should see a count printed to the serial port.
Blink Example
To help understand how the Dash CPU is programmed I'll walk through the blink example in a little more detail.
First to understand how the project is built look at the Makefile in the root of the examples/blink folder:
# Dash Blink Example Makefile # Copyright (c) 2015 Tony DiCola # Released under a MIT license: http://opensource.org/licenses/MIT BINARY = blink # Note if you have multiple source files, list them all in an OBJS variable. # The Makefile rules will pick them up and compile their source appropriately. # For example if you have a foo.c and bar.c to include set the OBJS variable: # OBJS = foo.o bar.o include ../Makefile.include
This Makefile is trivially simple because all of the rules and logic are in a separate Makefile.include and Makefile.rules file. Those files are based on the Makefiles from the libopencm3-examples project and provide all the typical rules for making and programming an ARM project, like compiling code to a .bin or .hex file and uploading with a programmer.
For an example project it only needs to specify the name of the output binary with the BINARY = name value. The Makefile will then try to build a binary.o file from a binary.c or binary.cpp file in the directory.
Notice the commented section of the Makefile which tells how to reference other sourcefiles too. Just set the OBJS variable to a list of object files that need to be built and the Makefile will take care of the rest.
You can set other variables in the Makefile to customize how an example is built too. I recommend skimming the Makefile.include and Makefile.rules file and examining other examples from the libopencm3-examples project to learn more.
Next open the blink.c file to see the code that powers the blink example. After including a few libopencm3 headers the code defines a few functions that deal with the system timer, or systick:
// Global state: volatile uint32_t systick_millis = 0; // Millisecond counter. // Delay for the specified number of milliseconds. // This is implemented by configuring the systick timer to increment a count // every millisecond and then busy waiting in a loop. static void delay(uint32_t milliseconds) { uint32_t target = systick_millis + milliseconds; while (target > systick_millis); } // Setup the systick timer to increment a count every millisecond. This is // useful for implementing a delay function based on wall clock time. static void systick_setup(void) { // By default the Dash CPU will use an internal 16mhz oscillator for the CPU // clock speed. To make the systick timer reset every millisecond (or 1000 // times a second) set its reload value to: // CPU_CLOCK_HZ / 1000 systick_set_reload(16000); // Set the systick clock source to the main CPU clock and enable it and its // reload interrupt. systick_set_clocksource(STK_CSR_CLKSOURCE_AHB); systick_counter_enable(); systick_interrupt_enable(); } // Systick timer reload interrupt handler. Called every time the systick timer // reaches its reload value. void sys_tick_handler(void) { // Increment the global millisecond count. systick_millis++; }
The system timer is a common feature of ARM processors and provides a timer to keep track of program execution time. This is very useful to understand how long something takes to execute, and in this example the systick timer is used to implement a millisecond delay function.
The millisecond delay works by configuring the systick timer to increment a count every millisecond and waiting for that count to reach a specific value. To configure systick to update each millisecond its reload value is set to 16,000. This value was computed based on the fact that the main CPU clock & systick timer are running at 16mhz (since no external clock source was configured the CPU defaults to its internal oscillator) and solving:
16,000,000 mhz / 1,000 time a second target = 16,000 systick reload value
The systick timer will count from 0 to 16,000 in one millisecond of wall-clock time, fire an interrupt which increases the millisecond count, and wrap back to zero to start the process again.
The next part of the code deals with how to configure the GPIO outputs that drive the Dash's LEDs:
// Setup and configure the GPIOs to control the LEDs on the Dash. static void gpio_setup(void) { // Enable the GPIO clocks for the two GPIO ports that will be used (A & B). rcc_periph_clock_enable(RCC_GPIOA); rcc_periph_clock_enable(RCC_GPIOB); // Set each LED GPIO as an output. gpio_mode_setup(GPIOA, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO8); // PA8, blue LED gpio_mode_setup(GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO6); // PB6, red LED gpio_mode_setup(GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO7); // PB7, green LED }
The gpio_setup function does a couple important thing:
- The GPIO clock is enabled for both GPIO ports that will be used (A and B). In general most peripherals on the CPU like GPIOs, UARTs, timers, etc. need to have their clock enabled before they can be used.
- The GPIOA and GPIOB ports are configured to drive PA8, PB6, and PB7 as outputs. This is similar to the pinMode function you might use in an Arduino sketch to setup a pin as an input or output.
Finally the main entry point and loop of the program looks like:
int main(void) { // Setup systick timer and GPIOs. systick_setup(); gpio_setup(); // Main loop. while (true) { // Light the red LED. Note that LEDs light up when the GPIO is pulled low // with the gpio_clear function, and turn off when pulled high with the // gpio_set function. gpio_clear(GPIOB, GPIO6); // Red LED on gpio_set(GPIOB, GPIO7); // Green LED off gpio_set(GPIOA, GPIO8); // Blue LED off delay(1000); // Wait 1 second (1000 milliseconds). // Now light just the green LED. gpio_set(GPIOB, GPIO6); // Red LED off gpio_clear(GPIOB, GPIO7); // Green LED on gpio_set(GPIOA, GPIO8); // Blue LED off delay(1000); // Finally light just the blue LED. gpio_set(GPIOB, GPIO6); // Red LED off gpio_set(GPIOB, GPIO7); // Green LED off gpio_clear(GPIOA, GPIO8); // Blue LED on delay(1000); } return 0; }
The main function is where execution of the example starts. Unlike an Arduino sketch there is no explicit setup or loop function, instead the main function performs both of those tasks itself. At the start of the main function it calls the previous systick and GPIO setup functions to initialize those parts of the example. Then the main function jumps into an infinite loop that will blink the LEDs forever.
Inside the main loop each LED is enabled and disabled using the gpio_clear and gpio_set functions to drive the GPIO pins low and high respectively. You can learn more about the GPIO functions from the libopencm3 GPIO function reference.
Between lighting each LED the delay function built from the systick timer is used to pause for a second, and the whole process repeats forever. That's all there is to the blink example code!
Reviving A Bricked Dash
As you explore the Dash and reprogram it to run your own code you might find yourself accidentally 'bricking' the device and being unable to reprogram it. This can happen if you accidentally disable the single-wire debug test headers, misconfigure the clock or other peripherals, etc. Luckily there's a 'safe mode' you can put the chip in to help get it into a good state to reprogram again.
If you try to program the Dash's CPU and receive errors that the STM32 processor cannot be detected (and you know for sure it's not a physical connection with the programmer, or software issue passing the programmer through to the VM) you can follow the steps mentioned in the workaround section of this page to get the Dash back into a good state.
First connect the RESET pin of the Dash to ground. This will force the chip to stop operating any faulty program and reset itself to receive new instructions. Make sure to keep the RESET pin connected to ground as you run the next steps. Then inside the VM run OpenOCD's telnet server (which is already installed) with the following command:
openocd -f interface/stlink-v2.cfg -f target/stm32f2x.cfg
This command will not return, instead OpenOCD will sit waiting for a connection.
Now open a new terminal and connect to the VM again (using the vagrant ssh command) and run the following to connect to OpenOCD's telnet server:
telnet localhost 4444
Once connected to OpenOCD's telnet server run the following command:
reset halt
This command should fail with a timed out error, like:
timed out while waiting for target halted TARGET: stm32f2x.cpu - Not halted in procedure 'reset' in procedure 'ocd_bouncer'
Now remove the connection between RESET and ground. You should see OpenOCD report that the CPU is halted:
target state: halted target halted due to debug-request, current mode: Thread xPSR: 0x01000000 pc: 0x080002cc msp: 0x20020000
Now run the following command to erase the flash memory and any faulty program:
stm32f2x mass_erase 0
After a few seconds the erase should complete:
device id = 0x20036411 flash size = 1024kbytes stm32x mass erase complete
Finally stop OpenOCD and close the telnet session by running the shutdown command:
shutdown
You should now be able to flash an example program back to the chip (like the blink example).
Be careful not to flash the same program that caused the problem or else you might need to un-brick again!
Going Further
Now that you know how to get code running on the Dash's CPU there's almost no limit to what you can do. Here are some good resources to help go further:
- libopencm3 Library - Check out the documentation for the libopencm3 library, and in particular look at the examples it has for the STM32 F2 and other chips. You don't have to limit yourself to just using libopencm3 too, be sure to check out STMicro's STM32F2xx HAL or other similar hardware libraries.
- STM32F2xx datasheet and technical reference manual - These are great to skim and reference to understand the features of the Dash's STM32F205RG6 CPU.
- ARM Cortex-M Series Documentation - The Dash's CPU is an ARM Cortex-M3 processor and this reference has all the details on ARM's instruction set, features, etc. Like the CPU's technical reference manual this is a very large document that's good to skim and reference later when needed. An easier to digest introduction to ARM Cortex-M3 processors is The Definitive Guide to the ARM Cortex-M3 and M4 Processors by Joseph Yiu.
- Broadcom WICED SDK - Although this guide didn't touch on the Dash's WiFi module yet, it's good to know the WICED SDK from Broadcom is what the Dash was designed to use for control of the CPU and WiFi module. The WICED platform provides a basic hardware abstraction layer (like libopencm3 provided in this guide) and WiFi, TCP/IP protocol, SSL/encryption support, and much more.
- Exploring Amazon Dash Button and Amazon Dash Button Teardown - These are a couple great resources with more information about the Dash hardware. For example the Dash has a ADMP441 microphone connected to the CPU which might allow for recording sound in your own code.
- Migrating Away from the Arduino IDE at Contextual Electronics - This is a new series from Contextual Electronics that will be exploring in more detail bare-metal CPU programming, similar to what was shown in this guide with the Dash.
Good luck hacking the Dash and be sure to share interesting things you create or discover with the hardware!
Page last edited August 04, 2015
Text editor powered by tinymce.