STM32F4: Consideraciones para el uso de la memoria flash (Parte I)

Siguiendo la serie de tutoriales y guías sobre el uso de la placa de desarrollo STM32F411-DISCOVERY, hoy hablaremos de la escritura, lectura y manejo de la memoria flash del microcontrolador (MCU). Como ya sabréis, el STM32F411VET6 cuenta con 128 Kbytes de memoria SRAM y 512 Kbytes de flash. La principal diferencia entre estos dos tipos de memoria es que la flash, al contrario que la SRAM, es no volátil, es decir, no pierde la información que almacena al desactivar la alimentación.  Esta característica hace que, en general:

  • La memoria SRAM se destine para el almacenamiento y manipulación de datos parciales, variables locales, cambios de contexto, etc.
  • La memoria flash se utilice parte para la memoria de programa, donde se almacena el código del programa a ejecutar por el procesador, y parte para el almacenamiento de datos de configuración o de los procesos que se controlan y se quieren conservar tras una desactivación temporal del sistema.

En este artículo nos centraremos en este último tipo de memoria, aportando algunas claves importantes para su uso, así como una serie de consideraciones y buenas prácticas a tener en cuenta para evitarnos algunos problemas frecuentes.

Lo primero que es importante tener claro es cómo se organiza la memoria flash de nuestro microcontrolador. Esto suele variar bastante de unos a otros. En general, la memoria se divide en bloques o bancos, que a su vez se pueden subdividir en sectores. La siguiente unidad de división de la memoria serían las páginas y, por último, las palabras, que se componen de un determinado número de bytes. En general, la palabra suele ser la unidad mínima de lectura/escritura en flash aunque, en algunos casos, es posible hacerlo a nivel de byte, y en otros se opera en double-word, es decir, de dos en dos palabras simultáneamente.

Para saber la organización en particular de un microcontrolador, lo mejor es acudir al manual de referencia del mismo. A continuación, podéis consultar tres ejemplos distintos del propio STMicroelectronics, para que podáis comprobar de primera mano cómo en cada uno la organización es de una forma distinta:

Este último ejemplo, el STM32F411VE, se corresponde con el MCU de nuestra placa Discovery, y será el caso concreto que utilizaremos en este artículo. En la siguiente imagen se muestra la información principal de nuestra memoria.

flashMemory
Figura 1. Captura de la sección “Embedded Flash memory in STM32F411xC/E”

En la tabla anterior podemos observar que los 512 Kbytes de memoria flash que tiene el MCU según se especifica en sus características, son en realidad un gran bloque llamado Main memory.  A parte, vemos que existen otros tres bloques reservados para usos específicos y que, por lo tanto, no contabilizan como memoria flash.

Lo primero que debemos decidir es qué parte de la memoria principal queremos reservarnos para nuestra disposición. En este caso, utilizaremos el Sector 7, al que nos referiremos como USER_FLASH. Una vez tenemos esto claro, es muy importante asegurarnos de que ese sector no se va a utilizar para nada más, de modo que ni nosotros machaquemos información de otra utilidad, ni otra utilidad sobrescriba nuestra información. Para garantizar esto, es necesario editar el linker script de nuestro proyecto, que ha sido generado automáticamente por CubeMX, y se encuentra en la ruta:

Este fichero es el encargado de dar las instrucciones al linker del uso y tamaño de cada área de memoria y, por lo tanto, debe editarse con sumo cuidado. Se escapa del alcance de esta publicación explicarlo en detalle, por lo que únicamente daremos las indicaciones básicas de las modificaciones necesarias. Para profundizar en su comprensión, os dejamos algo de documentación.

En primer lugar, en la sección MEMORY del fichero STM32F411VETx_FLASH.ld, que es donde se especifican las áreas de memoria, debemos reducir FLASH en los 128 Kbytes correspondientes al Sector 7, y añadir USER_FLASH indicando sus permisos, dirección de origen y tamaño. Con ello, esta sección pasa de esta configuración:

A esta otra:

En segundo lugar, hay que editar el apartado SECTIONS del mismo fichero, que es donde se definen las distintas secciones de memoria, ya sean para guardar código o datos. En concreto, vamos crear una nueva para guardar nuestros datos no volátiles en el área de memoria USER_FLASH. De este modo, desde nuestro código de programa, podremos declararnos variables de cualquier tipo que se almacenen en dicha sección y, por lo tanto, sean no volátiles. Para ello, al principio de este apartado, antes de la sección .isr_vector, añadimos el siguiente código:

Con estos pasos, ya tendríamos nuestra área de memoria flash delimitada de una manera segura y preparada para utilizarse desde nuestro código.

Para probar que todo está correctamente, vamos a proceder a leer y escribir un dato en memoria flash. Para ello, partiremos del proyecto desarrollado en el tutorial previo STM32F4: Interrupción externa. En concreto, lo que haremos es aprovechar la interrupción producida al pulsar el botón de usuario para conmutar la frecuencia de parpadeo del LED verde entre dos valores, que definiremos en main.c dentro de USER CODE BEGIN Includes.

Sin embargo, lo que haremos es guardar la variable que contiene el valor del delay que se esté utilizando en cada instante en la memoria flash. De este modo, en cualquier momento el sistema puede apagarse o resetearse y, al volver a funcionar, mantiene la última frecuencia de conmutación que tuvo operativa. Para ello, en main.c, dentro de USER CODE BEGIN PV, nos declaramos la variable globar current_delay del siguiente modo:

Lo más llamativo de esta declaración es el atributo:

Esta parte es justo la que le indica al linker que la variable current_delay de tipo uint32_t debe guardarse en la sección .user_data_flash, en vez de donde se guardan las variables por defecto. De este modo, el valor de la variable es no volátil. Hay que tener en cuenta que si bien esta variable se puede leer como cualquier otra, para escribir en ella, al tratarse de memoria flash, hay que seguir un procedimiento especial que comentaremos más adelante. Además, también hay que destacar que el atributo __section__ no se puede emplear para variables locales.

Por otra parte, si recordáis, en el tutorial de la interrupción externa se comentó que es recomendable minimizar el número de operaciones a realizar dentro del callback de la interrupción. Por ello, para este caso utilizaremos un flag que declararemos e inicializaremos en main.c, también dentro de USER CODE BEGIN PV, como:

Modificar el valor de este flag será lo único que haremos dentro del callback de la interrupción, que quedará del siguiente modo. Recordamos que este callback se encuentra en el fichero gpio.c.

Es importante destacar que para poder emplear la variable swap_flag en este otro fichero, hay que declararla en gpio.c, dentro de  USER CODE BEGIN 0, como extern.

Para este ejemplo, todo lo demás lo programamos dentro de la función main(). Queda en la mano del lector reorganizar este código en las funciones que considere oportunas para lograr un código de mayor calidad. Del mismo modo, queda pendiente el tratamiento de errores. Para el ejemplo, la función quedaría así:

Como asumimos ciertos conocimientos de programación por parte del lector, en este punto únicamente comentaremos ciertos aspectos relevantes:

  • Antes de escribir en memoria, hay que borrarla mediante la función HAL_FLASHEx_Erase(&EraseInitStruct, &PAGError), definida en el fichero stm32f4xx_hal_flash_ex.c. En la Sección 3.5.4 del Reference Manual RM0383 Rev 2 podéis encontrar más información al respecto.
  • La estructura EraseInitStruct se utiliza para configurar la operación de borrado de la memoria. En este caso, utilizamos siempre la misma configuración y por eso sólo se rellena una vez, pero esto no tiene por qué ser así. Podéis leer más sobre sus distintos campos en la cabecera de su definición, FLASH_EraseInitTypeDef, en el fichero stm32f4xx_hal_flash_ex.h.
  • Antes de realizar cualquier operación de borrado o escritura de la flash, es necesario desbloquearla con la función HAL_FLASH_Unlock(). Del mismo modo, al concluir dichas operaciones, hay que volver a bloquearla con la función HAL_FLASH_Lock().
  • Para escribir en memoria flash utilizamos la función HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, (uint32_t)&current_delay, (uint32_t)data), definida en el fichero stm32f4xx_hal_flash.c.
    • El primer argumento que le pasamos es una macro definida en stm32f4xx_hal_flash.h que indica que haremos una escritura de una palabra, es decir, un dato de 32 bits. En ese fichero podéis ver qué otras opciones permite este microcontrolador.
    • El segundo argumento que le pasamos es la dirección de la variable donde queremos guardar el dato.
    • El tercer argumento que le pasamos es el dato a guardar que, en este caso, debe coincidir con el tamaño de palabra.

Por último, para poder confirmar que la escritura se ha realizado correctamente, durante su ejecución en Debug, podemos monitorizar las distintas posiciones de memoria desde la pestaña Memory del entorno Eclipse. En este caso vemos como la variable current_delay se encuentra al principio de la sección .user_data_flash, en la dirección 0x0806 0000. 

memory
Figura 2. Herramienta de Eclipse para monitorizar la memoria del MCU.

Además, como esta memoria es no volátil, podría suceder, dependiendo de la configuración del Debugger, que al reprogramar el MCU no se borrase la flash de usuario. Para hacerlo manualmente, podemos usar la herramienta Erase chip del siguiente modo.

La herramienta Erase chip permite borrar toda la memoria del MCU por completo.
Figura 3. La herramienta Erase chip permite borrar toda la memoria del MCU por completo.

Hasta aquí la publicación de hoy. Esperemos que os haya parecido interesante y os animéis a profundizar en las distintas líneas que se quedan abiertas. En la siguiente parte, comentaremos más aspectos importantes a tener en cuenta al trabajar con la memoria flash, por ejemplo, el almacenamiento de múltiples datos en ella.