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;
}

16 comments:

  1. Why do you set GPIO_SetBits(GPIOE, GPIO_Pin_3) if you are using SPI?
    Data sheet says
    I2C/SPI mode selection (1: SPI idle mode / I2C communication
    enabled; 0: SPI communication mode / I2C disabled)

    I'm trying to communicate wirh my LIS3DSH on Stm32F4Discovery and now I'm confusued, should i set or reset CS bit?

    ReplyDelete
    Replies
    1. If you look at the timing diagram (top of the page), there's a signal labeled NSS (N - negative (active low), SS - slave select). Active low means that the master (STM32F4) can select a slave (LIS3DSH) by pulling the line from high to low. When idling (right after the setup is complete), this NSS signal should stay high (set) and this is exactly the reason why I set GPIOE Pin 3.

      If you look at the SPI_read() or SPI_write() functions, you can see that the first thing I do is reset the NSS (GPIO_ResetBits(GPIOE, GPIO_Pin_3);).

      Here's what typically happens to the NSS (GPIOE Pin 3) signal:
      1 - NSS is high (start point)
      2 - NSS goes low (you read/write from/to slave)
      3 - NSS goes high (idling)
      4 - NSS goes low (you read/write from/to slave)
      5 - NSS goes high again (idling)

      Delete
    2. Hi, my name is ali imran.. can you please tell me on which board you have tested this code.. I first have written the spi communication by myself but the accelerometer kept replying with bytes 0xff for whatever read I was performing. Then I've tried your code and the mems replies again with bytes 0xff. I am testing on stm32f407vg discovery Rev C with LIS3DSH MEMS on it.
      Thank you

      Delete
  2. Currently, I'm programming one code to read the acceleration from KXP84-2050. Unfortunately, although I followed your code to program but it only shows me the zero value of analog acceleration input throug hyperterminal
    http://www.mediafire.com/download/qtefzn3tk222so2/spi+v3.rar

    ReplyDelete
    Replies
    1. Is your hardware setup correct? Correct GPIOs initialized, correct SPI block used?

      If so, there are two parts in the code that are specific to (LIS3DSH) that you need to modify for (KXP84-2050):

      1) In SPI_read() function:
      address = 0x80 | address; // 0b10 - reading and clearing status
      You may not need this line at all or it may need to be modified, check the (KXP84-2050) documentation for how the data is read from the accelerometer.

      2) 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;
      Obviously, the values/addresses that I'm sending are specific to (LIS3DSH).

      Delete
  3. Yes I set up those parameters following the datasheet of KXP84 (attached below) and my micro controller is stm32f401 RE

    http://www.mediafire.com/download/lca8ytvxiuwidic/AE-KXP84.rar

    ReplyDelete
  4. For the hardware , PA5 (SCK of stm32f401 nucleo board) - 5 sclk (KXP) ; PA6(MISO) - 7 (SDO) ; PA7 (MOSI) - 9 (SDI) ; PB6 ( normal output) - 10 (CS). so, is it correct?
    Many thanks.

    ReplyDelete
  5. Hi, my name is George.. can you please tell me on which board you have tested this code.. I first have written the spi communication by myself but the accelerometer kept replying with bytes 0xff for whatever read I was performing. Then I've tried your code and the mems replies again with bytes 0xff. I am testing on stm32f407vg discovery Rev C with LIS3DSH MEMS on it.
    Thank you

    ReplyDelete
  6. i am finding same problem as George mention above please Help...

    ReplyDelete
  7. What is the -141 pls describing me?

    ReplyDelete
  8. hello sergey,

    thanks for a very helpful website with lots of good code.
    I am implementing a H3LIS200DL accelerometer using the stm32f4. This accelerometer happens to have many of the same registers as the LIS3DSH. So far, I am able to get the H3LIS200DL to respond to the "WHO_AM_I" command, using this:
    SPI_send( LIS200DL_WHO_AM_I, 0x0 );
    uint8_t rtnValue = SPI_read( LIS200DL_WHO_AM_I );
    printf("ID = %x\n", rtnValue); //reads back 32, correct

    //configuration
    SPI_send( LIS200DL_CTRL_REG1, 0x27 );
    rtnValue = SPI_read( LIS200DL_CTRL_REG1 );
    printf("ctl = %x\n", rtnValue); //reads back 0x27

    In main, running at 100ms timing, I have this:
    uint8_t dataX = SPI_read( 0x29 );
    if( dataX > 1 ) printf("LIS200DL X value = %x\n", dataX);
    uint8_t dataY = SPI_read( 0x2B );
    if( dataY > 1 )printf("LIS200DL Y value = %x\n", dataY);
    uint8_t dataZ = SPI_read( 0x2D );
    if( dataZ > 1 )printf("LIS200DL Z value = %x\n", dataZ);

    No matter what I do with the board (move it abruptly) it does not return anything other than a 0. I'm not sure if there is a problem?
    Right now, I'm just trying to get raw data out and I can't seem to do that. Can you suggest what I could try?

    Thanks!
    Roger



    ReplyDelete
    Replies
    1. Hi Roger,

      1. Check the hardware - the SPI bus. Since you are able to get a response to the "WHO_AM_I" command, it most likely means that physical SPI communication works. In case you are not sure, you can try increasing the delay between consecutive SPI reads and reducing SPI clock frequency.

      2. You would have to check H3LIS200DL datasheet or reference manual to see how it needs to be configured before it starts sampling the analog acceleration into its registers. Some accelerometers need to be configured and then enabled (sort of like a "start" command).

      3. In my example, SPI_read() has this line of code:
      address = 0x80 | address; // 0b10 - reading and clearing status
      Please note that this is specific to LIS3DSH accelerometer, your may not need that line at all for H3LIS200DL. You would have to check the documentation for that.

      Please let me know if you get it to work and perhaps share how you resolved the issue.

      Best regards,
      Sergey

      Delete
  9. Hi sergey,

    after a few days of poking at it when I had time,I got it working. The problem is, the device seems to have very poor response for some unknown reason. I configured it for interrupt mode, and set the threshold register at just 1g. However, even if I move the enclosure very fast, and abruptly, it sometimes does not trip at all. The values it registers are much lower than what I know should be there. I know it's a 100g device, but the way I am shaking the enclosure it must be at least 5g to 10g of acceleration! Here's my configuration after initialization of the SPI.

    SPI_send( LIS200DL_WHO_AM_I, 0x0 );
    rtnValue = SPI_read( LIS200DL_WHO_AM_I );
    printf("ID2 = %x\n", rtnValue);

    SPI_send( LIS200DL_CTRL_REG1, 0x27 );
    rtnValue = SPI_read( LIS200DL_CTRL_REG1 );
    printf("ctl1 = %x\n", rtnValue);

    //configure interrupt register
    SPI_send( LIS200DL_INT1CFG, 0x2A );
    rtnValue = SPI_read( LIS200DL_INT1CFG );
    printf("int1 reg = %x\n", rtnValue);

    //set threshold register = 1
    SPI_send( LIS200DL_INT1THS, 0x1 );
    rtnValue = SPI_read( LIS200DL_INT1THS );
    printf("thr reg = %x\n", rtnValue);

    The read return values for the threshold register agree with what I have sent. When I get an interrupt, I do a simple read:
    int8_t x = SPI_read( LIS200DL_OUT_X );
    int8_t y = SPI_read( LIS200DL_OUT_Y );
    int8_t z = SPI_read( LIS200DL_OUT_Z );

    And then perform the bit manipulations: (same for Y and Z)

    float32_t cX;

    if( x & 0x80 )
    {
    //it's negative, invert result
    val = x ^ 1;
    val += 0x1;
    cX = (val * 0.780);
    //printf("-X acc value = %.1f\n", cX);
    }
    else
    {
    cX = (x * 0.780);
    //printf("+X acc value = %.1f\n", cX);
    }

    As you might have experienced,trying to get any technical support from ST is useless. It's the second board and it's the same as the first. It would be nice to know if it's something I am missing....configuration...reading...

    Gary

    ReplyDelete
  10. hi sergey,
    after some more setting of registers, it might be working ok, but I need to confirm it in application. I set this register:
    SPI_send( LIS200DL_CTRL_REG1, 0x2F );
    which increased the data rate and seemed to make it somewhat more sensitive. I have gone through the other register settings and at this point I do not think any other configurations are necessary, just some testing. will keep you posted!

    thanks

    ReplyDelete
    Replies
    1. Hi Gary,

      Thanks for sharing this information; it will be useful for people using H3LIS200DL.

      Are you able to read roughly 1g or 9.8m/s^2 on the Z-axis when you have the accelerometer sitting on a table?

      I'm also thinking that you could try reading multiple samples from each axis and then average them to get more meaningful results.
      In my example, I'm reading 100 samples from each axis (3*100 SPI reads), then sort the data, then remove 10 samples from top and 10 from bottom (in case there are outliers), and finally average the remaining 80 samples.

      Best regards,
      Sergey

      Delete