嵌入式Linux——SPI总线(2):2440裸机GPIO模拟SPI控制FLASH

2019-07-13 09:20发布

简介:     本文主要讲解使用2440裸机的GPIO模拟SPI来控制flash进行数据的存储和读取。 所用开发板:JZ2440 V3 FLASH:W25Q16DV 声明:     本文主要还是学习韦东山老师视频后所做的学习总结。同时由于我在上面一篇文章中已经介绍了:2440裸机GPIO模拟SPI驱动OLED,而本文同样使用GPIO模拟SPI,所以可能与上篇文章有相似的部分。但为了让没有看过上篇文章的人看懂本篇文章。所以我还是会用上篇文章中的知识。这样可能会显得有些啰嗦,请大家原谅,谢谢。 flash介绍:     介绍知识点之前我还是想说,既然这篇文章中同样是使用GPIO模拟SPI,那么这篇文章的重点就落在了看懂flash的芯片手册上,因为我们不用去关心开发板上SPI控制器是如何实现的,我们只要用GPIO去模拟SPI就可以了。因此我们首先先了解我们所使用的flash芯片。(由于芯片的功能很多,而我们要做的是学习SPI,所以我们只是实现了他们最简单的功能,因此我只是将我用到的内容翻译了下来,这里我简称该芯片为25Q)。         25Q(16M-bit)由8192个可编程页组成,每个可编程页为256字节。因此一次可以编写256字节。同时16页(4KB),128页(32KB)或者256页(64KB)可以分为一组进行擦除。25Q支持标准的SPI接口,同时其时钟频率可以高达104MHz。     而25Q的引脚图为:     而25Q对应的引脚描述为: 引脚号 引脚名 I/O 功能 1 /CS I 片选输入 2 DO(IO1) I/O 数据输出 3 /WP(IO2) I/O 写保护输入 4 GND   地 5 DI(IO0) I/O 数据输入 6 CLK I 时钟输入 7 /HOLD(IO3) I/O HOLD输入 8 VCC   电源正 写保护引脚:低电平有效,硬件防止状态寄存器被更改。 HOLD引脚:当该引脚被选中,设备被暂停。当多设备共享SPI信号时,该引脚是有用的。     而我们看老师模块上flash的电路图     从上图可以看出写保护引脚和HOLD引脚都接到电源正极。所以我们的模块并没有使用到这两个引脚的功能。     下面我们继续介绍对flash的操作,我们知道要写flash就要先擦除flash原有的扇区,而擦除扇区之前是要去除对于扇区的保护的。而具体的去保护操作为: 1. 写使能 2. 写状态寄存器     我们先介绍写使能,flash上电自动进入写失能状态,即状态寄存器写使能锁存(WEL)位为0。而在进行页编程,扇区擦除,块擦除或者写状态寄存器指令前要发送写使能指令。而当完成编程,擦除或者写指令操作后写使能锁存自动清零,即进入写失能状态。     我们上面说了去保护要操作状态寄存器,我们现在看看状态寄存器中都有那些位:     上面就是两个状态寄存器了,而是否可以对其进行操作要设置寄存器中的两位:状态寄存器保护位SRP1,SRP0。而具体的设置看下图:     而具体的关于状态寄存器的操作或者说关于flash的操作就要通过指令进行操作了。 代码分析:     上面的介绍我们对SPI_FLASH的操作步骤有了基本的了解,我们下面通过分析代码来看具体如何对这个SPI_FLASH进行操作。     首先我们还是先来对2440中的GPIO进行初始化static void spi_gpio_init(void) { /* GPG2 FLASH_CS0 output * GPG4 OLED_D/C output * GPG5 SPI_MISO input * GPG6 SPI_MOSI output * GPG7 SPI_CLK output */ GPGCON &= ~((0x3<<(2*2)) | (0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)) | (0x3<<(7*2))); GPGCON |= ((0x1<<(2*2)) | (0x1<<(4*2)) | (0x1<<(6*2)) | (0x1<<(7*2))); GPGDAT |= (0x1<<2); }     初始化完端口我们就可以按着25Q的芯片手册来对flash进行操作了,我们从上面的知识知道,flash在擦除或者写寄存器之前都要对flash进行写使能操作。下面我们结合25Q的数据手册先完成写使能。 写使能(06h):     写使能指令设置状态寄存器中的写使能锁存位(WEL)为1 。在编程,擦除或者写寄存器之前都要写使能。当片选/CS为低电平时将指令码06在时钟上升沿时以最高有效位方式传入数据输入引脚,数据传输完成然后再拉高/CS,这时就将写使能指令写入芯片。     写使能对应的时序图为:      而相对于写使能的就是写失能了,我们看写失能做了什么。 写失能(04h):      写失能指令重设状态寄存器中的写使能锁存位(WEL)为0。当片选/CS为低电平时将指令码04在时钟上升沿时以最高有效位方式传入数据输入引脚,数据传输完成然后再拉高/CS,这时就将写失能指令写入芯片。注意当上电或完成编程擦除以及写寄存器时,WEL位会自动清零。       由于上面两个指令除了指令码不一样之外其他的设置都一样,所以我们将这两个指令放到了一个函数中。而这个函数的实现为:   static void spi_flash_write_enable(int enable) { if(enable){ spi_set_CS(0); flash_write_cmd(0x06); spi_set_CS(1); }else{ spi_set_CS(0); flash_write_cmd(0x04); spi_set_CS(1); } }     又由于在flash中写指令的格式都是一样的,即在时钟上升沿以最高有效位方式传输8位命令或者数据,所以我们将他提出用一个函数表示,所以写命令函数为: static void flash_write_cmd(unsigned char cmd) { spi_send_byte(cmd); }     同时由于我们在上一篇文章中已经写了一个类似的函数:spi_send_byte,所以在这里调用他,而spi_send_byte函数为: void spi_send_byte(unsigned char val) { int i; for(i=0;i<8;i++){ spi_set_clk(0); spi_set_DO(val & 0x80); spi_set_clk(1); val <<= 1; } }     这个函数与时序图的对照就更贴近一些。     我们在前面介绍了,要去保护,第一是写使能,而第二件事就是写状态寄存器。因此下面我们介绍操作状态寄存器,而操作状态寄存器之前我们先要将状态寄存器中的值读出来,然后再按位操作设置我们要设置的位,最后将这设置后的寄存器值写入状态寄存器。这样才能算是对状态寄存器的写操作。我们先来看一下25Q中读状态寄存器的操作。 读状态寄存器1(05h)和读状态寄存器2(35h):      通过读状态寄存器指令可以读取8位的状态寄存器的值。当片选/CS为低电平时将读状态寄存器1的指令码05或者将读状态寄存器2的指令码35在时钟上升沿时以最高有效位方式传入数据输入引脚。然后状态寄存器的值将同样按照最高有效位的方式在数据输出引脚等待在每一个时钟下降沿的时候被读取。      读状态寄存器的指令可以在任何时候都可以用到,甚至是在编程,擦除或者写寄存器的过程中。它允许BUSY态可以被检查到来显示是否完成上面的操作,并显示设备是否可以接收新的指令。状态寄存器可以连续的读取,直到当/CS设置为高时停止。     而我们对应这个时序图将函数写出,所以读寄存器操作为: static unsigned char spi_flash_read_status_Reg1(void) { unsigned char val = 0; spi_set_CS(0); flash_write_cmd(0x05); val = flash_read_byte(); spi_set_CS(1); return val; } static unsigned char spi_flash_read_status_Reg2(void) { unsigned char val = 0; spi_set_CS(0); flash_write_cmd(0x35); val = flash_read_byte(); spi_set_CS(1); return val; }     同时又由于在flash中读数据和读寄存器的操作是相同的,即在时钟上升沿以最高有效位方式读取8位数据这里需要说明的是:上面在芯片手册上说的是在时钟的下降沿读取数据,但是我们看时序图中红框,发现当指令传输完成时,在数据输出引脚上就有了数据(这里的数据就是状态寄存器的值),而在时钟的上升沿时正好可以读取数据,因此我们将程序改为了上升沿,并实验可以使用)。因此我们看数据接收函数static unsigned char flash_read_byte(void) { int i; unsigned char val = 0; for(i=0;i<8;i++){ val <<= 1; spi_set_clk(0); val |= spi_get_DI(); spi_set_clk(1); } return val; }     上面函数需要说明第一点是val <<= 1;的位置。我刚开始的时候将个语句放到了for语句的末尾,结果发现我的实验结果是错的,而当我将这个语句放到for语句的开头时,发现结果正确了。然后仔细分析程序我才知道,左移语句只要执行7次就可以了。而当放到for语句末尾时就会执行8次。得出的结果是正确答案的两倍。     好了,我们下一步就要对状态寄存器进行操作来去保护了。我们先去状态寄存器的保护,通过芯片手册知道,要去状态寄存器的保护需要操作状态寄存器的SRP1,SRP0位,使这两位都为清零。如下图:     而具体的去状态寄存器保护的 程序为: static void spi_flash_clear_protect_for_status_Reg(void) { unsigned char reg1,reg2; reg1 = spi_flash_read_status_Reg1(); reg2 = spi_flash_read_status_Reg2(); reg1 &= ~(1<<7); /* 清零SRP0 */ reg2 &= ~(1<<0); /* 清零SRP1 */ spi_flash_write_status_Reg(reg1,reg2); spi_flash_wait_when_busy(); }     而上面函数中写状态寄存器函数spi_flash_write_status_Reg(reg1,reg2)的原函数为: static void spi_flash_write_status_Reg(unsigned char reg1,unsigned char reg2) { spi_flash_write_enable(1); spi_set_CS(0); flash_write_cmd(0x01); flash_write_cmd(reg1); flash_write_cmd(reg2); spi_set_CS(1); spi_flash_wait_when_busy(); }     而关于写状态寄存器,25Q芯片手册这样描述: 写状态寄存器(01h):     写状态寄存器允许向状态寄存器中写入值。但只有非易失的状态寄存器位可以被写入值,如:SRP0,SEC,TB,BP2,BP1,BP0(状态寄存器1的bit7~2)以及CMP,LB3, LB2, LB1,QE,SRP1(状态寄存器2的bit14~8)。而剩余的位是不可被写入的。LB[3:1]是非易失的一次编程,一旦他们被设为1,他们将不会被清零。      而在写状态寄存器之前,写使能(06h)指令要先执行来使设备接收写写状态寄存器指令。一旦写使能,当片选/CS为低电平时将指令码01在时钟上升沿时以最高有效位方式传入数据输入引脚,然后将寄存器的值写入,最后拉高/CS,这时就将写状态寄存器指令写入芯片。      为了完成写状态寄存器指令,在写完8位或16位指令后一定要拉高/CS。如果没有拉高,那么这个指令将不被执行。如果/CS在第8个时钟后被拉高,CMP位和QE位将自动清零。     而他对应的时序图为:     状态寄存器去保护完成后我们就要去内存的保护,而在本程序中,我们要去除的是整个芯片的保护,对应的芯片手册的设置为:     我们的设置如上图红 {MOD}方框中所示,设置去除整个内存的保护,也就是设置CMP为0,BP2~0 都为0。而对应的函数为: static void spi_flash_clear_protect_for_data(void) { unsigned char reg1,reg2; reg1 = spi_flash_read_status_Reg1(); reg2 = spi_flash_read_status_Reg2(); reg1 &= ~(7<<2); /* BP2~0都为0 */ reg2 &= ~(1<<6); /* cmp为0 */ spi_flash_write_status_Reg(reg1,reg2); spi_flash_wait_when_busy(); }     在上面几个函数中都调用了一个函数spi_flash_wait_when_busy,我们现在介绍这个函数,我们看他的函数: static void spi_flash_wait_when_busy(void) { while(spi_flash_read_status_Reg1() & 1); }     从上面程序可以看出,其实这个函数就是读取状态寄存器1中的第0位BUSY位,来确定flash是否处在忙的状态。这里我们又要介绍一下BUSY状态了。我们看芯片手册: BUSY状态:      BUSY位在状态寄存器中是只读位,当设备执行编程,擦除,写寄存器操作时,该位被设置为1 。在这时,设备将忽略除读状态寄存器和擦除/编程悬挂指令外所有的指令。而当编程,擦除,写寄存器完成后该位将清零,表示该设备准备就绪,可以操作后面的指令。     而我们上面的程序就是不断的读取BUSY位是否为1,如果为1则一直在这里等待。     我们去完保护现在就可以对flash进行擦除了,而芯片手册中对flash的擦除有好几种,我们这里选择擦除面积最小的扇区擦除进行介绍,我们先看芯片手册的描述。 扇区擦除(20h):     扇区擦除指令用于擦除指定扇区中的所有内存。而在扇区擦除之前,写使能(06h)指令要先执行来使设备接收扇区擦除指令。一旦写使能,当片选/CS为低电平时将指令码20在时钟上升沿时以最高有效位方式传入数据输入引脚,然后再以最高有效位方式传入24位的地址(A23-A0)。完成地址的传输后,拉高/CS,这时就完成了扇区擦除。      当最后一个字节的第8位传输完成时,/CS一定要拉高。如果没有拉高,该指令将不会被执行。如果要擦除的地址被块保护位保护,那么该擦除指令将不会完成。     而他对应的程序为: /* erase 4K */ void spi_flash_erase_sector(unsigned int addr) { spi_flash_write_enable(1); spi_set_CS(0); flash_write_cmd(0x20); flash_write_addr(addr); spi_set_CS(1); spi_flash_wait_when_busy(); }     从上面时序图上看,我们传输完擦除指令后接着就要传输24位地址值,所以我们将他们封装了一个函数: static void flash_write_addr(unsigned int addr) { spi_send_byte((addr>>16)&0xff); spi_send_byte((addr>>8)&0xff); spi_send_byte(addr&0xff); }     从上面看我们先传输的是最高有效位。     擦除完我们就可以在刚擦除的区域进行写操作了,具体的操作我们看一下芯片手册。 页编程(02h):     页编程指令允许1~256个字节的数据写入到先前擦除的内存地址中。而在页编程之前,写使能(06h)指令要先执行来使设备接收页编程指令。一旦写使能,当片选/CS为低电平时将指令码02在时钟上升沿时以最高有效位方式传入数据输入引脚,然后再以最高有效位方式传入24位的地址(A23-A0)。完成地址的传输后,再将要写入内存的数据传入数据输入引脚,最后拉高/CS,这时就完成了页编程。     如果要完成256字节的页编程,最后的地址字节(最低有效地址位)应该设置为0。如果该位不是0,时钟数将会超出剩余的页长度,从而导致地址将回到页开始位置。     像写和擦除指令一样,当最后一个字节的第8位传输完成时,/CS一定要拉高。如果没有拉高,该指令将不会被执行。如果要写入的地址被块保护位保护,那么该页编程指令将不会完成。     而与时序图对应的函数为: void spi_flash_program(unsigned int addr,unsigned char *buf,int len) { int i; spi_flash_write_enable(1); spi_set_CS(0); flash_write_cmd(0x02); flash_write_addr(addr); for(i=0;i     我们向内存中写完东西都是要读的,因此我们要完成读函数。 读数据(03h):      读数据指令允许从内存中读取一个或者更多的字节数据。当片选/CS为低电平时将指令码03在时钟上升沿时以最高有效位方式传入数据输入引脚,然后再以最高有效位方式传入24位的地址(A23-A0)。完成地址的传输后,指定地址中的值将以最高有效位方式移到数据输出端口,然后在每个时钟的下降沿将数据读出。而在完成一字节数据传输后,指定地址的值将自动增加来传输下一字节的数据,直到传输完。这就意味着只要时钟不停整个内存的数据可以在一次指令中全部读完。而当/CS拉高时,该指令结束。   而相应的程序为:   void spi_flash_read(unsigned int addr,unsigned char *buf,int len) { int i; spi_set_CS(0); flash_write_cmd(0x03); flash_write_addr(addr); for(i=0;i     讲到这里我们就讲完了这个SPI_FLASH的操作过程。     我已将代码传到:2440裸板GPIO模拟SPI控制FLASH,如果需要全部的文档可以到这里下载。