Data abstraction instance based on hardware SPI

Keywords: IoT stm32 SPI

1. Write in front

spi (Serial Peripheral Interface) is the Serial Peripheral Interface. Like i2c, spi also uses the bus for peripheral device communication, which is essential for embedded development.

According to my previous experience, I summarize spi (mainly in the field of MCU, and Linux has mature driver equipment). The main purposes and realization are as follows:

  1. spi bus is separated from spi equipment;

  2. Quickly use new hardware spi or analog spi;

  3. It is convenient to transplant spi bus devices and spi peripheral programs to different mcu platforms.

2.spi bus abstraction

The source code of this part is spi_core.c spi_core.h

2.1 spi bus model external interface (API)

/*extern function*/
extern int spi_send_then_recv(struct spi_dev_device *spi_dev,const void *send_buff,
         					  unsigned short send_size,void *recv_buff,unsigned short recv_size);
extern int spi_send_then_send(struct spi_dev_device *spi_dev,const void *send_buff1,
         					  unsigned short send_size1,const void *send_buff2,unsigned short send_size2);
extern int spi_send_recv(struct spi_dev_device *spi_dev,const void *send_buff,void *recv_buff,unsigned short 					data_size);
extern int spi_send(struct spi_dev_device *spi_dev,const void *send_buff,unsigned short send_size);
  • 1)spi_send_then_recv, standard SPI, routine operation, receiving after sending a frame, such as reading the value of a chip register;
  • 2)spi_send_then_send, standard SPI, routine operation, sending after sending a frame, such as writing data to a chip register (address);
  • 3)spi_send_recv, non-standard SPI, see the chip timing diagram for details, generate clock signal, and receive when sending is completed; In the second case, only receive and send are used to generate clock signals, such as some AD chips;
  • 4)spi_send is used for both standard and non-standard SPI. It only sends no return value or ignores the return value, such as spi LCD screen.

2.2 spi Bus Abstract API implementation

Take the "spi_send_then_recv" function as an example:

  • 1)spi_ Dev: SPI device pointer, type "struct spi_dev_device". When driving an SPI peripheral, you need to initialize this pointer first;
  • 2)send_buff: data to be sent (CACHE);
  • 3)send_size: the amount of data sent (in bytes);
  • 4): recv_buff: store the return value data cache (address);
  • 5): recv_size: returns the data size.

For the other three functions, the first parameter is the spi device pointer, and the other parameters are the send / receive buffer, the amount of data sent and received, etc., which can be seen from the variable name.

2.3 struct spi_de_device

This structure is the key. When calling API to drive a peripheral, you need to initialize it first (similar to the registered device driver of Linux). A complete spi peripheral includes chip selection and bus quantity. A bus can be composed of multiple chip selections to drive multiple peripherals. Therefore, struct spi_ dev_ The design prototype of the device is:

struct spi_dev_device
    void (*spi_cs)(unsigned char state); 
    struct spi_bus_device *spi_bus; 
  • 1) The first parameter is the function pointer. The main function is to realize the selection (pull down / pull up) function of spi peripheral chip selection;

  • 2) The second parameter is the structure pointer related to the spi bus, which is mainly the function of receiving and sending related receipts at the bottom. Continue to change the structure.

2.4 struct spi_bus_device *spi_bus

The structure is implemented by the underlying hardware related SPI bus, which is specifically implemented by the actual requirements, such as hardware SPI or analog SPI. struct spi_ bus_ device*spi_ The bus prototype is:

struct spi_bus_device
    int (*spi_bus_xfer)(struct spi_dev_device *spi_bus,struct spi_dev_message *msg);
    void *spi_phy;
    unsigned char data_width;
  • 1) The first parameter is the function pointer, which is the spi bus transceiver function. This part is written when we write bare metal code, but it is put in a structure and implemented in the form of function pointer; The advantage of this is that the upper interface remains unchanged, which is better. When other MCU or using analog spi, only the function entity of this part needs to be modified, and the upper code does not need to be changed.

  • 2) The second parameter, a pointer, represents a specific physical spi, such as SPI1 and SPI2 of stm32, or an analog spi;

  • 3) The third parameter, data width, is generally 8bit or 16bit.

In fact, other parameters, such as data rate and spi mode, can also be placed here. I just feel that such parameters do not change frequently. In order to save memory, they are not added to this structure configuration. The following interrupt analysis function pointer

int (*spi_bus_xfer) (struct spi_dev_device *spi_bus,struct spi_dev_message *msg)

2.5 spi_bus_xfer

The function pointer entry parameters are the SPI device pointer (struct spi_dev_device) and the SPI device information frame pointer (struct spi_dev_message). struct spi_ dev_ Device is the same type of parameter as mentioned above, struct SPI_ dev_ Message is a data message frame, and its prototype is as follows:

struct spi_dev_message
    const void  *send_buf;
    void        *recv_buf;
    int  length;
    unsigned char cs_take    : 1;
    unsigned char cs_release : 1;
  • 1)send_buf: data to be sent (CACHE);

  • 2)recv_buf: cache (address) for storing returned value data;

  • 3) Length: sending / receiving data length;

  • 4)cs_take: enable film selection;

  • 5)cs_release: release the film selection.

3. Abstract implementation of SPI bus

The source code of this part is spi_hw.c spi_hw.h

3.1 spi Bus Abstract API implementation

  • Step 1: "spi_send_then_recv", the implementation code is as follows:
int spi_send_then_recv(struct spi_dev_device *spi_dev,const void *send_buff,unsigned short send_size,void *recv_buff,unsigned short recv_size)
    struct spi_dev_message message;
    message.length     = send_size;
    message.send_buf   = send_buff;
    message.recv_buf   = 0;
    message.cs_take    = 1;
    message.cs_release = 0;
    message.length     = recv_size;
    message.send_buf   = 0;
    message.recv_buf   = recv_buff;
    message.cs_take    = 0;
    message.cs_release = 1;
    return 0;
The function of spi is to receive a frame of data after sending a frame.

1)spi_dev is the incoming device pointer;

2) The sending / receiving parameters are mainly passed to "spi_dev_message";

3) For the first frame "spi_dev_message", the return value is not received, so recv_buf setting is null (0); At this time, the chip selection is pulled down (cs_take=1), and the chip selection cannot be pulled up after sending (cs_release=0). After the later received frame is received, the chip selection can be pulled up (cs_release=1), which can also be seen from the peripheral timing diagram;

4) For the second frame, the transmission data is empty at this time, so send_ When buf is set to 0, the transmission action at this time is not really transmission, but is only used to generate the clock signal of received data.

  • Step 2: spi_send_then_send, and spi_send_then_recv is similar, but the following "receive" action changes to "send" action, so there is no repeated analysis. See the attachment for the source code.

  • Step 3: spi_send_recv, the implementation code is as follows:

int spi_send_recv(struct spi_dev_device *spi_dev,const void *send_buff,void *recv_buff,unsigned short data_size)
    message.length   = data_size;
    message.send_buf = send_buff;
    message.recv_buf = recv_buff;
    message.cs_take  = 1;
    message.cs_release = 1;
    return 0;
The implementation function is to send and receive at the same time, or only receive. 1)spi_dev is the incoming device pointer; 2) The sending and receiving data and length are passed in by the user through formal parameters. When only receiving, the sending data cache can be set to empty (0); 3) Pull down the film selection before operation (cs_take=1), and pull up the film selection after operation (cs_release=1);
  • Step 4: spi_send, the implementation code is as follows:
int spi_send(struct spi_dev_device *spi_dev,const void *send_buff,unsigned short send_size)
    struct spi_dev_message message;
    message.length    = send_size;
    message.send_buf  = send_buff;
    message.recv_buf  = 0;
    message.cs_take   = 1;
    message.cs_release = 1;
    return 0;

This function is similar to spi_send_recv is very similar, but there is only "send" action and no "receive" action, so recv_buf is set to null (0).

3.2 bottom layer implementation of SPI bus abstraction (taking stm32 as an example)

It mainly implements the "spi_bus_xfer" function in "struct spi_bus_device", which is equivalent to ordinary bare metal code. Taking 8bit mode as an example, the code is as follows. See the attachment "spi_hw.c" for details.

static int stm32_spi_bus_xfer(struct spi_dev_device *spi_dev,struct spi_dev_message *msg)
   int size;
   SPI_TypeDef *SPI_NO;
   SPI_NO = (SPI_TypeDef *)spi_dev->spi_bus->spi_phy;
   size = msg->length;
   {/* take CS */
   if(spi_dev->spi_bus->data_width <=8)
       const unsigned short * send_ptr = msg->send_buf;
       unsigned short *recv_ptr = msg->recv_buf;
       	   unsigned short data = 0xFF;
	       if(send_ptr != 0)
	           data = *send_ptr++;
	       while (SPI_I2S_GetFlagStatus(SPI_NO, SPI_I2S_FLAG_TXE) == RESET); 
	       SPI_I2S_SendData(SPI_NO, data);
	       while (SPI_I2S_GetFlagStatus(SPI_NO, SPI_I2S_FLAG_RXNE) == RESET); 
	       data = SPI_I2S_ReceiveData(SPI_NO); 
	       if(recv_ptr != 0)
	           *recv_ptr++ = data;
   {/* release CS */ 
   return msg->length;

Main functions:

  • 1) Data sending and receiving, chip selection / release;
  • 2) It is divided into 8bit analog and 16bit receive / transmit modes.

3.3 finally, perform relevant initialization, such as IO port, clock and spi configuration.

void stm32f1xx_spi_init(struct spi_bus_device *spi0,unsigned char byte_size0,struct spi_bus_device *spi1,unsigned char byte_size1)
    SPI_InitTypeDef  SPI_InitStructure;
    GPIO_InitTypeDef GPIO_InitStructure; 
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
    if(byte_size0 <= 8)
        SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;  
        SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b; 
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;          
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64;
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;     
    SPI_InitStructure.SPI_CRCPolynomial = 7;
    SPI_Init(SPI1, &SPI_InitStructure);
    SPI_Cmd(SPI1, ENABLE); 
    //spi io
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC | RCC_APB2Periph_AFIO,ENABLE);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    spi0->data_width = byte_size0;
    spi0->spi_bus_xfer = stm32_spi_bus_xfer;
    spi0->spi_phy = SPI1;

Points for attention:

  • 1) The four SPI modes are selected according to the device timing diagram, generally the first (L, 1) or the third (H, 2);

  • 2) Software control mode must be selected for chip selection control mode (SPI_NSS);

  • 3) Set to "full duplex mode", which can adapt to various standard and non-standard spi;

  • 4) spi0 pointer initialization is just the function entity and related spi parameters we implemented in the previous step.

3.4 summary

So far, an stm32 hardware spi bus has been implemented, and the rest is to use this bus to drive an spi peripheral. You can also simulate the spi through the io port, and then write an article on using the simulated spi. The main changes are also here. The bus program or the following peripheral programs do not need to be modified.

4. Use spi abstraction (take 25aa256 EEPROM as an example)

The source code of this part is 25xx.c 25xx.h

4.1 initialization (registered device)

stm32 SPI2 is used to drive 25aa256, and the steps are as follows:

  • 1) Define spi bus device and EEPROM device
struct  spi_dev_device ee_25xx_spi_dev;
struct  spi_bus_device  spi_bus1;
  • 2) Initialize IO and 2 device pointers
    First, the chip selection function entity is realized, and the chip selection (CS) IO is PB12.
static void spi1_cs(unsigned char state)
    if (state)
 		GPIO_SetBits(GPIOB, GPIO_Pin_12);
 		GPIO_ResetBits(GPIOB, GPIO_Pin_12);

Initialize 25aa256

void ee_25xx_init(void)
    GPIO_InitTypeDef GPIO_InitStructure;
    /* SPI2 cs */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;          
    GPIO_Init(GPIOB, &GPIO_InitStructure); 
    GPIO_SetBits(GPIOB, GPIO_Pin_12);
    /* device init */
    ee_25xx_spi_dev.spi_cs  = spi1_cs;
    ee_25xx_spi_dev.spi_bus = &spi_bus1;   

"Ee_25xx_spi_dev" is the "registered" device. The following API operation 25aa256 can be described above, and the incoming parameter is the "ee_25xx_spi_dev" address (pointer).

4.2 operation (read / write) 25aa256

  • 1) 25aa256 enable register operation
void ee_25xx_write_enable(uint8_t select)

This operation works on the "spi_send" interface and has no return value. It is simple and clear!

  • 2) Write 1 byte data to 25aa256
void ee_25xx_write_byte(uint16_t write_addr,uint8_t write_data)
    uint8_t send_buff[3];
    send_buff[0] = REG_WRITE_COMMAND;
    send_buff[1] = (write_addr>>8)&0xff;
    send_buff[2] = write_addr&0xff;
This operation works on the "spi_send_then_send" interface, which can also be well understood from the function name. The basic steps are:
  • Enable 25aa256;
  • Send cache filling, which includes write command and write address;
  • Write data filling, single byte directly calls formal parameters, and no additional memory is applied;
  • Call "spi_send_then_send" to complete the write operation.
  • 3) Read data from 25aa256
void ee_25xx_read_bytes(uint16_t read_addr,uint8_t *read_buff,uint16_t read_bytes)
    uint8_t send_buff[3];
    send_buff[0] = REG_READ_COMMAND;
    send_buff[1] = (read_addr>>8)&0xff;
    send_buff[2] = read_addr&0xff;
This operation works on the "spi_send_then_recv" interface. The basic steps are:
  • Send cache filling, which includes write command and write address;
  • The formal parameter address is passed as the receiving address;
  • Call "spi_send_then_send" to complete the read operation.
  • 4) 25aa256 write status register and read status register. Similarly, see the source code for details.

  • 5) 25aa256 page writing algorithm is the same as the EEPROM (AT24c16) principle of i2c interface. You can see another article: EEPROM page writing algorithm

4.3 25aa256 drive summary

So far, the 25aa256 driver has been completed. When all operations are transplanted to the new mcu platform through the above four API interfaces, the device driver hardly needs to be modified, only the underlying functions of spi need to be modified. Drive other spi peripherals, which is consistent with the process steps of 25aa256.

In fact, through this problem, it can also be found that driving a device is relatively simple, and more difficulties are in applications, such as 25aa256 page writing algorithm. Therefore, after the bottom "wheel" is built, there is no need to build the wheel again and spend more time on research and application.

5. Summary

This paper mainly describes the abstract layering of spi bus under mcu. The main implementation means is to make full use of structure and function pointer.

  • 1) To use SPI bus, add a new bus, transplant to a new platform, just instantiate the function pointer in "struct spi_bus_device" and initialize the SPI related parameters;

  • 2) when driving a peripheral, first select the instantiation function, initialize "struct spi_dev_device", and then call the 4 API functions to operate peripherals.

  • 3) Multiple peripherals can be attached to the same bus, that is, multiple "struct spi_dev_device" pointers are defined. At this time, the chip selection function needs to be additionally defined, and the bus pointer is initialized to the initial bus instance; Suppose another adc peripheral is added:

struct  spi_dev_device adc_spi_dev;
static void spi1_cs1(unsigned char state)
    if (state)
        GPIO_SetBits(GPIOB, GPIO_Pin_11);
 		GPIO_ResetBits(GPIOB, GPIO_Pin_11);
void adc_init(void)
    GPIO_InitTypeDef GPIO_InitStructure;
    /* SPI cs */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;          
    GPIO_Init(GPIOB, &GPIO_InitStructure); 
    GPIO_SetBits(GPIOB, GPIO_Pin_11);
    /* device init */
    //st32f1xx_ spi_ init(0,&spi_bus1); /*  Shared spi1, which has been initialized. There is no need to initialize again*/
    adc_spi_dev.spi_cs = spi1_cs1;    /* Slice selection function must be independent */
    adc_spi_dev.spi_bus = &spi_bus1;  /* Point to spi1 */  
  • 4) As for non-standard spi, these four API s can be used in most cases. So far, all the spi devices I have used can be implemented.

6. Source code


7. Reference


Posted by Shiki on Tue, 12 Oct 2021 13:01:04 -0700