Polling
该示例通过使用 SPI 轮询模式进行与外部flash的通信。
使用轮询的方式,对外部flash进行数据的读取,数据的擦除,写入等操作。
示例中可以通过配置宏 SPI_MODE
选择SPI的通信模式为全双工或EEPROM。
可以通过配置宏 SPI_CONFIG_GPIO_SIM_CS
选择是否使用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硬件介绍 。
配置选项
该示例可配置的宏如下:
SPI_MODE
:配置该宏可修改SPI的通信模式,可选择的值如下:SPI_MODE_FULLDUPLEX
:SPI全双工通信模式。在此通信模式下,SPI的接收与发送操作同时进行。SPI_MODE_EEPROM
:SPI EEPROM通信模式。在此通信模式下,SPI在发送操作时不进行接收操作,直到发送完毕时进行数据接收。
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
请按照以下步骤操作构建并运行该示例:
打开工程文件。
按照 快速入门 中 编译APP Image 给出的步骤构建目标文件。
编译成功后,在路径
mdk\bin
或gcc\bin
下会生成 app binapp_MP_xxx.bin
文件。按下复位按键,开始运行。
测试验证
EVB启动后,在Debug Analyzer工具内观察log。
Start spi polling test!
SPI通信模式配置:
若配置宏
SPI_MODE
为SPI_MODE_FULLDUPLEX
,则会打印如下log。SPI is set to FullDulpex Mode!
若配置宏
SPI_MODE
为SPI_MODE_EEPROM
,则会打印如下log。SPI is set to EEPROM Mode!
读取外部flash id:
读取DEVICE_ID,前三个字节为dummy read,最后一个字节为DEVICE_ID。
flash id = 000000ff flash id = 000000ff flash id = 000000ff flash id = 00000017
读取MF_DEVICE_ID,前三个字节为dummy read,后两个字节为DEVICE_ID。
flash id = 000000ff flash id = 000000ff flash id = 000000ff flash id = 000000ef flash id = 00000017
读取JEDEC_ID
flash id = 000000ef flash id = 00000040 flash id = 00000018
将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
在Flash内的指定地址下进行数据写入操作,后再进行数据读取:
在全双工模式下,由于发送了四个字节的数据,接收数据的前四个字节为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
在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
代码介绍
该章节分为以下几个部分:
源码路径
工程路径:
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_init
和 driver_spi_init
。
board_spi_init
中包含了PAD与PINMUX设置:
配置PAD:设置引脚、PINMUX模式、PowerOn、内部上拉。
配置PINMUX:分配引脚分别为SPI0_CLK_MASTER、SPI0_MO_MASTER、SPI0_MI_MASTER、SPI0_CSN_0_MASTER功能。
若配置
SPI_CONFIG_GPIO_SIM_CS
为1,则需要将SPI_CS_PIN分配为DWGPIO功能。
driver_spi_init
包含了对spi外设的初始化:
使能SPI0时钟源。
根据
SPI_MODE
的配置去配置对应的SPI传输模式。在EEPROM传输模式下,需配置NDF值。
配置传输数据宽度
配置串行时钟的稳态悬空高,数据捕获于第二个时钟沿。
设置时钟分频系数为100.
设置接收数据长度阈值和数据传输格式。
备注
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
功能实现
循环三次执行
spi_flash_read_id
,分别读取外部flash DEVICE_ID/MF_DEVICE_ID/JEDEC_ID信息。根据需要读取的不同ID类型,修改发送数据的第一个字节对应的不同指令与需要接收的数据长度。
若配置宏
SPI_MODE
为SPI_MODE_EEPROM
:在发送数据前需要执行
SPI_SetReadLen()
,设置需要读取的数据长度。设置好接收长度后,只需要执行
SPI_SendBuffer()
发送一个字节的指令数据,即可循环执行SPI_ReceiveData()
函数进行数据接收。
若配置宏
SPI_MODE
为SPI_MODE_FULLDUPLEX
:由于全双工模式通信为收发同时进行,所以需要发送
recv_len + 1
长度的数据。在发送数据之后,执行一次
SPI_ReceiveData()
,执行一次dummy read,读取发送的指令字节对应回复的无效数据。循环执行
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
}
执行
spi_flash_sector_erase
,执行指定地址的数据擦除。编辑需要发送的数据。第一个字节为擦除指令,第二三四字节为需要指定的地址。
在写入数据之前,执行
spi_flash_write_enable
使能写操作。若配置宏
SPI_MODE
为SPI_MODE_EEPROM
:执行写操作时,只需要发送数据不需要接收数据,此时EEPROM的通信模式无法支持该场景的使用,所以需要切换模式至Tx Only模式进行发送数据。
在切回EEPROM模式之前,需要等待BUSY位置回RESET。
若配置宏
SPI_MODE
为SPI_MODE_FULLDUPLEX
:在FullDulpex模式下,在发送完一笔数据后,需要循环执行
SPI_GetRxFIFOLen()
,将RX FIFO内的数据读空。
执行
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();
}
执行
spi_flash_read
函数,读取flash指定地址下,指定长度的数据内容。编辑需要发送的数据。第一个字节为读指令,第二三四字节为需要指定的地址。
若配置宏
SPI_MODE
为SPI_MODE_EEPROM
:在发送数据前需要执行
SPI_SetReadLen()
,设置需要读取的数据长度为指定长度。执行
SPI_SendBuffer()
函数,发送读指令与指定地址。循环执行
SPI_GetRxFIFOLen()
,读取flash发回的数据。
若配置宏
SPI_MODE
为SPI_MODE_FULLDUPLEX
:在发送完指令+地址数据后,需要循环执行
SPI_GetRxFIFOLen()
,将RX FIFO内的无效数据读空。在接收数据阶段:每次接收数据前,都需要发送一笔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
}
执行
spi_flash_page_write
函数,将指定数据写入flash的指定地址下。(逻辑与spi_flash_sector_erase
类似)执行
spi_flash_read
函数,读取写入数据后该地址下的数据内容,检查是否正确。在设置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值一致。