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
- The Github for the current tutorial with full files and code:
- IDE:
- VSCode
- DevContainer extension
- Board:
- STM32F3Discovery
- L3GD20 3-axis MEMS gyroscope (U5, small chip near center)
- Datasheet:
- Logic Analyzer for SPI communication:
- Saleae Logic with Logic 2 software:
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 viaPE3, 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.