Salvando Dados em uma EEPROM em um Arduino Transacionalmente

A presença de uma EEPROM oferece aos desenvolvedores uma ferramenta conveniente para salvar parâmetros de configuração ou um estado de mudança lenta em que uma queda de energia deve sobreviver. Neste artigo, veremos como fazer isso da maneira mais segura e conveniente possível, para não esquecer nada e não lembrar o que não estava lá.

Suponha que temos uma variável e queremos armazená-la em uma EEPROM. Parece que todas as ferramentas para isso estão em nossas mãos:

#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); 

No entanto, uma análise mais detalhada revela que essa abordagem cria mais problemas do que resolve. Vamos discuti-los em ordem.

1. Como garantir que lemos exatamente o que escrevemos (para garantir a integridade )? Imagine a seguinte imagem. Escrevemos uma carta para nós mesmos em caso de morte súbita por perda de energia ou um sinal de redefinição e a colocamos em uma gaveta da mesa. Na próxima vida, abrimos a gaveta da mesa, pegamos um pedaço de papel, lemos a mensagem e continuamos nossa missão. O problema é que na caixa sempre há folhas de papel rabiscadas com texto aleatório. Então, precisamos de uma maneira de distinguir a mensagem correta da aleatória. Pode-se garantir a ele um notário público, mas, no caso mais simples, sua assinatura seria suficiente se tivéssemos uma maneira de verificar sua correção. Por exemplo, podemos usar o resultado de uma expressão matemática dependendo do texto como uma assinatura, para que a probabilidade de coincidência aleatória seja suficientemente pequena. No caso mais simples, esse é um CRC ou soma de verificação. Isso nos protegerá não apenas da leitura do que não escrevemos, mas também da leitura de uma mensagem danificada. Afinal, o texto desaparece com o tempo e os elétrons no obturador isolado são ainda menos duráveis ​​- uma partícula voa do espaço com energia suficiente e o bit muda. Mas há outra maneira de obter uma mensagem danificada - não é para adicioná-la ao final. Não é tão exótico, porque no momento da gravação, o consumo atual aumenta acentuadamente, o que pode provocar a morte prematura do escritor.

2. Suponha que estivéssemos convencidos da exatidão da mensagem, mas como posso ter certeza de que fui eu quem a escreveu (para garantir a autenticidade ). Como diz o ditado, sou diferente. De repente, alguém estava sentado nessa mesa antes da minha reencarnação, e ele teve uma missão diferente, e por que razão agora serei guiado por suas mensagens? Se fornecêssemos nossas anotações com um certo rótulo, seria mais fácil distinguir as nossas de estranhos. Por exemplo, esse rótulo pode ser o nome da variável que estamos salvando. O único problema é que na EEPROM não há muito espaço para colocar nomes de variáveis ​​e é inconveniente fazê-lo, porque eles têm comprimentos diferentes. Felizmente, porém, existe uma maneira mais simples: você pode calcular a soma de verificação em nome da variável e usá-la como um atalho. Ao mesmo tempo, é útil adicionar o tamanho da variável em bytes a essa soma de verificação para não ler acidentalmente a quantidade errada. Bem, por uma questão de completude, adicionamos outro identificador numérico para garantir a distinção de nossa variável de outra pessoa, mesmo que sejam chamadas da mesma. Chamamos esse número de identificador de instância (inspirado no OOP se o nome da variável for considerado como um campo de objeto). Se alguma vez atualizarmos nossa missão para uma versão radicalmente nova, para que esta atualização faça sentido tudo o que a antiga salvou, basta alterar o identificador da instância para invalidar tudo o que foi salvo pela versão antiga.

3. Como faço para que uma operação de gravação incompleta deixe o antigo valor armazenado inalterado? Ou seja, a operação de salvamento deve ter êxito ou não deve ter nenhum efeito observável. Em outras palavras, deve ser atômico ou transacional se estamos falando de uma transação que se resume a uma atualização incondicional de um único valor. Obviamente, não podemos garantir a atomicidade do registro reescrevendo o valor anterior; precisamos gravar em um novo local para que o antigo valor armazenado permaneça intacto, pelo menos até a conclusão da gravação do novo. Essa técnica costuma ser chamada de 'copiar na gravação' se apenas parte do valor salvo for atualizada, mas a parte que permanece inalterada ainda é copiada e gravada em um novo local. Desenvolvendo nossa analogia, escreveremos cartas para nós mesmos, deixando as antigas intocadas, mas fornecendo a cada letra um número de série crescente para que, em nossa próxima vida, tenhamos a oportunidade de encontrar a última carta que escrevemos. Ao mesmo tempo, porém, surge um novo problema - o local na caixa em que colocamos as cartas terminará mais cedo ou mais tarde se não jogarmos fora as cartas antigas que se tornaram irrelevantes. É fácil entender que basta armazenar apenas duas letras - uma antiga e outra nova, pode estar em processo de escrita. Consequentemente, o número da letra também não precisa de muitos bits.

Curiosamente, o autor não conseguiu encontrar uma única implementação que permitisse a organização do armazenamento de dados na EEPROM, garantindo integridade, autenticidade e atomicidade. Eu tive que escrever no github.com/olegv142/NvTx

Para salvar cada variável na EEPROM, são usadas 2 áreas consecutivas - células com a mesma estrutura. O identificador da variável calculada com base em seu tamanho, rótulo de texto e identificador de instância é gravado nos 2 primeiros bytes. Em seguida, os dados são gravados, seguidos por 2 bytes da soma de verificação. No primeiro byte, dois bits têm um propósito especial. O bit mais significativo é o sinalizador de correção; ao escrever, é sempre definido como um. O bit de ordem baixa é usado como um número de bit único da época; é necessário encontrar a última mensagem. A gravação é feita nas células 'em um círculo'. O número da época muda cada vez que um registro é feito na primeira célula. Daí o algoritmo para determinar a última célula registrada: se as épocas das células são as mesmas, então a segunda é escrita por último, se diferente - então a primeira.

O bit de correção parece redundante, mas tem uma função importante. Antes de tudo, lemos os dados armazenados e verificamos a correção de ambas as células. Se a célula não passar na verificação do identificador ou soma de verificação correta, redefiniremos o bit de correção. As operações de gravação subseqüentes podem não verificar a correção das células, mas dependem desse sinalizador, o que reduz a sobrecarga em cerca de 2 vezes.

Quem quiser se aprofundar nos detalhes da implementação pode ver as imagens e o código no repositório . Eu, para não aborrecer o leitor, passo a usá-lo. Cada uma das funções de gravação / leitura de dados recebe 5 parâmetros; portanto, a conveniência de seu uso é sacrificada em favor da flexibilidade. Mas é generosamente compensado por dois conjuntos de macros, que tornam o uso da biblioteca tão simples quanto no caso da EEPROM.get / put. O primeiro conjunto de macros é usado se você deseja salvar a variável no endereço fornecido:

 #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); 

Se houver várias variáveis ​​a serem salvas, cada uma terá que determinar o endereço e, ao mesmo tempo, considerar corretamente o tamanho para que as áreas de memória em que as variáveis ​​estão armazenadas não se sobreponham. Para simplificar a tarefa, o segundo conjunto de macros implementa a alocação automática de endereço e faz isso em tempo de compilação . Por exemplo, a biblioteca Arduino-EEPROMEx pode alocar memória em tempo de execução, enquanto armazena o endereço na RAM para cada variável armazenada. A biblioteca NvTx aloca espaço na EEPROM sem adicionar nada ao código executável ou ao conteúdo da 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); 

A macro NvPlace define o endereço inicial da área EEPROM, onde armazenaremos as variáveis ​​e o identificador da instância. A macro NvAfter reserva uma região da memória para armazenar seu primeiro argumento imediatamente após a região da memória reservada para o segundo. Ao alocar memória, também é verificado que não excedemos o tamanho da EEPROM disponível e também não reservamos áreas de memória sobrepostas (isso pode acontecer se duas macros do NvAfter tiverem o mesmo segundo argumento). Em caso de violação de qualquer uma das duas condições especificadas, o programa simplesmente não é compilado. Aqueles que desejam lidar com o mecanismo de alocação de memória o encontrarão no arquivo de cabeçalho NvTx.h. Todas as macros NvPlace e NvAfter fazem é definir as enumerações, formando seus nomes com base nos nomes das variáveis ​​e também usar a construção idiomática muito útil da declaração do tempo de compilação .

Esperamos que a biblioteca NvTx ajude os leitores a escrever um código confiável e de nível industrial.

Source: https://habr.com/ru/post/pt482918/


All Articles