External Flash

该示例演示 SPI 与外部 flash 通信的方法。

使用轮询的方式,对外部 flash 进行数据的读取,数据的擦除,写入等操作。

在 Debug Analyzer 内,打印 Flash ID 以及 FLASH 内部数据信息的读取和写入信息。

环境需求

该示例支持以下开发套件:

开发套件

Hardware Platforms

Board Name

RTL8752H HDK

RTL8752H EVB

更多信息请参考 快速入门

硬件连线

EVB 外接 W25Q128 模块,使用杜邦线连接 P4_0(SCK)和 CLK,P4_1(MISO)和 DO,P4_2(MOSI)和 DI,P4_3(CS)和 CS#。

硬件介绍

W25Q128 是一款 SPI 接口的 NOR Flash 芯片,支持标准串行外设接口(SPI)、双线/四线 SPI 以及 2-时钟指令周期四外设接口(QPI)。

此外,该器件还支持 JEDEC 标准的制造商和器件 ID 和 SFDP 寄存器、64 位唯一序列号和三个 256 字节安全寄存器。

其他细节说明可查阅该器件的应用指南。在示例中使用该器件作为 slave 进行 SPI 的通信测试。

编译和下载

该示例的工程路径如下:

Project file: board\evb\io_sample\SPI\Flash\mdk

Project file: board\evb\io_sample\SPI\Flash\gcc

请按照以下步骤操作构建并运行该示例:

  1. 打开工程文件。

  2. 按照 快速入门编译 APP Image 给出的步骤构建目标文件。

  3. 编译成功后,在路径 mdk\bingcc\bin 下会生成 app bin app_MP_xxx.bin 文件。

  4. 按照 快速入门MP Tool 给出的步骤将 app bin 烧录至 EVB 内。

  5. 按下 reset 按键,开始运行。

测试验证

  1. 读取外部 flash JEDEC_ID,打印 JEDEC_ID 信息。

    [io_spi] spi_demo: flash_id = EF 40 18
    
  2. 对指定地址下的 flash 数据进行擦除,读取擦除后该地址下的数据(全 FF)。

    [io_spi] spi_demo: spi_flash_sector_erase done
    [io_spi] spi_demo: read_data = FF FF FF ...
    
  3. 在 flash 内的指定地址下进行数据写入操作,后再进行数据读取。

    [io_spi] spi_demo: read_data = 00 01 02 ...
    

代码介绍

该章节分为以下几个部分:

  1. 源码路径

  2. 初始化函数将在 初始化 章节介绍。

  3. 初始化后的功能实现将在 功能实现 章节介绍。

源码路径

  • 工程路径: sdk\board\evb\io_sample\SPI\Flash

  • 源码路径: sdk\src\sample\io_sample\SPI\Flash

该工程的工程文件代码结构如下:

└── Project: flash
    └── secure_only_app
        └── include
            ├── app_define.h
            └── rom_uuid.h
        ├── cmsis                    includes CMSIS header files and startup files
            ├── overlay_mgr.c
            ├── system_rtl876x.c
            └── startup_rtl876x.s
        ├── lib                      includes all binary symbol files that user application is built on
            ├── rtl8752h_sdk.lib
            ├── gap_utils.lib
            ├── ROM.lib
            └── adc.lib
        ├── peripheral               includes all peripheral drivers and module code used by the application
            ├── rtl876x_rcc.c
            ├── rtl876x_pinmux.c
            ├── rtl876x_nvic.c
            └── rtl876x_spi.c
        ├── profile
        └── app                      includes the ble_peripheral user application implementation
            ├── main.c
            ├── ancs.c
            ├── app.c
            ├── app_task.c
            ├── spi_flash.c
            └── io_spi.c

初始化

当 EVB 复位启动时,调用 main() 函数,将执行以下流程:

int main(void)
{
    extern uint32_t random_seed_value;
    srand(random_seed_value);
    global_data_init();
    board_init();
    le_gap_init(APP_MAX_LINKS);
    gap_lib_init();
    app_le_gap_init();
    app_le_profile_init();
    pwr_mgr_init();
    task_init();
    os_sched_start();

    return 0;
}

备注

le_gap_init()gap_lib_init()app_le_gap_initapp_le_profile_init 等为 privacy 管理模块相关的初始化,参考 LE Peripheral Privacy 中的初始化流程介绍。

与外设相关的初始化流程具体如下:

  1. board_init 中,执行 board_spi_init ,该函数为 PAD/PINMUX 设置,包含如下流程:

    1. 配置 PAD:设置引脚,PINMUX 模式,PowerOn,内部上拉,输出高。

    2. 配置 PINMUX:分配引脚分别为 SPI0_CLK_MASTER、SPI0_MO_MASTER、SPI0_MI_MASTER、SPI0_SS_N_0_MASTER 功能。

  2. 在执行 os_sched_start() 开启任务调度后,在 app_main_task 主任务内,执行 driver_init 对外设驱动进行初始化配置。

  3. driver_init 中执行 driver_spi_init ,该函数为 SPI 外设的初始化,包含如下流程:

    1. 使能 RCC 时钟。

    2. 配置 SPI 的传输模式为全双工模式。

    3. 配置数据宽度为 8 位。

    4. 配置串行时钟的稳态悬空高,数据捕获于第二个时钟沿。

    5. 设置时钟分频系数为 20。

    6. 设置接收数据长度阈值和数据传输格式。

    7. 使能 SPI 外设。

    备注

    SPI 的时钟分频系数最小为 2,且只能为偶数。

    void driver_spi_init(void)
    {
        RCC_PeriphClockCmd(APBPeriph_SPI0, APBPeriph_SPI0_CLOCK, ENABLE);
    
        SPI_InitTypeDef  SPI_InitStruct;
        SPI_StructInit(&SPI_InitStruct);
    
        SPI_InitStruct.SPI_Direction   = SPI_Direction_FullDuplex;
        SPI_InitStruct.SPI_Mode        = SPI_Mode_Master;
        SPI_InitStruct.SPI_DataSize    = SPI_DataSize_8b;
        SPI_InitStruct.SPI_CPOL        = SPI_CPOL_High;
        SPI_InitStruct.SPI_CPHA        = SPI_CPHA_2Edge;
        SPI_InitStruct.SPI_BaudRatePrescaler  = 20;
        /* SPI_Direction_EEPROM mode read data lenth. */
        SPI_InitStruct.SPI_RxThresholdLevel  = 1;/* Flash id lenth = 3*/
        SPI_InitStruct.SPI_NDF               = 0;/* Flash id lenth = 3*/
        SPI_InitStruct.SPI_FrameFormat = SPI_Frame_Motorola;
        SPI_Init(SPI0, &SPI_InitStruct);
    
        SPI_Cmd(SPI0, ENABLE);
    
    }
    

功能实现

  1. 在主函数中执行 os_sched_start() ,开启任务调度。当 stack 准备好时,执行 app_handle_dev_state_evt ,执行 spi_demo

    1. 定义需要写入指定地址的数据内容。

    2. 按顺序执行以下流程:读取 ID -> 擦除指定地址数据 -> 读取指定地址数据 -> 对指定地址写入数据 -> 读取指定地址数据。

    void spi_demo(void)
    {
        uint8_t flash_id[10] = {0};
        uint8_t write_data[100];
        uint8_t read_data[105] = {0};
        for (uint16_t i = 0; i < 100; i++)
        {
            write_data[i] = i & 0xFF;
        }
    
        spi_flash_read_id(flash_id);
        APP_PRINT_INFO1("[io_spi] spi_demo: flash_id = %b ", TRACE_BINARY(3, flash_id));
    
        spi_flash_sector_erase(0x001000);
        APP_PRINT_INFO0("[io_spi] spi_demo: spi_flash_sector_erase done");
    
        spi_flash_read(SPI_FLASH_FAST_READ, 0x001000, read_data, 100);
    //    APP_PRINT_INFO1("[io_spi] spi_demo: read_data = %b,", TRACE_BINARY(100, &read_data[5]));
        APP_PRINT_INFO1("[io_spi] spi_demo: read_data = %b,", TRACE_BINARY(100, read_data));
    
        spi_flash_page_write(0x001000, write_data, 100);
    
        spi_flash_read(SPI_FLASH_FAST_READ, 0x001000, read_data, 100);
        APP_PRINT_INFO1("[io_spi] spi_demo: read_data = %b,", TRACE_BINARY(100, read_data));
    }
    
  2. 执行 spi_flash_read_id ,读取 JEDEC_ID 信息。

    1. 定义发送数据的第一个字节为读 JEDEC_ID 指令。根据 spec 说明,定义剩余三个字节的发送数据,用于接收 dummy 数据。

    2. 执行 SPI_SendBuffer(),发送四个字节的数据。

    3. 执行 SPI_ReceiveData(),读取 slave 端回复的数据信息。

    void spi_flash_read_id(uint8_t *pFlashId)
    {
        uint8_t send_buf[4] = {SPI_FLASH_JEDEC_ID, 0, 0, 0};
        uint8_t recv_buf[10] = {0};
        uint8_t recv_len = 3;
    
    #if (SPI_MODE_EEPROM == SPI_MODE)
        SPI_SetReadLen(FLASH_SPI, 3);
        SPI_SendBuffer(FLASH_SPI, send_buf, 1);
    #elif (SPI_MODE_FULLDUPLEX == SPI_MODE)
        SPI_SendBuffer(FLASH_SPI, send_buf, 4);
        recv_len += 1;
    #endif
    
        uint8_t idx = 0;
        while (recv_len--)
        {
            while (RESET == SPI_GetFlagState(FLASH_SPI, SPI_FLAG_RFNE));
            recv_buf[idx++] = SPI_ReceiveData(FLASH_SPI);
        }
    
    #if (SPI_MODE_EEPROM == SPI_MODE)
        memcpy(pFlashId, recv_buf, 3);
    #elif (SPI_MODE_FULLDUPLEX == SPI_MODE)
        memcpy(pFlashId, &recv_buf[1], 3);
    #endif
    }
    
  3. 执行 spi_flash_sector_erase ,对指定地址下的数据进行擦除。

    1. 定义发送数据的第一个字节为 sector 擦除指令,第 2-4 个字节为需要擦除数据的地址信息。

    2. 在写入数据之前,执行 spi_flash_write_enable 使能写操作,发送写使能指令至外部 flash。

    3. 执行 SPI_SendBuffer() 发送数据,并循环执行 SPI_ReceiveData() 清空 RX FIFO 内 slave 端回复的数据。

    4. 发送完毕后,执行 spi_flash_busy_check ,检查外部 flash 的状态。

    void spi_flash_sector_erase(uint32_t vAddress)
    {
        uint8_t send_buf[4] = {SPI_FLASH_SECTOR_ERASE, 0, 0, 0};
        uint8_t recv_len = 0;
    
        send_buf[1] = (vAddress >> 16) & 0xff;
        send_buf[2] = (vAddress >> 8) & 0xff;
        send_buf[3] = vAddress & 0xff;
    
        /* enable write */
        spi_flash_write_enable();
    
        /* erase data */
        SPI_SendBuffer(FLASH_SPI, send_buf, 4);
    
        while (SPI_GetFlagState(FLASH_SPI, SPI_FLAG_BUSY) == SET);
        recv_len = SPI_GetRxFIFOLen(FLASH_SPI);
        while (recv_len--)
        {
            SPI_ReceiveData(FLASH_SPI);
        }
        spi_flash_busy_check();
    }
    
  4. 执行 spi_flash_read,读取 flash 指定地址下,指定长度的数据。

    1. 编辑需要发送的数据。第一个字节为读指令,第二三四字节为指定地址。执行 SPI_SendBuffer() 发送指令和地址。

    2. 在发送完指令+地址数据后,循环执行 SPI_GetRxFIFOLen(),读取 dummy 数据。

    3. 由于全双工的通信特点,在每次接收数据之前,需要发送一笔 dummy 数据至 flash,随后才能接收 flash 发回的一笔数据。

    void spi_flash_read(uint8_t vReadCmd, uint32_t vReadAddr, uint8_t *pBuffer, uint16_t vLength)
    {
        uint8_t send_buf[10] = {0};
        uint8_t send_len = 0;
        uint16_t recv_len = 0;
    
        if (SPI_FLASH_READ_DATA == vReadCmd)
        {
            send_len = 4;
        }
        else if (SPI_FLASH_FAST_READ == vReadCmd)
        {
            send_len = 5;
        }
        send_buf[0] = vReadCmd;
        send_buf[1] = (vReadAddr >> 16) & 0xFF;
        send_buf[2] = (vReadAddr >> 8) & 0xFF;
        send_buf[3] = (vReadAddr) & 0xFF;
    
        SPI_SendBuffer(FLASH_SPI, send_buf, send_len);
        while (SPI_GetFlagState(FLASH_SPI, SPI_FLAG_RFNE) == RESET);
        for (uint8_t i = 0; i < send_len; i++)
        {
            SPI_ReceiveData(FLASH_SPI);//dummy read data
        }
        recv_len = vLength;
        while (recv_len--)
        {
            SPI_SendBuffer(FLASH_SPI, &send_buf[9], 1);
            while (SPI_GetFlagState(FLASH_SPI, SPI_FLAG_RFNE) == RESET);
            *pBuffer++ = SPI_ReceiveData(FLASH_SPI);
        }
    
    }
    
  5. 执行 spi_flash_page_write 函数,将指定数据写入 flash 的指定地址下。

    1. 定义发送数据,第一个字节为写指令,第二三四字节为地址。

    2. 在写入数据之前,执行 spi_flash_write_enable 使能写操作,发送写使能指令至外部 flash。

    3. 执行 SPI_SendBuffer() 发送数据,首先发送指令数据和地址数据,其次发送需要写入的数据内容。

    4. 循环接收 flash 发回的 dummy 数据。发送完毕后,执行 spi_flash_busy_check,检查外部 flash 的状态。

    void spi_flash_page_write(uint32_t vWriteAddr, uint8_t *pBuffer, uint16_t vLength)
    {
        uint16_t send_data_len = vLength;
        uint8_t recv_data_len = 0;
        uint8_t send_buf[4] = {SPI_FLASH_PAGE_PROGRAM, 0, 0, 0};
        send_buf[1] = (vWriteAddr >> 16) & 0xff;
        send_buf[2] = (vWriteAddr >> 8) & 0xff;
        send_buf[3] = vWriteAddr & 0xff;
    
        if (vLength > SPI_FLASH_PAGE_SIZE)
        {
            send_data_len = SPI_FLASH_PAGE_SIZE;
        }
    
        /* Enable write */
        spi_flash_write_enable();
        SPI_SendBuffer(FLASH_SPI, send_buf, 4);
        SPI_SendBuffer(FLASH_SPI, pBuffer, send_data_len);
    
        while (SPI_GetFlagState(FLASH_SPI, SPI_FLAG_BUSY) == SET);
        recv_data_len = SPI_GetRxFIFOLen(FLASH_SPI);
        while (recv_data_len--)
        {
            SPI_ReceiveData(FLASH_SPI);
        }
        spi_flash_busy_check();
    }