Saya mengajari siswa saya cara bekerja dengan mikrokontroler STM32F411RE, yang papan tulisnya memiliki ROM sebanyak 512 kB dan RAM 128 kB
Biasanya, sebuah program ditulis ke
ROM pada mikrokontroler ini, dan data variabel sangat sering dibutuhkan dalam
RAM sehingga konstanta berada dalam
ROM .
Dalam STM32F411RE,
ROM mikrokontroler, memori terletak di alamat dengan
0x08000000 ... 0x0807FFFF , dan
RAM dengan
0x20000000 ... 0x2001FFFF.Dan jika semua pengaturan linker benar, siswa menghitung bahwa dalam kode langsung seperti itu konstanta terletak pada
ROM :
class WantToBeInROM { private: int i; public: WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; }
Anda juga dapat mencoba menjawab pertanyaan: di mana konstanta myConstInROM dalam
ROM atau dalam
RAM ?
Jika Anda menjawab pertanyaan ini bahwa dalam
ROM , saya ucapkan selamat kepada Anda, bahkan kemungkinan besar Anda salah, konstanta umumnya terletak pada
RAM, dan untuk mengetahui cara menempatkan konstanta Anda dengan benar dan benar di
ROM - selamat datang di cat.
Pendahuluan
Pertama, penyimpangan kecil, mengapa repot-repot tentang ini.
Saat mengembangkan perangkat lunak penting keselamatan untuk alat ukur yang mematuhi IEC 61508-3: 2010 atau setara dengan domestik dari
GOST IEC 61508-3-2018 , sejumlah poin harus diperhitungkan yang tidak penting untuk perangkat lunak konvensional.
Pesan utama dari standar ini adalah bahwa perangkat lunak harus mendeteksi setiap kegagalan yang mempengaruhi keandalan sistem dan menempatkan sistem dalam mode "crash".Selain kegagalan mekanis yang jelas, misalnya, kegagalan atau degradasi sensor dan kegagalan komponen elektronik, kesalahan yang disebabkan oleh kegagalan lingkungan perangkat lunak, misalnya, mikrokontroler
RAM atau
ROM , harus dideteksi.
Dan jika dalam dua kasus pertama, dimungkinkan untuk mendeteksi kesalahan hanya dengan cara tidak langsung yang agak membingungkan (ada algoritma yang menentukan kegagalan sensor, misalnya,
Metode menilai keadaan konverter termal resistansi ), maka jika terjadi kegagalan lingkungan perangkat lunak, ini dapat dilakukan lebih mudah, misalnya, kegagalan memori dapat verifikasi dengan pemeriksaan integritas data sederhana. Jika integritas data dilanggar, maka tafsirkan ini sebagai kegagalan memori.
Jika data untuk waktu yang lama terletak pada RAM tanpa memeriksa dan memperbarui, maka kemungkinan sesuatu akan terjadi pada mereka karena kegagalan
RAM menjadi lebih tinggi dari waktu ke waktu. Contohnya adalah beberapa koefisien kalibrasi untuk menghitung suhu yang ditetapkan di pabrik dan ditulis ke EEPROM eksternal, saat startup mereka dibaca dan ditulis ke
RAM dan ada di sana sampai daya dimatikan. Dan dalam kehidupan, sensor suhu dapat bekerja sepanjang periode interval antar-kalibrasi, hingga 3-5 tahun. Jelas, data
RAM semacam itu harus dilindungi dan secara berkala diperiksa integritasnya.
Tetapi ada juga data, seperti konstanta yang dideklarasikan hanya untuk keterbacaan, objek driver LCD, SPI atau I2C, yang tidak boleh diubah, dibuat sekali dan tidak dihapus sampai daya dimatikan.
Data ini paling baik disimpan dalam
ROM . Ini lebih dapat diandalkan dari sudut pandang teknologi dan lebih mudah untuk memeriksanya, cukup untuk secara berkala membaca checksum dari seluruh memori read-only dalam beberapa tugas prioritas rendah. Jika checksum tidak cocok, Anda dapat melaporkan kegagalan
ROM dan sistem diagnostik akan menampilkan kecelakaan.
Jika data ini terletak pada
RAM , itu akan menjadi masalah atau bahkan tidak mungkin untuk menentukan integritasnya karena fakta bahwa tidak jelas di mana data yang tidak dapat diubah dalam RAM dan di mana itu bisa berubah, linker menempatkannya seperti yang diinginkan, dan untuk melindungi setiap objek RAM dengan checksum terlihat seperti paranoia.
Oleh karena itu, cara termudah adalah menjadi 100% yakin bahwa data konstan dalam
ROM . Cara melakukan ini saya ingin coba jelaskan. Tetapi pertama-tama Anda perlu berbicara tentang pengaturan memori di ARM.
Organisasi memori
Seperti yang Anda ketahui, inti ARM memiliki arsitektur Harvard - data dan kode bus dipisahkan. Biasanya ini berarti diasumsikan ada memori terpisah untuk program dan memori terpisah untuk data. Tetapi kenyataannya adalah bahwa ARM adalah arsitektur Harvard yang dimodifikasi, mis. akses ke memori dilakukan pada satu bus, dan perangkat manajemen memori sudah menyediakan pemisahan bus menggunakan sinyal kontrol: baca, tulis atau pilih area memori.
Dengan demikian, data dan kode dapat berada di area memori yang sama. Dalam ruang alamat tunggal ini dapat ditemukan dan memori
ROM dan
RAM dan periferal. Dan ini berarti bahwa sebenarnya kedua kode dan data bisa mendapatkan meskipun tergantung pada kompiler dan penghubung.
Oleh karena itu, untuk membedakan antara area memori untuk
ROM (Flash) dan
RAM, mereka biasanya ditunjukkan dalam pengaturan linker, misalnya, dalam IAR 8.40.1, terlihat seperti ini:
define symbol __ICFEDIT_region_ROM_start__ = 0x08000000; define symbol __ICFEDIT_region_ROM_end__ = 0x0807FFFF; define symbol __ICFEDIT_region_RAM_start__ = 0x20000000; define symbol __ICFEDIT_region_RAM_end__ = 0x2001FFFF; define region ROM_region = mem:[from __ICFEDIT_region_ROM_start__ to __ICFEDIT_region_ROM_end__]; define region RAM_region = mem:[from __ICFEDIT_region_RAM_start__ to __ICFEDIT_region_RAM_end__];
RAM dalam mikrokontroler ini terletak di
0x20000000 ... 0x2001FFF, dan
ROM di
0x008000000 ... 0x0807FFFF .
Anda dapat dengan mudah mengubah alamat awal ROM_start ke alamat RAM, katakanlah RAM_start dan alamat akhir ROM_end__ ke RAM_end__ dan program Anda akan sepenuhnya berada dalam RAM.
Anda bahkan dapat melakukan yang sebaliknya dan menentukan
RAM di area memori
ROM , dan program Anda akan berhasil merakit dan mem-flash, meskipun tidak akan berhasil :)
Beberapa mikrokontroler, seperti AVR, pada awalnya memiliki ruang alamat terpisah untuk memori program, memori data dan periferal, dan karenanya trik seperti itu tidak akan berfungsi di sana, dan program ini ditulis ke
ROM secara default.
Semua ruang alamat di CortexM adalah tunggal, dan kode dan data dapat ditemukan di mana saja. Menggunakan pengaturan tautan, Anda dapat mengatur wilayah untuk alamat ROM dan RAM . IAR menempatkan segmen kode .text di wilayah ROM
File dan segmen objek
Di atas, saya menyebutkan segmen kode, mari kita lihat apa itu.
File objek terpisah dibuat untuk setiap modul yang dikompilasi, yang berisi informasi berikut:
- Kode dan Segmen Data
- Informasi Debugging DWARF
- Tabel karakter
Kami tertarik pada
segmen kode dan data.
Segmen adalah elemen yang berisi potongan kode atau data yang harus ditempatkan pada alamat fisik dalam memori. Segmen dapat berisi beberapa fragmen, biasanya satu fragmen untuk setiap variabel atau fungsi. Segmen dapat ditempatkan di kedua
ROM dan
RAM .
Setiap segmen memiliki nama dan atribut yang mendefinisikan kontennya. Atribut digunakan untuk mendefinisikan segmen dalam konfigurasi untuk tautan. Misalnya, atribut mungkin:
- kode - kode yang dapat dieksekusi
- readonly - variabel konstan
- readwrite - variabel yang diinisialisasi
- zeroinit - variabel nol yang diinisialisasi
Tentu saja, ada jenis segmen lain, misalnya segmen yang berisi informasi debug, tetapi kami hanya akan tertarik pada segmen yang berisi kode atau data dari aplikasi kami.
Secara umum, segmen adalah blok yang dapat dinautkan terkecil. Namun, jika perlu, penghubung juga dapat menunjukkan blok yang lebih kecil (fragmen). Kami tidak akan mempertimbangkan opsi ini, kami akan lakukan dengan segmen.
Selama kompilasi, data dan fungsi ditempatkan di segmen yang berbeda. Dan selama menaut, tautan memberikan alamat fisik nyata ke segmen yang berbeda. Kompiler IAR memiliki nama segmen yang sudah ditentukan sebelumnya, beberapa di antaranya akan saya berikan di bawah ini:
- .bss - Berisi variabel statis dan global yang diinisialisasi ke 0
- .CSTACK - Berisi tumpukan yang digunakan oleh program
- .data - Berisi variabel inisialisasi statis dan global
- .data_init - Berisi nilai awal untuk data di bagian .data jika arahan inisialisasi untuk linker digunakan.
- HEAP - Berisi heap yang digunakan untuk meng-host data dinamis
- .intvec - Berisi tabel vektor interupsi
- .rodata - Berisi data konstan
- .text - Berisi kode program
Untuk memahami di mana konstanta berada, kami hanya akan tertarik pada segmen
.rodata - segmen di mana konstanta disimpan,
.data - segmen di mana semua variabel statis dan global yang diinisialisasi disimpan,
.bss - segmen di mana semua variabel
.data statis dan global yang diinisialisasi dengan nol (0) disimpan,
.text - segmen untuk menyimpan kode.
Dalam praktiknya, ini berarti bahwa jika Anda mendefinisikan variabel
int val = 3
, maka variabel itu sendiri akan ditemukan oleh kompiler di segmen
data. Dan ditandai dengan atribut
readwrite , dan angka 3 dapat ditempatkan baik di segmen
.text atau di segmen
.rodata atau, jika arahan khusus untuk tautan di.
data_init diterapkan dan juga ditandai sebagai hanya dapat
dibaca olehnya .
Segmen
.rodata berisi data konstan dan mencakup variabel konstan, string, literal agregat, dan sebagainya. Dan
segmen ini dapat ditempatkan di mana saja dalam memori.Sekarang menjadi lebih jelas apa yang ditentukan dalam pengaturan tautan dan mengapa:
place in ROM_region { readonly };
Artinya, semua data yang ditandai dengan atribut
readonly harus ditempatkan di ROM_region. Dengan demikian, data dari segmen yang berbeda, tetapi ditandai dengan atribut readonly, dapat masuk ke ROM.
Nah itu berarti semua konstanta harus dalam ROM, tetapi mengapa dalam kode kita, di awal artikel, apakah objek konstan masih terletak pada RAM? class WantToBeInROM { private: int i; public: WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; }
Data konstan
Sebelum mengklarifikasi situasi, mari kita ingat kembali bahwa variabel global dibuat dalam memori bersama, variabel lokal, yaitu variabel yang dideklarasikan di dalam fungsi "normal" dibuat di stack atau di register, dan variabel lokal statis juga dibuat di memori bersama.
Apa artinya ini di C ++. Mari kita lihat sebuah contoh:
void foo(const int& C1, const int& C2, const int& C3, const int& C4, const int& C5, const int& C6) { std::cout << C1 << C2 << C3 << C4 << C5 << C6 << std::endl; }
Ini semua data konstan. Tetapi untuk salah satu dari mereka aturan penciptaan yang dijelaskan di atas berlaku, variabel lokal dibuat pada stack. Karena itu, dengan pengaturan tautan kami, harus seperti ini:
- Konstanta global Case1 harus dalam ROM . Di segmen .rodata
- Konstanta global Case2 harus dalam ROM . Di segmen .rodata
- Konstanta lokal Case3 harus terletak pada RAM (konstanta dibuat pada stack di segmen STACK)
- Konstanta statis Case4 harus dalam ROM . Di segmen .rodata
- Konstanta lokal Case5 harus terletak pada RAM (kasus yang menarik, tetapi persis identik dengan Kasus 3.)
- Konstanta statis Case6 harus dalam ROM . Di segmen .rodata
Sekarang mari kita lihat informasi debug dan file peta yang dihasilkan. Debugger menunjukkan di mana alamat konstanta ini berada.

Seperti yang saya katakan sebelumnya, alamat 0x0800 ... ini adalah alamat
ROM , dan 0x200 ... ini adalah
RAM . Mari kita lihat di segmen mana kompiler mendistribusikan konstanta ini:
.rodata const 0x800'4e2c 0x4 main.o //Case1 .rodata const 0x800'4e30 0x4 main.o //Case2 .rodata const 0x800'4e34 0x4 main.o //Case4 .rodata const 0x800'4e38 0x4 main.o //Case6
Empat konstanta global dan statis jatuh ke segmen
.rodata , dan dua variabel lokal tidak jatuh ke file peta karena mereka dibuat di stack dan alamatnya sesuai dengan alamat stack. Segmen CSTACK dimulai pada 0x2000'2488 dan berakhir pada 0x2000'0488. Seperti yang dapat Anda lihat dari gambar, konstanta baru saja dibuat di awal tumpukan.
Kompiler menempatkan konstanta global dan statis di segmen .rodata , lokasi yang ditentukan dalam pengaturan tautan.
Perlu dicatat poin penting lainnya,
inisialisasi . Variabel global dan statis, termasuk konstanta, harus diinisialisasi. Dan ini bisa dilakukan dengan beberapa cara. Jika itu adalah bohong konstan di segmen
.rodata , inisialisasi terjadi pada tahap kompilasi, mis. nilai segera ditulis ke alamat di mana konstanta berada. Jika ini adalah variabel biasa, maka inisialisasi dapat terjadi dengan menyalin nilai dari memori ROM ke alamat variabel global:
Misalnya, jika variabel global
int i = 3
didefinisikan, maka kompiler mendefinisikannya dalam segmen data .d, penghubung meletakkannya di 0x20000000:
.data inited 0x2000'0000
,
dan nilai inisialisasi (3) akan terletak di segmen
.rodata di alamat 0x8000190:
Initializer bytes const 0x800'0190
Jika Anda menulis kode ini:
int i = 3; const int c = i;
Jelas bahwa konstanta global
, diinisialisasi hanya setelah variabel global
i
diinisialisasi, yaitu, dalam runtime. Dalam hal ini, konstanta akan ditempatkan di
RAMSekarang jika kita kembali ke
contoh awal class WantToBeInROM { private: int i; public: WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; }
Dan kami bertanya pada diri sendiri: di segmen apa kompiler mendefinisikan objek konstan
myConstInROM
? Dan kami mendapatkan jawabannya: konstanta akan terletak di segmen
.bss, yang berisi variabel statis dan global yang diinisialisasi ke nol (0).
.bss inited 0x2000'0004 0x4
myConstInROM 0x2000'0004 0x4
Mengapa Karena dalam C ++, objek data yang dideklarasikan sebagai konstanta dan yang membutuhkan inisialisasi dinamis terletak di memori baca-tulis dan akan diinisialisasi pada waktu pembuatan ..
Dalam kasus ini, inisialisasi dinamis terjadi,
const WantToBeInROM myConstInROM(10)
, dan kompiler meletakkan objek ini di segmen
.bss , menginisialisasi semua bidang 0 terlebih dahulu, dan kemudian, ketika membuat objek konstan, yang disebut konstruktor untuk menginisialisasi bidang
i
nilai 10.
Bagaimana kita dapat membuat kompiler menempatkan objek kita di segmen
.rodata ? Jawaban untuk pertanyaan ini sederhana, Anda harus selalu melakukan inisialisasi statis. Anda dapat melakukannya dengan cara ini:
1. Dalam contoh kami, dapat dilihat bahwa, pada prinsipnya, kompiler dapat mengoptimalkan inisialisasi dinamis menjadi statis, karena konstruktornya cukup sederhana. Untuk IAR dari kompiler, Anda dapat menandai konstanta dengan atribut
__ro_placement__ro_placement const WantToBeInROM myConstInROM
Dengan opsi ini, kompiler akan menempatkan variabel di alamat dalam ROM:
myConstInROM 0x800'0144 0x4 Data
Jelas, pendekatan ini tidak universal dan umumnya sangat spesifik. Karena itu, kami beralih ke metode yang benar :)
2. Ini adalah untuk membuat konstruktor
constexpr
. Kami segera memberi tahu kompiler untuk menggunakan inisialisasi statis, mis. pada tahap kompilasi, ketika seluruh objek akan sepenuhnya "dihitung" sebelumnya dan semua bidangnya akan diketahui. Yang perlu kita lakukan adalah menambahkan constexpr ke konstruktor.
Objek terbang ke ROM class WantToBeInROM { private: int i; public: constexpr WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; }
Jadi, untuk memastikan bahwa objek konstan Anda dalam ROM, Anda harus mengikuti aturan sederhana:
- Segmen .teks tempat kode ditempatkan harus dalam ROM. Ini dikonfigurasi dalam pengaturan tautan.
- Segmen .rodata di mana konstanta global dan statis ditempatkan harus dalam ROM. Ini dikonfigurasi dalam pengaturan tautan.
- Konstanta harus global atau statis.
- Atribut kelas variabel konstan tidak boleh berubah-ubah
- Inisialisasi objek harus statis, mis. Konstruktor kelas yang objeknya akan berupa konstanta harus constexpr atau tidak didefinisikan sama sekali (tidak ada inisialisasi dinamis)
- Jika memungkinkan, jika Anda yakin bahwa objek tersebut harus disimpan dalam ROM daripada menggunakan constexpr
Beberapa kata tentang constexpr dan konstruktor constexpr. Perbedaan utama antara const dan constexpr adalah bahwa inisialisasi variabel const dapat ditunda hingga runtime. Variabel constexpr harus diinisialisasi pada waktu kompilasi.
Semua variabel constexpr adalah tipe const.Definisi konstruktor constexpr harus memenuhi persyaratan berikut:
Konstruktor default implisit adalah konstruktor constexpr. Sekarang mari kita lihat beberapa contoh:
Contoh 1. Objek dalam ROM class Test { private: int i; public: Test() {} ; int Get() const { return i + 1; } } ; const Test test;
Lebih baik tidak menulis seperti ini, karena begitu Anda memutuskan untuk menginisialisasi atribut i, objek akan terbang ke RAM
Contoh 2. Objek dalam RAM class Test { private: int i = 1;
Contoh 3. Objek dalam RAM class Test { private: int i; public: Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; const Test test(10);
Contoh 4. Objek dalam ROM class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; const Test test(10);
Contoh 5. Objek dalam RAM class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; int main() { const Test test(10);
Contoh 6. Objek dalam ROM class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; int main() { static const Test test(10);
Contoh 7. Kesalahan kompilasi class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get()
Contoh 8. Objek dalam ROM, mewarisi dari kelas abstrak class ITest { private: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class Test: public ITest { private: int i; public: constexpr Test(int value): i(value), ITest(value+1) {} ; int Get() const override { return i + 1; } } ; const Test test(10);
Contoh 9. Objek dalam ROM mengagregasi objek yang terletak di RAM class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; TestImpl testImpl(1);
Contoh 10. Objek yang sama tetapi statis di ROM class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; class Test: public ITest { private: int i; TestImpl & obj;
Contoh 11. Dan sekarang objek konstan tidak statis dan karenanya dalam RAM class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; class Test: public ITest { private: int i; TestImpl & obj;
Contoh 12. Kesalahan kompilasi. class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; class Test: public ITest { private: int i; TestImpl obj;
Contoh 13. Kesalahan kompilasi class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: constexpr TestImpl(int value): k(value), ITest(value)
Contoh 14. Objek dalam ROM class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: constexpr TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) const
Dan akhirnya, objek konstan yang berisi array, dengan inisialisasi array melalui fungsi constexpr. class Test { private: int k[100]; constexpr void InitArray() { int i = 0; for(auto& it: k) { it = i++ ; } } public: constexpr Test(): k() { InitArray();
Referensi:
IAR C / C ++ Panduan PengembanganConstexpr constructors (C ++ 11)constexpr (C ++)PS.
Setelah diskusi yang sangat berguna dengan Valdaros, Anda perlu menambahkan konstanta tangen titik berikut. Menurut standar C ++ dan dokumen ini N1076.pdf1. Setiap perubahan ke objek konstan (dengan pengecualian anggota kelas yang bisa berubah) selama masa hidupnya mengarah ke Perilaku Tidak Terdefinisi. Yaitu
const int ci = 1 ; int* iptr = const_cast<int*>(&ci);
int i = 1; const int* ci = &i ; int* iptr = const_cast<int *> (ci);
2. Masalahnya adalah bahwa ini hanya berfungsi selama seumur hidup dari objek konstan, tetapi dalam konstruktor dan destruktor tidak berfungsi. Karena itu, cukup sah untuk melakukannya: class Test { public: int i; constexpr Test(): i(0) { foo(this) ; } } ; Test *test1; constexpr void foo(Test* value) { value->i = 1;
Dan itu dianggap legal. Terlepas dari kenyataan bahwa kami menggunakan konstruktor constexpr, dan fungsi constexpr di dalamnya. Objek langsung menuju ke RAM.Untuk menghindarinya, gunakan const - constexpr alih-alih const, maka akan ada kesalahan kompilasi yang akan memberi tahu Anda bahwa ada sesuatu yang salah dan objek tidak dapat konstan. class Test { public: int i; constexpr Test(): i(0) { foo(this) ; } } ; Test *test1; constexpr void foo(Test* value) { value->i = 1;