麦斯科技 · 2021年04月14日

使用闪存读取和写入数据

概述

本教程演示了如何使用Menta OS提供的BlockDevice API使用Portenta H7的板载闪存来读取和写入数据。由于内部存储器的大小有限,我们还将介绍如何将数据保存到QSPI闪存中。

您将学习

  • 使用Mbed的Flash应用程序内编程接口访问Portenta的内部闪存
  • 使用Mbed的Flash应用内编程接口访问Portenta的QSPI闪存
  • 读取内存的特征

所需的硬件和软件

  • Wonders H7开发板(https://store.arduino.cc/port...
  • USB-C电缆(从USB-A到USB-C或从USB-C到USB-C)
  • Arduino IDE 1.8.10+或Arduino Pro IDE 0.0.4+或Arduino CLI 0.13.0+

用于闪存的Mbed OS API

Portenta的核心基于Mbed操作系统,从而允许使用Mbed OS直接公开的API集成Arduino API。

Mbed OS具有丰富的API,可用于管理不同介质上的存储,范围从微控制器的小型内部闪存到具有大数据存储空间的外部SecureDigital卡。

在本教程中,我们将在闪存中永久保存一个值。即使重启板后,也可以访问该值。我们将使用FlashIAPBlockDevice API从闪存块中检索一些信息,并在内存的可用空间内创建一个块设备对象。如果是内部存储器,则是将草图上载到板上后剩下的空间。

请注意闪存的读/写限制:闪存的读/写周期有限。典型的闪存可以在开始“磨损”并开始失去保留数据的能力之前,对同一块执行大约10000个写周期。如果不正确地使用此示例和所描述的API,可能会使您的开发板变得毫无用处。

块设备块

可以通过块设备API访问闪存块。它们是字节可寻址的,但以块为单位进行操作。有三种类型的块可用于不同的块设备操作:读取块,擦除块和程序块。建议的数据编程步骤是首先擦除一个块,然后以程序块大小为单位对其进行编程。擦除,编程和读取块的大小可能不相同,但必须彼此成倍。请记住,在使用数据编程之前,已擦除块的状态是不确定的。

微信图片_20210413224610.png

块大小如何相互关联的概念视图

编程内部闪存

1.创建程序的结构

在开始之前,请务必牢记上述闪存读/写限制!因此,此方法仅应用于一次读写操作,例如读取中的用户设置setup()。将其用于不断更新的值(例如传感器数据)不是一个好主意。

考虑到这一点,是时候创建一个草图来对Portenta进行编程了。创建新草图并为其指定合适的名称(在本例中为FlashStorage.ino)之后,我们需要再创建一个供草图使用的文件,称为FlashIAPLimits.h,我们将使用它来定义一些辅助函数。这使我们以后可以将辅助文件重用于其他草图。

2.助手功能

在FlashIAPLimits.h文件内,我们首先包括必要的库并定义名称空间。

// Ensures that this file is only included once
#pragma once 

#include <Arduino.h>
#include <FlashIAP.h>
#include <FlashIAPBlockDevice.h>

using namespace mbed;

之后,我们创建一个结构,该结构以后将用于保存存储的属性。

// An helper struct for FlashIAP limits
struct FlashIAPLimits {
  size_t flash_size;
  uint32_t start_address;
  uint32_t available_size;
};

帮助程序文件的最后一部分包括getFlashIAPLimits()用于计算闪存大小以及可用内存的大小和起始地址的函数。这是通过Mbed的FlashIAP API完成的。它查找存储在微控制器ROM中的草图之后的第一个扇区的地址:FLASHIAP_APP_ROM_END_ADDR并使用FlashIAP通过来计算闪存的大小flash.get_flash_size()。可以使用相同的API确定其他参数。

// Get the actual start address and available size for the FlashIAP Block Device
// considering the space already occupied by the sketch (firmware).
FlashIAPLimits getFlashIAPLimits()
{
  // Alignment lambdas
  auto align_down = [](uint64_t val, uint64_t size) {
    return (((val) / size)) * size;
  };
  auto align_up = [](uint32_t val, uint32_t size) {
    return (((val - 1) / size) + 1) * size;
  };

  size_t flash_size;
  uint32_t flash_start_address;
  uint32_t start_address;
  FlashIAP flash;

  auto result = flash.init();
  if (result != 0)
    return { };

  // Find the start of first sector after text area
  int sector_size = flash.get_sector_size(FLASHIAP_APP_ROM_END_ADDR);
  start_address = align_up(FLASHIAP_APP_ROM_END_ADDR, sector_size);
  flash_start_address = flash.get_flash_start();
  flash_size = flash.get_flash_size();

  result = flash.deinit();

  int available_size = flash_start_address + flash_size - start_address;
  if (available_size % (sector_size * 2)) {
    available_size = align_down(available_size, sector_size * 2);
  }

  return { flash_size, start_address, available_size };
}

3.读写数据

回到该FlashStorage.ino文件,还需要包含更多文件,以实现对Flash的读写功能。该FlashIAPBlockDevice.hAPI将用于在内存的空白部分中创建块设备。另外,我们包括帮助程序文件FlashIAPLimits.h,可以访问我们刚创建的地址和大小计算功能。我们还引用mbed命名空间以提高可读性。

#include <FlashIAPBlockDevice.h>
#include "FlashIAPLimits.h"

using namespace mbed;

该setup()函数将首先等待,直到建立串行连接,然后再馈入随机数生成器,该随机数生成器将在本教程的稍后部分使用,以便在每次启动设备时在闪存中写入随机数。

void setup() {
  Serial.begin(115200);
  while (!Serial);

  Serial.println("FlashIAPBlockDevice Test");
  Serial.println("------------------------");  

  // Feed the random number generator for later content generation
  randomSeed(analogRead(0));

接下来FlashIAPLimits.h,将调用文件中定义的帮助器函数以计算内存属性,然后使用该FlashIAPBlockDevice.h库来创建块设备。

// Get limits of the the internal flash of the microcontroller
auto [flashSize, startAddress, iapSize] = getFlashIAPLimits();

Serial.print("Flash Size: ");
Serial.print(flashSize / 1024.0 / 1024.0);
Serial.println(" MB");
Serial.print("FlashIAP Start Address: 0x");
Serial.println(startAddress, HEX);
Serial.print("FlashIAP Size: ");
Serial.print(iapSize / 1024.0 / 1024.0);
Serial.println(" MB");

// Create a block device on the available space of the flash
FlashIAPBlockDevice blockDevice(startAddress, iapSize);

在使用块设备之前,第一步是使用初始化它blockDevice.init()。初始化后,它可以提供用于对闪存进行编程的块的大小。就读写闪存块而言,可读块的大小(以字节为单位),可编程块(始终是读取大小的倍数)和可擦除块(始终是可编程块的倍数)之间存在区别。。

直接在闪存中进行读写操作时,我们始终需要分配一个缓冲区,缓冲区的大小应为程序块大小的倍数。所需程序块的数量可以通过将数据大小除以程序块大小来确定。最终的缓冲区大小等于程序块的数量乘以程序块的大小。

// Initialize the Flash IAP block device and print the memory layout
blockDevice.init();

const auto eraseBlockSize = blockDevice.get_erase_size();
const auto programBlockSize = blockDevice.get_program_size();

Serial.println("Block device size: " + String((unsigned int) blockDevice.size() / 1024.0 / 1024.0) + " MB");
Serial.println("Readable block size: " + String((unsigned int) blockDevice.get_read_size())  + " bytes");
Serial.println("Programmable block size: " + String((unsigned int) programBlockSize) + " bytes");
Serial.println("Erasable block size: " + String((unsigned int) eraseBlockSize / 1024) + " KB");

String newMessage = "Random number: " + String(random(1024));

// Calculate the amount of bytes needed to store the message
// This has to be a multiple of the program block size
const auto messageSize = newMessage.length() + 1; // C String takes 1 byte for NULL termination
const unsigned int requiredEraseBlocks = ceil(messageSize / (float)  eraseBlockSize);
const unsigned int requiredProgramBlocks = ceil(messageSize / (float)  programBlockSize);
const auto dataSize = requiredProgramBlocks * programBlockSize;  
char buffer[dataSize] {};

在setup()函数的最后一部分,我们现在可以使用块设备读取和写入数据。首先,使用缓冲区读取上一次执行中存储的内容,然后擦除存储器并使用新内容对其进行重新编程。在读写过程结束时,需要使用再次取消对块设备的初始化blockDevice.deinit()。

// Read back what was stored at previous execution
Serial.println("Reading previous message...");
blockDevice.read(buffer, 0, dataSize);
Serial.println(buffer);

// Erase a block starting at the offset 0 relative
// to the block device start address
blockDevice.erase(0, requiredEraseBlocks * eraseBlockSize);

// Write an updated message to the first block
Serial.println("Writing new message...");
Serial.println(newMessage);  
blockDevice.program(newMessage.c_str(), 0, dataSize);

// Deinitialize the device
blockDevice.deinit();
Serial.println("Done.");

最后loop(),考虑到应尽量减少Flash读写过程,此草图的功能将保留为空。

4.上传草图
下面是本教程的完整草图,包括主草图和FlashIAPLimits.h帮助文件,将它们都上传到您的Portenta H7进行试用。

FlashIAPLimits.h

Helper functions for calculating FlashIAP block device limits
**/

// Ensures that this file is only included once
#pragma once 

#include <Arduino.h>
#include <FlashIAP.h>
#include <FlashIAPBlockDevice.h>

using namespace mbed;

// A helper struct for FlashIAP limits
struct FlashIAPLimits {
  size_t flash_size;
  uint32_t start_address;
  uint32_t available_size;
};

// Get the actual start address and available size for the FlashIAP Block Device
// considering the space already occupied by the sketch (firmware).
FlashIAPLimits getFlashIAPLimits()
{
  // Alignment lambdas
  auto align_down = [](uint64_t val, uint64_t size) {
    return (((val) / size)) * size;
  };
  auto align_up = [](uint32_t val, uint32_t size) {
    return (((val - 1) / size) + 1) * size;
  };

  size_t flash_size;
  uint32_t flash_start_address;
  uint32_t start_address;
  FlashIAP flash;

  auto result = flash.init();
  if (result != 0)
    return { };

  // Find the start of first sector after text area
  int sector_size = flash.get_sector_size(FLASHIAP_APP_ROM_END_ADDR);
  start_address = align_up(FLASHIAP_APP_ROM_END_ADDR, sector_size);
  flash_start_address = flash.get_flash_start();
  flash_size = flash.get_flash_size();

  result = flash.deinit();

  int available_size = flash_start_address + flash_size - start_address;
  if (available_size % (sector_size * 2)) {
    available_size = align_down(available_size, sector_size * 2);
  }

  return { flash_size, start_address, available_size };
}

FlashStorage.ino



#include <FlashIAPBlockDevice.h>
#include "FlashIAPLimits.h"

using namespace mbed;

void setup() {
  Serial.begin(115200);
  while (!Serial);

  Serial.println("FlashIAPBlockDevice Test");
  Serial.println("------------------------");  

  // Feed the random number generator for later content generation
  randomSeed(analogRead(0));

  // Get limits of the the internal flash of the microcontroller
  auto [flashSize, startAddress, iapSize] = getFlashIAPLimits();

  Serial.print("Flash Size: ");
  Serial.print(flashSize / 1024.0 / 1024.0);
  Serial.println(" MB");
  Serial.print("FlashIAP Start Address: 0x");
  Serial.println(startAddress, HEX);
  Serial.print("FlashIAP Size: ");
  Serial.print(iapSize / 1024.0 / 1024.0);
  Serial.println(" MB");

  // Create a block device on the available space of the flash
  FlashIAPBlockDevice blockDevice(startAddress, iapSize);

  // Initialize the Flash IAP block device and print the memory layout
  blockDevice.init();

  const auto eraseBlockSize = blockDevice.get_erase_size();
  const auto programBlockSize = blockDevice.get_program_size();

  Serial.println("Block device size: " + String((unsigned int) blockDevice.size() / 1024.0 / 1024.0) + " MB");
  Serial.println("Readable block size: " + String((unsigned int) blockDevice.get_read_size())  + " bytes");
  Serial.println("Programmable block size: " + String((unsigned int) programBlockSize) + " bytes");
  Serial.println("Erasable block size: " + String((unsigned int) eraseBlockSize / 1024) + " KB");

  String newMessage = "Random number: " + String(random(1024));

  // Calculate the amount of bytes needed to store the message
  // This has to be a multiple of the program block size
  const auto messageSize = newMessage.length() + 1; // C String takes 1 byte for NULL termination
  const unsigned int requiredEraseBlocks = ceil(messageSize / (float)  eraseBlockSize);
  const unsigned int requiredProgramBlocks = ceil(messageSize / (float)  programBlockSize);
  const auto dataSize = requiredProgramBlocks * programBlockSize;  
  char buffer[dataSize] {};

  // Read back what was stored at previous execution
  Serial.println("Reading previous message...");
  blockDevice.read(buffer, 0, dataSize);
  Serial.println(buffer);

  // Erase a block starting at the offset 0 relative
  // to the block device start address
  blockDevice.erase(0, requiredEraseBlocks * eraseBlockSize);

  // Write an updated message to the first block
  Serial.println("Writing new message...");
  Serial.println(newMessage);  
  blockDevice.program(newMessage.c_str(), 0, dataSize);

  // Deinitialize the device
  blockDevice.deinit();
  Serial.println("Done.");
}

void loop() {}

5.结果

上载草图后,打开串行监视器以开始Flash读写过程。首次启动脚本时,将随机填充块设备。现在尝试重置或断开Portenta并重新连接。在上一次执行中,您应该会看到一条消息,其中随机数已写入闪存。

请注意,如果板子复位或断开连接,则写入闪存的值将保持不变。但是,一旦将新的草图上传到Portenta,闪存将被重新编程,并且可能会覆盖存储在闪存中的数据。

编程QSPI Flash
内部闪存的一个问题是它的大小有限并且擦除块很大。这为您的草图留出了很小的空间,您可能会很快遇到更复杂的应用程序的问题。因此,我们可以使用外部QSPI闪存,该闪存具有足够的空间来存储数据。为此,需要对块设备进行不同的初始化,但是其余的草图保持不变。要初始化设备,我们使用QSPIFBlockDevice类,该类是基于NOR的QSPI Flash设备的块设备驱动程序。

#define BLOCK_DEVICE_SIZE 1024 * 8 // 8 KB
#define PARTITION_TYPE 0x0B // FAT 32

// Create a block device on the available space of the flash
QSPIFBlockDevice root(PD_11, PD_12, PF_7, PD_13,  PF_10, PG_6, QSPIF_POLARITY_MODE_1, 40000000);
MBRBlockDevice blockDevice(&root, 1);  

// Initialize the Flash IAP block device and print the memory layout
if(blockDevice.init() != 0 || blockDevice.size() != BLOCK_DEVICE_SIZE) {    
  Serial.println("Partitioning block device...");
  blockDevice.deinit();
  // Allocate a FAT 32 partition
  MBRBlockDevice::partition(&root, 1, PARTITION_TYPE, 0, BLOCK_DEVICE_SIZE);
  blockDevice.init();
}

虽然可以直接使用QSPI块设备的内存,但最好使用分区表,因为QSPI存储中还填充了其他数据,例如WiFi固件。为此,我们使用MBRBlockDevice类并分配一个8 KB的分区,该分区可用于读取和写入数据。草图的完整QSPI版本如下:

#include "QSPIFBlockDevice.h"
#include "MBRBlockDevice.h"

using namespace mbed;

#define BLOCK_DEVICE_SIZE 1024 * 8 // 8 KB
#define PARTITION_TYPE 0x0B // FAT 32

void setup() {
  Serial.begin(115200);
  while (!Serial);

  Serial.println("QSPI Block Device Test");
  Serial.println("------------------------");  

  // Feed the random number generator for later content generation
  randomSeed(analogRead(0));

  // Create a block device on the available space of the flash
  QSPIFBlockDevice root(PD_11, PD_12, PF_7, PD_13,  PF_10, PG_6, QSPIF_POLARITY_MODE_1, 40000000);
  MBRBlockDevice blockDevice(&root, 1);  

  // Initialize the Flash IAP block device and print the memory layout
  if(blockDevice.init() != 0 || blockDevice.size() != BLOCK_DEVICE_SIZE) {    
    Serial.println("Partitioning block device...");
    blockDevice.deinit();
    // Allocate a FAT 32 partition
    MBRBlockDevice::partition(&root, 1, PARTITION_TYPE, 0, BLOCK_DEVICE_SIZE);
    blockDevice.init();
  }

  const auto eraseBlockSize = blockDevice.get_erase_size();
  const auto programBlockSize = blockDevice.get_program_size();

  Serial.println("Block device size: " + String((unsigned int) blockDevice.size() / 1024) + " KB");  
  Serial.println("Readable block size: " + String((unsigned int) blockDevice.get_read_size())  + " bytes");
  Serial.println("Programmable block size: " + String((unsigned int) programBlockSize) + " bytes");
  Serial.println("Erasable block size: " + String((unsigned int) eraseBlockSize / 1024) + " KB");

  String newMessage = "Random number: " + String(random(1024));

  // Calculate the amount of bytes needed to store the message
  // This has to be a multiple of the program block size
  const auto messageSize = newMessage.length() + 1; // C String takes 1 byte for NULL termination
  const unsigned int requiredEraseBlocks = ceil(messageSize / (float)  eraseBlockSize);
  const unsigned int requiredBlocks = ceil(messageSize / (float)  programBlockSize);
  const auto dataSize = requiredBlocks * programBlockSize;  
  char buffer[dataSize] {};  

  // Read back what was stored at previous execution  
  Serial.println("Reading previous message...");
  blockDevice.read(buffer, 0, dataSize);
  Serial.println(buffer);

  // Erase a block starting at the offset 0 relative
  // to the block device start address
  blockDevice.erase(0, requiredEraseBlocks * eraseBlockSize);

  // Write an updated message to the first block
  Serial.println("Writing new message...");
  Serial.println(newMessage);  
  blockDevice.program(newMessage.c_str(), 0, dataSize);

  // Deinitialize the device
  blockDevice.deinit();
  Serial.println("Done.");
}

void loop() {}

结论

我们已经了解了如何使用微控制器闪存中的可用空间来读取和保存自定义数据。不建议将微控制器的闪存用作数据密集型应用程序的主要存储。它更适合偶尔执行一次的读/写操作,例如存储和检索应用程序配置或持久性参数。

下一步

既然您已经知道如何使用块设备来执行读写闪存,那么您可以阅读下一篇有关如何使用TDBStore API在闪存中创建键值存储的教程。

作者: Giampaolo Mancini,塞巴斯蒂安·罗梅罗
评论: Lenard George,Jose Garcia [2021-01-28]
最新修订:塞巴斯蒂安·罗梅罗[2021-03-25]

推荐阅读
关注数
5759
内容数
525
定期发布Arm相关软件信息,微信公众号 ArmSWDevs,欢迎关注~
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息