关于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;

     ); //等待传输结束

    NRF_SPI0->EVENTS_READY = ;
    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 = (<<)   //MSB first
                     | (<< )
                     | ( << ) 

基本传输要求满足了后,现在需要操作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 ;

    ; i<WAIT_COUNT; i++ ){
       ret = flash_status_read();
       ) 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>>)&0xff );//依次传输3字节地址,高字节先发送
    spi_transfer( (addr>>)&0xff );
    spi_transfer( addr&0xff);

    //开始获取读到的数据
    ; 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>>)&0xff );   //依次传输3字节地址,高字节先发送
    spi_transfer( (addr>>)&0xff );
    spi_transfer( addr&0xff);

    //开始传数据给flsh芯片
    ; 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>>)&0xff );//依次传输3字节地址,高字节先发送
    spi_transfer( (addr>>)&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函数就行了。

#define PIN_CS                28
#define PIN_MISO            29
#define PIN_MOSI            24
#define PIN_CLK                25

#define WRITE_ENABLE_CMD    0x06
#define READ_CMD            0x03
#define PAGE_PROGRAM        0x02
#define SECTOR_ERASE        0x20
#define READ_STATUS            0x05 //我们读状态寄存器只是为了判断flash当前是不是正在忙,所以只需判断s0位,所以用0x05命令读出s0-s7就可以了

#define SUCCESS                0x01
#define FAIL                0x00
void init_spi_master(void){

    //按手册要求设置
    nrf_gpio_cfg_input(PIN_MISO, NRF_GPIO_PIN_NOPULL);
    nrf_gpio_cfg_output(PIN_MOSI);
    nrf_gpio_cfg_output(PIN_CLK);
    nrf_gpio_cfg_output(PIN_CS);
    nrf_gpio_pin_set(PIN_CS);

    //flash芯片要求在MSB先传输,并且在第一个上升沿采样。所以设置下。 CPOL和CPHA都设置成1也是可以的,这里都设置成0了。
    NRF_SPI0->CONFIG = (<<)    //MSB first
                        | (<< )
                        | ( << );
    NRF_SPI0->FREQUENCY = 0x10000000;
    NRF_SPI0->PSELSCK = PIN_CLK;
    NRF_SPI0->PSELMOSI = PIN_MOSI;
    NRF_SPI0->PSELMISO = PIN_MISO;

    NRF_SPI0->ENABLE = ;

}

//SPI传输的基本函数,该函数传输一个字节给对方同时取得对方发送过来的字节
uint8_t spi_transfer(uint8_t data){
    uint8_t ret;
    NRF_SPI0->TXD = data;
     );    //等待传输结束
    NRF_SPI0->EVENTS_READY = ;

    ret = NRF_SPI0->RXD;

    return ret;
}
void enable_cs_pin(void){
    nrf_gpio_pin_clear(PIN_CS);
}

void disable_cs_pin(void){
    nrf_gpio_pin_set(PIN_CS);
}

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 ;
    ; i<WAIT_COUNT; i++ ){
        ret = flash_status_read();
        ) break;
    }
    if ( i == WAIT_COUNT ) return FAIL;

    return SUCCESS;

}

uint8_t flash_write_enable(void){

    if(wait_flash_ready() != SUCCESS ) return FAIL;

    enable_cs_pin();
    spi_transfer(WRITE_ENABLE_CMD);
    disable_cs_pin();
    if(wait_flash_ready() != SUCCESS ) return FAIL;

    return SUCCESS;
}

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>>)&0xff );    //依次传输3字节地址,高字节先发送
    spi_transfer( (addr>>)&0xff );
    spi_transfer( addr&0xff);
    //开始获取读到的数据
    ; i < len; i++){
        ret = spi_transfer(0xff);    //这里主要是读数据,所以随便传输一个数据给对方
        buff[i] = ret;
    }
    disable_cs_pin();    

    return SUCCESS;
}

//板子上的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();
    if( SUCCESS) return ret;

    enable_cs_pin();
    spi_transfer(PAGE_PROGRAM);        //先传命令
    spi_transfer( (addr>>)&0xff );    //依次传输3字节地址,高字节先发送
    spi_transfer( (addr>>)&0xff );
    spi_transfer( addr&0xff);
    //开始传数据给flsh芯片
    ; i < len; i++){
        spi_transfer(data[i]);    //这里主要是写数据,所以忽略返回值。
    }
    disable_cs_pin();    

    return SUCCESS;

}
uint8_t flash_sector_erase(uint32_t addr){
    uint8_t ret;
    ret = flash_write_enable();
    if ( ret != SUCCESS ) return ret;

    enable_cs_pin();
    spi_transfer(SECTOR_ERASE);        //先传命令
    spi_transfer( (addr>>)&0xff );    //依次传输3字节地址,高字节先发送
    spi_transfer( (addr>>)&0xff );
    spi_transfer( addr&0xff);
    disable_cs_pin();

    return SUCCESS;

}

/**@brief Application main function.
 */
uint8_t *data;
int main(void)
{

    uint32_t err_code;
    bool erase_bonds;
    uint8_t start_string[] = START_STRING;
    uint8_t buff[];
    uint8_t ret;

    uart_init();
    printf("start\r\n");

    init_spi_master();
    //先读出200字节
    ret = flash_read(, buff, );
    if(ret!=SUCCESS)printf("read error\r\n");
    printf("原始数据\r\n");
    ; i < ; i++){
        printf("%d ",buff[i]);
    }
    printf("\r\n");

    //擦除第0个sector,并再读取下,
    ret = flash_sector_erase(0x0);
    if ( ret!=SUCCESS ) printf("erase error\r\n");
    ret = flash_read(, buff, );
    printf("擦除后数据\r\n");
    if(ret!=SUCCESS)printf("read error\r\n");
    ; i < ; i++){
        printf("%d ",buff[i]);
    }
    printf("\r\n");    

    ; i < ; i++){
        buff[i] = i;
    }
    //现在已经擦除过了,写数据进去
    ret = flash_page_write(, buff, );
    if ( ret!=SUCCESS ) printf("write error\r\n");

    //再读数据看对不对
    ret = flash_read(, buff, );
    printf("写后数据:\r\n");
    if(ret!=SUCCESS)printf("read error\r\n");
    ; i < ; i++){
        printf("%d ",buff[i]);
    }
    printf("\r\n");    

    );    

}

nrf51822裸机教程-SPI(主)的更多相关文章

  1. nrf51822裸机教程-IIC

    关于IIC总线的核心有以下几点: :时钟线高电平期间必须保持数据线不变. :时钟线低电平期间可以改变数据. :时钟线和数据线上都要接上拉电阻,以使总线不工作时,两根线的电平都处于高电平状态. :每个传 ...

  2. nrf51822裸机教程-UART

    art硬件模块通常都有内置的硬件接收buff,比如51822的硬件uart模块图如下 因为通常接收到uart数据时都会做一些处理.比如保存到数据,或者对数据做一些判断之类的. 如果uart的波特率设置 ...

  3. nrf51822裸机教程-RTC

    RTC0被协议栈使用了.所以在跑蓝牙程序的情况下.RTC0不能使用. RTC相关寄存器如下: EVTEN,EVTENSET,EVTENCLR. 这三个寄存器用来设置是否使能某个事件.(TICK,OVR ...

  4. nrf51822裸机教程-PWM

    先简单介绍一下PWM的原理. 原理很简单. 假设COUNTER是个从0开始递增的计数器.  我们设置两个值 counter0 和counter1 在 COUNTER 计数到counter0的值时候翻转 ...

  5. nrf51822裸机教程-PPI

    Programmable Peripheral Interconnect即可编程外设互联 系统,该模块是51822 提供的一个特性. 目的是为了让51822 的外围模块可以不通过处理器而自动相互作用. ...

  6. nrf51822裸机教程-GPIOTE

    GPIO通常都会具有中断功能,上一讲的GPIO中并没有涉及到中断的相关寄存器. 51822将GPIO的中断相关做成了一个单独的模块GPIOTE,这个模块不仅提供了GPIO的中断功能,同时提供了 通过t ...

  7. nrf51822裸机教程-硬件timer

    该讲介绍51822的Timer/Counter模块工作在timer模式下(定时器模式,还可以工作为计数器模式) 如何操作 51822的Timer/Counter结构如下图所示 Timer模块从PCLK ...

  8. nrf51822裸机教程-GPIO

    首先看看一下相关的寄存器说明 Out寄存器 输出设置寄存器 每个比特按顺序对应每个引脚,bit0对应的就是 引脚0 该寄存器用来设置 引脚作为输出的时候的 输出电平为高还是低. 与输出设置相关的 还有 ...

  9. TI BLE CC2541的SPI主模式

    SPI就是用4条线来串行传输数据, 2541只能用模拟的方式用GPIO来做. //*********************************************************** ...

随机推荐

  1. AutoMapper使用笔记

    AutoMapper是一个.NET的对象映射工具. 项目地址:https://github.com/AutoMapper/AutoMapper. 帮助文档:https://github.com/Aut ...

  2. 一个DIV三列布局100%高度自适应的好例子(国外)

    <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W ...

  3. 吃豆子(Packman)

    ZLYD团队总结--吃豆子(Packman) 一.设计主要内容 玩家可以通过键盘或鼠标控制游戏区.游戏采取分数和血量制,当血量用尽时游戏结束,游戏以最终玩家获得的分数来判断玩家水平的高低.使用IEDA ...

  4. POJ1679 The Unique MST(次小生成树)

    可以依次枚举MST上的各条边并删去再求最小生成树,如果结果和第一次求的一样,那就是最小生成树不唯一. 用prim算法,时间复杂度O(n^3). #include<cstdio> #incl ...

  5. BZOJ3790 : 神奇项链

    Manacher求出所有极长回文子串后,得到一堆线段,转化成线段覆盖问题 预处理出g[i]表示左端点不超过i的右端点的最大值 贪心地线段覆盖即可 时间复杂度$O(n)$ #include<cst ...

  6. [Unity2D]Box Collider 2D盒子碰撞器

    盒子碰撞器(BoxCollider2D)是Unity2D中常用的碰撞器,所有为碰撞器,顾名思义,就是用于检测物体之间的碰撞情况的,Unity2D里面除了BoxCollider2D碰撞器之外还集成Box ...

  7. 【BZOJ】3038: 上帝造题的七分钟2(线段树+暴力)

    http://www.lydsy.com:808/JudgeOnline/problem.php?id=3038 这题我就有得吐槽了,先是线段树更新写错,然后不知哪没pushup导致te,精度问题sq ...

  8. BZOJ4417: [Shoi2013]超级跳马

    Description 现有一个n行m列的棋盘,一只马欲从棋盘的左上角跳到右下角.每一步它向右跳奇数列,且跳到本行或相邻行.跳越期间,马不能离开棋盘.例如,当n = 3, m = 10时,下图是一种可 ...

  9. SecureCrt脚本(一)顶级对象之Crt

    Crt自动化 测试 SecureCrt脚本 JS脚本   1.引言 2.关于脚本表头 3.顶级对象'crt'的子属性和方法 3.1.属性 3.1.1.Dialog 3.1.2.Screen 3.1.3 ...

  10. 连接mysql遇到的问题

    1. 首先使用一个用户名不行时,可以新建一个用户名点击用户,输入名和密码,此时一定要记住密码,别忘了.不过也可以点击编辑用户改.此时连接时发现还是连接不上.出现错误信息为: Access denied ...