Reading and Writing Flash Memory
This tutorial demonstrates how to use the on-board Flash memory of the Portenta H7 to read and write data using the BlockDevice API provided by Mbed OS.
Overview
This tutorial demonstrates how to use the on-board Flash memory of the Portenta H7 to read and write data using the BlockDevice API provided by Mbed OS. As the internal memory is limited in size, we will also take a look at saving data to the QSPI Flash memory.
Goals
- Accessing the Portenta's internal Flash memory using Mbed's Flash In-Application Programming Interface
- Accessing the Portenta's QSPI Flash memory using Mbed's Flash In-Application Programming Interface
- Reading the memory's characteristics
Required Hardware and Software
- Portenta H7 (ABX00042) or Portenta H7 Lite Connected (ABX00046)
- USB-C® cable (either USB-A to USB-C® or USB-C® to USB-C®)
- Arduino IDE 1.8.10+ or Arduino Pro IDE 0.0.4+ or Arduino CLI 0.13.0+
Mbed OS APIs for Flash Storage
Portenta's core is based on the Mbed operating system, allowing for Arduino APIs to be integrated using APIs exposed directly by Mbed OS.
Mbed OS has a rich API for managing storage on different mediums, ranging from the small internal Flash memory of a microcontroller to external SecureDigital cards with large data storage space.
In this tutorial, you are going to save a value persistently inside the Flash memory. That allows to access that value even after rebooting the board. You will retrieve some information from a Flash block by using the FlashIAPBlockDevice API and create a block device object within the available space of the memory. In case of the internal memory, it corresponds to the space which is left after uploading a sketch to the board.
Be aware of the Flash r/w limits: Flash memories have a limited amount of read/write cycles. Typical Flash memories can perform about 10000 writes cycles to the same block before starting to "wear out" and begin to lose the ability to retain data. You can render your board useless with improper use of this example and described APIs.
Block Device Blocks
Blocks of Flash memory can be accessed through the block device APIs. They are byte addressable but operate in units of blocks. There are three types of blocks for the different block device operations: read blocks, erase blocks and program blocks. The recommended procedure for programming data is to first erase a block and then programming it in units of the program block size. The sizes of the erase, program and read blocks may not be the same, but they must be multiples of each another. Keep in mind that the state of an erased block is undefined until you program it with data.
Programming the Internal Flash
1. Create the Structure of the Program
Before we start it's important to keep the above mentioned Flash r/w limits in mind! Therefore this method should only be used for once-in-a-while read and write operations, such as reading a user setting in the
setup()
. It is not a good idea to use it for constantly updated values such as sensor data.Having this in mind, it is time to create a sketch to program the Portenta. After creating new sketch and giving it a fitting name (in this case
FlashStorage.ino
), you need to create one more file to be used by the sketch, called FlashIAPLimits.h
, that you will use to define some helper functions. This allows you to reuse the helper file later for other sketches.2. The Helper Functions
Within the
FlashIAPLimits.h
file, you can start by including necessary libraries and defining the namespace. 1// Ensures that this file is only included once2#pragma once 3
4#include <Arduino.h>5#include <FlashIAP.h>6#include <FlashIAPBlockDevice.h>7
8using namespace mbed;
After that, you can create a struct which will later be used to save the storage's properties.
1// An helper struct for FlashIAP limits2struct FlashIAPLimits {3 size_t flash_size;4 uint32_t start_address;5 uint32_t available_size;6};
The last part of the helper file consists of the
getFlashIAPLimits()
function used to calculate both the size of the Flash memory as well as the size and start address of the available memory.This is done with Mbed's FlashIAP API. It finds the address of the first sector after the sketch stored in the microcontroller's ROM:
FLASHIAP_APP_ROM_END_ADDR
and uses the FlashIAP to calculate the Flash memory's size with flash.get_flash_size()
. The other parameters can be determined using the same API.1// Get the actual start address and available size for the FlashIAP Block Device2// considering the space already occupied by the sketch (firmware).3FlashIAPLimits getFlashIAPLimits()4{5 // Alignment lambdas6 auto align_down = [](uint64_t val, uint64_t size) {7 return (((val) / size)) * size;8 };9 auto align_up = [](uint32_t val, uint32_t size) {10 return (((val - 1) / size) + 1) * size;11 };12
13 size_t flash_size;14 uint32_t flash_start_address;15 uint32_t start_address;16 FlashIAP flash;17
18 auto result = flash.init();19 if (result != 0)20 return { };21
22 // Find the start of first sector after text area23 int sector_size = flash.get_sector_size(FLASHIAP_APP_ROM_END_ADDR);24 start_address = align_up(FLASHIAP_APP_ROM_END_ADDR, sector_size);25 flash_start_address = flash.get_flash_start();26 flash_size = flash.get_flash_size();27
28 result = flash.deinit();29
30 int available_size = flash_start_address + flash_size - start_address;31 if (available_size % (sector_size * 2)) {32 available_size = align_down(available_size, sector_size * 2);33 }34
35 return { flash_size, start_address, available_size };36}
3. Reading & Writing Data
Going back to the
FlashStorage.ino
file, some more files need to be included in order to implement reading and writing to the Flash. The FlashIAPBlockDevice.h
API will be used to create a block device in the empty part of the memory. Additionally, you can include the helper file FlashIAPLimits.h
to have access to the address and size calculation function that you just created. You can also reference the mbed
namespace for better readability.1#include <FlashIAPBlockDevice.h>2#include "FlashIAPLimits.h"3
4using namespace mbed;
The
setup()
function will first wait until a serial connection is established and then feed the random number generator, which is used later in this tutorial to write a random number in the Flash memory every time the device boots up.1void setup() {2 Serial.begin(115200);3 while (!Serial);4
5 Serial.println("FlashIAPBlockDevice Test");6 Serial.println("------------------------"); 7
8 // Feed the random number generator for later content generation9 randomSeed(analogRead(0));
Next the helper function, defined in the
FlashIAPLimits.h
file is called to calculate the memory properties, which are then used to create a block device using the FlashIAPBlockDevice.h
library.1// Get limits of the the internal flash of the microcontroller2auto [flashSize, startAddress, iapSize] = getFlashIAPLimits();3
4Serial.print("Flash Size: ");5Serial.print(flashSize / 1024.0 / 1024.0);6Serial.println(" MB");7Serial.print("FlashIAP Start Address: 0x");8Serial.println(startAddress, HEX);9Serial.print("FlashIAP Size: ");10Serial.print(iapSize / 1024.0 / 1024.0);11Serial.println(" MB");12
13// Create a block device on the available space of the flash14FlashIAPBlockDevice blockDevice(startAddress, iapSize);
Before using the block device, the first step is to initialize it using
blockDevice.init()
. Once initialized, it can provide the sizes of the blocks for programming the Flash. In terms of reading and writing Flash memory blocks, there is a distinction between the size of a readable block in bytes, a programmable block, which size is always a multiple of the read size, and an erasable block, which is always a multiple of a programmable block.When reading and writing directly from and to the Flash memory, you need to always allocate a buffer with a multiple of the program block size. The amount of required program blocks can be determined by dividing the data size by the program block size. The final buffer size is equal to the amount of program blocks multiplied by the program block size.
1// Initialize the Flash IAP block device and print the memory layout2blockDevice.init();3
4const auto eraseBlockSize = blockDevice.get_erase_size();5const auto programBlockSize = blockDevice.get_program_size();6
7Serial.println("Block device size: " + String((unsigned int) blockDevice.size() / 1024.0 / 1024.0) + " MB");8Serial.println("Readable block size: " + String((unsigned int) blockDevice.get_read_size()) + " bytes");9Serial.println("Programmable block size: " + String((unsigned int) programBlockSize) + " bytes");10Serial.println("Erasable block size: " + String((unsigned int) eraseBlockSize / 1024) + " KB");11 12String newMessage = "Random number: " + String(random(1024));13
14// Calculate the amount of bytes needed to store the message15// This has to be a multiple of the program block size16const auto messageSize = newMessage.length() + 1; // C String takes 1 byte for NULL termination17const unsigned int requiredEraseBlocks = ceil(messageSize / (float) eraseBlockSize);18const unsigned int requiredProgramBlocks = ceil(messageSize / (float) programBlockSize);19const auto dataSize = requiredProgramBlocks * programBlockSize; 20char buffer[dataSize] {};
In the last part of the
setup()
function you can now use the block device to read and write data. First the buffer is used to read what was stored within the previous execution, then the memory gets erased and reprogrammed with the new content. At the end of the reading and writing process, the block device needs to be de-initialized again using blockDevice.deinit()
.1// Read back what was stored at previous execution2Serial.println("Reading previous message...");3blockDevice.read(buffer, 0, dataSize);4Serial.println(buffer);5
6// Erase a block starting at the offset 0 relative7// to the block device start address8blockDevice.erase(0, requiredEraseBlocks * eraseBlockSize);9
10// Write an updated message to the first block11Serial.println("Writing new message...");12Serial.println(newMessage); 13blockDevice.program(newMessage.c_str(), 0, dataSize);14
15// Deinitialize the device16blockDevice.deinit();17Serial.println("Done.");
Finally the
loop()
function of this sketch will be left empty, considering that the Flash reading and writing process should be carried out as little as possible.4. Upload the Sketch
Below is the complete sketch of this tutorial consisting of the main sketch and the
FlashIAPLimits.h
helper file, upload both of them to your Portenta H7 to try it out.FlashIAPLimits.h
1/**2Helper functions for calculating FlashIAP block device limits3**/4
5// Ensures that this file is only included once6#pragma once 7
8#include <Arduino.h>9#include <FlashIAP.h>10#include <FlashIAPBlockDevice.h>11
12using namespace mbed;13
14// A helper struct for FlashIAP limits15struct FlashIAPLimits {16 size_t flash_size;17 uint32_t start_address;18 uint32_t available_size;19};20
21// Get the actual start address and available size for the FlashIAP Block Device22// considering the space already occupied by the sketch (firmware).23FlashIAPLimits getFlashIAPLimits()24{25 // Alignment lambdas26 auto align_down = [](uint64_t val, uint64_t size) {27 return (((val) / size)) * size;28 };29 auto align_up = [](uint32_t val, uint32_t size) {30 return (((val - 1) / size) + 1) * size;31 };32
33 size_t flash_size;34 uint32_t flash_start_address;35 uint32_t start_address;36 FlashIAP flash;37
38 auto result = flash.init();39 if (result != 0)40 return { };41
42 // Find the start of first sector after text area43 int sector_size = flash.get_sector_size(FLASHIAP_APP_ROM_END_ADDR);44 start_address = align_up(FLASHIAP_APP_ROM_END_ADDR, sector_size);45 flash_start_address = flash.get_flash_start();46 flash_size = flash.get_flash_size();47
48 result = flash.deinit();49
50 int available_size = flash_start_address + flash_size - start_address;51 if (available_size % (sector_size * 2)) {52 available_size = align_down(available_size, sector_size * 2);53 }54
55 return { flash_size, start_address, available_size };56}
FlashStorage.ino
1#include <FlashIAPBlockDevice.h>2#include "FlashIAPLimits.h"3
4using namespace mbed;5
6void setup() {7 Serial.begin(115200);8 while (!Serial);9
10 Serial.println("FlashIAPBlockDevice Test");11 Serial.println("------------------------"); 12
13 // Feed the random number generator for later content generation14 randomSeed(analogRead(0));15
16 // Get limits of the the internal flash of the microcontroller17 auto [flashSize, startAddress, iapSize] = getFlashIAPLimits();18
19 Serial.print("Flash Size: ");20 Serial.print(flashSize / 1024.0 / 1024.0);21 Serial.println(" MB");22 Serial.print("FlashIAP Start Address: 0x");23 Serial.println(startAddress, HEX);24 Serial.print("FlashIAP Size: ");25 Serial.print(iapSize / 1024.0 / 1024.0);26 Serial.println(" MB");27
28 // Create a block device on the available space of the flash29 FlashIAPBlockDevice blockDevice(startAddress, iapSize);30
31 // Initialize the Flash IAP block device and print the memory layout32 blockDevice.init();33 34 const auto eraseBlockSize = blockDevice.get_erase_size();35 const auto programBlockSize = blockDevice.get_program_size();36 37 Serial.println("Block device size: " + String((unsigned int) blockDevice.size() / 1024.0 / 1024.0) + " MB");38 Serial.println("Readable block size: " + String((unsigned int) blockDevice.get_read_size()) + " bytes");39 Serial.println("Programmable block size: " + String((unsigned int) programBlockSize) + " bytes");40 Serial.println("Erasable block size: " + String((unsigned int) eraseBlockSize / 1024) + " KB");41 42 String newMessage = "Random number: " + String(random(1024));43 44 // Calculate the amount of bytes needed to store the message45 // This has to be a multiple of the program block size46 const auto messageSize = newMessage.length() + 1; // C String takes 1 byte for NULL termination47 const unsigned int requiredEraseBlocks = ceil(messageSize / (float) eraseBlockSize);48 const unsigned int requiredProgramBlocks = ceil(messageSize / (float) programBlockSize);49 const auto dataSize = requiredProgramBlocks * programBlockSize; 50 char buffer[dataSize] {};51
52 // Read back what was stored at previous execution53 Serial.println("Reading previous message...");54 blockDevice.read(buffer, 0, dataSize);55 Serial.println(buffer);56
57 // Erase a block starting at the offset 0 relative58 // to the block device start address59 blockDevice.erase(0, requiredEraseBlocks * eraseBlockSize);60
61 // Write an updated message to the first block62 Serial.println("Writing new message...");63 Serial.println(newMessage); 64 blockDevice.program(newMessage.c_str(), 0, dataSize);65
66 // Deinitialize the device67 blockDevice.deinit();68 Serial.println("Done.");69}70
71void loop() {}
5. Results
After uploading the sketch open the Serial Monitor to start the Flash reading and writing process. The first time you start the script, the block device will be filled randomly. Now try to reset or disconnect the Portenta and reconnect it, you should see a message with the random number written to the Flash storage in the previous execution.
Note that the value written to the Flash storage will persist if the board is reset or disconnected. However, the Flash storage will be reprogrammed once a new sketch is uploaded to the Portenta and may overwrite the data stored in the Flash.
Programming the QSPI Flash
One issue with the internal Flash is that it is limited in size and the erase blocks are pretty large. This leaves very little space for your sketch and you may quickly run into issues with more complex applications. Therefore, you can use the external QSPI Flash which has plenty of space to store data. For that, the block device needs to be initialized differently, but the rest of the sketch remains the same. To initialize the device you can use the QSPIFBlockDevice class which is a block device driver for NOR-based QSPI Flash devices.
1#define BLOCK_DEVICE_SIZE 1024 * 8 // 8 KB2#define PARTITION_TYPE 0x0B // FAT 323
4// Create a block device on the available space of the flash5QSPIFBlockDevice root(PD_11, PD_12, PF_7, PD_13, PF_10, PG_6, QSPIF_POLARITY_MODE_1, 40000000);6MBRBlockDevice blockDevice(&root, 1); 7
8// Initialize the Flash IAP block device and print the memory layout9if(blockDevice.init() != 0 || blockDevice.size() != BLOCK_DEVICE_SIZE) { 10 Serial.println("Partitioning block device...");11 blockDevice.deinit();12 // Allocate a FAT 32 partition13 MBRBlockDevice::partition(&root, 1, PARTITION_TYPE, 0, BLOCK_DEVICE_SIZE);14 blockDevice.init();15}
While the QSPI block device memory can be used directly, it is better to use a partition table as the QSPI storage is also filled with other data, such as the Wi-Fi firmware. For that you use the MBRBlockDevice class and allocate a 8 KB partition, which can then be used to read and write data.
The full QSPI version of the sketch is as follows:
1#include "QSPIFBlockDevice.h"2#include "MBRBlockDevice.h"3
4using namespace mbed;5
6#define BLOCK_DEVICE_SIZE 1024 * 8 // 8 KB7#define PARTITION_TYPE 0x0B // FAT 328
9void setup() {10 Serial.begin(115200);11 while (!Serial);12
13 Serial.println("QSPI Block Device Test");14 Serial.println("------------------------"); 15
16 // Feed the random number generator for later content generation17 randomSeed(analogRead(0));18
19 // Create a block device on the available space of the flash20 QSPIFBlockDevice root(PD_11, PD_12, PF_7, PD_13, PF_10, PG_6, QSPIF_POLARITY_MODE_1, 40000000);21 MBRBlockDevice blockDevice(&root, 1); 22
23 // Initialize the Flash IAP block device and print the memory layout24 if(blockDevice.init() != 0 || blockDevice.size() != BLOCK_DEVICE_SIZE) { 25 Serial.println("Partitioning block device...");26 blockDevice.deinit();27 // Allocate a FAT 32 partition28 MBRBlockDevice::partition(&root, 1, PARTITION_TYPE, 0, BLOCK_DEVICE_SIZE);29 blockDevice.init();30 }31 32 const auto eraseBlockSize = blockDevice.get_erase_size();33 const auto programBlockSize = blockDevice.get_program_size();34
35 Serial.println("Block device size: " + String((unsigned int) blockDevice.size() / 1024) + " KB"); 36 Serial.println("Readable block size: " + String((unsigned int) blockDevice.get_read_size()) + " bytes");37 Serial.println("Programmable block size: " + String((unsigned int) programBlockSize) + " bytes");38 Serial.println("Erasable block size: " + String((unsigned int) eraseBlockSize / 1024) + " KB");39 40 String newMessage = "Random number: " + String(random(1024));41 42 // Calculate the amount of bytes needed to store the message43 // This has to be a multiple of the program block size44 const auto messageSize = newMessage.length() + 1; // C String takes 1 byte for NULL termination45 const unsigned int requiredEraseBlocks = ceil(messageSize / (float) eraseBlockSize);46 const unsigned int requiredBlocks = ceil(messageSize / (float) programBlockSize);47 const auto dataSize = requiredBlocks * programBlockSize; 48 char buffer[dataSize] {}; 49
50 // Read back what was stored at previous execution 51 Serial.println("Reading previous message...");52 blockDevice.read(buffer, 0, dataSize);53 Serial.println(buffer);54
55 // Erase a block starting at the offset 0 relative56 // to the block device start address57 blockDevice.erase(0, requiredEraseBlocks * eraseBlockSize);58
59 // Write an updated message to the first block60 Serial.println("Writing new message...");61 Serial.println(newMessage); 62 blockDevice.program(newMessage.c_str(), 0, dataSize);63
64 // Deinitialize the device65 blockDevice.deinit();66 Serial.println("Done.");67}68
69void loop() {}
Conclusion
We have learned how to use the available space in the Flash memory of the microcontroller to read and save custom data. It is not recommended to use the Flash of the microcontroller as the primary storage for data-intensive applications. It is better suited for read/write operations that are performed only once in a while such as storing and retrieving application configurations or persistent parameters.
Next Steps
Now that you know how to use block device to perform reading and writing Flash memory, you can look into the next tutorial on how to use the TDBStore API to create a key value store in the Flash memory.
Suggest changes
The content on docs.arduino.cc is facilitated through a public GitHub repository. If you see anything wrong, you can edit this page here.
License
The Arduino documentation is licensed under the Creative Commons Attribution-Share Alike 4.0 license.