最近在项目中遇到了 通过SPI DMA方式读写W25Q256;
网上许多例程都是直接读取flash,读写会占用比较长的时间 ,如果这个SPI上还挂载了其他SPI 器件,如SPI显示屏,就需要通过开启DMA来提升速度了。
在这里记录一下我的调试过程:
一.详细CUBE 配置
1.基础设置,我用的是SPI6.
2.DMA设置,并设置中断优先级
3.SPI global interrupt不用开启
然后生成代码;
二,代码。
基础代码是正点原子的SPI flash 函数
基本读写函数没有开启DMA,因为只有单字节读写,所以不用开启DMA,后面两个就是DMA读写函数(hal库操作还是贼简单)
static inline void W25Q_SpiDmaStop()
{
while(__HAL_DMA_GET_FLAG(&hdma_spi2_tx,DMA_FLAG_TCIF3_7)) { asm(""); }
while (hspiflash.State != HAL_SPI_STATE_READY);
// W25Q_SpiLcdCsDisable();
}
uint8_t W25Q_SpiReadWriteByte(uint8_t TxData)
{
uint8_t Rxdata;
HAL_SPI_TransmitReceive(&hspiflash,&TxData,&Rxdata,1, 1000);
return Rxdata; //返回收到的数据
}
uint16_t W25Q_SpiWriteDMA(uint8_t *pTxData, uint16_t Size)
{
W25Q_SpiDmaStop();
// uint16_t retval = HAL_SPI_Transmit_DMA(&hspiflash,pTxData,Size);
uint16_t retval = HAL_SPI_TransmitReceive_DMA(&hspiflash,pTxData,pTxData,Size);
return retval;
}
/* 开始读数据,因为底层DMA缓冲区有限,必须分包读 */
uint16_t W25Q_SpiReadDMA( uint8_t *pRxData, uint32_t Size)
{
uint16_t rem;
uint32_t g_spiLen;
uint8_t g_spiRxBuf[SPI_BUFFER_SIZE];
uint8_t g_spiTxBuf[SPI_BUFFER_SIZE];
uint8_t retval = 0;
for (uint16_t i = 0; i <( Size / SPI_BUFFER_SIZE); i++)
{
g_spiLen = SPI_BUFFER_SIZE;
HAL_SPI_TransmitReceive_DMA(&hspiflash,g_spiTxBuf,g_spiRxBuf,g_spiLen);
memcpy(pRxData, g_spiRxBuf, SPI_BUFFER_SIZE);
pRxData += SPI_BUFFER_SIZE;
}
rem = Size % SPI_BUFFER_SIZE; /* 剩余字节 */
if (rem > 0)
{
g_spiLen = rem;
HAL_SPI_TransmitReceive_DMA(&hspiflash,g_spiTxBuf,g_spiRxBuf,g_spiLen);
memcpy(pRxData, g_spiRxBuf, rem);
}
return retval;
}
基本读写函数写好了,就只需要去改flash的块读写操作函数即可,中间有一个很容易忽略的问题在后面提出
//读取SPI FLASH
//在指定地址开始读取指定长度的数据
//pBuffer:数据存储区
//ReadAddr:开始读取的地址(24bit)
//NumByteToRead:要读取的字节数(最大65535)
void W25QXX_Read(u8* pBuffer,u32 ReadAddr,u32 NumByteToRead)
{
u16 i;
W25QXX_CS=0; //使能器件
SPI6_ReadWriteByte(W25X_ReadData); //发送读取命令
if(W25QXX_TYPE==W25Q256) //如果是W25Q256的话地址为4字节的,要发送最高8位
{
SPI6_ReadWriteByte((u8)((ReadAddr)>>24));
}
SPI6_ReadWriteByte((u8)((ReadAddr)>>16)); //发送24bit地址
SPI6_ReadWriteByte((u8)((ReadAddr)>>8));
SPI6_ReadWriteByte((u8)ReadAddr);
#ifdef SPI_USE_DMA
W25Q_SpiReadDMA(pBuffer,NumByteToRead);
W25Q_SpiCsDisable();
W25Q_SpiDmaStop(); //等待写入完成
#else
for(i=0;i<NumByteToRead;i++)
{
pBuffer[i]=SPI6_ReadWriteByte(0XFF); //循环读数
}
W25Q_SpiCsDisable();
#endif
}
//SPI在一页(0~65535)内写入少于256个字节的数据
//在指定地址开始写入最大256字节的数据
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!
void W25QXX_Write_Page(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)
{
u16 i;
u8* rxBuffer;
W25QXX_Write_Enable(); //SET WEL
W25Q_SpiCsEnable(); //使能器件
SPI6_ReadWriteByte(W25X_PageProgram); //发送写页命令
if(W25QXX_TYPE==W25Q256) //如果是W25Q256的话地址为4字节的,要发送最高8位
{
SPI6_ReadWriteByte((u8)((WriteAddr)>>24));
}
SPI6_ReadWriteByte((u8)((WriteAddr)>>16)); //发送24bit地址
SPI6_ReadWriteByte((u8)((WriteAddr)>>8));
SPI6_ReadWriteByte((u8)WriteAddr);
#ifdef SPI_USE_DMA
W25Q_SpiWriteDMA(pBuffer,NumByteToWrite);
W25Q_SpiDmaStop();
W25Q_SpiCsDisable(); //取消片选
#else
for(i=0;i<NumByteToWrite;i++)SPI6_ReadWriteByte(pBuffer[i]);//循环写数
W25Q_SpiCsDisable(); //取消片选
#endif
W25QXX_Wait_Busy(); //等待写入结束
}
上面通过宏定义区分是否使用DMA,其中需要注意的就是W25Q64
的片选信号在DMA方式时不能马上取消片选;因为SPI6_WriteDMA函数实际只是打开了DMA,数据并没有发送完成。
当然在这里等待写入完成也是个坑,暂时没想到怎么填,如果SPI上只挂载了flash,就不用等了,可以去干其他的事。所以只能在回调函数中去取消片选。
W25QXX_Wait_Busy(); //等待写入结束
添加回调函数
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
if(hspi== &hspi6)
{
W25QXX_CS=1;
}
}
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)
{
if(hspi== &hspi6)
{
W25QXX_CS=1;
}
}
到这里;DMA方式读写flash才算完成配置了。
总结如下:
1. 配置SPI 及对应Tx RX DMA;接收DMA要分包读取,一次性读取大量数据DMA缓存不够
2.改写flash块读写函数;
3.添加回调函数,取消片选; ->3.直接等待DMA传输完成就取消片选
速度测试:
写100K数据用时2s-3s,因为100K数据需要擦除32个扇区,
W25QXX_Erase_Sector扇区擦除命令就占了90ms,所以spi-flash最好不要实时存取数据,在开关机阶段操作存取保存的数据比较好