Electronics - STM32 - Reading gyroscope data with SPI on STM32F3Discovery

So far we have blinked LEDs, rotated them with a timer, caught a button interrupt, and sent text over UART.

Time to talk to an external chip.

In this tutorial we are going to read angular velocity on X, Y, Z axes from the L3GD20 gyroscope soldered on the STM32F3Discovery board.

We'll use the SPI1 peripheral in bare metal C in order to decode a live transaction on a logic analyzer but also directly from the OpenOCD console.

Let's get started.

Shopping list

First of ​all

The L3GD20 is a separate chip physically soldered on the PCB next to the MCU.

The two chips are connected by 4 copper traces on the board, that is the SPI bus.

That's why we need to set this SPI bus.

On STM32, every peripheral is off by default to save power.

Before using any peripheral, you must enable its clock via RCC.

Without the clock, writing to peripheral registers does nothing.

We then enable them:

  • RCC->AHBENR  |= RCC_AHBENR_IOPAEN;   // enable clock for GPIOA (SCK, MISO, MOSI)
  • RCC->AHBENR  |= RCC_AHBENR_IOPEEN;   // enable clock for GPIOE (CS pin PE3)
  • RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;  // enable clock for SPI1 peripheral itself
 

Three peripherals, three clock enables.

Miss one and nothing works.

Of course, we enable clocks only on the peripherals used in our project (GPIOA, GPIOE and SPI1).

But before going further let's have a look at some glossary explanation.

Peripheral Glossary

  • RCC (Reset and Clock Control): enables and disables clocks for all peripherals
  • GPIO (General Purpose Input/Output): controls physical pins of the MCU
  • SPI (Serial Peripheral Interface): hardware peripheral handling SPI communication
  • AHB (Advanced High-performance Bus): bus connecting GPIO peripherals to the MCU core
  • APB2 (Advanced Peripheral Bus 2): bus connecting SPI1 to the MCU core

Register Glossary

RCC registers

  • RCC->AHBENR (AHB Enable Register): enable clocks for GPIO ports
  • RCC->APB2ENR (APB2 Enable Register): enable clock for SPI1

GPIO registers

  • GPIO->MODER (Mode Register): pin direction, Input/Output/AF/Analog, 2 bits per pin
  • GPIO->OSPEEDR (Output Speed Register): slew rate, Low/Medium/High/Very High, 2 bits per pin
  • GPIO->AFR[0] (Alternate Function Register Low): select AF0..AF15 for pins 0..7, 4 bits per pin
  • GPIO->AFR[1] (Alternate Function Register High): select AF0..AF15 for pins 8..15, 4 bits per pin
  • GPIO->BSRR (Bit Set/Reset Register): atomically set or reset individual pins

SPI registers

  • SPI->CR1 (Control Register 1): master/slave, CPOL/CPHA, baud rate, NSS, enable
  • SPI->CR2 (Control Register 2): data frame size, FIFO threshold
  • SPI->SR (Status Register): TXE, RXNE, BSY flags
  • SPI->DR (Data Register): write to transmit, read to receive

SPI Protocol

SPI uses 4 wires:

  • SCK (Serial Clock): generated by master, drives all transfers
  • MOSI (Master Out Slave In): commands sent from STM32 to L3GD20
  • MISO (Master In Slave Out): data returned from L3GD20 to STM32
  • NSS (Negative Slave Select): active LOW, pull to 0 to select the slave

NSS is also called CS (Chip Select). "Negative" means active LOW:

  • NSS = 0 (LOW): slave selected, transfer active
  • NSS = 1 (HIGH): slave deselected, ignores the bus

SPI is full-duplex: MOSI and MISO transfer simultaneously on every clock edge.

When we send a dummy 0x00 byte to read data, a real byte is being sent at the same time on MOSI, that is just how the protocol works.

Software CS vs hardware NSS

The STM32 SPI peripheral has a dedicated NSS pin.

In this exercise we manage CS manually via PE3, a plain GPIO output.

This gives full control over CS timing and avoids the constraints of hardware NSS management.

Since we use software CS, we set SSM=1 and SSI=1 in CR1.

SSM tells the hardware to ignore the NSS pin.

SSI forces the internal NSS signal HIGH to prevent MODF (mode fault) errors that would trigger if the hardware saw NSS as LOW while in master mode.

SPI Mode 3

The L3GD20 requires SPI Mode 3, defined by two parameters:

  • CPOL = 1: clock idles HIGH
  • CPHA = 1: data sampled on the 2nd (falling) edge
SCK  :  ```\___/```\___/```   (idles HIGH, CPOL=1)
MOSI :  ====X  valid  X====   (data changes on rising edge)
MISO :  ====X  valid  X====   (data sampled on falling edge, CPHA=1)
NSS  :  ```\_______________/  (active LOW during transfer)

 

Logic Analyzer Setup (Saleae Logic)

Wire connections

  • Black wire, CH0 (GND), connected to GND on the board
  • Brown wire, CH1, connected to PA5 (SCK)
  • Red wire, CH2, connected to PA7 (MOSI)
  • Orange wire, CH3, connected to PA6 (MISO)
  • Yellow wire, CH4, connected to PE3 (CS)

Logic 2 SPI decoder settings

  • Clock: CH1
  • MOSI: CH2
  • MISO: CH3
  • Enable (CS): CH4
  • Clock State: Clock is high when inactive (CPOL=1)
  • Clock Phase: Data is valid on clock trailing edge (CPHA=1)
  • Significant Bit: Most significant bit first
  • Bits per transfer: 8
  • Enable line: Active low

Decoding a captured frame

Example captured on logic analyzer:

MISO: 0x00  0x1D  0xFC  0x7B  0x02  0x7C  0x00
X = (0xFC << 8) | 0x1D = 0xFC1D = -995
Y = (0x02 << 8) | 0x7B = 0x027B =  635
Z = (0x00 << 8) | 0x7C = 0x007C =  124

Terminal output for the same frame:

  -995      635      124

Exact match, SPI communication validated.

How to freeze one frame and compare

Add a getchar() in the main loop to step through one frame at a time:

while (1) {
    l3gd20_read_xyz(&x, &y, &z);
    printf("%6d   %6d   %6d\r\n", (int)x, (int)y, (int)z);
    getchar();  // press Enter to advance one frame
    leds_show_rotation(z);
    delay_ms(200);
}

Capture on the logic analyzer, press Enter in the OpenOCD console, then compare the decoded bytes with the printed values.

Building, flashing, monitoring

To build, flash and monitor in one command, use the run.sh file provided in this tutorial.

Simply do from the firmware/stm32f3discovery folder:

$ ./run.sh all

Conclusion

We have configured SPI1 from scratch, read raw gyroscope data from the L3GD20, and verified every byte on a logic analyzer.

The data matches, the protocol is understood.

Good job, you did it cool

 

Add new comment

Plain text

  • No HTML tags allowed.
  • Lines and paragraphs break automatically.