Як здійснюються завантаження та запуск мікроконтролера, поетапно?


17

Коли C код записується, компілюється та завантажується в мікроконтролер, мікроконтролер починає працювати. Але якщо взяти цей процес завантаження та запуску поетапно, у мене виникають певні плутанини щодо того, що насправді відбувається всередині MCU (пам'ять, процесор, завантажувач). Ось (швидше за все, неправильно), що я відповів би, якби хтось запитав мене:

  1. Скомпільований двійковий код записується на флеш-диск (або EEPROM) через USB
  2. Завантажувач копіює частину цього коду в оперативну пам'ять. Якщо так, як завантажувач знає, що скопіювати (яку частину ПЗУ скопіювати в ОЗУ)?
  3. Процесор починає отримувати інструкції та дані коду з ПЗУ та ОЗУ

Це неправильно?

Чи можливо узагальнити цей процес завантаження та запуску з деякою інформацією про те, як пам'ять, завантажувач та процесор взаємодіють на цій фазі?

Я знайшов багато основних пояснень того, як ПК завантажується через BIOS. Але я застряг у процесі запуску мікроконтролера.

Відповіді:


31

1) складений двійковий файл записується на пром / флеш так. USB, serial, i2c, jtag тощо, залежать від пристрою щодо того, що підтримується цим пристроєм, незалежно від розуміння процесу завантаження.

2) Це, як правило, не стосується мікроконтролера, головним випадком використання є вказівки в режимі rom / flash та дані в операційній пам’яті. Незалежно від архітектури. для немікроконтролера, вашого ПК, ноутбука, вашого сервера, програма копіюється з енергонезалежного (дискового) в таран, а потім запустіть звідти. Деякі мікроконтролери дозволяють також використовувати таран, навіть ті, які заявляють, що вони є голодними, навіть якщо це, мабуть, порушує визначення. Немає нічого про гарвард, що не заважає вам зіставити барана в сторону інструкцій, вам просто потрібно мати механізм, щоб отримати вказівки там після підключення живлення (що порушує визначення, але системи гарвардів повинні зробити це, щоб бути корисним іншим ніж як мікроконтролери).

3) роду.

Кожен процесор "завантажується" детермінованим, як задумано, способом. Найпоширеніший спосіб - це векторна таблиця, де адреса перших інструкцій, що запускаються після включення, перебуває у векторі скидання, адреса, яку апаратне зчитування потім використовує цю адресу, щоб почати працювати. Інший загальний спосіб - почати виконання процесора без векторної таблиці за деякою відомою адресою. Іноді в мікросхемі будуть "ремінці", деякі шпильки, які ви можете зв'язати високими або низькими, перш ніж випускати скидання, які логіка використовує для завантаження різними способами. Ви повинні відокремити сам процесор, ядро ​​процесора від решти системи. Зрозумійте, як працює процесор, а потім зрозумійте, що дизайнери мікросхем / систем мають декодери адрес установки навколо зовнішньої сторони процесора, щоб частина частини адресного простору cpus спілкувалася спалахом, а деякі - з оперативними та деякі з периферійними пристроями (uart, i2c, spi, gpio тощо). Ви можете взяти те саме ядро ​​процесора, якщо хочете, і обгорнути його по-іншому. Це те, що ви отримуєте, купуючи щось на базі руки або мипу. рука і миші роблять процесорні ядра, які чіп люди купують і обертають своїми руками навколо, з різних причин вони не роблять ці речі сумісними від марки до марки. Ось чому рідко можна задати загальне питання про руку, якщо мова йде про що-небудь поза ядром.

Мікроконтролер намагається бути системою на мікросхемі, тому його енергонезалежна пам’ять (flash / rom), летюча (sram) та cpu - це одна і та ж мікросхема разом із сумішшю периферійних пристроїв. Але чіп розроблений внутрішньо таким чином, що спалах відображається в адресному просторі процесора, що відповідає характеристикам завантаження цього процесора. Якщо, наприклад, у процесора є вектор скидання за адресою 0xFFFC, тоді повинен бути flash / rom, який відповідає на цю адресу, яку ми можемо запрограмувати через 1), а також достатньо flash / rom в адресному просторі для корисних програм. Дизайнер мікросхем може вибрати, що 0x1000 байт спалаху починається від 0xF000, щоб задовольнити ці вимоги. І, можливо, вони ставлять якусь кількість оперативної пам'яті за нижньою адресою, а може, і 0x0000, а периферійні пристрої десь посередині.

Інша архітектура процесора може почати виконувати за адресою нуль, тому їм потрібно буде робити навпаки, розмістити спалах, щоб він відповідав діапазону адрес навколо нуля. наприклад, 0x0000 до 0x0FFF. а потім покласти десь барана в іншому місці.

Дизайнери чіпів знають, як завантажуються процесорні системи, і вони помістили туди енергонезалежне сховище (flash / rom). Тоді саме люди з програмного забезпечення повинні писати завантажувальний код, щоб відповідати загальновідомій поведінці цього процесора. Ви повинні розмістити адресу вектора скидання у векторі скидання, а ваш код завантаження за адресою, визначеною у векторі скидання. Тут дуже допоможе вам ланцюжок інструментів. Іноді, esp з ідентифікаторами точок і клацань, або іншими пісочницями, вони можуть зробити для вас більшу частину роботи, все, що ви робите, - це викликати apis мовою високого рівня (C).

Але, однак це робиться, програма, завантажена у flash / rom, повинна відповідати поведінці завантажуваного процесора з жорстким проводом. Перед частиною C у програмі main () та після, якщо ви використовуєте main як вхідну точку, деякі речі потрібно зробити. Програміст змінного струму припускає, що коли оголошують змінну з початковим значенням, вони очікують, що вона справді спрацює. Добре, що змінні, крім const, є в операційній пам’яті, але якщо у вас є початкове значення, то початкове значення повинно бути в енергонезалежному барі. Отже, це сегмент .data, і завантажувальний файл C потребує копіювання матеріалів .data з флеш-пам'яті (там, де зазвичай це визначається для вас інструментальною ланцюжком). Глобальні змінні, які ви заявляєте без початкового значення, вважаються нульовими до запуску вашої програми, хоча ви не повинні цього вважати і, на щастя, деякі компілятори починають попереджати про неініціалізовані змінні. Цей сегмент. Знову інструментальна мережа тут вам дуже допомагає. І нарешті, мінімальний мінімум - вам потрібно встановити покажчик стека, оскільки програми C очікують мати локальні змінні та викликати інші функції. Тоді, можливо, робиться якийсь інший специфічний матеріал для чіпів, або ми залишаємо, щоб решта специфічних чіпів траплялася в C. не потрібно зберігати в енергонезалежній пам'яті, але вихідна адреса і скільки робить. Знову інструментальна мережа тут вам дуже допомагає. І нарешті, мінімальний мінімум - вам потрібно встановити покажчик стека, оскільки програми C очікують мати локальні змінні та викликати інші функції. Тоді, можливо, робиться якийсь інший специфічний матеріал для чіпів, або ми залишаємо, щоб решта специфічних чіпів траплялася в C. не потрібно зберігати в енергонезалежній пам'яті, але вихідна адреса і скільки робить. Знову інструментальна мережа тут вам дуже допомагає. І нарешті, мінімальний мінімум - вам потрібно встановити покажчик стека, оскільки програми C очікують мати локальні змінні та викликати інші функції. Тоді, можливо, робиться якийсь інший специфічний матеріал для чіпів, або ми залишаємо, щоб решта специфічних чіпів траплялася в C.

Ядра серії cortex-m з руки зроблять щось для вас, вказівник стека знаходиться у векторній таблиці, є вектор скидання, який вказує на код, який слід запустити після скидання, так що крім того, що вам потрібно зробити щоб генерувати векторну таблицю (для якої ви зазвичай використовуєте ASM), ви можете перейти до чистого C без asm. тепер ви не отримуєте свої .data скопійовані ні ваш .bss нульовий, тому вам доведеться це робити самостійно, якщо ви хочете спробувати пройти без asm на чомусь на базі cortex-m. Більшою особливістю є не вектор скидання, а переривання векторів, де апаратне забезпечення дотримується зброї, рекомендованої C викликом виклику, і зберігає регістри для вас, і використовує правильне повернення для цього вектора, так що вам не доведеться обертати правильну зону навколо кожного обробника ( або мати конкретні директиви для вашої цілі, щоб ланцюжок інструментів обертав її для вас).

Наприклад, для мікросхем часто використовуються мікроконтролери в системах, що базуються на акумуляторах, тому низька потужність, тому деякі виходять із скидання, коли більшість периферійних пристроїв вимкнено, і вам потрібно ввімкнути кожну з цих підсистем, щоб ви могли їх використовувати . Uarts, gpios тощо. Часто використовується тактова частота з низьким бажанням прямо від кристала або внутрішнього генератора. І ваш дизайн системи може показати, що вам потрібен швидший годинник, тому ви ініціалізуєте це. Ваш годинник може бути занадто швидким для спалаху чи оперативної пам’яті, тому, можливо, вам знадобилося змінити стан очікування перед тим, як підняти годинник. Можливо, потрібно налаштувати uart, usb чи інші інтерфейси. тоді ваша заява може зробити своє.

Настільний комп'ютер, ноутбук, сервер та мікроконтролер не відрізняються тим, як вони завантажуються / працюють. За винятком того, що вони знаходяться в основному не на одній мікросхемі. Програма bios часто знаходиться на окремому флеш-пам’яті / rom від процесора. Хоча останнім часом x86 cpus залучає все більше і більше того, що раніше використовували чіпи підтримки в той же пакет (pcie-контролери тощо), але у вас все ще є більшість ваших оперативної пам’яті та чім-оф, але це все-таки система, і вона все ще працює точно те саме на високому рівні. Процес завантаження процесора добре відомий, дизайнери плати розміщують flash / rom у адресному просторі, де завантажується процесор. що програма (частина BIOS на x86 pc) виконує всі згадані вище речі, вона запускає різні периферійні пристрої, ініціалізує драма, перераховує шини pcie тощо. Зазвичай користувач може налаштовуватися на основі біологічних налаштувань або того, що ми звикли називати налаштуваннями cmos, тому що в той час саме використовувались технології. Неважливо, є налаштування користувача, які ви можете перейти та змінити, щоб повідомити код завантаження bios, як змінювати те, що він робить.

різні люди будуть використовувати різну термінологію. чіп-чоботи, це перший код, який працює. іноді називають завантажувальним. завантажувач із завантажувачем слів часто означає, що якщо ви нічого не робите, щоб заважати, це завантажувальний засіб, який переносить вас із загального завантаження в щось більше, вашу програму чи операційну систему. але частина завантажувача означає, що ви можете перервати процес завантаження, а потім, можливо, завантажити інші тестові програми. якщо ви коли-небудь використовували uboot, наприклад, у вбудованій системі Linux, ви можете натиснути клавішу і зупинити нормальну завантаження, тоді ви можете завантажити тестове ядро ​​в операційний і завантажувати його замість того, яке знаходиться у спалах, або ви можете завантажити свій власні програми, або ви можете завантажити нове ядро, потім завантажувач завантажить його у спалах, щоб наступного разу при запуску він запустив новий матеріал.

Що стосується самого процесора, то основного процесора, який не знає оперативної пам'яті від спалаху з периферійних пристроїв. Немає поняття завантажувач, операційна система, додаток. Це просто послідовність інструкцій, які подаються на процесор, який потрібно виконати. Це програмні терміни, щоб відрізняти різні завдання програмування одне від одного. Поняття програмного забезпечення одне від одного.

Деякі мікроконтролери мають окремий завантажувач, наданий постачальником мікросхем в окремій спалаху або окремій області спалаху, яку ви, можливо, не зможете змінити. У цьому випадку часто є шпилька або набір штифтів (я називаю їх ремінцями), що якщо ви пов’язуєте їх високими або низькими, перш ніж скинути скидання, ви говорите логіці та / або тому завантажувачу, що робити, наприклад, одна комбінація ременів може скажіть мікросхемі запустити цей завантажувач і чекати на uart, щоб дані були запрограмовані у спалах. Встановіть ремінці іншим способом, і ваша програма завантажується не завантажувачем постачальників чіпів, що дозволяє програмувати чіп на місцях або відновлюватись після збоїв у програмі. Іноді це просто чиста логіка, яка дозволяє програмувати спалах. Це досить часто в ці дні,

Причиною, по якій у більшості мікроконтролерів є набагато більше спалаху, ніж оперативної пам'яті, є те, що основним випадком використання є запуск програми безпосередньо з спалаху, і достатньо лише оперативної пам'яті для покриття стеку та змінних. Хоча в деяких випадках ви можете запускати програми з оперативної пам’яті, які вам потрібно скласти правильно і зберігати у спалах, а потім скопіювати перед викликом.

EDIT

спалахи

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

Отже, це приклад для cortex-m0, а cortex-ms працюють так само, наскільки це стосується цього прикладу. У цьому прикладі конкретний чіп має спалах додатка за адресою 0x00000000 в адресному просторі руки та оперативної пам’яті в 0x20000000.

Спосіб завантаження cortex-m - 32-бітове слово за адресою 0x0000 - це адреса для ініціалізації вказівника стека. Мені не потрібно багато стека для цього прикладу, так що 0x20001000 буде достатньо, очевидно, що має бути оперативна пам'ять під цією адресою (спосіб натискання на руку, спочатку віднімає потім натискає, тому якщо ви встановите 0x20001000, перший елемент стека знаходиться за адресою 0x2000FFFC вам не доведеться використовувати 0x2000FFFC). 32-бітове слово за адресою 0x0004 - це адреса обробника скидання, в основному перший код, який запускається після скидання. Потім є більше оброблювачів переривань і подій, які є специфічними для цього ядра і мікросхема cortex, можливо, аж 128 або 256, якщо ви їх не використовуєте, то вам не потрібно налаштовувати таблицю для них, я підкинув декілька для демонстрації цілей.

Мені не потрібно мати справу з .data ні .bss у цьому прикладі, тому що я знаю, що в цих сегментах нічого немає, дивлячись на код. Якби вони були, я би розібрався з цим, і буде за секунду.

Таким чином, стек - це налаштування, перевірка, .data подбали, перевірка, .bss, перевірка, так що завантажувальний файл C виконаний, може розгалужуватися до функції введення для C. Оскільки деякі компілятори додадуть додаткові непотрібні, якщо вони побачать функцію main () і на шляху до main, я не використовую точну назву, я тут не використовував notmain () як мою точку входу C. Таким чином, обробник скидання викликає notmain (), тоді, якщо / коли notmain () повертається, він переходить до зависання, що є просто нескінченним циклом, можливо, погано названим.

Я твердо вірю в оволодіння інструментами, багато хто не робить, але те, що ви знайдете, це те, що кожен розробник голого металу робить свою власну справу через майже повну свободу, а не віддалено настільки обмежений, як ви робите програми чи веб-сторінки . Вони знову роблять свою справу. Я вважаю за краще мати свій код завантажувального коду та сценарій посилання. Інші покладаються на інструментальну ланцюжок або грають у пісочниці постачальників, де більшу частину роботи робить хтось інший (і якщо щось зламаєш, ти потрапляєш у світ боляче, а з голим металом речі ламаються часто і драматично).

Тому я збираю, збираю та зв'язую з інструментами gnu:

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

То як же завантажувач знає, де речі. Тому що компілятор зробив роботу. У першому випадку асемблер генерував код для flash.s, і тим самим знає, де мітки (мітки - це просто адреси, як імена функцій, імена змінних тощо), тому мені не довелося рахувати байти та заповнювати вектор таблиці вручну, я використовував назву мітки, і асемблер зробив це для мене. Тепер ви запитуєте, якщо скиданням є адреса 0x14, чому асемблер поставив 0x15 у векторну таблицю. Ну це cortex-m, і він завантажується і працює лише в режимі великого пальця. За допомогою ARM, коли ви підключаєтесь до адреси, якщо розгалуження до режиму великого пальця, потрібно встановити lsbit, якщо режим руки потім скинути. Тож вам завжди потрібен цей набір бітів. Я знаю інструменти і, поставивши .thumb_func перед міткою, якщо ця мітка використовується такою, як є у векторній таблиці, або для розгалуження на будь-яку іншу. Мережа інструментів знає встановити lsbit. Отже, вона має тут 0x14 | 1 = 0x15. Аналогічно і для підвішування. Тепер розбиральник не показує 0x1D для виклику notmain (), але не хвилюйтесь, що інструменти правильно побудували інструкцію.

Тепер, коли код у notmain, ці локальні змінні не використовуються, вони є мертвим кодом. Компілятор навіть коментує цей факт, кажучи, що y встановлено, але не використовується.

Зверніть увагу на адресний простір, всі вони починаються з адреси 0x0000 і йдуть звідти, щоб векторна таблиця була правильно розміщена, .text або програмний простір також правильно розміщені, як я отримав flash.s перед кодом notmain.c знаючи інструменти, поширена помилка - це не виправдатися, а врізатися і сильно спалити. ІМО вам потрібно розібрати, щоб переконатися, що речі розміщені прямо перед першим завантаженням. Після того, як ви знайдете речі в потрібному місці, вам обов’язково потрібно перевіряти кожен раз. Просто для нових проектів або якщо вони висять.

Тепер те, що дивує деяких людей, - це те, що немає жодної причини очікувати, що будь-який два компілятори будуть отримувати однаковий вихід з одного входу. Або навіть той самий компілятор з різними налаштуваннями. Використовуючи clang, компілятор llvm, я отримую ці два виходи з оптимізацією та без неї

llvm / clang оптимізовано

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

не оптимізований

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

так що це брехня, компілятор оптимізував додавання, але він виділив два елементи в стеку для змінних, оскільки це локальні змінні, вони знаходяться в операційній пам’яті, але в стеці не за фіксованими адресами, з глобаліками побачать, що це зміни. Але компілятор зрозумів, що він може обчислити y під час компіляції, і немає підстав обчислювати його під час виконання, тому він просто розмістив 1 у просторі стеку, виділеному для x, і 2 для простору стека, виділеного для y. компілятор "виділяє" цей простір внутрішніми таблицями, декларую стек плюс 0 для змінної y та стек плюс 4 для змінної x. компілятор може робити все, що завгодно, поки код, який він реалізує, відповідає стандарту C або очікуванням програміста C. Немає причини, чому компілятор повинен залишати х у стеці + 4 протягом тривалості функції,

Якщо я додаю функцію манекен в асемблері

.thumb_func
.globl dummy
dummy:
    bx lr

а потім зателефонуйте

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

вихід змінюється

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

Тепер, коли ми вклали функції, функції notmain необхідно зберегти свою зворотну адресу, щоб вона могла зменшити зворотну адресу для вкладеного виклику. це тому, що рука використовує реєстр для повернення, якби він використовував стек, як, скажімо, x86 або якийсь інший добре ... він би все одно використовував стек, але інакше. Тепер ви запитуєте, чому він натиснув r4? Ну, не так давно конвенція виклику змінилася, щоб стек вирівнювався на 64-бітових (два слова) межі замість 32-бітових, меж одного слова. Тому їм потрібно щось натиснути, щоб стек вирівнювався, тому компілятор довільно вибрав r4 чомусь, неважливо, чому. Перейти на r4 було б помилкою, хоча згідно з умовою виклику для цієї мети, ми не робимо clobber r4 під час виклику функції, ми можемо клобувати r0 через r3. r0 - повернене значення. Можливо, це робить оптимізацію хвоста,

Але ми бачимо, що математика x і y оптимізована до твердо кодованого значення 2, яке передається функції фіктивного (манекен був спеціально закодований в окремому файлі, в цьому випадку asm, щоб компілятор не оптимізував функцію виклику повністю, якби у мене була фіктивна функція, яка просто поверталася в C у notmain.c, оптимізатор видалив би виклик функції x, y та манекен, оскільки всі вони мертвий / непотрібний код).

Також зауважте, що через те, що код flash.s збільшився, головне не є, і ланцюжок інструментів подбав про виправлення всіх адрес для нас, тому нам не потрібно робити це вручну.

неоптимізований стук для довідки

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}

оптимізований клакс

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}

цей автор компілятора вирішив використовувати r7 як фіктивну змінну для вирівнювання стека, також він створює покажчик кадру за допомогою r7, хоча у кадрі стека немає нічого. в основному інструкція могла бути оптимізована. але він використовував поп, щоб повернути не три інструкції, це, мабуть, було на мене, я думаю, що я можу отримати gcc, щоб зробити це за допомогою правильних параметрів командного рядка (із зазначенням процесора).

це здебільшого має відповісти на всі ваші запитання

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

Зараз у мене є глобалісти. тож вони йдуть у будь-які .data чи .bss, якщо вони не оптимізуються.

перед тим, як ми подивимось на кінцевий вихід, давайте подивимось на проміжний об'єкт

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

тепер у цій інформації відсутня інформація, але вона дає уявлення про те, що відбувається, лінкер - це той, який бере об’єкти і пов'язує їх разом з інформацією, наданою ним (у цьому випадку flash.ld), яка повідомляє, де .text і. дані і так. компілятор не знає таких речей, він може зосередитися лише на представленому коді, будь-який зовнішній він повинен залишити отвір для того, щоб лінкер заповнив з'єднання. Будь-які дані, які він має, залишають спосіб з'єднати ці речі разом, тому адреси для всіх нульові, засновані тут просто тому, що компілятор і цей розбиральник не знають. тут не відображається інша інформація, яку лінкер використовує для розміщення речей. код тут є позицією досить незалежною, щоб лінкер міг зробити свою роботу.

Тоді ми бачимо принаймні демонтаж зв'язаного виводу

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

компілятор в основному запитував дві 32-бітні змінні в операційному режимі. Один є в .bss, тому що я не ініціалізував його, тому передбачається, що він init як нуль. інше - .дані, тому що я ініціалізував це при оголошенні.

Тепер, оскільки це глобальні змінні, передбачається, що інші функції можуть змінювати їх. компілятор не робить припущень щодо того, коли notmain можна викликати, тому він не може оптимізувати те, що він може бачити, y = x + 1 математика, тому він повинен виконувати цей час виконання. Він повинен прочитати з оперативної пам'яті дві змінні, додати їх і зберегти назад.

Тепер чітко цей код не працює. Чому? тому що мій завантажувальний пристрій, як показано тут, не готує оперативної пам’яті до виклику notmain, тому те, що сміття було в 0x20000000 та 0x20000004, коли чіп прокинувся - це те, що буде використано для y та x.

Не збираюся цього показувати. Ви можете прочитати мої ще більш звиті бурення на .data та .bss, і чому я ніколи не потребую їх у своєму голому металевому коді, але якщо ви вважаєте, що вам потрібно і хочете освоїти інструменти, а не сподіватися, що хтось зробив це правильно? .

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

сценарії лінкерів і завантажувальні програми дещо специфічні для компілятора, тому все, що ви дізнаєтесь про одну версію одного компілятора, може бути перекинуто на наступну версію або з іншим компілятором, але ще одна причина, чому я не вкладаю багато зусиль у підготовку .data та .bss просто щоб бути таким ледачим:

unsigned int x=1;

Я б набагато скоріше це зробив

unsigned int x;
...
x = 1;

і нехай компілятор помістить його в .text для мене. Іноді таким чином економить спалах, іноді більше горять. Це, безумовно, набагато простіше програмувати і переносити з версії інструментарію або одного компілятора до іншого. Набагато надійніший, менше схильний до помилок. Так, не відповідає стандарту С.

тепер що робити, якщо ми робимо ці статичні глобалі?

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

добре

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

очевидно, що ці змінні не можуть бути змінені іншим кодом, тому компілятор тепер може під час компіляції оптимізувати мертвий код, як це було раніше.

неоптимізований

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

Цей компілятор, який використовував стек для місцевих жителів, тепер використовує ram для глобальних мереж, і цей код, як написано, порушено, тому що я не обробляв .data і .bss належним чином.

і одне останнє, що ми не можемо побачити при розбиранні.

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

Я змінив x, щоб бути попередньо init з 0x12345678. У моєму скрипті лінкера (це для gnu ld) є ця теда на bob річ. що повідомляє лінкеру, що я хочу, щоб остаточне місце знаходилось у адресному просторі Тед, але зберігайте його у двійковому у адресному просторі Тед, і хтось перемістить його за вас. І ми можемо бачити, що це сталося. це формат Intel hex. і ми можемо бачити 0x12345678

:0400480078563412A0

знаходиться у адресному просторі спалаху двійкового файлу.

readelf також це показує

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

рядок LOAD, де віртуальна адреса 0x20000004, а фізична - 0x48


на самому початку у мене є дві розмиті картини з речей:
user16307

1.) "Основним випадком використання є вказівки в rom / flash та дані в операційній пам'яті." коли ви говорите «дані в оперативній пам’яті тут», ви маєте на увазі дані, що генеруються в процесі роботи програми. чи ви також включаєте ініціалізовані дані. я маю на увазі, коли ми завантажуємо код в ПЗУ, в нашому коді вже є ініціалізовані дані. наприклад, у нашому oode, якщо у нас є: int x = 1; int y = x +1; у наведеному вище коді є інструкції, і є початкові дані, які дорівнюють 1. (x = 1). чи ці дані також копіюються в оперативну пам'ять або залишаються лише в ПЗУ.
користувач16307

13
га, я тепер знаю обмеження символів для відповіді обміну стеками!
old_timer

2
Ви повинні написати книгу, яка пояснює такі поняття для новачків. "У мене є gillionub прикладів" - Чи можна поділитися кількома прикладами
AkshayImmanuelD

1
Я тільки що зробив. Не той, хто робить щось корисне, але все ж це приклад коду для мікроконтролера. І я поклав посилання github, з якого ви можете знайти все інше, що я поділився, добре, погано чи іншим чином.
old_timer

8

Ця відповідь буде зосереджена більше на процесі завантаження. По-перше, виправлення - запис у flash виконується після того, як MCU (або принаймні частина його) вже запущена. На деяких MCU (як правило, більш вдосконалених) ЦП може сам працювати з послідовними портами і записувати у флеш-регістри. Тому написання та виконання програми - це різні процеси. Я припускаю, що програма вже написана для спалаху.

Ось основний процес завантаження. Я назву деякі поширені варіанти, але в основному я тримаю це просто.

  1. Скидання: Є два основних типи. Перший - це скидання живлення, яке виробляється всередині країни під час збільшення напруги живлення. Другий - зовнішнє перемикання штифтів. Незалежно від цього, скидання змушує всіх тривог в MCU до заданого стану.

  2. Додаткова ініціалізація обладнання: Перед тим, як процесор почне працювати, можуть знадобитися додатковий час та / або цикли годин. Наприклад, в TI MCU, над якими я працюю, є ланцюг сканування внутрішньої конфігурації, яка завантажується.

  3. Завантаження завантаження процесора: ЦП отримує свою першу інструкцію зі спеціальної адреси, що називається вектором скидання. Ця адреса визначається під час розробки ЦП. Звідти це просто нормальне виконання програми.

    Процесор повторює три основні кроки знову і знову:

    • Вилучення: прочитайте інструкцію (8-, 16- або 32-бітове значення) з адреси, що зберігається в регістрі програмного лічильника (ПК), а потім збільшуйте ПК.
    • Декодування: Перетворіть бінарну інструкцію в набір значень для внутрішніх сигналів управління процесором.
    • Виконати: виконати інструкцію - додати два регістри, прочитати чи записати в пам'ять, відділити (змінити ПК) чи інше.

    (Це насправді складніше, ніж це. Процесорні процесори, як правило, конвеєрні , а це означає, що вони можуть робити кожен з перерахованих вище кроків за різними інструкціями одночасно. Кожен з вищезазначених кроків може мати декілька етапів конвеєра. Тоді є паралельні трубопроводи, передбачення гілок , і всі модні елементи комп'ютерної архітектури, завдяки чому ці процесори Intel вимагають мільярд транзисторів.)

    Вам може бути цікаво, як працює збірка. ЦП має шину, що складається з адресних (вихідних) та даних (введення / виведення) сигналів. Щоб зробити вибір, процесор встановлює свої адресні рядки значенням у програмному лічильнику, після чого надсилає годинник по шині. Адреса розшифровується, щоб увімкнути пам'ять. Пам'ять отримує годинник і адресу і ставить значення за цією адресою в рядки даних. ЦП отримує це значення. Дані читання і запису схожі, за винятком того, що адреса надходить з інструкції або значення в регістрі загального призначення, а не на ПК.

    Процесори з архітектурою фон Неймана мають єдину шину, яка використовується як для інструкцій, так і для даних. Процесори з архітектурою Гарварду мають одну шину для інструкцій та одну для даних. У справжніх MCU обидві ці шини можуть бути підключені до одних і тих же пам’яті, тому часто (але не завжди) щось не потрібно турбуватися.

    Повернення до процесу завантаження. Після скидання на ПК завантажується початкове значення, яке називається вектором скидання. Це може бути вбудовано в апаратне забезпечення, або (в процесорах ARM Cortex-M), воно може зчитуватися з пам'яті автоматично. Процесор отримує інструкцію з вектора скидання і починає циклічно виконувати кроки вище. У цей момент процесор виконує нормально.

  4. Завантажувач: Часто є деякі налаштування низького рівня, які потрібно зробити, щоб решта MCU працювала. Сюди можна віднести такі речі, як очищення оперативної пам’яті та завантаження параметрів обробки обробки для аналогових компонентів. Можливо, також є можливість завантаження коду із зовнішнього джерела, такого як послідовний порт або зовнішня пам'ять. MCU може включати завантажувальний ROM, який містить невелику програму для виконання цих дій. У цьому випадку векторний CPU скидає вказівки на адресний простір завантажувального ROM. Це в основному нормальний код, він просто надається виробником, тому вам не доведеться писати його самостійно. :-) У ПК BIOS є еквівалентом завантажувального ПЗУ.

  5. Налаштування середовища C: C розраховує на наявність стека (RAM-область для зберігання стану під час викликів функцій) та ініціалізовані місця пам'яті для глобальних змінних. Це розділи .stack, .data та .bss, про які говорить Dwelch. На цьому кроці ініціалізовані глобальні змінні мають значення ініціалізації, скопійовані з спалаху в оперативну пам'ять. У неініціалізованих глобальних змінних є RAM-адреси, які є близько один до одного, тому весь блок пам'яті можна легко ініціалізувати до нуля. Стек не потрібно ініціалізувати (хоча це може бути) - все, що вам потрібно зробити, це встановити реєстр покажчиків стека процесора, щоб він вказував на призначений регіон в ОЗП.

  6. Основна функція : Після налаштування середовища C завантажувач C викликає функцію main (). Ось тут зазвичай починається ваш код програми. Якщо ви хочете, ви можете залишити стандартну бібліотеку, пропустити налаштування середовища C та написати власний код, щоб зателефонувати main (). Деякі MCU можуть дозволити вам написати власний завантажувач, і тоді ви можете виконати всі налаштування низького рівня самостійно.

Різне: багато MCU дозволять вам виконувати код із оперативної пам’яті для кращої продуктивності. Зазвичай це встановлюється в конфігурації лінкера. Лінкер призначає кожній функції дві адреси - адресу завантаження , де вперше зберігається код (як правило, спалах), та адресу запуску , яка є адресою, завантаженою в ПК для виконання функції (спалах або оперативна пам'ять). Щоб виконати код з оперативної пам’яті, ви записуєте код, щоб змусити ЦП скопіювати функціональний код зі своєї завантажувальної адреси у спалах на адресу запуску в оперативній пам'яті, а потім викликати функцію за адресою запуску. Лінкер може визначити глобальні змінні, щоб допомогти у цьому. Але виконання коду з оперативної пам’яті необов’язково в MCU. Зазвичай ви це робите, лише якщо вам справді потрібна висока продуктивність або якщо ви хочете переписати спалах.


1

Ваше резюме приблизно відповідає правильній архітектурі Von Neumann . Початковий код зазвичай завантажується в оперативну пам'ять через завантажувач, але не (як правило) програмний завантажувач програмного забезпечення, до якого зазвичай відноситься термін. Це, як правило, поведінка, що «врізається в кремній». Виконання коду в цій архітектурі часто включає передбачуване кешування інструкцій з ПЗУ таким чином, щоб процесор максимізував свій час виконання коду і не чекав завантаження коду в ОЗУ. Я десь читав, що MSP430 є прикладом цієї архітектури.

У пристрої архітектури Гарвардські інструкції виконуються безпосередньо з ПЗУ, в той час як до пам'яті даних (ОЗП) можна отримати доступ через окрему шину. У цій архітектурі код просто починається виконувати з вектора скидання. PIC24 та dsPIC33 - приклади такої архітектури.

Що стосується фактичного відгортання бітів, що відбиває ці процеси, вони можуть відрізнятися від пристрою до пристрою і можуть включати налагоджувачі, JTAG, власні методи тощо.


Але ви швидко пропускаєте деякі очки. Давайте візьмемо це повільний рух. Скажімо, двійковий код "спочатку" записується на ROM. Ок .. Після цього ви пишете "Доступ до пам'яті даних" .... Але звідки вперше беруться дані "в ОЗУ" при запуску? Знов приходить з ПЗУ? І якщо так, то як завантажувач знає, яка частина ПЗУ буде записана в ОЗУ на початку?
користувач16307

Ви маєте рацію, я пропустив багато. Інші хлопці мають кращі відповіді. Я радий, що ти отримав те, що шукав.
злегка
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.