【SPI】SPI总线协议及驱动框架

发布时间 2023-08-29 15:11:50作者: -zx-

SPI通讯协议

SPI控制方式

SPI采用主-从(master-slave))模式的控制的方式。一个Master设备可以通过提供Clock以及对slave设备进行片选来控制多个Slave设备,SPI协议还规定Slave设备的Clock由Master设备通过SCK管脚提供给Slave设备,Slave设备本身不能产生和控制Clock,没有Clock则Slave设备不能工作。

SPI传输方式

SPI采用同步的方式传输数据,Master 设备会根据将要交换的数据来产生相应的时钟脉冲(Clock Pulse), 时钟脉冲组成了时钟信号(Clock Signal) , 时钟信号通过时钟极性 (CPOL) 和 时钟相位 (CPHA) 控制着两个 SPI 设备间何时数据交换以及何时对接收到的数据进行采样, 来保证数据在两个设备之间是同步传输的。

image

SPI数据交换

SPI数据交换框图:

image

SSPBUF(Synchronous Serial Port Buffer):泛指 SPI 设备里面的内部缓冲区, 一般在物理上是以 FIFO 的形式, 保存传输过程中的临时数据;

SSPSR(Synchronous Serial Port Register):泛指 SPI 设备里面的移位寄存器(Shift Regitser), 它的作用是根据设置好的数据位宽(bit-width) 把数据移入或者移出 SSPBUF;

Controller:泛指 SPI 设备里面的控制寄存器, 可以通过配置它们来设置 SPI 总线的传输模式。

SPI工作机制

SPI的极性Polarity和相位Phase,最常见的写法是CPOL和CPHA,其他的一些写法简单总结如下:

(1) CKPOL (Clock Polarity) = CPOL = POL = Polarity = (时钟)极性

(2) CKPHA (Clock Phase) = CPHA = PHA = Phase = (时钟)相位

(3) SCK=SCLK=SPI的时钟

(4) Edge=边沿,即时钟电平变化的时刻,即上升沿(rising edge)或者下降沿(falling edge)

对于一个时钟周期内,有两个edge,分别称为:

Leading edge=前一个边沿=第一个边沿,对于开始电压是1,那么就是1变成0的时候,对于开始电压是0,那么就是0变成1的时候;

Trailing edge=后一个边沿=第二个边沿,对于开始电压是1,那么就是0变成1的时候(即在第一次1变成0之后,才可能有后面的0变成1),对于开始电压是0,那么就是1变成0的时候;

CPOL极性

什么是SCLK时钟的空闲时刻,其就是当SCLK在数发送8个bit比特数据之前和之后的状态,于此对应的,SCLK在发送数据的时候,就是正常的工作的时候,有效active的时刻了。

先说英文,其精简解释为:Clock Polarity = IDLE state of SCK。

再用中文详解:

SPI的CPOL,表示当SCLK空闲idle的时候,其电平的值是低电平0还是高电平1:

CPOL=0,时钟空闲idle时候的电平是低电平,所以当SCLK有效的时候,就是高电平,就是所谓的active-high;

CPOL=1,时钟空闲idle时候的电平是高电平,所以当SCLK有效的时候,就是低电平,就是所谓的active-low;

CPHA相位

capture strobe = latch = read = sample,都是表示数据采样,数据有效的时刻。相位,对应着数据采样是在第几个边沿(edge),是第一个边沿还是第二个边沿,0对应着第一个边沿,1对应着第二个边沿。

对于:

CPHA=0,表示第一个边沿:

对于CPOL=0,idle时候的是低电平,第一个边沿就是从低变到高,所以是上升沿;

对于CPOL=1,idle时候的是高电平,第一个边沿就是从高变到低,所以是下降沿;

CPHA=1,表示第二个边沿:

对于CPOL=0,idle时候的是低电平,第二个边沿就是从高变到低,所以是下降沿;

极性和相位图:

image

image

软件设置极性和相位

SPI分主设备和从设备,两者通过SPI协议通讯。

而设置SPI的模式,是从设备的模式,决定主设备的模式。所以要先搞懂从设备的SPI模式,然后再将主设备的SPI的模式,设置和从设备相同的模式,即可正常的通讯。

对于从设备的SPI是什么模式,有两种:

1.固定的,有SPI从设备硬件决定的

SPI从设备,具体是什么模式,相关的datasheet中会有描述,需要自己去datasheet中找到相关的描述,即:

关于SPI从设备,在空闲的时候,是高电平还是低电平,即决定了CPOL是0还是1;

然后再找到关于设备是在上升沿还是下降沿去采样数据,这样就是,在定了CPOL的值的前提下,对应着可以推算出CPHA是0还是1了。

2.可配置的,由软件自己设定

从设备也是一个SPI控制器,4种模式都支持,此时只要自己设置为某种模式即可。

然后知道了从设备的模式后,再去将SPI主设备的模式,设置为和从设备模式一样,即可。

对于如何配置SPI的CPOL和CPHA的话,不多细说,多数都是直接去写对应的SPI控制器中对应寄存器中的CPOL和CPHA那两位,写0或写1即可。

Linux下的SPI驱动框架

SPI驱动架构

Linux系统对spi设备具有很好的支持,linux系统下的spi驱动程序从逻辑上可以分为3个部分:

1.spi核心(SPI Core):SPI Core是Linux内核用来维护和管理spi的核心部分,SPI Core提供操作接口函数,允许一个spi master,spi driver和spi device初始化时在SPI Core中进行注册,以及推出时进行注销。

2.spi控制器驱动(SPI Master Driver):SPI Master针对不同类型的spi控制器硬件,实现spi总线的硬件访问操作。SPI Master通过接口函数向SPI Core注册一个控制器。

3.spi设备驱动(SPI Device Driver):SPI Driver是对应于spi设备端的驱动程序,通过接口函数向SPI Core进行注册,SPI Driver的作用是将spi设备挂接到spi总线上;

image

重要结构体

  • spi_master
//SPI控制器
struct spi_master {
  struct device  dev;

  struct list_head list; //控制器链表

   //控制器对应的SPI总线号 SPI-2 对应bus_num= 2
  s16      bus_num;
  u16      num_chipselect;//控制器支持的片选数量,即能支持多少个spi设备 
  u16      dma_alignment;//DMA缓冲区对齐方式
  u16      mode_bits;// mode标志

  /* other constraints relevant to this driver */
  u16      flags;
#define SPI_MASTER_HALF_DUPLEX  BIT(0)  /* can't do full duplex */
#define SPI_MASTER_NO_RX  BIT(1)    /* can't do buffer read */
#define SPI_MASTER_NO_TX  BIT(2)    /* can't do buffer write */

  // 并发同步时使用
  spinlock_t    bus_lock_spinlock;
  struct mutex    bus_lock_mutex;

  /* flag indicating that the SPI bus is locked for exclusive use */
  bool      bus_lock_flag;

    //设置SPI mode和时钟, 在spi_add_device中调用
  int      (*setup)(struct spi_device *spi);
    //传输数据函数, 实现数据的双向传输
  int      (*transfer)(struct spi_device *spi,
            struct spi_message *mesg);
  //注销时回调
  void      (*cleanup)(struct spi_device *spi);

  /*
   * These hooks are for drivers that want to use the generic
   * master transfer queueing mechanism. If these are used, the
   * transfer() function above must NOT be specified by the driver.
   * Over time we expect SPI drivers to be phased over to this API.
   */
  bool        queued;
  struct kthread_worker    kworker;
  struct task_struct    *kworker_task;
  struct kthread_work    pump_messages;
  spinlock_t      queue_lock;
  struct list_head    queue;
  struct spi_message    *cur_msg;
  bool        busy;
  bool        running;
  bool        rt;

  int (*prepare_transfer_hardware)(struct spi_master *master);
  int (*transfer_one_message)(struct spi_master *master,
            struct spi_message *mesg);
  int (*unprepare_transfer_hardware)(struct spi_master *master);
}
  • spi_driver
//SPI驱动,和platform_driver,i2c_driver类似
struct spi_driver {
  const struct spi_device_id *id_table;
  int  (*probe)(struct spi_device *spi);
  int  (*remove)(struct spi_device *spi);
  void  (*shutdown)(struct spi_device *spi);
  int  (*suspend)(struct spi_device *spi, pm_message_t mesg);
  int  (*resume)(struct spi_device *spi);
  struct device_driver  driver;
};
  • spi_device
//SPI 设备
struct spi_device {
  struct device    dev;
  struct spi_master  *master; //指向SPI控制器
  u32      max_speed_hz; //最大速率
  u8      chip_select; //片选
  u8      mode; //SPI设备模式,使用下面的宏
#define  SPI_CPHA  0x01      /* clock phase */
#define  SPI_CPOL  0x02      /* clock polarity */
#define  SPI_MODE_0  (0|0)      /* (original MicroWire) */
#define  SPI_MODE_1  (0|SPI_CPHA)
#define  SPI_MODE_2  (SPI_CPOL|0)
#define  SPI_MODE_3  (SPI_CPOL|SPI_CPHA)
#define  SPI_CS_HIGH  0x04      /* chipselect active high? */
#define  SPI_LSB_FIRST  0x08      /* per-word bits-on-wire */
#define  SPI_3WIRE  0x10      /* SI/SO signals shared */
#define  SPI_LOOP  0x20      /* loopback mode */
#define  SPI_NO_CS  0x40      /* 1 dev/bus, no chipselect */
#define  SPI_READY  0x80      /* slave pulls low to pause */
  u8      bits_per_word;
  int      irq;
  void      *controller_state; //控制器运行状态
  void      *controller_data; //特定板子为控制器定义的数据
  char      modalias[SPI_NAME_SIZE];

};
  • spi_message
//SPI传输数据结构体
struct spi_message {
  struct list_head  transfers; // spi_transfer链表头

  struct spi_device  *spi; //spi设备

  unsigned    is_dma_mapped:1;

  //发送完成回调
  void      (*complete)(void *context);
  void      *context;
  unsigned    actual_length;
  int      status;

  /* for optional use by whatever driver currently owns the
   * spi_message ...  between calls to spi_async and then later
   * complete(), that's the spi_master controller driver.
   */
  struct list_head  queue;
  void      *state;
};
  • spi_transfer
// 该结构体是spi_message下的子单元,
struct spi_transfer {
  
  const void  *tx_buf;// 发送的数据缓存区
  void    *rx_buf;// 接收的数据缓存区
  unsigned  len;

  dma_addr_t  tx_dma; //tx_buf的DMA地址
  dma_addr_t  rx_dma; //rx_buf的DMA地址

  unsigned  cs_change:1;
  u8    bits_per_word;
  u16    delay_usecs;
  u32    speed_hz;

  struct list_head transfer_list;
};

上面结构体间的关系:

1.spi_driver和spi_device

spi_driver对应一套驱动方法,包含probe,remove的方法。spi_device对应真实的物理设备,每一个spi设备都需要一个spi_device来描述。spi_driver与spi_device是一对多的关系,一个spi_driver上可以支持多个同类型的spi_device

2.spi_master和spi_device

spi_master和spi_device的关系和硬件上控制器与设备关系一致,即spi_device依附于spi_master。

3.spi_message和spi_transfer

spi传输数据是以spi_message为单位的,我们需要传输的内容在spi_transfer中。spi_transfer是spi_message的子单元。

1 . 将本次需要传输的 spi_transfer 以 spi_transfer->transfer_list 为链表项,连接成一个transfer_list链表,挂接在本次传输的spi_message spi_message->transfers链表下。

2 . 将所有等待传输的 spi_message 以 spi_message->queue 为链表项,连接成个链表挂接在queue下。

image

API函数

//分配一个spi_master
struct spi_master *spi_alloc_master(struct device *dev, unsigned size)

//注册和注销spi_master
int spi_register_master(struct spi_master *master)
void spi_unregister_master(struct spi_master *master)

//注册和注销spi_driver
int spi_register_driver(struct spi_driver *sdrv)
void spi_unregister_driver(struct spi_driver *sdrv)

//初始化spi_message
void spi_message_init(struct spi_message *m)
//向spi_message添加transfers
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
//异步发送spi_message
int spi_async(struct spi_device *spi, struct spi_message *message)
//同步发送spi_message
int spi_sync(struct spi_device *spi, struct spi_message *message)

//spi同步写(封装了上面的函数)
int spi_write(struct spi_device *spi, const void *buf, size_t len)
//spi同步读(封装了上面的函数)
int spi_read(struct spi_device *spi, void *buf, size_t len)
//同步写并读取(封装了上面的函数)
int spi_write_then_read(struct spi_device *spi,
    const void *txbuf, unsigned n_tx,
    void *rxbuf, unsigned n_rx)

使用spi_async()需要注意的是,在complete未返回前不要轻易访问你一提交的spi_transfer中的buffer。也不能释放SPI系统正在使用的buffer。一旦你的complete返回了,这些buffer就又是你的了。

spi_sync是同步的,spi_sync提交完spi_message后不会立即返回,会一直等待其被处理。一旦返回就可以重新使用buffer了。spi_sync()调用了spi_async(),并休眠直至complete返回。

上面的传输函数最终都是调用spi_master的transfer()函数。

参考源码

STM32裸机驱动框架

SPI是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便。

SPI接口一般四根线:

MISO:主设备数据输入,从设备数据输出。

MOSI:主设备数据输出,从设备数据输入。

SCLK:时钟信号,由主设备产生。

CS:从设备片选信号,由主设备控制。

SPI主要特点有:可以同时发出和接收串行数据;可以当作主机或从机工作;提供频率可编程时钟;发送结束中断标志;写冲突保护;总线竞争保护等。

STM32的SPI功能很强大,SPI时钟最多可以到18Mhz,支持DMA,可以配置为SPI协议或者I2S协议

具体硬件性能可以查询相关芯片手册

STM32标准库驱动

typedef struct
{
    uint16_t SPI_Direction; // 设置SPI 的通信方式,可以选择为半双工,全双工,以及串行发和串行收方式
   uint16_t SPI_Mode; // 设置SPI 的主从模式
   uint16_t SPI_DataSize; // 为8 位还是16 位帧格式选择项
   uint16_t SPI_CPOL; // 设置时钟极性
   uint16_t SPI_CPHA; // 设置时钟相位
   uint16_t SPI_NSS;   //设置NSS 信号由硬件(NSS管脚)还是软件控制
   uint16_t SPI_BaudRatePrescaler;  //设置SPI 波特率预分频值
   uint16_t SPI_FirstBit;    //设置数据传输顺序是MSB 位在前还是LSB 位在前
   uint16_t SPI_CRCPolynomial; //设置CRC 校验多项式,提高通信可靠性,大于1 即可
}SPI_InitTypeDef;

第一个参数SPI_Direction 是用来设置SPI的通信方式,可以选择为半双工,全双工,以及串行发和串行收方式,这里我们选择全双工模式SPI_Direction_2Lines_FullDuplex。

第二个参数SPI_Mode用来设置SPI的主从模式,这里我们设置为主机模式 SPI_Mode_Master,当然有需要你也可以选择为从机模式 SPI_Mode_Slave。

第三个参数SPI_DataSiz为8位还是16位帧格式选择项,这里我们是8位传输,选择SPI_DataSize_8b。

第四个参数SPI_CPOL用来设置时钟极性,我们设置串行同步时钟的空闲状态为高电平所以我们选择SPI_CPOL_High。

第五个参数SPI_CPHA 用来设置时钟相位,也就是选择在串行同步时钟的第几个跳变沿(上升或下降)数据被采样,可以为第一个或者第二个条边沿采集,这里我们选择第二个跳变沿,所以选择 SPI_CPHA_2Edge

第六个参数SPI_NSS设置NSS信号由硬件(NSS管脚)还是软件控制,这里我们通过软件控制NSS关键,而不是硬件自动控制,所以选择 SPI_NSS_Soft。

第七个参数 SPI_BaudRatePrescaler很关键,就是设置 SPI 波特率预分频值也就是决定 SPI 的时钟的参数,从不分频道256分频8个可选值,初始化的时候我们选择256分频值SPI_BaudRatePrescaler_256,传输速度为36M/256=140.625KHz。

第八个参数 SPI_FirstBit设置数据传输顺序是 MSB 位在前还是LSB位在前,这里我们选择SPI_FirstBit_MSB高位在前。

第九个参数 SPI_CRCPolynomial是用来设置CRC校验多项式,提高通信可靠性,大于1即可。

SPI初始化示例代码:

void  SPIInit( void )
{
    SPI_InitTypeDef SPI_InitStructure;
    FLASH_GPIO_Init();
    /*!< Deselect the FLASH: Chip Select high */
    GPIO_SetBits( GPIOA, GPIO_Pin_4 );
    /*!< SPI configuration */
    SPI_InitStructure.SPI_Direction     = SPI_Direction_2Lines_FullDuplex;      /* 双线双向全双工 */
    SPI_InitStructure.SPI_Mode      = SPI_Mode_Master;                      /* 主 SPI */
    SPI_InitStructure.SPI_DataSize      = SPI_DataSize_8b;                      /* SPI 发送接收 8 位帧结构 */
    SPI_InitStructure.SPI_CPOL      = SPI_CPOL_High;                        /* 串行同步时钟的空闲状态为高电平 */
    SPI_InitStructure.SPI_CPHA      = SPI_CPHA_2Edge;                       /* 第二个跳变沿数据被采样 */
    SPI_InitStructure.SPI_NSS       = SPI_NSS_Soft;                         /* NSS 信号由软件控制 */
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;             /* 预分频 16 */
 
    SPI_InitStructure.SPI_FirstBit      = SPI_FirstBit_MSB;                     /* 数据传输从 MSB 位开始 */
    SPI_InitStructure.SPI_CRCPolynomial = 7;                                    /* CRC 值计算的多项式 */
    SPI_Init( SPI1, &SPI_InitStructure );
    /*!< Enable the sFLASH_SPI  */
    SPI_Cmd( SPI1, ENABLE );
}

SPI的应用

SPI的常用应用NorFlash

从数据手册上看到,SPI传输:CKPOL=1 , CKPHA=1

image

所以STM32的SPI读取NorFlash的配置如下

image

抓取下面代码波形

image

抓取的波形如下

image

0100 1011 就是0X4B

其中看到:

起始电平是高电平,也就是CKPOL=1

第二个边沿采样,也就是CKPHA=1

其实说成类似IIC的高电平有效也是没有问题的

下面这句话是写模拟SPI的核心

自己的理解:在下降沿转换数据,在上升沿采样数据

除了抓取波形,在华邦Flash也看到了时序图

image

示例代码:

读取norflash

/**
  * @brief  Initializes the peripherals used by the SPI FLASH driver.
  * @param  None
  * @retval None
  */
void FLASH_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStructure;
  
  /*!< Enable the SPI clock */
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
  
  /*!< Enable GPIO clocks */
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);  
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE); 
  /*!< SPI pins configuration *************************************************/
  
  /*!< Connect SPI pins to AF5 */  
  GPIO_PinAFConfig(GPIOA, 5, GPIO_AF_SPI1);
  GPIO_PinAFConfig(GPIOA, 6, GPIO_AF_SPI1);
  GPIO_PinAFConfig(GPIOB, 5, GPIO_AF_SPI1);
  
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
  GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP;//GPIO_PuPd_DOWN;
  
  /*!< SPI SCK pin configuration */
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
  GPIO_Init(GPIOA, &GPIO_InitStructure);
  
  /*!< SPI MISO pin configuration */
  GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_6;
  GPIO_Init(GPIOA, &GPIO_InitStructure);
  
  /*!< SPI MOSI pin configuration */
  GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_5;
  GPIO_Init(GPIOB, &GPIO_InitStructure);
  
  /*!< Configure sFLASH Card CS pin in output pushpull mode */
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
  GPIO_Init(GPIOE, &GPIO_InitStructure);
}
 
/**
  * @brief  Initializes the peripherals used by the SPI FLASH driver.
  * @param  None
  * @retval None
  */
void  FLASH_SPIInit(void)
{  
  
  SPI_InitTypeDef  SPI_InitStructure;
  
  FLASH_GPIO_Init();
  
  /*!< Deselect the FLASH: Chip Select high */
  GPIO_SetBits(GPIOE,GPIO_Pin_12);
  
  /*!< SPI configuration */
  SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//双线双向全双工
  SPI_InitStructure.SPI_Mode = SPI_Mode_Master;//主 SPI
  SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;// SPI 发送接收 8 位帧结构
  SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;//串行同步时钟的空闲状态为高电平 
  SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;//第二个跳变沿数据被采样
  SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;//NSS 信号由软件控制
  SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;//预分频 16
  
  SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;//数据传输从 MSB 位开始
  SPI_InitStructure.SPI_CRCPolynomial = 7;//CRC 值计算的多项式
  SPI_Init(SPI1, &SPI_InitStructure);
  /*!< Enable the sFLASH_SPI  */
  SPI_Cmd(SPI1, ENABLE);
}

软件模拟SPI协议

/**
  * @brief  Sends a byte through the SPI interface and return the byte received
  *         from the SPI bus.
  * @param  byte: byte to send.
  * @retval The value of the received byte.
  */
uint8_t SPI_ReadWriteByte(uint8_t data)
{
  uint8_t i,data_read = 0;  
  if(data!=0xA5){
    for(i=0;i<8;i++){
      MSPI_CLK_L();                                                    
      if(data&0x80){                                                    
    MSPI_MOSI_H();
      }else{
    MSPI_MOSI_L();
      }
      MSPI_DELAY();
      data = data<<1;                                                    
      MSPI_CLK_H();                                                     
      MSPI_DELAY();                                                        
    }
    return data_read;
  }else{
    for(i=0;i<8;i++){
      MSPI_CLK_L();                                         
      MSPI_DELAY();                                         
      data_read = data_read<<1;                                       
      MSPI_CLK_H();                                        
      if(MSPI_READ_IN()){                                                
    data_read |= 0x01;                                   
      }
      MSPI_DELAY();
    }
    return data_read;
  }                                          
}
 
/**
  * @brief  Initializes the peripherals used by the SPI FLASH driver.
  * @param  None
  * @retval None
  */
void FLASH_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStructure;
 
  /*!< Enable GPIO clocks */
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);  
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);
  
  /*!< Configure sFLASH Card CS pin in output pushpull mode */
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
  GPIO_Init(GPIOE, &GPIO_InitStructure);
  
  /*!< SPI SCK pin configuration */
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
  GPIO_Init(GPIOA, &GPIO_InitStructure);
  MSPI_CLK_H();
  
  /*!< SPI MOSI pin configuration */
  GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_5;
  GPIO_Init(GPIOB, &GPIO_InitStructure);
  MSPI_MOSI_H();
  
  /*!< SPI MISO pin configuration */
  GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_6;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
  GPIO_Init(GPIOA, &GPIO_InitStructure);
}
 
/**
  * @brief  Initializes the peripherals used by the SPI FLASH driver.
  * @param  None
  * @retval None
  */
void  FLASH_SPIInit(void)
{  
  
  FLASH_GPIO_Init();
  
  /*!< Deselect the FLASH: Chip Select high */
  GPIO_SetBits(GPIOE,GPIO_Pin_12);
}

参考文章:

STM32 SPI详解 - 知乎 (zhihu.com)

Linux驱动分析之SPI驱动架构 - 知乎 (zhihu.com)

Linux下SPI驱动详解_linux下的spi驱动_一口Linux的博客-CSDN博客