Samstag, 4. Juni 2016

STM32 microcontroller #3: HAL & better makefile

Introduction

In my last post, I changed the clock speed of my STM32 controller by reading the datasheet/manual and setting some registers.
For bigger projects, it is just too much work to read the Reference Manual and find all registers settings to use a peripheral of a STM32 controller. For that reason, ST-Microelectronics created a software library that hides direct register access behind a layer of higher level C-functions. This is called "harware abstraction layer", or HAL. I will show an example that uses the HAL library to change the clock speed and flashes a LED. I will provide a makefile that should be good enough for future projects.

A simple HAL program

The HAL comes as part of the STMCube software package. For my STM32F103RBT6 controller, all files are in the directory STM32Cube_FW_F1_V1.3.0/Drivers/STM32F1xx_HAL_Driver. It contains one file (stm32f1xx_hal_conf_template.h) which has to be copied into any new project that uses the HAL library and has to be renamed to stm32f1xx_hal_conf.h.

Before starting to write code, documentation is needed. There is a document with the title "Description of STM32F1xx HAL drivers" which you can find by searching for UM1850 on the ST-Microelectronics webpage. I think that is best resource of information for the HAL, but if I found it not as good as the "Reference Manual" in terms of quality and completeness. But that might be related to my limited experience in using HAL. I would even say, that getting started with the HAL programming (without any prior knowledge) is just as difficult as starting to use the bare registers. The good thing is: I found lots of example code written using the HAL inside the STM32Cube package, so it is possible to get a working program in short time by copy/paste from these examples and a bit of adaptation. On the one hand that is nice, on the other hand it often prevents deeper understanding of what is going on behind the scenes.

I created a new directory with the following content:
  • makfile (see below) 
  • src/ 
    • main.c 
    • stm32f1xx_hal_conf.h (copied from the stm32f1xx_hal_conf_template.h) 
The content of main.c was mainly taken from the GPIO example project for the STM32F103RB-Nucleo develompent board. This can be found in the STMCube package in the directory STM32Cube_FW_F1_V1.3.0/Projects/STM32F103RB-Nucleo/Examples/GPIO/.

Many parts of the HAL depend on a time base which is incremented inside the SysTick_Handler interrupt routine. So, this routine must be defined and it must call the HAL_IncTick() function. There is a function HAL_Init(), that should be called before using any other HAL function. It mainly initializes the time base counter.

The steps for setting the clock frequency are identical to my previous post, only that no single register is accessed directly. All settings are done by setting values in a C-structures of type RCC_OscInitTypeDef, and of type RCC_ClkInitTypeDef, which are then passed to functions HAL_RCC_OscConfig and HAL_RCC_ClockConfig, respectively.

Enabling a GPIO pin also works by filling values to a structure of type GPIO_InitTypeDef and passing it to the function HAL_GPIO_Init(). Toggling a pin works by calling HAL_GPIO_TogglePin(). Because of the integrated timebase, the HAL_Delay() function provides precise delays in units of miliseconds.

Here is the complete code of main.c:

/**
******************************************************************************
* based on: 
*   Projects/STM32F103RB-Nucleo/Examples/GPIO/GPIO_IOToggle/Src/main.c
*   from the STM32Cube_FW_F1_V1.3.0 software package package
******************************************************************************
* @attention
*
* <h2><center>&copy; COPYRIGHT(c) 2015 STMicroelectronics</center></h2>
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
* 1. Redistributions of source code must retain the above copyright notice,
*    this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
*    this list of conditions and the following disclaimer in the documentation
*    and/or other materials provided with the distribution.
* 3. Neither the name of STMicroelectronics nor the names of its contributors
*    may be used to endorse or promote products derived from this software
*    without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
******************************************************************************
*/

#include "stm32f1xx.h"
#include "stm32f1xx_hal.h"

void SysTick_Handler(void)
{
 HAL_IncTick();
}

void SystemClock_Config(void)
{
 // Configure PLL with HSE_VALUE = 16000000UL = 16 MHz
 // PLL configuration: 
 //   PLLCLK = (HSE / 2) * PLLMUL = (16 / 2) * 9 = 72 MHz 
 RCC_OscInitTypeDef oscinitstruct = {0};
 oscinitstruct.OscillatorType      = RCC_OSCILLATORTYPE_HSE;
 oscinitstruct.HSEState            = RCC_HSE_ON;
 oscinitstruct.LSEState            = RCC_LSE_OFF;
 oscinitstruct.HSIState            = RCC_HSI_OFF;
 oscinitstruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
 oscinitstruct.HSEPredivValue      = RCC_HSE_PREDIV_DIV2;
 oscinitstruct.PLL.PLLState        = RCC_PLL_ON;
 oscinitstruct.PLL.PLLSource       = RCC_PLLSOURCE_HSE;
 oscinitstruct.PLL.PLLMUL          = RCC_PLL_MUL9;
 HAL_RCC_OscConfig(&oscinitstruct);
 
 // Select PLL as system clock source and configure 
 // the HCLK, PCLK1 and PCLK2 clocks dividers 
 RCC_ClkInitTypeDef clkinitstruct = {0};
 clkinitstruct.ClockType = (RCC_CLOCKTYPE_SYSCLK 
                             | RCC_CLOCKTYPE_HCLK 
                             | RCC_CLOCKTYPE_PCLK1 
                             | RCC_CLOCKTYPE_PCLK2);
 clkinitstruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
 clkinitstruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
 clkinitstruct.APB2CLKDivider = RCC_HCLK_DIV1;
 clkinitstruct.APB1CLKDivider = RCC_HCLK_DIV2;  
 HAL_RCC_ClockConfig(&clkinitstruct, FLASH_LATENCY_2);
}

int main(void)
{
 //  STM32F103xB HAL library initialization:
 // - Configure the Flash prefetch
 // - Systick timer is configured by default as source of time base, but user 
 //  can eventually implement his proper time base source (a general purpose 
 //  timer for example or other time source), keeping in mind that Time base 
 //  duration should be kept 1ms since PPP_TIMEOUT_VALUEs are defined and 
 //  handled in milliseconds basis.
 // - Set NVIC Group Priority to 4
 // - Low Level Initialization
 HAL_Init();
 
 // Configure the system clock to 72 MHz 
 SystemClock_Config();
 
 // -1- Enable GPIO Clock (to be able to program the configuration registers) 
 __HAL_RCC_GPIOC_CLK_ENABLE();
 
 // -2- Configure IO in output push-pull mode to drive external LED
 GPIO_InitTypeDef  GPIO_InitStruct;
 GPIO_InitStruct.Mode  = GPIO_MODE_OUTPUT_PP;
 GPIO_InitStruct.Pull  = GPIO_PULLUP;
 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
 GPIO_InitStruct.Pin   = GPIO_PIN_4;
 HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
 
 // -3- Toggle IO in an infinite loop 
 while (1)
 {
  HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_4);
  HAL_Delay(500); // 500 ms = 1/2 s
 }
}

The new makefile

I have written a new makefile that requires a minimum amount of manual work to start a new project. The only file that needs to be copied from the STM32Cube package is stm32f1xx_hal_conf_template.h that should be renamed into stm32f1xx_hal_conf.h. Here, individual parts of the HAL can be switched off if they are not needed. All other files are directly compiled from the STM32Cube package. At the top of the makefile, the root directory of the STM32Cube package has to be specified, as well as some information about the device.
With minor modifications, this makefile should also work for another SMT32FXXX family, but I did not test that yet.

########################################################################
# Makefile for STM32F3 MCU using arm-none-eabi-gcc toolchain
#  for compiling, and openocd with st-link-v2 for uploading.
# 
# For project setup, copy linker script into same directory as this
# makefile and create a src/ subdirectory that contains the application
# code and stm32f1xx_hal_conf.h 
#
# Use this at your own risk.
#
# Note: using this makefile for other families might require changes.
########################################################################

# root path of STM32Cube package
STM32CUBE_PATH = ../STM32Cube_FW_F1_V1.3.0

# specify device type by family, type and number: 
#  (all letters in capitals)
#   e.g. for STM32 F1 03 RBT6
#      FAMILY = F1
#      TYPE = 03
#      NUMBER = X6    (X replaces RBT or C8T)

# device family
FAMILY = F1
# device type
TYPE = 03
# device number
NUMBER = X6
# the CPU
CPU = cortex-m3

# external quarz frequency
HSE_VALUE = 16000000UL

# program name
PROGRAM = main

# list of additional program modules (.o files)
OBJS = \
 
########################################################################
# Changes below this line should not be necessary.
########################################################################

# device name
lc = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$1))))))))))))))))))))))))))
Device = STM32$(FAMILY)$(TYPE)$(call lc,$(NUMBER))
DEVICE = STM32$(FAMILY)$(TYPE)$(NUMBER)
device = $(call lc,$(DEVICE))
family = $(call lc,$(FAMILY))


vpath %.c $(STM32CUBE_PATH)/Drivers/STM32$(FAMILY)xx_HAL_Driver/Src \
          $(STM32CUBE_PATH)/Drivers/CMSIS/Device/ST/STM32$(FAMILY)xx/Source/Templates/ \
          src
          
vpath %.h $(STM32CUBE_PATH)/Drivers/STM32$(FAMILY)xx_HAL_Driver/Inc  \
          $(STM32CUBE_PATH)/Middlewares/Third_Party/FreeRTOS/Source/include/ \
          $(STM32CUBE_PATH)/Drivers/CMSIS/Device/ST/STM32$(FAMILY)xx/Include/ \
          src
          
vpath %.s $(STM32CUBE_PATH)/Drivers/CMSIS/Device/ST/STM32$(FAMILY)xx/Source/Templates/gcc/

vpath %.o obj

CFLAGS = -I $(STM32CUBE_PATH)/Drivers/CMSIS/Device/ST/STM32$(FAMILY)xx/Include \
         -I $(STM32CUBE_PATH)/Drivers/CMSIS/Include \
   -I $(STM32CUBE_PATH)/Drivers/STM32$(FAMILY)xx_HAL_Driver/Inc/ \
   -I src \
         -Wall -Os -std=c99 -mcpu=$(CPU) -mlittle-endian -mthumb  -D$(Device) -DHSE_VALUE=$(HSE_VALUE) 
         
# the stm32 hardware abstraction library
STM_HAL_OBJS = $(shell ls $(STM32CUBE_PATH)/Drivers/STM32$(FAMILY)xx_HAL_Driver/Src | sed  's/\.c/\.o/g')

# startup and system initialization code
STM_BASIC_OBJS = \
 system_stm32$(family)xx.o        \
 startup_$(device).o              \
 
# generic rules
%.o: %.c %.h
 arm-none-eabi-gcc $(CFLAGS)  -c $< -o $@

%.o: %.c 
 arm-none-eabi-gcc $(CFLAGS)  -c $< -o $@

%.o: %.s
 arm-none-eabi-gcc $(CFLAGS)  -c $< -o $@

# main target
all: $(PROGRAM).hex

$(PROGRAM).hex: $(PROGRAM).elf
 arm-none-eabi-objcopy -Oihex $(PROGRAM).elf  $(PROGRAM).hex

$(PROGRAM).elf: $(PROGRAM).o $(OBJS) $(STM_HAL_OBJS) $(STM_BASIC_OBJS) $(DEVICE)_FLASH.ld
 arm-none-eabi-gcc $(CFLAGS)  -T $(DEVICE)_FLASH.ld \
 -Wl,--gc-sections  $(PROGRAM).o $(OBJS) $(STM_HAL_OBJS) $(STM_BASIC_OBJS) -o $(PROGRAM).elf

# make a local copy of the linkcer script that has no '0' characters 
#   (why are there lines starting with '0' ?)
LINKER_SCRIPT = $(STM32CUBE_PATH)/Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/gcc/linker/$(DEVICE)_FLASH.ld
$(DEVICE)_FLASH.ld: $(LINKER_SCRIPT)
 sed 's/^0/ /g' $(LINKER_SCRIPT) > $(DEVICE)_FLASH.ld

flash: $(PROGRAM).hex
 sudo openocd -f /usr/share/openocd/scripts/interface/stlink-v2.cfg     \
              -f /usr/share/openocd/scripts/target/stm32$(family)x.cfg  \
              -c "program $(PROGRAM).hex verify reset exit"

clean:
 rm -f *.o $(PROGRAM).elf *.ld
 
.PHONY: all flash clean






Freitag, 3. Juni 2016

STM32 microcontroller #2: setting the clock speed

Introduction

In my first post about the STM32F103RBT6 device, I was not having a crystal oscillator on the board, and the code did not change any of the clock settings. That means, the controller is running at 8 MHz from an internal oscillator. Some applications need more precise timings, in particular USB communication. My breakout PCB contains a place for a crystal. After putting a 16 MHz quartz in place (along with a USB mini-B), my board looks like this:
The schematic drawing looks like this:

Changing the clock frequency is - again - just a matter of setting/clearing the correct bits in the correct registers in the correct order. I'm used to do this on an ATMEGA8, but with the STM32F103RBT6 there are roughly 4 times more bits to set to achieve something. ST-Microelectronics offers a hardware abstraction layer (HAL) library, that replaces the bit-fiddling with higher level function calls. But I still like to see that things can work from scratch, just to convince myself that there is no hidden magic happening. That's why I will show here how to configure the STM32F103RBT6 to run at 48 MHz from a 16 MHz crystal.

The core frequency can be higher than the primary clock source, because the chip contains a phase-locked-loop (PLL) circuit. I think of a PLL as an oscillator (typically faster than the PLL input) which runs not independently, but has a fixed phase relation with another oscillator (the quartz in this case). In this way, the long term stability of the quartz oscillator is also present on the faster PLL oscillator. So, effectively a PLL multiplies the input frequency.
The registers for the clock and PLL configuration are described on page 133 of the Reference Manual. Here are the steps to get to 48 MHz from a quartz:

  • Configure the quartz as PLL input. The term "high speed external oscillator" (HSE) is used. (It is also possible to use the internal oscillator as PLL input to make the controller run faster and if the high stability of a quartz is no needed.)
  • Activate the HSE oscillator. (it is off by default)
  • Configure the PLL to multiply the input frequency by 6. (severel other multiplication factors are possible)
  • Divide the HSE frequency by 2 before entering the PLL.
  • Activate the PLL circuit.
  • Wait until the PLL is locked, i.e. the phase stability between input and output is established.
  • Set main system clock to PLL output.
With a 16 MHz HSE clock, the resulting frequency should be 16/2 *6 = 48. Alternatively, I could have chosen a PLL multiplication of 3, and not divide the HSE frequency by 2.

Here is the main program that does these steps. After setting the clock, the SystemCoreClock is calculated inside the SystemCoreClockUpdate() function from the system_stm32f1xx.h header file. The system clock is displayed as a binary number on a LED array, which is connected to 10 GPIO pins.

#include "stm32f1xx.h"
#include "system_stm32f1xx.h"
#include <stdint.h>

void wait(uint32_t delta)
{
 volatile uint32_t i;
 for (i = delta; i; --i) ;
}

// function to display up to 10 bit integer value 
//   in binary represenation on LED string
void display(uint16_t n);

// configure GPIO registers 
void configure_GPIO();

int main()
{
 configure_GPIO();
 
 // clock setup 
 // register description on page 133 of reference manual
 
 // we can set the PLL entry clock source because PLL is off 
 //   (this doesn't work if PLL is on)
 // HSE as PLL source if bit is set, otherwise PLL source is HSI
 RCC->CFGR   |= RCC_CFGR_PLLSRC;            
 // activate HSE oscillator
 RCC->CR     |= RCC_CR_HSEON;     
 // PLL multiplies by factor of 6          
 RCC->CFGR   |= RCC_CFGR_PLLMULL6;
 // HSE crystal frequency divided by 2  before entering PLL
 RCC->CFGR   |= RCC_CFGR_PLLXTPRE_HSE_DIV2; 
 
 RCC->CR     |= RCC_CR_PLLON;        // activate PLL
 while (!(RCC_CR_PLLRDY & RCC->CR)); // wait until PLL is locked
 
 
 RCC->CFGR   |= RCC_CFGR_SW_PLL;   // use PLL as system clock
 //RCC->CFGR   |= RCC_CFGR_SW_HSE; // this would use ext. crystal 
                                   // directly (without PLL)
 
 // Use this function from the file system_stm32f1xx.h to
 // compute the system core clock frequency based on 
 // register settings and display the number on the LED string.
 SystemCoreClockUpdate();
 display(SystemCoreClock/1000000);
 
 // blink LED
 for(uint16_t i = 1;;)
 {
  GPIOC->BSRR |= GPIO_BSRR_BS4;  // set pin4 (LED on)
  wait(1000000);
  GPIOC->BRR |= GPIO_BRR_BR4;    // reset pin4 (LED off)
  wait(1000000);
 }
}


The makefile is the same as in post #1, only the CFLAGS have an additional entry, namely

-DHSE_VALUE=16000000UL

This is equivalent of writing #define HSE_VALUE=16000000UL in every source file. It tells the software which quartz frequency is physically present. An 8 MHz or 12 MHz quartz would also work, but the numbers in the clock setup have to be updated accordingly.

After compiling and flashing, the LEDs indicate that the SystemCoreClock/1000000 is 0b0000110000 = 48.


Outlook

For some reason, I could not go to the specified 72 MHz frequency with that method. Probably, I'm missing something. In the next post, I will use the STM32 hardware abstraction layer (HAL) library to do the same thing.

Appendix

For completeness, here are the functions for the GPIO configuration and the LED output:
// function to display up to 10 bit integer value 
//   in binary represenation on LED string
void display(uint16_t n)
{
 if (n & (((uint16_t)1)<<0))  GPIOB->BSRR |= GPIO_BSRR_BS12;
 else                         GPIOB->BRR  |= GPIO_BRR_BR12;

 if (n & (((uint16_t)1)<<1))  GPIOB->BSRR |= GPIO_BSRR_BS13;
 else                         GPIOB->BRR  |= GPIO_BRR_BR13;

 if (n & (((uint16_t)1)<<2))  GPIOB->BSRR |= GPIO_BSRR_BS14;
 else                         GPIOB->BRR  |= GPIO_BRR_BR14;

 if (n & (((uint16_t)1)<<3))  GPIOB->BSRR |= GPIO_BSRR_BS15;
 else                         GPIOB->BRR  |= GPIO_BRR_BR15;

 if (n & (((uint16_t)1)<<4))  GPIOC->BSRR |= GPIO_BSRR_BS6;
 else                         GPIOC->BRR  |= GPIO_BRR_BR6;
                                    
 if (n & (((uint16_t)1)<<5))  GPIOC->BSRR |= GPIO_BSRR_BS7;
 else                         GPIOC->BRR  |= GPIO_BRR_BR7;
                                    
 if (n & (((uint16_t)1)<<6))  GPIOC->BSRR |= GPIO_BSRR_BS8;
 else                         GPIOC->BRR  |= GPIO_BRR_BR8;
                                    
 if (n & (((uint16_t)1)<<7))  GPIOC->BSRR |= GPIO_BSRR_BS9;
 else                         GPIOC->BRR  |= GPIO_BRR_BR9;
                                    
 if (n & (((uint16_t)1)<<8))  GPIOA->BSRR |= GPIO_BSRR_BS8;
 else                         GPIOA->BRR  |= GPIO_BRR_BR8;
                                    
 if (n & (((uint16_t)1)<<9))  GPIOA->BSRR |= GPIO_BSRR_BS9;
 else                         GPIOA->BRR  |= GPIO_BRR_BR9;
}

void configure_GPIO()
{
 // Enbale GPIOC (A,B, and C)
 RCC->APB2ENR   |= RCC_APB2ENR_IOPAEN;
 RCC->APB2ENR   |= RCC_APB2ENR_IOPBEN;
 RCC->APB2ENR   |= RCC_APB2ENR_IOPCEN;
 
 // Configure GPIOC pin 4 by setting bits in the lower half 
 // of the port configuration register CRL
 // (reset value of GPIOC->CRL = 0x44444444)
 GPIOC->CRL |=   GPIO_CRL_MODE4; // MODE4 = 11 (50 MHz = fast mode)
 GPIOC->CRL &= ~(GPIO_CRL_CNF4); // CNF4  = 00 (Push-Pull mode)
 
 GPIOC->CRL |=   GPIO_CRL_MODE6; // same for other GPIOs
 GPIOC->CRL &= ~(GPIO_CRL_CNF6); 
 GPIOC->CRL |=   GPIO_CRL_MODE7; 
 GPIOC->CRL &= ~(GPIO_CRL_CNF7); 
 GPIOC->CRH |=   GPIO_CRH_MODE8; 
 GPIOC->CRH &= ~(GPIO_CRH_CNF8); 
 GPIOC->CRH |=   GPIO_CRH_MODE9; 
 GPIOC->CRH &= ~(GPIO_CRH_CNF9); 
 GPIOB->CRH |=   GPIO_CRH_MODE12; 
 GPIOB->CRH &= ~(GPIO_CRH_CNF12); 
 GPIOB->CRH |=   GPIO_CRH_MODE13; 
 GPIOB->CRH &= ~(GPIO_CRH_CNF13); 
 GPIOB->CRH |=   GPIO_CRH_MODE14; 
 GPIOB->CRH &= ~(GPIO_CRH_CNF14); 
 GPIOB->CRH |=   GPIO_CRH_MODE15; 
 GPIOB->CRH &= ~(GPIO_CRH_CNF15); 
 GPIOA->CRH |=   GPIO_CRH_MODE8; 
 GPIOA->CRH &= ~(GPIO_CRH_CNF8); 
 GPIOA->CRH |=   GPIO_CRH_MODE9; 
 GPIOA->CRH &= ~(GPIO_CRH_CNF9); 
}

Next Part

Next post will show how to use the Hardware abstraction layer from ST-Microelectronics to set the clock speed.