Utilisation de la mémoire flash du Pico

Il existe des situations ou il est nécessaire de conserver des données entre deux exécutions d'un même programme. Sur un système comme Windows, c'est une utilisation courante de clés de la base de registres, ou des fichiers avec extension".ini". Dans cet exemple, nous souhaitons conserver des constantes d'étalonnage. Un programme fait des mesures analogiques sur des capteurs avec conditionneurs reliés aux entrées analogiques. Ces capteurs ont une constante d'étalonnage pour convertir la tension mesurée en une unité physique. Comme le capteur, le convertisseur ADC du Pico, voire même le cablage peuvent varier dans le temps, il est nécessaire de manière périodique d'étalonner les mesures en les comparant à un outil de référence externe. On en déduit des constantes d'étalonnage. Celles ci doivent donc être conservées même lorque le Pico est arrété et/ou perd son alimentation.


Sur un PC ou un Raspberry Pi, cela ne pose pas de problèmes car on dispose de mémoire de masse : disque dur, carte SD, clé USB, etc... Le Pico, en standard ne dispose pas de ces ressources. On peut, bien sur, lui adjoindre un lecteur de cartes SD (surtout si l'on a des dizaines de MO à sauver). Mais il existe un moyen plus simple qui permet de mettre en oeuvre de telles sauvegardes sans ajout de matériel : La mémoire flash du Pico.


Le Pico dispose d'une mémoire flash ROM de 2 MO. C'est dans cette mémoire flash que le programme chargé sur le Pico est sauvegardé. Si le programme ne prend pas toute cette mémoire, ce serait un cas extrème, il nous est possible d'utiliser cette mémoire pour y stocker tout ce que l'on souhaite. C'est le but de cet exemple : Sauver et relire 3 constantes d'étallonage de 3 capteurs fictifs sous forme de valeurs réelles de type "float".

Ou est la mémoire flash ? Ou pouvons écrire et lire des données ?

Sur le Pico, la mémoire flash se situe 256 MO après le début de la zone d'adressage du processeur 32 bits. En hexadéciaml cela implique que la mémoire flash commence à l'adresse 0x10000000. Le programme qu'exécute le Pico est donc stocké à partir de cette adresse. A titre d'exemple, si ce programme à une taille de 64 K0, il occupera la zone mémoire de 0x10000000 à 0x10010000.


Il n'est pas nécessaire de se rappeler de cette valeur 0x10000000. Le SDK met à notre disposition une constante nommée "XIP_BASE " contenant cette valeur.


Pour savoir à quel endroit vous pouvez stocker des données dans la mémoire flash, il faut savoir quelle taille tient votre programme dans cette mémoire pour ne pas écraser tout ou partie de votre code. Lorsque le compilateur génère votre programme exécutable (fichier elf), il inscrit un flag nommé "flash_binary_end" en fin du fichier. Si l'on sait ou se trouve, ce flag, on connait la dernière adresse qu'occupe notre programme dans la mémoire flash. La commande "objdump" permet de lire la position de ce flag. Donc pour lire ce flag, dans un terminal, vous pouvez exécuter le commande suivante :


objdump --all monprogramme.elf | grep flash_binary_end

Vous obtiendrez un résultat comme celui-ci :


10040300 g -ARM.attributes 00000000 __flash_binary_end

Ce qui nous interesse dans cette réponse est le premier chiffre : "10040300". C'est très exactement l'adresse du dernier octet de notre programme dans la mémoire flash du Pico. Dans cet exemple, cela correspond à 256 KO après le début de la mémoire flash. En résumé, les adresses comprises entre XIP_BASE et XIP_BASE + 0x00040300 nous sont interdites. En pratique, on ne situera pas nos données à partir de l'octet suivant la fin de notre programme. Celui-ci pouvant être amené à être modifié. Dans un cas comme celui-ci, il nous reste beaucoup de place, on pourrait choisir de se situer 512 KO après le début de la mémoire flash, c'est à dire à partir de l'adresse XIP_BASE + 0x00080000.

Lire, écrire, effacer une zone de la mémoire flash

Lire


Lire la mémoire flash est une opération simple, pour lire un octet, il suffit de définir un pointeur vers l'adresse de cet octet à partir de XIP_BASE. A titre d'exemple, si l'on veut lire le contenu du millième octet de notre programme, il suffit de faire :


char *p = (char *)(XIP_BASE + 1000);
char *Byte1000 = *p;

Ecrire


Pour écrire dans la mémoire flash, le SDK met à votre disposition une fonction : "flash_range_program" dont la signature est la suivante :


void flash_range_program(uint32_t flash_offs, const uint8_t* data, size_t count);

Les paramètres de cette fonction sont les suivants :


- flash_offs : L'adresse à partir de laquelle vous voulez placer vos données. A titre d'exemple, si vous voulez les positionner à 512 KO du début de la mémoire flash, il faudra passer ici la valeur XIP_BASE + 0x00080000.
- data : Pointeur vers les données à écrire.
- count : Nombre d'octets à écrire. Ici, il y a une subtilité : Cette valeur doit obligatoirement être un multiple de 256. C'est la taille d'une page de la mémoire flash. Une constante nommée "FLASH_PAGE_SIZE" retourne cette valeur de 256. Ainsi, notre tableau de données à écrire devrait toujours être d'une taille multiple de "FLASH_PAGE_SIZE".


Effacer


Pour effacer une zone de la mémoire flash, le SDK met à votre disposition une fonction : "flash_range_erase" dont la signature est la suivante :


void flash_range_erase(uint32_t flash_offs, size_t count);

Les paramètres de cette fonction sont les suivants :


- flash_offs : L'adresse à partir de laquelle vous voulez effacer la mémoire flash. A titre d'exemple, si vous voulez effacer la mémoire flsh à partir de 512 KO du début de la mémoire flash, il faudra passer ici la valeur XIP_BASE + 0x00080000.
- count : Nombre d'octets à effacer. Ici, il y a une subtilité : Cette valeur doit obligatoirement être un multiple de 4096. C'est la taille d'un secteur de la mémoire flash. Une constante nommée "FLASH_SECTOR_SIZE.

Ne pas être interrompu

Une opération d'écriture ou d'effacement de la mémoire flash ne doit jamais être interrompue. Même si votre programme n'emploie pas d'interruptions, il reste sain de se prémunir de ce problème. A titre d'exemple, l'interface USB serait tout à fait en mesure de générer des interuptions indépendamment de votre programme. Il faut donc, toujours faire précéder les fonctions d'écriture ou d'effacement de la mémoire flash d'un appel à la fonction "save_and_disable_interrupts", et de les faire suivre d'un appel à la fonction "restore_interrupts ".

Identifier les données

Lorsque sur un PC ou un Raspberry Pi, un programme doit lire un fichier de configuration, s'il ne trouve pas ce fichier, il prend des valeurs par défaut codées en dur dans le code. C'est une situation classique, la premiére fois qu'un programme est exécuté. Il faut pouvoir disposer du même moyen avec la mémoire flash. La solution est de faire précéder les données sauvegardées dans la mémoire flash d'une suite de quelques octets dont les valeurs sont figées et codées en dur dans le code. Lors de la lecture, si on ne lit pas ces octets, on sait qu'il n'y a pas de données dans la mémoire et on peut se rabattre sur des valeurs par défaut.

Programme d'exemple

Ci dessous les fichiers "CMakeLists.txt" et "flash.cpp" d'un projet du même nom : "flash" :

CMakeLists.txt

# CMake version we need
cmake_minimum_required(VERSION 3.13)

# Define C anc CXX standards
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# Pull in Raspberry Pi Pico SDK (must be before project)
include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)

if (PICO_SDK_VERSION_STRING VERSION_LESS "1.4.0")
  message(FATAL_ERROR "Raspberry Pi Pico SDK version 1.4.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}")
endif()

# Declare the project name and languages
project(flash C CXX ASM)

# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()

# Add executable. Default name is the project name
add_executable(flash flash.cpp)

# Add some info to the executable (For picotool....)
pico_set_program_name(flash "flash")
pico_set_program_version(flash "0.1")

# Where do stdio functions send their stuff
pico_enable_stdio_uart(flash 1)
pico_enable_stdio_usb(flash 0)

# Add the standard include files to the build
target_include_directories(flash PRIVATE
  ${CMAKE_CURRENT_LIST_DIR}
  ${CMAKE_CURRENT_LIST_DIR}/.. # for our common lwipopts (Pico W) or any other standard includes, if required
)

# create map/bin/hex/uf2 file etc.
pico_add_extra_outputs(flash)

# Add any user requested libraries
target_link_libraries(flash
  pico_stdlib
  hardware_flash
  hardware_sync
)

identify.cpp

#include ‹stdio.h›
#include "pico/stdlib.h"
#include "hardware/flash.h"
#include "hardware/sync.h"

// On stockera nos données à 512 KO du début de la mémoire flash
#define FIRST_BYTE_ADDRESS_OFFSET (512 * 1024)

// On défini un poineur vers le premier octet de la zone de mémoire flash que l'on utilise
const uint8_t *flash_target_contents = (const uint8_t *) (XIP_BASE + FIRST_BYTE_ADDRESS_OFFSET);

// On se définie un identificateur
const uint8_t ID[] = {0x01, 0x10, 0x02, 0x20, 0x04, 0x40, 0x08, 0x80};

// Nos 3 constantes d'étalonnage
float Chan1Const;
float Chan2Const;
float Chan3Const;

// On définie un tableau pour les données à sauvegarder : Multiple de FLASH_PAGE_SIZE (256)
uint8_t FlashBytes[FLASH_PAGE_SIZE];

// On se définie des valeurs par défaut pour nos constantes d'étalonnage
#define CHANNEL1_DEFAULT_CONSTANT 1.0f
#define CHANNEL2_DEFAULT_CONSTANT 2.0f
#define CHANNEL3_DEFAULT_CONSTANT 4.0f

// Lecture de la flash ROM
void readConstants()
{
  uint8_t Pointer = 0; // Pointe vers l'octet à lire

  // Lecture et vérification de l'identificateur ID
  for(int i = 0; i ‹ 8; i++)
  {
    if(ID[i] != flash_target_contents[Pointer])
    {
      // Dès qu'un octet est différent de celui attendu, l'ID n'est pas vérifié
      // donc on prend les valeurs par défaut et on sort
      Chan1Const = CHANNEL1_DEFAULT_CONSTANT;
      Chan2Const = CHANNEL2_DEFAULT_CONSTANT;
      Chan3Const = CHANNEL3_DEFAULT_CONSTANT;
      printf("Pas de constantes dans la Flash ROM. Utilisation des constantes par défaut\n");
      return;
    }
    Pointer++;
  }

  // l'Identificateur est vérifié. On peut lire les constantes
  float* pf = (float *)(&(flash_target_contents[Pointer]));
  Chan1Const = *pf;
  Pointer += sizeof(float);
  pf = (float *)(&(flash_target_contents[Pointer]));
  Chan2Const = *pf;
  Pointer += sizeof(float);
  pf = (float *)(&(flash_target_contents[Pointer]));
  Chan3Const = *pf;

  printf("Constantes lues avec succès\n");
}

// Utlitaire permettant de placer les données à écrire sur la flash ROM dans un tableau d'octets
// Fonction appelée par la fonction saveConstants
void FillDescriptorBytesArray()
{
  float* pF;
  uint16_t Pointer = 0;

  // Initialisation du tableau descripteur à 0
  for(int i = 0; i ‹ FLASH_PAGE_SIZE; i++) FlashBytes[i] = 0;

  // On écrit l'identificateur dans le taableau descripteur
  for(int i = 0; i ‹ 8; i++)
  {
    FlashBytes[Pointer] = ID[i];
    Pointer++;
  }

  // On écrit les données dans le tableau descripteur
  pF = (float *)(&(FlashBytes[Pointer]));
  *pF = Chan1Const;
  Pointer += sizeof(float);
  pF = (float *)(&(FlashBytes[Pointer]));
  *pF = Chan2Const;
  Pointer += sizeof(float);
  pF = (float *)(&(FlashBytes[Pointer]));
  *pF = Chan3Const;
}

// Ecriture de la flash ROM
void saveConstants()
{
  // Appel de la fonction utilitaire préparant les données à sauvegarder
  FillDescriptorBytesArray();

  // Arrêt des interruptions
  uint32_t ints = save_and_disable_interrupts();

  // Effacement de la flash ROM
  flash_range_erase (FIRST_BYTE_ADDRESS_OFFSET, FLASH_SECTOR_SIZE);

  // Ecriture de nos données préparées dans le tableau descripteur vers la flash ROM
  flash_range_program (FIRST_BYTE_ADDRESS_OFFSET, FlashBytes, FLASH_PAGE_SIZE);

  // Reprise des interruptions
  restore_interrupts (ints);
}

int main()
{
  stdio_init_all();

  // Lecture des constantes
  printf("Lecture des constantes dans la flash ROM\n");
  readConstants();
  printf("- Valeurs des constantes : %.3f - %.3f - %.3f\n", Chan1Const, Chan2Const, Chan3Const);

  // Juste pour l'exemple, on donne des valeurs quelconques à nos constantes
  Chan1Const = 1.234f;
  Chan2Const = 3.456f;
  Chan3Const = 5.678f;;

  //Ecriture des constantes
  saveConstants();
  printf("Ecriture des constantes dans la flash ROM\n");

  return 0;
}