欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

STM32 入门:介绍 SPI 总线,读写 W25Q64 (FLASH)(硬件 + 模拟时序) - SPI 时序比较简单,CPU 如果没有硬件支持,可以直接使用 IO 端口编写代码来模拟,下面以模拟时序代码为例:

最编程 2024-04-07 08:44:02
...
SPI的模式1:
u8 SPI_ReadWriteOneByte(u8 tx_data)
{
  u8 i,rx_data=0;
  SCK=0; //空闲电平(默认初始化情况)
  for(i=0;i<8;i++)
  {
    /*1. 主机发送一位数据*/
    SCK=0;//告诉从机,主机将要发送数据
    if(tx_data&0x80)MOSI=1; //发送数据
    else MOSI=0;
    SCK=1; //告诉从机,主机数据发送完毕
    tx_data<<=1; //继续发送下一位
    
    /*2. 主机接收一位数据*/
    rx_data<<=1; //默认认为接收到0
    if(MISO)rx_data|=0x01;
  }
  SCK=0; //恢复空闲电平
  return rx_data;
}
 
SPI的模式2:
u8 SPI_ReadWriteOneByte(u8 tx_data)
{
  u8 i,rx_data=0;
  SCK=0; //空闲电平(默认初始化情况)
  for(i=0;i<8;i++)
  {
    /*1. 主机发送一位数据*/
    SCK=1;//告诉从机,主机将要发送数据
    if(tx_data&0x80)MOSI=1; //发送数据
    else MOSI=0;
    SCK=0; //告诉从机,主机数据发送完毕
    tx_data<<=1; //继续发送下一位
    
    /*2. 主机接收一位数据*/
    rx_data<<=1; //默认认为接收到0
    if(MISO)rx_data|=0x01;
  }
  SCK=0; //恢复空闲电平
  return rx_data;
}
 
 
SPI的模式3:
u8 SPI_ReadWriteOneByte(u8 tx_data)
{
  u8 i,rx_data=0;
  SCK=1; //空闲电平(默认初始化情况)
  for(i=0;i<8;i++)
  {
    /*1. 主机发送一位数据*/
    SCK=1;//告诉从机,主机将要发送数据
    if(tx_data&0x80)MOSI=1; //发送数据
    else MOSI=0;
    SCK=0; //告诉从机,主机数据发送完毕
    tx_data<<=1; //继续发送下一位
    
    /*2. 主机接收一位数据*/
    rx_data<<=1; //默认认为接收到0
    if(MISO)rx_data|=0x01;
  }
  SCK=1; //恢复空闲电平
  return rx_data;
}
 
SPI的模式4:
u8 SPI_ReadWriteOneByte(u8 tx_data)
{
  u8 i,rx_data=0;
  SCK=1; //空闲电平(默认初始化情况)
  for(i=0;i<8;i++)
  {
    /*1. 主机发送一位数据*/
    SCK=0;//告诉从机,主机将要发送数据
    if(tx_data&0x80)MOSI=1; //发送数据
    else MOSI=0;
    SCK=1; //告诉从机,主机数据发送完毕
    tx_data<<=1; //继续发送下一位
    
    /*2. 主机接收一位数据*/
    rx_data<<=1; //默认认为接收到0
    if(MISO)rx_data|=0x01;
  }
  SCK=1; //恢复空闲电平
  return rx_data;
}

四、W25Q64的示例代码

4.1 STM32采用硬件SPI读写W25Q64示例代码

/*
函数功能:SPI初始化(模拟SPI)
硬件连接:
MISO--->PB14
MOSI--->PB15
SCLK--->PB13
*/
void SPI_Init(void)
{
  /*开启时钟*/
  RCC->APB1ENR|=1<<14;   //开启SPI2时钟
  RCC->APB2ENR|=1<<3;    //PB
  GPIOB->CRH&=0X000FFFFF; //清除寄存器
  GPIOB->CRH|=0XB8B00000;
  GPIOB->ODR|=0X7<<13;      //PB13/14/15上拉--输出高电平
  /*SPI2基本配置*/
  SPI2->CR1=0X0;    //清空寄存器
  SPI2->CR1|=0<<15; //选择“双线双向”模式
  SPI2->CR1|=0<<11; //使用8位数据帧格式进行发送/接收;
  SPI2->CR1|=0<<10; //全双工(发送和接收);
  SPI2->CR1|=1<<9;  //启用软件从设备管理
  SPI2->CR1|=1<<8;  //NSS
  SPI2->CR1|=0<<7;  //帧格式,先发送高位
  SPI2->CR1|=0x0<<3;//当总线频率为36MHZ时,SPI速度为18MHZ,高速。
  SPI2->CR1|=1<<2;  //配置为主设备
  SPI2->CR1|=1<<1;  //空闲状态时, SCK保持高电平。
  SPI2->CR1|=1<<0;  //数据采样从第二个时钟边沿开始。
  SPI2->CR1|=1<<6;  //开启SPI设备。
}
 
 
/*
函数功能:SPI读写一个字节
*/
u8 SPI_ReadWriteOneByte(u8 data_tx)
{
    u16 cnt=0;         
    while((SPI2->SR&1<<1)==0)    //等待发送区空--等待发送缓冲为空 
    {
      cnt++;
      if(cnt>=65530)return 0;     //超时退出  u16=2个字节
    } 
    SPI2->DR=data_tx;               //发送一个byte 
    cnt=0;
    while((SPI2->SR&1<<0)==0)     //等待接收完一个byte   
    {
      cnt++;
      if(cnt>=65530)return 0;    //超时退出
    }                   
    return SPI2->DR;              //返回收到的数据 
}
 
 
/*
函数功能:W25Q64初始化
硬件连接:
MOSI--->PB15
MISO--->PB14
SCLK--->PB13
CS----->PB12
*/
void W25Q64_Init(void)
{
  /*1. 开时钟*/
  RCC->APB2ENR|=1<<3; //PB
  
  /*2. 配置GPIO口模式*/
  GPIOB->CRH&=0xFFF0FFFF;
  GPIOB->CRH|=0x00030000;
  
  W25Q64_CS=1; //未选中芯片
  SPI_Init();   //SPI初始化
}
 
 
/*
函数功能:读取芯片的ID号
*/
u16 W25Q64_ReadID(void)
{
  u16 id;
  /*1. 拉低片选*/
  W25Q64_CS=0;
  
  /*2. 发送读取ID的指令*/
  SPI_ReadWriteOneByte(0x90);
  
  /*3. 发送24位的地址-0*/
  SPI_ReadWriteOneByte(0);
  SPI_ReadWriteOneByte(0);
  SPI_ReadWriteOneByte(0);
  
  /*4. 读取芯片的ID*/
  id=SPI_ReadWriteOneByte(0xFF)<<8;
  id|=SPI_ReadWriteOneByte(0xFF);
 
  /*5. 拉高片选*/
  W25Q64_CS=1;
  return id;
}
 
/*
函数功能:检测W25Q64状态
*/
void W25Q64_CheckStat(void)
{
  u8 stat=1;
  while(stat&1<<0)
  {
    W25Q64_CS=0; //选中芯片
    SPI_ReadWriteOneByte(0x05);      //发送读状态寄存器1指令
    stat=SPI_ReadWriteOneByte(0xFF); //读取状态
    W25Q64_CS=1; //取消选中芯片
  }
}
 
 
/*
函数功能:页编程
说    明:一页最多写256个字节。 写数据之前,必须保证空间是0xFF
函数参数:
u32 addr:页编程起始地址
u8 *buff:写入的数据缓冲区
u16 len :写入的字节长度
*/
void W25Q64_PageWrite(u32 addr,u8 *buff,u16 len)
{
  u16 i;
  W25Q64_Enabled();             //写使能
  W25Q64_CS=0; //选中芯片
  SPI_ReadWriteOneByte(0x02); //页编程指令
  SPI_ReadWriteOneByte(addr>>16); //24~16地址
  SPI_ReadWriteOneByte(addr>>8);  //16~8地址
  SPI_ReadWriteOneByte(addr);     //8~0地址
 
  for(i=0;i<len;i++)
  {
    SPI_ReadWriteOneByte(buff[i]);     //8~0地址  
  }
  W25Q64_CS=1; //取消选中芯片
  W25Q64_CheckStat();  //检测芯片忙状态
}
 
 
/*
函数功能:连续读数据
函数参数:
u32 addr:读取数据的起始地址
u8 *buff:读取数据存放的缓冲区
u32 len :读取字节的长度
*/
void W25Q64_ReadByteData(u32 addr,u8 *buff,u32 len)
{
  u32 i;
  W25Q64_CS=0; //选中芯片
  SPI_ReadWriteOneByte(0x03);     //读数据指令
  SPI_ReadWriteOneByte(addr>>16); //24~16地址
  SPI_ReadWriteOneByte(addr>>8);  //16~8地址
  SPI_ReadWriteOneByte(addr);     //8~0地址
  for(i=0;i<len;i++)buff[i]=SPI_ReadWriteOneByte(0xFF);
  W25Q64_CS=1; //取消选中芯片
}
 
 
/*
函数功能:擦除一个扇区
函数参数:
u32 addr:擦除扇区的地址范围
*/
void W25Q64_ClearSector(u32 addr)
{
  W25Q64_Enabled();             //写使能
  W25Q64_CS=0; //选中芯片
  SPI_ReadWriteOneByte(0x20);     //扇区擦除指令
  SPI_ReadWriteOneByte(addr>>16); //24~16地址
  SPI_ReadWriteOneByte(addr>>8);  //16~8地址
  SPI_ReadWriteOneByte(addr);     //8~0地址
  W25Q64_CS=1;        //取消选中芯片
  W25Q64_CheckStat();  //检测芯片忙状态
}
 
/*
函数功能:写使能
*/
void W25Q64_Enabled(void)
{
  W25Q64_CS=0; //选中芯片
  SPI_ReadWriteOneByte(0x06);     //写使能
  W25Q64_CS=1; //取消选中芯片
}
 
 
/*
函数功能:指定位置写入指定个数的数据,不考虑擦除问题
注意事项:W25Q64只能将1写为,不能将0写为1。
函数参数:
u32 addr---写入数据的起始地址
u8 *buff---写入的数据
u32 len---长度
*/
void W25Q64_WriteByteDataNoCheck(u32 addr,u8 *buff,u32 len)
{
  u32 page_remain=256-addr%256; //计算当前页还可以写下多少数据
  if(len<=page_remain) //如果当前写入的字节长度小于剩余的长度
  {
    page_remain=len;
  }
  while(1)
  {
    W25Q64_PageWrite(addr,buff,page_remain);
    if(page_remain==len)break; //表明数据已经写入完毕
    buff+=page_remain; //buff向后偏移地址
    addr+=page_remain; //起始地址向后偏移
    len-=page_remain;  //减去已经写入的字节数
    if(len>256)page_remain=256;  //如果大于一页,每次就直接写256字节
    else page_remain=len;
  }
}
 
 
/*
函数功能:指定位置写入指定个数的数据,考虑擦除问题,完善代码
函数参数:
u32 addr---写入数据的起始地址
u8 *buff---写入的数据
u32 len---长度
说明:擦除的最小单位扇区,4096字节
*/
static u8 W25Q64_READ_WRITE_CHECK_BUFF[4096];
void W25Q64_WriteByteData(u32 addr,u8 *buff,u32 len)
{
    u32 i;
    u32 len_w;
    u32 sector_addr; //存放扇区的地址
    u32 sector_move; //扇区向后偏移的地址
    u32 sector_size; //扇区大小。(剩余的空间大小)
    u8 *p=W25Q64_READ_WRITE_CHECK_BUFF;//存放指针
    sector_addr=addr/4096; //传入的地址是处于第几个扇区
    sector_move=addr%4096; //计算传入的地址存于当前的扇区的偏移量位置
    sector_size=4096-sector_move; //得到当前扇区剩余的空间
 
    if(len<=sector_size)
    {
            sector_size=len; //判断第一种可能性、一次可以写完
    }
    
    while(1)
    {
        W25Q64_ReadByteData(addr,p,sector_size);   //读取剩余扇区里的数据
        for(i=0;i<sector_size;i++)
        {
            if(p[i]!=0xFF)break;
        }
        if(i!=sector_size)  //判断是否需要擦除
        {
             W25Q64_ClearSector(sector_addr*4096);
        }
//        for(i=0;i<len;i++)
//        {
//             W25Q64_READ_WRITE_CHECK_BUFF[i]=buff[len_w++]; 
        }
//        W25Q64_WriteByteDataNoCheck(addr,W25Q64_READ_WRITE_CHECK_BUFF,sector_size);
        W25Q64_WriteByteDataNoCheck(addr,buff,sector_size);
        if(sector_size==len)break;
 
        addr+=sector_size; //向后偏移地址
        buff+=sector_size ;//向后偏移
        len-=sector_size;  //减去已经写入的数据
        sector_addr++;     //校验第下个扇区
        if(len>4096)       //表明还可以写一个扇区
        {
                sector_size=4096;//继续写一个扇区
        }
        else
        {
                sector_size=len; //剩余的空间可以写完
        }
    }
}

4.2 STM32采用硬件SPI读写W25Q64示例代码

#include "spi.h"
 
 
/*
函数功能:SPI初始化(模拟SPI)
硬件连接:
MISO--->PB14
MOSI--->PB15
SCLK--->PB13
*/
void SPI_Init(void)
{
  /*1. 开时钟*/
  RCC->APB2ENR|=1<<3; //PB
 
  /*2. 配置GPIO口模式*/
  GPIOB->CRH&=0x000FFFFF;
  GPIOB->CRH|=0x38300000;
 
  /*3. 上拉*/
  SPI_MOSI=1;
  SPI_MISO=1;
  SPI_SCLK=1;
}
 
/*
函数功能:SPI读写一个字节
*/
u8 SPI_ReadWriteOneByte(u8 data_tx)
{
  u8 data_rx=0; //存放读取的数据
  u8 i;
  for(i=0;i<8;i++)
  {
    SPI_SCLK=0; //准备发送数据
    if(data_tx&0x80)SPI_MOSI=1;
    else SPI_MOSI=0;
    data_tx<<=1; //依次发送最高位
    SPI_SCLK=1;  //表示主机数据发送完成,表示从机发送完毕
    
    data_rx<<=1; //表示默认接收的是0
    if(SPI_MISO)data_rx|=0x01;
  }
  return data_rx;
}
 
#include "W25Q64.h"
 
/*
函数功能:W25Q64初始化
硬件连接:
MOSI--->PB15
MISO--->PB14
SCLK--->PB13
CS----->PB12
*/
void W25Q64_Init(void)
{
  /*1. 开时钟*/
  RCC->APB2ENR|=1<<3; //PB
  
  /*2. 配置GPIO口模式*/
  GPIOB->CRH&=0xFFF0FFFF;
  GPIOB->CRH|=0x00030000;
  
  W25Q64_CS=1; //未选中芯片
  SPI_Init();   //SPI初始化
}
 
 
/*
函数功能:读取芯片的ID号
*/
u16 W25Q64_ReadID(void)
{
  u16 id;
  /*1. 拉低片选*/
  W25Q64_CS=0;
 
  /*2. 发送读取ID的指令*/
  SPI_ReadWriteOneByte(0x90);
 
  /*3. 发送24位的地址-0*/
  SPI_ReadWriteOneByte(0);
  SPI_ReadWriteOneByte(0);
  SPI_ReadWriteOneByte(0);
 
  /*4. 读取芯片的ID*/
  id=SPI_ReadWriteOneByte(0xFF)<<8;
  id|=SPI_ReadWriteOneByte(0xFF);
 
  /*5. 拉高片选*/
  W25Q64_CS=1;
  return id;
}
 
/*
函数功能:检测W25Q64状态
*/
void W25Q64_CheckStat(void)
{
  u8 stat=1;
  while(stat&1<<0)
  {
    W25Q64_CS=0; //选中芯片
    SPI_ReadWriteOneByte(0x05);      //发送读状态寄存器1指令
    stat=SPI_ReadWriteOneByte(0xFF); //读取状态
    W25Q64_CS=1; //取消选中芯片
  }
}
 
 
/*
函数功能:页编程
说    明:一页最多写256个字节。 写数据之前,必须保证空间是0xFF
函数参数:
u32 addr:页编程起始地址
u8 *buff:写入的数据缓冲区
u16 len :写入的字节长度
*/
void W25Q64_PageWrite(u32 addr,u8 *buff,u16 len)
{
  u16 i;
  W25Q64_Enabled();             //写使能
  W25Q64_CS=0; //选中芯片
  SPI_ReadWriteOneByte(0x02); //页编程指令
  SPI_ReadWriteOneByte(addr>>16); //24~16地址
  SPI_ReadWriteOneByte(addr>>8);  //16~8地址
  SPI_ReadWriteOneByte(addr);     //8~0地址
 
  for(i=0;i<len;i++)
  {
    SPI_ReadWriteOneByte(buff[i]);     //8~0地址  
  }
  W25Q64_CS=1; //取消选中芯片
  W25Q64_CheckStat();  //检测芯片忙状态
}
 
 
/*
函数功能:连续读数据
函数参数:
u32 addr:读取数据的起始地址
u8 *buff:读取数据存放的缓冲区
u32 len :读取字节的长度
*/
void W25Q64_ReadByteData(u32 addr,u8 *buff,u32 len)
{
  u32 i;
  W25Q64_CS=0; //选中芯片
  SPI_ReadWriteOneByte(0x03);     //读数据指令
  SPI_ReadWriteOneByte(addr>>16); //24~16地址
  SPI_ReadWriteOneByte(addr>>8);  //16~8地址
  SPI_ReadWriteOneByte(addr);     //8~0地址
  for(i=0;i<len;i++)buff[i]=SPI_ReadWriteOneByte(0xFF);
  W25Q64_CS=1; //取消选中芯片
}
 
 
/*
函数功能:擦除一个扇区
函数参数:
        u32 addr:擦除扇区的地址范围
*/
void W25Q64_ClearSector(u32 addr)
{
  W25Q64_Enabled();             //写使能
  W25Q64_CS=0; //选中芯片
  SPI_ReadWriteOneByte(0x20);     //扇区擦除指令
  SPI_ReadWriteOneByte(addr>>16); //24~16地址
  SPI_ReadWriteOneByte(addr>>8);  //16~8地址
  SPI_ReadWriteOneByte(addr);     //8~0地址
  W25Q64_CS=1;        //取消选中芯片
  W25Q64_CheckStat();  //检测芯片忙状态
}
 
/*
函数功能:写使能
*/
void W25Q64_Enabled(void)
{
  W25Q64_CS=0; //选中芯片
  SPI_ReadWriteOneByte(0x06);     //写使能
  W25Q64_CS=1; //取消选中芯片
}
 
 
/*
函数功能:指定位置写入指定个数的数据,不考虑擦除问题
注意事项:W25Q64只能将1写为,不能将0写为1。
函数参数:
u32 addr---写入数据的起始地址
u8 *buff---写入的数据
u32 len---长度
*/
void W25Q64_WriteByteDataNoCheck(u32 addr,u8 *buff,u32 len)
{
  u32 page_remain=256-addr%256; //计算当前页还可以写下多少数据
  if(len<=page_remain) //如果当前写入的字节长度小于剩余的长度
  {
    page_remain=len;
  }
  while(1)
  {
    W25Q64_PageWrite(addr,buff,page_remain);
    if(page_remain==len)break; //表明数据已经写入完毕
    buff+=page_remain; //buff向后偏移地址
    addr+=page_remain; //起始地址向后偏移
    len-=page_remain;  //减去已经写入的字节数
    if(len>256)page_remain=256;  //如果大于一页,每次就直接写256字节
    else page_remain=len;
  }
}
 
 
/*
函数功能:指定位置写入指定个数的数据,考虑擦除问题,完善代码
函数参数:
u32 addr---写入数据的起始地址
u8 *buff---写入的数据
u32 len---长度
说明:擦除的最小单位扇区,4096字节
*/
static u8 W25Q64_READ_WRITE_CHECK_BUFF[4096];
void W25Q64_WriteByteData(u32 addr,u8 *buff,u32 len)
{
  u32 i;
  u32 sector_addr; //存放扇区的地址
  u32 sector_move; //扇区向后偏移的地址
  u32 sector_size; //扇区大小。(剩余的空间大小)
  u8 *p=W25Q64_READ_WRITE_CHECK_BUFF;//存放指针
  sector_addr=addr/4096; //传入的地址是处于第几个扇区
  sector_move=addr%4096; //计算传入的地址存于当前的扇区的偏移量位置
  sector_size=4096-sector_move; //得到当前扇区剩余的空间
 
  if(len<=sector_size)
  {
    sector_size=len; //判断第一种可能性、一次可以写完
  }
 
  while(1)
  {
    W25Q64_ReadByteData(addr,p,sector_size);   //读取剩余扇区里的数据
    for(i=0;i<sector_size;i++)
    {
      if(p[i]!=0xFF)break;
    }
    if(i!=sector_size)  //判断是否需要擦除
    {
      W25Q64_ClearSector(sector_addr*4096);
    }
    W25Q64_WriteByteDataNoCheck(addr,buff,sector_size);
    if(sector_size==len)break;
 
    addr+=sector_size; //向后偏移地址
    buff+=sector_size ;//向后偏移
    len-=sector_size;  //减去已经写入的数据
    sector_addr++;     //校验第下个扇区
    if(len>4096)       //表明还可以写一个扇区
    {
      sector_size=4096;//继续写一个扇区
    }
    else
    {
      sector_size=len; //剩余的空间可以写完
    }
  }
}