STM32 F4 SPI Accelerometer

Goal: 
  • getting the data from LIS3DSH accelerometer using SPI
  • determining tilt of the board (trigonometry refresher)

The picture below shows how 16 bits of data are transmitted using SPI. I used an oscilloscope to visualize one frame (took multiple pictures and made a 'collage'). 
In the picture below, I'm sending two bytes: 0x20 and 0x67 as an example. 

0x20 in binary: 0010 0000. Here's what every bit/part means: 
00 = operating code, write mode (00-write mode, 01-read mode, 10-read and clear status, 11-read device info)
10 0000 = register address

0x67 in binary: 0110 0111. Here's what every bit/part means:
0110 = output data rate 100Hz (0000-power down, see accelerometer app note for more info)
0 = block data update, register not updated until MSB and LSB reading (1-continious mode)
111 = X,Y,Z enabled



































Startup sequence given in the app note: write 0x67 to CTRL_REG4 and write 0xC8 to CTRL_REG3. 
I used the following sequence (this is specific to LIS3DSH):
SPI_send(0x23, 0xc9); // resetting the whole internal circuit
SPI_send(0x20, 0x67); // 100Hz data update rate, block data update disable, x/y/z enabled 
SPI_send(0x24, 0x20); // Anti aliasing filter bandwidth 800Hz, 16G (very sensitive), no self-test, 4-wire interface


Accelerometer data for each axis is split into two 8-bit registers. The registers need to be merged to form a 16-bit value expressed in 2's complement.
MSB = SPI_read(0x29)      // X-axis MSB
LSB = SPI_read(0x28);     // X-axis LSB
Xg = (MSB << 8) | (LSB);  // Merging


It is pretty much necessary to pass the raw data through an averaging filter, preferably removing samples that could skew the data. Here's an example with ADC raw data ADC DMA Temperature Sensor.
The sensor can be calibrated by placing offset values in OFF_X (0x10), OFF_Y (0x11), OFF_Z (0x12) registers. This picture explains how to read the sensor data (0x29 = MSB) (0x28 = LSB):




















Next step is figuring out tilt of the board using the acceleration data. The picture below shows a reference point (sensor/board orientation) that I used for writing the code. When resting on a table, the accelerometer Z-axis should be outputting -1000mg (-9.8m/s^2) (disregarding altitude, latitude, and whatnot). Both X and Y axes should be 0. This picture helps visualize the tilt from the original position (90 degrees for both Z-X and Z-Y planes). Note that the Z-axis is different from the right-hand Cartesian coordinate system, it is flipped.

























These are the kind of values I get when the board is just sitting on the table. As expected, both Z-Y and Z-X are 90 degrees (refer to the picture above). X, Y, Z values are given in meters per second squared (the signal is noisy even after an averaging filter so there's some work to be done here). Tilt is calculated by using this formula (arctan(opposite/adjacent)/pi)*180 (e.g. (arctan(Z/Y)/pi)*180).
X and Y are not exactly 0, Z is not exactly -9.8m/s^2, but close enough for the time being.




































The code below can be a starting point for further improvement.
#include "std_periph_and_stuff.h"


uint8_t i;
uint8_t MSB, LSB;
int16_t Xg, Zg;                                 // 16-bit values from accelerometer
int16_t x_array[100];                           // 100 samples for X-axis
int16_t z_array[100];                           // 100 samples for Z-axis
float x_average;                                // x average of samples
float z_average;                                // z average of samples
float zx_theta;                                 // degrees between Z and X planes
char print_buffer[20];                          // printing the values in Putty


void SPI_send(uint8_t address, uint8_t data);
uint8_t SPI_read(uint8_t address);
void Sort_Signed(int16_t A[], uint8_t L);       // Bubble sort min to max, input: Array/Length
float gToDegrees(float V, float H);             // output: degrees between two planes, input: Vertical/Horizontal force


int main(void)
{
  SPI_InitTypeDef SPI_InitTypeDefStruct;
  GPIO_InitTypeDef GPIO_InitTypeDefStruct;
 
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE , ENABLE);
 
  SPI_InitTypeDefStruct.SPI_Direction         = SPI_Direction_2Lines_FullDuplex;
  SPI_InitTypeDefStruct.SPI_Mode              = SPI_Mode_Master;
  SPI_InitTypeDefStruct.SPI_DataSize          = SPI_DataSize_8b;
  SPI_InitTypeDefStruct.SPI_CPOL              = SPI_CPOL_High;
  SPI_InitTypeDefStruct.SPI_CPHA              = SPI_CPHA_2Edge;
  SPI_InitTypeDefStruct.SPI_NSS               = SPI_NSS_Soft | SPI_NSSInternalSoft_Set;
  SPI_InitTypeDefStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;
  SPI_InitTypeDefStruct.SPI_FirstBit          = SPI_FirstBit_MSB;
  SPI_Init(SPI1, &SPI_InitTypeDefStruct);

  GPIO_InitTypeDefStruct.GPIO_Pin   = GPIO_Pin_5 | GPIO_Pin_7 | GPIO_Pin_6;
  GPIO_InitTypeDefStruct.GPIO_Mode  = GPIO_Mode_AF;
  GPIO_InitTypeDefStruct.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitTypeDefStruct.GPIO_OType = GPIO_OType_PP;
  GPIO_InitTypeDefStruct.GPIO_PuPd  = GPIO_PuPd_NOPULL;
  GPIO_Init(GPIOA, &GPIO_InitTypeDefStruct);

  GPIO_InitTypeDefStruct.GPIO_Pin   = GPIO_Pin_3;
  GPIO_InitTypeDefStruct.GPIO_Mode  = GPIO_Mode_OUT;
  GPIO_InitTypeDefStruct.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitTypeDefStruct.GPIO_PuPd  = GPIO_PuPd_UP;
  GPIO_InitTypeDefStruct.GPIO_OType = GPIO_OType_PP;
  GPIO_Init(GPIOE, &GPIO_InitTypeDefStruct);

  GPIO_PinAFConfig(GPIOA, GPIO_PinSource5, GPIO_AF_SPI1);
  GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_SPI1);
  GPIO_PinAFConfig(GPIOA, GPIO_PinSource7, GPIO_AF_SPI1);

  GPIO_SetBits(GPIOE, GPIO_Pin_3);
  SPI_Cmd(SPI1, ENABLE);
  
  SPI_send(0x23, 0xc9);                         // resetting the accelerometer internal circuit
  SPI_send(0x20, 0x67);                         // 100Hz data update rate, block data update disable, x/y/z enabled 
  SPI_send(0x24, 0x20);                         // Anti aliasing filter bandwidth 800Hz, 16G (very sensitive), no self-test, 4-wire interface
  SPI_send(0x10, 0x00);                         // Output(X) = Measurement(X) - OFFSET(X) * 32;
  SPI_send(0x11, 0x00);                         // Output(Y) = Measurement(Y) - OFFSET(Y) * 32;
  SPI_send(0x12, 0x00);                         // Output(Z) = Measurement(Z) - OFFSET(Z) * 32;

  while(1)                                      // X and Z axes example
  {
    for(i = 0; i < 100; i++)                    // getting 100 samples
    {
      MSB = SPI_read(0x29);                     // X-axis MSB
      LSB = SPI_read(0x28);                     // X-axis LSB
      Xg = (MSB << 8) | (LSB);                  // Merging
      x_array[i] = Xg;
   
      MSB = SPI_read(0x2d);                     // Z-axis MSB
      LSB = SPI_read(0x2c);                     // Z-axis LSB
      Zg = (MSB << 8) | (LSB);                  // Merging
      z_array[i] = Zg;
    }
 
    Sort_Signed(x_array, 100);                  // Sorting min to max
    Sort_Signed(z_array, 100);                  // Sorting min to max
 
    x_average = 0;
    z_average = 0;
    for(i = 10; i < 90; i++)                    // removing 10 samples from bottom and 10 from top
    {
      x_average += x_array[i];                  // summing up
      z_average += z_array[i];                  // summing up
    }
 
    x_average /= 80;                            // dividing by the number of samples used
    x_average /= -141;                          // converting to meters per second squared
 
    z_average /= 80;                            // dividing by the number of samples used
    z_average /= -141;                          // converting to meters per second squared
 
    zx_theta = gToDegrees(z, x);                // getting the degrees between Z and X planes
 
    sprintf(print_buffer, "x: %.0f\tz: %.0f\tZ-X: %.0f", x_average, -z_average, zx_theta);
 
    (void)SHELL->Print(print_buffer);           // printing in Putty
  }
}



void SPI_send(uint8_t address, uint8_t data)
{
  GPIO_ResetBits(GPIOE, GPIO_Pin_3);
 
  while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); 
  SPI_I2S_SendData(SPI1, address);
  while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
  SPI_I2S_ReceiveData(SPI1);
 
  while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); 
  SPI_I2S_SendData(SPI1, data);
  while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
  SPI_I2S_ReceiveData(SPI1);
 
  GPIO_SetBits(GPIOE, GPIO_Pin_3);
}



uint8_t SPI_read(uint8_t address)
{
  GPIO_ResetBits(GPIOE, GPIO_Pin_3); 
  address = 0x80 | address;                         // 0b10 - reading and clearing status
  
  while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); 
  SPI_I2S_SendData(SPI1, address);
  while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
  SPI_I2S_ReceiveData(SPI1);
 
  while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); 
  SPI_I2S_SendData(SPI1, 0x00);
  while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
 
  GPIO_SetBits(GPIOE, GPIO_Pin_3);
 
  return SPI_I2S_ReceiveData(SPI1);
}



void Sort_Signed(int16_t A[], uint8_t L)
{
  uint8_t i = 0;
  uint8_t status = 1;
 
  while(status == 1)
  {
    status = 0;
    for(i = 0; i < L-1; i++)
    {
      if (A[i] > A[i+1])
      {
        A[i]^=A[i+1];
        A[i+1]^=A[i];
        A[i]^=A[i+1];
        status = 1;    
      }
    }
  }
}



float gToDegrees(float V, float H)               // refer to the orientation pic above
{
  float retval;
  uint16_t orientation;
 
  if (H == 0) H = 0.001;                         // preventing division by zero
  if (V == 0) V = 0.001;                         // preventing division by zero
 
  if ((H > 0) && (V > 0)) orientation = 0;
  if ((H < 0) && (V > 0)) orientation = 90; 
  if ((H < 0) && (V < 0)) orientation = 180;
  if ((H > 0) && (V < 0)) orientation = 270;
 
  retval = ((atan(V/H)/3.14159)*180);
  if (retval < 0) retval += 90;
  retval = abs(retval) + orientation;
  return retval;
}