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

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.

Conceptual view of how the block sizes relate to one another

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 once
2#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 limits
2struct 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 Device
2// considering the space already occupied by the sketch (firmware).
3FlashIAPLimits getFlashIAPLimits()
4{
5 // Alignment lambdas
6 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 area
23 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 generation
9 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 microcontroller
2auto [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 flash
14FlashIAPBlockDevice 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 layout
2blockDevice.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 message
15// This has to be a multiple of the program block size
16const auto messageSize = newMessage.length() + 1; // C String takes 1 byte for NULL termination
17const 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 execution
2Serial.println("Reading previous message...");
3blockDevice.read(buffer, 0, dataSize);
4Serial.println(buffer);
5
6// Erase a block starting at the offset 0 relative
7// to the block device start address
8blockDevice.erase(0, requiredEraseBlocks * eraseBlockSize);
9
10// Write an updated message to the first block
11Serial.println("Writing new message...");
12Serial.println(newMessage);
13blockDevice.program(newMessage.c_str(), 0, dataSize);
14
15// Deinitialize the device
16blockDevice.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 limits
3**/
4
5// Ensures that this file is only included once
6#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 limits
15struct 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 Device
22// considering the space already occupied by the sketch (firmware).
23FlashIAPLimits getFlashIAPLimits()
24{
25 // Alignment lambdas
26 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 area
43 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 generation
14 randomSeed(analogRead(0));
15
16 // Get limits of the the internal flash of the microcontroller
17 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 flash
29 FlashIAPBlockDevice blockDevice(startAddress, iapSize);
30
31 // Initialize the Flash IAP block device and print the memory layout
32 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 message
45 // This has to be a multiple of the program block size
46 const auto messageSize = newMessage.length() + 1; // C String takes 1 byte for NULL termination
47 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 execution
53 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 relative
58 // to the block device start address
59 blockDevice.erase(0, requiredEraseBlocks * eraseBlockSize);
60
61 // Write an updated message to the first block
62 Serial.println("Writing new message...");
63 Serial.println(newMessage);
64 blockDevice.program(newMessage.c_str(), 0, dataSize);
65
66 // Deinitialize the device
67 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 KB
2#define PARTITION_TYPE 0x0B // FAT 32
3
4// Create a block device on the available space of the flash
5QSPIFBlockDevice 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 layout
9if(blockDevice.init() != 0 || blockDevice.size() != BLOCK_DEVICE_SIZE) {
10 Serial.println("Partitioning block device...");
11 blockDevice.deinit();
12 // Allocate a FAT 32 partition
13 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 KB
7#define PARTITION_TYPE 0x0B // FAT 32
8
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 generation
17 randomSeed(analogRead(0));
18
19 // Create a block device on the available space of the flash
20 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 layout
24 if(blockDevice.init() != 0 || blockDevice.size() != BLOCK_DEVICE_SIZE) {
25 Serial.println("Partitioning block device...");
26 blockDevice.deinit();
27 // Allocate a FAT 32 partition
28 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 message
43 // This has to be a multiple of the program block size
44 const auto messageSize = newMessage.length() + 1; // C String takes 1 byte for NULL termination
45 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 relative
56 // to the block device start address
57 blockDevice.erase(0, requiredEraseBlocks * eraseBlockSize);
58
59 // Write an updated message to the first block
60 Serial.println("Writing new message...");
61 Serial.println(newMessage);
62 blockDevice.program(newMessage.c_str(), 0, dataSize);
63
64 // Deinitialize the device
65 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.