STM32(三十七)SPI读取W25Q128flash的厂商ID、设备ID以及读写数据(硬件SPI)

一、原理图分析

 

 

 

 

 

 由原理图可知w25Q128 CS片选引脚为PB14、MISO是PB4、MOSI是PB5.

二、程序编写

1、spi初始化以及读写函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include "spi.h"
 
void Spi_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;
    SPI_InitTypeDef  SPI_InitStruct;
     
    //使能端口 B 的硬件时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
     
    //使能SPI的硬件时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
     
    //PB3-PB5引脚连接到SPI1的硬件
    GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5;//PB3  PB4  PB5
    GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_AF;//复用模式
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;//推挽输出
    GPIO_InitStruct.GPIO_Speed = GPIO_Fast_Speed;//速度 快速 25MHz
    GPIO_InitStruct.GPIO_PuPd  = GPIO_PuPd_UP;//上拉
    GPIO_Init(GPIOB,&GPIO_InitStruct); 
     
    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_InitStruct.GPIO_Pin   = GPIO_Pin_14;//PB14
    GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_OUT;//输出模式
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;//推挽输出
    GPIO_InitStruct.GPIO_Speed = GPIO_Fast_Speed;//速度 快速 25MHz
    GPIO_InitStruct.GPIO_PuPd  = GPIO_PuPd_UP;//上拉
    GPIO_Init(GPIOB,&GPIO_InitStruct);
     
    //PB14初始电平状态?  
    SPI_CS = 1;//片选引脚   低电平有效选择,高电平无效选择
     
    //配置SPI相关参数
   SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//双线全双工通信
   SPI_InitStruct.SPI_Mode = SPI_Mode_Master;//默认是主机角色,主动控制从机
   SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;//默认是8位数据传输,主要根据从机的设备进行配置 【看从机的数据手册的时序图】
   //模式3
   SPI_InitStruct.SPI_CPOL = SPI_CPOL_High;//SPI总线空闲的时候,时钟线为高电平  CPOL=1,【看从机的数据手册的时序图】
   SPI_InitStruct.SPI_CPHA = SPI_CPHA_2Edge;//CPHA = 1,,就是主机会对MOSI引脚进行电平采样在时钟的第二个条边沿【看从机的数据手册的时序图】
   SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;//片选引脚有软件代码控制【看从机的数据手册的时序图】
   SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;//SPI的硬件时钟=84MHz/4=21MHz {看从机的数据手册的芯片描述,一般在开头介绍}
  
   SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;//高位先出【看从机的数据手册的时序图】
   //SPI_InitStruct.SPI_CRCPolynomial = 7;//主要是用在两个M4芯片进行通信,最后添加CRC检验码
   SPI_Init(SPI1, &SPI_InitStruct);
 
    //使能SPI1硬件
   SPI_Cmd(SPI1, ENABLE);
 
}
 
/*
 * 功能:SPI 读写一个字节函数    ---》数据交换
 * 参数:发送一个字节数据
 * 返回值:返回读取的数据
*/
 
uint16_t spi_read_writeByte(uint8_t TXdata)
{
    while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);   //等待上一次的数据发完
    SPI_I2S_SendData(SPI1,TXdata);//发送数据
     
    while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);  //等上次数据接收完
    return SPI_I2S_ReceiveData(SPI1);//接收数据
}

 2、读写厂商ID和设备ID(09H)--模式3

 

 由上图可知厂商ID是0xEF,设备ID是0x17.

 该指令与Release from Power-Down/Device ID指令相似。该指令以/CS拉低开始,然后通过DI传输指令代码90H和24位的地址(全为00000H)。这之后WINBOND的ID(EFH)和芯片ID将在时钟的下降沿以高位在前的方式传出。关于W25Q128BV的芯片和制造商ID,在图29中列出。如果24位地址传输的是00001H,那么芯片ID将首先被传出,然后紧接着的是制造商ID。这两个是连续读出来的。该指令以/CS拉高结束。

  • CS拉低表示开始进行数据传输。
  • 第一个字节发送指令0x90,代表开始读取ID.
  • 第二个字节、第三个字节为dummy(任意值)、第四个字节为0x00
  • 第五、六个字节随便发两个字节数据,分别返回制造商ID和设备ID.
  • CS拉高表示结束。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
uint16_t w25qxx_read_id(void)
{
    uint16_t id = 0;
    //片选有效
    SPI_CS = 0;
     
    //发送0x90,读取厂商ID和设备ID
    spi_read_writeByte(0x90);
     
    //发送24位地址(3个字节)  前面两个字节可以任意,第三个字节必须是0x00
    spi_read_writeByte(0x00);
    spi_read_writeByte(0x00);
    spi_read_writeByte(0x00);//一定是0x00
     
 
    //随便发2个字节的数据
    id |= spi_read_writeByte(0xFF)<<8; //id:0xEF17  厂商ID:0xEF    
    id |= spi_read_writeByte(0xFF);    //设备ID:0x17
     
    //片选无效
    SPI_CS = 1;
     
    return id;
}

3、 读数据(03H) ---模式3

  读数据指令允许从存储器读一一个字 节和连续多个字节。该指令是以/CS拉低开始,然后通DI在时钟的上升沿来传输指令代码(03H)和24位地址。当芯片接受完地址位后,相应地址处的值将会,在时钟的下降沿,以高位在前低位在后的方式,在DO.上传输。如果连续的读多个字节的话,地址是自动加1的。这意味着可以一次读出整个芯片。该指令也是以/CS拉高来结束的。如果当BUSY=1时执行该指令,该指令将被忽略,并且对正在执行的其他指令不会有任何影响。读数据指令的时钟可以从D.C到最大的fR.

 

 读数据流程:

  • CS拉低开始
  • 第一个字节发送指令0x03,代表开始读取数据。
  • 发送一个24bit要读取的地址(三个字节)。
  • 数据读取。
  • CS拉高结束。

代码编写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
 * 功能:w25q128 读取一个字节函数    ---》数据交换
 * 参数:addr     ----->打算从 addr 这个地址开始读取数据
         pbuf     ----->你要读取的数据所在的缓冲区
         lenth    ----->你要读取的字节数
 * 返回值:返回读取的数据
*/
void w25qxx_read_data(uint32_t addr,uint8_t *pbuf,uint32_t lenth)
{
    uint8_t *p = pbuf;
    //片选有效
    SPI_CS = 0;
     
    //发送0x03,读取读取数据
    spi_read_writeByte(Read_Data);
     
    //接下来发一个你要读取的24位地址   0xyy123456
    spi_read_writeByte((addr>>16)&0xFF);//0x12   发送[23:16]
    spi_read_writeByte((addr>>8)&0xFF);//0x34    发送[15:8]
    spi_read_writeByte((addr>>0)&0xFF);//0x56    发送[7:0]
     
    while(lenth--)
    {
        *p++ = spi_read_writeByte(0xFF);//随意加的,你可以改成其它试试
    }
     
    //片选无效
    SPI_CS = 1;
}

4、擦除扇区(20H)

  扇区擦除可以擦除4K-byte存储空间(全为0XFF)。进行扇区擦写指令之前,必须进行写使能指令。该指令是以/CS拉低开始的,然后在DI.上传输指令代码20H和24位地址。时序图如图21。当最后字节的第8位进入芯片后,/CS必须拉高。如果/CS没有拉高,那么扇区擦写指令将不被执行。/CS拉高后,扇区擦写指令的内建时间为tSE。在扇区擦写指令执行期间,读状态寄存器指令仍然可以识别,以此来进行检查BUSY位。当扇区擦写指令执行期间,BUSY 位为了1。当执行完后,BUSY 为0,表明可以接受新的指令了。扇区擦写指令完成后WEL位自动清零。如果该指令要操作的任何--页已经被保护起来,那么该指令也将不执行。

 

 扇区擦除流程:

  • CS拉低开始。
  • 发送指令0x20,代表擦除扇区开始。
  • 发送一个要擦除的24bit地址
  • CS拉高结束。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
 * 功能:w25q128  写入一页(256Byte)函数    ---》数据交换
 * 参数:addr     ----->打算从 addr 这个地址开始擦除
          
 * 返回值:无
*/
 
void w25qxx_EraseSector(uint32_t addr)
{
    //片选有效
    SPI_CS = 0;
     
    //发送0x20,扇区擦除
    spi_read_writeByte(Sector_Erase);
     
    //接下来发一个你要读取的24位地址   0xyy123456
    spi_read_writeByte((addr>>16)&0xFF);//0x12   发送[23:16]
    spi_read_writeByte((addr>>8)&0xFF);//0x34    发送[15:8]
    spi_read_writeByte((addr>>0)&0xFF);//0x56    发送[7:0]
     
    //片选无效
    SPI_CS = 1;
}  

5、读状态寄存器1指令(05H)和读状态寄存器2指令(35H)

  读状态寄存器指令允许读8位状态寄存器位。这条指令是以/CS拉低开始,然后通过DI在时钟的上升沿传输指令代码05H(读寄存器1指令)或者是35H(读寄存器2指令),然后状态寄存器的相应位通过DO在时钟的下降沿从高位到低位依次传出。最后以/CS拉高结束。读状态寄存指令可以任何时间使用,在擦写,写状态寄存器指令周期中依然可以。这样就可以随时检查BUSY位,检查相应的指令周期有没有结束,芯片是不是可以接受新的指令。状态寄存器可以连续的读出来,如图7。.

 

 

 读状态寄存器流程:

  • CS拉低。
  • 发送指令0x05,表示读取状态寄存器1.
  • 接收数据。
  • CS拉高。

代码编写:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
*
 * 功能:w25q128  读取状态寄存器1
 * 参数:无          
 * 返回值:状态寄存的值
*/
uint8_t w25qxx_read_SR1(void)
{
    uint16_t status = 0;
    //片选有效
    SPI_CS = 0;
     
    //发送0x05,读取状态寄存器1的值   发送0x35,读取状态寄存器2的值
    spi_read_writeByte(Read_SR1);
     
    //接收数据  就是状态寄存器1+状态寄存器2
    status = spi_read_writeByte(0xFF);
     
    //片选无效
    SPI_CS = 1;
     
    return  status;
}

 

6、写使能指令(06H)

  写使能指可以设置状态寄存器中的WEL位置1。在页写,QUAD页写,扇区擦除,块擦除,片擦除,写状态寄存器,擦写安全寄存器指令之前,必须先将WEL位置1。写使能指令是以/CS拉低开始的,将06H通过DI在时钟的上升沿锁存,然后/CS拉高来结束指令。

 

 写使能流程:

  • CS拉低
  • 发送写使能指令0x06.
  • CS拉高。

代码编写:

1
2
3
4
5
6
7
8
9
10
11
void w25qxx_wirte_enable(void)
{
    //片选有效
    SPI_CS = 0;
     
    //发送0x06,写使能
    spi_read_writeByte(Write_Enable);
     
    //片选无效
    SPI_CS = 1;
}  

 

7、判断擦除是否完成

 

 判断状态寄存器1的S0为是否为0,值为0则擦除完成。

1
2
3
4
void w25qxx_wait_busy(void)
{
    while((w25qxx_read_SR1()&(0x01<<0)));//当busy为0,即擦除完毕  当busy为1,即擦除还在继续
}  

8、页写指令(02H)

  页编程指令允许1到256字节写入存储器的某- -页,这一页必须是被擦除过的(也就是只能写.0,不能写1,擦除时是全写为1)。在页编程指令之前,必须先写入写使能指令。页编程指令是以/CS拉低开始,然后在DI上传输指令代码02H,再接着传输24位的地址,接着是至少-一个字节的数据。/CS管脚必须一直保持低。页编程指令的时序图如图19。如果一-次写-整页数据(256 字节),最后的地址字节应该全为0。如果最后8字节地址不为0,但是要写入的数据长度超过页剩下的长度,那么芯片会回到当前页的开始地址写。写入少于256字节的的数据,对页内的其他数据没有任何影响。对于这种情况的惟一要求是,时钟数不能超过剩下页的长度。如果一-次写入多于是256字节的数据,那么在页内会回头写,先前写的数据可能已经被覆盖。作为擦写指令,当最后字节的第8位进入芯片后,/CS必须拉高。如果/CS没有拉高, .那么页写指令将不被执行。/CS拉高后,页编程指令的内建时间为tpp。在页写指令执行期间,读状态寄存器指令仍然可以识别,以此来进行检查BUSY位。当页写指令执行期间,BUSY 位为了1。当执行完后,BUSY 为0,表明可以接受新的指令了。页写指令完成后WEL位自动清零。如果该指令要操作的页已经被保护起来,那么该指令也将不执行。

 

 页写流程:

  • 写使能。
  • 擦除扇区(擦除也是个写操作,写0)
  • 判断扇区是否擦除完毕。
  • 擦除完毕后写使能。
  • CS拉低,片选有效。
  • 发送页写指令0x02,代表页写开始
  • 发送一个要写入的24bit地址。
  • 开始写数据,写入一页数据(354byte)
  • CS拉高,片选无效。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*
 * 功能:w25q128  写入一页(256Byte)函数    ---》数据交换
 * 参数:addr     ----->打算从 addr 这个地址开始写入数据
         pbuf     ----->你要写入的数据所在的缓冲区
         lenth    ----->你要写入的字节数
 * 返回值:返回读取的数据
*/
void w25qxx_write_page(uint32_t addr,uint8_t *pbuf,uint32_t lenth)
{
    uint8_t *p = pbuf;
     
    //擦除之前必须进行写使能
    w25qxx_wirte_enable();
     
    //擦除扇区
    //w25qxx_EraseSector(0x000000);
    w25qxx_EraseSector(addr/4096*4096);
     
    //判忙
    w25qxx_wait_busy();
     
    //写入必须进行写使能
    w25qxx_wirte_enable();
     
    //开始写入数据
    //片选有效
    SPI_CS = 0;
     
    //发送0x02,写入数据
    spi_read_writeByte(Page_Program);
     
    //接下来发一个你要写入的24位地址   0xyy123456
    spi_read_writeByte((addr>>16)&0xFF);//0x12   发送[23:16]
    spi_read_writeByte((addr>>8)&0xFF);//0x34    发送[15:8]
    spi_read_writeByte((addr>>0)&0xFF);//0x56    发送[7:0]
     
    while(lenth--)
    {
        spi_read_writeByte(*p++);
    }
     
    //片选无效
    SPI_CS = 1;
}  

  

三、主函数测试

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uint8_t i;
    uint8_t wbuf[8]={'h','e','l','l','o','b','b','a'};
    uint8_t rbuf[8]={0};
    uint16_t id;
id = w25qxx_read_id();
    printf("id=0x%X\r\n",id);
     
    w25qxx_write_page(10086,wbuf,8);
    delay_ms(50);
    w25qxx_read_data(10086,rbuf,8);
    printf("addr10086 read 8bit data:");
    for(i=0;i<8;i++)
    {
        printf("%c ",rbuf[i]);
    }
    printf("\r\n");

 

  

  

 


 

posted @   轻轻的吻  阅读(14453)  评论(1编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示