La presencia de EEPROM brinda a los desarrolladores una herramienta conveniente para guardar los parámetros de configuración o un estado que cambia lentamente para que un apagón pueda sobrevivir. En este artículo veremos cómo hacer esto de la manera más segura y conveniente posible para no olvidar nada y no recordar lo que no estaba allí.
Supongamos que tenemos una variable y queremos almacenarla en una EEPROM. Parece que todas las herramientas para esto están en nuestras manos:
#include <EEPROM.h> int my_var = DEFAULT_VALUE; EEPROM.get(MY_VAR_ADDR, my_var); my_var = NEW_VALUE; EEPROM.put(MY_VAR_ADDR, my_var);
Sin embargo, una mirada más cercana revela que este enfoque crea más problemas de los que resuelve. Los discutiremos en orden.
1. ¿Cómo asegurarnos de que leemos exactamente lo que escribimos (para garantizar la
integridad )? Imagina la siguiente imagen. Nos escribimos una carta a nosotros mismos en caso de nuestra muerte repentina por una pérdida de energía o una señal de reinicio y la guardamos en un cajón del escritorio. En la próxima vida, abrimos el cajón del escritorio, sacamos un trozo de papel, leemos el mensaje y continuamos nuestra misión. El problema es que en el cuadro siempre hay hojas de papel garabateadas con texto aleatorio. Por lo tanto, necesitamos una forma de distinguir el mensaje correcto del aleatorio. Uno podría asegurarle un notario público, pero en el caso más simple, su firma sería suficiente si tenemos una forma de verificar su exactitud. Por ejemplo, podemos usar el resultado de una expresión matemática dependiendo del texto como firma, de modo que la probabilidad de coincidencia aleatoria sea suficientemente pequeña. En el caso más simple, este es un CRC o suma de verificación. Nos protegerá no solo de leer lo que no escribimos, sino también de leer un mensaje dañado. Después de todo, el texto se desvanece con el tiempo, y los electrones en el obturador aislado son aún menos duraderos: una partícula volará desde el espacio con suficiente energía y la broca cambiará. Pero hay otra forma de obtener un mensaje dañado: esto no es agregarlo al final. No es tan exótico, porque en el momento de la grabación, el consumo actual aumenta bruscamente, lo que puede provocar una muerte prematura del escritor.
2. Supongamos que estamos convencidos de la exactitud del mensaje, pero ¿cómo puedo asegurarme de que fui yo quien lo escribió (para garantizar la
autenticidad )? Como dice el refrán, soy diferente. De repente, alguien más estaba sentado en esta mesa antes de mi reencarnación, y él tenía una misión diferente, y ¿por qué razón ahora me guiaré por sus mensajes? Si proporcionáramos nuestras notas con una etiqueta determinada, sería más fácil para nosotros distinguir las nuestras de los extraños. Por ejemplo, dicha etiqueta podría ser el nombre de la variable que estamos guardando. El único problema es que en EEPROM no hay mucho espacio para poner nombres de variables allí, y es inconveniente hacerlo, porque son de diferentes longitudes. Pero, afortunadamente, hay una forma más simple: puede calcular la suma de verificación en nombre de la variable y usarla como un acceso directo. Al mismo tiempo, es útil agregar el tamaño de la variable en bytes a esta suma de verificación para no leer accidentalmente la cantidad incorrecta. Bueno, en aras de la exhaustividad, agregamos otro identificador numérico allí, para garantizar distinguir nuestra variable de otra persona, incluso si se les llama igual. Llamamos a este número el identificador de instancia (inspirado por OOP si el nombre de la variable se considera como un campo de objeto). Si alguna vez mejoramos nuestra misión a una versión radicalmente nueva, para que esta actualización no tenga sentido todo lo que guardó la anterior, entonces solo tenemos que cambiar el identificador de instancia para invalidar todo lo guardado por la versión anterior.
3. ¿Cómo puedo hacer que una operación de escritura incompleta deje el antiguo valor almacenado sin cambios? Es decir, la operación de salvar debería tener éxito o no debería tener ningún efecto observable. En otras palabras, debería ser
atómico o transaccional si estamos hablando de una transacción que se reduce a una actualización incondicional de un solo valor. Obviamente, no podemos garantizar la atomicidad del registro reescribiendo el valor anterior, debemos escribir en un nuevo lugar para que el valor almacenado anterior permanezca intacto, al menos hasta que se complete la grabación del nuevo. Esta técnica a menudo se llama 'copiar en escritura' si solo se actualiza parte del valor guardado, pero la parte que permanece sin cambios todavía se copia y se escribe en una nueva ubicación. Al desarrollar nuestra analogía, nos escribiremos cartas a nosotros mismos, dejando intactas las antiguas, pero suministrando a cada letra un número de serie cada vez mayor para que en nuestra próxima vida tengamos la oportunidad de encontrar la última carta que escribimos. Al mismo tiempo, sin embargo, surge un nuevo problema: el lugar en el cuadro donde colocamos las letras terminará tarde o temprano si no desechamos las letras viejas que se han vuelto irrelevantes. Es fácil entender que es suficiente almacenar solo 2 letras, una antigua y una nueva, puede estar en proceso de escritura. En consecuencia, el número de letra tampoco necesita muchos bits.
Por extraño que parezca, el autor no pudo encontrar una implementación única que permitiera la organización del almacenamiento de datos en EEPROM, al tiempo que garantizaba la integridad, la autenticidad y la atomicidad. Tuve que escribir a
github.com/olegv142/NvTx yo mismo
Para guardar cada variable en la EEPROM, se utilizan 2 áreas consecutivas: celdas con la misma estructura. El identificador de la variable calculado sobre la base de su tamaño, etiqueta de texto e identificador de instancia se escribe en los primeros 2 bytes. A continuación, se escriben los datos, seguidos de 2 bytes de la suma de verificación. En el primer byte, dos bits tienen un propósito especial. El bit más significativo es el indicador de corrección; cuando se escribe, siempre se establece en uno. El bit de orden inferior se usa como un número de un solo bit de la era; es necesario para encontrar el último mensaje. La grabación se realiza en celdas 'en círculo'. El número de la era cambia cada vez que se realiza un registro en la primera celda. De ahí el algoritmo para determinar la última celda registrada: si las épocas de las celdas son iguales, entonces el segundo se escribe último, si es diferente, entonces el primero.
El bit de corrección parece redundante, pero tiene una función importante. En primer lugar, leemos los datos almacenados y verificamos la corrección de ambas celdas. Si la celda no pasa la verificación del identificador o suma de verificación correctos, restablecemos el bit de corrección. Las operaciones de escritura posteriores pueden no verificar la corrección de las celdas, pero se basan en este indicador, que reduce la sobrecarga en aproximadamente 2 veces.
Aquellos que quieran profundizar en los detalles de implementación pueden ver las imágenes y el código en el
repositorio . Yo, para no aburrir al lector, paso al uso. Las funciones de escritura / lectura de datos reciben cada una 5 parámetros, por lo que se sacrifica la conveniencia de su uso en favor de la flexibilidad. Pero está generosamente compensado por dos conjuntos de macros, que hacen que el uso de la biblioteca sea tan simple como en el caso de EEPROM.get / put. El primer conjunto de macros se usa si solo desea guardar la variable en la dirección dada:
#include <NvTx.h> int my_var = DEFAULT_VALUE; bool have_my_var = NvTxGetAt(my_var, MY_VAR_ADDR); my_var = NEW_VALUE; NvTxPutAt(my_var, MY_VAR_ADDR);
Si hay varias variables para guardar, cada una tendrá que determinar la dirección y, al mismo tiempo, considerar correctamente el tamaño para que las áreas de memoria donde se almacenan las variables no se superpongan. Para simplificar la tarea, el segundo conjunto de macros implementa la asignación automática de direcciones, y lo hace
en tiempo de compilación . Por ejemplo, la
biblioteca Arduino-EEPROMEx puede asignar memoria en tiempo de ejecución, mientras almacena la dirección en RAM para cada variable almacenada. La biblioteca
NvTx asigna espacio en la EEPROM sin agregar nada al código ejecutable o al contenido de la RAM.
#include <NvTx.h> int my_var = DEFAULT_VALUE; char my_string[16] = ""; NvPlace(my_var, MY_START_ADDR, MY_INST_ID); NvAfter(my_string, my_var); bool have_my_var = NvTxGet(my_var); my_var = NEW_VALUE; NvTxPut(my_var);
La macro NvPlace establece la dirección de inicio del área EEPROM, donde almacenaremos las variables y el identificador de instancia. La macro NvAfter reserva una región de memoria para almacenar su primer argumento inmediatamente después de la región de memoria reservada para el segundo. Al asignar memoria, también se verifica que no excedimos el tamaño de EEPROM disponible y que no reservamos áreas de memoria superpuestas (esto puede suceder si dos macros NvAfter tienen el mismo segundo argumento). En caso de violación de cualquiera de las dos condiciones especificadas, el programa simplemente no compila. Aquellos que quieran lidiar con el mecanismo de asignación de memoria lo encontrarán en el archivo de encabezado
NvTx.h. Todo lo que hacen las macros NvPlace y NvAfter es definir las enumeraciones, formando sus nombres basados en los nombres de las variables, y también usar la construcción idiomática muy útil de la
afirmación de tiempo de compilación .
Esperemos que la biblioteca
NvTx ayude a los lectores a escribir código confiable de grado industrial.