【STM32F769Discovery开发板试用】便捷的SD卡读取BMP文件&QSPI读写&SDRAM读写
[复制链接]
STM32F769Discovery板子外接了多种存储器,有SD卡,QSPI FLASH,SDRAM和EEPROM,属于掉电保存的大容量FLASH有SD卡和QSPI FLASH两种,EEPROM也可掉电保存但容量小,SDRAM不可掉电保存,本帖主要讲SD卡读取BMP文件和使用QSPI FLASH存储读写不带文件头的BMP数据,SDRAM和EEPROM会简单介绍,一笔带过。
先来说说SD卡读取BMP文件,这个基本上没有难度,F769的库就写好了直接从BMP文件获取文件头信息和数据并显示的函数,只要像之前第三帖那样搭建好SD卡的文件系统,成功挂载SD卡就能正常读取BMP文件了,不需要像JPEG文件那样要解码,甚至不需要考虑BMP文件的长宽尺寸,文件占用空间,文件头各项数据(前提是BMP文件的文件头格式遵循Windows标准BMP协议),方便快捷,只不过还是老问题,只能读取24位色或16位色的BMP文件,无法读取32位色的BMP文件。这种方式适合的实际项目是不考虑SD卡的成本和外壳占用位置的项目,更新BMP文件数据只需要将SD卡取出来,在电脑上操作即可,SD卡能放多少张BMP文件就能存取多少,不会被嵌入式主控的RAM和ROM空间所限制。在使用这个方式之前,先普及一下BMP文件头数据格式:
typedef struct tagBITMAPFILEHEADER
{
UINT16 bfType;
DWORD bfSize;
UINT16 bfReserved1;
UINT16 bfReserved2;
DWORD bfOffBits;
} BITMAPFILEHEADER;
BMP文件头格式的信息非常精炼,包含的信息有文件占用空间,图片的长和宽,数据部分偏移量,调色板,位色(16/24/32),之后就是数据部分了,简单的50多个字节,包含了文件读取的所有必要信息。CubeF7库里面已经写好了读取BMP文件并显示的函数:
uint32_t Storage_OpenReadFile(uint8_t *Address, const char* BmpName ,int *rsize)
{
uint32_t index = 0, i1 = 0;
uint32_t BmpAddress;
FIL F1;
int size;
if (f_open(&F1, (TCHAR const*)BmpName, FA_READ) != FR_OK)
{
return 1;
}
if (f_read (&F1, sector, 30, (UINT *)&BytesRead) != FR_OK)
{
return 2;
}
BmpAddress = (uint32_t)sector;
/* Read bitmap size */
size = *(uint16_t *) (BmpAddress + 2);
size |= (*(uint16_t *) (BmpAddress + 4)) << 16;
*rsize = size;
/* Get bitmap data address offset */
index = *(uint16_t *) (BmpAddress + 10);
index |= (*(uint16_t *) (BmpAddress + 12)) << 16;
f_close (&F1);
f_open (&F1, (TCHAR const*)BmpName, FA_READ);
do
{
if (size < 256*2)
{
i1 = size;
}
else
{
i1 = 256*2;
}
size -= i1;
f_read (&F1, sector, i1, (UINT *)&BytesRead);
for (index = 0; index < i1; index++)
{
*(volatile uint8_t*) (Address) = *(volatile uint8_t *)BmpAddress;
BmpAddress++;
Address++;
}
BmpAddress = (uint32_t)sector;
}
while (size > 0);
f_close (&F1);
return 0;
}
读取数据之后,当然就是显示了:
void BSP_LCD_DrawBitmap(uint32_t Xpos, uint32_t Ypos, uint8_t *pbmp)
{
uint32_t index = 0, width = 0, height = 0, bit_pixel = 0;
uint32_t Address;
uint32_t InputColorMode = 0;
/* Get bitmap data address offset */
index = pbmp[10] + (pbmp[11] << 8) + (pbmp[12] << 16) + (pbmp[13] << 24);
/* Read bitmap width */
width = pbmp[18] + (pbmp[19] << 8) + (pbmp[20] << 16) + (pbmp[21] << 24);
/* Read bitmap height */
height = pbmp[22] + (pbmp[23] << 8) + (pbmp[24] << 16) + (pbmp[25] << 24);
/* Read bit/pixel */
bit_pixel = pbmp[28] + (pbmp[29] << 8);
/* Set the address */
Address = hltdc_discovery.LayerCfg[ActiveLayer].FBStartAdress + (((BSP_LCD_GetXSize()*Ypos) + Xpos)*(4));
/* Get the layer pixel format */
if ((bit_pixel/8) == 4)
{
InputColorMode = DMA2D_INPUT_ARGB8888;
}
else if ((bit_pixel/8) == 2)
{
InputColorMode = DMA2D_INPUT_RGB565;
}
else
{
InputColorMode = DMA2D_INPUT_RGB888;
}
/* Bypass the bitmap header */
pbmp += (index + (width * (height - 1) * (bit_pixel/8)));
/* Convert picture to ARGB8888 pixel format */
for(index=0; index < height; index++)
{
/* Pixel format conversion */
LL_ConvertLineToARGB8888((uint32_t *)pbmp, (uint32_t *)Address, width, InputColorMode);
/* Increment the source and destination buffers */
Address+= (BSP_LCD_GetXSize()*4);
pbmp -= width*(bit_pixel/8);
}
}
真的是一条龙服务,简单的两个函数就完成了,官方的源代码有不合理的位置我已经修改过来了,比如f_open() f_read()这些操作,即使返回失败也不应用while(1)死循环来处理,然后就是修改了返回值,返回0成功,返回大于0的值为不同原因的失败,尽管这两个函数还有很大的改进空间,算法还是有许多不完善的地方,但简单读取SD卡BMP文件够用了。读取BMP文件的主循环代码:
while(1)
{
Storage_OpenReadFile(uwInternalBuffer,"/Media/2.bmp",&rsize);
BSP_LCD_DrawBitmap(0,0,uwInternalBuffer);
Storage_OpenReadFile(uwInternalBuffer,"/Media/3.bmp",&rsize);
BSP_LCD_DrawBitmap(0,0,uwInternalBuffer);
Storage_OpenReadFile(uwInternalBuffer,"/Media/4.bmp",&rsize);
BSP_LCD_DrawBitmap(0,0,uwInternalBuffer);
Storage_OpenReadFile(uwInternalBuffer,"/Media/5.bmp",&rsize);
BSP_LCD_DrawBitmap(0,0,uwInternalBuffer);
}
不过这里有一个地方是需要注意的,那就是uwInternalBuffer的地址,官方的代码是直接使用SDRAM中LTDC的两层图层的地址,任选一层,分别为layer0和layer1的两层数据空间,对这部分内存空间的写入,会直接作用到液晶屏显示上面:
uint8_t* uwInternalBuffer = (uint8_t *)(0xC0000000+800*480*4);
或
uint8_t* uwInternalBuffer = (uint8_t *)(0xC0000000+800*480*8);
这是有原因的,由于STM32F769的片内RAM空间中,可供用户调用的部分不足800*480*4=1536000(约等于1500KB)即缓存一帧BMP图所需要的空间(甚至800*480*3=1152000,约等于1100KB都不够),即无法直接定义一个大小为1152000的非常量数组,会直接报错:
unsigned char temp[1152000];
因此官方代码是直接使用(0xC0000000+800*480*4)~(0xC0000000+800*480*8)之间的3MB左右的空间,定义一个指针指向这块空间,将从SD卡中读取出来的缓存数据存放到此处。这里可能有坛友要问了,那么SDRAM其余几十MB的空间哪里去了?为什么不用?别问,问就是ST官方代码有漏洞,后面我尝试过,也不成功,之后再探讨。
读取BMP文件刷屏效果如下,非常简单便捷,再次声明只能读取16位或者24位色的BMP文件:
SD卡读取BMP文件显示的测评完成了,之后就是读写QSPI FLASH了,同样也不难,官方都把大部分的代码都封装好了,只需要用到官方的三个函数,BSP_QSPI_Erase_Block(),BSP_QSPI_Write(),BSP_QSPI_Read(),QSPI FLASH的块/扇区在写入之前需要擦除:
uint8_t BSP_QSPI_Erase_Block(uint32_t BlockAddress)
{
QSPI_CommandTypeDef s_command;
/* Initialize the erase command */
s_command.InstructionMode = QSPI_INSTRUCTION_4_LINES;
s_command.Instruction = SUBSECTOR_ERASE_4_BYTE_ADDR_CMD;
s_command.AddressMode = QSPI_ADDRESS_4_LINES;
s_command.AddressSize = QSPI_ADDRESS_32_BITS;
s_command.Address = BlockAddress;
s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
s_command.DataMode = QSPI_DATA_NONE;
s_command.DummyCycles = 0;
s_command.DdrMode = QSPI_DDR_MODE_DISABLE;
s_command.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY;
s_command.SIOOMode = QSPI_SIOO_INST_EVERY_CMD;
/* Enable write operations */
if (QSPI_WriteEnable(&QSPIHandle) != QSPI_OK)
{
return QSPI_ERROR;
}
/* Send the command */
if (HAL_QSPI_Command(&QSPIHandle, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
return QSPI_ERROR;
}
/* Configure automatic polling mode to wait for end of erase */
if (QSPI_AutoPollingMemReady(&QSPIHandle, MX25L512_SUBSECTOR_ERASE_MAX_TIME) != QSPI_OK)
{
return QSPI_ERROR;
}
return QSPI_OK;
}
uint8_t BSP_QSPI_Write(uint8_t* pData, uint32_t WriteAddr, uint32_t Size)
{
QSPI_CommandTypeDef s_command;
uint32_t end_addr, current_size, current_addr;
/* Calculation of the size between the write address and the end of the page */
current_size = MX25L512_PAGE_SIZE - (WriteAddr % MX25L512_PAGE_SIZE);
/* Check if the size of the data is less than the remaining place in the page */
if (current_size > Size)
{
current_size = Size;
}
/* Initialize the address variables */
current_addr = WriteAddr;
end_addr = WriteAddr + Size;
/* Initialize the program command */
s_command.InstructionMode = QSPI_INSTRUCTION_4_LINES;
s_command.Instruction = QPI_PAGE_PROG_4_BYTE_ADDR_CMD;
s_command.AddressMode = QSPI_ADDRESS_4_LINES;
s_command.AddressSize = QSPI_ADDRESS_32_BITS;
s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
s_command.DataMode = QSPI_DATA_4_LINES;
s_command.DummyCycles = 0;
s_command.DdrMode = QSPI_DDR_MODE_DISABLE;
s_command.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY;
s_command.SIOOMode = QSPI_SIOO_INST_EVERY_CMD;
/* Perform the write page by page */
do
{
s_command.Address = current_addr;
s_command.NbData = current_size;
/* Enable write operations */
if (QSPI_WriteEnable(&QSPIHandle) != QSPI_OK)
{
return QSPI_ERROR;
}
/* Configure the command */
if (HAL_QSPI_Command(&QSPIHandle, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
return QSPI_ERROR;
}
/* Transmission of the data */
if (HAL_QSPI_Transmit(&QSPIHandle, pData, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
return QSPI_ERROR;
}
/* Configure automatic polling mode to wait for end of program */
if (QSPI_AutoPollingMemReady(&QSPIHandle, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != QSPI_OK)
{
return QSPI_ERROR;
}
/* Update the address and size variables for next page programming */
current_addr += current_size;
pData += current_size;
current_size = ((current_addr + MX25L512_PAGE_SIZE) > end_addr) ? (end_addr - current_addr) : MX25L512_PAGE_SIZE;
} while (current_addr < end_addr);
return QSPI_OK;
}
uint8_t BSP_QSPI_Read(uint8_t* pData, uint32_t ReadAddr, uint32_t Size)
{
QSPI_CommandTypeDef s_command;
/* Initialize the read command */
s_command.InstructionMode = QSPI_INSTRUCTION_4_LINES;
s_command.Instruction = QPI_READ_4_BYTE_ADDR_CMD;
s_command.AddressMode = QSPI_ADDRESS_4_LINES;
s_command.AddressSize = QSPI_ADDRESS_32_BITS;
s_command.Address = ReadAddr;
s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
s_command.DataMode = QSPI_DATA_4_LINES;
s_command.DummyCycles = MX25L512_DUMMY_CYCLES_READ_QUAD_IO;
s_command.NbData = Size;
s_command.DdrMode = QSPI_DDR_MODE_DISABLE;
s_command.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY;
s_command.SIOOMode = QSPI_SIOO_INST_EVERY_CMD;
/* Configure the command */
if (HAL_QSPI_Command(&QSPIHandle, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
return QSPI_ERROR;
}
/* Set S# timing for Read command */
MODIFY_REG(QSPIHandle.Instance->DCR, QUADSPI_DCR_CSHT, QSPI_CS_HIGH_TIME_1_CYCLE);
/* Reception of the data */
if (HAL_QSPI_Receive(&QSPIHandle, pData, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
return QSPI_ERROR;
}
/* Restore S# timing for nonRead commands */
MODIFY_REG(QSPIHandle.Instance->DCR, QUADSPI_DCR_CSHT, QSPI_CS_HIGH_TIME_4_CYCLE);
return QSPI_OK;
}
擦除的单位是块或者扇区,一个块长度为512字节,如果要擦除多个连续块,则起始地址每次加512,比如要擦除0~1023这个空间的所对应的两个块:
BSP_QSPI_Erase_Block(0);
BSP_QSPI_Erase_Block(512);
依据此原理,要存储BMP图片的数据到QSPI FLASH里面就非常简单了,我自己写了一个便捷的函数,可以根据写入BMP图片数据的长/宽/位色参数进行调整:
void BSP_QSPI_Store_BMP_Buffer(int w,int h,int bit_pixel,unsigned char buff[],int start_addr)
{
int i;
for(i=0;i<w*h*bit_pixel/8/512;i++)
{
BSP_QSPI_Erase_Block(i*512+start_addr);
}
BSP_QSPI_Write((uint8_t*)buff,start_addr,w*h*bit_pixel/8);
}
写入一张24位800*480图片的BMP数据(大小为1152000字节)到起始地址1200000的操作如下:
const unsigned char image1[1152000]={...};
BSP_QSPI_Store_BMP_Buffer(800,480,24,image1,1200000);
这里我对三张图片的BMP数据进行写入,分别存放于起始地址0,起始地址1200000,起始地址2400000,写入一张800*480*3的图片BMP数组到QSPI FLASH空间的大致用时是一分多钟,同一个地址写入一次之后,后面就可以一直读了,能读多少次看QSPI FLASH的寿命:
写入的评测非常简单顺利,但是要从QSPI FLASH中读取的话就有点波折了,还是老问题,STM32F76不支持整张800*480*3图片这么大的缓存空间,如果想要缓存这么大的图片,要么把图片缓存到上述的外扩SDRAM空间地址(uint8_t *)(0xC0000000+800*480*4)中,要么切片缓存显示,一般而言为了不在缓存过程中影响液晶屏显示,都是采用切片缓存显示的方法,也就是将液晶屏的480行分成多个行数相同的组,每组的形状都是矩形,依次对每个组采用DMA2D方式进行局部矩形刷图,那既然要切片缓存显示,那就要开辟一个全局数组lcd_buf[]用于每一组数据的缓存,在全局数组lcd_buf[]缓存空间,每一组显示的数据量,QSPI读取次数之间找到一个平衡点,每一组的行数越大,QSPI读取次数就越少,全局lcd_buf[]缓存空间=每一组行数*800*3,比如说我这边定义每一组是16行,即800*3*16=38400,即开辟一个长度为38400的全局缓存数组unsigned char lcd_buf[38400],这个缓存数组用于每次从QSPI FLASH读取长度为38400的数据并使用DMA2D搬运到SDRAM地址空间中,这个搬运函数是从下至上搬运的,如果每组是16行,那么一共就要搬运30次,这里我可以画一个简单的示意图:
读取函数如下:
void BSP_QSPI_Load_Show_BMP_Buffer(int start_addr)
{
int i,j;
for(j=0;j<30;j++)
{
BSP_QSPI_Read(lcd_buf,j*38400+start_addr,38400);
BSP_LCD_DrawBuffer(0,464-j*16,800,16,24,lcd_buf);
}
}
while(1)
{
BSP_QSPI_Load_Show_BMP_Buffer(0);
BSP_QSPI_Load_Show_BMP_Buffer(1200000);
BSP_QSPI_Load_Show_BMP_Buffer(2400000);
}
看看效果:
可以看出来从QSPI FLASH读取BMP数据和从SD卡读取的速度差不多,由于QSPI FLASH是切片读取缓存,所以会有从下至上的刷屏效果。
然后是SDRAM读写函数,不知道是什么原因,一次能连续操作的SDRAM空间极其有限,如果超过了空间限制的话,读写就会有问题,官方的代码是读写32位长度单位的空间,实际上可以以8位长度为单位进行,只需要改成HAL_SDRAM_Write_8b():
uint8_t BSP_SDRAM_WriteBytes(uint32_t uwStartAddress, uint8_t *pData, uint32_t uwDataSize)
{
if(HAL_SDRAM_Write_8b(&sdramHandle, (uint32_t *)uwStartAddress, pData, uwDataSize) != HAL_OK)
{
return SDRAM_ERROR;
}
else
{
return SDRAM_OK;
}
}
uint8_t BSP_SDRAM_ReadBytes(uint32_t uwStartAddress, uint8_t *pData, uint32_t uwDataSize)
{
if(HAL_SDRAM_Read_8b(&sdramHandle, (uint32_t *)uwStartAddress, pData, uwDataSize) != HAL_OK)
{
return SDRAM_ERROR;
}
else
{
return SDRAM_OK;
}
}
|