SPI using Registers in STM32
This is another tutorial in the register based series for STM32, and today we will cover the SPI. I will cover both sending and receiving data in the master mode. Also I am going to use the ADXL345 for the demonstration
SPI communication uses 4 Pins i.e
- MOSI Master Out Slave In is used to send the data to the Slave
- MISO Master in Slave out is used to receive the data from the slave
- SCK Serial Clock is used to keep the master and slave in sync
- SS Slave select is used to select and unselect the slave
- In Full-duplex mode, all four pins are used
- There are separate pins for send (MOSI) and for receive (MISO)
- When we write data into the Data Register of the master, this data goes into the Tx Shift Register of the master
- Which then gets shifted to the Rx Shift Register of the Slave
- And finally goes into the data register of the slave, from where it is read by the slave device.
Slave select (NSS) pin management
In Master mode, Slave Select management can be done using two different ways
- Software NSS management : This means that we want to manage the slave using the software. So we don’t have to rely on the physical NSS pin, and instead we can use any pin from the mcu as the Slave Select pin.
- Hardware NSS management : Here we have to use the actual NSS pin (fixed pin) to control the slave device. The NSS signal is driven low as soon as the SPI is enabled in master mode, and is kept low until the SPI is disabled.
We will be using the software NSS management in this tutorial.
SPI Configuration
The configuration for the SPI is relatively simpler than what we have seen in other peripherals. Here we only have to modify 1 register. Let’s see the steps to configure the SPI
/************** STEPS TO FOLLOW *****************
1. Enable SPI clock
2. Configure the Control Register 1
3. Configure the CR2
************************************************/
Like I mentioned, it’s very simple to configure the SPI. See the configuration code below
void SPIConfig (void)
{
RCC->APB2ENR |= (1<<12); // Enable SPI1 CLock
SPI1->CR1 |= (1<<0)|(1<<1); // CPOL=1, CPHA=1
SPI1->CR1 |= (1<<2); // Master Mode
SPI1->CR1 |= (3<<3); // BR[2:0] = 011: fPCLK/16, PCLK2 = 80MHz, SPI clk = 5MHz
SPI1->CR1 &= ~(1<<7); // LSBFIRST = 0, MSB first
SPI1->CR1 |= (1<<8) | (1<<9); // SSM=1, SSi=1 -> Software Slave Management
SPI1->CR1 &= ~(1<<10); // RXONLY = 0, full-duplex
SPI1->CR1 &= ~(1<<11); // DFF=0, 8 bit data
SPI1->CR2 = 0;
}
- First of all enable the SPI 1 clock in the RCC_APB2ENR Register
- Now will modify the CPOL and CPHA bits according to the slave requirement ( watch video if you don’t know what this means)
- Enable the master mode
- Next is the prescalar. The slave device can support upto 5 MBits/s, so I am keeping the presclalar of 16
This will divide the APB2 clock (80 MHz) by 16, and bring it down to 5MHz - The data format is selected as MSB first
- Then we will configure the software slave management. Like I said in the beginning, we will use the software to control the slave, so we need to set these SSM and SSI bits
- Next we will configure the full duplex mode by resetting the RXONLY (10th) bit
- Next is the data length bit, and we will keep the 8 bit data.
- Then reset the entire CR2 register, since we will not be setting up any DMA or Interrupt in this tutorial
SPI TRANSMIT
- Before Transmitting the data, we will wait for the TXE (Transmit Register Empty) bit in the Status Register to set. This indicates that the Transmit Register is empty and we can load the data
- After transmitting the data, we will wait for the BSY (Busy) bit in the Status Register to reset. This will indicate that the SPI is not busy in communication anymore and we can proceed with other things
void SPI_Transmit (uint8_t *data, int size)
{
/************** STEPS TO FOLLOW *****************
1. Wait for the TXE bit to set in the Status Register
2. Write the data to the Data Register
3. After the data has been transmitted, wait for the BSY bit to reset in Status Register
4. Clear the Overrun flag by reading DR and SR
************************************************/
int i=0;
while (i<size)
{
while (!((SPI1->SR)&(1<<1))) {}; // wait for TXE bit to set -> This will indicate that the buffer is empty
SPI1->DR = data[i]; // load the data into the Data Register
i++;
}
/*During discontinuous communications, there is a 2 APB clock period delay between the
write operation to the SPI_DR register and BSY bit setting. As a consequence it is
mandatory to wait first until TXE is set and then until BSY is cleared after writing the last
data.
*/
while (!((SPI1->SR)&(1<<1))) {}; // wait for TXE bit to set -> This will indicate that the buffer is empty
while (((SPI1->SR)&(1<<7))) {}; // wait for BSY bit to Reset -> This will indicate that SPI is not busy in communication
// Clear the Overrun flag by reading DR and SR
uint8_t temp = SPI1->DR;
temp = SPI1->SR;
}
- Here we are waiting for the TXE bit to set before sending the data
- To send the data, we have to copy it in the DR (Data Register)
- After all the data has been transmitted, we will wait for the busy flag to reset
- Before exiting the Transmit function, we will make a dummy read to the data register and the status register.
- This is to clear the overrun flag, which gets set when we transfer the data to the device.
SPI RECEIVE
The receiving process is as follows:-
- Before receiving the data, we must send some dummy byte to the device. Since the slave device is in the transmission mode, this dummy byte does not change the registers or the data of the registers
- On receiving the dummy byte, the device transmits one byte of data.
- This will set the RXNE (Receive Buffer Not Empty) bit. This will indicate that there is some data in the Data Register, which is ready to be read.
- And later we can copy the data from the Data Register into our buffer. This clears the RXNE bit
void SPI_Receive (uint8_t *data, int size)
{
/************** STEPS TO FOLLOW *****************
1. Wait for the BSY bit to reset in Status Register
2. Send some Dummy data before reading the DATA
3. Wait for the RXNE bit to Set in the status Register
4. Read data from Data Register
************************************************/
while (size)
{
while (((SPI1->SR)&(1<<7))) {}; // wait for BSY bit to Reset -> This will indicate that SPI is not busy in communication
SPI1->DR = 0; // send dummy data
while (!((SPI1->SR) &(1<<0))){}; // Wait for RXNE to set -> This will indicate that the Rx buffer is not empty
*data++ = (SPI1->DR);
size--;
}
}
- As you can see above, we wait for the busy flag to reset.
- Then we send some dummy byte to the device. I am transmitting 0
- next we wait for the RXNE bit to set
- And finally copy the data from the DR into our buffer
GPIO CONFIGURATION
F4 GPIO CONFIG
I am using the following Pins for the SPI 1:
- PA5 -> CLK
- PA6 -> MISO
- PA7 -> MOSI
- PA9 -> Slave Select
The first three pins need to be set in the alternate function mode, and the PA9 will be set as the general output pin.
The Alternate function mode depends on the microcontroller. For example, let’s see the Alternate function description for F446RE
You can see in the picture above that the SPI 1 is the AF5. Also since I am using Pins PA5, 6 and 7, I am going to use the AFRL Register. In case you are using the Pins 8 to 15, you must use the AFRH register
Let’s see the configuration now
void GPIOConfig (void)
{
RCC->AHB1ENR |= (1<<0); // Enable GPIO Clock
GPIOA->MODER |= (2<<10)|(2<<12)|(2<<14)|(1<<18); // Alternate functions for PA5, PA6, PA7 and Output for PA9
GPIOA->OSPEEDR |= (3<<10)|(3<<12)|(3<<14)|(3<<18); // HIGH Speed for PA5, PA6, PA7, PA9
GPIOA->AFR[0] |= (5<<20)|(5<<24)|(5<<28); // AF5(SPI1) for PA5, PA6, PA7
}
- First we will enable the GPIOA clock
- Then select the Alternate Function mode for PA5, PA6 and PA7 and the output mode for PA9
- Next we will set the speed for all four pins. The speed is set to HIGH Speed
- And finally configure the Alternate Function in the AFR[0] (AFRL)
F103 GPIO CONFIG
The Configuration in F103 is slightly different. F103 do not have the Alternate Function Registers and therefore we have to use the Control Register to configure the SPI pins
Below is the Picture from the F103 Reference manual, suggesting the configuration for the SPI
Since we are using SPI in Master Full Duplex mode, we will configure the pins accordingly. I have already covered how to configure the GPIO in F103, you can check the article STM32F103 Clock Setup using Registers
void GPIOConfig (void)
{
RCC->APB2ENR |= (1<<2); // Enable GPIOA clock
GPIOA->CRL = 0;
GPIOA->CRL |= (11U<<20); // PA5 (SCK) AF output Push Pull
GPIOA->CRL |= (11U<<28); // PA7 (MOSI) AF output Push Pull
GPIOA->CRL |= (1<<26); // PA6 (MISO) Input mode (floating)
GPIOA->CRL |= (3<<16); // PA4 used for CS, GPIO Output
}
- Here we will enable the GPIOA clock
- Then reset the entire Control Register
- Now set the AF output Push Pull mode for PA5 -> Clock
- Do the Same for the PA7 -> MOSI
- We need to set the Alternate Function input mode for the PA6 -> MISO
- And set the general Output mode for the PA4 -> SS
Some Other Functions
Here are some other function that we will be using in this tutorial.
void SPI_Enable (void)
{
SPI1->CR1 |= (1<<6); // SPE=1, Peripheral enabled
}
void SPI_Disable (void)
{
SPI1->CR1 &= ~(1<<6); // SPE=0, Peripheral Disabled
}
void CS_Enable (void)
{
GPIOA->BSRR |= (1<<9)<<16;
}
void CS_Disable (void)
{
GPIOA->BSRR |= (1<<9);
}
float xg, yg, zg;
int16_t x,y,z;
uint8_t RxData[6];
void adxl_write (uint8_t address, uint8_t value)
{
uint8_t data[2];
data[0] = address|0x40; // multibyte write
data[1] = value;
CS_Enable (); // pull the cs pin low
SPI_Transmit (data, 2); // write data to register
CS_Disable (); // pull the cs pin high
}
void adxl_read (uint8_t address)
{
address |= 0x80; // read operation
address |= 0x40; // multibyte read
uint8_t rec;
CS_Enable (); // pull the pin low
SPI_Transmit (&address, 1); // send address
SPI_Receive (RxData, 6); // receive 6 bytes data
CS_Disable ();; // pull the pin high
}
void adxl_init (void)
{
adxl_write (0x31, 0x01); // data_format range= +- 4g
adxl_write (0x2d, 0x00); // reset all bits
adxl_write (0x2d, 0x08); // power_cntl measure and wake up 8hz
}
Here I am keeping the focus on the SPI, so we will see only the SPI related part. I am not going to talk about how the ADXL actually works. You can check out the other article I wrote about it ADXL345 Accelerometer and STM32
- To ENABLE the SPI, we will set the SPE bit (bit 6 in CR1)
- To DISABLE SPI, we will simply clear that bit
- The slave can be selected by Pulling the SS pin LOW
- And to release the Slave, we have to Pull the SS pin HIGH
In order to communicate with the slave device, we have to follow the procedure as mentioned below
To send the data
- Select the Slave device
- Send the register address
- Send the data
- Release the slave device
To read the data
- Select the slave device
- Send the Register address, from where you want to read
- Read the data
- Unselect the slave device
The MAIN Function
int main ()
{
SysClockConfig ();
GPIOConfig ();
TIM6Config ();
SPIConfig ();
SPI_Enable ();
adxl_init ();
while (1)
{
adxl_read (0x32);
x = ((RxData[1]<<8)|RxData[0]);
y = ((RxData[3]<<8)|RxData[2]);
z = ((RxData[5]<<8)|RxData[4]);
xg = x*.0078;
yg = y*.0078;
zg = z*.0078;
Delay_ms (500);
}
}
- Here we will configure the system, SPI, GPIO etc.
- Then Initialize the ADXL
- Read 6 Bytes of data from the ADXL
- And later convert this data into the acceleration values
RESULT
You can check the video to see the complete working.