SPI is the most popular serial synchronous data transfer interface between the microcontroller and peripherals. There are two SPI modules in the STM32F103C8. This interface can operate in Master (bus master) or Slave (bus slave) mode. Generally speaking, the SPI interface is quite a tricky thing. Specifically in STM32 SPI can calculate the checksum of received and transmitted frames by a given polynomial, work in Multimaster mode, hardware to work with the output NSS, as well as communicate in half-duplex mode (MOSI and MISO go on the same wire). Therefore, to properly configure SPI you need to carefully study all the registers of this module. Well and one more thing: SPI in STM32 can work in I2S mode (not to be confused with I2C!!!). I2S is an SPI-like interface for data transfer between digital audio devices. Don't let this confuse you, by default this module works in SPI mode and we will not consider registers that are needed only for I2S mode.
To connect two or more devices, you need 4 wires (+ground, where without it):
- MOSI (Master Out / Slave In) - this wire carries data from master to slave device
- MISO (Master In / Slave Out) - and here it is the other way around: data goes from the slave to the master.
- SCK (Serial Clock) - a clock signal that goes from the master to the slave. At each new period of the clock signal, the bus Master sends a new data bit to the Slave, and the Slave in turn sends a data bit to the Master.
- NSS (Slave select) is an optional wire that is needed if we have several slaves on the SPI bus. Thus, with the help of NSS we can select with which Slave we want to exchange data.
SPI registers
Here I will describe the registers that apply only to SPI. Everything that relates to I2S has been thrown out.
SPI control register 1 (SPI_CR1)
BIDIMODE: Enable bidirectional data output operation mode.
- 0: 2-wire operation mode with unidirectional data line transmission
- 1: 1-wire operation mode with bidirectional data line transmission
This bit, in conjunction with the BIDIMODE bit, selects the direction of transmission in bidirectional mode. In master mode, the MOSI pin is used for data transmission, while in slave mode, the MISO pin is used.
- 0: Output disabled (receive only)
- 1: Output enabled (transmit only)
- 0: CRC calculation disabled
- 1: CRC calculation enabled
- 0: Data transfer stage
- 1: The next transmission will be completed by RCR transmission.
- 0: Transmission frame size 8 bits
- 1: Transmission frame size 16 bits
- 0: Full duplex - transmit and receive
- 1: Output disabled - receive only
- 0: Software Slave Management disabled
- 1: Software Slave Management enabled
LSBFIRST: Frame Format
- 0: MSB transmitted first
- 1: LSB is transmitted first
- 0: SPI disabled
- 1: SPI enabled
- 000: fPCLK/2
- 001: fPCLK/4
- 010: fPCLK/8
- 011: fPCLK/16
- 100: fPCLK/32
- 101: fPCLK/64
- 110: fPCLK/128
- 111: fPCLK/256
- 0: Slave mode
- 1: Master mode
- 0: CK to 0 at idle
- 1: CK to 1 when idle
- 0: The first clock transition is a data capture edge
- 1: The second clock transition is the edge of data capture
TXEIE: Interrupt emptying of the Tx data transfer buffer
- 0: TXE interrupt prohibited
- 1: TXE interrupt enabled. Used to generate an interrupt when the TXE flag is set.
- 0: RXNE interrupt disabled
- 1: RXNE interrupt is enabled. Used to generate an interrupt when the RXNE flag is set.
- 0: Interrupt on error occurrence prohibited
- 1: Error interrupts are enabled.
- 0: SS output is disabled in master mode and it is possible to work in multimaster mode.
- 1: SS output is enabled in master mode and no multimaster operation is possible.
RXDMAEN: When this bit is set, a DMA request occurs when the RXNE flag is set.
SPI status register (SPI_SR)
BSY: Busy Flag. This flag is set and reset by hardware
- 0: SPI is not busy
- 1: SPI is busy with communication or the Tx transmit buffer is not empty.
- 0: No overflow occurred
- 1: Overflow occurred
- 0: The received CRC value matched the value of the SPI_RXCRCR register.
- 1: The received CRC value did not match the value of the register SPI_RXCRCR
- 0: Tx buffer is not empty
- 1: Tx buffer empty
- 0: Rx buffer empty
- 1: Rx buffer not empty
DR[15:0]: Data register. This register is divided into two buffers, one for writing (transmitter buffer) and one for reading (receiver buffer). A write operation to the SPI_DR register writes data to the transmitter buffer, and a read operation from SPI_DR returns a value from the receiver buffer.
SPI CRC polynomial register (SPI_CRCPR)
CRCPOLY[15:0]: CRC polynomial register, default is 0007h
SPI RX CRC register (SPI_RXCRCR)
RXCRC[15:0]: The CRC value of the received data. When CRC calculation is enabled, RXCRC contains the calculated CRC value of the received data. This register is reset to zero when the CRCEN bit in the SPI_CR1 register is set to one.
SPI TX CRC register (SPI_TXCRCR)
TXCRC[15:0]: The CRC value of the transmitted data. When CRC calculation is enabled, TXCRC contains the calculated CRC value of the transmitted data. This register is reset to zero when the CRCEN bit in the SPI_CR1 register is set to one.
Configuring SPI in Master mode without interrupts
After we have studied the SPI registers, let's start practicing. The task is to configure SPI1 in Master mode and start continuous sending of one byte without using interrupts. Well, let's go!
Let's call the initialization function SPI1_Init():
Code: Select all
void SPI1_Init(void)
{
}
Let's not bother with Remap for now. From Fig. 7 shows that SPI1 is connected to the GPIOA port to the following pins:
- NSS - PA4
- SCK - PA5
- MISO - PA6
- MOSI - PA7
The red rectangles highlight the settings for our case. So, we have the necessary information, now we can customize. As we already know, before we start working with any peripheral, it is necessary to turn on the clock signal:
Code: Select all
//Enable SPI1 and GPIOA clocking
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN | RCC_APB2ENR_IOPAEN;
Code: Select all
//First reset all configuration bits to zeros
GPIOA->CRL &= ~(GPIO_CRL_CNF5_Msk | GPIO_CRL_MODE5_Msk
| GPIO_CRL_CNF6_Msk | GPIO_CRL_MODE6_Msk
| GPIO_CRL_CNF7_Msk | GPIO_CRL_MODE7_Msk);
//Customize
//SCK: MODE5 = 0x03 (11b); CNF5 = 0x02 (10b)
GPIOA->CRL |= (0x02<<GPIO_CRL_CNF5_Pos) | (0x03<<GPIO_CRL_MODE5_Pos);
//MISO: MODE6 = 0x00 (00b); CNF6 = 0x01 (01b)
GPIOA->CRL |= (0x01<<GPIO_CRL_CNF6_Pos) | (0x00<<GPIO_CRL_MODE6_Pos);
//MOSI: MODE7 = 0x03 (11b); CNF7 = 0x02 (10b)
GPIOA->CRL |= (0x02<<GPIO_CRL_CNF7_Pos) | (0x03<<GPIO_CRL_MODE7_Pos);
Code: Select all
SPI1->CR1 = 0<<SPI_CR1_DFF_Pos //Frame size 8 bits
| 0<<SPI_CR1_LSBFIRST_Pos //MSB first
| 1<<SPI_CR1_SSM_Pos //SSS program control
| 1<<SPI_CR1_SSI_Pos //SS in high state
| 0x04<<SPI_CR1_BR_Pos //Baud rate: F_PCLK/32
| 1<<SPI_CR1_MSTR_Pos //Master mode (master)
| 0<<SPI_CR1_CPOL_Pos | 0<<SPI_CR1_CPHA_Pos; // SPI operating mode: 0
All that remains now is to enable SPI1:
Code: Select all
SPI1->CR1 |= 1<<SPI_CR1_SPE_Pos; //Enable SPI
Code: Select all
void SPI1_Init(void)
{
//Turn on SPI1 and GPIOA clocking
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN | RCC_APB2ENR_IOPAEN;
/**********************************************************/
/*** Configuring GPIOA pins to work together with SPI1 ***/
/**********************************************************/
//PA7 - MOSI
//PA6 - MISO
//PA5 - SCK
//First reset all configuration bits to zeros
GPIOA->CRL &= ~(GPIO_CRL_CNF5_Msk | GPIO_CRL_MODE5_Msk
| GPIO_CRL_CNF6_Msk | GPIO_CRL_MODE6_Msk
| GPIO_CRL_CNF7_Msk | GPIO_CRL_MODE7_Msk);
//Customize
//SCK: MODE5 = 0x03 (11b); CNF5 = 0x02 (10b)
GPIOA->CRL |= (0x02<<GPIO_CRL_CNF5_Pos) | (0x03<<GPIO_CRL_MODE5_Pos);
//MISO: MODE6 = 0x00 (00b); CNF6 = 0x01 (01b)
GPIOA->CRL |= (0x01<<GPIO_CRL_CNF6_Pos) | (0x00<<GPIO_CRL_MODE6_Pos);
//MOSI: MODE7 = 0x03 (11b); CNF7 = 0x02 (10b)
GPIOA->CRL |= (0x02<<GPIO_CRL_CNF7_Pos) | (0x03<<GPIO_CRL_MODE7_Pos);
/**********************/
/*** Setting SPI1 ***/
/**********************/
SPI1->CR1 = 0<<SPI_CR1_DFF_Pos //Frame size 8 bits
| 0<<SPI_CR1_LSBFIRST_Pos //MSB first
| 1<<SPI_CR1_SSM_Pos //SSS program control
| 1<<SPI_CR1_SSI_Pos //SS in high state
| 0x04<<SPI_CR1_BR_Pos //Baud rate: F_PCLK/32
| 1<<SPI_CR1_MSTR_Pos //Master mode (master)
| 0<<SPI_CR1_CPOL_Pos | 0<<SPI_CR1_CPHA_Pos; // SPI operating mode: 0
SPI1->CR1 |= 1<<SPI_CR1_SPE_Pos; //Enable SPI
}
SPI has a Shift register, a transmit buffer (Tx buffer) and a receiver buffer (Rx buffer). There are three very interesting flags in the SR register: BSY, TXE and RXNE. The TXE flag is set if the transmitter buffer (Tx buffer) is empty and the next value can be loaded into it, RXNE is set to one if a new value has arrived in the receiver buffer (Rx buffer) and can be read. BSY is set if the SPI module is busy with a communication operation or if the transmitter buffer is not empty.
The logic of operation is as follows: the operation of writing to the DR register fills the transmitter buffer with a data frame (8 or 16 bits, depending on the setting), and the BSY flag is set, and TXE is reset. The value from the transmitter buffer is then loaded into the shift register and the SPI data transfer process is started, and the TXE flag is set to one, indicating that a new value can be loaded into the Tx buffer. If another value is loaded into the Tx buffer, TXE is reset to zero until the current data frame transfer is completed and the next Tx buffer value is loaded into the shift register.
With each new period of the SCK synchronization signal, the shift register spits out another bit into MOSI and pops a new data bit from MISO into its tail (this is true for Master mode, vice versa for Slave). After the last bit has been received, the shift register value is loaded from the receiver buffer (Rx buffer) and the RXNE flag is set. If no new value has been loaded into the Tx buffer, the data transfer is terminated and the BSY flag is reset to zero.
Code: Select all
Sending data to [i]SPI [/i]will look like this:
void SPI1_Write(uint16_t data)
{
//Wait until the transmitter buffer is empty
while(!(SPI1->SR & SPI_SR_TXE))
;
//fill the transmitter buffer
SPI1->DR = data;
}
Here is the data reception:
Code: Select all
uint16_t SPI1_Read(void)
{
SPI1->DR = 0; //start exchange
//Wait until a new value appears
//in the receiver buffer
while(!(SPI1->SR & SPI_SR_RXNE))
;
//return the value of the receiver buffer
return SPI1->DR;
}
Code: Select all
void main()
{
ClockInit();
SPI1_Init();
for(;;)
{
SPI1_Write(0x34);
}
}
Figure 11 shows that the data is streaming continuously without delay. That's great!
Configuring SPI in Master mode with interrupts
Now let's do the same thing, but only on interrupts. The task is as follows: we have a certain array of bytes, which must be spit out to SPI using interrupts. I will not go into the details of interrupts in STM32, for this will be a separate article, I will limit myself to the necessary minimum.
The peripheral module can have several events that can cause an interrupt, for SPI it is TXEIE, RXNEIE and ERRIE (see Fig. 2). However, the interrupt handler in most cases is only one: SPI1_IRQHandler(). Thus, if we have multiple event interrupts enabled, we need to look at the SR status register in SPI1_IRQHandler() to understand what happened.
In order for the interrupt to be triggered, we need to perform 3 actions:
- Enable the interrupt in the SPI module.
- Enable the interrupt from the SPI in the NVIC. When any SPI enabled interrupt occurs, the SPI1_IRQHandler() handler will be called.
- Allow interrupts globally (by default, after resetting the microcontroller, they are allowed).
Code: Select all
void SPI1_Init(void)
{
//Turn on SPI1 and GPIOA clocking
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN | RCC_APB2ENR_IOPAEN;
/**********************************************************/
/*** Configuring GPIOA pins to work together with SPI1 ***/
/**********************************************************/
//PA7 - MOSI
//PA6 - MISO
//PA5 - SCK
//First reset all configuration bits to zeros
GPIOA->CRL &= ~(GPIO_CRL_CNF5_Msk | GPIO_CRL_MODE5_Msk
| GPIO_CRL_CNF6_Msk | GPIO_CRL_MODE6_Msk
| GPIO_CRL_CNF7_Msk | GPIO_CRL_MODE7_Msk);
//Customize
//SCK: MODE5 = 0x03 (11b); CNF5 = 0x02 (10b)
GPIOA->CRL |= (0x02<<GPIO_CRL_CNF5_Pos) | (0x03<<GPIO_CRL_MODE5_Pos);
//MISO: MODE6 = 0x00 (00b); CNF6 = 0x01 (01b)
GPIOA->CRL |= (0x01<<GPIO_CRL_CNF6_Pos) | (0x00<<GPIO_CRL_MODE6_Pos);
//MOSI: MODE7 = 0x03 (11b); CNF7 = 0x02 (10b)
GPIOA->CRL |= (0x02<<GPIO_CRL_CNF7_Pos) | (0x03<<GPIO_CRL_MODE7_Pos);
/*
//SS MODE4 = 0x03 (11b); CNF4 = 0x02 (10b)
GPIOA->CRL |= (0x02<<GPIO_CRL_CNF4_Pos) | (0x03<<GPIO_CRL_MODE4_Pos);
*/
/**********************/
/*** Setting SPI1 ***/
/**********************/
SPI1->CR1 = 0<<SPI_CR1_DFF_Pos //Frame size 8 bits
| 0<<SPI_CR1_LSBFIRST_Pos //MSB first
| 1<<SPI_CR1_SSM_Pos //SSS program control
| 1<<SPI_CR1_SSI_Pos //SS in high state
| 0x04<<SPI_CR1_BR_Pos //Baud rate: F_PCLK/32
| 1<<SPI_CR1_MSTR_Pos //Master mode (master)
| 0<<SPI_CR1_CPOL_Pos | 0<<SPI_CR1_CPHA_Pos; // SPI operating mode: 0
NVIC_EnableIRQ(SPI1_IRQn); //Allow interrupts from SPI1
SPI1->CR1 |= 1<<SPI_CR1_SPE_Pos; //Enable SPI
}
Code: Select all
int32_t tx_index = 0; //this stores the number of bytes transferred
int32_t tx_len = 0; //how many bytes to transfer
uint8_t *tx_data; //pointer to the array with transferred data
void SPI1_Tx(uint8_t *data, int32_t len)
{
if(len<=0)
return;
//Wait until SPI is free from the previous transfer
while(SPI1->SR & SPI_SR_BSY)
;
//Set the variables that will be
//used in the SPI interrupt handler
tx_index = 0;
tx_len = len;
tx_data = data;
//Resolve TXEIE interrupt and start the exchange
SPI1->CR2 |= (1<<SPI_CR2_TXEIE_Pos);
}
It works like this. In the initial state, the SPI is not transmitting any data and the TXE flag in the SR register is set to one. This means that if the TXEIE interrupt is enabled, it will be triggered immediately. After all the preliminary settings we enable the TXEIE interrupt, thus starting the process of sending data over SPI. The interrupt handler, where all the main work takes place, looks like this:
Code: Select all
void SPI1_IRQHandler(void)
{
SPI1->DR = tx_data[tx_index]; //Write the new value to DR
tx_index++; //increase the counter of transferred bytes by one
//if all the bytes have been transferred, then disable the interrupt,
// thus ending the data transfer
if(tx_index >= tx_len)
SPI1->CR2 &= ~(1<<SPI_CR2_TXEIE_Pos);
}
Code: Select all
uint8_t data[10];
void main()
{
ClockInit(); //initialization of the clocking system
SPI1_Init(); //initialization of SPI1
//fill the data[] array with data
for(int i=0; i<sizeof(data); i++)
{
data[i] = i+1;
}
//start data transfer
SPI1_Tx(data, sizeof(data));
//infinite loop
//you can do something useful here
for(;;)
{
}
}
Everything works correctly, as much as said - so much and sent Bytes go one after another without delay. That's great.
And this is how sending 10 bytes looks like:
That's all for now, the article has already turned out to be a big one. I haven't decided yet what will be in the next part, but we should make articles about NVIC interrupt controller and DMA direct memory access controller. And SPI in Slave mode should be considered.