1053|6

174

帖子

0

资源

一粒金砂(中级)

【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文件头数据格式:

16.jpg

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文件:

17.jpg 18.jpg 7.gif

 

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的寿命:

7.bmp 6.bmp 9.bmp

15.jpg 20.jpg

写入的评测非常简单顺利,但是要从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次,这里我可以画一个简单的示意图:

19.jpg

读取函数如下:

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);
}

看看效果:

8.gif

可以看出来从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;
  } 
}

 


回复

174

帖子

0

资源

一粒金砂(中级)

二楼放出工程文件

STM32F769.zip (2.92 MB, 下载次数: 0)


回复

7588

帖子

20

资源

版主

这个板子的屏幕不错,H7好像也是这块屏,看你的代码简单改一下,弄成TF卡加载JPG文件就可以实现一个电子相册了。

点评

读取JPG文件之前有帖子写了,并不顺利  详情 回复 发表于 2020-9-8 09:53
个人签名虾扯蛋

回复

174

帖子

0

资源

一粒金砂(中级)

littleshrimp 发表于 2020-9-8 09:25 这个板子的屏幕不错,H7好像也是这块屏,看你的代码简单改一下,弄成TF卡加载JPG文件就可以实现一个电子相 ...

读取JPG文件之前有帖子写了,并不顺利


回复

2681

帖子

1

资源

五彩晶圆(初级)

图片不错。搞了这么多事情,楼主算是把F769摸透了

点评

一个月之前的帖子...  详情 回复 发表于 2020-9-8 16:58
个人签名人已离开,无事别找,找也找不到。

回复

174

帖子

0

资源

一粒金砂(中级)

freebsder 发表于 2020-9-8 16:03 图片不错。搞了这么多事情,楼主算是把F769摸透了

一个月之前的帖子...

点评

时时要鞭策自己向先进学习,别说一个月,就是一年以前的都要不断温习!  详情 回复 发表于 2020-9-9 13:34

回复

2681

帖子

1

资源

五彩晶圆(初级)

donatello1996 发表于 2020-9-8 16:58 一个月之前的帖子...

时时要鞭策自己向先进学习,别说一个月,就是一年以前的都要不断温习!

个人签名人已离开,无事别找,找也找不到。

回复
您需要登录后才可以回帖 登录 | 注册

关闭
站长推荐上一条 1/1 下一条

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区知春路23号集成电路设计园量子银座1305 电话:(010)82350740 邮编:100191

电子工程世界版权所有 京ICP证060456号 京ICP备10001474号 电信业务审批[2006]字第258号函 京公海网安备110108001534 Copyright © 2005-2020 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表