STM32
直播中

刘军

7年用户 1364经验值
私信 关注
[问答]

如何利用SPI串行外设接口去读取W25Q128的数据呢

SPI是什么?SPI有哪几种模式呢?

如何利用SPI串行外设接口去读取W25Q128的数据呢?

回帖(1)

刘俊

2021-12-16 10:08:00
一、简介

  SPI是串行外设接口(Serial Peripheral lnterface)的缩写。SPI是一种高速的、全双工、同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,如今越来越多的芯片集成了这种通信协议,如NRF24L01、VS1053、SD卡等。
  (1)速度:串口的通信一般也就是115200bps,但是SPI的通信速度可以达到10Mbps,接近快了一百倍,所以在配置的时候需要注意,一般不超过10Mbps。
  (2)同步:采用同步方式(Synchronous)传输数据,主设备会根据将要交换的数据来产生相应的时钟脉冲(Clock Pulse), 时钟脉冲组成了时钟信号(Clock Signal) , 时钟信号通过时钟极性 (CPOL) 和 时钟相位 (CPHA) 控制着两个 SPI 设备间何时数据交换以及何时对接收到的数据进行采样,来保证数据在两个设备之间是同步传输。—简单地说,发送数据的时候,必须同时接收到数据,由时钟控制。
  (3)全双工:发送数据的时候能够接收数据,接收数据的时候能够发送数据,即可以同时双向通信。
  二、 spi四种模式

  SPI的相位(CPHA)和极性(CPOL)都可以为0或1,对应的4种组合构成了SPI的4种模式
Mode 0: CPOL=0, CPHA=0
Mode 1 :CPOL=0, CPHA=1
Mode 2 :CPOL=1, CPHA=0
Mode 3 :CPOL=1, CPHA=1
  
   时钟极性CPOL: 即SPI空闲时,时钟信号SCLK的电平(1:空闲时高电平; 0:空闲时低电平)
时钟相位CPHA:即SPI在SCLK第几个边沿开始采样(0:第一个边沿开始; 1:第二个边沿开始)
极性:polarity
相位:phase
常用的是mode 0 和mode 3,这两种模式的相同的地方是都在时钟上升沿采样传输数据,区别这两种方式的简单方法就是看空闲时,时钟的电平状态,低电平为mode 0 ,高电平为mode 3。
  
  三、spi四线

  SPI FLASH 一般用于存储LCD显示屏所要显示的图片,视频数据。
四根线:SDI(数据输入)、SDO(数据输出)、SCLK(时钟)、CS(片选)。
或者说是MOSI、MISO、SCLK、CS四根,CS和SS是一样的,只是表示方法不同。
  SCLK:串行时钟线,用于数据的同步。
MOSI:主机输出数据,从机接收数据。
MISO:主机接收数据,从机输出数据。
SS/CS:控制从机是否工作,往往是低电平有效,低电平选中从设备。
  
  

  

就比如STM32和LCD屏,使用SPI进行通信,单片机就是主机,LCD就是从机,单片机主机发送数据给从机LCD。

  

  

其中的SCLK、MOSI、MISO三根线是可以复用连接的,唯独片选信号线CS是要连接到器件N,这也代表了多个从设备接在一起 的时候,被片选的设备才可以进行工作,只能多选一,当其中一个设备被选中CS引脚为低电平的时候,其他引脚一定要设置为高电平,避免传输数据的时候发生紊乱。
  四、编程

  SPI读取W25Q128
  (1)读取设备ID

  1.根据设备数据手册读时序图,根据固件库参考手册编写函数
(1)GPIO初始化、SPI初始化
(2)SPI write初始化(主机发送数据的时候,从机会返回数据给单片机)
(3)读取ID,读取id的目的是验证自己的写数据函数是否正确。
  
   如果出现可上可下的梯形时序图,那么表示可以是任意数据,下图简单示例一下,学会看图。
根据你所采用的SPI模式进行对比,例如SPI模式0,传输数据开始:
(1)片选信号从高变低,选中从设备。
(2)空闲时时钟的电平状态为低电平,时钟第一个边沿触发,从下图读出的数据就是0000 0100,即0x04,一字节数据发送完毕。
(3)片选信号从高变低,不选中从设备。
  
  
  

  

  读从设备的ID,Read Manufacturer / Device lD (90h),读取厂商的ID,90h是命令,具体得看从器件的数据手册,一般你Ctrl+F搜索ID就可以找得到,通信开始拉低片选信号,结束时候拉高。

  

  

大概意思:
  
   读取设备lD指令,该指令是通过驱动CS引脚拉低,并移动指令90h后跟一个0x00的24位地址来启动的。最高有效位(MSB)优先。
  
  初始化、发送字节数据、读取ID的部分代码

static GPIO_InitTypeDef          GPIO_InitStructure;
static SPI_InitTypeDef          SPI_InitStructure;
#define W25QXX_SS                        PBout(14)


void w25qxx_init(void)
{
        /*!< Enable the SPI clock,使能SPI1硬件时钟 */
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);


        /*!< Enable GPIO clocks,使能GPIOB硬件时钟 */
        RCC_AHB1PeriphClockCmd( RCC_AHB1Periph_GPIOB, ENABLE);
       
        //SPI1端口配置 PB3 PB4 PB5
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5;        
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;        //复用功能                                                                       
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度50MHz                                                                       
        GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽复用输出                                                                                
        GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉                                                                       
        GPIO_Init(GPIOB,&GPIO_InitStructure);        
       
        /*!< Connect SPI1 pins to AF3 AF4 AF5 */  
        GPIO_PinAFConfig(GPIOB, GPIO_PinSource3, GPIO_AF_SPI1);
        GPIO_PinAFConfig(GPIOB, GPIO_PinSource4, GPIO_AF_SPI1);
        GPIO_PinAFConfig(GPIOB, GPIO_PinSource5, GPIO_AF_SPI1);


        //初始化片选引脚 PB14
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;                                
        //输出功能                                               
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
        //速度50MHz                                                               
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;               
        //推挽复用输出                                               
        GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;                
        //上拉                                                       
        GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;                                                                        
        GPIO_Init(GPIOB,&GPIO_InitStructure);        
       
        //由于M4芯片还没有真正配置好,先不让外部SPI设备工作
        W25QXX_SS = 1;
       
        /*!< SPI configuration ,SPI的配置*/
        //设置SPI为双线双向全双工通信
        SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;       
        //配置M4工作在主机模式                       
        SPI_InitStructure.SPI_Mode = SPI_Mode_Master;               
        //SPI的发送和接收都是8位数据位                                                       
        SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;                       
        //串口时钟线(SCLK)空闲的时候为高电平,这里电平的设置要根据通信的外围设备有关系的                                               
        SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;               
        //串行时钟的第二跳变沿进行数据采样,即采用了模式3                                                               
        SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;       
        //很多时候基于多设备通信,片选引脚都设置为软件控制                                                               
        SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;                                                                               
        SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;                                //SPI通信时钟 = 84MHz/16=5.25MHz
        //最高有效位优先,根据通信的外围设备有关系的
        SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;                                                               
        SPI_Init(SPI1, &SPI_InitStructure);


        /*!< Enable the sFLASH_SPI  ,使能SPI1硬件*/
        SPI_Cmd(SPI1, ENABLE);
       
}




//发送一个字节数据
uint8_t SPI1_SendByte(uint8_t byte)
{
        /*!< Loop while DR register in not emplty */
        while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);


        /*!< Send byte through the SPI1 peripheral */
        SPI_I2S_SendData(SPI1, byte);


        /*!< Wait to receive a byte */
        while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);


        /*!< Return the byte read from the SPI bus */
        return SPI_I2S_ReceiveData(SPI1);
}


//读取厂家ID数据
uint16_t w25qxx_read_id(void)
{
        uint16_t id=0;
       
        //片选引脚为低电平
        W25QXX_SS = 0;
       
        //发送0x90指令
        SPI1_SendByte(0x90);
       
        //发送24bit地址,全都是0
        SPI1_SendByte(0x00);       
        SPI1_SendByte(0x00);
        SPI1_SendByte(0x00);


        //读取厂商ID,填写任意参数,十六位数据,先将获取的放在高八位
        id = SPI1_SendByte(0xFF)<<8;
       
        //读取设备ID,填写任意参数,读取第八位数据
        id |= SPI1_SendByte(0xFF);


        //片选引脚为高电平
        W25QXX_SS = 1;
        //打印出ID
        return id;
}
(2)读取指定地址的数据

  
  

  

英文大概意思:
  

(1)读取数据指令允许从内存中顺序地读取一个或多个数据字节。
(2)该指令是通过驱动/CS引脚低,然后移动指令代码03h ,以及后面的24位地址到Dl引脚发的。
(3)代码和地址位被锁在时钟引脚的上升边缘上。地址被接收后,寻址存储器位置的数据字节将被移出在以最有效位(MSB)首先的CLK的下降边缘的DO引脚上。在每个字节的数据被移出后,这个地址会自动增加到下一个更高的地址,从而允许一个连续的数据流。这意味着,只要时钟继续,就可以用一条指令访问整个内存。本指令由driving /CS high完成。
(4)如果在erase、Program或Write cycle (BuSY=1)进程中发出读数据指令,该指令将被忽略,不会对当前周期产生任何影响。读取数据指令允许时钟速率从直流到最大fR(参阅交流电气特性)。
  
  
  

  

参数:addr是24位地址,*pbuf需要用户在外部定义数组来存储返回的数据,len是数组的长度


void w25qxx_read_data(uint32_t addr,uint8_t *pbuf,uint32_t len)
{
        W25QXX_SS=0;//拉低片选信号
        SPI1_SendByte(0x03);//发送读数据指令
        SPI1_SendByte((addr>>16)&0xFF);//发送读取数据的地址,24位,一次一个字节
        SPI1_SendByte((addr>>8)&0xFF);//总共三次
        SPI1_SendByte((addr>>0)&0xFF);
        while(len--)
        {
                *pbuf++ = SPI1_SendByte(0XFF);//返回读取的数据
        }
        W25QXX_SS=1;//拉高片选信号
}
调用:

int main(void)
{
        uint16_t id=0;
        uint8_t buf[64];
        uint32_t i=0;


        //系统定时器初始化,时钟源来自HCLK,且进行8分频,
        SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
               
        //串口1,波特率115200bps,开启接收中断
        USART1_Init(115200);
       
        //w25qxx初始化
        w25qxx_init();


        //读取ID
        id = w25qxx_read_id();
       
        printf("w25qxx id=%X rn",id);


        //读取数据,从0地址开始读取,连续读取64字节
        w25qxx_read_data(0,buf,64);
       
        printf("w25qxx read addr from 0 data is:rn");
       
       
        for(i=0; i<64; i++)
        {
                printf("%02X ",buf);
        }
       
        printf("rn");//打印完回车换行


        while(1)
}
(3)扇区擦除(这部分较为繁琐)

  
  

  

英文的大概意思:
  

   (1)扇区擦除都是以4KB为单位,擦除后,所有数据都变为0xFF。
(2)进行扇区擦除之前,先执行写使能指令,解除写保护。
(3)去检查是否已经擦除完成,必须得执行读取状态寄存器指令,如果读取到BUSY位为1,代表说还没有完成;如果BUSY为0,表示已经擦除完成。
(4)执行完擦除扇区指令后,开启写保护
  
  写使能:

  


//解除写保护
void w25qxx_write_enable(void)
{
        //片选引脚为低电平
        W25QXX_SS = 0;
       
        //发送0x06指令
        SPI1_SendByte(0x06);       
       
        //片选引脚为高电平
        W25QXX_SS = 1;


}
写失能

  

  

//开启写保护
void w25qxx_write_disable(void)
{
        //片选引脚为低电平
        W25QXX_SS = 0;
       
        //发送0x04指令
        SPI1_SendByte(0x04);       
       
        //片选引脚为高电平
        W25QXX_SS = 1;


}
寄存器

  

  

//读状态寄存器1
uint8_t w25qxx_read_status1(void)
{
        uint8_t status;
       
        //片选引脚为低电平
        W25QXX_SS = 0;
       
        //发送0x05指令
        SPI1_SendByte(0x05);


        //读取状态寄存器1的值
        status = SPI1_SendByte(0xFF);
       
        //片选引脚为高电平
        W25QXX_SS = 1;
       
        return status;


}
将上述结合起来,进行扇区擦除

void w25qxx_erase_sector(uint32_t addr)
{
        uint8_t status;
       
        //解除写保护
        w25qxx_write_enable();
       
        //延时1ms,让W25Q128能够识别到CS引脚电平的变化
        delay_ms(1);
       
        //片选引脚为低电平
        W25QXX_SS = 0;


        //发送0x20指令
        SPI1_SendByte(0x20);
       
        //发送24bit地址
        SPI1_SendByte((addr>>16)&0xFF);       
        SPI1_SendByte((addr>>8)&0xFF);
        SPI1_SendByte( addr&0xFF);       
       
        //片选引脚为高电平
        W25QXX_SS = 1;       
       
       
        while(1)
        {
                //读取状态寄存器1
                status= w25qxx_read_status1();
               
                //若BUSY位为0,则跳出循环
                if((status & 0x01) == 0)
                        break;
       
        }
       
        //开启写保护
        w25qxx_write_disable();
}
调用

int main(void)
{
        uint16_t id=0;
        uint8_t buf[64];
        uint32_t i=0;


        SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);


        //串口1,波特率115200bps,开启接收中断
        USART1_Init(115200);
       
        //w25qxx初始化
        w25qxx_init();


        //读取ID
        id = w25qxx_read_id();
       
        printf("w25qxx id=%X rn",id);
       
        //进行扇区擦除
        printf("w25qxx erase from 0 rn");
        w25qxx_erase_sector(0);
       
        //读取数据,从0地址开始读取,连续读取64字节
        w25qxx_read_data(0,buf,64);
       
        printf("w25qxx read addr from 0 data is:rn");
       
        for(i=0; i<64; i++)
        {
                printf("%02X ",buf);
       
        }
       
        printf("rn");
        while(1)
}
(4)页写

  

  


和扇区擦除过程有点像
void w25qxx_write_data(uint32_t addr,uint8_t *pbuf,uint32_t len)
{


        uint8_t status;
       
        //解除写保护
        w25qxx_write_enable();
       
        //延时1ms,让W25Q128能够识别到CS引脚电平的变化
        delay_ms(1);
       
        //片选引脚为低电平
        W25QXX_SS = 0;


        //发送0x02指令
        SPI1_SendByte(0x02);
       
        //发送24bit地址
        SPI1_SendByte((addr>>16)&0xFF);       
        SPI1_SendByte((addr>>8)&0xFF);
        SPI1_SendByte( addr&0xFF);       
       
        //写入数据
        while(len--)
                SPI1_SendByte(*pbuf++);
       
       
        //片选引脚为高电平
        W25QXX_SS = 1;       
       
       
        while(1)
        {
                //读取状态寄存器1
                status= w25qxx_read_status1();
               
                //若BUSY位为0,则跳出循环
                if((status & 0x01) == 0)
                        break;
       
        }
       
        //开启写保护
        w25qxx_write_disable();


}
调用的时候需要添加头文件,string.h,在主函数中进行调用。

uint8_t SPI1_SendByte(uint8_t byte)


(5)下一篇文章写使用普通的IO口来模拟SPI时序,替代发送一个字节数据函数:
  uint8_t SPI1_SendByte(uint8_t byte)



举报

更多回帖

×
20
完善资料,
赚取积分