Polling

该示例通过使用 SPI Polling模式进行与外部flash的通信。 使用polling的方式,对外部flash进行数据的读取,数据的擦除,写入等操作。 示例中可以通过宏选择SPI的通信模式,和是否使用GPIO模拟CS线通信。

环境需求

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

开发套件

Hardware Platforms

Board Name

RTL87x2G HDK

RTL87x2G EVB

更多信息请参考 快速入门

硬件连线

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

备注

W25Q128的模块介绍参考 W25Q128硬件介绍

配置选项

该示例可配置的宏如下:

  1. SPI_MODE :配置该宏可修改SPI的通信模式,可选择的值如下:

    • SPI_MODE_FULLDUPLEX :SPI全双工通信模式。在此通信模式下,SPI的接收与发送操作同时进行。

    • SPI_MODE_EEPROM:SPI EEPROM通信模式。在此通信模式下,SPI在发送操作时不进行接收操作,直到发送完毕时进行数据接收。

  2. SPI_CONFIG_GPIO_SIM_CS :配置该宏可选择是否使用GPIO模拟CS线进行通信。配置 1 则代表使用GPIO模拟CS线通信, 0 则代表不使用GPIO进行模拟。

备注

SPI的FullDuplex和EEPROM通信方式的具体区别,可参考 常见问题

编译和下载

该示例的工程路径如下:

Project file: samples\peripheral\spi\polling\proj\rtl87x2g\mdk

Project file: samples\peripheral\spi\polling\proj\rtl87x2g\gcc

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

  1. 打开工程文件。

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

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

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

  5. 按下复位按键,开始运行。

测试验证

  1. EVB启动后,在DebugAnalyzer工具内观察LOG。

    Start spi polling test!
    
  2. SPI通信模式配置:

    1. 若配置为 SPI_MODE_FULLDUPLEX ,则会打印如下LOG。

      SPI is set to FullDulpex Mode!
      
    2. 若配置为 SPI_MODE_EEPROM ,则会打印如下LOG。

      SPI is set to EEPROM Mode!
      
  3. 读取外挂flash id:

    1. 读取DEVICE_ID,前三个字节为dummy read,最后一个字节为DEVICE_ID。

      flash id = 000000ff
      flash id = 000000ff
      flash id = 000000ff
      flash id = 00000017
      
    2. 读取MF_DEVICE_ID,前三个字节为dummy read,后两个字节为DEVICE_ID。

      flash id = 000000ff
      flash id = 000000ff
      flash id = 000000ff
      flash id = 000000ef
      flash id = 00000017
      
    3. 读取JEDEC_ID

      flash id = 000000ef
      flash id = 00000040
      flash id = 00000018
      
  4. 将Flash指定地址下的数据进行擦除操作,读取擦除之后的数据(全FF)。

    spi_demo: spi_flash_sector_erase done
    spi_demo: after erase read_data[0] = 0x000000ff
    ...
    spi_demo: after erase read_data[99] = 0x000000ff
    
  5. 在Flash内的指定地址下进行数据写入操作,后再进行数据读取:

    1. 在全双工模式下,由于发送了四个字节的数据,接收数据的前四个字节为FF(无效数据),第五个字节开始为有效数据。

      spi_demo: after write read_data[0] = 0x000000ff
      spi_demo: after write read_data[1] = 0x000000ff
      spi_demo: after write read_data[2] = 0x000000ff
      spi_demo: after write read_data[3] = 0x000000ff
      spi_demo: after write read_data[4] = 0x0000000a
      ...
      spi_demo: after write read_data[99] = 0x00000069
      
    2. 在EEPROM模式下,在发送数据时不进行接收,所以接收的所有数据都为有效信息。

      spi_demo: after write read_data[0] = 0x0000000a
      spi_demo: after write read_data[1] = 0x0000000b
      ...
      spi_demo: after write read_data[99] = 0x0000006d
      

代码介绍

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

  1. 源码路径

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

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

源码路径

  • 工程路径: sdk\samples\peripheral\spi\polling\proj

  • 源码路径: sdk\samples\peripheral\spi\polling\src

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

└── Project: polling
    └── secure_only_app
        └── Device                   includes startup code
            ├── startup_rtl.c
            └── system_rtl.c
        ├── CMSIS                    includes CMSIS header files
        ├── CMSE Library             Non-secure callable lib
        ├── Lib                      includes all binary symbol files that user application is built on
            └── rtl87x2g_io.lib
        ├── Peripheral               includes all peripheral drivers and module code used by the application
            ├── rtl_rcc.c
            ├── rtl_pinmux.c
            ├── rtl_gpio.c
            └── rtl_spi.c
        └── APP                      includes the ble_peripheral user application implementation
            ├── main_ns.c
            ├── spi_flash.c          includes a wrapper implementation of functions to communicate with external flash
            └── io_spi.c

初始化

初始化流程包括了 board_spi_initdriver_spi_init


board_spi_init 中包含了PAD与PINMUX设置:

  1. 配置PAD:设置引脚、PINMUX模式、PowerOn、内部上拉。

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

    1. 若配置 SPI_CONFIG_GPIO_SIM_CS 为1,则需要将SPI_CS_PIN 分配为 DWGPIO 功能。


driver_spi_init 包含了对spi外设的初始化:

  1. 使能SPI0时钟源。

  2. 根据 SPI_MODE 的配置去配置对应的SPI传输模式。

  3. EEPROM 传输模式下,需配置NDF值。

  4. 配置传输数据宽度

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

  6. 设置时钟分频系数为100.

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

备注

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

RCC_PeriphClockCmd(APBPeriph_SPI0, APBPeriph_SPI0_CLOCK, ENABLE);
...
#if (SPI_MODE_FULLDUPLEX == SPI_MODE)
  SPI_InitStruct.SPI_Direction   = SPI_Direction_FullDuplex;
       DBG_DIRECT("SPI is set to FullDulpex Mode!");
#elif (SPI_MODE_EEPROM == SPI_MODE)
       SPI_InitStruct.SPI_Direction   = SPI_Direction_EEPROM;
       /* SPI_Direction_EEPROM mode read data lenth. */
       SPI_InitStruct.SPI_NDF         = 1;
       DBG_DIRECT("SPI is set to EEPROM Mode!");
#endif

功能实现

  1. 循环三次执行 spi_flash_read_id ,分别读取外部flash DEVICE_ID/MF_DEVICE_ID/JEDEC_ID信息。

    1. 根据需要读取的不同ID类型,修改发送数据的第一个字节对应的不同指令与需要接收的数据长度。

    2. 若设置为 EEPROM 模式通信:

      1. 在发送数据前需要执行 SPI_SetReadLen() ,设置需要读取的数据长度。

      2. 设置好接收长度后,只需要执行 SPI_SendBuffer() 发送一个字节的指令数据,即可循环执行 SPI_ReceiveData() 函数进行数据接收。

    3. 若设置为 FullDulpex 模式通信:

      1. 由于全双工模式通信为收发同时进行,所以需要发送 recv_len + 1 长度的数据。

      2. 在发送数据之后,执行一次 SPI_ReceiveData() ,执行一次dummy read,读取发送的指令字节对应回复的无效数据。

      3. 循环执行 SPI_ReceiveData() 函数进行剩余数据接收。

void spi_flash_read_id(Flash_ID_Type vFlashIdType, uint8_t *pFlashId)
{
#if (SPI_CONFIG_GPIO_SIM_CS == 1)
        GPIO_ResetBits(GPIO_GetPort(SPI_CS_PIN), GPIO_GetPin(SPI_CS_PIN));
#endif
   uint8_t send_buf[4] = {SPI_FLASH_JEDEC_ID, 0, 0, 0};
   uint8_t recv_len = 3;

   switch (vFlashIdType)
   {
   ...
   }

#if (SPI_MODE_EEPROM == SPI_MODE)
   SPI_SetReadLen(FLASH_SPI, recv_len);
   SPI_SendBuffer(FLASH_SPI, send_buf, 1);
#elif (SPI_MODE_FULLDUPLEX == SPI_MODE)
   SPI_SendBuffer(FLASH_SPI, send_buf, recv_len + 1);
   while (SPI_GetFlagState(FLASH_SPI, SPI_FLAG_RFNE) == RESET){}
   SPI_ReceiveData(FLASH_SPI);//dummy read data
#endif

   *pFlashId++ = recv_len;
   uint8_t idx = 1;
   while (recv_len--)
   {
      while (RESET == SPI_GetFlagState(FLASH_SPI, SPI_FLAG_RFNE)){}
         *pFlashId++ = SPI_ReceiveData(FLASH_SPI);
   }

#if (SPI_CONFIG_GPIO_SIM_CS == 1)
   GPIO_SetBits(GPIO_GetPort(SPI_CS_PIN), GPIO_GetPin(SPI_CS_PIN));
#endif
}
  1. 执行 spi_flash_sector_erase ,执行指定地址的数据擦除。

    1. 编辑需要发送的数据。第一个字节为擦除指令,第二三四字节为需要指定的地址。

    2. 在写入数据之前,执行 spi_flash_write_enable 使能写操作。

    3. 若设置为 EEPROM 模式通信:

      1. 执行写操作时,只需要发送数据不需要接收数据,此时 EEPROM 的通信模式无法支持该场景的使用,所以需要切换模式至 Tx Only 模式进行发送数据。

      2. 在切回 EEPROM 模式之前,需要等待BUSY位置回RESET。

    4. 若设置为 FullDulpex 模式通信:

      1. FullDulpex 模式下,在发送完一笔数据后,需要循环执行 SPI_GetRxFIFOLen() ,将RX FIFO内的数据读空。

    5. 执行 spi_flash_busy_check ,检查外部flash的状态。

void spi_flash_sector_erase(uint32_t vAddress)
{
#if (SPI_CONFIG_GPIO_SIM_CS == 1)
   GPIO_ResetBits(GPIO_GetPort(SPI_CS_PIN), GPIO_GetPin(SPI_CS_PIN));
#endif

   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 */
#if (SPI_MODE_EEPROM == SPI_MODE)
   SPI_ChangeDirection(FLASH_SPI, SPI_Direction_TxOnly);
   SPI_SendBuffer(FLASH_SPI, send_buf, 4);
   while (SPI_GetFlagState(FLASH_SPI, SPI_FLAG_BUSY) == SET){}
   SPI_ChangeDirection(FLASH_SPI, SPI_Direction_EEPROM);
#elif (SPI_MODE_FULLDUPLEX == SPI_MODE)
   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);
   }
#endif

#if (SPI_CONFIG_GPIO_SIM_CS == 1)
   GPIO_SetBits(GPIO_GetPort(SPI_CS_PIN), GPIO_GetPin(SPI_CS_PIN));
#endif

   spi_flash_busy_check();
}
  1. 执行 spi_flash_read 函数,读取flash指定地址下,指定长度的数据内容。

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

    2. 若设置为 EEPROM 模式通信:

      1. 在发送数据前需要执行 SPI_SetReadLen() ,设置需要读取的数据长度为指定长度。

      2. 执行 SPI_SendBuffer() 函数,发送读指令与指定地址。

      3. 循环执行 SPI_GetRxFIFOLen() ,读取flash发回的数据。

    3. 若设置为 FullDulpex 模式通信:

      1. 在发送完指令+地址数据后,需要循环执行 SPI_GetRxFIFOLen() ,将RX FIFO内的无效数据读空。

      2. 在接收数据阶段:每次接收数据前,都需要发送一笔dummy数据至flash,才可接收一笔数据。

void spi_flash_read(uint8_t vReadCmd, uint32_t vReadAddr, uint8_t *pBuffer, uint16_t vLength)
{
#if (SPI_CONFIG_GPIO_SIM_CS == 1)
   GPIO_ResetBits(GPIO_GetPort(SPI_CS_PIN), GPIO_GetPin(SPI_CS_PIN));
#endif

   ...

#if (SPI_MODE_EEPROM == SPI_MODE)
   SPI_SetReadLen(FLASH_SPI, vLength);
   SPI_SendBuffer(FLASH_SPI, send_buf, send_len);
#elif (SPI_MODE_FULLDUPLEX == SPI_MODE)
   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
   }
#endif

   recv_len = vLength;
   while (recv_len--)
   {
#if (SPI_MODE_FULLDUPLEX == SPI_MODE)
      SPI_SendBuffer(FLASH_SPI, &send_buf[9], 1);
#endif
      while (SPI_GetFlagState(FLASH_SPI, SPI_FLAG_RFNE) == RESET){}
      *pBuffer++ = SPI_ReceiveData(FLASH_SPI);
   }

#if (SPI_CONFIG_GPIO_SIM_CS == 1)
   GPIO_SetBits(GPIO_GetPort(SPI_CS_PIN), GPIO_GetPin(SPI_CS_PIN));
#endif
}
  1. 执行 spi_flash_page_write 函数,将指定数据写入flash的指定地址下。(逻辑与 spi_flash_sector_erase 类似)

  2. 执行 spi_flash_read 函数,读取写入数据后该地址下的数据内容,检查是否正确。

  3. 在设置GPIO模拟CS线时,在每次通信开始之前执行 GPIO_ResetBits() 函数将CS线拉低,通信结束之后执行 GPIO_SetBits() 函数将CS线拉高。

常见问题

SPI四种通信方式简介

FullDuplex 在FullDuplex通信模式下,发送与接收逻辑均有效。数据传输按照选定的帧格式(串行协议)正常进行。

Transmit Only 在Transmit Only通信模式下,接收数据的逻辑无效,且不会存储在FIFO中。

Receive Only 在Receive Only通信模式下,发送数据无效。在传输过程中,输出保持恒定逻辑电平。

EEPROM 在EEPROM通信模式下,一般用于发送数据用于向EEPROM设备发送操作码和地址。在传输操作码和地址期间,接收逻辑无效,不会进行接收数据。 主站继续发送数据,直到发送 FIFO 为空。当发送 FIFO 为空时,接收线路上的数据有效,并存储在接收 FIFO 中。 传输继续进行,直到主站接收到的数据数量与初始化中配置的NDF值一致。