Bagaimana cara mikrokontroler boot dan memulai, langkah demi langkah?

17

Ketika kode C ditulis, dikompilasi dan diunggah ke mikrokontroler, mikrokontroler mulai berjalan. Tetapi jika kita mengambil proses pengunggahan dan startup ini langkah demi langkah dalam gerakan lambat, saya memiliki beberapa kebingungan tentang apa yang sebenarnya terjadi di dalam MCU (memori, CPU, bootloader). Inilah (kemungkinan besar salah) apa yang akan saya jawab jika seseorang bertanya kepada saya:

  1. Kode biner yang dikompilasi ditulis ke flash ROM (atau EEPROM) melalui USB
  2. Bootloader menyalin beberapa bagian dari kode ini ke RAM. Jika benar, bagaimana boot-loader tahu apa yang harus disalin (bagian mana dari ROM yang akan disalin ke RAM)?
  3. CPU mulai mengambil instruksi dan data kode dari ROM dan RAM

Apakah ini salah?

Apakah mungkin untuk merangkum proses booting dan startup ini dengan beberapa informasi tentang bagaimana memori, bootloader, dan CPU berinteraksi dalam fase ini?

Saya telah menemukan banyak penjelasan dasar tentang cara boot PC melalui BIOS. Tapi saya terjebak dengan proses startup mikrokontroler.

pengguna16307
sumber

Jawaban:

31

1) biner yang dikompilasi ditulis ke prom / flash ya. USB, serial, i2c, jtag, dll tergantung pada perangkat apa yang didukung oleh perangkat itu, tidak dapat dipahami untuk memahami proses booting.

2) Ini biasanya tidak benar untuk mikrokontroler, kasus penggunaan utama adalah memiliki instruksi dalam rom / flash dan data dalam ram. Tidak masalah apa arsitekturnya. untuk non-mikrokontroler, pc Anda, laptop Anda, server Anda, program ini disalin dari non-volatile (disk) ke ram kemudian dijalankan dari sana. Beberapa mikrokontroler membiarkan Anda menggunakan ram juga, bahkan yang mengklaim harvard meskipun tampaknya melanggar definisi. Tidak ada apa-apa tentang harvard yang mencegah Anda dari memetakan ram ke sisi instruksi, Anda hanya perlu memiliki mekanisme untuk mendapatkan instruksi di sana setelah daya menyala (yang melanggar definisi, tetapi sistem harvard harus melakukan itu untuk menjadi berguna lainnya selain sebagai mikrokontroler).

3) semacam.

Setiap cpu "boot" dengan cara deterministik, seperti yang dirancang. Cara yang paling umum adalah tabel vektor di mana alamat untuk instruksi pertama yang dijalankan setelah powering berada dalam vektor reset, alamat yang dibaca perangkat keras kemudian menggunakan alamat itu untuk mulai berjalan. Cara umum lainnya adalah membuat prosesor mulai mengeksekusi tanpa tabel vektor di beberapa alamat yang terkenal. Kadang-kadang chip akan memiliki "tali", beberapa pin yang dapat Anda ikat tinggi atau rendah sebelum melepaskan reset, yang digunakan logika untuk mem-boot cara yang berbeda. Anda harus memisahkan cpu itu sendiri, inti prosesor dari sisa sistem. Memahami bagaimana cpu beroperasi, dan kemudian mengerti bahwa perancang chip / sistem memiliki decoder alamat penyiapan di luar cpu sehingga beberapa bagian dari ruang alamat cpus berkomunikasi dengan flash, dan beberapa dengan ram dan beberapa dengan periferal (uart, i2c, spi, gpio, dll). Anda dapat mengambil cpu core yang sama jika diinginkan, dan membungkusnya secara berbeda. Ini adalah apa yang Anda dapatkan ketika Anda membeli sesuatu berdasarkan lengan atau mips. lengan dan pel membuat core cpu, yang chip orang membeli dan membungkus barang-barang mereka sendiri, untuk berbagai alasan mereka tidak membuat barang-barang itu kompatibel dari merek ke merek. Itulah sebabnya jarang bisa mengajukan pertanyaan lengan generik ketika datang ke apa pun di luar inti.

Sebuah mikrokontroler berusaha menjadi sistem pada sebuah chip, sehingga memori non-volatile (flash / rom), volatile (sram), dan cpu semuanya berada pada chip yang sama bersama dengan campuran periferal. Tetapi chip dirancang secara internal sehingga flash dipetakan ke dalam ruang alamat cpu yang sesuai dengan karakteristik boot cpu itu. Jika misalnya cpu memiliki reset vektor di alamat 0xFFFC, maka perlu ada flash / rom yang menanggapi alamat yang dapat kita program melalui 1), bersama dengan cukup flash / rom di ruang alamat untuk program yang bermanfaat. Perancang chip dapat memilih untuk memiliki 0x1000 byte flash mulai dari 0xF000 untuk memenuhi persyaratan tersebut. Dan mungkin mereka menaruh sejumlah ram di alamat yang lebih rendah atau mungkin 0x0000, dan periferal di suatu tempat di tengah.

Arsitektur cpu lain mungkin mulai dieksekusi pada alamat nol, jadi mereka perlu melakukan yang sebaliknya, letakkan flash sehingga ia menjawab kisaran alamat sekitar nol. katakanlah 0x0000 hingga 0x0FFF misalnya. dan kemudian menaruh beberapa ram di tempat lain.

Desainer chip tahu bagaimana cpu melakukan booting dan mereka telah menempatkan penyimpanan non-volatil di sana (flash / rom). Terserah orang perangkat lunak untuk menulis kode boot agar sesuai dengan perilaku cpu yang terkenal itu. Anda harus meletakkan alamat penyetelan ulang vektor dalam penyetelan ulang dan kode booting Anda pada alamat yang Anda tentukan dalam penyetelan ulang vektor. Toolchain dapat sangat membantu Anda di sini. kadang-kadang, terutama dengan point dan klik ides atau kotak pasir lain yang dapat mereka lakukan sebagian besar untuk Anda semua yang Anda lakukan adalah memanggil apis dalam bahasa tingkat tinggi (C).

Namun, bagaimanapun hal itu dilakukan, program yang dimuat ke dalam flash / rom harus sesuai dengan perilaku boot bawaan cpu. Sebelum bagian C dari main program Anda () dan aktif jika Anda menggunakan main sebagai titik masuk Anda, beberapa hal harus dilakukan. Pemrogram AC mengasumsikan bahwa ketika mendeklarasikan variabel dengan nilai awal, mereka berharap itu benar-benar berfungsi. Nah, variabel, selain yang const, berada di ram, tetapi jika Anda memiliki satu dengan nilai awal bahwa nilai awal harus dalam ram non-volatile. Jadi ini adalah segmen .data dan bootstrap C perlu menyalin hal .data dari flash ke ram (di mana biasanya ditentukan untuk Anda oleh toolchain). Variabel global yang Anda nyatakan tanpa nilai awal diasumsikan nol sebelum program Anda dimulai meskipun Anda seharusnya tidak benar-benar berasumsi itu dan untungnya beberapa kompiler mulai memperingatkan tentang variabel yang tidak diinisialisasi. Ini adalah segmen .bss, dan nol bootstrap C yang keluar di ram, konten, nol, tidak harus disimpan dalam memori non-volatil, tetapi alamat awal dan berapa banyak. Sekali lagi toolchain sangat membantu Anda di sini. Dan terakhir minimum adalah Anda perlu mengatur stack pointer karena program C berharap dapat memiliki variabel lokal dan memanggil fungsi lainnya. Maka mungkin beberapa hal spesifik chip lainnya dilakukan, atau kami membiarkan sisanya terjadi pada C. tidak harus disimpan dalam memori non-volatile, tetapi alamat awal dan berapa banyak. Sekali lagi toolchain sangat membantu Anda di sini. Dan terakhir minimum adalah Anda perlu mengatur stack pointer karena program C berharap dapat memiliki variabel lokal dan memanggil fungsi lainnya. Maka mungkin beberapa hal spesifik chip lainnya dilakukan, atau kami membiarkan sisanya terjadi pada C. tidak harus disimpan dalam memori non-volatile, tetapi alamat awal dan berapa banyak. Sekali lagi toolchain sangat membantu Anda di sini. Dan terakhir minimum adalah Anda perlu mengatur stack pointer karena program C berharap dapat memiliki variabel lokal dan memanggil fungsi lainnya. Maka mungkin beberapa hal spesifik chip lainnya dilakukan, atau kami membiarkan sisanya terjadi pada C.

Core seri korteks-m dari arm akan melakukan beberapa hal ini untuk Anda, stack pointer ada di tabel vektor, ada vektor reset untuk menunjuk kode yang akan dijalankan setelah reset, sehingga selain apa pun yang harus Anda lakukan untuk menghasilkan tabel vektor (yang biasanya Anda gunakan asm), Anda dapat menggunakan C murni tanpa asm. sekarang Anda tidak menyalin data Anda. Atau .bss Anda memusatkan perhatian sehingga Anda harus melakukannya sendiri jika Anda ingin mencoba untuk pergi tanpa asm pada sesuatu berbasis korteks-m. Fitur yang lebih besar bukanlah vektor reset tetapi interupsi vektor di mana perangkat keras mengikuti lengan yang direkomendasikan konvensi pemanggilan C dan menyimpan register untuk Anda, dan menggunakan pengembalian yang benar untuk vektor itu, sehingga Anda tidak perlu membungkus asm yang tepat di sekitar masing-masing handler ( atau memiliki arahan spesifik toolchain untuk target Anda agar toolchain membungkusnya untuk Anda).

Hal-hal spesifik chip mungkin misalnya, mikrokontroler sering digunakan dalam sistem berbasis baterai, sehingga daya rendah sehingga beberapa keluar dari reset dengan sebagian besar periferal dimatikan, dan Anda harus mengaktifkan masing-masing sub sistem ini sehingga Anda dapat menggunakannya . Uarts, gpios, dll. Seringkali kecepatan clock ish rendah digunakan, langsung dari kristal atau osilator internal. Dan desain sistem Anda mungkin menunjukkan bahwa Anda membutuhkan jam yang lebih cepat, jadi Anda menginisialisasi itu. jam Anda mungkin terlalu cepat untuk flash atau ram sehingga Anda mungkin perlu mengubah status tunggu sebelum menaikkan jam. Mungkin perlu mengatur uart, atau usb atau antarmuka lainnya. maka aplikasi Anda dapat melakukan hal tersebut.

Desktop komputer, laptop, server, dan mikrokontroler tidak berbeda dalam cara mereka boot / bekerja. Kecuali bahwa mereka tidak sebagian besar dalam satu chip. Program bios sering pada chip flash / rom terpisah dari cpu. Meskipun baru-baru ini CPU x86 menarik lebih banyak dari apa yang dulunya merupakan chip pendukung ke dalam paket yang sama (pengontrol pcie, dll.) Tetapi Anda masih memiliki sebagian besar ram dan rom dari chip Anda, tetapi masih berupa sistem dan masih berfungsi dengan tepat sama di level tinggi. Proses booting cpu sudah dikenal luas, para desainer papan menempatkan flash / rom di ruang alamat di mana cpu boot. program itu (bagian dari BIOS pada PC x86) melakukan semua hal yang disebutkan di atas, ia memulai berbagai peripheral, menginisialisasi dram, menghitung bus pcie, dan sebagainya. Seringkali cukup dapat dikonfigurasi oleh pengguna berdasarkan pada pengaturan bios atau apa yang kita sebut pengaturan cmos, karena pada saat itulah teknologi digunakan. Tidak masalah, ada pengaturan pengguna yang dapat Anda buka dan ubah untuk memberi tahu kode booting BIOS bagaimana memvariasikan apa yang dilakukannya.

orang yang berbeda akan menggunakan terminologi yang berbeda. sebuah chip boot, itu adalah kode pertama yang berjalan. kadang-kadang disebut bootstrap. bootloader dengan kata loader sering berarti bahwa jika Anda tidak melakukan apa pun untuk mengganggu itu adalah bootstrap yang membawa Anda dari booting generik ke sesuatu yang lebih besar, aplikasi atau sistem operasi Anda. tetapi bagian loader menyiratkan bahwa Anda dapat mengganggu proses boot dan kemudian mungkin memuat program pengujian lainnya. jika Anda pernah menggunakan uboot misalnya pada sistem linux tertanam, Anda dapat menekan tombol dan menghentikan boot normal maka Anda dapat mengunduh kernel tes ke ram dan boot itu bukan yang ada di flash, atau Anda dapat mengunduh program sendiri, atau Anda dapat mengunduh kernel baru kemudian minta bootloader menuliskannya ke flash sehingga lain kali Anda mem-boot-nya menjalankan hal-hal baru.

Sejauh CPU itu sendiri, prosesor inti, yang tidak tahu ram dari flash dari periferal. Tidak ada gagasan tentang bootloader, sistem operasi, aplikasi. Itu hanya urutan instruksi yang dimasukkan ke dalam cpu untuk dieksekusi. Ini adalah istilah perangkat lunak untuk membedakan tugas pemrograman yang berbeda satu sama lain. Konsep perangkat lunak satu sama lain.

Beberapa mikrokontroler memiliki bootloader terpisah yang disediakan oleh vendor chip dalam flash terpisah atau area flash terpisah yang mungkin tidak dapat Anda modifikasi. Dalam hal ini sering ada pin atau set pin (saya menyebutnya straps) bahwa jika Anda mengikatnya tinggi atau rendah sebelum reset dilepaskan Anda memberi tahu logika dan / atau bahwa bootloader apa yang harus dilakukan, misalnya satu kombinasi tali mungkin beri tahu chip untuk menjalankan bootloader itu dan tunggu di uart untuk data diprogram ke dalam flash. Atur pengikat dengan cara lain dan program Anda melakukan booting bukan bootloader vendor chip, memungkinkan untuk pemrograman di lapangan chip atau memulihkan dari crash program Anda. Terkadang itu hanya logika murni yang memungkinkan Anda untuk memprogram flash. Ini sangat umum hari ini,

Alasan mengapa sebagian besar mikrokontroler memiliki lebih banyak flash daripada ram adalah bahwa use case utama adalah menjalankan program langsung dari flash, dan hanya memiliki cukup ram untuk menutupi tumpukan dan variabel. Meskipun dalam beberapa kasus Anda dapat menjalankan program dari ram yang harus Anda kompilasi dan simpan dalam flash kemudian salin sebelum menelepon.

EDIT

flash.s

.cpu cortex-m0
.thumb

.thumb_func
.global _start
_start:
stacktop: .word 0x20001000
.word reset
.word hang
.word hang
.word hang

.thumb_func
reset:
    bl notmain
    b hang

.thumb_func
hang:   b .

notmain.c

int notmain ( void )
{
    unsigned int x=1;
    unsigned int y;
    y = x + 1;

    return(0);
}

flash.ld

MEMORY
{
    bob : ORIGIN = 0x00000000, LENGTH = 0x1000
    ted : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
    .text : { *(.text*) } > bob
    .rodata : { *(.rodata*) } > bob
    .bss : { *(.bss*) } > ted
    .data : { *(.bss*) } > ted AT > bob
}

Jadi ini adalah contoh untuk korteks-m0, korteks-ms semuanya bekerja sama sejauh contoh ini. Chip tertentu, untuk contoh ini, memiliki aplikasi flash di alamat 0x00000000 di ruang alamat lengan dan ram di 0x20000000.

Cara boot cortex-m adalah kata 32 bit pada alamat 0x0000 adalah alamat untuk menginisialisasi stack pointer. Saya tidak perlu banyak tumpukan untuk contoh ini sehingga 0x20001000 akan cukup, jelas harus ada ram di bawah alamat itu (cara lengan mendorong, apakah mengurangi terlebih dahulu kemudian mendorong jadi jika Anda menetapkan 0x20001000 item pertama pada tumpukan adalah di alamat 0x2000FFFC Anda tidak perlu menggunakan 0x2000FFFC). Kata 32 bit pada alamat 0x0004 adalah alamat untuk reset handler, pada dasarnya kode pertama yang berjalan setelah reset. Lalu ada lebih banyak interrupt dan event handler yang spesifik untuk inti dan chip korteks, mungkin sebanyak 128 atau 256, jika Anda tidak menggunakannya maka Anda tidak perlu mengatur tabel untuk mereka, saya memberikan beberapa untuk demonstrasi tujuan.

Saya tidak perlu berurusan dengan. Data atau .bss dalam contoh ini karena saya tahu sudah tidak ada di segmen tersebut dengan melihat kode. Jika ada saya akan menghadapinya, dan akan dalam sedetik.

Jadi tumpukan sudah diatur, periksa, .data diurus, periksa, .bss, periksa, sehingga hal-hal bootstrap C selesai, dapat bercabang ke fungsi entri untuk C. Karena beberapa kompiler akan menambahkan sampah tambahan jika mereka melihat fungsinya main () dan dalam perjalanan ke main, saya tidak menggunakan nama yang tepat, saya menggunakan notmain () di sini sebagai titik masuk C saya. Jadi reset handler memanggil notmain () lalu jika / ketika notmain () mengembalikannya untuk hang yang hanya merupakan infinite loop, mungkin namanya buruk.

Saya sangat percaya pada penguasaan alat, banyak orang tidak, tetapi apa yang akan Anda temukan adalah bahwa setiap pengembang logam telanjang melakukan hal sendiri, karena kebebasan yang hampir sempurna, tidak jauh dari kendala seperti Anda akan membuat aplikasi atau halaman web . Mereka lagi melakukan hal mereka sendiri. Saya lebih suka memiliki kode bootstrap dan skrip tautan saya sendiri. Yang lain bergantung pada rantai alat, atau bermain di kotak pasir vendor tempat sebagian besar pekerjaan dilakukan oleh orang lain (dan jika ada sesuatu yang pecah, Anda berada dalam dunia yang terluka, dan dengan benda-benda logam sering pecah dan dengan cara dramatis).

Jadi merakit, mengkompilasi dan menghubungkan dengan alat gnu saya dapatkan:

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   2000        movs    r0, #0
  1e:   4770        bx  lr

Jadi bagaimana bootloader tahu di mana barang-barang berada. Karena kompiler melakukan pekerjaan. Dalam kasus pertama assembler menghasilkan kode untuk flash.s, dan dengan melakukan itu tahu di mana label berada (label hanya alamat seperti nama fungsi atau nama variabel, dll) jadi saya tidak perlu menghitung byte dan mengisi vektor tabel secara manual, saya menggunakan nama label dan assembler melakukannya untuk saya. Sekarang Anda bertanya, jika reset adalah alamat 0x14 mengapa assembler memasukkan 0x15 dalam tabel vektor. Nah ini adalah korteks-m dan boot dan hanya berjalan dalam mode ibu jari. Dengan ARM saat Anda bercabang ke alamat jika mode percabangan ke ibu jari lsbit perlu diatur, jika mode lengan kemudian diatur ulang. Jadi Anda selalu membutuhkan set bit itu. Saya tahu alat dan dengan menempatkan. Thumb_func di depan label, jika label itu digunakan seperti dalam tabel vektor atau untuk bercabang ke atau apa pun. Toolchain tahu untuk mengatur lsbit. Jadi ada di sini 0x14 | 1 = 0x15. Begitu juga untuk hang. Sekarang disassembler tidak menunjukkan 0x1D untuk panggilan ke notmain () tapi jangan khawatir alat-alat telah membangun instruksi dengan benar.

Sekarang kode di notmain, variabel-variabel lokal, tidak digunakan, mereka adalah kode mati. Kompilator bahkan mengomentari fakta itu dengan mengatakan y diatur tetapi tidak digunakan.

Perhatikan ruang alamat, semuanya dimulai dari alamat 0x0000 dan pergi dari sana sehingga tabel vektor ditempatkan dengan benar, ruang teks atau program juga ditempatkan dengan benar, bagaimana saya mendapat flash.s di depan kode notmain.c adalah dengan Mengetahui alat, kesalahan umum adalah tidak mendapatkan yang benar dan crash dan membakar keras. IMO Anda harus membongkar untuk memastikan hal-hal ditempatkan tepat sebelum Anda boot pertama kali, setelah Anda memiliki barang-barang di tempat yang tepat Anda tidak perlu memeriksa setiap waktu. Hanya untuk proyek baru atau jika mereka hang.

Sekarang sesuatu yang mengejutkan beberapa orang adalah bahwa tidak ada alasan apa pun untuk mengharapkan dua kompiler untuk menghasilkan output yang sama dari input yang sama. Atau bahkan kompiler yang sama dengan pengaturan berbeda. Menggunakan dentang, kompiler llvm saya mendapatkan dua output ini dengan dan tanpa optimasi

llvm / dentang dioptimalkan

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   2000        movs    r0, #0
  1e:   4770        bx  lr

tidak dioptimalkan

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   b082        sub sp, #8
  1e:   2001        movs    r0, #1
  20:   9001        str r0, [sp, #4]
  22:   2002        movs    r0, #2
  24:   9000        str r0, [sp, #0]
  26:   2000        movs    r0, #0
  28:   b002        add sp, #8
  2a:   4770        bx  lr

Jadi itu adalah kebohongan kompilator tidak mengoptimalkan penambahan, tapi itu mengalokasikan dua item pada stack untuk variabel, karena ini adalah variabel lokal mereka di ram tetapi pada stack tidak pada alamat tetap, akan melihat dengan global bahwa perubahan. Tetapi kompiler menyadari bahwa ia dapat menghitung y pada waktu kompilasi dan tidak ada alasan untuk menghitungnya pada saat dijalankan sehingga ia hanya menempatkan 1 pada ruang stack yang dialokasikan untuk x dan 2 untuk ruang stack yang dialokasikan untuk y. kompiler "mengalokasikan" ruang ini dengan tabel internal saya mendeklarasikan stack plus 0 untuk variabel y dan stack plus 4 untuk variabel x. kompiler dapat melakukan apa saja yang diinginkan selama kode yang diterapkan sesuai dengan standar C atau ekspetasi programmer C. Tidak ada alasan mengapa kompiler harus meninggalkan x pada stack + 4 selama durasi fungsi,

Jika saya menambahkan fungsi dummy di assembler

.thumb_func
.globl dummy
dummy:
    bx lr

dan kemudian menyebutnya

void dummy ( unsigned int );
int notmain ( void )
{
    unsigned int x=1;
    unsigned int y;
    y = x + 1;
    dummy(y);
    return(0);
}

output berubah

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f804   bl  20 <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <dummy>:
  1c:   4770        bx  lr
    ...

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   2002        movs    r0, #2
  24:   f7ff fffa   bl  1c <dummy>
  28:   2000        movs    r0, #0
  2a:   bc10        pop {r4}
  2c:   bc02        pop {r1}
  2e:   4708        bx  r1

sekarang kita memiliki fungsi bersarang, fungsi notmain perlu mempertahankan alamat pengirimnya, sehingga ia dapat mengalahkan alamat pengirim untuk panggilan bersarang. ini karena lengan menggunakan register untuk pengembalian, jika ia menggunakan stack seperti katakanlah x86 atau yang lainnya dengan baik ... ia masih menggunakan stack tetapi berbeda. Sekarang Anda bertanya mengapa itu mendorong r4? Nah, konvensi pemanggilan belum lama berubah untuk menjaga tumpukan disejajarkan pada batas 64 bit (dua kata) alih-alih 32 bit, batas satu kata. Jadi mereka perlu mendorong sesuatu agar tumpukan tetap selaras, sehingga kompiler secara sewenang-wenang memilih r4 karena suatu alasan, tidak masalah mengapa. Popping ke r4 akan menjadi bug meskipun sesuai dengan konvensi pemanggilan untuk target ini, kita tidak clobber r4 pada panggilan fungsi, kita bisa clobber r0 melalui r3. r0 adalah nilai kembali. Sepertinya itu melakukan optimasi ekor mungkin,

Tapi kita melihat bahwa x dan y matematika dioptimalkan ke nilai hardcoded 2 yang diteruskan ke fungsi dummy (dummy secara khusus dikodekan dalam file terpisah, dalam hal ini asm, sehingga kompiler tidak akan mengoptimalkan fungsi memanggil sepenuhnya, jika saya memiliki fungsi dummy yang hanya dikembalikan dalam C di notmain.c optimizer akan menghapus panggilan fungsi x, y, dan dummy karena semuanya mati / kode tidak berguna).

Juga catat bahwa karena kode flash.s jadi lebih besar bukan yang utama juga ada di sini dan toolchain telah mengurus untuk menambal semua alamat untuk kami sehingga kami tidak perlu melakukannya secara manual.

dentang tidak dioptimalkan untuk referensi

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   b082        sub sp, #8
  26:   2001        movs    r0, #1
  28:   9001        str r0, [sp, #4]
  2a:   2002        movs    r0, #2
  2c:   9000        str r0, [sp, #0]
  2e:   f7ff fff5   bl  1c <dummy>
  32:   2000        movs    r0, #0
  34:   b002        add sp, #8
  36:   bd80        pop {r7, pc}

dentang dioptimalkan

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   2002        movs    r0, #2
  26:   f7ff fff9   bl  1c <dummy>
  2a:   2000        movs    r0, #0
  2c:   bd80        pop {r7, pc}

penulis kompiler yang memilih untuk menggunakan r7 sebagai variabel dummy untuk menyelaraskan tumpukan, juga itu membuat frame pointer menggunakan r7 meskipun ia tidak memiliki apa pun dalam bingkai tumpukan. pada dasarnya instruksi bisa dioptimalkan. tetapi menggunakan pop untuk mengembalikan bukan tiga instruksi, itu mungkin pada saya, saya yakin saya bisa mendapatkan gcc untuk melakukannya dengan opsi baris perintah yang tepat (menentukan prosesor).

ini sebagian besar harus menjawab sisa pertanyaan Anda

void dummy ( unsigned int );
unsigned int x=1;
unsigned int y;
int notmain ( void )
{
    y = x + 1;
    dummy(y);
    return(0);
}

Saya memiliki global sekarang. jadi mereka masuk baik data atau .bss jika mereka tidak dioptimalkan.

sebelum kita melihat hasil akhirnya mari kita lihat objek itermediate

00000000 <notmain>:
   0:   b510        push    {r4, lr}
   2:   4b05        ldr r3, [pc, #20]   ; (18 <notmain+0x18>)
   4:   6818        ldr r0, [r3, #0]
   6:   4b05        ldr r3, [pc, #20]   ; (1c <notmain+0x1c>)
   8:   3001        adds    r0, #1
   a:   6018        str r0, [r3, #0]
   c:   f7ff fffe   bl  0 <dummy>
  10:   2000        movs    r0, #0
  12:   bc10        pop {r4}
  14:   bc02        pop {r1}
  16:   4708        bx  r1
    ...

Disassembly of section .data:
00000000 <x>:
   0:   00000001    andeq   r0, r0, r1

sekarang ada info yang hilang dari ini tetapi memberikan gambaran tentang apa yang terjadi, penghubung adalah orang yang mengambil objek dan menautkannya bersama dengan informasi yang disediakan (dalam hal ini flash.ld) yang memberitahukan di mana .text dan. data dan semacamnya. kompiler tidak tahu hal-hal seperti itu, ia hanya dapat fokus pada kode yang disajikan, setiap eksternal harus meninggalkan lubang bagi linker untuk mengisi koneksi. Data apa pun harus meninggalkan cara untuk menghubungkan hal-hal itu bersama-sama, sehingga alamat untuk semuanya adalah nol di sini hanya karena kompiler dan pembongkar ini tidak tahu. ada info lain yang tidak ditampilkan di sini yang digunakan tautan untuk menempatkan sesuatu. kode di sini cukup independen sehingga penghubung dapat melakukan tugasnya.

kita kemudian melihat setidaknya pembongkaran dari output terkait

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   4b05        ldr r3, [pc, #20]   ; (38 <notmain+0x18>)
  24:   6818        ldr r0, [r3, #0]
  26:   4b05        ldr r3, [pc, #20]   ; (3c <notmain+0x1c>)
  28:   3001        adds    r0, #1
  2a:   6018        str r0, [r3, #0]
  2c:   f7ff fff6   bl  1c <dummy>
  30:   2000        movs    r0, #0
  32:   bc10        pop {r4}
  34:   bc02        pop {r1}
  36:   4708        bx  r1
  38:   20000004    andcs   r0, r0, r4
  3c:   20000000    andcs   r0, r0, r0

Disassembly of section .bss:

20000000 <y>:
20000000:   00000000    andeq   r0, r0, r0

Disassembly of section .data:

20000004 <x>:
20000004:   00000001    andeq   r0, r0, r1

kompiler pada dasarnya meminta dua variabel 32 bit dalam ram. Salah satunya dalam .bss karena saya tidak menginisialisasi sehingga diasumsikan init sebagai nol. yang lain adalah data. Karena saya menginisialisasi pada deklarasi.

Sekarang karena ini adalah variabel global, diasumsikan bahwa fungsi lain dapat memodifikasinya. kompiler tidak membuat asumsi kapan notmain dapat dipanggil sehingga ia tidak dapat mengoptimalkan dengan apa yang dapat dilihatnya, y = x + 1 matematika, sehingga harus melakukan runtime itu. Itu harus membaca dari ram kedua variabel menambahkannya dan menyimpan kembali.

Sekarang jelas kode ini tidak akan berfungsi. Mengapa? karena bootstrap saya seperti yang ditunjukkan di sini tidak menyiapkan ram sebelum memanggil notmain, jadi sampah apa pun yang ada di 0x20000000 dan 0x20000004 ketika chip bangun adalah apa yang akan digunakan untuk y dan x.

Tidak akan menunjukkan itu di sini. Anda dapat membaca bertele-tele bahkan lebih lama saya pada. data dan .bss dan mengapa saya tidak pernah membutuhkannya dalam kode logam kosong saya, tetapi jika Anda merasa Anda harus dan ingin menguasai alat daripada berharap orang lain melakukannya dengan benar .. .

https://github.com/dwelch67/raspberrypi/tree/master/bssdata

skrip linker, dan bootstrap agak spesifik untuk kompiler sehingga semua yang Anda pelajari tentang satu versi dari satu kompiler dapat dilemparkan ke versi berikutnya atau dengan beberapa kompiler lain, namun alasan lain mengapa saya tidak melakukan banyak upaya dalam persiapan data dan .bss hanya untuk menjadi malas ini:

unsigned int x=1;

Saya lebih suka melakukan ini

unsigned int x;
...
x = 1;

dan biarkan kompiler memasukkannya ke dalam .text untuk saya. Terkadang menyimpan flash dengan cara itu terkadang membakar lebih banyak. Tentunya jauh lebih mudah untuk memprogram dan port dari versi toolchain atau satu kompiler ke yang lain. Jauh lebih andal, lebih sedikit kesalahan. Yap, tidak sesuai dengan standar C.

sekarang bagaimana jika kita membuat global yang statis ini?

void dummy ( unsigned int );
static unsigned int x=1;
static unsigned int y;
int notmain ( void )
{
    y = x + 1;
    dummy(y);
    return(0);
}

baik

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   2002        movs    r0, #2
  24:   f7ff fffa   bl  1c <dummy>
  28:   2000        movs    r0, #0
  2a:   bc10        pop {r4}
  2c:   bc02        pop {r1}
  2e:   4708        bx  r1

jelas variabel-variabel itu tidak dapat dimodifikasi oleh kode lain, sehingga kompiler sekarang dapat pada waktu kompilasi mengoptimalkan kode mati, seperti yang dilakukan sebelumnya.

tidak dioptimalkan

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   4804        ldr r0, [pc, #16]   ; (38 <notmain+0x18>)
  26:   6800        ldr r0, [r0, #0]
  28:   1c40        adds    r0, r0, #1
  2a:   4904        ldr r1, [pc, #16]   ; (3c <notmain+0x1c>)
  2c:   6008        str r0, [r1, #0]
  2e:   f7ff fff5   bl  1c <dummy>
  32:   2000        movs    r0, #0
  34:   bd80        pop {r7, pc}
  36:   46c0        nop         ; (mov r8, r8)
  38:   20000004    andcs   r0, r0, r4
  3c:   20000000    andcs   r0, r0, r0

kompiler ini yang menggunakan stack untuk penduduk setempat, sekarang menggunakan ram untuk global dan kode ini seperti yang ditulis rusak karena saya tidak menangani. data atau .bss dengan benar.

dan satu hal terakhir yang tidak bisa kita lihat dalam pembongkaran.

:1000000000100020150000001B0000001B00000075
:100010001B00000000F004F8FFE7FEE77047000057
:1000200080B500AF04480068401C04490860FFF731
:10003000F5FF002080BDC046040000200000002025
:08004000E0FFFF7F010000005A
:0400480078563412A0
:00000001FF

Saya mengubah x menjadi pre-init dengan 0x12345678. Skrip linker saya (ini untuk gnu ld) memiliki ini ted at bob thing. yang memberitahu linker saya ingin tempat terakhir berada di ruang alamat ted, tetapi menyimpannya dalam biner di ruang alamat ted dan seseorang akan memindahkannya untuk Anda. Dan kita bisa melihat itu terjadi. ini adalah format intel hex. dan kita bisa melihat 0x12345678

:0400480078563412A0

ada di ruang alamat flash biner.

readelf juga menunjukkan ini

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  EXIDX          0x010040 0x00000040 0x00000040 0x00008 0x00008 R   0x4
  LOAD           0x010000 0x00000000 0x00000000 0x00048 0x00048 R E 0x10000
  LOAD           0x020004 0x20000004 0x00000048 0x00004 0x00004 RW  0x10000
  LOAD           0x030000 0x20000000 0x20000000 0x00000 0x00004 RW  0x10000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10

baris LOAD di mana alamat virtual 0x20000004 dan fisiknya 0x48

old_timer
sumber
pada awalnya saya memiliki dua gambar blur hal:
user16307
1.) "kasus penggunaan utama adalah memiliki instruksi dalam rom / flash dan data dalam ram." ketika Anda mengatakan "data dalam RAM di sini", maksud Anda data yang dimasukkan dalam proses program. atau apakah Anda juga memasukkan data yang diinisialisasi. maksud saya ketika kami mengunggah kode ke ROM, sudah ada data yang diinisialisasi dalam kode kami. misalnya di oode kami jika kami memiliki: int x = 1; int y = x +1; kode di atas ada instruksi dan ada data awal yaitu 1. (x = 1). apakah data ini juga disalin ke RAM atau hanya disimpan dalam ROM.
user16307
13
hah, sekarang saya tahu batas karakter untuk jawaban pertukaran tumpukan!
old_timer
2
Anda harus menulis buku yang menjelaskan konsep-konsep tersebut kepada pemula. "Saya punya miliaran contoh di github" - Mungkinkah untuk membagikan beberapa contoh
AkshayImmanuelD
1
Saya baru saja melakukannya. Tidak ada yang melakukan sesuatu yang bermanfaat, tetapi tetap saja itu adalah contoh kode untuk mikrokontroler. Dan saya memang memasang tautan github dari mana Anda dapat menemukan semua hal lain yang telah saya bagikan, baik, buruk, atau sebaliknya.
old_timer
8

Jawaban ini akan lebih fokus pada proses boot. Pertama, koreksi - tulis ke flash dilakukan setelah MCU (atau paling tidak sebagian darinya) sudah dimulai. Pada beberapa MCU (biasanya yang lebih maju), CPU itu sendiri mungkin mengoperasikan port serial dan menulis ke register flash. Jadi menulis dan menjalankan program adalah proses yang berbeda. Saya akan berasumsi bahwa program telah ditulis ke flash.

Inilah proses boot dasar. Saya akan menyebutkan beberapa variasi umum, tetapi sebagian besar saya menjaga ini tetap sederhana.

  1. Reset: Ada dua tipe dasar. Yang pertama adalah power-on reset, yang dihasilkan secara internal saat tegangan suplai meningkat. Yang kedua adalah pin toggle eksternal. Apapun itu, pengaturan ulang memaksa semua sandal jepit di MCU ke kondisi yang telah ditentukan.

  2. Inisialisasi perangkat keras tambahan: Waktu ekstra dan / atau siklus jam mungkin diperlukan sebelum CPU mulai berjalan. Misalnya, dalam MCU TI yang saya kerjakan, ada rantai pemindaian konfigurasi internal yang dimuat.

  3. Boot CPU: CPU mengambil instruksi pertamanya dari alamat khusus yang disebut vektor reset. Alamat ini ditentukan saat CPU dirancang. Dari sana, itu hanya eksekusi program biasa.

    CPU mengulangi tiga langkah dasar berulang-ulang:

    • Ambil: Baca instruksi (nilai 8-, 16-, atau 32-bit) dari alamat yang disimpan dalam register program register (PC), kemudian tambahkan PC.
    • Decode: Konversi instruksi biner menjadi satu set nilai untuk sinyal kontrol internal CPU.
    • Jalankan: Jalankan instruksi - tambahkan dua register, baca dari atau tulis ke memori, cabang (ganti PC), atau apa pun.

    (Ini sebenarnya lebih rumit dari ini. CPU biasanya pipelined , yang berarti mereka dapat melakukan masing-masing langkah di atas pada instruksi yang berbeda pada saat yang sama. Masing-masing langkah di atas mungkin memiliki beberapa tahap pipa. Kemudian ada pipa paralel, prediksi cabang , dan semua hal arsitektur komputer mewah yang membuat CPU Intel tersebut membutuhkan milyaran transistor untuk dirancang.)

    Anda mungkin bertanya-tanya bagaimana cara kerjanya. CPU memiliki bus yang terdiri dari sinyal alamat (keluar) dan data (masuk / keluar). Untuk melakukan pengambilan, CPU menetapkan garis alamatnya ke nilai di penghitung program, lalu mengirimkan jam melewati bus. Alamat diterjemahkan untuk mengaktifkan memori. Memori menerima jam dan alamat, dan menempatkan nilai pada alamat itu pada baris data. CPU menerima nilai ini. Data membaca dan menulis serupa, kecuali alamatnya berasal dari instruksi atau nilai dalam register tujuan umum, bukan PC.

    CPU dengan arsitektur von Neumann memiliki bus tunggal yang digunakan untuk instruksi dan data. CPU dengan arsitektur Harvard memiliki satu bus untuk instruksi dan satu untuk data. Dalam MCU nyata, kedua bus ini mungkin terhubung ke memori yang sama, jadi seringkali (tetapi tidak selalu) sesuatu yang tidak perlu Anda khawatirkan.

    Kembali ke proses boot. Setelah reset, PC dimuat dengan nilai awal yang disebut vektor reset. Ini dapat dibangun ke dalam perangkat keras, atau (dalam ARM Cortex-M CPU) dapat dibaca dari memori secara otomatis. CPU mengambil instruksi dari vektor reset dan mulai mengulangi langkah-langkah di atas. Pada titik ini, CPU berjalan normal.

  4. Boot loader: Seringkali ada beberapa pengaturan tingkat rendah yang perlu dilakukan untuk membuat MCU lainnya beroperasi. Ini dapat mencakup hal-hal seperti membersihkan RAM dan memuat pengaturan trim manufaktur untuk komponen analog. Mungkin juga ada opsi untuk memuat kode dari sumber eksternal seperti port serial atau memori eksternal. MCU dapat menyertakan boot ROM yang berisi program kecil untuk melakukan hal-hal ini. Dalam kasus ini, CPU reset vector menunjuk ke ruang alamat ROM boot. Ini pada dasarnya adalah kode normal, itu hanya disediakan oleh pabrikan sehingga Anda tidak perlu menulisnya sendiri. :-) Di PC, BIOS setara dengan boot ROM.

  5. Pengaturan lingkungan C: C berharap memiliki tumpukan (area RAM untuk menyimpan status selama panggilan fungsi) dan menginisialisasi lokasi memori untuk variabel global. Ini adalah bagian .stack, .data, dan .bss yang dibicarakan Dwelch. Variabel global yang diinisialisasi memiliki nilai inisialisasi yang disalin dari flash ke RAM pada langkah ini. Variabel global yang tidak diinisialisasi memiliki alamat RAM yang berdekatan, sehingga seluruh blok memori dapat diinisialisasi ke nol dengan sangat mudah. Tumpukan tidak perlu diinisialisasi (meskipun bisa) - yang perlu Anda lakukan hanyalah mengatur register penunjuk tumpukan CPU sehingga menunjuk ke wilayah yang ditetapkan dalam RAM.

  6. Fungsi utama : Setelah lingkungan C diatur, loader C memanggil fungsi utama (). Di situlah kode aplikasi Anda biasanya dimulai. Jika mau, Anda dapat meninggalkan perpustakaan standar, melewati pengaturan lingkungan C, dan menulis kode Anda sendiri untuk memanggil main (). Beberapa MCU mungkin membiarkan Anda menulis bootloader Anda sendiri, dan kemudian Anda dapat melakukan semua pengaturan tingkat rendah sendiri.

Hal-hal lain-lain: Banyak MCU akan membiarkan Anda mengeksekusi kode dari RAM untuk kinerja yang lebih baik. Ini biasanya diatur dalam konfigurasi tautan. Linker memberikan dua alamat untuk setiap fungsi - alamat beban , yang merupakan tempat kode pertama kali disimpan (biasanya flash), dan alamat run , yang merupakan alamat yang dimuat ke PC untuk menjalankan fungsi (flash atau RAM). Untuk menjalankan kode dari RAM, Anda menulis kode untuk membuat CPU menyalin kode fungsi dari alamat pemuatannya dalam flash ke alamat yang dijalankan di RAM, kemudian memanggil fungsi di alamat yang dijalankan. Linker dapat mendefinisikan variabel global untuk membantu ini. Tetapi mengeksekusi kode keluar dari RAM adalah opsional di MCU. Anda biasanya hanya melakukannya jika Anda benar-benar membutuhkan kinerja tinggi atau jika Anda ingin menulis ulang flash.

Adam Haun
sumber
1

Ringkasan Anda kira-kira tepat untuk arsitektur Von Neumann . Kode awal biasanya dimuat ke dalam RAM melalui bootloader, tetapi tidak (biasanya) bootloader perangkat lunak yang merujuk istilah umum. Ini biasanya perilaku 'dimasukkan ke silikon'. Eksekusi kode dalam arsitektur ini seringkali melibatkan instruksi caching prediktif dari ROM sedemikian rupa sehingga prosesor memaksimalkan waktu mengeksekusi kode dan tidak menunggu kode untuk dimuat ke RAM. Saya telah membaca di suatu tempat bahwa MSP430 adalah contoh dari arsitektur ini.

Dalam perangkat Arsitektur Harvard , instruksi dieksekusi langsung dari ROM sementara data memori (RAM) diakses melalui bus yang terpisah. Dalam arsitektur ini, kode mulai dijalankan dari reset vektor. PIC24 dan dsPIC33 adalah contoh arsitektur ini.

Adapun flipping bit aktual yang memulai proses ini, yang dapat bervariasi dari satu perangkat ke perangkat lainnya dan dapat melibatkan para debugger, JTAG, metode kepemilikan, dll.

sedikit menggerutu
sumber
Tetapi Anda melewatkan beberapa poin dengan cepat. Mari kita lakukan gerakan lambat. Katakanlah kode biner "pertama" ditulis ke ROM. Ok .. Setelah itu Anda menulis "Memori data diakses" .... Tapi dari mana data "ke RAM" pertama kali berasal pada saat start up? Apakah ini berasal dari ROM lagi? Dan jika demikian, bagaimana boot-loader tahu bagian ROM yang akan ditulis ke RAM di awal?
user16307
Anda benar, saya melewatkan banyak hal. Orang lain punya jawaban yang lebih baik. Saya senang Anda mendapatkan apa yang Anda cari.
slightlynybbled