Getting Started with STM32 ARM Cortex-M0+

Created by Christopher Jordan - Denny, last modified on Aug 18, 2015

Introduction:

Document Purpose

The purpose of this guide is to get you started with the STM32 and its peripherals. In particular, this guide focuses on the STM32L053R8T6 used on the Nucleo for STM32L0 evaluation board. This guide is targeted for those with a basic understanding of micro-controllers and their operation.

Warning: Code and circuit schematics are provided for reference only

Note: The most updated code will be on GitHub. Please see this for complete code, all code snippets are code references to the GitHub version.

Device Description

ST has provided an affordable and flexible way of developing and prototyping using their STM32 line of micro-controllers. ST’s Nucleo line of evaluation boards is extremely cost effective in comparison with its competitors. You can use one of ST’s 32-bit micro-controllers with DSP capabilities for just about half the price of an Arduino Uno! The Nucleo boards also offer a flexible way of prototyping by using the Arduino form factor along with their own “Morpho Connectors.”This means that you can easily use the shields created for Arduino for prototyping and still have full access to all the micro-controller pins through the Morpho Connector. Also included on the Nucleo boards is an integrated ST-LINK/V2-1 debugger and programmer.

Hardware Used

The hardware that was used in development can be found below. If you would like to order the parts just click on the name to be directed to the Digi-Key part page.

Device Part # Price (USD) Description Picture
STMicroelectronics NUCLEO-L053R8 497-14710-ND 10.33 STM32 Micro-controller evaluation board
STMicroelectronics X-NUCLEO-IKS01A1 497-15062-ND 15.25 ST sensor evaluation board image
Adafruit Ultimate GPS Shield 1528-1045-ND 49.95 SD Card, GPS shield for arduino
XBee Pro S1 XBP24-AWI-001-ND 32.00 Wireless Transciever

Documentation:

Below I have listed the most useful datasheets I used when developing for the micro-controller. I have provided these for the purpose of quick access in hopes to minimize time searching for the correct Datasheet for the information you need.

STM32L053xx Nucleo Board Documents

This is a list of useful datasheets and documents for developing on the STM32L053R8.

Document Title Description
NUCLEO L053R8 Schematic Circuit Diagram for the Nucleo STM32L0 Evaluation board
STM32L053xx Technical Data General information, Characteristics and description of capabilities
STM32L053xx Reference Register Descriptions and functionality
STM32L053xx Getting Started Board Schematic, Pin location and descriptions
I2C Timing Configuration Describes Timing Configuration Tool

STM32L053xx Motion MEMS and environmental Sensor Expansion Board Documents

This is a list of useful datasheets and documents when using some of the sensors produced by ST.

Document Title Description
X Nucleo ISK01A1 User Manual Board user manual
X Nucleo ISK01A1 Schematic Board Schematic
Temperature Humidity Sensor Datasheet for temperature and humidity sensor
Pressure Sensor Datasheet for pressure sensor
Magnetometer Datasheet for magnetometer
Accelerometer and Gyroscope Datasheet for accelerometer and Gyro

Adafruit Ultimate GPS Logger Shield Documents

This is a list of useful datasheets and documents when using the Adafruit Ultimate GPS Shield.

Document Title Description
Adafruit GPS Shield Schematic Ultimate GPS Shield Schematic
FGPMMOPA6B GPS Module Data sheet Contains GPS basic data and also NMEA Message format
Format and Protocol Gives more details about NMEA Sentence format
PMTK Commands Contains PMTK commands sent to the GPS module

XBee Documents

Document Title Description
XBee Pro Manual Everything you need to know about the XBee!

Setup:

Board Setup

Warning: If the maximum current consumption of the NUCLEO and its extension boards exceeds 300mA, it is mandatory to power the NUCLEO using an external power supply connected to E5V or VIN.

Jumpers on the Nucleo board should be checked first for desired operation. This information can also be found in STM32L053xx Getting Started on page 8. Refer to page 16 for the power option setting with JP1 and JP5. The table below describes the Jumper configuration.

Jumper Name Jumper Location
JP1 No jumper
JP5 Connecting PWR and U5V
JP6 Add Jumper

Next you can connect a USB cable, LD1 and LD3 will turn red, meaning you are ready to program. If LD3 does not turn red you must refer to the warning above and read page 16.

Programs

There are many programs to choose from for interacting with the STM32L0 Nucleo Development board, in this case I will be using tools provided by Keil. Once directed to the keil page choose Keil MDK-ARM. The IDE provided by Keil is called µVision and will be the focus of the programming in this guide. At the time of this guide I used keil MDK-ARM version 5.15.

Drivers and Libraries

I would also like to mention the STM32CubeMX software by ST. This software is a GUI interface for C code initializations for the STM32 Series of micro-controllers. It is very helpful for setting up the clock,GPIO,ADC,UART or other peripherals. The reason I will not be using this software is because of the bloated drivers that are generated. It uses HAL(Hardware Abstraction) drivers which provide everything you need to start programming your board. So if you would like to continue using the free lite version of Keil microVision, I would recommend avoiding the HAL libraries.

If you are familiar with STM32 micro-controllers you might be wondering where are the standard peripheral libraries for the L0 series! I have some bad news for you, there aren’t any. The STM32CubeMX which generate the HAL libraries is your only option.

Setting Up Keil MDK-ARM and ST-Link

After signing up with keil and completing your download and installation of Keil a few steps must be completed.

  1. Within microVision: update the package installation for the STM32L0 Series (Package installer menu path: Project → Manage → Pack installer)
    a. ARM::CMSIS Version: 4.3.0 and 4.2.0
    b. KEIL::MDK-Middleware Version: 6.4.0 and 6.2.0
    c. KEIL:STM32L0xx_DFP Version: 1.3.0
    d. KEIL::STM32NUCLEO_BSP Version: 1.3.0
  2. ST-Link Installation
    a. If ST-Link did not install automatically you can always run the .bat file below yourself
    b. C:\keil_v5\ARM\STLink\USBDriver and run the .bat file called stlink_winusb_install.bat
  3. Options for Target setup
    a. Go to Projects → options for target
    b. Choose your device[Device Tab]
    image
    c. Ensure your clock is set to 32.0 MHz [Target Tab]
    image
    d. Check the box “Use memory layout from target dialog”[Linker Tab]
    image
    e. Use ST-Link Debugger and change settings to match picture[Debug Tab]
    image
    image
  4. That’s all, you should now be ready to program. In addition to the example code on this page, you can also download a couple examples in the package installer within µVision.

Example of Reset and Clock Control (RCC)

Initializing System Core Clock Walk-through

A good reference for all the registers and settings can be found in STM32L053R8 Reference Manual. In this part of the guide we will be setting up the System and Peripheral Clocks. For a visual aid in what we want our final system to look like I have used the STM32CubeMx software to show the settings used. Again this is a visual aid and was not used to generate the code for the clock configuration.

Warning: The USART1CLK and I2C1CLK are not necessarily correct. ONLY the path from HSI RC to blue peripheral clock boxes are correct

As you can see from the above diagram we will use the 16 MHz internal clock and scale it to become 32MHz. To be able to accomplish this we are first going to set the system clock to the internal High Speed Clock (HSI16), then set the flash and power settings then finally setup the PLL to utilize the HSIRC clock and scale it to 32MHz.

STEP1: Enable HSI16

All we have to do here is set HSI16ON bit in the RCC_CR register. Then just wait for the internal high speed clock to be ready by waiting for HSI16RDYF in the RCC_CR register.

Note: For some reason the device header doesn’t call the bits by the name on the datasheet, so the code is correct. Example, you would think HSI16ON would be RCC_CR_HSI16ON but differs to be RCC_CR_HSION.

/* Enable HSI */
  RCC->CR |= ((uint32_t)RCC_CR_HSION);
   
  /* Wait for HSI to be ready */
while ((RCC->CR & RCC_CR_HSIRDY) == 0){
      // Nop
  }

STEP2: Set HSI to System Clock

Now that we have the HSI enabled we can use it for our system clock by writing to the SW[1:0] bits in the RCC_CFGR register. Then wait for the system clock to be ready.

  /* Set HSI as the System Clock */
RCC->CFGR = RCC_CFGR_SW_HSI;
   
  /* Wait for HSI to be used for teh system clock */
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_HSI){
      // Nop
  }

STEP3: Setup FLASH and PWR

FLASH->ACR |= FLASH_ACR_PRFTEN;                          // Enable Prefetch Buffer
FLASH->ACR |= FLASH_ACR_LATENCY;                         // Flash 1 wait state
RCC->APB1ENR |= RCC_APB1ENR_PWREN;                       // Enable the PWR APB1 Clock
PWR->CR = PWR_CR_VOS_0;                                  // Select the Voltage Range 1 (1.8V)
while((PWR->CSR & PWR_CSR_VOSF) != 0);                   // Wait for Voltage Regulator Ready

STEP4: Setup PLL

We now need to configure PLL. The first step is to select the PLLSRC in the RCC_CFGR to be HSI, then set PLLMUL[3:0] to multiply by 4 and lastly set PLLDIV[1:0] to divide by 2.

/* PLLCLK = (HSI * 4)/2 = 32 MHz */
RCC->CFGR &= ~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLMUL | RCC_CFGR_PLLDIV);                /* Clear */
RCC->CFGR |=  (RCC_CFGR_PLLSRC_HSI | RCC_CFGR_PLLMUL4 | RCC_CFGR_PLLDIV2);         /* Set   */

STEP5: Setup Peripheral Clock Divisors

We can leave all the peripherals to have a Divisor of 1.

/* Peripheral Clock divisors */
RCC->CFGR |= RCC_CFGR_HPRE_DIV1;                         // HCLK = SYSCLK
RCC->CFGR |= RCC_CFGR_PPRE1_DIV1;                        // PCLK1 = HCLK
RCC->CFGR |= RCC_CFGR_PPRE2_DIV1;                        // PCLK2 = HCLK

STEP6: Set PLL as System Clock

We need to enable the PLL by writing to PLLON bit in the RCC_CR register. Once we wait for the PLL to be ready by checking the PLLRDY bit in the RCC_CR register we can set it as the system clock by writing to the SW[1:0] bits the appropriate value.

/* Enable PLL */
 RCC->CR &= ~RCC_CR_PLLON;       /* Disable PLL */
 RCC->CR |= RCC_CR_PLLON;        /* Enable PLL     */
    
   /* Wait until the PLL is ready */
 while((RCC->CR & RCC_CR_PLLRDY) == 0){
       //Nop
   }
    
   /* Select PLL as system Clock */
 RCC->CFGR &= ~RCC_CFGR_SW;            /* Clear */
 RCC->CFGR |=  RCC_CFGR_SW_PLL;    /* Set   */
    
   /* Wait for PLL to become system core clock */
 while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL){
       //Nop
   }

Initializing System Core Clock Code:

You can find the full code on GitHub. But you can get the gist of what you should do below.

Timing.c
/**
  \fn          void SystemCoreClockInit(void)
    \brief       SystemCoreClockConfigure: Uses HSI clock
*/
void SystemCoreClockInit(void){
  /* Enable HSI */
    RCC->CR |= ((uint32_t)RCC_CR_HSION);
     
    /* Wait for HSI to be ready */
  while ((RCC->CR & RCC_CR_HSIRDY) == 0){
        // Nop
    }
    /* Set HSI as the System Clock */
  RCC->CFGR = RCC_CFGR_SW_HSI;
     
    /* Wait for HSI to be used for teh system clock */
  while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_HSI){
        // Nop
    }
     
    FLASH->ACR |= FLASH_ACR_PRFTEN;                          // Enable Prefetch Buffer
  FLASH->ACR |= FLASH_ACR_LATENCY;                         // Flash 1 wait state
  RCC->APB1ENR |= RCC_APB1ENR_PWREN;                       // Enable the PWR APB1 Clock
  PWR->CR = PWR_CR_VOS_0;                                  // Select the Voltage Range 1 (1.8V)
  while((PWR->CSR & PWR_CSR_VOSF) != 0);                   // Wait for Voltage Regulator Ready
  /* PLLCLK = (HSI * 4)/2 = 32 MHz */
  RCC->CFGR &= ~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLMUL | RCC_CFGR_PLLDIV);                /* Clear */
  RCC->CFGR |=  (RCC_CFGR_PLLSRC_HSI | RCC_CFGR_PLLMUL4 | RCC_CFGR_PLLDIV2);    /* Set   */
     
    /* Peripheral Clock divisors */
  RCC->CFGR |= RCC_CFGR_HPRE_DIV1;                         // HCLK = SYSCLK
  RCC->CFGR |= RCC_CFGR_PPRE1_DIV1;                        // PCLK1 = HCLK
  RCC->CFGR |= RCC_CFGR_PPRE2_DIV1;                        // PCLK2 = HCLK
    /* Enable PLL */
  RCC->CR &= ~RCC_CR_PLLON;        /* Disable PLL */
  RCC->CR |= RCC_CR_PLLON;        /* Enable PLL     */
     
    /* Wait until the PLL is ready */
  while((RCC->CR & RCC_CR_PLLRDY) == 0){
        //Nop
    }
     
    /* Select PLL as system Clock */
  RCC->CFGR &= ~RCC_CFGR_SW;            /* Clear */
  RCC->CFGR |=  RCC_CFGR_SW_PLL;    /* Set   */
     
    /* Wait for PLL to become system core clock */
  while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL){
        //Nop
    }
}

Example of GPIO

Initializing GPIO Walk-through

A good reference for all the registers and settings can be found in STM32L053R8 Reference Manual. Lets use the green on-board LED for our example and blue user button by setting up PORTA pin 5 as an output and PORTC pin 13 as input. To verify the location of the LED and blue user button refer to NUCLEO L053R8 Schematic.

STEP1: Setup the Clock

This must be completed first, otherwise the registers will not be affected by changes made! RCC_IOPENR enables different I/O clocks, so we can write to IOPAEN bit and enable PORTA’ s clock.

RCC->IOPENR |=  (1UL << 0);        // Enable GPIOA clock

STEP2: Setup the mode (input/output etc…)

Since we are using the green LED as an example we want to set PORTA pin 5 as an output pin. In the picture below MODE0…MODE15 labeled in the diagram can be thought of as Pin0…Pin15. We can set pint 5 to output by writing the appropriate bit series to MODE5 bits in the GPIOx_MODER register.

GPIOA->MODER   &= ~((3ul << 2*pin));       // write 00 to pin location
GPIOA->MODER   |=  ((mode << 2*pin));      // Choose mode

STEP3: Setup the output type (Push Pull/Open drain)

If you want you do not have to edit this register as its reset state is Push/Pull, which we want for our LED.

  1. Open Drain Mode: A “0” in the Output register activates the N-MOS whereas a “1 in the Output register leaves the port in Hi-Z (the P-MOS is never activated).
  2. Push/Pull: Push-pull mode: A “0” in the Output register activates the N-MOS whereas a “1” in the Output register activates the P-MOS.

GPIOA->OTYPER  |= (0 << 5);        //Output type Push/Pull

STEP4: Setup Speed

I chose medium speed for the green LED.

  1. Very Low Speed: 400kHz
  2. Low Speed: 2MHz
  3. Medium Speed: 10MHz
  4. High Speed: 40MHz
GPIOC->OSPEEDR |=  ((3 << 2*5));   //medium speed

STEP5: Setup Pull Up/Pull Down register

If you want you do not have to edit this register as its reset state is Push/Pull, which we want for our LED. Please refer to Step 3 Figure 25 for a schematic of the GPIO pin and the pull up and pull down resistors.

GPIOC->PUPDR   |= (pupd << 2*5);   //No pull up pull down

This Same process can be followed for setting up the blue user button.

Initializing GPIO Code

You can find the full code on GitHub.

GPIO.c
/**
  \fn             void GPIO_Init(GPIO_TypeDef* GPIOx, struct GPIO_Parameters GPIO)
  \brief        Initialize GPIO
    \param        GPIO_TypeDef* GPIOx: Which port to initialize, i.e. GPIOA,GPIOB
    \param        struct GPIO_Parameters GPIO: Structure containing all GPIO parameters:
                        * Pin
                        *    Mode
                        *    Output Type
                        * Output Speed
                        * Pull up / Pull down
*/
void GPIO_Init(GPIO_TypeDef* GPIOx, struct GPIO_Parameters GPIO){
     
    /* Enable GPIO Clock depending on port */
    if(GPIOx == GPIOA) RCC->IOPENR |= RCC_IOPENR_GPIOAEN;        // Enable GPIOA clock
    if(GPIOx == GPIOB) RCC->IOPENR |= RCC_IOPENR_GPIOBEN;        // Enable GPIOB clock
    if(GPIOx == GPIOC) RCC->IOPENR |= RCC_IOPENR_GPIOCEN;        // Enable GPIOC clock
    if(GPIOx == GPIOD) RCC->IOPENR |= RCC_IOPENR_GPIODEN;        // Enbale GPIOD clock
     
    /* GPIO Mode Init */
  GPIOx->MODER   &= ~((3ul << 2*GPIO.Pin));               // write 00 to pin location
  GPIOx->MODER   |=  ((GPIO.Mode << 2*GPIO.Pin));        // Choose mode
     
    /* Output Type Init */
  GPIOx->OTYPER  &= ~((~(GPIO.OType) << GPIO.Pin));
     
    /* GPIO Speed Init */
  GPIOx->OSPEEDR |=  ((GPIO.Speed << 2*GPIO.Pin));
     
    /* GPIO PULLUP/PULLDOWN Init */
  GPIOx->PUPDR   |= (GPIO.PuPd << 2*GPIO.Pin);
}
 
/**
  \fn          void Button_Initialize (void)
  \brief       Initialize User Button
*/
void Button_Initialize(void){
     
    /* Set port parameters */
    struct GPIO_Parameters GPIO;
    GPIO.Pin    = Blue_Button;
    GPIO.Mode = Input;
    GPIO.OType = Push_Pull;
    GPIO.PuPd = No_PuPd;
    GPIO.Speed = Low_Speed;
     
    /* Initialize button */
    GPIO_Init(GPIOC,GPIO);
}
 
/**
  \fn          void LED_Init(void)
  \brief       Initialize LD2
*/
void LED_Init(void){
     
    /* Set port parameters */
    struct GPIO_Parameters GPIO;
    GPIO.Pin = Green_LED;
    GPIO.Mode = Output;
    GPIO.OType = Push_Pull;
    GPIO.PuPd = No_PuPd;
    GPIO.Speed = High_Speed;
     
    /* Initialize the LED */
    GPIO_Init(GPIOA,GPIO);
}
GPIO.h
struct GPIO_Parameters
{
    int Pin;
    int Mode;
    int Speed;
    int OType;
    int PuPd;
};
typedef enum Mode_Choices
{
    Input                     = 0,
    Output                    = 1,
    Alternate_Function        = 2,
    Analog_Mode               = 3
}Mode_Choices;
typedef enum OType_Choices
{
    Push_Pull     = 0,
    Open_Drain    = 1
}OType_Choices;
typedef enum Speed_Choices
{
    Very_Low_Speed        = 0,
    Low_Speed             = 1,
    Medium_Speed          = 2,
    High_Speed            = 3
}Speed_Choices;
typedef enum PuPd_Choices
{
    No_PuPd          = 0,
    Pull_Up          = 1,
    Pull_Down        = 2,
}PuPd_Choices;

Setting Alternate Function Walk-through

In many cases you may have to use a GPIO alternate function. In this walk-through I will show how to setup the I2C1_SCL and I2C1_SDA alternate functions on PORTB pin 8 and pin 9 respectively.

STEP1: Find which alternate function is correct

If we look below we can see that we want AF4 to set PB8 and PB9 to I2C1_SCL and I2C1_SDA respectively

STEP2: Write value to the AFRL and AFRH registers

int SCL = 8;                            //SCL pin on PORTB alt fnc 4
int SDA = 9;                            //SDA pin on PORTB alt fnc 4
 
/*Enable Clock for I2C*/
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
 
/* Enable GPIO Clock */
RCC->IOPENR |=  (1UL << 1);
 
/** GPIOB Setup
    *    Alternate Mode PORTB pin 8 and pin 9.............(1)
    *    Set alternate function 4 for both pin 8 an 9.....(2)
    */
    GPIOB->MODER = ~((~GPIOB->MODER) | ((1 << 2*SCL) + (1 << 2*SDA)));    /*(1)*/
    GPIOB->AFR[1] = 0x00000044;                                           /*(2)*/

Just a note about writing to AFR registers,

/**
  * Set AFRL to AF_Value on Pin...(1)
  * Set AFRH to AF_Value on Pin...(2)
*/
 
GPIOB->AFR[0] |= (AF_Value << 4*Pin)       /* (1) */
GPIOB->AFR[1] |= (AF_Value << 4*(Pin - 8)) /* (2) */

Example of ADC

All code can be found on GitHub.

Initializing ADC Walk-through

STEP1: Setup the Clock

RCC->APB2ENR |= (1UL << 9);

STEP2: Enable Built in Calibration

You must make sure that the ADC is not yet enabled, and if it is disable it. You can then set the ADCAL bit which will start the calibration. Then wait for the calibration to finish before clearing it by writing a 1 to EOCAL in the ISR register.

/* Calibration Setup */
if((ADC1->CR & ADC_CR_ADEN) != 0){
    ADC1->CR &= (uint32_t)(~ADC_CR_ADEN);
}
 
ADC1->CR |= ADC_CR_ADCAL;
while((ADC1->ISR & ADC_ISR_EOCAL) == 0);
ADC1->ISR |= ADC_ISR_EOCAL;

STEP3: Enable ADC

Enable the ADC by writing to the ADEN bit in the CR register. You should then make sure the ADC is ready by checking the ADRDY bit in the ISR register.

/* Enable ADC */
ADC1->CR |= (1UL << 0);
 
/* Wait for ISR bit to set */
while((ADC1->ISR & 1) == 0);

STEP4: Initialize ADC Pin

ADC1->CHSELR |= (1UL << pin);

STEP5: Configure Resolution, Alignment and mode

/* Enable Continuous mode */
ADC1->CFGR1 |= (1UL << 13);
 
/* Right Aligned data in DR register */
ADC1->CFGR1 |= (0UL << 5);
 
/* 12-Bit Resolution */
ADC1->CFGR1 |= (0Ul << 3);

STEP6: Start the conversion and grab the data

/* Start Conversion */
ADC1->CR |= (1UL << 2);
 
/* Grab the last 12-Bit Data part */
ADC_Conversion = ADC1->DR & 0x00000FFF;

Example of I2C

Initializing I2C Walk-through

A good reference for all the registers and settings can be found in STM32L053R8 Reference Manual. It is also useful to look at the STM32L053xx Tech Data for finding alternate functions for pins. This walk-through assumes you have already setup your alternate function. If you haven’t please refer to Setting Alternate Function Walk-through. In this walk-through we will be setting up I2C1_SCL on PB8 and I2C1_SDA on PB9 respectively.

STEP1: Setup the Clock

You must setup 2 clocks, 1: for enabling the GPIO clock and 2: for enabling the I2C clock

/*Enable Clock for I2C*/
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
 
/* Enable GPIO Clock */
RCC->IOPENR |=  (1UL << 1);

STEP2: Setup I2C Timing Configuration

The easiest way to set this register is to use a tool made by ST called I2C Timing Configuration. It is a tool that runs in Excel and will configure the clock based on your settings.

/** GPIOB Setup
    *    Standard Mode @100kHz with I2CCLK = 16MHz, rise time = 100ns, fall time = 10ns.(1)
    */
    I2C1->TIMINGR = (uint32_t)0x00503D5A;    /*(1)*/

STEP3: Enable I2C

The last step is to enable I2C. This must be the last step in I2C initialization.

I2C1->CR1 |= I2C_CR1_PE;     //Enable I2C1 peripheral

Initializing I2C Code

You can find the full code on GitHub.

Note that I have commented out the Interrupt enable for I2C, I decided on a polling method rather than the interrupt for communicating with the various sensors.

I2C.c
/**
  \fn         void I2C_Init(void)
  \brief            I2C initialization
                            *PORTB-8: SCL
                            *PORTB-9: SDA
                            *Digital Noise filter with supression of 1 I2Cclk
                            *fast Mode @400kHz with I2CCLK = 16MHz, rise time = 100ns, fall time = 10ns
*/
void I2C_Init(void){
     
    int SCL = 8;                            //SCL pin on PORTB alt fnc 4
    int SDA = 9;                            //SDA pin on PORTB alt fnc 4
     
    /*Enable Clock for I2C*/
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
     
    /* Enable GPIO Clock */
    RCC->IOPENR |=  (1UL << 1);
     
    /* Don't forget to also enable which interrupts you want in CR1 */
    //interrupt init
//    NVIC_EnableIRQ(I2C1_IRQn);
//    NVIC_SetPriority(I2C1_IRQn,0);
     
/** GPIOB Setup
    *    Digital Noise filter with supression of 1 I2Cclk.(1)
    *    Alternate Mode PORTB pin 8 and pin 9.............(2)
    *    Set alternate function 4 for both pin 8 an 9.....(3)
    */
    I2C1->CR1 |= (1<<8);                                                  /*(1)*/
    GPIOB->MODER = ~((~GPIOB->MODER) | ((1 << 2*SCL) + (1 << 2*SDA)));    /*(2)*/
    GPIOB->AFR[1] = 0x00000044;                                           /*(3)*/
     
/** GPIOB Setup
    *    Standard Mode @100kHz with I2CCLK = 16MHz, rise time = 100ns, fall time = 10ns.(1)
    *    Enable I2C1 peripheral.........................................................(2)
    */
    I2C1->TIMINGR = (uint32_t)0x00503D5A;    /*(1)*/
    I2C1->CR1 |= I2C_CR1_PE;                 /*(2)*/
}

Communicating with HTS221 Temperature/Humidity Sensor Walk-through

A good reference for all the registers and settings can be found in Temperature Humidity Sensor datasheet. In this section of the guide I will describe how to communicate, set up the HTS221 temperature and humidity sensor and read both the temperature and humidity data.

STEP1:Communication Sequence

The following descriptions of the I2C communication is written in the perspective of the master, in my case the STM32L053R8.

The writing sequence can be better described as:

  1. Send Start sequence

  2. Send Slave Address + read/write (Read = 1,Write=0)

  3. Receive Slave Acknowledge (Should be a 0)

  4. Send Sub address, which register you want to write to

  5. Receive Slave Acknowledge (Should be a 0)

  6. Send Data to be written to the register

  7. Receive Slave Acknowledge (Should be a 0)

  8. Send Stop Sequence

The reading sequence can be better described as:

  1. Send Start sequence

  2. Send Slave Address + read/write (Read = 1,Write=0)

  3. Receive Slave Acknowledge (Should be a 0)

  4. Send Subaddress, which register you want to write to

  5. Receive Slave Acknowledge (Should be a 0)

  6. Send Start sequence again

  7. Send Slave Address + read/write (Read = 1,Write=0)

  8. Receive Slave Acknowledge (Should be a 0)

  9. Receive Data from SUB address

  10. Send No Master Acknowledge

  11. Send Stop Sequence

The following communication sequence shows a read on I2C using the STM32L053R8 as the master and the HTS221 as the slave.

Below is an example of what steps 1-3 for reading a register would look like.

image

This sequence can be broken up:

Start Condition Slave Address Read/Write Slave Acknowledge
High to low transition 1011111 0 0

Note: The slave is sending a NACK, so really its saying its Not Not Acknowledged.

Below is an example of steps 4 and 5 .

image

This sequence can be broken up:

Sub Address Slave Acknowledge
00001111 0

Below is an example of receiving data from the slave steps 6-10 .

image

This sequence can be broken up:

Restart Condition Slave Address Read/Write Slave Acknowledge Sub Address Nack (Master)
High to low transition 1011111 1 0 10111100 1

Below is an example of the stop sequence step 11 .

image

STEP2:Communication Sequence Code

You can find the full code on GitHub.

I2C.c
/**
  \fn                    uint32_t I2C_Read_Reg(uint32_t Register)
  \brief            Reads a register, the entire sequence to read (at least for HTS221)
    \param            uint32_t Device: The slave address of the device
    \param            uint32_t Register: The Register to read from
    \returns        uint32_t I2C1_RX_Data: The data read
*/
uint32_t I2C_Read_Reg(uint32_t Device,uint32_t Register){
 
    //Reset CR2 Register
    I2C1->CR2 = 0x00000000;
 
    //Check to see if the bus is busy
    while((I2C1->ISR & I2C_ISR_BUSY) == I2C_ISR_BUSY);
 
    //Set CR2 for 1-byte transfer for Device
    I2C1->CR2 |=(1UL<<16) | (Device<<1);
 
    //Start communication
    I2C1->CR2 |= I2C_CR2_START;
 
    //Check Tx empty before writing to it
    if((I2C1->ISR & I2C_ISR_TXE) == (I2C_ISR_TXE)){
        I2C1->TXDR = Register;
    }
 
    //Wait for transfer to complete
    while((I2C1->ISR & I2C_ISR_TC) == 0);
    //Clear CR2 for new configuration
    I2C1->CR2 = 0x00000000;
 
    //Set CR2 for 1-byte transfer, in read mode for Device
    I2C1->CR2 |= (1UL<<16) | I2C_CR2_RD_WRN | (Device<<1);
 
    //Start communication
    I2C1->CR2 |= I2C_CR2_START;
 
    //Wait for transfer to complete
    while((I2C1->ISR & I2C_ISR_TC) == 0);
 
    //Send Stop Condition
    I2C1->CR2 |= I2C_CR2_STOP;
 
    //Check to see if the bus is busy
    while((I2C1->ISR & I2C_ISR_BUSY) == I2C_ISR_BUSY);
    //Clear Stop bit flag
    I2C1->ICR |= I2C_ICR_STOPCF;
 
    return(I2C1_RX_Data);
}
/**
  \fn                    uint32_t I2C_Read_Reg(uint32_t Register)
  \brief            Reads a register, the entire sequence to read (at least for HTS221)
    \param            uint32_t Device: The slave address to written to
    \param            uint32_t Register: The register that you would like to write to
    \param            uint32_t Data: The data that you would like to write to the register
*/
void I2C_Write_Reg(uint32_t Device,uint32_t Register, uint32_t Data){
 
    //Reset CR2 Register
    I2C1->CR2 = 0x00000000;
 
    //Check to see if the bus is busy
    while((I2C1->ISR & I2C_ISR_BUSY) == I2C_ISR_BUSY);
 
    //Set CR2 for 2-Byte Transfer, for Device
    I2C1->CR2 |= (2UL<<16) | (Device<<1);
 
    //Start communication
    I2C1->CR2 |= I2C_CR2_START;
 
    //Check Tx empty before writing to it
    if((I2C1->ISR & I2C_ISR_TXE) == (I2C_ISR_TXE)){
        I2C1->TXDR = Register;
    }
 
    //Wait for TX Register to clear
    while((I2C1->ISR & I2C_ISR_TXE) == 0);
 
    //Check Tx empty before writing to it
    if((I2C1->ISR & I2C_ISR_TXE) == I2C_ISR_TXE){
        I2C1->TXDR = Data;
    }
 
    //Wait for transfer to complete
    while((I2C1->ISR & I2C_ISR_TC) == 0);
 
    //Send Stop Condition
    I2C1->CR2 |= I2C_CR2_STOP;   
 
    //Check to see if the bus is busy
    while((I2C1->ISR & I2C_ISR_BUSY) == I2C_ISR_BUSY);
 
    //Clear Stop bit flag
    I2C1->ICR |= I2C_ICR_STOPCF;
}

STEP3: Configure HTS221

For this configuration I have the HTS221 in one-shot mode, which means I just read one set of data at a time. There are two registers that we have to be concerned with for this case.


AV_Conf:

This register is a trade-off of accuracy for power consumption. The more accurate the more power consumed.

  • Number of averages for Temperatures set to 16
  • Number of averages for Humidity set to 32

CTRL_REG1:

It is important to remember to turn the device on, and then block the data update. The reason for blocking the data update is because the data is 16 bits in length but stored in 8 bit registers. By blocking the data update you must read both the Low and High Data registers before the device will update the value in the register. This prevents you from reading the low bits, the data being updated, and then reading the high bits, leaving you with two different numbers in one of the readings.

  • Write 1 to PD
  • Write 1 to BDU

Please view the full code on GitHub. Below is the gist of what you should do.

HTS221.c
/**
  \fn                    void HTS221_Init(void)
  \brief            Initialize the HTS221 and check device signature
    \returns        uint8_t Device_Found - Determines if the device was detected
*/
uint8_t HTS221_Init(void){
     
    //Local variables
    uint8_t Device_Found = 0;
    uint32_t AV_CONF_Init = 0x1B;        /*16 Temp (AVGT) and 32 Hum (AVGT)*/
 
    //Read data from register and check signature   
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_WHO_AM_I);
     
    //Check if device signature is correct
    if (I2C1->RXDR == HTS221_DEVICE_ID){
        Device_Found = 1;
    }
    else Device_Found = 0;
     
    /* Setup HTS221_AV_CONF Register */
    if(Device_Found){
        //Set to Default Configuration
        I2C_Write_Reg(HTS221_ADDRESS,HTS221_AV_CONF,AV_CONF_Init);
         
        //Activate and Block Data Update, this will ensure that both the higher and lower bits are read
        I2C_Write_Reg(HTS221_ADDRESS,HTS221_CTRL_REG1,(HTS221_CTRL_REG1_PD | HTS221_CTRL_REG1_BDU));
    }
     
    return(Device_Found);
}

STEP4: Read Temperature and Humidity Data

This is definitely the strangest and hardest part, mainly because the datasheet doesn’t really describe what you need to do. But don’t worry I have it all figured out based on the HAL drivers written for the ISK01A1 board. There are two critical equations you need to calculate the temperature and Humidity.

Temperature Interpolation:

Temperature(^\circ{}C) = T0\_DegC + \frac{(T\_OUT - T0\_OUT) * (T1\_DegC - T0\_DegC)}{T1\_OUT - T0\_OUT}

Humidity Interpolation:

Humidity(rH\%) = H0\_rH + \frac{(H\_OUT - H0\_T0\_OUT) * (H1\_rH - H0\_rH)}{H1\_T0\_OUT - H0\_T0\_OUT}

Look at the calibration registers and output registers to find all the values in the above equations.

Please view the full code on GitHub. Below is the gist of what you should do.

HTS221.c
/**
  \fn                    HTS221_Temp_Read(void)
  \brief            Reads the temperature from HTS221 in one-shot mode
    \returns        float Temperature_In_F: The temperature in Fahrenheit
*/
float HTS221_Temp_Read(void){
     
    /* Local Variables */
    uint8_t STATUS_REG = 0;
     
    //T0_degC and T1_degC
    uint16_t T0_degC_x8 = 0;
    uint16_t T1_degC_x8 = 0;
    uint16_t Msb_TO_T1_degC = 0;
    float T0_DegC = 0;
    float T1_DegC = 0;
     
    //T_OUT
    uint16_t T_OUT_L = 0;
    uint16_t T_OUT_H = 0;
    float T_OUT = 0;
     
    //T0_OUT and T1_OUT
    int16_t T0_OUT_L = 0;
    int16_t T0_OUT_H = 0;
    int16_t T1_OUT_L = 0;
    int16_t T1_OUT_H = 0;
    float T0_OUT = 0;
    float T1_OUT = 0;
     
    //Temperature Variables
    float Temperature_In_C = 0;
    float Temperature_In_F = 0;
     
    //Start a temperature conversion
    I2C_Write_Reg(HTS221_ADDRESS,HTS221_CTRL_REG2,HTS221_CTRL_REG2_ONE_SHOT);
     
    //Wait for Temperature data to be ready
    do{
        I2C_Read_Reg(HTS221_ADDRESS,HTS221_STATUS_REG);
        STATUS_REG = I2C1->RXDR;
    }while((STATUS_REG & HTS221_STATUS_REG_TDA) == 0);
     
    //Read Temperature Data and Calibration
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_TEMP_OUT_L);
    T_OUT_L = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_TEMP_OUT_H);
    T_OUT_H = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_TO_OUT_L);
    T0_OUT_L = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_T0_OUT_H);
    T0_OUT_H = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_T1_OUT_L);
    T1_OUT_L = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_T1_OUT_H);
    T1_OUT_H = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_T0_degC_x8);
    T0_degC_x8 = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_T1_degC_x8);
    T1_degC_x8 = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_T1_T0_Msb);
    Msb_TO_T1_degC = I2C1->RXDR;
     
    //Process Calibration Registers
    T0_DegC = ((float)(((Msb_TO_T1_degC & 0x3) << 8) | (T0_degC_x8))/8.0);
    T1_DegC = ((float)(((Msb_TO_T1_degC & 0xC) << 6) | (T1_degC_x8))/8.0); //Value in 3rd and 4th bit so only shift 6
    T0_OUT = (float)((T0_OUT_H << 8) | T0_OUT_L);
    T1_OUT = (float)((T1_OUT_H << 8) | T1_OUT_L);
    T_OUT = (float)((T_OUT_H << 8) | T_OUT_L);
     
    //Calculate Temperatuer using linear interpolation and convert to Fahrenheit
    Temperature_In_C = (float)(T0_DegC + ((T_OUT - T0_OUT)*(T1_DegC - T0_DegC))/(T1_OUT - T0_OUT));
    Temperature_In_F = (Temperature_In_C*(9.0/5.0)) +32.0;
     
    return(Temperature_In_F);
}
/**
  \fn                    float HTS221_Humidity_Read(void)
  \brief            Reads the Humidity from HTS221 in one-shot mode
    \returns        float Humidity_rH: The relative humidity %
*/
float HTS221_Humidity_Read(void){
     
    /* Local Variables */
    uint8_t STATUS_REG = 0;
     
    //H0_rH and H1_rH
    uint8_t H0_rH_x2 = 0;
    float H0_rH = 0;
    uint8_t H1_rH_x2 = 0;
    float H1_rH = 0;
    //H_OUT
    float H_OUT = 0;
    uint16_t H_OUT_L = 0;
    uint16_t H_OUT_H = 0;
    //H0_TO_OUT and H1_TO_OUT
    float H0_T0_OUT = 0;
    float H1_T0_OUT = 0;
    uint16_t H0_T0_OUT_L = 0;
    uint16_t H0_T0_OUT_H = 0;
    uint16_t H1_T0_OUT_L = 0;
    uint16_t H1_T0_OUT_H = 0;
    //Humidity Variables
    float Humidity_rH = 0;
     
    //Start a humidity conversion
    I2C_Write_Reg(HTS221_ADDRESS,HTS221_CTRL_REG2,HTS221_CTRL_REG2_ONE_SHOT);
     
    //Wait for Humidity data to be ready
    do{
        I2C_Read_Reg(HTS221_ADDRESS,HTS221_STATUS_REG);
        STATUS_REG = I2C1->RXDR;
    }while((STATUS_REG & HTS221_STATUS_REG_HDA) == 0);
     
    //Read Humidity data and Calibration
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_H0_rH_x2);
    H0_rH_x2 = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_H1_rH_x2);
    H1_rH_x2 = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_HUMIDITY_OUT_L);
    H_OUT_L = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_HUMIDITY_OUT_H);
    H_OUT_H = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_H0_T0_OUT_L);
    H0_T0_OUT_L = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_H0_T0_OUT_H);
    H0_T0_OUT_H = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_H1_T0_OUT_L);
    H1_T0_OUT_L = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_H1_T0_OUT_H);
    H1_T0_OUT_H = I2C1->RXDR;
     
    //Process Calibration Registers
    H0_rH = (float)H0_rH_x2/2.0;
    H1_rH = (float)H1_rH_x2/2.0;
    H_OUT = (float)((H_OUT_H << 8) | H_OUT_L);
    H0_T0_OUT = (float)((H0_T0_OUT_H << 8) | H0_T0_OUT_L);
    H1_T0_OUT = (float)((H1_T0_OUT_H << 8) | H1_T0_OUT_L);
     
    //Calculate the relative Humidity using linear interpolation
    Humidity_rH = ( float )(((( H_OUT - H0_T0_OUT ) * ( H1_rH - H0_rH )) / ( H1_T0_OUT - H0_T0_OUT )) + H0_rH );
     
    return(Humidity_rH);
}

Communicating with LPS25HB Pressure Sensor Walk-through

A good reference for all the registers and settings can be found in Pressure Sensor datasheet. In this section of the guide I will describe how to communicate, set up the LPS25HB pressure sensor and read the pressure sensor data.

STEP1: Communication Sequence

The communication sequence is the same as the HTS221. If you haven’t read this section click here.

STEP2: Configure LPS25HB

Similar to the HTS221 we are going to use one-shot mode and read one data set at a time. We again need to power on the device, and block the data update so we aren’t getting conflicting data (because the data is 24 bits split up into 8 bit registers). All the code can be found on GitHub, but the gist of what you want to do is below.

/**
  \fn                    uint8_t LPS25HB_Init(void))
  \brief            Initializes the LPS25HB Pressure sensor
    \returns         uint8_t Device_Found:  1 - Device found, 0 - Device not found
*/
uint8_t LPS25HB_Init(void){
     
    uint8_t Device_Found = 0;
     
    //Read data from register and check signature   
    I2C_Read_Reg(LPS25HB_ADDRESS,LPS25HB_WHO_AM_I);
     
    //Check if device signature is correct
    if (I2C1->RXDR == LPS25HB_DEVICE_ID){
        Device_Found = 1;
    }
    else{
        Device_Found = 0;
    }
     
    if(Device_Found){
        //Power on the device and Block Data Update
        I2C_Write_Reg(LPS25HB_ADDRESS,LPS25HB_CTRL_REG1,(LPS25HB_CTRL_REG1_PD | LPS25HB_CTRL_REG1_BDU));
         
        //Configure the resolution for pressure for 16 internal averages
        I2C_Write_Reg(LPS25HB_ADDRESS,LPS25HB_RES_CONF,LPS25HB_RES_CONF_AVGP0);
    }
     
    return(Device_Found);
}

STEP3: Read Pressure Data

Luckily there is no linear interpolation involved in reading the pressure data. But there is still one operation to complete before getting the pressure reading.

image

According to the datasheet the pressure sensitivity is 1 bit/hPa, so we must apply the following equation to the 24 bit value.

LPS25HB\_Pressure(hPa) = \frac{Raw\_Pressure}{4096}

One other trick we must complete is to convert the two’s compliment 24 bit value into a 32 bit two’s compliment value. This can be done by checking the 24 bit values MSB and then extending the remaining 8 bits with the appropriate value.

//convert the 2's complement 24 bit to 2's complement 32 bit
    if (Raw_Pressure & 0x00800000){
            Raw_Pressure |= 0xFF000000;
    }

All the code can be found on GitHub, but the gist of what should be done can be found below.

LPS25HB.c
/**
  \fn                    void LPS25HB_Configuration(void)
  \brief            Prints important Configuration registers
    \returns        float LPS25HB_Pressure: pressure measured in mbar
*/
float LPS25HB_Pressure_Read(void){
     
    //Local Variables
    uint8_t PRESS_OUT_XL = 0;
    uint8_t PRESS_OUT_L = 0;
    uint8_t PRESS_OUT_H = 0;
    float Pressure = 0;
    int32_t Raw_Pressure = 0;
    uint8_t LPS25HB_STATUS = 0;
     
    //Start a temperature conversion
    I2C_Write_Reg(LPS25HB_ADDRESS,LPS25HB_CTRL_REG2,LPS25HB_CTRL_REG2_ONE_SHOT);
     
    //Wait for Temperature data to be ready
    do{
        I2C_Read_Reg(LPS25HB_ADDRESS,LPS25HB_STATUS_REG);
        LPS25HB_STATUS = I2C1->RXDR;
    }while((LPS25HB_STATUS & LPS25HB_STATUS_REG_PDA) == 0);
     
    //Read the pressure output registers
    I2C_Read_Reg(LPS25HB_ADDRESS,LPS25HB_PRESS_OUT_XL);
    PRESS_OUT_XL = I2C1->RXDR;
     
    I2C_Read_Reg(LPS25HB_ADDRESS,LPS25HB_PRESS_OUT_L);
    PRESS_OUT_L = I2C1->RXDR;
     
    I2C_Read_Reg(LPS25HB_ADDRESS,LPS25HB_PRESS_OUT_H);
    PRESS_OUT_H = I2C1->RXDR;
     
    //Read the reference Register
     
    /*    Combine pressure into 24 bit value
            PRESS_OUT_H is the High bits     23 - 16
            PRESS_OUT_L is the mid bits     15 - 8
            PRESS_OUT_XL is the lsb                7 - 0
    */
    Raw_Pressure = ((PRESS_OUT_H << 16) | (PRESS_OUT_L << 8) | (PRESS_OUT_XL));
     
    //convert the 2's complement 24 bit to 2's complement 32 bit
    if (Raw_Pressure & 0x00800000){
            Raw_Pressure |= 0xFF000000;
    }
     
    //Calculate Pressure in mbar
    Pressure = (float)Raw_Pressure/4096.0f;
     
    return(Pressure);
}

Converting Pressure to Altitude

One of the uses for the pressure sensor is to calculate the altitude. I have found a very useful article about the derivation of the correlation between altitude and pressure. You can download this article here, but if you just want the equation for finding the altitude here you go.

As you can see from the above equation, most of the values are constants. There is only one unknown for calculating altitude which is your pressure reading.

/**
  \fn                    ISK01A1_Get_Altitude(void)
  \brief            Calculates the altitude based on the pressure
    \returns        float Z: Altitude calculation in meters
*/
float ISK01A1_Get_Altitude(void){
     
    /* Calculation should be good up to 11km */
    /* Local Variables */
    const float T0 = 288.15;                            /* Temperatuer at zero altitude, ISA */
    float P = 0.0;                                                /* Measured Pressure                 */
    const float P0 = 101325.0;                        /* Pressure at zero altitude, ISA    */
    const float g = 9.80655;                            /* Acceleration due to gravity       */
    const float L = -0.0065;                            /* Lapse Rate, ISA                   */
    const float R = 287.053;                            /* Gas constant for air              */
     
    /* Read Pressure */
    P = LPS25HB_Pressure_Read()*100.0;            /* Convert mbar to Pa */
     
    /* Calculate Altitude in meters */
    ISK01A1.Altitude = (T0/L)*(pow((P/P0),((-L*R)/g))-1);
     
    return(ISK01A1.Altitude);
}

Communicating with LSM6DS0 Accelerometer/Gyroscope Sensor Walk-through

A good reference for all the registers and settings can be found in Accelerometer and Gyroscope datasheet. In this section of the guide I will describe how to communicate, set up the LSM6DS0 Accelerometer/Gyroscope sensor and read the Roll,Yaw,Pitch,Acceleration in X,Acceleration in Y, and Acceleration in Z.

STEP1: Communication Sequence

The communication sequence is the same as the HTS221. If you haven’t read this section click here.

STEP2: Configure LSM6DS0

This device is slightly different in comparison to the other sensors, because there is no one shot mode, and powering on the device is not similar to the other sensors. This device can be in a couple different states given by the diagram below.

I’ve decided that I want the Accelerometer and Gyro both in an on state, so we must write a value greater than 000(reset) to CTRL_REG1_G for its ODR_G[2:0] bits. By doing this we come out of power down mode and set an Output Data Rate. I decided to select 238Hz as my ODR, which means the device is in normal mode (not low powered) and consumes 4.3 mA of current.

Similar to the other sensors, we still want to write to the BDU bit in the CTRL_REG8 register (because the data is 16 bits split up into 8 bit registers).All the code can be found on GitHub, but the gist of what you want to do is below.

/**
  \fn                    uint8_t LSM6DS0_Init(void)
  \brief            Initialize LSM6DS0 Gyro and accelerometer
    \returns        uint8_t Device_Found: 1 - Device found, 0 - Device not found   
*/
uint8_t LSM6DS0_Init(void){
     
    //Global Variables
    uint8_t Device_Found = 0;
     
    //Read data from register and check signature   
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_WHO_AM_I);
     
    //Check if device signature is correct
    if (I2C1->RXDR == LSM6DS0_DEVICE_ID){
        Device_Found = 1;
    }
    else Device_Found = 0;
     
    if(Device_Found){
         
        //Enable Block Data Update until MSB and LSB read
        I2C_Write_Reg(LSM6DS0_ADDRESS,LSM6DS0_CTRL_REG8,LSM6DS0_CTRL_REG8_BDU);
         
        //Activate both the gyro and the accelerometer at the same ODR of 238 Hz
        I2C_Write_Reg(LSM6DS0_ADDRESS,LSM6DS0_CTRL_REG1_G,LSM6DS0_CTRL_REG1_G_ODR_G2);
    }
     
    return(Device_Found);
}

STEP3: Read Sensor Data

This is very similar to the LPS25HB Pressure sensor, all we need to do is convert the raw reading with a simple formula.

By default the device is FS = ±2g for linear acceleration and FS = ±245 dps this means our equations will be.

LSM6DS0\_Acceleration(miliG - Force) = Raw\_Acceleration * 0.61
LSM6DS0\_Gyroscope(mdps) = Raw\_Gyro\_Reading * 8.75

All the code can be found on GitHub, but the gist of what you want to do is below.

LSM6DS0.c
/**
  \fn                    float LSM6DS0_X_Acceleration_Read(void)
  \brief            Retrieves X-Direction Acceleration
    \returns        float Acceleration_X: Acceleration in mg
*/
float LSM6DS0_X_Acceleration_Read(void){
     
    //Local Variables
    uint8_t LSM6DS0_STATUS = 0;
    uint8_t Out_X_XL_L = 0;
    uint8_t Out_X_XL_H = 0;
    int16_t Raw_X = 0;
    float Acceleration_X = 0;
     
    //Wait for acceleration data to be ready
    do{
        I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_STATUS_REG);
        LSM6DS0_STATUS = I2C1->RXDR;
    }while((LSM6DS0_STATUS & LSM6DS0_STATUS_REG_XLDA) == 0);
     
    //Read acceleration output registers
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_X_XL_L);
    Out_X_XL_L = I2C1->RXDR;
     
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_X_XL_H);
    Out_X_XL_H = I2C1->RXDR;
     
    //Combine Lower and upper bits
    Raw_X = ((Out_X_XL_H << 8) | Out_X_XL_L);
     
    //Calculate acceleration based on FS configuration, see datasheet
    Acceleration_X = (float)Raw_X*0.061f;
     
    return(Acceleration_X);
}
/**
  \fn                    float LSM6DS0_Y_Acceleration_Read(void)
  \brief            Retrieves Y-Direction Acceleration
    \returns        float Acceleration_Y: Acceleration in mg
*/
float LSM6DS0_Y_Acceleration_Read(void){
     
    //Local Variables
    uint8_t LSM6DS0_STATUS = 0;
    uint8_t Out_Y_XL_L = 0;
    uint8_t Out_Y_XL_H = 0;
    int16_t Raw_Y = 0;
    float Acceleration_Y = 0;
     
    //Wait for acceleration data to be ready
    do{
        I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_STATUS_REG);
        LSM6DS0_STATUS = I2C1->RXDR;
    }while((LSM6DS0_STATUS & LSM6DS0_STATUS_REG_XLDA) == 0);
     
    //Read acceleration output registers
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_Y_XL_L);
    Out_Y_XL_L = I2C1->RXDR;
     
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_Y_XL_H);
    Out_Y_XL_H = I2C1->RXDR;
     
    //Combine Lower and upper bits
    Raw_Y = ((Out_Y_XL_H << 8) | Out_Y_XL_L);
     
    //Calculate acceleration based on FS configuration, see datasheet
    Acceleration_Y = (float)Raw_Y*0.061f;
     
    return(Acceleration_Y);
}
/**
  \fn                    float LSM6DS0_Z_Acceleration_Read(void)
  \brief            Retrieves Z-Direction Acceleration
    \returns        float Acceleration_Z: Acceleration in mg
*/
float LSM6DS0_Z_Acceleration_Read(void){
     
    //Local Variables
    uint8_t LSM6DS0_STATUS = 0;
    uint8_t Out_Z_XL_L = 0;
    uint8_t Out_Z_XL_H = 0;
    int16_t Raw_Z = 0;
    float Acceleration_Z = 0;
     
    //Wait for acceleration data to be ready
    do{
        I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_STATUS_REG);
        LSM6DS0_STATUS = I2C1->RXDR;
    }while((LSM6DS0_STATUS & LSM6DS0_STATUS_REG_XLDA) == 0);
     
    //Read acceleration output registers
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_Z_XL_L);
    Out_Z_XL_L = I2C1->RXDR;
     
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_Z_XL_H);
    Out_Z_XL_H = I2C1->RXDR;
     
    //Combine the lower and upper bits
    Raw_Z = ((Out_Z_XL_H << 8) | Out_Z_XL_L);
     
    //Calculate acceleration based on FS configuration, see datasheet
    Acceleration_Z = (float)Raw_Z*0.061f;
     
    return(Acceleration_Z);
}
/**
  \fn                    float LSM6DS0_Gyroscope_Roll_Read(void)
  \brief            Retrieves X-Direction(Roll)
    \returns        float Roll: Roll in mdps
*/
float LSM6DS0_Gyroscope_Roll_Read(void){
     
    //Local Variables
    uint8_t LSM6DS0_STATUS = 0;
    uint8_t Roll_L = 0;
    uint8_t Roll_H = 0;
    int16_t Raw_Roll = 0;
    float Roll = 0;
     
    //Wait for roll data to be ready
    do{
        I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_STATUS_REG);
        LSM6DS0_STATUS = I2C1->RXDR;
    }while((LSM6DS0_STATUS & LSM6DS0_STATUS_REG_GDA) == 0);
     
    //Read Gyroscope output registers
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_X_G_L);
    Roll_L = I2C1->RXDR;
     
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_X_G_H);
    Roll_H = I2C1->RXDR;
     
    //Combine lower and upper bits
    Raw_Roll = ((Roll_H << 8) | Roll_L);
     
    //Calculate Roll based on FS configuration,see datasheet
    Roll = (float)Raw_Roll*8.75f;
     
    return(Roll);
}
/**
  \fn                    float LSM6DS0_Gyroscope_Pitch_Read(void)
  \brief            Retrieves Y-Direction(Pitch)
    \returns        float Pitch: Pitch in mdps
*/
float LSM6DS0_Gyroscope_Pitch_Read(void){
     
    //Local Variables
    uint8_t LSM6DS0_STATUS = 0;
    uint8_t Pitch_L = 0;
    uint8_t Pitch_H = 0;
    int16_t Raw_Pitch = 0;
    float Pitch = 0;
     
    //Wait for pitch data to be ready
    do{
        I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_STATUS_REG);
        LSM6DS0_STATUS = I2C1->RXDR;
    }while((LSM6DS0_STATUS & LSM6DS0_STATUS_REG_GDA) == 0);
     
    //Read gyroscope output registers
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_Y_G_L);
    Pitch_L = I2C1->RXDR;
     
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_Y_G_H);
    Pitch_H = I2C1->RXDR;
     
    //Combine lower and upper bits
    Raw_Pitch = ((Pitch_H << 8) | Pitch_L);
     
    //Calculate Pitch based on FS configuration,see datasheet
    Pitch = (float)Raw_Pitch*8.75f;
     
    return(Pitch);
}
/**
  \fn                    float LSM6DS0_Gyroscope_Yaw_Read(void)
  \brief            Retrieves Z-Direction(Yaw)
    \returns        float Yaw: Yaw in mdps
*/
float LSM6DS0_Gyroscope_Yaw_Read(void){
     
    //Local Variables
    uint8_t LSM6DS0_STATUS = 0;
    uint8_t Yaw_L = 0;
    uint8_t Yaw_H = 0;
    int16_t Raw_Yaw = 0;
    float Yaw = 0;
     
    //Wait for Yaw data to be ready
    do{
        I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_STATUS_REG);
        LSM6DS0_STATUS = I2C1->RXDR;
    }while((LSM6DS0_STATUS & LSM6DS0_STATUS_REG_GDA) == 0);
     
    //Read gyroscope output registers
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_Z_G_L);
    Yaw_L = I2C1->RXDR;
     
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_Z_G_H);
    Yaw_H = I2C1->RXDR;
     
    //Combine lower and upper bits
    Raw_Yaw = ((Yaw_H << 8) | Yaw_L);
     
    //Calculate Yaw based on FS configuration,see datasheet
    Yaw = (float)Raw_Yaw*8.75f;
     
    return(Yaw);
}

Communicating with LIS3MDL Magnetometer Sensor Walk-through

A good reference for all the registers and settings can be found in Magnetometer datasheet. In this section of the guide I will describe how to communicate, set up the LIS3MDL magnetometer sensor and read the magnetic field data in the X,Y and Z directions.

STEP1: Communication Sequence

The communication sequence is the same as the HTS221. If you haven’t read this section click here.

STEP2: Configure the LIS3MDL

There are 4 different registers that you have to concern yourself with.

In CTRL_REG1 we can set the power consumption for the X, Y directions by setting the correct bit sequence for OM[1:0] . The code below sets this to medium performance mode. In addition to this setting we can also set the output data rate by writing the correct bit sequence to DO[2:0]. In the case below I chose to write a 10Hz output data rate.

//Performance Vs. Power consumption XY (medium), and set data rate to 10Hz
I2C_Write_Reg(LIS3MDL_ADDRESS,LIS3MDL_CTRL_REG1,(LIS3MDL_CTRL_REG1_OM0 | LIS3MDL_CTRL_REG1_DO2));
         
//Full scale = +/- 4 gauss (Default value) - just in case
I2C_Write_Reg(LIS3MDL_ADDRESS,LIS3MDL_CTRL_REG2,0x0);
         
//Performance Vs. Power consumption Z (medium)
I2C_Write_Reg(LIS3MDL_ADDRESS,LTS3MDL_CTRL_REG4,LIS3MDL_CTRL_REG4_OMZ0);
         
//Enable BDU so you ensure MSB and LSB have been read
I2C_Write_Reg(LIS3MDL_ADDRESS,LTS3MDL_CTRL_REG5,LIS3MDL_CTRL_REG5_BDU);

STEP3: Read Sensor Data

Similar to the other sensor’s we have to do one small modification to our reading. In my case I have a FS = ±4, which means we need to divide the reading by 6842.

Magnetometer\ Reading(mili - Gauss) = \frac{Raw\_Reading}{6842}

All the code can be found on GitHub, but the gist of what you want to do is below.

LIS3MDL.c
/**
  \fn                    float LIS3MDL_X_Read(void)
  \brief            Reads the magnetic field from the X axis
    \returns         float OUT_X: the magnetic field in mG
*/
float LIS3MDL_X_Read(void){
     
    //Local variables
    uint8_t LIS3MDL_STATUS = 0;
    uint8_t OUT_X_L = 0;
    uint8_t OUT_X_H = 0;
    float OUT_X = 0;
    int16_t Raw_X = 0;
     
    //Set device to continuous conversion mode
    I2C_Write_Reg(LIS3MDL_ADDRESS,LTS3MDL_CTRL_REG3,LIS3MDL_CTRL_REG3_MD0);
     
    //Wait for X coordinate data to be ready
    do{
        I2C_Read_Reg(LIS3MDL_ADDRESS,LIS3MDL_STATUS_REG);
        LIS3MDL_STATUS = I2C1->RXDR;
    }while((LIS3MDL_STATUS & LIS3MDL_STATUS_REG_XDA) == 0);
     
    //Read X Axis magnetic field
    I2C_Read_Reg(LIS3MDL_ADDRESS,LIS3MDL_OUT_X_L);
    OUT_X_L = I2C1->RXDR;
     
    I2C_Read_Reg(LIS3MDL_ADDRESS,LIS3MDL_OUT_X_H);
    OUT_X_H = I2C1->RXDR;
     
    //Process X coordinates
    Raw_X = ((OUT_X_H << 8) | OUT_X_L);
     
    /*
     *1/6842 = ~0.146 mG/LSB according to datasheet
     */
    OUT_X = (float)Raw_X * 0.146f;        // when using +/- 4 gauss
    return(OUT_X);
}
/**
  \fn                    float LIS3MDL_Y_Read(void)
  \brief            Reads the magnetic field from the Y axis
    \returns         float OUT_Y: the magnetic field in mG
*/
float LIS3MDL_Y_Read(void){
     
    //Local Variables
    uint8_t LIS3MDL_STATUS = 0;
    uint8_t OUT_Y_L = 0;
    uint8_t OUT_Y_H = 0;
    float OUT_Y = 0;
    int16_t Raw_Y = 0;
     
    //Set device to continuous conversion mode
    I2C_Write_Reg(LIS3MDL_ADDRESS,LTS3MDL_CTRL_REG3,LIS3MDL_CTRL_REG3_MD0);
     
    //Wait for X coordinate data to be ready
    do{
        I2C_Read_Reg(LIS3MDL_ADDRESS,LIS3MDL_STATUS_REG);
        LIS3MDL_STATUS = I2C1->RXDR;
    }while((LIS3MDL_STATUS & LIS3MDL_STATUS_REG_YDA) == 0);
     
    //Read Y Axis magnetic field
    I2C_Read_Reg(LIS3MDL_ADDRESS,LIS3MDL_OUT_Y_L);
    OUT_Y_L = I2C1->RXDR;
     
    I2C_Read_Reg(LIS3MDL_ADDRESS,LIS3MDL_OUT_Y_H);
    OUT_Y_H = I2C1->RXDR;
     
    /*
    1/6842 = ~0.146 mG/LSB according to datasheet
    */
    Raw_Y = ((OUT_Y_H << 8) | OUT_Y_L);
    OUT_Y = (float)Raw_Y * 0.146f;        // when using +/- 4 gauss
    return(OUT_Y);
}
/**
  \fn                    float LIS3MDL_Z_Read(void)
  \brief            Reads the magnetic field from the Z axis
    \returns         float OUT_Z: the magnetic field in mG
*/
float LIS3MDL_Z_Read(void){
     
    //Local Variables
    uint8_t LIS3MDL_STATUS = 0;
    uint8_t OUT_Z_L = 0;
    uint8_t OUT_Z_H = 0;
    float OUT_Z = 0;
    int16_t Raw_Z = 0;
     
    //Set device to continuous conversion mode
    I2C_Write_Reg(LIS3MDL_ADDRESS,LTS3MDL_CTRL_REG3,LIS3MDL_CTRL_REG3_MD0);
     
    //Wait for X coordinate data to be ready
    do{
        I2C_Read_Reg(LIS3MDL_ADDRESS,LIS3MDL_STATUS_REG);
        LIS3MDL_STATUS = I2C1->RXDR;
    }while((LIS3MDL_STATUS & LIS3MDL_STATUS_REG_ZDA) == 0);
     
    //Read Z Axis magnetic field
    I2C_Read_Reg(LIS3MDL_ADDRESS,LIS3MDL_OUT_Z_L);
    OUT_Z_L = I2C1->RXDR;
     
    I2C_Read_Reg(LIS3MDL_ADDRESS,LIS3MDL_OUT_Z_H);
    OUT_Z_H = I2C1->RXDR;
     
    Raw_Z = ((OUT_Z_H << 8) | OUT_Z_L);
     
    /*
    1/6842 = ~0.146 mG/LSB according to datasheet
    */
    OUT_Z = (float)Raw_Z * 0.146f;        // when using +/- 4 gauss
    return(OUT_Z);
}

Example of USART/LPUART

Initializing USART/LPUART Walk-through

A good reference for all the registers and settings can be found in STM32L053R8 Reference Manual. It is also useful to look at the STM32L053xx Tech Data for finding alternate functions for pins. This walk-through assumes you have already setup your alternate function. If you haven’t please refer to Setting Alternate Function Walk-through. In this walk-through we will be setting up a couple UART’s. The STM32L053R8 has a coupe of different UART’s. The first is a normal USART (Universal Synchronous Asynchronous Receiver Transmitter) and the second is a Low Powered UART (LPUART). The LPUART is just about the same as the setup for the USART but I figured I should be thorough.

STEP1: Setup the clock

Its a simple as writing to IOPAEN in the RCC_IOPENR register and also enabling the USART clock by writing to USART1EN in the RCC_APB2ENR register. By default the clock is PCLK.

RCC->IOPENR   |=   RCC_IOPENR_GPIOAEN;      /* Enable GPIOA clock */
RCC->APB2ENR  |=   RCC_APB2ENR_USART1EN;    /* Enable USART#1 clock */

STEP2:Configure the USART BRR

There are a couple of equations to be concerned about when calculating the baud-rate. It is all based on how much you are oversampling or if you are using the LPUART. What is meant by oversampling? All this means is that we will sample a bit stream received by the UART multiple times for each bit. After the sampling has been completed the majority value (either 0 or 1) will be picked as the value.

Warning: When oversampling by 8 you CANNOT just write the calculated USARTDIV value.

When oversampling by 16 bits (default)

LPUARTDIV = \frac{F_{ck}}{Baud\ Rate}

You can see that one bit time is sampled 16 times to determine the value.

When oversampling by 8 bits

LPUARTDIV = \frac{2 * F_{ck}}{Baud\ Rate}

image

You can see that one bit time is sampled 8 times to determine the value.

Basically there is a set of rules you must follow to write the correct value to the register. Please refer to STM32L053xx Reference page 759 to verify the steps I have written.

  1. BRR[2:0] = Calculated USARTDIV[3:0] but shifted once to the right
  2. BRR[3] = 0, Always!
  3. BRR[15:4] = USARTDIV[15:4].

When using the LPUART

LPUARTDIV = \frac{256 * F_{ck}}{Baud\ Rate}

There is no choice in oversampling, hence this is your only equation for the LPUART.

USART BRR Code:

/* Define statements outside function */
#define PCLK    32000000                                    // Peripheral Clock
#define BAUD    9600                                            // Baud rate
 
/* Local variables */
uint16_t USARTDIV = 0;
uint16_t USART_FRACTION = 0;
uint16_t USART_MANTISSA = 0;
 
/* Check to see if oversampling by 8 or 16 to properly set baud rate*/
  if((USART2->CR1 & USART_CR1_OVER8) == 1){
      USARTDIV = PCLK/BAUD;
      USART_FRACTION = ((USARTDIV & 0x0F) >> 1) & (0xB);
      USART_MANTISSA = ((USARTDIV & 0xFFF0) << 4);
      USARTDIV = USART_MANTISSA | USART_FRACTION;
      USART2->BRR = USARTDIV;                                                            /* 9600 Baud with 32MHz peripheral clock 8bit oversampling */
  }
  else{
      USART2->BRR = PCLK/BAUD;                                                        /* 9600 Baud with 32MHz peripheral clock 16bit oversampling */   
  }

LPUART BRR Code:

Since our MCU is 32 bit, we only have a max value of 2^32-1 = 4294967295 which cannot handle a value like 256*32,000,000. But we can do a little trick and divide first then multiply, it would look something like this.

/* Define statements outside function */
#define PCLK    32000000                                    // Peripheral Clock
#define BAUD    9600                                            // Baud rate
 
LPUART1->BRR      = (unsigned long)((256.0f/BAUD)*PCLK);         /* 9600 baud @ 32MHz */

STEP3: Configure the CR1 Register

We want a couple different things from this register. We want to enable RX and TX so we write to both RE and TE . We also want to say what our word length is going to be, we can do this by writing the appropriate value to M[1:0] . Please notice that M0 is in bit position 12 and M1 is in bit position 28.

USART2->CR3    = 0x0000;                                                      /* no flow control */
USART2->CR2    = 0x0000;                                                      /* 1 stop bit */
 
/* 1 stop bit, 8 data bits */
  USART2->CR1    = ((USART_CR1_RE) |                                          /* enable RX  */
                    (USART_CR1_TE) |                                          /* enable TX  */
                    (USART_CR1_UE) |                                          /* enable USART */
                    (USART_CR1_RXNEIE));                                      /* Enable Interrupt */

Initializing USART/LPUART Code (Reading/Writing Also!)

All the code can be found on GitHub, but the gist of what you want to do is below.

Serial.c
/*------------------------------------------------------------------------------------------------------
 * Name:    Serial.c
 * Purpose: Used to communicat with your computer using USART2
 * Date:         7/14/15
 * Author:    Christopher Jordan - Denny
 *------------------------------------------------------------------------------------------------------
 * Note(s):    SER_PutChar are used to redefine printf function
 *----------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------Include Statements---------------------------------*/
#include "stm32l0xx.h"                  // Specific Device header
#include "Serial.h"
/*-------------------------------------------------Define Statements----------------------------------*/
#define PCLK    32000000                                    // Peripheral Clock
#define BAUD    9600                                            // Baud rate
/*-------------------------------------------------Functions------------------------------------------*/
/**
  \fn          void SER_Initialize (void)
  \brief       Initializes USART2 to be used with the serial monitor
*/
     
void SER_Initialize (void){
  /* Local variables */
  uint16_t USARTDIV = 0;
  uint16_t USART_FRACTION = 0;
  uint16_t USART_MANTISSA = 0;
  RCC->IOPENR   |=   ( 1ul <<  0);         /* Enable GPIOA clock   */
  RCC->APB1ENR  |=   ( 1ul << 17);         /* Enable USART#2 clock */
  /* Configure PA3 to USART2_RX, PA2 to USART2_TX */
  GPIOA->AFR[0] &= ~((15ul << 4* 3) | (15ul << 4* 2) );
  GPIOA->AFR[0] |=  (( 4ul << 4* 3) | ( 4ul << 4* 2) );
  GPIOA->MODER  &= ~(( 3ul << 2* 3) | ( 3ul << 2* 2) );
  GPIOA->MODER  |=  (( 2ul << 2* 3) | ( 2ul << 2* 2) );
  /* Check to see if oversampling by 8 or 16 to properly set baud rate*/
  if((USART2->CR1 & USART_CR1_OVER8) == 1){
      USARTDIV = PCLK/BAUD;
      USART_FRACTION = ((USARTDIV & 0x0F) >> 1) & (0xB);
      USART_MANTISSA = ((USARTDIV & 0xFFF0) << 4);
      USARTDIV = USART_MANTISSA | USART_FRACTION;
      USART2->BRR = USARTDIV;                                                            /* 9600 Baud with 32MHz peripheral clock 8bit oversampling */
  }
  else{
      USART2->BRR = PCLK/BAUD;                                                        /* 9600 Baud with 32MHz peripheral clock 16bit oversampling */   
  }
  USART2->CR3   = 0x0000;                 /* no flow control */
  USART2->CR2   = 0x0000;                 /* 1 stop bit */
     
    /* 1 stop bit, 8 data bits */
  USART2->CR1    = ((USART_CR1_RE) |                                                /* enable RX  */
                     (USART_CR1_TE) |                                                /* enable TX  */
                     (USART_CR1_UE) |                                          /* enable USART */
                                         (USART_CR1_RXNEIE));                                        /* Enable Interrupt */
}
/**
  \fn                    char SER_PutChar(char ch)
  \brief            Put character to the serial monitor
    \param            char ch: Character to send to the serial monitor
    \returns        char ch: The character that was sent to the serial monitor
*/
char SER_PutChar(char ch){
    //Wait for buffer to be empty
  while ((USART2->ISR & USART_ISR_TXE) == 0){
            //Nop
    }
     
    //Send character
  USART2->TDR = (ch);
  return (ch);
}
/**
  \fn                    int SER_GetChar(void)
  \brief            Get character from the serial monitor
    \returns        int USART2->RDR: The value of character from the serial monitor
*/
int SER_GetChar(void){
  if (USART2->ISR & USART_ISR_RXNE)
    return (USART2->RDR);
  return (-1);
}

Redirecting Printf

There is a great trick that you can do to use the printf function in C with one of your USART’s. As always All the code can be found on GitHub.

/*------------------------------------------------------------------------------------------------------
 * Name:    Retarget.c
 * Purpose: Retargets printf C function to be used with USART2
 * Date:         7/14/15
 * Author:    Christopher Jordan - Denny
 *------------------------------------------------------------------------------------------------------
 * Note(s):    This is really just magic
 *----------------------------------------------------------------------------------------------------*/
/*--------------------------------------Include Statements--------------------------------------------*/
#include <stdio.h>
#include <rt_misc.h>
#include "Serial.h"
/*--------------------------------------Additional Compiler Info--------------------------------------*/
#pragma import(__use_no_semihosting_swi)
/*--------------------------------------Structure Definitions-----------------------------------------*/
struct __FILE { int handle;};
FILE __stdout;
FILE __stdin;
/*--------------------------------------Functions-----------------------------------------------------*/
int fputc(int c, FILE *f){
  return (SER_PutChar(c));
}
int fgetc(FILE *f){
  return (SER_GetChar());
}
void _sys_exit(int return_code){
label:  goto label;  /* endless loop */
}

Communicating with FGPMMOPA6H GPS Module Walk-through

A good reference for all the settings and NMEA sentences can be found in FGPMMOPA6B GPS Module Data sheet. For a full list of commands please see PMTK Commands. In this section of the guide I will describe how to communicate, set up the FGPMMOPA6H GPS Module. I will also cover how I parsed the data that was received. For this section I would highly advice to look at the full code on GitHub. If you have not setup your USART please read Initializing USART/LPUART Walk-through.

STEP1: Setup the refresh rate

First you must declare what the position echo time will be, and then you must declare what the update time will be. Both must be set for you to change the refresh time.

#define PMTK_SET_NMEA_UPDATE_200_MILLIHERTZ        "$PMTK220,5000*1B\r\n"  // Once every 5 seconds, 200 millihertz.
#define PMTK_API_SET_FIX_CTL_200_MILLIHERTZ        "$PMTK300,5000,0,0,0,0*18\r\n"  // Once every 5 seconds, 200 millihertz.
 
USART1_Send(PMTK_API_SET_FIX_CTL_200_MILLIHERTZ);        /* 5s Position echo time   */
USART1_Send(PMTK_SET_NMEA_UPDATE_200_MILLIHERTZ);        /* 5s update time                */

STEP2: Setup GPS messages to receive

Originally there are 5 messages that you will be receiving, $GPGGA, $GPGSA, $GPGSV, $GPRMC and $GPVTG. The most useful I found to be $GPRMC and $GPGGA. To select these two messages there is one simple command.

// turn on GPRMC and GGA
#define PMTK_SET_NMEA_OUTPUT_RMCGGA                        "$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n"
 
USART1_Send(PMTK_SET_NMEA_OUTPUT_RMCGGA);                      /* Output RMC Data and GGA */

STEP3: Sort the Received GPS Data

I have created a flowchart to better understand how to sort through the messages received. This will only put messages in their appropriate tag not parse the data. In this case I am using an interrupt to handle receiving data.

FGPMMOPA6H.c
/*---------------------------------NMEA Output Sentences----------------------------------------------*/
static const char GGA_Tag[] = "$GPGGA";
static const char GSA_Tag[] = "$GPGSA";
static const char GSV_Tag[] = "$GPGSV";
static const char RMC_Tag[] = "$GPRMC";
static const char VTG_Tag[] = "$GPVTG";
/*---------------------------------Globals------------------------------------------------------------*/
volatile int                 CharIndex = 0;                                                /* Character index of the char array */
const int                     NMEA_LENGTH = 128;                                        /* The max length of one NMEA line */
char                                 Rx_Data[NMEA_LENGTH] = "0";                        /* Rx Sring */
volatile uint8_t         Transmission_In_Progress = FALSE;            /* Are we in between a $ and \n */
char                                 GGA_Message[128];                                            /* Original GGA message */
char                                 GSA_Message[128];                                            /* Original GSA message */
char                                 GSV_Message[128];                                            /* Original GSV message */
char                                 RMC_Message[128];                                            /* Original RMC message */
char                                 VTG_Message[128];                                            /* Original VTG message */
 
/**
  \fn          void USART1_IRQHandler(void)
  \brief       Global interrupt handler for USART1, handles Rx interrupt
*/
void USART1_IRQHandler(void){
     
    if(USART1->ISR & USART_ISR_RXNE){
         
        /* Reads and CLEARS RXNE Flag */
    Rx_Data[CharIndex] = USART1->RDR;
         
        /* Data Not Ready */
        RMC.New_Data_Ready = FALSE;
        GGA.New_Data_Ready = FALSE;
         
        /* If Rx_Data = $, then we are in transmission */
        if(Rx_Data[CharIndex] == '$'){
            Transmission_In_Progress = TRUE;
        }
         
        /* If we are transmitting then save to proper message once complete */
        if(Transmission_In_Progress == TRUE){
            if(Rx_Data[CharIndex] == '\n'){
                if(strncmp(GGA_Tag,Rx_Data,(sizeof(GGA_Tag)-1)) == 0){
                    strcpy(GGA_Message,Rx_Data);
                    GGA.New_Data_Ready = TRUE;
                }
                if(strncmp(GSA_Tag,Rx_Data,(sizeof(GSA_Tag)-1)) == 0){
                    strcpy(GSA_Message,Rx_Data);
                }
                if(strncmp(GSV_Tag,Rx_Data,(sizeof(GSV_Tag)-1)) == 0){
                    strcpy(GSV_Message,Rx_Data);
                }
                if(strncmp(RMC_Tag,Rx_Data,(sizeof(RMC_Tag)-1)) == 0){
                    strcpy(RMC_Message,Rx_Data);
                    RMC.New_Data_Ready = TRUE;
                }
                if(strncmp(VTG_Tag,Rx_Data,(sizeof(VTG_Tag)-1)) == 0){
                    strcpy(VTG_Message,Rx_Data);
                }
                Transmission_In_Progress = FALSE;
                CharIndex = 0;
                memset(Rx_Data,0,sizeof(Rx_Data));
            }
            else{
                CharIndex++;
            }
        }
    }
}

STEP4: Parse the specific message data

All the data from the GPS comes in comma delimitted and ends with a or carridge return, Line Feed. For example the $GPRMC message might look something like this,

$GPRMC,064951.000,A,2307.1256,N,12016.4438,E,0.03,165.48,260406,3.05,W,A*2C

This means that we just need to parse based on the comma delimitter. The strtok C function i perfect for performing this, its use is slightly strange, and I would highly recommend researching more about it. I would also highly recommend that you check out this code on GitHub. However the gist of what you want to do can be seen below.

FGPMMOPA6H.c
/**
  \fn          void FGPMMOPA6H_Parse_RMC_Data(void)
  \brief       Parses the RMC Data based on the , delimeter                   
*/
void FGPMMOPA6H_Parse_RMC_Data(void){
     
    //Local Variables
    char RMC_Message_Copy[128] = "";
    const char delimeter[2] = ",";
    char *token = "";
    int i = 0;
    char temp[11][12];            /* [11][12]: 11 strings, of length 12 */
     
    //Copy original RMC to a copy in order to not destroy message
    strcpy(RMC_Message_Copy,RMC_Message);
     
    //Seperated Message
    /* get the first token */
   token = strtok(RMC_Message_Copy, delimeter);
    
   /* walk through other tokens */
   while( token != NULL )
   {
         strcpy(temp[i],token);
         i++;
     token = strtok(NULL, delimeter);
   }
      
     //Copy the message into its individual components
    strcpy(RMC.Message_ID,temp[0]);
    strcpy(RMC.UTC_Time,temp[1]);
    strcpy(RMC.Status,temp[2]);
    strcpy(RMC.Latitude,temp[3]);
    strcpy(RMC.N_S_Indicator,temp[4]);
    strcpy(RMC.Longitude,temp[5]);
    strcpy(RMC.E_W_Indicator,temp[6]);
    strcpy(RMC.Speed_Over_Ground,temp[7]);
    strcpy(RMC.Course_Over_Ground,temp[8]);
    strcpy(RMC.Date,temp[9]);
    strcpy(RMC.Mode,temp[10]);
}

Communicating with XBee Pro S1 Walk-through

A good reference for all the settings can be found in XBee Pro Manual. In this section of the guide I will describe how to communicate and set up the XBee Pro S1. I would highly advice to look at the full code on GitHub. If you have not setup your USART please read Initializing USART/LPUART Walk-through. If you aren’t familiar with any Serial Monitor programs please see Talking to XBee with CoolTerm for a quick tutorial on setup.

STEP1: Setup the addresses

There are a couple of addresses to concern yourself with.

Address Type Command Issued Purpose
MY ATMY Read/Write the MY address which is the source address of the XBee
PAN ATID Personal Area Network - Read/Write the 16-bit ID of the XBee. Only XBee’s on the same PAN can communicate
DH ATDH Destination High - Read/Write the higher 32-bit Destination address of the XBee
DL ATDL Destination Low - Read/Write the lower 32-bit Destination address of the XBee
CH ATCH Read/Write the operating channel of the XBee(1)

(1) The XBee Channel is determined by the following formula which adheres to the 802.15.4 standard

Center\ Frequency(MHz) = 2.405 + (CH - 11.0) * 5

By default CH = 0x0C or 12, meaning the center frequency = 7.405MHz.

The command sequence to change these values is as follows.

image

Below I will show you an example of setting up a peer to peer network between two XBee’s. Again if you are unfamiliar with a serial monitor program please see Talking to XBee with CoolTerm.

You will notice that each time that a register has been successfully written to, the XBee will respond with an OK. We can use this fact to ensure registers have been set and use an interrupt to handle the received data like so.

XBeePro24.c
/**
    \fn            void RNG_LPUART1_IRQHandler(void)
    \brief    Global interrupt handler for LPUART, Currently only handles RX
*/
void RNG_LPUART1_IRQHandler(void){
    if((LPUART1->ISR & USART_ISR_RXNE) == USART_ISR_RXNE){
         
        /* Read RX Data */
        RX_Data[ChIndex] = LPUART1->RDR;
         
        /* Check for end of recieved data */
        if(RX_Data[ChIndex] == '\r'){
             
            /* Compare string to OK to see if Acknowledged */
            if(strncmp(OK,RX_Data,(sizeof(OK)-1)) == 0){
                Device_Ack_Flag = TRUE;
            }
             
            /* Copy XBee message */
            strcpy(XBee_Message,RX_Data);
             
            /* set data ready to read flag */
            XBee_Ready_To_Read = TRUE;
             
            /* Clear RX_Data */
            ChIndex = 0;
            memset(RX_Data,0,sizeof(RX_Data));
             
        }else ChIndex++;
    }
}

To apply the settings we have as an example in TeraTerm we can do the following.

XBeePro24.c
/**
    \fn                void XBee_Init(void)
    \brief        Initializes the XBEE
*/
void XBee_Init(void){
     
    /* Enter AT command mode */
    Delay(1000);
    LPUART1_Send(ENTER_AT_COMMAND_MODE);
    Wait_For_OK();
     
    /* MY address = 2 */
    LPUART1_Send(SET_ATMY);
    Wait_For_OK();
     
    /* PAN = 3001 */
    LPUART1_Send(SET_ATID);
    Wait_For_OK();
     
    /* Set Destination address high */
    LPUART1_Send(SET_ATDH);
    Wait_For_OK();
     
    /* Set Destination Address low */
    LPUART1_Send(SET_ATDL);
    Wait_For_OK();
     
    /* Set Channel */
    LPUART1_Send(SET_ATCH);
    Wait_For_OK();
     
    /* End AT command mode */
    LPUART1_Send(EXIT_AT_COMMAND_MODE);
    Wait_For_OK();
     
    printf("#####  XBee            Initialized  #####\r\n");
}
 
/**
    \fn                void Wait_For_OK(void)
    \brief        Waits until the XBEE has sent the OK message
                        This is done when it has changed its settings
*/
void Wait_For_OK(void){
     
    /* Wait for XBee Acknowledge */
    while(Device_Ack_Flag == 0){
        //Nop
    }
     
    /* Reset Flags */
    Device_Ack_Flag = FALSE;
    XBee_Ready_To_Read = FALSE;
     
}
/**
    \fn                void Wait_For_Data(void)
    \brief        This waits for the data the XBEE writes to the bus
*/
void Wait_For_Data(void){
     
    /* Wait for data to be copied */
    while(XBee_Ready_To_Read == 0){
        //Nop
    }
     
    /* Reset Flags */
    Device_Ack_Flag = FALSE;
    XBee_Ready_To_Read = FALSE;
}

STEP2: Send Data

After the addresses have been setup correctly, it is simple to send data, just write to the XBee.

/* Send data over the XBEE */
LPUART1_Send(Data);

Talking to XBee with CoolTerm

This is a short guide on setting up CoolTerm for your serial monitor. You can download CoolTerm from here.

STEP1: Setup Serial Port

Click on the gear and wrench icon called Options , you can use the settings below to talk to the XBee Pro s1 when the XBee is in its default settings.

STEP2: Setup Terminal

In the menu on the left click Terminal and click the Local Echo box.

image

That’s it, your all setup to monitor and communicate over the com port with your XBee or other device all you have to do is hit the connect button.

Example of Pulse Width Modulation

Initialize PWM Walk-through

A good reference for all the settings can be found in STM32L053xx Reference manual. In this section of the guide I will describe how to enable P ulse W idth M odulation (PWM) using the general purpose timers (TIM21/22). This signal will be used to control a servo motor. I would highly recommend to look at the full code on GitHub.

STEP1: Enable Clock

 RCC->APB2ENR |= RCC_APB2ENR_TIM22EN;

STEP2: Setup Period

This code will setup the clock speed for PWM, the period of PWM and also the length of the pulse. In my example I have the clock setup at 32MHz, so to get a nice 1MHz clock we can set the value in TIM22_PSC to 31. This comes from the formula:

CK\_CNT = \frac{FCK\_PSC}{PSC[15:0] + 1}

Where FCK_PSC is the clock speed (32MHz), PSC[15:0] is the value written to the TIM22_PSC register (31), and CK_CNT is the timer clock. This can best be described in the following picture.

Now we can setup the period that we want, since I am using PWM for controlling a Servo motor I chose 20ms. Since we now have a 1 MHz clock (1µs period) we can write 2000 to the TIM22_ARR register to obtain a 20ms period. The last thing we have to calculate is the width of the pulse. This can be done by subtracting the pulse duration from the TIM22_ARR register value and writing this to the TIM22_CCR1 register.

TIM22->PSC = 31;                            //CK_CNT=Fck_psc/(PSC[15:0]+1), so 32MHz clock becomes 1MHz
TIM22->ARR = 20000;                        //Clock is 1MHz so period becomes 20ms
  
/*Pulse Width Calculation*/
TIM22->CCR1 = (TIM22->ARR)-Pulse_Duration;

STEP3: More Settings

A few more settings are required before we are done. In my case I used output compare 1 mode PWM mode 2. This will make channel 1 inactive as long as TIMx_CNT<TIMx_CCR1 when up-counting. Up-counting is the default setting. I also enabled the Output compare 1 preload. Next we can set the active high polarity on OC1 by writing the TIM_CCER_CC1E bit to the TIMx_CCER register. Lastly we enable PWM by writing the TIM_CR1_CEN bit to the TIMx_CR1 register, and re-initializing the counter and registers by writing the TIM_EGR_UG bit to the TIMx_EGR .

/*Select PWM 2 on OC1 and enable preload register*/
TIMx->CCMR1 |= TIM_CCMR1_OC1M_2|TIM_CCMR1_OC1M_1|TIM_CCMR1_OC1M_0
                               |TIM_CCMR1_OC1PE
#if PULSE_WITHOUT_DELAY > 0
                               |TIM_CCMR1_OC1FE
#endif
;
 
/*Select active high polarity on OC1*/
TIMx->CCER |= TIM_CCER_CC1E;
 
/*Enable PWM*/
TIMx->CR1 = TIM_CR1_CEN;
TIMx->EGR = TIM_EGR_UG;

STEP4: Select GPIO

The last step is to select what pin you would like to use. This involves checking alternate functions and setting up the GPIO. If you haven’t read the section on Example of GPIO, or Setting Alternate Function Walk-through please do so now. the code I used for setting up the GPIO for PWM can be found below.

/*Initialize GPIO*/
struct GPIO_Parameters GPIO;
GPIO.Pin     = Pin;
GPIO.Mode     = Alternate_Function;
GPIO.Speed = High_Speed;
GPIO.PuPd     = Pull_Down;
GPIO_Init(GPIOx,GPIO);

Initialize PWM Code

All code found on GitHub.

PWM.c
void PWM(TIM_TypeDef* TIMx, int Pulse_Duration,GPIO_TypeDef* GPIOx, int Pin){
  
 /*Initialize GPIO*/
 struct GPIO_Parameters GPIO;
 GPIO.Pin     = Pin;
 GPIO.Mode     = Alternate_Function;
 GPIO.Speed = High_Speed;
 GPIO.PuPd     = Pull_Down;
 GPIO_Init(GPIOx,GPIO);
  
 /*Enable TIM clock*/
 if(TIMx == TIM22){
        RCC->APB2ENR |= RCC_APB2ENR_TIM22EN;
 }
 if(TIMx == TIM21){
     RCC->APB2ENR |= RCC_APB2ENR_TIM21EN;
 }
  
 TIMx->PSC = 31;                            //CK_CNT=Fck_psc/(PSC[15:0]+1), so 32MHz clock becomes 1MHz
 TIMx->ARR = 20000;                        //Clock is 1MHz so period becomes 20ms
  
 /*Pulse Width Calculation*/
 TIMx->CCR1 = (TIMx->ARR)-Pulse_Duration;
  
 /*Select PWM 2 on OC1 and enable preload register*/
 TIMx->CCMR1 |= TIM_CCMR1_OC1M_2|TIM_CCMR1_OC1M_1|TIM_CCMR1_OC1M_0
                                |TIM_CCMR1_OC1PE
 #if PULSE_WITHOUT_DELAY > 0
                                |TIM_CCMR1_OC1FE
 #endif
 ;
  
 /*Select active high polarity on OC1*/
 TIMx->CCER |= TIM_CCER_CC1E;
  
 /*Enable PWM*/
 TIMx->CR1 = TIM_CR1_CEN;
 TIMx->EGR = TIM_EGR_UG;
}

Author’s Note

I encourage all to use, edit, improve my code found on GitHub.