我本仁慈,奈何苍天不许
分类: 嵌入式
2016-07-21 14:13:14
原文地址:nrf51822裸机教程-SPI(主) 作者:ifndef
关于SPI总线的介绍这里就不细说了,网上有很多介绍SPI总线时序的。
SPI总线的本质就是一个环形总线结构,在时钟驱动下两个双向移位寄存器进行数据交换。
所以SPI总线的特色就是:传输一字节数据的同时也会接收到一字节数据。支持SPI操作的芯片通常都会有一个CS引脚作为片选信号,所以总线上可以挂多个支持SPI操作的芯片,每次想对哪个操作就直接使能那个芯片就可以了。
对比于IIC总线,IIC总是在一个时钟周期内传输一个bit, IIC总线总是在每个时钟周期的高电平部分采样数据。
而SPI的特点是在时钟周期的跳变沿时采样数据或传输数据。一个时钟周期有两个跳变沿,一个上升沿一个下降沿。于是SPI总线的传输的特性就是在一个时钟周期的上升沿发送自己的一个bit数据,同时在该时钟周期的下降沿接收对端设备发送过来的一bit数据。或者是上升沿接收数据,下降沿发送数据。这都是可以设置的。
所以使用SPI总线时,当你要传输一个数据时总是同时会收到对端发来的一个数据,但是你没想要这个数据啊,很简单直接忽略就行了。
同理当你想读取对端的一个数据时,也随便发一个数据过去就行了。
51822发送数据是通过将数据写入TXD寄存器,因为每发送一个数据的同时会收到一个数据,该数据会放在RXD寄存器中以供读取使用。每当发送和接收到一个字节后,就会产生READY事件,以方便告知我们传输完成。所以SPI基本的一个字节的传输和接收操作通过如下函数实现
//SPI传输的基本函数,该函数传输一个字节给对方同时取得对方发送过来的字//节
uint8_t spi_transfer(uint8_t data){
uint8_t ret;
NRF_SPI0->TXD = data;
while ( NRF_SPI0->EVENTS_READY == 0 ); //等待传输结束
NRF_SPI0->EVENTS_READY = 0;
ret = NRF_SPI0->RXD;
return ret;
}
下面介绍对flash GD25Q128B进行数据的写入和读取操作。
上面提到过,SPI总线可以设置在一个 上升沿接收数据,下降沿发送数据,或者相反。 那么我们就需要看一下这个板子上的这个flash芯片对这个有没有要求,如果没要求我们就可以随便设置。查看GD25Q128B手册知道该芯片要求每个字节的最高bit先传输,同时要求在时钟的上升沿会获取总线上的数据。这里是针对这个flash芯片来说的,那么对于51822,就需要在上升沿把数据发送出去。
查看51822手册的SPI部分,找到CONFIG寄存器,根据上图的要求,则需要设置第一bit位0, 关于时钟极性(总线空闲时是保持低电平还是高电平)flash手册中没要求所以可以随便设置,所以我们只要满足flash芯片可以在时钟周期的上升沿能收到数据就行了。也就是51822在上升沿将数据发送出去。
所以 如果设置 CPOL为0,即空闲时保持高电平,那么当开始工作时,每个时钟的第一个沿就是下降沿,第二个沿是上升沿。根据下图说明这时候我们就要设置CPHA为0,即让51822在时钟周期第二个沿
将数据发送出去。
同理,如果CPOL位1,那么开始工作时第一个沿就是上升沿,第二个沿就是下降沿,于是就要设置51822在第一个沿将数据发送出去也就是设置CPHA为1.
//flash芯片要求在MSB先传输,并且在第一个上升沿采样。所以设置下。
//CPOL和CPHA都设置成1也是可以的,这里都设置成0了。
NRF_SPI0->CONFIG = (0<<0) //MSB first
| (0<< 1)
| (0 << 2)
基本传输要求满足了后,现在需要操作flash的读写,那么就需要看flash手册。了解关于读写命令的要求和命令码。查看手册说明,我们需要用到的命令有如下几个读/写命令。Flash操作都是以页为单位写(该芯片一页256字节),并且写之前需要先擦除,所以还需要用到sector erase(以4K为单位擦除)命令来擦数, block erase擦除的单位更大,这里用sector erase就行了。
读操作直接就可以完成,但是写操作发送后flash内被还需要时间去执行,以将数据写入flash中。所以我们还需要获取flash的执行状态,即当前是否在忙。所以还需要用到 读状态命令
另外,GD25Q128B这个flash芯片每次发送 页写或者 擦除命令之前需要先发送 Write Enable命令。
下面针对几个用到的命令看下 flash的手册说明
先看下读状态寄存器 命令,这里我们只需要读取状态寄存器的前7位就行了。然后判断 第0位的WIP是否为1,为1则表示flash芯片正在忙(正在写数据到flash中或者正在擦除),否则为不忙,则可以执行
写或擦除命令。
所以读状态寄存器命令我们用0x05就行了,因为我们只需要看第0位指示的flash芯片是否在忙,所以我们只要用0x05命令读取s0-s7位就行了。
通过读取状态寄存器,然后判断第0位,我们就可以知道设备是否在忙。
因为 基本的SPI传输和接收一个字节的函数我们已经实现了。所以等待设备不忙函数实现如下:
uint8_t
flash_status_read(void){
uint8_t ret;
enable_cs_pin();
spi_transfer(READ_STATUS);//发命令
ret = spi_transfer(0xff);//读取状态寄存器的s0-s7, 写数据随便填的。
disable_cs_pin();
return ret;
}
#define
WAIT_COUNT (200000UL)
uint8_t
wait_flash_ready(void){
uint8_t ret;
uint32_t i ;
for(i= 0; i
ret = flash_status_read();
if( (ret&0x01) == 0) break;
}
if ( i == WAIT_COUNT ) return FAIL;
return SUCCESS;
}
再看下write
enable命令,由下图可知该命令只需要 使能片选信号cs(拉低)后,发送一个 0x06命令码就可以了。之后才可以执行 页写和擦除命令
有了前面的基本spi的
写入和读取 一个字节的基本函数后,我们就可以实现如下的write enable函数
uint8_t
flash_write_enable(void){
//需要等待设备不忙
if(wait_flash_ready() != SUCCESS ) return
FAIL;
enable_cs_pin(); //命令发送前需要先 使能片选信号
spi_transfer(WRITE_ENABLE_CMD);
disable_cs_pin();
// 等待设备不忙,即该命令再flash芯片内部执行好了,然后才能去执行
// 写/擦除命令
if(wait_flash_ready() != SUCCESS ) return
FAIL;
return SUCCESS;
}
再看下读命令:
由下图可知 通过写命令码0x03 + 3字节的地址,然后就可以获取指定地址的数据了。获得得的数据量由自己定义。该flash芯片每次发送一字节数据后,内部会对读取地址自动加一,所以我们只要一直随便发一个数据给该flash芯片,flash芯片就会一直回复数据,并自动对读取地址加1.
同样由基本 传输/接收函数实现
读取函数如下:
uint8_t
flash_read(uint32_t addr, uint8_t *buff, uint32_t len){
uint8_t ret;
uint8_t i;
if( NULL == buff ) return FAIL;
wait_flash_ready(); //等待设备当前是不忙的
enable_cs_pin();
spi_transfer(READ_CMD); //先传命令
spi_transfer( (addr>>16)&0xff );//依次传输3字节地址,高字节先发送
spi_transfer( (addr>>8)&0xff );
spi_transfer( addr&0xff);
//开始获取读到的数据
for( i = 0; i < len; i++){
ret = spi_transfer(0xff); //这里主要是读数据,所以随便传输一//个数据给对方
buff[i] = ret;
}
disable_cs_pin();
return SUCCESS;
}
页写命令:该命令执行前需要先执行了 write enable命令。
由下面可知 通过写 命令码0x02+3字节地址+要写的数据,数据量应该小于等于256字节。不然只有最后的256字节才会被写入。
函数实现如下:
//板子上的flash芯片一页是256字节,如果写超过256字节,只有最后的256字节能被写到flash中
uint8_t
flash_page_write(uint32_t addr, uint8_t *data, uint32_t len){
uint8_t ret;
uint8_t i;
ret = flash_write_enable(); //先执行write enable
if(ret!= SUCCESS) return ret;
enable_cs_pin();
spi_transfer(PAGE_PROGRAM); //先传命令
spi_transfer( (addr>>16)&0xff ); //依次传输3字节地址,高字节先发送
spi_transfer( (addr>>8)&0xff );
spi_transfer( addr&0xff);
//开始传数据给flsh芯片
for( i = 0; i < len; i++){
spi_transfer(data[i]); //这里主要是写数据,所以忽略返回值。
}
disable_cs_pin();
return SUCCESS;
}
最后看一下 擦除命令:该命令执行前需要先执行 write enable命令。
由下图可知通过写命令码0x20+3字节地址就可以了。
所以实现 擦除函数如下:
uint8_t flash_sector_erase(uint32_t
addr){
uint8_t ret;
ret = flash_write_enable(); //先写 write enable命令
if ( ret != SUCCESS ) return ret;
enable_cs_pin();
spi_transfer(SECTOR_ERASE); //先传命令
spi_transfer( (addr>>16)&0xff );//依次传输3字节地址,高字节先发送
spi_transfer( (addr>>8)&0xff );
spi_transfer( addr&0xff);
disable_cs_pin();
return SUCCESS;
}
各个功能函数都有了就可以操作flash了。
下面将各个功能函数和 测试的main函数都贴出来。
为了可以通过打印来验证flash操作的正确性,可以直接用sdk下面的 uart的ble demo。虽然该demo是跑ble的。但是我们直接将main函数换掉就行了。用该例子的原因就是为了方便初学者可以使用Printf函数来打印信息验证flash操作的正确性。
直接打开sdk中的ble_app_uart例子,然后将下列代码复制到 main函数的上面,然后将Main函数替换成 最下面提供的那个main函数就行了。
点击(此处)折叠或打开