PROGMEM: tenho que copiar dados do flash para a RAM para ler?

8

Eu tenho algumas dificuldades para entender o gerenciamento de memória.

A documentação do Arduino diz que é possível manter constantes como seqüências de caracteres ou o que eu não quiser alterar durante o tempo de execução na memória do programa. Penso nisso como incorporado em algum lugar do segmento de código, o que deve ser razoavelmente possível dentro de uma arquitetura von-Neumann. Quero fazer uso disso para tornar possível o meu menu de interface do usuário em um LCD.

Mas fico perplexo com essas instruções para apenas ler e imprimir dados da memória do programa:

strcpy_P(buffer, (char*)pgm_read_word(&(string_table[i]))); // Necessary casts and dereferencing, just copy. 
    Serial.println( buffer );

Por que diabos eu tenho que copiar o conteúdo maldito para a RAM antes de acessá-lo? E se isso for verdade, o que acontece com todo o código? Também é carregado na RAM antes da execução? Como o código (32kiB) é tratado com apenas 2kiB de RAM? Onde estão aqueles duendes carregando disquetes?

E ainda mais interessante: o que acontece com constantes literais como nesta expressão:

a = 5*(10+7)

5, 10 e 7 são realmente copiados para a RAM antes de carregá-los nos registros? Eu simplesmente não posso acreditar nisso.

Ariser - restabelecer Monica
fonte
Uma variável global é carregada na memória e nunca é liberada dela. O código acima apenas copia os dados para a memória quando necessário e os libera quando concluídos. Observe também que o código acima lê apenas um byte da string_tablematriz. Essa matriz pode ter 20 KB e nunca caberia na memória (mesmo que temporariamente). No entanto, você pode carregar apenas um índice usando o método acima.
Gerben 12/10
@ Gerben: Esta é uma verdadeira desvantagem nas variáveis ​​globais, eu não levei isso em consideração ainda. Estou com dor de cabeça agora. E o trecho de código foi apenas um exemplo da documentação. Eu me abstive de programar sth. antes de ter esclarecimentos sobre os conceitos. Mas eu tenho algumas dicas agora. Obrigado!
Ariser - reinstala Monica
Achei a documentação um pouco confusa quando a li pela primeira vez. Tente ver também alguns exemplos da vida real (como, por exemplo, uma biblioteca).
Gerben 13/10

Respostas:

10

O AVR é ​​uma família de arquitetura Harvard modificada , portanto, o código é armazenado apenas em flash, enquanto os dados existem principalmente na RAM ao serem manipulados.

Com isso em mente, vamos abordar suas perguntas.

Por que diabos eu tenho que copiar o conteúdo maldito para a RAM antes de acessá-lo?

Você não precisa fazer isso por si só, mas, por padrão, o código pressupõe que os dados estejam na RAM, a menos que o código seja modificado para procurá-lo especificamente em flash (como com strcpy_P()).

E se isso for verdade, o que acontece com todo o código? Também é carregado na RAM antes da execução?

Não. Arquitetura de Harvard. Veja a página da Wikipedia para mais detalhes.

Como o código (32kiB) é tratado com apenas 2kiB de RAM?

O preâmbulo gerado pelo compilador copia os dados que devem ser modificáveis ​​/ modificados na SRAM antes de executar o programa real.

Onde estão aqueles duendes carregando disquetes?

Não sei. Mas se você os vir, não há nada que eu possa fazer para ajudar.

... 5, 10 e 7 são realmente copiados para a RAM antes de carregá-los nos registros?

Nah. O compilador avalia a expressão em tempo de compilação. O que quer que aconteça depende das linhas de código ao seu redor.

Ignacio Vazquez-Abrams
fonte
Ok, eu não sabia que o AVR era harvard. Mas estou familiarizado com esse conceito. Os goblins de lado, acho que sei quando usar essas funções de cópia agora. Eu tenho que restringir o uso do PROGMEM aos dados que raramente são usados ​​para salvar os ciclos da CPU.
Ariser - reinstala Monica
Ou modifique seu código para usá-lo diretamente do flash.
Ignacio Vazquez-Abrams
Mas como seria esse código? digamos que eu tenha várias matrizes de uint8_t representando as strings que eu quero colocar em um monitor LCD via SPI. const uint8_t test1[5]= { 0x54, 0x65, 0x73, 0x74, 0x31 }; const uint8_t bla[9]= { 0x62, 0x6c, 0x61, 0x62, 0x6c, 0x61, 0x62, 0x6c, 0x62 }; const uint8_t Menu[4]= { 0x3d, 0x65, 0x6e, 0x75};como faço para trazer esses dados para piscar e mais tarde para a função SPI.transfer (), que leva um uint8_t por chamada.
Ariser - reinstala Monica
8

É assim que as Print::printimpressões são impressas na memória do programa na biblioteca do Arduino:

size_t Print::print(const __FlashStringHelper *ifsh)
{
  const char PROGMEM *p = (const char PROGMEM *)ifsh;
  size_t n = 0;
  while (1) {
    unsigned char c = pgm_read_byte(p++);
    if (c == 0) break;
    n += write(c);
  }
  return n;
}

__FlashStringHelper*é uma classe vazia que permite que funções sobrecarregadas, como a impressão, diferenciem um ponteiro para programar a memória de uma para a memória normal, pois ambas são vistas const char*pelo compilador (consulte /programming/16597437/arduino-f- o que-faz-realmente-faz )

Assim, você pode sobrecarregar a printfunção do seu monitor LCD para que ele pegue um __FlashStringHelper*argumento, vamos chamá-lo LCD::printe, em seguida, use lcd.print(F("this is a string in progmem"));' to call it.F () `é uma macro que garante que a string esteja na memória do programa.

Para predefinir a string (para ser compatível com a impressão interna do Arduino), usei:

const char firmware_version_s[] PROGMEM = {"1.0.2"};
__FlashStringHelper* firmware_version = (__FlashStringHelper*) firmware_version_s;
...
Serial.println(firmware_version);

Eu acho que uma alternativa seria algo como

size_t LCD::print_from_flash(const char *pgms)
{
  const char PROGMEM *p = (const char PROGMEM *) pgms;
  size_t n = 0;
  while (1) {
    unsigned char c = pgm_read_byte(p++);
    if (c == 0) break;
    n += write(c);
  }
  return n;
}

o que evitaria o __FlashStringHelperelenco.

geometrikal
fonte
2

A documentação do Arduino diz que é possível manter constantes como seqüências de caracteres ou o que eu não quiser alterar durante o tempo de execução na memória do programa.

Todas as constantes estão inicialmente na memória do programa. Onde mais eles estariam quando a energia estiver desligada?

Penso nisso como incorporado em algum lugar do segmento de código, o que deve ser razoavelmente possível dentro de uma arquitetura von-Neumann.

Na verdade, é a arquitetura de Harvard .

Por que diabos eu tenho que copiar o conteúdo maldito para a RAM antes de acessá-lo?

Você não De fato, há uma instrução de hardware (LPM - Load Program Memory) que move os dados diretamente da memória do programa para um registro.

Eu tenho um exemplo dessa técnica na saída do Arduino Uno para o monitor VGA . Nesse código, há uma fonte de bitmap armazenada na memória do programa. É lido a partir do momento e copiado para a saída da seguinte maneira:

  // blit pixel data to screen    
  while (i--)
    UDR0 = pgm_read_byte (linePtr + (* messagePtr++));

Uma desmontagem dessas linhas mostra (em parte):

  f1a:  e4 91           lpm r30, Z+
  f1c:  e0 93 c6 00     sts 0x00C6, r30

Você pode ver que um byte da memória do programa foi copiado no R30 e, em seguida, armazenado imediatamente no registro USART UDR0. Nenhuma RAM envolvida.


No entanto, há uma complexidade. Para seqüências normais, o compilador espera encontrar dados na RAM e não PROGMEM. São espaços de endereço diferentes e, portanto, 0x200 na RAM é algo diferente de 0x200 no PROGMEM. Portanto, o compilador se preocupa em copiar constantes (como seqüências de caracteres) na RAM na inicialização do programa, para que não precise se preocupar em saber a diferença posteriormente.

Como o código (32kiB) é tratado com apenas 2kiB de RAM?

Boa pergunta. Você não terá mais de 2 KB de seqüências constantes, porque não haverá espaço para copiá-las todas.

É por isso que as pessoas que escrevem coisas como menus e outras coisas prolixo, tomam medidas extras para atribuir às seqüências o atributo PROGMEM, que as impede de serem copiadas na RAM.

Mas fico perplexo com essas instruções para apenas ler e imprimir dados da memória do programa:

Se você adicionar o atributo PROGMEM, precisará executar etapas para que o compilador saiba que essas seqüências de caracteres estão em um espaço de endereço diferente. Fazer uma cópia completa (temporária) é uma maneira. Ou apenas imprima diretamente do PROGMEM, um byte de cada vez. Um exemplo disso é:

// Print a string from Program Memory directly to save RAM 
void printProgStr (const char * str)
{
  char c;
  if (!str) 
    return;
  while ((c = pgm_read_byte(str++)))
    Serial.print (c);
} // end of printProgStr

Se você passar essa função como ponteiro para uma seqüência de caracteres no PROGMEM, ela fará a "leitura especial" (pgm_read_byte) para extrair os dados do PROGMEM, e não da RAM, e os imprimirá. Observe que isso leva um ciclo de clock adicional, por byte.

E ainda mais interessante: o que acontece com constantes literais como nesta expressão a = 5*(10+7)são 5, 10 e 7 realmente copiadas para a RAM antes de carregá-las nos registradores? Eu simplesmente não posso acreditar nisso.

Não, porque eles não precisam ser. Isso seria compilado em uma instrução "carregar literal no registrador". Essa instrução já está em PROGMEM, então o literal agora é tratado. Não é necessário copiá-lo para a RAM e depois lê-lo novamente.


Tenho uma descrição extensa dessas coisas na página Colocando dados constantes na memória do programa (PROGMEM) . Que possui um código de exemplo para configurar strings e matrizes de strings, razoavelmente facilmente.

Ele também menciona a macro F (), que é uma maneira fácil de simplesmente imprimir a partir de PROGMEM:

Serial.println (F("Hello, world"));

Um pouco de complexidade do pré-processador permite que ele seja compilado em uma função auxiliar, que extrai os bytes da string do PROGMEM, um byte por vez. Nenhum uso intermediário de RAM é necessário.

É bastante fácil usar essa técnica para outras coisas que não Serial (por exemplo, seu LCD) derivando a impressão de LCD da classe Print.

Como exemplo, em uma das bibliotecas de LCD que escrevi, fiz exatamente isso:

class I2C_graphical_LCD_display : public Print
{
...
    size_t write(uint8_t c);
};

O ponto principal aqui é derivar do Print e substituir a função "write". Agora sua função substituída faz o que for necessário para gerar um caractere. Como é derivado do Print, agora você pode usar a macro F (). por exemplo.

lcd.println (F("Hello, world"));
Nick Gammon
fonte