0
votes

So I'm learning embedded development, and I recently learned the basics of SPI. As a project, I wanted to communicate with the LIS3DSH accelerometer on my STM32F407G-DISC1 board using only the CMSIS headers.

I pasted the entire code below, but I'll explain it first because no one wants to read all that code.

As a reference, these are the pins needed (according to the MCU's datasheet) to communicate via SPI:

  • PA5 - SPI1_SCK
  • PA7 - SPI1_MOSI
  • PA6 - SPI1_MISO
  • PE3 - CS_I2C/SPI

Here's the steps I took in my code:

  1. Enabled the clock for GPIOA and GPIOE using the AHB1ENR register.
  2. For GPIOA, I set the three pins as alternate function, output is push-pull, speed is low, no pull-up/pull-down, and configured the alternate function as SPI.
  3. For GPIOE, set it as GPIO mode, push-pull, low speed, pull-up, and then set it high (as in wrote to the BSSR register).
  4. Enabled the clock for SPI using the APB2ENR register.
  5. Configured SPI1: first disabled it, enabled 2-line unidirectional mode, set baud rate to fPCL/16 since the APB2 peripheral clock is 84MHz and the max clock of the accelerometer is 10MHz. Then set clock phase and polarity to 1. 8-bit data frame, MSB first, enabled software slave management, and also enabled master configuration. Finally, enabled SPI1.
  6. After all this, I transmit 0x63 to the 0x20 register of the accelerometer. This sets the output rate to 100Hz and enables both the x and y axis. I have no idea if this is actually working. I'm assuming it is because the TX buffer is empty when I check the SPI status register.
  7. Then to test whether I can recieve, I attempt to get the data from the WHO_AM_I register of the accelerometer. But I only see garbage data when I debug it (0xFF).

I've googled around to see why this may be, and a lot of people suggested that the clock polarity and phase may be incorrect. However, I've checked it multiple times, and I'm fairly certain I configured it properly.

I've tried setting interrupts. During the interrupt, even though RXNE (RX buffer not empty) is true, it still reads only 0xFF. I'm stumped as to why this is happening.

The code is below. The starting point is accelerometer_init(). The reading of the data from the WHO_AM_I register is in turn_on_accelerometer().

#include <stdint.h>
#include <stdbool.h>
#include "stm32f4xx.h"
#include "accelerometer.h"

static void gpio_clock_enable(void);
static void gpio_a_init(void);
static void gpio_e_init(void);
static void accelerometer_clock_enable(void);
static void configure_accelerometer(void);
static void pull_slave_high(void);
static void pull_slave_low(void);
static void turn_on_accelerometer(void);
static void wait_till_transmit_complete(void);
static void transmit_only(uint8_t address, uint8_t data);
static void receive_dummy_data(void);

void accelerometer_init(void) {
    gpio_clock_enable();
    gpio_a_init();
    gpio_e_init();

    accelerometer_clock_enable();
    configure_accelerometer();
    turn_on_accelerometer();
}

void gpio_clock_enable(void) {
    RCC_TypeDef *rcc = RCC;
    rcc->AHB1ENR |= (1 << 0) | (1 << 4);
}

void gpio_a_init(void) {
    GPIO_TypeDef *gpio_a = GPIOA;

    // Reset mode and set as alternate function
    gpio_a->MODER &= ~(0x3 << 10) & ~(0x3 << 12) & ~(0x3 << 14);
    gpio_a->MODER |= (0x2 << 10) | (0x2 << 12) | (0x2 << 14);

    // Set output to PP
    gpio_a->OTYPER &= ~(1 << 5) & ~(1 << 6) & ~(1 << 7);

    // Set speed to low
    gpio_a->OSPEEDR &= ~(0x3 << 10) & ~(0x3 << 12) & ~(0x3 << 14);

    // Set to no pull-up / pull-down
    gpio_a->PUPDR &= ~(0x3 << 10) & ~(0x3 << 12) & ~(0x3 << 14);

    // Reset alternate function and set to SPI
    gpio_a->AFR[0] &= ~(0xF << 20) & ~(0xF << 24) & ~(0xF << 28);
    gpio_a->AFR[0] |= (0x5 << 20) | (0x5 << 24) | (0x5 << 28);
}

void gpio_e_init(void) {
    GPIO_TypeDef *gpio_e = GPIOE;

    // Set as general purpose output mode
    gpio_e->MODER &= ~(0x3 << 6);
    gpio_e->MODER |= (1 << 6);

    // Set as push pull
    gpio_e->OTYPER &= ~(1 << 3);

    // Set as low speed
    gpio_e->OSPEEDR &= ~(0x3 << 6);

    // Set to pull up
    gpio_e->PUPDR &= ~(0x3 << 6);
    gpio_e->PUPDR |= (1 << 6);

    // Set it high
    pull_slave_high();
}

void accelerometer_clock_enable(void) {
    RCC_TypeDef *rcc = RCC;
    rcc->APB2ENR |= (1 << 12);
}

void configure_accelerometer(void) {
    SPI_TypeDef *spi_1 = SPI1;

    // First disable it while we configure SPI
    spi_1->CR1 &= ~(1 << 6);

    // 2-line unidirectional data mode enabled
    spi_1->CR1 &= ~(1 << 15);

    // Reset baud rate and set to fPCLK/16
    // because APB2 peripheral clock currently is 84 MHz
    // and the max clock of the accelerometer is 10 MHz.
    spi_1->CR1 &= ~(0x7 << 3);
    spi_1->CR1 |= (0x3 << 3);

    // Set clock phase to 1
    spi_1->CR1 |= (1 << 0);

    // Set clock polarity to 1
    spi_1->CR1 |= (1 << 1);

    // 8 bit data frame format
    spi_1->CR1 &= ~(1 << 11);

    // MSB first
    spi_1->CR1 &= ~(1 << 7);

    // Software slave management enabled
    spi_1->CR1 |= (1 << 9);
    spi_1->CR1 |= (1 << 8);

    // Master configuration enabled
    spi_1->CR1 |= (1 << 2);

    // SS output enabled
//    spi_1->CR2 |= (1 << 2);

    // Enable SPI
    spi_1->CR1 |= (1 << 6);

    // Wait a little bit for accelerometer to turn on
    for (int i=0; i<1000000; i++);
}

void pull_slave_high(void) {
    // Wait until SPI is no longer busy
    SPI_TypeDef *spi_1 = SPI1;
    while ((spi_1->SR >> 7) & 1);

    GPIO_TypeDef *gpio_e = GPIOE;
    gpio_e->BSRR |= (1 << 19);
}

void pull_slave_low(void) {
    // Wait until SPI is no longer busy
    SPI_TypeDef *spi_1 = SPI1;
    while ((spi_1->SR >> 7) & 1);

    GPIO_TypeDef *gpio_e = GPIOE;
    gpio_e->BSRR |= (1 << 3);
}

void turn_on_accelerometer(void) {
    // Set output data rate to 100Hz
    // and enable X-axis, Y-axis.
    transmit_only(0x20, 0x63);
    receive_dummy_data();

    // Temp test checking the WHO_AM_I register on the accelerometer.
    SPI_TypeDef *spi_1 = SPI1;
    pull_slave_low();
    wait_till_transmit_complete();
    uint8_t address = 0x0F | 0x80;
    spi_1->DR = address;
    wait_till_transmit_complete();

    while (true) {
        volatile bool is_busy = (spi_1->SR >> 7) & 1;
        volatile bool is_rx_buffer_not_empty = (spi_1->SR >> 0) & 1;

        if (!is_busy && is_rx_buffer_not_empty) {
            break;
        }
    }
    volatile uint32_t data = spi_1->DR;
    pull_slave_high();
}

/*
 * Transmit is synchronous.
 */
void transmit_only(uint8_t address, uint8_t data) {
    SPI_TypeDef *spi_1 = SPI1;

    // Select the accelerometer as the slave
    pull_slave_low();

    // Wait till transmit buffer is ready
    wait_till_transmit_complete();

    spi_1->DR = address;

    // Wait till transmit buffer is ready
    wait_till_transmit_complete();

    spi_1->DR = data;

    // Wait till transmit buffer has been read
    wait_till_transmit_complete();

    // Deselect the slave
    pull_slave_high();
}

void wait_till_transmit_complete(void) {
    SPI_TypeDef *spi_1 = SPI1;

    while (true) {
        volatile bool is_busy = (spi_1->SR >> 7) & 1;
        volatile bool is_transmit_buffer_empty = (spi_1->SR >> 1) & 1;

        if (!is_busy && is_transmit_buffer_empty) {
            break;
        }
    }
}

void receive_dummy_data(void) {
    SPI_TypeDef *spi_1 = SPI1;
    spi_1->DR;
    spi_1->SR;
}
1
Use a logic analyzer or oscilloscope to verify whether the signals are working as expected. Is CS going low? Is SCK toggling? Is the data on MOSI/MISO what you expect? It won't hurt to try the other three combinations of clock polarity and phase.kkrambo
I agree with @kkrambo. The best would be to probe the wires with an oscilloscope. If you only receive 0xFF then it seems to me that the accelerometer is not responding, as that could just be the default state of the line (high data idle, or pull-ups).Hein Wessels
Indeed you can't even start consider an application like this if you don't have a scope. It is a mandatory tool for all embedded software development.Lundin
The maximum frequency of any GPIO at any particular "speed" setting depends your supply voltage and load capacitance. The "low speed" setting is marginal at best at the frequency you are clocking (5.25MHz?). You should use at least "Medium" speed mode. This is where a scope becomes essential for verifying signal integrity and timing. If the line is too "slow", the clock signal might not be valid due to excessively low slew rate.Clifford
... or divide PCLK still further - you don't need a very high rate to get the accelerometer data out in time. For two 16 bit registers at 100sps, 10KHz would be more than fast enough.Clifford

1 Answers

1
votes

You are working with SPI incorrectly.

This bus works in this way:

  • master (MCU) sends byte in MOSI
  • line at the same (!) time slave (LIS) sends byte in MISO line. In this moment slave dont know, what exactly byte master transfers to it.

To transfer one byte, you should:

  • write byte in data register
  • wait for completition of transfer
  • read data register

Thus, to read WHO_AM_I register, we obtain next sequence:

  • init SPI
  • flush data register (just read SPI->DR)
  • send command
  • wait
  • read dummy data (your 0xFF)
  • write second byte (0x00 or 0xFF, it doesn't matter)
  • wait
  • read correct answer from LIS