PROGMEM: apakah saya harus menyalin data dari flash ke RAM untuk membaca?

8

Saya mengalami kesulitan memahami manajemen memori.

Dokumentasi Arduino mengatakan, adalah mungkin untuk menjaga konstanta seperti string atau apa pun yang saya tidak ingin ubah selama runtime dalam memori program. Saya pikir itu tertanam di suatu tempat di segmen kode, yang harus cukup mungkin di dalam arsitektur von-Neumann. Saya ingin memanfaatkan itu untuk membuat menu UI saya pada LCD mungkin.

Tapi saya bingung dengan instruksi itu untuk hanya membaca dan mencetak data dari memori program:

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

Kenapa saya harus menyalin konten sialan ke RAM sebelum mengaksesnya? Dan jika ini benar, lalu apa yang terjadi pada semua kode? Apakah ini juga dimuat ke RAM sebelum eksekusi? Bagaimana kode (32kiB) ditangani kemudian dengan hanya 2kiB RAM? Di mana goblin-goblin kecil itu membawa disket?

Dan yang lebih menarik: Apa yang terjadi pada konstanta literal seperti dalam ungkapan ini:

a = 5*(10+7)

Apakah 5, 10 dan 7 benar-benar disalin ke RAM sebelum memuatnya ke dalam register? Aku tidak percaya itu.

Ariser - mengembalikan Monica
sumber
Variabel global dimuat ke dalam memori, dan tidak pernah dilepaskan darinya. Kode di atas hanya menyalin data ke memori bila diperlukan, dan melepaskannya setelah selesai. Perhatikan juga bahwa kode di atas hanya membaca satu byte dari string_tablearray. Array itu mungkin 20KB, dan tidak akan pernah muat dalam memori (bahkan sementara). Namun Anda dapat memuat hanya satu indeks menggunakan metode di atas.
Gerben
@Gerben: Ini adalah kelemahan nyata pada variabel global, saya belum mempertimbangkan ini. Saya sakit kepala sekarang. Dan potongan kode hanyalah contoh dari dokumentasi. Saya menahan diri untuk memprogram sth. sendiri sebelum memiliki klarifikasi tentang konsep-konsep tersebut. Tapi saya punya wawasan sekarang. Terima kasih!
Ariser - mengembalikan Monica
Saya menemukan dokumentasi agak membingungkan ketika saya pertama kali membacanya. Coba lihat beberapa contoh kehidupan nyata juga (seperti perpustakaan misalnya).
Gerben

Jawaban:

10

AVR adalah keluarga arsitektur Harvard yang dimodifikasi , jadi kode disimpan dalam flash saja, sedangkan data ada terutama dalam RAM ketika sedang dimanipulasi.

Dengan mengingat hal itu, mari kita jawab pertanyaan Anda.

Kenapa saya harus menyalin konten sialan ke RAM sebelum mengaksesnya?

Anda tidak perlu per se, tetapi dengan kode default mengasumsikan bahwa data ada dalam RAM kecuali jika kode tersebut dimodifikasi untuk secara khusus mencari di flash untuk itu (seperti dengan strcpy_P()).

Dan jika ini benar, lalu apa yang terjadi pada semua kode? Apakah ini juga dimuat ke RAM sebelum eksekusi?

Nggak. Arsitektur Harvard. Lihat halaman Wikipedia untuk detail selengkapnya.

Bagaimana kode (32kiB) ditangani kemudian dengan hanya 2kiB RAM?

Pembukaan yang dihasilkan oleh kompiler menyalin data yang harus dimodifikasi / dimodifikasi menjadi SRAM sebelum menjalankan program yang sebenarnya.

Di mana goblin-goblin kecil itu membawa disket?

Tidak tahu Tetapi jika Anda melihat mereka maka tidak ada yang bisa saya lakukan untuk membantu.

... apakah 5, 10 dan 7 benar-benar disalin ke RAM sebelum memuatnya ke dalam register?

Tidak Kompiler mengevaluasi ekspresi pada waktu kompilasi. Apa pun yang terjadi tergantung pada baris kode di sekitarnya.

Ignacio Vazquez-Abrams
sumber
Ok, saya tidak tahu AVR adalah harvard. Tapi saya akrab dengan konsep itu. Selain goblin, saya pikir saya tahu kapan harus menggunakan fungsi-fungsi salin sekarang. Saya harus membatasi penggunaan PROGMEM untuk data yang jarang digunakan untuk menghemat siklus CPU.
Ariser - mengembalikan Monica
Atau modifikasi kode Anda untuk menggunakannya langsung dari flash.
Ignacio Vazquez-Abrams
Tapi bagaimana kode ini terlihat? katakanlah saya memiliki beberapa array dari uint8_t yang mewakili string yang ingin saya tampilkan ke layar LCD melalui 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};bagaimana cara membawa data ini ke flash dan kemudian ke fungsi SPI.transfer (), yang membutuhkan satu uint8_t per panggilan.
Ariser - mengembalikan Monica
8

Ini adalah cara Print::printmencetak dari memori program di perpustakaan 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*adalah kelas kosong yang memungkinkan fungsi kelebihan beban seperti cetak untuk membedakan pointer ke memori program dari satu ke memori normal, karena keduanya dilihat const char*oleh kompiler (lihat /programming/16597437/arduino-f- apa-apa-itu-sebenarnya-lakukan )

Jadi Anda bisa membebani printfungsi untuk layar LCD Anda sehingga butuh __FlashStringHelper*argumen, sebut saja LCD::print, lalu gunakan lcd.print(F("this is a string in progmem"));' to call it.F () `adalah makro yang memastikan string ada di memori program.

Untuk menetapkan standar string (agar kompatibel dengan cetak Arduino bawaan) saya telah menggunakan:

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

Saya pikir alternatif akan menjadi sesuatu seperti

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;
}

yang akan menghindari para __FlashStringHelperpemain.

geometrikal
sumber
2

Dokumentasi Arduino mengatakan, adalah mungkin untuk menjaga konstanta seperti string atau apa pun yang saya tidak ingin ubah selama runtime dalam memori program.

Semua konstanta awalnya dalam memori program. Di mana lagi mereka berada ketika listrik mati?

Saya pikir itu tertanam di suatu tempat di segmen kode, yang harus cukup mungkin di dalam arsitektur von-Neumann.

Ini sebenarnya arsitektur Harvard .

Kenapa saya harus menyalin konten sialan ke RAM sebelum mengaksesnya?

Kamu tidak. Bahkan ada instruksi perangkat keras (LPM - Load Program Memory) yang memindahkan data langsung dari memori program ke register.

Saya punya contoh teknik ini dalam output Arduino Uno ke monitor VGA . Dalam kode itu ada font bitmap yang disimpan dalam memori program. Itu dibaca dari on-the-fly dan disalin ke output seperti ini:

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

Pembongkaran garis-garis tersebut menunjukkan (sebagian):

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

Anda dapat melihat bahwa satu byte memori program disalin ke R30, dan kemudian segera disimpan ke dalam register USART UDR0. Tidak ada RAM yang terlibat.


Namun ada kerumitan. Untuk string normal, kompiler mengharapkan untuk menemukan data dalam RAM bukan PROGMEM. Mereka adalah ruang alamat yang berbeda, dan karena itu 0x200 dalam RAM adalah sesuatu yang berbeda dari 0x200 dalam PROGMEM. Jadi kompiler mengalami kesulitan menyalin konstanta (seperti string) ke dalam RAM pada saat startup program, sehingga tidak perlu khawatir mengetahui perbedaannya nanti.

Bagaimana kode (32kiB) ditangani kemudian dengan hanya 2kiB RAM?

Pertanyaan bagus. Anda tidak akan lolos dengan string konstan lebih dari 2 KB, karena tidak akan ada ruang untuk menyalin semuanya.

Itu sebabnya orang-orang yang menulis hal-hal seperti menu dan hal-hal bertele-tele lainnya, mengambil langkah-langkah tambahan untuk memberikan string atribut PROGMEM, yang menonaktifkan mereka disalin ke dalam RAM.

Tapi saya bingung dengan instruksi itu untuk hanya membaca dan mencetak data dari memori program:

Jika Anda menambahkan atribut PROGMEM Anda harus mengambil langkah-langkah untuk memberi tahu kompilator bahwa string ini berada dalam ruang alamat yang berbeda. Membuat salinan lengkap (sementara) adalah satu cara. Atau hanya mencetak langsung dari PROGMEM, satu byte setiap kali. Contohnya adalah:

// 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

Jika Anda melewatkan fungsi ini sebagai penunjuk ke string di PROGMEM, ia melakukan "pembacaan khusus" (pgm_read_byte) untuk menarik data dari PROGMEM daripada RAM, dan mencetaknya. Perhatikan bahwa ini membutuhkan satu siklus clock tambahan, per byte.

Dan yang lebih menarik: Apa yang terjadi pada konstanta literal seperti dalam ungkapan ini a = 5*(10+7)adalah 5, 10 dan 7 benar-benar disalin ke RAM sebelum memuatnya ke dalam register? Aku tidak percaya itu.

Tidak, karena mereka tidak harus seperti itu. Itu akan dikompilasi menjadi instruksi "muat literal ke register". Instruksi itu sudah ada dalam PROGMEM, jadi literal sekarang ditangani. Tidak perlu menyalinnya ke RAM dan kemudian membacanya kembali.


Saya memiliki deskripsi panjang tentang hal-hal ini di halaman Menempatkan data konstan ke dalam memori program (PROGMEM) . Itu memiliki kode contoh untuk mengatur string, dan array string, cukup mudah.

Itu juga menyebutkan makro F () yang merupakan cara mudah hanya mencetak dari PROGMEM:

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

Sedikit kompleksitas preprosesor memungkinkan kompilasi menjadi fungsi pembantu yang menarik byte dalam string dari PROGMEM byte pada suatu waktu. Tidak diperlukan penggunaan menengah RAM.

Cukup mudah untuk menggunakan teknik itu untuk hal-hal selain Serial (mis. LCD Anda) dengan menurunkan pencetakan LCD dari kelas Print.

Sebagai contoh, di salah satu perpustakaan LCD yang saya tulis, saya melakukan hal itu:

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

Poin kuncinya di sini adalah untuk memperoleh dari Print, dan menimpa fungsi "write". Sekarang fungsi utama Anda melakukan apa pun yang dibutuhkan untuk menghasilkan karakter. Karena berasal dari Print, Anda sekarang dapat menggunakan makro F (). misalnya.

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