Blog

October 07, 2016 | Electronics |

Getting Started with STM32 ARM Cortex-M3 using GCC (STM32F103, NUCLEO-F103RB)

STM32

AVR to ARM

My first introduction to microcontrollers was via the Arduino platform. But I soon started digging a little deeper and got into (8-bit) AVR programming. I was especially fascinated with the tinyAVR line, which I still think of as tiny computers that I can (almost) wrap my head around. I was hearing more and more about 32-bit ARM Cortex-M processors, but was intimidated by the apparent complexity of programming these chips. As I started working with connected devices like Nordic nRF51/52 (BLE) and TI CC3200 (WiFi), I got introduced to the concept of SoC - System On a Chip, where the radio was paired with a processor - often an ARM Cortex-M. Then came the Arduino Zero (and compatible boards like the Neutrino) which adopted the Atmel SAMD21 ARM Cortex-M0+ chip. So I started getting familiar with ARM, but the various layers of libraries and tools I used hid the ARM architecture from me.

In my view, an obvious advantage of ARM compared to (8-bit) AVR was superior capabilities - faster processor speeds, more number of peripherals like ADCs and GPIOs, USB, support for RTOS (Real Time Operating Systems), etc. But another surprising discovery was the cost factor. 32 bit ARM processors were actually cheaper than 8-bit AVRs! So if you were developing a product, the writing was on the wall. It was becoming a necessity to learn about these processors.

Pick an ARM, Code an ARM

Programming AVRs was a walk in the park. Just download avrdude + avr-gcc, get a cheap USB programmer, and you were set. Compared to that, developing for ARM is like trying to stroll in the middle of a raging battlefield. First, you need to choose an ARM chip - Cortex-M0, Cortex-M3, etc. Then, you need to choose a vendor - Atmel, ST, NXP, Microchip, etc., and go through their scary variety of offerings to make your final selection. Next comes the choice of a development board for your chip - either one with an integrated debugger (eg. J-LINK OB, ST-Link), or separate board and generic debugger (eg. J-Link). (The hardware helps you debug your code running in the chip, as well as upload it to the flash memory.) And then comes the most fun - selecting development tools for your chip. “Professional” tools for ARM program are, for the lack of a more apt adjective, obscenely priced - like a few thousand US dollars.(Vendors also offer free code size limited versions of their expensive Pro tools.) Fortunately, Open Source comes to the rescue again, in the form of ARM GCC, OpenOCD and Eclipse. You can read a rather lengthy discussion on this topic here.

Once you have decided all of the above, you still have to make a choice on how to structure your code - use “bare metal” (just registers + C), vendor’s HAL or middleware, CMSIS, or maybe go “full me(n)tal” and code in assembly.

My own experience with selecting an ARM Cortex-M3 chip went as follows: I trawled through various internet forums for posts that compared Atmel vs. NXP vs. ST vs. Microchip vs. WhateverARM. After a couple of days, I was convinced of one thing - we humans are an unhappy lot, and can’t agree on anything. So I made my choice solely based on economics - ST has the lowest price on ARM chips - especially if I source them from China.

So in the end, I decided to begin my ARM journey with the STM32F103 ARM Cortex-M3 chip from STMicroelectronics.

Getting Started with STM32

When starting with ARM programming, I feel it’s best to get the vendor’s official development board, rather than muck around with third-party breakout boards, for these reasons:

  • They are usually very economical.
  • You get a well documented, working reference design.
  • Comes with a hardware debugger.
  • Easy to prototype with.

Since it’s hard to argue about a $11 (inflated to Rs.1129 in our case) board, I got the STMicroelectronics NUCLEO-F103RB board with the STM32F103RB chip.

In this article, I’ll build a small project using this board. Our goals are:

  1. Set up a development environment.
  2. Learn how to debug and upload code.
  3. Make an LED blink.
  4. Learn how to use the USART peripheral.

The first thing to do is to set up a development environment. As I mentioned, I decided to go with GCC, but even with GCC there are several choices, including paid options with support. I chose the ARM GCC toolchain, Eclipse IDE, and OpenOCD for debugging. I found that the simplest way of using these tools was via the Eclipse extensions for ARM GCC developed by Liviu Ionescu.

For installation please follow the excellent documentation by Liviu below:

http://gnuarmeclipse.github.io/install/

Make sure you follow the steps in the given order. To quote the author:

“Note for beginners: If performed for the first time, it is recommended to follow the steps by the book and avoid poetic licenses, since they might lead to tricky situations and subtle functional problems.”

Once you have gone through the installation, start an new project in Eclipse by choosing File->New->C Project where you get this dialog:

STM32

Choose the appropriate chip, and then you get this dialog:

STM32

In content you can just choose empty. At the end the dialog, you will end up with a directory structure similar to the one below:

stm32F103_blinky_serial mahesh$ tree -L 3
.
├── Debug
│   ├── makefile
│   ├── objects.mk
│   ├── sources.mk
│   ├── src
│   │   ├── main.d
│   │   ├── main.o
│   │   └── subdir.mk
│   ├── stm32F103_blinky_serial.elf
│   ├── stm32F103_blinky_serial.hex
│   ├── stm32F103_blinky_serial.map
│   └── system
│       └── src
├── Release
│   ├── makefile
│   ├── objects.mk
│   ├── sources.mk
│   ├── src
│   │   ├── main.d
│   │   ├── main.o
│   │   └── subdir.mk
│   ├── stm32F103_blinky_serial.elf
│   ├── stm32F103_blinky_serial.hex
│   ├── stm32F103_blinky_serial.map
│   └── system
│       └── src
├── include
│   └── stm32f10x_conf.h
├── ldscripts
│   ├── libs.ld
│   ├── mem.ld
│   └── sections.ld
├── src
│   └── main.c
└── system
    ├── include
    │   ├── arm
    │   ├── cmsis
    │   ├── cortexm
    │   ├── diag
    │   └── stm32f1-stdperiph
    └── src
        ├── cmsis
        ├── cortexm
        ├── diag
        ├── newlib
        └── stm32f1-stdperiph

In the above tree, I removed the _write.c file supplied by the project, and I also have Debug/Release populated above because I have built the executable.

The stm32f1-stdperiph directory above is the Standard Peripheral Library which is a layer of code provided by STMicroelectronics which lets you use the chip peripherals like GPIOs, ADSs, USARTs, etc. without directly setting up the registers. (More on this later.)

One thing to note is that a lot of the unused code is filtered out in the project settings, which can be found here:

STM32

Once you start using any of these files (eg. stm32f10x_usart.c) you need remove that from the exclude filter above.

Next, you need to set up OpenOCD for debugging. This is already covered in the installation list. But one thing I found to be missing is using OpenOCD to write the code to the flash memory of the chip. You can easily set that up under
Run->External Tools->External Tools Configuration:

STM32

(To clarify, OpenOCD debugging only loads the code onto the RAM of the chip, which is cleared when the chip is reset. The above writes to the Flash memory of the chip, so it’s retained on reset.)

Now it’s almost time to start coding. But before that, you need to do a bit of reading, and here’s your list:

  1. STM32F103x datasheet from STMicroelectronics
  2. STM32F103x reference manual from STMicroelectronics
  3. UM1724 User manual for STM32 Nucleo-64 boards from STMicroelectronics
  4. AN2586 Application note - Getting started with STM32F10xxx hardware development

(1) above gives an overview of the chip whereas (2) goes into details - setting up registers and programming the chip. (3) is a handy reference for the Nucleo board, and (4) is helpful when you are designing your own PCB with the chip.

The above documents are all pretty cryptic, but fortunately there exists a great, free, comprehensible reference to get started on STM32:

Discovering the STM32 Microcontroller by Prof. Geoffrey Brown

You can find the PDF link at the bottom of his list of publications.

http://homes.soic.indiana.edu/geobrown/index.cgi/Publications

I highly recommend that you get the above PDF and spend some time reading it before you do any programming.

Before getting into what our program does, let’s briefly look at a few approaches (not comprehensive) to programming STM32:

  1. Use Registers + C code
  2. Use the Standard Peripheral Library
  3. Use CMSIS
  4. Use STM32Cube + HAL
  5. Use some of the above and write your own portable “middleware”

You can read up on CMSIS and STM32Cube, and maybe read this long discussion on eevblog, but making any decision there is beyond the scope of this article. So let’s talk a bit about simpler approaches (1) and (2) below.

Here’s some code that sets up pin PA5 as output so we can blink the LED (USR LD2) on the Nucleo board.

// enable clock for GPIOA and GPIOC
RCC->APB2ENR |= (RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPCEN);

// set up PA5 as output
//
// clear bits Mode5[1:0] and CNF5[1:0]
uint32_t bitmask = GPIO_CRL_MODE5 | GPIO_CRL_CNF5;
uint32_t tmpReg = GPIOA->CRL;
tmpReg &= ~bitmask;
// set CNF5[1:0] and Mode5[1:0]
// CNF5[1:0]  -> 00: General purpose output push-pull
// MODE5[1:0] -> 10: Output mode, max speed 2 MHz.
tmpReg |= (GPIO_CRL_MODE5_1);
// set values to GPIOA_CRL
GPIOA->CRL = tmpReg;

Above, we first enable the peripheral clock for GPIOA, and then proceed to set the appropriate registers (see datasheet) to set PA5 as an output.

It could be argued that the above code isn’t “bare metal” enough. Definitions like RCC->APB2ENR come from the stm32f10x.h file. You could certainly skip these headers and code directly with register addresses given in the datasheet - if you are feeling particularly masochistic.

Now let’s code the same using the Standard Peripheral Library:

// enable clock for peripheral
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

// initialize GPIO structure
GPIO_InitTypeDef gpioInitStruct;
GPIO_StructInit(&gpioInitStruct);
gpioInitStruct.GPIO_Pin = GPIO_Pin_5;
gpioInitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
gpioInitStruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOA, &gpioInitStruct);

As you can see, the code is much more readable that the first version. Another fact is that this code is very easy to port across other chips from STMicroelectronics. But the downside is that the code, being generic, covers a lot of stuff that may not apply to your specific use. Take a look at GPIO_Init implementation in stm32f10x_gpio.c and compare it with the “bare metal” code above.

One approach I can suggest is to use the Standard Peripheral Library, but not blindly. Read through the implementation of the functions you are using. When the need arises, write your own optimized functions that use just what you need.

Now let’s get on with our program. We start with some initializations:

// LED is on PA5
// enable clock for peripheral GPIOA and GPIOC
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC, ENABLE);

// initialize GPIO structure
GPIO_InitTypeDef gpioInitStruct;
GPIO_StructInit(&gpioInitStruct);
gpioInitStruct.GPIO_Pin = GPIO_Pin_5;
gpioInitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
gpioInitStruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOA, &gpioInitStruct);

// set PC13 (User button 1) as input
gpioInitStruct.GPIO_Pin = GPIO_Pin_13;
gpioInitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOC, &gpioInitStruct);

In the above code, we set up PA5 (LED on Nucleo board) as output, and PC13 (Blue button on Nucleo board) as input. As before, peripheral clocks are enabled for these ports.

Next, we set up serial communications on USART2.

// serial comms
// use PA2 (TX) and PA3 (RX)
// USART2
// These are connected to the ST-Link circuit on the Nucleo board
// so the serial output will be via USB, on your computer

// enable clock for USART2
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);

// setup Tx/RX pins:

// init TX
gpioInitStruct.GPIO_Pin = GPIO_Pin_2;
gpioInitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
gpioInitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpioInitStruct);
// init RX
gpioInitStruct.GPIO_Pin = GPIO_Pin_3;
gpioInitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &gpioInitStruct);

// setup USART2:

// initialize struct
USART_InitTypeDef usartInitStruct;
USART_StructInit(&usartInitStruct);
// set parameters
usartInitStruct.USART_BaudRate = 9600;
usartInitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART2, &usartInitStruct);
USART_Cmd(USART2, ENABLE);

We’re going to use PA2 (TX) and PA3 (RX) for serial communications. This is because they are already connected to the ST-Link hardware which is connected via USB to your computer. (You could use PA9 and PA10 instead, but then you’ll need to hook up a separate USB to serial adapter on those lines for testing.) Once you start using Standard Peripheral Library, the initialization scheme is quite uniform, so the code above is pretty much self-explanatory.

Now let’s look at the main loop:

// Infinite loop
volatile uint8_t val = 0;
uart_putc(USART2, '.');
uart_putc(USART2, '\n');
while (1)
{
  // read PC13 input
  uint8_t input = GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_13);
  if (!input) {
    trace_printf("button pressed!\n");
    val = 1;
  }

  // if flag set go into serial input mode
  if (val) {
    // show prompt
    uart_putc(USART2, '>');
    // get input
    char c = uart_getc(USART2);
    uart_putc(USART2, '\n');
    uart_putc(USART2, '.');
    uart_putc(USART2, '\n');
    // set LED
    int led = (c == '0') ? 0 : 1;
    GPIO_WriteBit(GPIOA, GPIO_Pin_5, led);
    // reset input mode flag
    val = 0;
  }

  // wait
  myDelay(500);
}

The main loop proceeds as follows: We first read the button input on PC13. If the button is pressed, we set a flag val. If the flag is set, we enter an “input mode” using the blocking call uart_getc(USART2);. We then read user input (expecting ‘1’/’0’) and use that to toggle the LED on PA. The UART methods used are all defined in stm32f10x_usart.c, and the GPIO methods in stm32f10x_gpio.c - all part of Standard Peripheral Library.

We’ll talk a bit about the myDelay() function. This is implemented by hooking into the SysTick_Handler of the chip:

static volatile uint32_t sysTickCount = 0;

void myDelay(uint32_t nTime)
{
	sysTickCount = nTime;
	while(sysTickCount != 0);
}

void SysTick_Handler()
{
	if (sysTickCount != 0) {
		sysTickCount--;
	}
}

Our SysTick_Handler overrides the default implementation in exception_handlers.c, and we use this to implement a delay function, as shown above. (This implementation is from Brown’s book.)

To test the code, upload it the chip, open any serial communications application on your computer (I use CoolTerm.), and connect at 9600 baud and default settings.

The code above is far from robust. If you are using serial communications, you should probably use interrupts, for instance. But that’s for another time.

External Programming with NUCLEO-F103RB

As I mentioned, one advantage of getting the official development board is the build-in hardware debugger. This hardware in most cases can be used to program external devices. In the case of the NUCLEO-F103RB, there is an SWD interface which can be used for this purpose.

STM32

The above figure is from the Nucleo user manual, and it shows how you can remove CN2 jumpers (move them to grounded CN11, CN12 so you don’t lose them) and use the CN4 SWD connector for programming an external target.

I bought a cheap (Rs. 280, $4) STM32F103C8T6 board from ebay to test this out.

STM32

It worked great! Pins 1 to 5 on CN4 on the Nucleo board were needed. There’s nothing special to be done - once connected, debug and program will automatically go to the external target. I wrote a simple blinky program for testing, and for this board, the LED was on PC13.

Conclusion

Starting with ARM can be intimidating, especially if you come from the 8-bit AVR world. But there are good learning resources and free Open Source tools available that can help you with this task. Just be ready to do some heavy reading. :-)

Downloads

You can get the complete source code for this project here:

https://github.com/electronut/stm32-start


Questions/Comments

We love hearing from our readers. Email us at info@electronut.in for questions or comments on this article. If you found this article useful, please support us by buying some of our Open Source hardware products - like ZeroDriver - an Arduino Zero compatible motor driver for robotics. ZeroDriver is crowdfunding right now. Please click here to support our campaign. and bring this product to market!


Please sign up for updates

Once in a while, we will send you an email update on the latest Electronut Labs projects and products. Your email address will never be shared or abused, ever.

2016 Electronut Labs. All rights reserved.