6 ways to communicate with STM32F103C8T6. Part 1. Zero to blinking an led

This image came from here: https://developer.mbed.org/users/hudakz/code/STM32F103C8T6_Hello/

This is the first in a series of posts about 6 ways to communicate with the stm32f103c8t6. In each of the posts, I will talk about a way to communicate with the part as well as something interesting that can be done with that mode of communication.
I have a video for this post:

The first way is to blink an LED on the stm32. To do this, you need a couple things:

  • Two cheap pieces of hardware
  • To generate boilerplate code which sets up the runtime environment, initializes the clock and some other low level stuff
  • To add code that blinks the LED pin
  • To compile the resulting code
  • To transfer the binary to the board

This post describes how to do these in the linux environment.

Needed hardware

To reproduce what I do in this post, you’ll need two things:

  1. stm32f103c8t6 board from aliexpress/ebay. <$2
  2. st-link v2 programmer. Also from aliexpress/ebay. <$2
click to enlarge

Boilerplate with STMCubeMX

Getting a microcontroller up and moving is actually fairly involved. There’s a ton of stuff that needs to happen after plugging in the power before any real work begins to happen. The ST company has produced a software package than can deal with a lot of this for you. 1
To install it, go to http://www.st.com/en/development-tools/stm32cubemx.html, scroll to the bottom and click to download. You’ll need to give an email address and all that, but then they email you a link to a page that includes windows and linux binaries.
You’ll also need to download the STM32F1 hardware abstraction layer:2

Can’t use OpenJDK

Note that STMCubeMX didn’t work for me with OpenJDK. I needed to install Oracle’s Java
sudo add-apt-repository ppa:webupd8team/java -y
sudo apt-get update
sudo apt-get -y install oracle-java8-installer
For instructions on how to get the boilerplate, please go to the video linked above.

Add Code needed to blink

To turn on pin 13:
To turn it off:
To learn more about this code, I recommend ST’s RM0008 Reference manual, section 9.2.4 “Port output data register (GPIOx_ODR)”
To generate a delay for blinking this pin, you could just write a delay loop like this one:
void delayLoop() {
volatile uint32_t delayCount = 100000;
while (delayCount > 0) {
but there’s a better way. SysTick. The STM32 hardware abstraction layer includes a timer interrupt that ticks every millisecond. The learn more about it, I recommend ST’s UM1850 User manual: Description of STM32F1xx HAL drivers. This is the document I’ve found most helpful.
For the code below, I’ve omitted code not relevant to this post. The interrupt handler is in Src/stm32f1xx_it.c:
void SysTick_Handler(void) {
void HAL_SYSTICK_IRQHandler(void)
__weak void HAL_SYSTICK_Callback(void)
/* NOTE : This function Should not be modified, when the
callback is needed, the HAL_SYSTICK_Callback could be
implemented in the user file
so we do as instructed and implement our own HAL_SYSTICK_Callback. Since the HAL one is marked weak, we can redefine it. Something like this:
void HAL_SYSTICK_Callback(void) {
static uint8_t ledon = 1;
if ((HAL_GetTick() % 1000) == 0) {
if (ledon) {
} else {
ledon = !ledon;
While this works, note that it’s bad practice. You don’t want to do real work in an interrupt handler. Part of my point is to show you how easy it can be to deal with interrupts.

Compiling the code

Now that we have some code, we need to compile it. Many/most of the other tutorials that I’ve seen out there focus on using some sort of IDE. Call me old, but I like the old fashioned way, calling gcc, make,…
To compile, you’ll need a version of the gcc/gdb/g++… toolchain that’s built for ARM: https://developer.arm.com/open-source/gnu-toolchain/gnu-rm/downloads. Download the latest version, untar it, and note where you put it.
Now you need to modify the Makefile that STMCubeMX generated for you. You’ll want to update the BINPATH var:
BINPATH = /bubba/electronicsDS/stm32/gcc/gcc-arm-none-eabi-6-2017-q1-update/bin
I also recommend making two additional debugger/gdb related changes to the Makefile:

  • adding -Werror next to the existing -Wall flags. 3
  • change -g to -g3. This enables you to see #define values in gdb.

Now you can just run make. I get something like this at the end of the output:
/bubba/electronicsDS/stm32/gcc/gcc-arm-none-eabi-6-2017-q1-update/bin/arm-none-eabi-size build/blinky.elf
text data bss dec hex filename
4084 16 1568 5668 1624 build/blinky.elf
A good explanation of these lines can be found here. In a nutshell:

  • text (~4k) is how much flash we’re using (out of 64k or 128k, in the case of stm32f103
  • data is initialized RAM values. globals with initialization.
  • bss is uninitialized RAM. globals without initialization.

Transfer the binary to the board

The STM32 products come with a bootloader. Many of the stm arduino tutorials use that to get the arduino bootloader on there. I prefer st-link because once you have that setup, you also have debug access. All of this isn’t hard, though I didn’t find a good step-by-step with the little details. Here’s my version (remember this is for ubuntu. 16.04 in my case). I use openocd. I found this page helpful in getting started on openocd
wget https://sourceforge.net/projects/openocd/files/openocd/0.10.0/openocd-0.10.0.tar.gz
tar -xf openocd-0.10.0.tar.gz
cd openocd-0.10.0/
mkdir install
sudo apt-get install -y libtool automake libusb-1.0.0-dev texinfo libusb-dev texlive-base libftdi-dev
# the stlink and usb blaster flags are not required, openocd will automatically include these if you
# have the needed includes in your system. Adding the flags forces it to include and will trigger
# an error if the includes aren’t there.
./configure –prefix=`realpath install` –enable-stlink –enable-usb-blaster
make install
this will put openocd into openocd-0.10.0/install
before we can check it, we need to connect the stm board to the st-link. See the picture at the beginning of this post. Both devices are labeled.

  • st-link 3.3  – stm 3.3
  • st-link gnd – stm gnd
  • st-link swclk – stm swclk
  • st-link swdio – stm swo

linux permissions to st-link v2

Once you plug in the programmer (which will also power the stm32; no need to plug in both) 4, linux probably won’t let you access the programmer yet.
First let’s see that the programmer is recognized:
–> lsusb
Bus 001 Device 013: ID 0483:3748 STMicroelectronics ST-LINK/V2
–> ls -l /dev/ttyUSB*
crw-rw—- 1 root dialout 188, 0 Jul 5 10:43 /dev/ttyUSB0
crw-rw-rw- 1 root dialout 188, 1 Jul 5 10:38 /dev/ttyUSB1
Note that the USB tty devices belong to the dialout group. Add yourself to this group and you’ll be able to proceed (after logging out and then back in)
sudo usermod -a -G dialout $USER
If all goes well, you can do this:
export OPENOCD=/bubba/electronicsDS/stm32/openocd-0.10.0/install
$OPENOCD/bin/openocd -f $OPENOCD/share/openocd/scripts/interface/stlink-v2.cfg -f $OPENOCD/share/openocd/scripts/target/stm32f1x.cfg
This is what I get:
Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
Info : auto-selecting first available session transport "hla_swd". To override use ‘transport select <transport>’.
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
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 v17 API v2 SWIM v4 VID 0x0483 PID 0x3748
Info : using stlink api v2
Info : Target voltage: 3.227913
Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints
If you get this instead, you have a permissions error:
Error: libusb_open() failed with LIBUSB_ERROR_ACCESS
Error: open failed
An alternative to adding yourself to dialout group is to add these lines to /etc/udev/rules.d/49-stlinkv2.rules
# stm32 discovery boards, with onboard st/linkv2
# ie, STM32L, STM32F4.
# STM32VL has st/linkv1, which is quite different
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="3748", \
MODE:="0666", SYMLINK+="stlinkv2_%n"
Finally, we can transfer the code. I like to have gdb upload the code every time I invoke it so I know it’s always uptodate. I do this with a local .gdbinit file. This file is mostly reusable from project to project. Just update the two mentions of blinky.elf. Also note the two env variables mentioned in comments.
#export GDBEXEC=/bubba/electronicsDS/stm32/gcc/gcc-arm-none-eabi-6-2017-q1-update/bin/arm-none-eabi-gdb-py
#export OPENOCD=/bubba/electronicsDS/stm32/openocd-0.10.0/install
file build/blinky.elf
target remote | $OPENOCD/bin/openocd -f $OPENOCD/share/openocd/scripts/interface/stlink-v2.cfg -f $OPENOCD/share/openocd/scripts/target/stm32f1x.cfg -c "gdb_port pipe; log_output openocd.log"
define restart
monitor reset halt
define reload
monitor reset halt
monitor stm32f1x mass_erase 0
monitor program build/blinky.elf verify
monitor reset halt
# add your breakpoints.
break main
Now I run gdb
export GDBEXEC=/bubba/electronicsDS/stm32/gcc/gcc-arm-none-eabi-6-2017-q1-update/bin/arm-none-eabi-gdb-py
export OPENOCD=/bubba/electronicsDS/stm32/openocd-0.10.0/install
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000178 msp: 0x20005000
stm32x mass erase complete
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0xfffffffe msp: 0xfffffffc
** Programming Started **
auto erase enabled
target halted due to breakpoint, current mode: Thread
xPSR: 0x61000000 pc: 0x2000003a msp: 0xfffffffc
wrote 5120 bytes from file build/blinky.elf in 0.365343s (13.686 KiB/s)
** Programming Finished **
** Verify Started **
target halted due to breakpoint, current mode: Thread
xPSR: 0x61000000 pc: 0x2000002e msp: 0xfffffffc
verified 4100 bytes in 0.102377s (39.109 KiB/s)
** Verified OK **
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000f18 msp: 0x20005000
Breakpoint 1 at 0x8000286: file Src/main.c, line 86.
Note: automatically using hardware breakpoints for read-only addresses.
Breakpoint 1, main () at Src/main.c:86
86 HAL_Init();
(gdb) c
You should now have a blinking light on your board.

  1. Some months ago, I had some clocking issues using the generated code. Thankfully, they fixed it. Also a new feature is Makefile generation. There is still at least one major bug I’ve had to work around. (I2C stability)

  2. In theory, STMCube will download this for you, but when I did it, the application crashed

  3. the main reason I do this it to get an error on non-void functions that don’t return a value. I forget to do this with small functions all the time.

  4. most tutorials advise not to plug in both, or if you do, to disconnect the power wire on all but one. This is good conservative advice. I have not had issues when all are connected to various usb ports on the same computer.

3 responses to “6 ways to communicate with STM32F103C8T6. Part 1. Zero to blinking an led”

  1. hello, i wanted to upload the code without using ST-LINK, is there a way to use the USB to mini placed on the board.I went on a search but can’t seem to understand the real reason for this.,i mean why use st-link when we can programmed it through the USB.

    • Why use st-link? Partially, because that’s how it’s meant to be programmed. There is the alternative of flashing the arduino bootloader. The first time, you’d need an st-link but after you can use USB. The big downside is that it won’t give you debugger support.

Leave a Reply

Your email address will not be published. Required fields are marked *