STM32通关之路——SPI

目录

系列文章目录

文章目录

一、SPI协议简介

1.1、SPI物理层

1.2、协议层

二、STM32的SPI架构剖析

1.SPI初始化结构体

 三、SPI——读写串行FLASH实验

总结



一、SPI协议简介

 SPI(Serial Peripheral Interface),串行外围设备接口,是一种高速全双工的通信总线。它被广泛使用在ADC、LCD等设备与MCU间、要求通讯速率较高的场合。

1.1、SPI物理层

SPI通讯系统
  • Slave Select:从设备选择信号线,常称为片选信号线,也称为NSS、CS。 当有多个 SPI 从设备与 SPI 主机相连时,设备的其它信号线 SCK、MOSI 及 MISO 同时并联到相 同的 SPI 总线上,即无论有多少个从设备,都共同只使用这 3 条总线;而每个从设备都有独立的 这一条 NSS 信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。当主机要选择从设备时,把该从设备的NSS信号线设置为低电平,接着主机开始与从设备进行SPI通讯,即SPI通讯以NSS线置低电平为开始信号,以NSS线被拉高作为结束信号
  • SCK:时钟信号线,用于通讯数据同步。它由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样,两个设备之间通讯时,通讯速率受限于低速设备。
  • MOSI:主出从入,即数据从主机到从机。
  • MISO:主入从出,即数据从从机到主机。

1.2、协议层

通讯时序(四种通讯模式的一种)

1. 通讯的起始和停止信号:

                片选信号线置低电平为开始,拉高为结束信号;

2. 数据有效性:

               SCK 的下降沿时刻,MOSI 及 MISO 的数据有效SCK每个时钟周期MOSI、MISO传输一位数据,且数据输入输出是同时进行的。数据传输时SPI 每次数据传输可以 8 位或 16 位为单位,每次传输的单位数不受限制。

3. CPOL/CPHA及通讯模式:

四种模式(0、3采用较多)

        时钟极性CPOL是指SPI通讯设备处于空闲状态(通讯开始之前)时,SCK信号线的电平信号。CPOL=0 时,SCK 在空闲状态时为低电平,CPOL=1,则相反。

        时钟相位CPHA是指数据的采样时刻,当CPHA=0时,MOSI或MISO数据线上的信号将会在SCK时钟线的“奇数边沿”被采样,当CPHA=1时,数据线在SCK的“偶数边沿”采样。

二、STM32的SPI架构剖析

STM32 的 SPI 外设可用作通讯的主机及从机,支持最高的 SCK 时钟频率为 fpclk/2 (STM32F103 型 号的芯片默认 fpclk1 为 36MHz,fpclk2 为 72MHz),完全支持 SPI 协议的 4 种模式,数据帧长度可设置为 8 位或 16 位,可设置数据 MSB 先行或 LSB 先行。它还支持双线全双工、双线单向以及单线模式。

1. 时钟控制逻辑:

        SCK线的时钟信号,由波特率发生器根据”控制寄存器CR1“中的BR[0:2]位控制,该位时对fplck时钟的分频因子 ,对 fpclk 的分频结果就是 SCK 引脚的输出时钟频率。

1.SPI初始化结构体

typedef struct
{
    uint16_t SPI_Direction;         /* 设置 SPI 的单双向模式 */
    uint16_t SPI_Mode;              /* 设置 SPI 的主/从机端模式 */
    uint16_t SPI_DataSize;          /* 设置 SPI 的数据帧长度,可选 8/16 位 */
    uint16_t SPI_CPOL;              /* 设置时钟极性 CPOL,可选高/低电平 */
    uint16_t SPI_CPHA;              /* 设置时钟相位,可选奇/偶数边沿采样 */
    uint16_t SPI_NSS;               /* 设置NSS引脚由SPI硬件控制还是软件控制 */
    uint16_t SPI_BaudRatePrescaler; /* 设置时钟分频因子,fpclk/分频数 =fSCK */
    uint16_t SPI_FirstBit;          /* 设置 MSB/LSB 先行 */
    uint16_t SPI_CRCPolynomial;     /* 设置 CRC 校验的表达式 */
} SPI_InitTypeDef;
  • SPI_Direction:设置SPI的通讯方向,可设置为双线全(SPI_Direction_2Lines_FullDuplex)、双线只接收(SPI_Direction_2Lines_RxOnly)、单线只接收(SPI_Direction_1Line_Rx)、单线只发送(SPI_Direction_1Line_Tx)模式;
  • SPI_Mode:设置SPI工作在主机模式或从机模式,这两个模式 的最大区别为 SPI 的 SCK 信号线的时序来源。

 三、SPI——读写串行FLASH实验

FLSAH 存储器又称闪存,它与 EEPROM 都是掉电后数据不丢失的存储器,但 FLASH 存储器容量普遍大于 EEPROM,现在基本取代了它的地位。 FLASH 芯片只能一大片一大片地擦写,EEPROM 可以单个字节擦写

流程:

  1. 初始化通讯使用的目标引脚及端口时钟;
  2. 使能 SPI 外设的时钟;
  3. 配置 SPI 外设的模式、地址、速率等参数并使能 SPI 外设;
  4. 编写基本 SPI 按字节收发的函数;(通过发送读ID指令获取设备ID)
  5. 编写对 FLASH 擦除及读写操作的的函数;(FLASH在写入前需要进行擦除操作)
  6. 编写测试程序,对读写数据进行校验。

3.1、spi.h中进行FLASH操作的指令宏定义:

指令表
#define W25X_WriteEnable                    0x06 
#define W25X_WriteDisable                   0x04 
#define W25X_ReadStatusReg                  0x05 
#define W25X_WriteStatusReg                 0x01 
#define W25X_ReadData                       0x03 
#define W25X_FastReadData                   0x0B 
#define W25X_FastReadDual                   0x3B 
#define W25X_PageProgram                    0x02 
#define W25X_BlockErase                     0xD8 
#define W25X_SectorErase                    0x20 
#define W25X_ChipErase                      0xC7 
#define W25X_PowerDown                      0xB9 
#define W25X_ReleasePowerDown               0xAB 
#define W25X_DeviceID                       0xAB 
#define W25X_ManufactDeviceID               0x90 
#define W25X_JedecDeviceID                  0x9F

#define sFLASH_ID                           0XEF4017
#define Dummy_Byte                          0XFF
#define SPI_FLASH_PageSize                  256
#define SPI_FLASH_PerWritePageSize          256
#define SPI1_TIME_OUT                       1000

 3.2、声明应用的操作函数

void SPI_FLASH_SectorErase(uint32_t SectorAddr);
uint32_t  SPI_Flash_ReadID(void);
void SPI_Flash_Erase_Chip(void);
void SPI_Flash_Read(uint32_t ReadAddr,uint16_t NumByteToRead,uint8_t* pBuffer);
void SPI_Flash_Write(uint32_t WriteAddr,uint16_t NumByteToWrite,uint8_t* pBuffer);
void SPI_Flash_Write_Page(uint32_t WriteAddr,uint16_t NumByteToWrite,uint8_t* pBuffer);

3.3、在spi.c中实现读写FLASH相关函数 

3.3.1、对片选进行宏定义

// 使用了PC0引脚,作为SPI使能信号
#define SPI_FLASH_CS_H()   HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_SET) 
#define SPI_FLASH_CS_L()   HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_RESET)

3.3.2、读写字节函数 

/******************************************************
* function:SPI读一个数据
* return:返回通过SPIx接收的数据
******************************************************/
uint8_t SPI1_ReadByte(void)
{                     
   uint8_t RxData;            
   HAL_SPI_Receive(&hspi1, &RxData, 1, SPI1_TIME_OUT);                     
   return RxData;                                      
}


/******************************************************
* function:SPI写一个数据
******************************************************/
void SPI1_WriteByte(uint8_t TxData)
{                                
    HAL_SPI_Transmit(&hspi1, &TxData, 1, SPI1_TIME_OUT);                                                                     
}

 3.3.3、FLASH的写使能和非使能

/**************************************************
 * function:SPI_FLASH写使能,将WEL置位
 ***************************************************/
void SPI_FLASH_Write_Enable(void)   
{
   SPI_FLASH_CS_L();                      //使能器件   
   SPI1_WriteByte(W25X_WriteEnable);      //发送写使能  
   SPI_FLASH_CS_H();                      //取消片选                  
} 


/*****************************************************
 * function:SPI_FLASH写禁止,将WEL清零
 *****************************************************/
void SPI_FLASH_Write_Disable(void)   
{  
   SPI_FLASH_CS_L();                      //使能器件   
   SPI1_WriteByte(W25X_WriteDisable);     //发送写禁止指令    
   SPI_FLASH_CS_H();                      //取消片选                  
}

3.3.4、读取SPI_FLASH的状态寄存器,判断FLASH的状态 

/*******************************************
 * function:读取SPI_FLASH的状态寄存器
 ********************************************/
// BIT7  6   5   4   3   2   1   0
// SPR   RV  TB BP2 BP1 BP0 WEL BUSY
// SPR:默认0,状态寄存器保护位,配合WP使用
// TB,BP2,BP1,BP0:FLASH区域写保护设置
// WEL:写使能锁定
// BUSY:忙标记位(1,忙; 0,空闲)
// 默认:0x00
uint8_t SPI_Flash_ReadSR(void)   
{  
    uint8_t byte=0;   
    SPI_FLASH_CS_L();                       // 使能器件   
    SPI1_WriteByte(W25X_ReadStatusReg);     // 发送读取状态寄存器命令    
    byte = SPI1_ReadByte();                 // 读取一个字节  
    SPI_FLASH_CS_H();                       // 取消片选     
    return byte;   
} 

/************************************************
 * function:等待空闲
 ************************************************/
void SPI_Flash_Wait_Busy(void)   
{   
    while ((SPI_Flash_ReadSR()&0x01)==0x01);   // 等待BUSY位清空
}

 3.3.5、读取芯片ID,判断FLASH是否正常


/*******************************************
 *function:读取芯片ID W25Q64的ID
 *******************************************/
uint32_t  SPI_Flash_ReadID(void)
{
   uint32_t Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0; 
   SPI_FLASH_Write_Enable(); 
   SPI_FLASH_CS_L();                
   SPI1_WriteByte(W25X_JedecDeviceID);    //发送读取ID命令           
   Temp0 = SPI1_ReadByte();       
   Temp1 = SPI1_ReadByte();              
   Temp2 = SPI1_ReadByte();       

  /* 把数据组合起来,作为函数的返回值 */
   Temp = (Temp0 << 16) | (Temp1 << 8) | Temp2;
   SPI_FLASH_CS_H();              
   return Temp;
}

3.3.6、FLASH扇区擦除函数(FLASH在写之前一定要进行擦除,否则不能正常写入) 

FLASH芯片最小擦除单位为扇区,一个块包含16个扇区
/**********************************************************
 * function:擦除整个芯片
 **********************************************************/
//整片擦除时间:
//W25X16:25s 
//W25X32:40s 
//W25X64:40s 
//等待时间超长...
void SPI_Flash_Erase_Chip(void)   
{                                             
   SPI_FLASH_Write_Enable();               //SET WEL 
   SPI_Flash_Wait_Busy();   
   SPI_FLASH_CS_L();                      //使能器件   
   SPI1_WriteByte(W25X_ChipErase);        //发送片擦除命令  
   SPI_FLASH_CS_H();                      //取消片选                  
   SPI_Flash_Wait_Busy();                 //等待芯片擦除结束
}  


/**********************************************************
 * function:SPI_FLASH_SectorErase
 * annotation:Flash的一个扇区是4K,所以输入的地址要和4K对其。
 **********************************************************/
void SPI_FLASH_SectorErase(uint32_t SectorAddr)
{
    SPI_FLASH_Write_Enable(); 
    SPI_Flash_Wait_Busy();       
    SPI_FLASH_CS_L(); 
    SPI1_WriteByte(W25X_SectorErase); 
    SPI1_WriteByte((SectorAddr & 0xFF0000) >> 16); 
    SPI1_WriteByte((SectorAddr & 0xFF00) >> 8); 
    SPI1_WriteByte(SectorAddr & 0xFF);
    SPI_FLASH_CS_H();      
    SPI_Flash_Wait_Busy();
}

 3.3.7、读取FLASH函数

/**********************************************
 * function:读取SPI FLASH  
 **********************************************/
// 在指定地址开始读取指定长度的数据
// pBuffer:数据存储区
// ReadAddr:开始读取的地址(24bit)
// NumByteToRead:要读取的字节数(最大65535)
void SPI_Flash_Read(uint32_t ReadAddr,uint16_t NumByteToRead,uint8_t* pBuffer)   
{ 
   uint16_t i;                                                                                            
   SPI_FLASH_CS_L();                                    //使能器件   
   SPI1_WriteByte(W25X_ReadData);                       //发送读取命令   
   SPI1_WriteByte((uint8_t)((ReadAddr)>>16));           //发送24bit地址    
   SPI1_WriteByte((uint8_t)((ReadAddr)>>8));
   SPI1_WriteByte((uint8_t)ReadAddr);
   for(i=0;i<NumByteToRead; i++)
   { 
      pBuffer[i] = SPI1_ReadByte();                      //循环读数  
   }
   SPI_FLASH_CS_H();                                    //取消片选
}

3.3.8、FLASH的按页写入,写入不是随机,可以任意写入、按页写入,一页最大数据是256字节

/************************************************************
 * function:SPI在一页(0~65535)内写入少于256个字节的数据 
 * annotation:一页最大256个字节
 ************************************************************/
// 在指定地址开始写入最大256字节的数据
// pBuffer:数据存储区
// WriteAddr:开始写入的地址(24bit)
// NumByteToWrite:要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!
void SPI_Flash_Write_Page(uint32_t WriteAddr,uint16_t NumByteToWrite,uint8_t* pBuffer)
{
   uint16_t i;
   SPI_FLASH_Write_Enable();                                 // SET WEL
   SPI_FLASH_CS_L();                                         // 使能器件
   SPI1_WriteByte(W25X_PageProgram);                         // 发送写页命令
   SPI1_WriteByte((uint8_t)((WriteAddr)>>16));               // 发送24bit地址
   SPI1_WriteByte((uint8_t)((WriteAddr)>>8));
   SPI1_WriteByte((uint8_t)WriteAddr);
   for(i=0;i<NumByteToWrite;i++) SPI1_WriteByte(pBuffer[i]); // 循环写数
   SPI_FLASH_CS_H();                                         // 取消片选 
   SPI_Flash_Wait_Busy();                                    // 等待写入结束                                                                                     
}


/***************************************************************
 *@function 不定量的写入数据,先确保写入前擦出扇区
 *@annotation
 *@param
 *@retval
 ****************************************************************/
void SPI_Flash_Write(uint32_t WriteAddr, uint16_t NumByteToWrite, uint8_t* pBuffer)
{
    uint8_t NumOfPage=0, NumOfSingle=0, Addr=0, count=0, temp=0;

    /* 计算Addr,写入的地址是否和PageSize对齐 */
    Addr = WriteAddr % SPI_FLASH_PageSize;

    /* count 为剩余的地址 */
    count = SPI_FLASH_PageSize - Addr;

    /* 计算能写入多少整数页 */
    NumOfPage = NumByteToWrite % SPI_FLASH_PageSize;

    /* 计算不满一页的数据 */
    NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;

    /* Addr=0,则 WriteAddr 刚好按页对齐 aligned */
    if(Addr == 0)
       {
        /* NumByteToWrite < SPI_FLASH_PageSize,一页能写完 */
          if(NumOfPage == 0)
          {
              SPI_Flash_Write_Page(WriteAddr,NumByteToWrite,pBuffer);
          }
          else/* NumByteToWrite > SPI_FLASH_PageSize,一页写不完,先写整数页,在写剩下的 */
          {
             /*先把整数页都写了*/
             while (NumOfPage--)
             {
                 SPI_Flash_Write_Page(WriteAddr,SPI_FLASH_PageSize,pBuffer);
                 WriteAddr+=SPI_FLASH_PageSize; // flash 的地址加一页的大小
                 pBuffer+=SPI_FLASH_PageSize;   // 写缓存数据的地址加一页的大小
             }
             /*若有多余的不满一页的数据,把它写完*/
             SPI_Flash_Write_Page(WriteAddr,NumOfSingle,pBuffer);
           }
       }
       else/* 若地址与 SPI_FLASH_PageSize 不对齐 */
       {
                     /* NumByteToWrite < SPI_FLASH_PageSize */
              if (NumOfPage == 0)
              {
                     /*当前页剩余的 count 个位置比 NumOfSingle 小,一页写不完*/
                     if(NumOfSingle >count)
                     {
                            temp = NumOfSingle -count;
                            /*先写满当前页*/
                            SPI_Flash_Write_Page(WriteAddr,count,pBuffer);
                            WriteAddr += count;
                            pBuffer += count;
                            /*再写剩余的数据*/
                            SPI_Flash_Write_Page(WriteAddr,temp,pBuffer);
                     }
                     else/*当前页剩余的 count 个位置能写完 NumOfSingle 个数据*/
                     {
                            SPI_Flash_Write_Page(WriteAddr,NumByteToWrite,pBuffer);
                     }
              }
              else/* NumByteToWrite > SPI_FLASH_PageSize */
              {
                     /*地址不对齐多出的 count 分开处理,不加入这个运算*/
                     NumByteToWrite -= count;
                     NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
                     NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
                     /* 先写完 count 个数据,为的是让下一次要写的地址对齐 */
                     SPI_Flash_Write_Page(WriteAddr,count,pBuffer);
                     /* 接下来就重复地址对齐的情况 */
                     WriteAddr += count;
                     pBuffer += count;
                     /*把整数页都写了*/
                     while (NumOfPage--)
                     {
                            SPI_Flash_Write_Page(WriteAddr,SPI_FLASH_PageSize,pBuffer);
                            WriteAddr += SPI_FLASH_PageSize;
                            pBuffer += SPI_FLASH_PageSize;
                     }
                     /*若有多余的不满一页的数据,把它写完*/
                     if (NumOfSingle != 0)
                     {
                            SPI_Flash_Write_Page(WriteAddr,NumOfSingle,pBuffer);
                     }
              }
       }              
}

3.4、main.c中对FLASH的读写地址进行宏定义 

#define  FLASH_WriteAddress     0x00000
#define  FLASH_ReadAddress      FLASH_WriteAddress
#define  FLASH_SectorToErase    FLASH_WriteAddress
#define FLASH_SPI hspi1

3.5、main.c 中定义变量 

uint32_t flash_ID = 0;
/* 获取缓冲区的长度 */
#define countof(a)      (sizeof(a) / sizeof(*(a)))
uint8_t Tx_Buffer[] = "现在进行FLASH的读写测试\r\n";
#define  BufferSize (countof(Tx_Buffer)-1)
uint8_t Rx_Buffer[BufferSize];

3.6、main函数中进行测试 

 printf("*************this is test for coding...**********\t\n");
       printf("this is test code for spi1 read and write flash w25Q64 \r\n");
       flash_ID = SPI_Flash_ReadID();
       printf("\r\n flash ID is 0X%x\r\n",flash_ID);
       SPI_FLASH_SectorErase(FLASH_SectorToErase);       
       SPI_Flash_Write(FLASH_WriteAddress,BufferSize,Tx_Buffer);
       printf("\r\n 写入的数据为:%s \r\t", Tx_Buffer);
       SPI_Flash_Read(FLASH_ReadAddress,BufferSize,Rx_Buffer);
       printf("\r\n 读的数据为:%s \r\t", Rx_Buffer);
       /* 检查写入的数据与读出的数据是否相等 */
       if(memcmp(Tx_Buffer, Rx_Buffer, BufferSize)==0)
       {
              printf("写入的和读出的数据是正常的!\r\n");
       }
       printf("SPI 试验结束......!\r\n");
       printf("*************this is test end....**********\t\n");
   

3.7、函数重定向,使用串口调试助手

/***********************************************
  * @brief 重定向c库函数printf到USARTx
  * @retval None
  **********************************************/
int fputc(int ch, FILE *f)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
    return ch;
}
 

/***********************************************
  * @brief 重定向c库函数getchar,scanf到USARTx
  * @retval None
  **********************************************/
int fgetc(FILE *f)
{
    uint8_t ch = 0;
    HAL_UART_Receive(&huart1, &ch, 1, 0xffff);
    return ch;
}


总结

SPI的使用流程:cubeMX配置->指令表定义->读写FLASH函数定义->获取设备ID->擦除目标存储地址(扇区擦除、块擦除和整片擦除)->写入FLASH(页面写入、不定量写入)->读取FLASH

  • 12
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值