DMA Single Block
该示例通过使用 SPI 与外部flash以 DMA 搬运数据方式进行数据传输。
SPI通过DMA与外部flash通信,对外部flash进行数据的读取,写入,擦除等操作。
示例中采用全双工的模式进行通信,可以配置宏 CONFIG_SPI_SW_SIM_CS
选择是否使用软件模拟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是一款SPI接口的NOR Flash芯片,支持标准串行外设接口(SPI)、双线/四线 SPI 以及 2-时钟指令周期四外设接口(QPI)。
此外,该器件还支持JEDEC标准的制造商和器件ID和SFDP寄存器、64位唯一序列号和三个256字节安全寄存器。
其他细节说明可查阅该器件的应用指南。在示例中使用该器件作为slave进行SPI的通信测试。
配置选项
该示例可配置的宏如下:
CONFIG_SPI_SW_SIM_CS
:配置该宏可选择是否使用软件模拟CS线进行通信。配置1
则代表使用软件模拟CS线通信,0
则代表不使用软件进行模拟。
编译和下载
该示例的工程路径如下:
Project file: samples\peripheral\spi\gdma_singleblock\proj\rtl87x2g\mdk
Project file: samples\peripheral\spi\gdma_singleblock\proj\rtl87x2g\gcc
请按照以下步骤操作构建并运行该示例:
打开工程文件。
按照 快速入门 中 编译APP Image 给出的步骤构建目标文件。
编译成功后,在路径
mdk\bin
或gcc\bin
下会生成 app binapp_MP_xxx.bin
文件。按下复位按键,开始运行。
测试验证
当DMA TX通道数据传输完成时,会打印log如下
gdma tx data finish!
当DMA RX通道数据传输完成时,会打印传输完成说明,收到的数据长度和数据内容。
读取flash id时,打印如下内容:第一个字节为dummy read,后三个字节为JEDEC_ID内容。
data_len = 4 dma rx data[0] = 000000ff dma rx data[1] = 000000ef dma rx data[2] = 00000040 dma rx data[3] = 00000018
使能写操作时,打印如下内容:第一个字节为dummy read。
data_len = 1 dma rx data[0] = 000000ff
执行擦除操作时,打印如下内容:写操作对应的四个字节为dummy read。
data_len = 4 dma rx data[0] = 000000ff dma rx data[1] = 000000ff dma rx data[2] = 000000ff dma rx data[3] = 000000ff
擦除完成后,读取该地址下的数据,打印如下内容:由于该地址下数据被擦除,因此读到的数据均为FF,前5个字节为dummy read。
data_len = 105 dma rx data[0] = 000000ff dma rx data[1] = 000000ff ... dma rx data[104] = 000000ff
执行数据写入操作,打印如下内容:写操作对应的前4个字节为指令的dummy read,后100个字节为数据的dummy read。
data_len = 104 dma rx data[0] = 000000ff ... dma rx data[103] = 000000ff
写入数据之后再次进行数据读取,打印读取的数据内容:前5个字节为dummy read,后100个字节为读取到的数据。
data_len = 105 dma rx data[0] = 000000ff dma rx data[1] = 000000ff dma rx data[2] = 000000ff dma rx data[3] = 000000ff dma rx data[4] = 000000ff dma rx data[5] = 0000000a dma rx data[6] = 0000000b dma rx data[7] = 0000000c ... dma rx data[104] = 0000006d
代码介绍
该章节分为以下几个部分:
源码路径
工程路径:
sdk\samples\peripheral\spi\gdma_singleblock\proj
源码路径:
sdk\samples\peripheral\spi\gdma_singleblock\src
该工程的工程文件代码结构如下:
└── Project: gdma_singleblock
└── 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_nvic.c
├── rtl_gdma.c
└── rtl_spi.c
└── APP includes the ble_peripheral user application implementation
├── main_ns.c
└── io_spi.c
初始化
初始化流程包括了 board_spi_init
、driver_spi_init
、driver_tx_gdma_init
和 driver_rx_gdma_init
。
board_spi_init
中包含了对SPI的PAD与PINMUX设置。
配置PAD:设置引脚、PINMUX模式、PowerOn、内部上拉。
配置PINMUX:分配引脚分别为SPI0_CLK_MASTER、SPI0_MO_MASTER、SPI0_MI_MASTER、SPI0_CSN_0_MASTER功能。
如果开启宏
CONFIG_SPI_SW_SIM_CS
,则需要将对应的CS线引脚设置为SW模式,无需配置PINMUX。
driver_spi_init
包含了对SPI外设的初始化:
使能PCC时钟。
设置通信模式为全双工模式。
其他基础设置参考 SPI Polling 初始化流程。
使能SPI的DMA发送与接收功能。
设置RxWaterlevel为1-1,设置TxWaterlevel为FIFO_SIZE - 1。
RCC_PeriphClockCmd(APBPERIPH_SPI, APBPERIPH_SPI_CLOCK, ENABLE);
...
SPI_InitStructure.SPI_Direction = SPI_Direction_FullDuplex;
...
SPI_InitStructure.SPI_RxDmaEn = ENABLE;
SPI_InitStructure.SPI_TxDmaEn = ENABLE;
SPI_InitStructure.SPI_RxWaterlevel = 1 - 1;
SPI_InitStructure.SPI_TxWaterlevel = SPI_TX_FIFO_SIZE - 1;
备注
SPI使用DMA传输时,推荐配置SPI_TxWaterlevel为SPI_TX_FIFO_SIZE - MSize,SPI_RxWaterlevel为MSize - 1。
driver_tx_gdma_init
包含了对DMA TX的初始化:
使能PCC时钟。
使用GDMA通道2。
传输方向为内存到外设传输。
设置源端地址自增,目的端地址固定。
设置源端和目的端的MSize为1。
源端地址为
GDMA_Send_Buffer
,目的端地址为SPI0->SPI_DR
。配置GDMA TX总传输完成中断
GDMA_INT_Transfer
。
RCC_PeriphClockCmd(APBPeriph_GDMA, APBPeriph_GDMA_CLOCK, ENABLE);
...
GDMA_InitStruct.GDMA_ChannelNum = GDMA_TX_CHANNEL_NUM;
GDMA_InitStruct.GDMA_DIR = GDMA_DIR_MemoryToPeripheral;
GDMA_InitStruct.GDMA_SourceInc = DMA_SourceInc_Inc;
GDMA_InitStruct.GDMA_DestinationInc = DMA_DestinationInc_Fix;
GDMA_InitStruct.GDMA_SourceMsize = GDMA_Msize_1;
GDMA_InitStruct.GDMA_DestinationMsize = GDMA_Msize_1;
...
GDMA_InitStruct.GDMA_SourceAddr = (uint32_t)GDMA_Send_Buffer;
GDMA_InitStruct.GDMA_DestinationAddr = (uint32_t)SPI0->SPI_DR;
...
GDMA_INTConfig(GDMA_TX_CHANNEL_NUM, GDMA_INT_Transfer, ENABLE);
...
driver_rx_gdma_init
包含了对DMA TX的初始化:
使能PCC时钟。
使用GDMA通道4。
传输方向为外设到内存传输。
设置源端地址固定,目的端地址自增。
设置源端和目的端的MSize为1。
源端地址为
SPI0->SPI_DR
,目的端地址为GDMA_Recv_Buffer
。配置GDMA RX总传输完成中断
GDMA_INT_Transfer
。
RCC_PeriphClockCmd(APBPeriph_GDMA, APBPeriph_GDMA_CLOCK, ENABLE);
...
GDMA_InitStruct.GDMA_ChannelNum = GDMA_RX_CHANNEL_NUM;
GDMA_InitStruct.GDMA_DIR = GDMA_DIR_PeripheralToMemory;
GDMA_InitStruct.GDMA_SourceInc = DMA_SourceInc_Fix;
GDMA_InitStruct.GDMA_DestinationInc = DMA_DestinationInc_Inc;
GDMA_InitStruct.GDMA_SourceMsize = GDMA_Msize_1;
GDMA_InitStruct.GDMA_DestinationMsize = GDMA_Msize_1;
...
GDMA_InitStruct.GDMA_SourceAddr = (uint32_t)SPI0->SPI_DR;
GDMA_InitStruct.GDMA_DestinationAddr = (uint32_t)GDMA_Recv_Buffer;
...
GDMA_INTConfig(GDMA_RX_CHANNEL_NUM, GDMA_INT_Transfer, ENABLE);
功能实现
执行
spi_flash_read_id()
,读取外部flash的JEDEC ID信息。初始化DMA接收和发送数组的内容信息。
设置要发送的指令和发送长度。根据W25Q128的硬件信息,若要读取JEDEC ID,需要通讯的总长度为4字节。因此设置DMA的BufferSize为4。
使能SPI,使能DMA接收通道,使能DMA发送通道。若使用软件模拟CS通讯,则在通讯开始之前,需要将CS线拉低。
等待DMA发送和接收通道完成后,可以将CS线拉高,继续后续步骤。
DBG_DIRECT("===================read_flash_id==================");
memset(GDMA_Send_Buffer, 0, sizeof(GDMA_Send_Buffer) / sizeof(GDMA_Send_Buffer[0]));
memset(GDMA_Recv_Buffer, 0, sizeof(GDMA_Recv_Buffer) / sizeof(GDMA_Recv_Buffer[0]));
GDMA_Send_Buffer[0] = SPI_FLASH_JEDEC_ID;
GDMA_SetBufferSize(GDMA_TX_CHANNEL, 4);
GDMA_SetBufferSize(GDMA_RX_CHANNEL, 4);
GDMA_INTConfig(GDMA_TX_CHANNEL_NUM, GDMA_INT_Transfer, ENABLE);
GDMA_INTConfig(GDMA_RX_CHANNEL_NUM, GDMA_INT_Transfer, ENABLE);
pull_cs_down(true);
SPI_Cmd(SPI0, ENABLE);
GDMA_Cmd(GDMA_RX_CHANNEL_NUM, ENABLE);
GDMA_Cmd(GDMA_TX_CHANNEL_NUM, ENABLE);
DBG_DIRECT("after dma cmd enable");
//wait for read id finish!
while ((isDMATxDone != true) || (isDMARxDone != true))
{
DBG_DIRECT("isDMATxDone = %d", isDMATxDone);
DBG_DIRECT("isDMARxDone = %d", isDMARxDone);
platform_delay_ms(1000);
}
isDMATxDone = false;
isDMARxDone = false;
pull_cs_down(false);
...
执行
spi_flash_sector_erase()
函数,将指定地址的数据进行擦除操作。在执行写操作之前,需要执行
spi_flash_write_enable()
对写操作进行使能。通过DMA将写使能指令发送至外部flash,并等待DMA传输完成。设置需要发送的数据内容和数据长度,使能DMA接收和发送通道,等待DMA传输完成。
...
GDMA_Send_Buffer[0] = SPI_FLASH_SECTOR_ERASE;
GDMA_Send_Buffer[1] = (vAddress >> 16) & 0xff;
GDMA_Send_Buffer[2] = (vAddress >> 8) & 0xff;
GDMA_Send_Buffer[3] = vAddress & 0xff;
GDMA_SetBufferSize(GDMA_TX_CHANNEL, 4);
GDMA_SetBufferSize(GDMA_RX_CHANNEL, 4);
...
执行
spi_flash_read()
函数,读取指定地址下的数据内容。执行
spi_flash_page_write()
函数,在指定地址下写入指定长度的数据。再次执行
spi_flash_read()
函数,读取指定地址下的数据内容。
void spi_dma_demo(void)
{
uint8_t write_data[100];
uint8_t read_data[105] = {0};
for (uint16_t i = 0; i < 100; i++)
{
write_data[i] = (i + 10) & 0xFF;
}
//read flash JEDEC id
spi_flash_read_id();
//sector erase
spi_flash_sector_erase(0x001000);
spi_flash_read(SPI_FLASH_FAST_READ, 0x001000, read_data, 100);
spi_flash_page_write(0x001000, write_data, 100);
spi_flash_read(SPI_FLASH_FAST_READ, 0x001000, read_data, 100);
}
当DMA TX通道搬运数据完成后触发
GDMA_INT_Transfer
中断,进入中断处理函数GDMA_TX_CHANNEL_HANDLER
:失能GDMA TX通道中断,打印进入中断信息,清除GDMA中断悬挂位。
将DMA TX完成的标志位置位true。
void GDMA_TX_CHANNEL_HANDLER(void)
{
GDMA_INTConfig(GDMA_TX_CHANNEL_NUM, GDMA_INT_Transfer, DISABLE);
GDMA_ClearINTPendingBit(GDMA_TX_CHANNEL_NUM, GDMA_INT_Transfer);
DBG_DIRECT("gdma tx data finish!");
isDMATxDone = true;
GDMA_Cmd(GDMA_TX_CHANNEL_NUM, DISABLE);
}
当DMA RX通道搬运数据完成后触发
GDMA_INT_Transfer
中断,进入中断处理函数GDMA_RX_CHANNEL_HANDLER
:失能GDMA RX通道中断,清除GDMA中断悬挂位。
打印DMA收到的数据长度和全部数据内容。
将DMA RX完成的标志位置位true。
void GDMA_RX_CHANNEL_HANDLER(void)
{
GDMA_INTConfig(GDMA_RX_CHANNEL_NUM, GDMA_INT_Transfer, DISABLE);
GDMA_ClearINTPendingBit(GDMA_RX_CHANNEL_NUM, GDMA_INT_Transfer);
DBG_DIRECT("gdma rx data finish!");
uint16_t data_len = GDMA_GetTransferLen(GDMA_RX_CHANNEL);
DBG_DIRECT("data_len = %d", data_len);
for (uint16_t i = 0; i < data_len; i++)
{
DBG_DIRECT("dma rx data[%d] = %x", i, GDMA_Recv_Buffer[i]);
}
isDMARxDone = true;
GDMA_Cmd(GDMA_RX_CHANNEL_NUM, DISABLE);
}
常见问题
当需要使用的SPI频率过高(大于10MHz)时,推荐使用DMA传输数据。 如果发现传输数据过程中出现CS线被拉高的情况,可以增大GDMA的MSize以提高DMA的传输效率,或使用软件模拟CS通信。