Как микроконтроллер загружается и запускается, шаг за шагом?

17

Когда код на С написан, скомпилирован и загружен в микроконтроллер, микроконтроллер начинает работать. Но если мы пошагово начнем процесс загрузки и запуска в замедленном режиме, у меня возникнут некоторые сомнения относительно того, что на самом деле происходит внутри MCU (память, процессор, загрузчик). Вот (скорее всего, неправильно), что бы я ответил, если бы кто-то спросил меня:

  1. Скомпилированный двоичный код записывается на флэш-ПЗУ (или EEPROM) через USB
  2. Загрузчик копирует некоторую часть этого кода в оперативную память. Если это правда, как загрузчик знает, что копировать (какую часть ПЗУ копировать в ОЗУ)?
  3. CPU начинает извлекать инструкции и данные кода из ПЗУ и ОЗУ

Это неправильно?

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

Я нашел много основных объяснений того, как компьютер загружается через BIOS. Но я застрял с процессом запуска микроконтроллера.

user16307
источник

Ответы:

31

1) скомпилированный бинарный файл записывается в prom / flash yes. USB, последовательный порт, i2c, jtag и т. Д. Зависит от устройства в отношении того, что поддерживается этим устройством, и не имеет отношения к пониманию процесса загрузки.

2) Как правило, это не так для микроконтроллера, основной вариант использования состоит в том, чтобы иметь инструкции в rom / flash и данные в ram. Не важно какая архитектура. для немикроконтроллера, вашего компьютера, вашего ноутбука, вашего сервера, программа копируется с энергонезависимой (диск) в оперативную память, а затем запускается оттуда. Некоторые микроконтроллеры позволяют вам использовать оперативную память, даже те, которые претендуют на гарвард, даже если кажется, что это нарушает определение. В гарварде нет ничего, что мешало бы вам отображать оперативную память в сторону инструкций, вам просто нужен механизм для получения инструкций после включения питания (что нарушает определение, но гарвардские системы должны были бы делать это, чтобы быть полезными для других). чем в качестве микроконтроллеров).

3) вроде.

Каждый процессор «загружается» детерминированным, как задумано, способом. Наиболее распространенным способом является векторная таблица, в которой адрес для первых команд, запускаемых после включения питания, находится в векторе сброса, а адрес, который считывает аппаратное обеспечение, использует этот адрес для запуска. Другой общий способ - запустить процессор без таблицы векторов по какому-либо общеизвестному адресу. Иногда чип будет иметь «ремешки», некоторые контакты, которые вы можете привязать высоко или низко перед сбросом сброса, которые логика использует для загрузки различными способами. Вы должны отделить сам процессор, ядро ​​процессора от остальной части системы. Понять, как работает процессор, а затем понять, что разработчики микросхем / систем имеют адресные декодеры настройки вокруг внешней стороны процессора, так что некоторая часть адресного пространства процессора взаимодействует со вспышкой, и некоторые с оперативной памятью, а некоторые с периферийными устройствами (uart, i2c, spi, gpio и т. д.). Вы можете взять то же ядро ​​процессора, если хотите, и обернуть его по-другому. Это то, что вы получаете, когда покупаете что-то на основе рук или мипов. arm и mips создают процессорные ядра, которые люди покупают и оборачивают вокруг, по разным причинам, которые они не делают совместимыми от бренда к бренду. Вот почему редко можно задать общий вопрос о руке, когда речь идет о чем-то за пределами ядра.

Микроконтроллер пытается быть системой на чипе, поэтому его энергонезависимая память (flash / rom), volatile (sram) и процессор находятся на одном чипе вместе со смесью периферийных устройств. Но микросхема спроектирована так, что флэш-память отображается в адресное пространство процессора, которое соответствует характеристикам загрузки этого процессора. Если, например, процессор имеет вектор сброса по адресу 0xFFFC, то должен быть flash / rom, который отвечает на этот адрес, который мы можем запрограммировать через 1), вместе с достаточным количеством flash / rom в адресном пространстве для полезных программ. Разработчик чипа может выбрать 0x1000 байт флэш-памяти, начиная с 0xF000, чтобы удовлетворить эти требования. И, возможно, они помещают некоторое количество оперативной памяти по более низкому адресу или, может быть, 0x0000, а периферия где-то посередине.

Другая архитектура cpu может начать работать с нулевого адреса, поэтому им придется сделать обратное, разместить флэш-память так, чтобы она отвечала диапазону адресов около нуля. скажем, от 0x0000 до 0x0FFF, например. а затем положите несколько баранов в другом месте.

Разработчики чипов знают, как процессор загружается, и они разместили там энергонезависимую память (flash / rom). Затем сами программисты должны написать загрузочный код, чтобы он соответствовал хорошо известному поведению этого процессора. Вы должны поместить адрес вектора сброса в вектор сброса, а ваш загрузочный код - по адресу, который вы определили в векторе сброса. Набор инструментов может вам сильно помочь здесь. иногда, особенно с помощью указателей и кликов или других песочниц, они могут сделать большую часть работы за вас, все, что вы делаете, это вызываете apis на языке высокого уровня (C).

Но, как бы то ни было, программа, загружаемая во флэш-память, должна соответствовать режиму аппаратной загрузки процессора. Перед частью C вашей программы main () и включенной функцией main в качестве точки входа необходимо выполнить некоторые действия. Программист переменного тока предполагает, что когда объявляют переменную с начальным значением, они ожидают, что это действительно сработает. Ну, переменные, отличные от const, находятся в ram, но если у вас есть переменная с начальным значением, то это начальное значение должно быть в энергонезависимом ram. Так что это сегмент .data, и загрузчик C должен скопировать материал .data из флэш-памяти в оперативную память (где это обычно определяется цепочкой инструментов). Предполагается, что глобальные переменные, которые вы объявляете без начального значения, перед запуском вашей программы равны нулю, хотя на самом деле это не следует считать, и, к счастью, некоторые компиляторы начинают предупреждать о неинициализированных переменных. Это сегмент .bss, и нули начальной загрузки C, которые в ram, содержании, нулях, не должны храниться в энергонезависимой памяти, а только начальный адрес и объем. Опять же, набор инструментов очень вам здесь помогает. И, наконец, минимум: вам нужно установить указатель стека, так как программы на Си предполагают иметь локальные переменные и вызывать другие функции. Тогда, может быть, сделаны какие-то другие специфичные для чипов вещи, или мы позволим остальным специфическим для чипов вещам происходить в C. не должен быть сохранен в энергонезависимой памяти, но начальный адрес и сколько делает. Опять же, набор инструментов очень вам здесь помогает. И, наконец, минимум: вам нужно установить указатель стека, так как программы на Си предполагают иметь локальные переменные и вызывать другие функции. Тогда, может быть, сделаны какие-то другие специфичные для чипов вещи, или мы позволим остальным специфическим для чипов вещам происходить в C. не должен быть сохранен в энергонезависимой памяти, но начальный адрес и сколько делает. Опять же, набор инструментов очень вам здесь помогает. И, наконец, минимум: вам нужно установить указатель стека, так как программы на Си предполагают иметь локальные переменные и вызывать другие функции. Тогда, может быть, сделаны какие-то другие специфичные для чипов вещи, или мы позволим остальным специфическим для чипов вещам происходить в C.

Ядра серии cortex-m от arm сделают это за вас, указатель стека находится в таблице векторов, есть вектор сброса, указывающий на код, который будет запущен после сброса, так что все остальное, что вам нужно сделать чтобы сгенерировать векторную таблицу (которую вы все равно обычно используете asm), вы можете использовать чистый C без asm. Теперь вы не копируете свои данные .dd и не обнуляете свой файл .bss, так что вы должны сделать это самостоятельно, если хотите попробовать без asm что-то на основе cortex-m. Большей особенностью является не вектор сброса, а векторы прерываний, когда аппаратное обеспечение следует рекомендациям C, принятым для охраны, и сохраняет регистры для вас, а также использует правильный возврат для этого вектора, так что вам не нужно оборачивать правильный asm вокруг каждого обработчика ( или иметь специальные директивы цепочки инструментов для вашей цели, чтобы цепочка инструментов обернула ее для вас).

Например, для микросхем можно использовать микроконтроллеры в системах с батарейным питанием, поэтому они потребляют мало энергии, поэтому некоторые выходят из состояния сброса при отключении большинства периферийных устройств, и вам необходимо включить каждую из этих подсистем, чтобы их можно было использовать , Uarts, gpios и т. Д. Часто используется низкая тактовая частота, прямо из кристалла или внутреннего генератора. И дизайн вашей системы может показать, что вам нужны более быстрые часы, поэтому вы инициализируете это. ваши часы могут быть слишком быстрыми для вспышки или оперативной памяти, поэтому вам, возможно, потребовалось изменить состояния ожидания, прежде чем поднимать часы. Может потребоваться настройка UART, USB или других интерфейсов. тогда ваше приложение может сделать свое дело.

Рабочий стол компьютера, ноутбук, сервер и микроконтроллер ничем не отличаются в том, как они загружаются / работают. За исключением того, что они в основном не на одном чипе. Программа bios часто находится на отдельной флеш-памяти / чипе от процессора. Хотя в последнее время процессоры x86 все больше и больше вытягивают то, что раньше поддерживало микросхемы, в один и тот же пакет (контроллеры pcie и т. Д.), Но у вас все еще есть большая часть оперативной памяти, но это все еще система, и она все еще работает точно то же самое на высоком уровне. Процесс загрузки процессора хорошо известен, дизайнеры платы помещают флэш-память в адресное пространство, где загружается процессор. эта программа (часть BIOS на компьютере x86) выполняет все вышеперечисленное, запускает различные периферийные устройства, инициализирует драм, перечисляет шины pcie и т. д. Часто настраивается пользователем в зависимости от настроек BIOS или от того, что мы называли настройками CMOS, потому что в то время именно эта технология использовалась. Неважно, есть пользовательские настройки, которые вы можете изменить и указать загрузочному коду BIOS, как изменить то, что он делает.

разные люди будут использовать разные термины. чип загружается, это первый код, который запускается. иногда называется начальной загрузкой. загрузчик со словом loader часто означает, что если вы ничего не делаете, чтобы помешать, это загрузчик, который избавляет вас от обычной загрузки чего-то большего, вашего приложения или операционной системы. но часть загрузчика подразумевает, что вы можете прервать процесс загрузки и затем загрузить другие тестовые программы. если вы когда-либо использовали uboot, например, во встроенной системе Linux, вы можете нажать клавишу и остановить нормальную загрузку, затем вы можете загрузить тестовое ядро ​​в ram и загрузить его вместо того, которое есть на flash, или вы можете скачать собственные программы, или вы можете загрузить новое ядро, затем загрузчик запишет его на флэш-память, чтобы при следующей загрузке он запускал новое.

Насколько сам процессор, процессор ядра, который не знает оперативной памяти от флэш-памяти от периферийных устройств. Здесь нет понятия загрузчик, операционная система, приложение. Это просто последовательность инструкций, которые вводятся в процессор для выполнения. Это программные термины, позволяющие отличать разные задачи программирования друг от друга. Концепции программного обеспечения друг от друга.

Некоторые микроконтроллеры имеют отдельный загрузчик, предоставляемый поставщиком микросхемы в отдельной флэш-памяти или отдельной области флэш-памяти, которую вы не сможете изменить. В этом случае часто есть вывод или набор выводов (я называю их ремнями), которые, если вы привязываете их высоко или низко до того, как сбросится, вы говорите логике и / или загрузчику, что делать, например, одна комбинация ремней может скажите чипу запустить этот загрузчик и подождите на Uart данные, которые будут запрограммированы во флэш-память. Установите ремешки в другую сторону, и ваша программа загружается не с загрузчика чипов, что позволяет программировать чип на месте или восстанавливаться после сбоя вашей программы. Иногда это просто чистая логика, которая позволяет программировать вспышку. Это довольно часто в наши дни,

Причина, по которой большинство микроконтроллеров имеет гораздо больше флэш-памяти, чем оперативной памяти, заключается в том, что основной вариант использования - запуск программы непосредственно с флэш-памяти, и у них достаточно оперативной памяти, чтобы охватить стек и переменные. Хотя в некоторых случаях вы можете запускать программы из оперативной памяти, которые нужно правильно скомпилировать и сохранить во флэш-памяти, а затем скопировать перед вызовом.

РЕДАКТИРОВАТЬ

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
}

Так что это пример для cortex-m0, cortex-ms все работает так же, как и в этом примере. Конкретный чип, в этом примере, имеет приложение flash по адресу 0x00000000 в адресном пространстве плеча и оперативной памяти по адресу 0x20000000.

Способ загрузки cortex-m - это 32-битное слово по адресу 0x0000 - это адрес для инициализации указателя стека. Мне не нужно много стека для этого примера, так что 0x20001000 будет достаточно, очевидно, что под этим адресом должен быть ram (способ, которым рука выталкивает, сначала вычитает, а затем толкает, поэтому, если вы установите 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, эти локальные переменные не используются, они являются мертвым кодом. Компилятор даже комментирует этот факт, говоря, что у установлен, но не используется.

Обратите внимание на адресное пространство, все эти вещи начинаются с адреса 0x0000 и идут оттуда, чтобы таблица векторов была правильно размещена, текстовые или программные пространства также были правильно размещены, как я получил flash.s перед кодом notmain.c: зная инструменты, распространенная ошибка состоит в том, чтобы не понять это правильно и разбить и сжечь сильно. IMO, вам нужно разобрать, чтобы убедиться, что все расположено прямо перед первой загрузкой, когда у вас есть вещи в нужном месте, вам не обязательно каждый раз проверять. Просто для новых проектов или если они зависнут.

Теперь кое-что, что удивляет некоторых, состоит в том, что нет никаких оснований ожидать, что любые два компилятора будут выдавать одинаковые выходные данные из одного и того же ввода. Или даже тот же компилятор с разными настройками. Используя 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. Нет причины, по которой компилятор должен оставлять x в стеке + 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 было бы ошибкой, хотя в соответствии с соглашением о вызовах для этой цели, мы не закрываем r4 при вызове функции, мы можем замкнуть с r0 по r3. r0 - возвращаемое значение. Похоже, что это делает оптимизацию хвоста, может быть,

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

Также обратите внимание, что, поскольку код flash.s стал больше, notmain - это другое, а набор инструментов позаботился о том, чтобы исправить все адреса для нас, поэтому нам не нужно делать это вручную.

неоптимизированный лязг для справки

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, даже если в фрейме стека ничего нет. в основном инструкция могла бы быть оптимизирована. но он использовал pop для возврата не трех инструкций, которые, вероятно, были на мне, я уверен, я мог бы заставить 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 равен нулю. другой - .data, потому что я инициализировал его при объявлении.

Теперь, поскольку это глобальные переменные, предполагается, что другие функции могут изменять их. компилятор не делает никаких предположений относительно того, когда 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 для меня. Иногда это экономит вспышку, иногда горит больше. Определенно, гораздо проще программировать и переносить из версии набора инструментов или одного компилятора в другой. Гораздо надежнее, меньше подвержено ошибкам. Да, не соответствует стандарту C.

что теперь, если мы сделаем эти статические глобалы?

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

Я изменил х, чтобы быть pre-init с 0x12345678. Мой скрипт компоновщика (это для GNU LD) имеет эту вещь Тед в Боб вещь. это говорит компоновщику, что я хочу, чтобы последнее место было в адресном пространстве ted, но сохраните его в двоичном виде в адресном пространстве ted, и кто-то переместит его для вас. И мы видим, что это произошло. это формат 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 и данные в ram». когда вы говорите «данные в ОЗУ здесь», вы имеете в виду данные, полученные в процессе работы программы. или вы также включаете инициализированные данные. Я имею в виду, когда мы загружаем код в ПЗУ, в нашем коде уже есть инициализированные данные. например, в нашем oode, если у нас есть: int x = 1; int y = x +1; В приведенном выше коде есть инструкции и есть начальные данные, равные 1. (x = 1). эти данные также копируются в ОЗУ или остаются только в ПЗУ.
user16307
13
ха, теперь я знаю ограничение на количество символов для ответа на стек!
old_timer
2
Вы должны написать книгу, объясняющую такие понятия новичкам. « У меня есть примеры мильона на GitHub» - Можно ли разделить несколько примеров
AkshayImmanuelD
1
Я только что сделал. Не тот, который делает что-нибудь полезное, но все же это пример кода для микроконтроллера. И я поместил ссылку на GitHub, из которой вы можете найти все, что я поделился, хорошо, плохо или иначе.
old_timer
8

Этот ответ будет больше фокусироваться на процессе загрузки. Во-первых, исправление - запись во флэш-память выполняется после того, как MCU (или хотя бы его часть) уже запущен. На некоторых MCU (обычно более продвинутых) сам CPU может управлять последовательными портами и записывать во флэш-регистры. Таким образом, написание и выполнение программы - это разные процессы. Я собираюсь предположить, что программа уже была написана для прошивки.

Вот основной процесс загрузки. Я назову некоторые общие варианты, но в основном я придерживаюсь этого простого

  1. Сброс: Есть два основных типа. Первый - это сброс при включении питания, который генерируется изнутри, когда напряжение питания увеличивается. Второй - это внешний контактный переключатель. В любом случае, сброс приводит все триггеры в MCU к предопределенному состоянию.

  2. Дополнительная аппаратная инициализация: может потребоваться дополнительное время и / или тактовые циклы, прежде чем процессор начнет работать. Например, в микроконтроллерах TI, над которыми я работаю, загружается внутренняя цепочка сканирования конфигурации.

  3. Загрузка CPU: CPU получает свою первую инструкцию со специального адреса, называемого вектором сброса.Этот адрес определяется при проектировании ЦП. Оттуда это просто нормальное выполнение программы.

    Процессор повторяет три основных шага снова и снова:

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

    (Это на самом деле сложнее, чем это. Процессоры обычно конвейерны , что означает, что они могут выполнять каждый из вышеперечисленных шагов для разных инструкций одновременно. Каждый из вышеперечисленных шагов может иметь несколько этапов конвейера. Затем есть параллельные конвейеры, прогноз ветвления и все причудливые вещи компьютерной архитектуры, которые заставляют эти процессоры Intel разрабатывать миллиард транзисторов.)

    Вам может быть интересно, как работает выборка. Процессор имеет шину, состоящую из сигналов адреса (выход) и данных (вход / выход). Чтобы получить выборку, ЦПУ устанавливает свои адресные строки в значение в счетчике программы, а затем отправляет часы по шине. Адрес декодируется, чтобы включить память. Память получает часы и адрес и помещает значение по этому адресу в строки данных. Процессор получает это значение. Чтение и запись данных аналогичны, за исключением того, что адрес исходит из инструкции или значения в регистре общего назначения, а не с ПК.

    Процессоры с архитектурой фон Неймана имеют единую шину, которая используется как для инструкций, так и для данных. Процессоры с гарвардской архитектурой имеют одну шину для инструкций и одну для данных. В реальных микроконтроллерах обе эти шины могут быть подключены к одной и той же памяти, поэтому часто (но не всегда) вам не о чем беспокоиться.

    Вернуться к процессу загрузки. После сброса на ПК загружается начальное значение, называемое вектором сброса. Это может быть встроено в аппаратное обеспечение или (в процессорах ARM Cortex-M) оно может автоматически считываться из памяти. Процессор выбирает инструкцию из вектора сброса и начинает цикл по шагам выше. В этот момент процессор работает нормально.

  4. Загрузчик: часто необходимо выполнить низкоуровневую настройку, чтобы остальная часть MCU работала. Это может включать в себя такие вещи, как очистка ОЗУ и загрузка производственных параметров настройки для аналоговых компонентов. Также может быть опция загрузки кода из внешнего источника, такого как последовательный порт или внешняя память. MCU может включать в себя загрузочное ПЗУ, которое содержит небольшую программу для выполнения этих задач. В этом случае вектор сброса ЦП указывает на адресное пространство загрузочного ПЗУ. Это в основном нормальный код, он предоставляется производителем, поэтому вам не нужно писать его самостоятельно. :-) В ПК BIOS является эквивалентом загрузочного ПЗУ.

  5. Настройка среды C: C предполагает наличие стека (области ОЗУ для хранения состояния во время вызовов функций) и инициализированных областей памяти для глобальных переменных. Это разделы .stack, .data и .bss, о которых говорит Двелч. На этом этапе инициализированные глобальные переменные копируют значения инициализации из флэш-памяти в ОЗУ. Неинициализированные глобальные переменные имеют адреса ОЗУ, которые расположены близко друг к другу, поэтому весь блок памяти можно очень легко инициализировать на ноль. Стек не нужно инициализировать (хотя это может быть) - все, что вам действительно нужно сделать, это установить регистр указателя стека процессора, чтобы он указывал на назначенную область в ОЗУ.

  6. Основная функция : после настройки среды C загрузчик C вызывает функцию main (). Вот где обычно начинается код вашего приложения. При желании вы можете опустить стандартную библиотеку, пропустить настройку среды C и написать собственный код для вызова main (). Некоторые микроконтроллеры могут позволить вам написать свой собственный загрузчик, а затем вы можете выполнить все низкоуровневые настройки самостоятельно.

Разное: многие микроконтроллеры позволяют выполнять код из оперативной памяти для повышения производительности. Обычно это настраивается в конфигурации компоновщика. Компоновщик назначает два адреса каждой функции - адрес загрузки , где сначала хранится код (обычно флэш-память), и адрес запуска , который является адресом, загруженным в ПК для выполнения функции (флэш-память или ОЗУ). Чтобы выполнить код из ОЗУ, вы пишете код, чтобы заставить ЦП скопировать код функции со своего адреса загрузки во флэш-памяти на свой адрес выполнения в ОЗУ, а затем вызвать функцию по адресу выполнения. Линкер может определить глобальные переменные, чтобы помочь с этим. Но выполнение кода из ОЗУ в MCU не является обязательным. Обычно вы делаете это только в том случае, если вам действительно нужна высокая производительность или если вы хотите переписать флэш-память.

Адам Хаун
источник
1

Ваше резюме примерно соответствует архитектуре фон Неймана . Исходный код обычно загружается в ОЗУ через загрузчик, но не (как правило) загрузчик программного обеспечения, к которому этот термин обычно относится. Это обычно поведение «запекания в кремнии». Выполнение кода в этой архитектуре часто включает в себя прогнозное кеширование команд из ПЗУ таким образом, что процессор максимизирует время выполнения кода и не ожидает загрузки кода в ОЗУ. Я где-то читал, что MSP430 является примером этой архитектуры.

В Гарвардской архитектуры инструкции выполняются непосредственно из ПЗУ, а доступ к памяти данных (ОЗУ) осуществляется через отдельную шину. В этой архитектуре код просто начинает выполняться из вектора сброса. PIC24 и dsPIC33 являются примерами этой архитектуры.

Что касается фактического переключения битов, которое запускает эти процессы, которое может варьироваться от устройства к устройству и может включать отладчики, JTAG, проприетарные методы и т. Д.

slightlynybbled
источник
Но вы пропускаете некоторые очки быстро. Давайте возьмем это медленное движение. Допустим, двоичный код «первый» записывается в ПЗУ. Хорошо ... После этого вы пишете "Доступ к памяти данных" .... Но откуда данные "в ОЗУ" впервые поступают при запуске? Это снова приходит из ROM? И если да, то как загрузчик знает, какая часть ПЗУ будет записана в ОЗУ в начале?
user16307
Вы правы, я много пропустил. У других парней есть лучшие ответы. Я рад, что вы получили то, что искали.
чуть-чуть