STM32F103 from "Scratch" lab notes

These are the notes belonging to this video: https://diode.zone/videos/watch/515f8edb-3766-419f-a107-a7c2902a881d

Setup environment. I use Linux, but in principle this should all work on Windows or Mac also.

  • install ST-link drivers or OpenOCD
  • install arm gdb and build tools
  • install rustup and correct target

Look up the datasheet for the chip, and note that it's a ARM Cortex M3. Rust calls this thumbv7m.

init rust program:

$ cargo init bluepill-blink

Create the most basic "possible" program (not quite there):

#![no_main]  //  Don't use the Rust standard bootstrap. We will provide our own.
#![no_std]  //  Don't use the Rust standard library. We are building a binary that can run on its own.

extern crate cortex_m_rt;  //  Startup and runtime functions for ARM Cortex-M3.

use cortex_m_rt::entry;

#[entry]
fn main() -> ! {
    loop {
    }
}

And in Cargo.toml add a dependency:

cortex-m-rt = "*"

Try to build this:

 $ cargo build --target=thumbv7m-none-eabi

This doesn't build, because you also need a panic handler. The best "Easy" way to do this is to use the panic-halt crate.

use panic_halt;

And in Cargo.toml add

panic-halt = "*"

So now we have something that will build.

The built file is in target/thumbv7m-none-eabi/debug/

To upload it we need to connect a programmer. I'm using a ST-Link clone, that I got for a low price of ebay. We need to connect GND, 3.3V, SWDIO and SWCLK.

The way we will program is using a debugger, specifically gdb. Gdb will not talk directly to the ST-Link, it will need something else to talk to. There is an ST-program that can do the job, st-util. To be honest, that is probably the easiest option, just run st-util. But I like open software, so I will use OpenOCD.

We can give this a try, it needs some scripts to run properly, one for the interface and one for the target $ openocd -f interface/stlink-v2.cfg -f target/stm32f1x.cfg

In my case this doesn't work, because me st-link gives a weird id number. So I copy /usr/share/openocd/scripts/target/stm32f1x.cfg to the openocd folder in my project. I edit it to give the same ID as the error code the last command tells me (So I change 1 to 2). And then I run $ openocd -s openocd -f interface/stlink-v2.cfg stm32f1x.cfg It's no waiting for a debugger on port 3333

and start the debugger session, and connect to the target $ cd target/thumbv7m-none-eabi/debug/ $ arm-none-eabi-gdb target remote :3333

and we're in! try to upload file bluepill-blink load

It's not working.

We can kind of see the reason if we exit the debugger and have a look at the file: quit $ arm-none-eabi-readelf -a bluepill-blink You see that all the memory starts at address 0. But on this chip it should start somewhere else.

We will need to tell the linker where to put the program. To do this we make a cargo config file (this is not the toml file!).

 $ mkdir .cargo
 $ vim .cargo/config

In this file write

[build]
target = "thumbv7m-none-eabi"

[target.thumbv7m-none-eabi]
rustflags = [
  "-C", "link-arg=-Tlink.x",
]

(We also straight away add the default target, so we don't need to specify that every time we build)

Now if we try to build, it tells us we are missing a file. As we are going to be using the stm32f1xx-hal crate later, we might as well copy it from there. Just put it in the main folder for your project.

Now it complains about missing vectors, so we really do need a library for our chip. In cargo.toml:

[dependencies.stm32f1xx-hal]
version = "0.3.0"
features = ["stm32f103", "rt"]

In the source file:

extern crate stm32f1xx_hal;

and we build again, and have a look using the readelf program, we see that a lot of stuff starts at 0x08000000, which we need. We try loading it again

 $ arm-none-eabi-gdb
 target remote :3333
 file bluepill-blink
 load
 continue

Our program runs!

 ctrl-c
 bt

We are somewhere in our program!

Now we want to make the LED blink. First we need to turn the LED on, on our board this is done by setting pin 13 to low. The stm32f1xx-hal library we are using provides the requird functions.

We have to import the right things:

use stm32f1xx_hal::pac::Peripherals;
use stm32f1xx_hal::prelude::*;

The prelude is there because we need to import some traits that will be used, and the easiest is just to get them all.

Then in main we take pin 13, set it to push pull mode, and set it low:

// Setup Device Peripherals
let dp = Peripherals::take().unwrap();
let mut rcc = dp.RCC.constrain();
let mut gpioc = dp.GPIOC.split(&mut rcc.apb2);
let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);

led.set_low();

When we run the program, the LED goes on!

If we want to make it blink, the proper way is to set up the timers, and you can see an example of how to do that in the stm32f1xx-hal documentation. But just to finally get to blink, we will do it the dirty way:

loop {
    for _ in 0..1000 {
        led.set_low();
    }
    for _ in 0..1000 {
        led.set_high();
    }
}

If you want to run this in release mode, you will need to make the loop about 100x longer, so make it loop from 0 to 100000.