Sonntag, 4. September 2016

STM32 microcontroller #4: HAL ADC test

Setup

I will present the code to read analog values from a pin on the STM32 and my setup to test the code. I'll use the most simple program that I could write to do the following: reading a single value from the ADC peripheral and display it on 12 LEDs (it is a 12-bit value).
There are more sophisticated methods of using the ADC, e.g. direct memory access (DMA), and I plan to use these later. First, a picture of the setup

Sampling frequency

One interesting aspect is the sampling frequency of the setup. I believe that one can calculate it based on information that is available in the datasheet, but I'm too lazy to do that now and will just measure it roughly: after each sample, a LED is toggled (too fast for the human eye). On the oscilloscope, it is possible to see the toggling frequency. The timescale in the picture is 10 us / div, so I have ~25 us / sample, which corresponds to ~40 kHz sampling rate.

Firmware

The firmware code is based on the STM32F103RB-Nucleo examples that are part of the STM32Cube_FW_F1_V1.3.0 package from where I copy/pasted it together. I'm running the chip with 72 MHz, generated from the 16MHz crystal on my test board. The code is all in the following main.c. It compiles with the makefile shown in my previous post.


/**
  ******************************************************************************
  * 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_DIV2;
 clkinitstruct.APB1CLKDivider = RCC_HCLK_DIV1;  
 HAL_RCC_ClockConfig(&clkinitstruct, FLASH_LATENCY_2);
}

void ConfigureADC(ADC_HandleTypeDef *AdcHandle)
{
    GPIO_InitTypeDef gpioInit;
 
    __HAL_RCC_GPIOC_CLK_ENABLE();
    __HAL_RCC_ADC1_CLK_ENABLE();

    gpioInit.Pin = GPIO_PIN_1;
    gpioInit.Mode = GPIO_MODE_ANALOG;
    gpioInit.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOC, &gpioInit);
 
 
 
    AdcHandle->Instance = ADC1;
 
    AdcHandle->Init.ScanConvMode = DISABLE;
    AdcHandle->Init.ContinuousConvMode = ENABLE;
    AdcHandle->Init.DiscontinuousConvMode = DISABLE;
    AdcHandle->Init.NbrOfDiscConversion = 0;
    AdcHandle->Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T1_CC1;
    AdcHandle->Init.DataAlign = ADC_DATAALIGN_RIGHT;
    AdcHandle->Init.NbrOfConversion = 1;
 
    HAL_ADC_Init(AdcHandle);
 
    ADC_ChannelConfTypeDef adcChannel;
    adcChannel.Channel = ADC_CHANNEL_1;
    adcChannel.Rank = 1;
    adcChannel.SamplingTime = ADC_SAMPLETIME_7CYCLES_5;
    //adcChannel.Offset = 0;
 
    if (HAL_ADC_ConfigChannel(AdcHandle, &adcChannel) != HAL_OK)
    {
        //asm("bkpt 255");
    }
}


void display(uint32_t value)
{
 if (value & (1<<0))  HAL_GPIO_WritePin(GPIOB,GPIO_PIN_12,GPIO_PIN_SET);
 else                 HAL_GPIO_WritePin(GPIOB,GPIO_PIN_12,GPIO_PIN_RESET); 
 if (value & (1<<1))  HAL_GPIO_WritePin(GPIOB,GPIO_PIN_13,GPIO_PIN_SET);
 else                 HAL_GPIO_WritePin(GPIOB,GPIO_PIN_13,GPIO_PIN_RESET); 
 if (value & (1<<2))  HAL_GPIO_WritePin(GPIOB,GPIO_PIN_14,GPIO_PIN_SET);
 else                 HAL_GPIO_WritePin(GPIOB,GPIO_PIN_14,GPIO_PIN_RESET); 
 if (value & (1<<3))  HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_SET);
 else                 HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_RESET); 
 if (value & (1<<4))  HAL_GPIO_WritePin(GPIOC,GPIO_PIN_6,GPIO_PIN_SET);
 else                 HAL_GPIO_WritePin(GPIOC,GPIO_PIN_6,GPIO_PIN_RESET); 
 if (value & (1<<5))  HAL_GPIO_WritePin(GPIOC,GPIO_PIN_7,GPIO_PIN_SET);
 else                 HAL_GPIO_WritePin(GPIOC,GPIO_PIN_7,GPIO_PIN_RESET); 
 if (value & (1<<6))  HAL_GPIO_WritePin(GPIOC,GPIO_PIN_8,GPIO_PIN_SET);
 else                 HAL_GPIO_WritePin(GPIOC,GPIO_PIN_8,GPIO_PIN_RESET); 
 if (value & (1<<7))  HAL_GPIO_WritePin(GPIOC,GPIO_PIN_9,GPIO_PIN_SET);
 else                 HAL_GPIO_WritePin(GPIOC,GPIO_PIN_9,GPIO_PIN_RESET); 
 if (value & (1<<8))  HAL_GPIO_WritePin(GPIOA,GPIO_PIN_8,GPIO_PIN_SET);
 else                 HAL_GPIO_WritePin(GPIOA,GPIO_PIN_8,GPIO_PIN_RESET); 
 if (value & (1<<9))  HAL_GPIO_WritePin(GPIOA,GPIO_PIN_9,GPIO_PIN_SET);
 else                 HAL_GPIO_WritePin(GPIOA,GPIO_PIN_9,GPIO_PIN_RESET); 
 if (value & (1<<10)) HAL_GPIO_WritePin(GPIOA,GPIO_PIN_10,GPIO_PIN_SET);
 else                 HAL_GPIO_WritePin(GPIOA,GPIO_PIN_10,GPIO_PIN_RESET); 
 if (value & (1<<11)) HAL_GPIO_WritePin(GPIOA,GPIO_PIN_11,GPIO_PIN_SET);
 else                 HAL_GPIO_WritePin(GPIOA,GPIO_PIN_11,GPIO_PIN_RESET); 
}

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_GPIOA_CLK_ENABLE();
 __HAL_RCC_GPIOB_CLK_ENABLE();
 __HAL_RCC_GPIOC_CLK_ENABLE();
 
 // -2- Configure IO in output push-pull mode to drive external LEDs
 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 | GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9;
 HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
 
 GPIO_InitStruct.Pin   = GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15;
 HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
 
 GPIO_InitStruct.Pin   = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11;
 HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

 // Configure the ADC peripheral
 ADC_HandleTypeDef    AdcHandle;
 ConfigureADC(&AdcHandle);

 for (;;)
 {
  // ADC measurment & display value
  HAL_ADC_Start(&AdcHandle);
  HAL_ADC_PollForConversion(&AdcHandle, 1000); // 1000 is timeout in miliseconds
  display(HAL_ADC_GetValue(&AdcHandle));
  HAL_ADC_Stop(&AdcHandle);

  // flash an additional LED to measure the sampling rate
  HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_4);
 }
}

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.

Samstag, 19. März 2016

STM32 microcontroller #1: toolchain & LED blink

Introduction

I wanted to start playing around with an ARM microcontroller. After some research I decided to buy the STM32F103RBT6 from STMicroelectronics. It is fast and has lots of peripherals, but is still small enough to be put an adapter PCB for use on a bread board. A popular first step in using a new microcontroller is to get a blinking LED. I'll show the essential steps to achieve this on a STM32F103RBT6 with a free and simple Linux toolchain. It is also important to me to program a bare chip (with no bootloader).

A lot of information about the device is available on the STMicroelectronics web page. I'll refer here to the Reference Manual and the application note AN2586 on that page.
In addition I got valuable information and hints from the following sources:

I'm thankful to all people, providing all that helpful content. In this post, I try to summarize the most useful information (based on my personal judgement).


Toolchain Installation

The toolchain is the set of programs that is needed to compile the program code into an executable file that can be copied on the target chip. On my Arch Linux I installed the following packages in order to get the gcc compiler for ARM processors:
  • arm-none-eabi-binutils
  • arm-none-eabi-gcc
  • arm-none-eabi-gdb
  • arm-none-eabi-newlib
That gives me a compiler and all related tools. But I need more. A chip like the STM32F103RBT6 is rather complex. It needs some startup code before the main() function is entered. And in order to control the peripherals (ADC, GPIO, ...), one needs all the addresses of the registers to control them. In addition a linker script is needed to put the code together into a program that will run on the target device. All these things are provided on the STMicroelectronics website. The package is called STM32Cube. It is available for different STM32 devices (F1, F2, F3,...). I need the package STM32Cube F1 (link to .zip file is on the bottom of that page), because I use STM32F103RBT6. 

I also need a device that can be plugged into the USB-port of my PC and copies the program on the chip. I bought a ST-Link V2 (a cheap, ebay clone from China) for that purpose. That device can be controlled with a program called Open On-Chip Debugger. On Arch Linux, I got this program after installing the package
  • openocd

The PCB

Because I bought a bare chip, I need a PCB to put it on.
A thing of beauty
I want a PCB that can be plugged onto a bread board (as I saw here), but I wanted to add the decoupling capacitors on the PCB, a programming socket and a mini-USB connector in case I later want to play with the USB interface of the controller.
During the board design I followed the application note AN2586. I used KiCAD to draw schematic and board layout, and produced it with the toner transfer method.
Front side of the board with a blinking LED. It is not fully populated yet (missing quarz and USB) but works already

Back side of the board with all the decoupling capacitors.

Blinking LED Program

I created a project directory and copied the following files from the STM32Cube package:
  • STM32Cube_FW_F1_V1.3.0/Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/gcc/startup_stm32f103x6.s (startup code written in assembly language. I need that file, because I have STM32F103RBT6)
  • STM32Cube_FW_F1_V1.3.0/Middlewares/Third_Party/FreeRTOS/Source/include/stdint.readme -> stdint.h (rename the file to stdint.h)
  • STM32Cube_FW_F1_V1.3.0/Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/gcc/linker/STM32F103X6_FLASH.ld (the linker script, There are '0' characters in some lines that I needed to remove)
  • STM32Cube_FW_F1_V1.3.0/Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/system_stm32f1xx.c (more startup code written in C, which is called from the assembly startup code)
  • STM32Cube_FW_F1_V1.3.0/Drivers/CMSIS/Device/ST/STM32F1xx/Include/system_stm32f1xx.h (header file for the startup code)
Then I created a main.c file that contains the program code. Basically, one has to set a few bits in the correct registers to make the chip do what I want. For the blinking LED, I need to drive the general purpose input output (GPIO) pins of the device. The relevant information for the GPIO is found on pages 111-113 (important parts are marked in red)
and page 170 of the Reference Manual:
The program code is using the names for the registers and the numbers, such as GPIO_CRL_MODE4 instead of the corresponding hex number 0x00030000. They are provided after including the fille stm32f1xx.h

#include "stm32f1xx.h"

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

int main()
{
 // Enbale GPIOC
 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)
 
 for (;;)
 {
  GPIOC->BSRR |= GPIO_BSRR_BS4;  // set pin4 (LED on)
  wait(200000);
  GPIOC->BRR |= GPIO_BRR_BR4;    // reset pin4 (LED off)
  wait(200000);
 }
}

Finally, a makefile is needed to compile everything. The CFLAGS contain include paths to the STM32Cube package. In case you have put it somewhere else, that path has to be changed accordingly. The rest of the CFLAGS variable specifies the processor core (cortex-m3) and the device type (STM32F103x6). With -mthumb the compiler uses a reduced ARM instruction set to save memory space, more info here.
Note, that this makefile is written in a simple way in order to make it easy to see the individual commands (no use of default rules). It compiles the main program and the startup (C code and assembly code), then combines everything into main.elf using the linker script provided in the STM32Cube package. Finally, it converts the program to Intel hex format.


CFLAGS = -I ../STM32Cube_FW_F1_V1.3.0/Drivers/CMSIS/Device/ST/STM32F1xx/Include \
         -I ../STM32Cube_FW_F1_V1.3.0/Drivers/CMSIS/Include \
         -Wall -mcpu=cortex-m3 -mlittle-endian -mthumb  -DSTM32F103x6 -Os 
 
main.hex: main.elf
 arm-none-eabi-objcopy -Oihex main.elf main.hex

main.elf: system.o main.o startup.o
 arm-none-eabi-gcc $(CFLAGS)  -T STM32F103X6_FLASH.ld \
 -Wl,--gc-sections system.o main.o startup.o -o main.elf

startup.o: startup_stm32f103x6.s
 arm-none-eabi-gcc $(CFLAGS)  -c startup_stm32f103x6.s -o startup.o

system.o: system_stm32f1xx.c
 arm-none-eabi-gcc $(CFLAGS)  -c system_stm32f1xx.c -o system.o
 
main.o: main.c
 arm-none-eabi-gcc $(CFLAGS)  -Os -c main.c -o main.o

flash: main.hex
 openocd -f /usr/share/openocd/scripts/interface/stlink-v2.cfg \
         -f /usr/share/openocd/scripts/target/stm32f1x.cfg     \
         -c "program main.hex verify reset exit"

clean:
 rm *.o main.elf

Writing to Flash

In order to copy the program (main.hex) to the flash memory on the chip, I use the ST-Link V2 mentioned above. The ST-Link is controlled by the openocd program called with the correct command line arguments. These arguments are two configuration file for the programmer and for the target device, and a list of commands to execute: program verify reset exit. The correct call can be seen in the makefile under the flash: target. Note that you might change the path to the interface and target configuration files. These files are installed together with the openocd program. The paths I've used here work for my Linux distribution.
The ST-Link device has to be connected with 5 cables to the chip
pin_on_STLink -> pin_on_STM32:
  • 2 cables with supply voltage: GND -> GND and 3.3V -> VDD
  • 2 cables for data transfer: SWDIO -> PA13  and SWCLK -> PA14
  • 1 cable for reset: RST ->NRST

After typing make flash in the terminal, the LED (connected to pin PC4 and through a resistor to GND) starts blinking. 

Next Part

Part 2, where I will set a higher clock speed