1i7

Лабораторная работа 6: знакомство с промышленной реализацией архитектуры MIPS на примере pic32 (2)

Jul 28, 2013 00:53

Продолжение, начало Лабораторная работа 6: знакомство с промышленной реализацией архитектуры MIPS на примере pic32 (1) <<

Структура памяти

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

Большинство особенностей подобного рода лежит за пределами спецификаций архитектуры MIPS. Какие конкретно это будут устройства, интерфейсы и подсистемы и каким образом они будут связаны между собой определяет каждый производитель системы на кристалле самостоятельно (мы в своей реализации системы с процессором сделали по-своему, Microchip в своем pic32 - по-своему). А т.к. из прошлых занятий мы уже давно узнали, что основной способ взаимодействия ядра процессора с внешним миром - это шина инструкций и шина данных, т.е. внешняя память, то вполне логично, что многие особенности конкретного чипа в полной мере отразятся на том, каким образом эта самая память организована.

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

Как делали мы

Итак, вспомним, каким образом выглядела память в финальной реализации нашего процессора со всей обвязкой, необходимой для тестового запуска на ПЛИС:




На схеме видим следующее:
1. Вся доступная память разделена на два независимых адресных пространства - Память инструкций (Instruction memory) и Память данных (Data memory).
2. Каждое адресное пространство может теоретически содержать максимум по 4 Гб данных (32бит на адрес) - это виртуальная память.
3. Реально доступной физической памяти гораздо меньше, при этом она разбита на несколько блоков, которые раскиданы по разным виртуальным адресам.
4. Адреса для блоков физической памяти назначаются не случайно - для каждого адреса назначена специальная роль, заранее определенная в спецификации на контроллер.

Блоки на картинке:
1. ROM (Prog) - Read Only Memory - блок памяти, доступной только для чтения, содержащий код выполняемой процессором программы. Как помним, этот блок извне вообще неизменяем, сохраняет все содержимое при отключении питания схемы, прошивается в ПЛИС вместе с остальными частями аппаратного дизайна.
2. Он же - блок BOOT - загрузчик - блок памяти, содержащий код, который процессор начинает выполнять в момент подачи питания. В нашем максимально упрощенном случае от загрузчика никаких специальных действий не требуется, поэтому он просто совпадает с основным кодом программы.
3. RAM (Data) - Random Access Memory - блок памяти для произвольного чтения и записи данных. Оперативная память для данных - содержимое сбрасывается при отключении питания схемы.
4. I/O Devices - устройства ввода-вывода. Блок памяти с подключенными устройствами ввода-вывода. Хотя памятью его можно называть совсем условно, т.к. подключенные устройства могут вообще говоря ничего не запоминать, программный интерфейс позволяет скрыть подобные различия.

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

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

Итого плохого: память инструкций не пригодна ни для чего, кроме одноразового запуска тестового приложения - у нас нет возможности загрузить на выполнение произвольную внешнюю программу или даже обновить текущую без перепрошивки ПЛИС вместе со всем процессором - таким образом в реальном мире дела не делаются.

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

Как реализовано в PIC32

Для pic32 схема организации памяти выглядит следующим образом (для пространства физических адресов):




На картинке видим следующее:
1. Память инструкций и память данных объеденены в рамках единого адресного пространства - здесь изображена вся доступная память.
2. Память также разбита на блоки, отличающиеся как ролью, так и физическим воплощением.

Блоки такие:
1. RAM - оперативная память для чтения и записи. Может содержать как код, так и данные. Не сохраняет данные между отключениями питания.
2. FLASH - постоянная память для чтения и записи. Сохраняет данные между отключениями питания.
3. BOOT - загрузчик. Содержит код (и вероятно данные), исполняемый в момент загрузки контроллера. Сохраняет данные между отключениями питания.
4. SFR - Special Function Register - набор регистров для управления специальными устройствами - в первую очередь для нас будут интересны пины ввода-вывода (они же ножки процессора, они же порты ввода-вывода).

В общем, в этот момент все могло бы быть уже достаточно хорошо:
- Подаем питание на устройство, процессор запускается и начинает выполнять код, записанный в блок с загрузчиком BOOT.
- Код загрузчика выполняет необходимые инициализации типа настройки оборудования и аппаратных интерфейсов.
- После этого загрузчик передает управление коду прошивки (например ядру операционной системы или более простому пользовательскому приложению) в FLASH, который был прошит туда предварительно при помощи специального программатора.
- Операционная система или более простая пользовательская программа запущена - дальше она будет действовать по своему усмотрению - например начнет делать какие-то полезные математические вычичления используя раздел RAM для хранения переменных и промежуточных результатов и производить разные манипуляции с внешними устройствами, осуществляя чтение и запись по адресам из области SFR.

Замечание: память FLASH обычно работает медленнее (на pic32 примерно в 3-4 раза) памяти RAM, однако этой скорости достаточно для того, чтобы процессор мог выполнять код прямо с нее. По этой причине загрузчик имеет возможность передавать управление туда напрямую. В другой ситуации с более медленной FLASH-памятью (или например если бы у нас вместо FLASH-памяти был жесткий диск или какая-нибудь sd-карта), когда исполнение кода прошивки напрямую было бы невозможно, загрузчик мог бы сначала скопировать необходимый исполняемый код в RAM и уже после этого передавать управление в RAM же (на больших десктопах с большими жесткими дисками очевидно используется именно такой подход, на маленькой FLASH-памяти лежит только BIOS). Исполняемый код также можно копировать самостоятельно из собственного приложения в специальную область RAM и передавать на него управление, если для этого есть достаточно веские причины - например для организации работы динамически подгружаемых библиотек или при необходимости осуществить запись обратно в FLASH-память (в момент записи FLASH на некоторое время перестанет быть доступна на чтение).

В общем, как видим, проблема с доставкой и обновлением полезного исполняемого кода в зону доступа процессора решена - программатор просто прошивает его в память FLASH, которая видна процессору в адресном пространстве инструкций.

Однако разработчики PIC32 пошли еще дальше и при помощи дополнительных механизмов организации памяти решили такие проблемы, с которыми мы даже не сталкивались.

На самом деле пользовательские приложения и операционная система в процессе работы имеют дело не с физическими адресами блоков, которые указаны выше, а с пространством виртуальных адресов, которые делают карту памяти чуть более разнообразной:




Как видим, у нас все еще один длинный блок памяти для кода и данных, но он здесь уже разделен на две части:
- Kernel space - пространство адресов для ядра (приложения уровня операционной системы)
- User space - пространство адресов пользовательских приложений, запущенных внутри операционной системы.

Замечание: Под пользовательскими приложениями из User Space в данном случае подразумеваются приложения, запущенные внутри операционной системы самой операционной системой, которая сама была предварительно загружена в режиме Kernel Space при помощи загрузчика. Механизмы контроллера pic32, связанные напрямую с механизмом управления памятью, позволяют различать эти два режима исполнения кода на аппаратном уровне и переключать их в процессе выполнения (включать или не включать этот режим для определенных участков кода должна сама операционная система). Код, запущенный в режиме User, будет иметь доступ только к пространству адресов из User Space - при попытке обращения к адресам из Kernel Space будет сгенерировано немедленное исключение (ошибка) на аппаратном уровне. Т.е. "пользовательские" приложения работают в специальном защищенном режиме и им не позволяется обращаться напрямую с загручиком, прошивкой операционной системы, специальными регистрами и т.п. Приложение уровня Kernel имеет доступ ко всему пространству адресов без ограничений. Однако в значительном числе применений встраевымые приложения не подразумевают динамическую загрузку стороннего кода, для которого потребуется специальный защищенный режим. Даже многие операционные системы реального времени (RTOS) не поддерживают механизмы динамической загрузки кода за ненадобностью, а пользовательские приложения объединяются вместе с ядром операционной системы в одну монолитную прошивку еще на этапе компиляции. Можно назвать разные причины целесообразности подобного подхода: микроконтроллеры часто обладают ограниченными ресурсами, поэтому их не желательно тратить на динамическую загрузку, код прошивки для встраиваемых систем обычно создается под специализированную задачу, хорошо тестируется на этапе разработки и не подразумевает дальнейшей подгрузки сторонних модулей, от которых имеет смысл защищаться, и т.п. С другой стороны, подобный подход перестает быть эффективным при необходимости применения более сложных операционных систем типа ядра Linux или семейства ядер BSD, которые обычно запускаются на более производительных устройствах, обладают высокой степенью универсальности во время выполнения и используют механизмы защиты памяти в полной мере. Так или иначе, в данный момент первого знакомства с голым железом контроллера pic32 мы не будем использовать даже простые RTOS - наши прошивки из нескольких строк элементарного кода будут прошиваться с компьютера в FLASH, грузиться загрузчиком и работать в режиме Kernel, т.е. будут сами себе являться операционной системой и пользовательским приложением в одном лице. По этой причине режим User Space еще более подробно по крайней мере сейчас рассматриваться не будет.

На картинке выше представлены блоки с адресами, доступными для приложений уровня операционной системы (Kernel Space):
1. RAM теперь разделена на два блока, границу которого можно также изменять при помощи специального настроечного регистра BMXDKPBA (часть механизма управления памятью bus matrix BMX, про который чуть ниже, но без лишних подробностей):
1.1. RAM (Data) - блок оперативной памяти для данных.
1.2. RAM (Prog) - блок оперативной памяти для исполняемого кода.
2. FLASH - все та же постоянная память (в действительности она разделена еще на два блока Cached и Uncached, но здесь подробно останавливаться на этом не будем).
3. BOOT - все тот же загрузчик (обратить внимание на новый адрес: 0xBFC00000 - о нем ниже).
4. SFR - все тот же Special Function Register для работы с пинами ввода-вывода и другими специальными устройствами.

Пространство User Space слева от Kernel Space выглядит примерно также, только еще проще - там находятся блоки User RAM (Data), User RAM (Prog) и User FLASH, блоки BOOT и SFR недоступны. Специальной картинки не будет, чтобы не отвлекать внимание - при желании за дополнительными подробностями можно обратиться к документации или к рекомендованной ранее книге Programming 32-bit Microcontrollers in C. Exploring the PIC32. Lucio Di Jasio на 134й странице.

Итак, мы видим, что механизмы управления памятью в pic32 достаточно сложны и функциональны (по крайней мере по сравнению с тем, что мы успели набросать на предыдущей лабе). Управление адресным пространством - перевод виртуальных адресов в физические и распределение блоков памяти по шине - в pic32 осуществляет специальный модуль с именем FMT (fixed mapping translation - дословно перевод фиксированных проекций). Разбиением физических блоков памяти на дополнительные блоки и всеми вариантами контроля доступа к памяти (разделение RAM на данные и инструкции, контроль для режимов User и Kernel, а также другие вещи, которых мы вообще не касались типа механизмов прямого доступа к памяти DMA и внутрисхемного отладчика In-Circuit Debugger ICD) - заведует модуль BMX (bus matrix - дословно матрица шины). В сумме BMX и FMT можно назвать модулем контроллера памяти.

Замечание: Кстати говоря, в pic32 для процессора память данных и память инструкций - это все-таки тоже две разных шины, как было и у нас, но проецирование большинства блоков памяти (FLASH, BOOT, RAM) на одинаковые адреса на обе шины одновременно позволяет считать, что они фактически совпадают и рассматривать их на одной схеме. Однако есть и нюансы, которые вскрывают истинную сущность происходящего - например блок памяти SFR недоступен на шине инструкций, т.е. на него нельзя передать управление для исполнения кода, хотя данные можно читать и записывать.

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

На этом вводный обзор структуры памяти и механизмов управления можно завершить. За более полной информацией по организации памяти в контроллере pic32 можно обратиться к технической документации микроконтроллера pic32 - глава "4.0 Memory Organization". Теперь добавлю еще немного интересных подробностей про каждый из блоков и можно будет переходить к практике.

Блок памяти RAM

RAM - Random Access Memory. Здесь все понятно - быстрая оперативная память, в которой размещаются данные и при необходимости код программы во время выполнения. Как уже было сказано выше, разделение физического блока RAM на виртуальные блоки с исполняемым кодом (Prog) и данными (Data) осуществляется контроллером памяти при помощи механизмов модуля BMX. Память RAM не сохраняет записанные в нее значение при отключении питания, поэтому для доставки первичного исполняемого кода к процессору при загрузке системы используются другие специальные блоки памяти FLASH и BOOT.

Блок памяти FLASH

FLASH - постоянная память, сохраняет записанные в нее значения между перезагрузками системы. Работает медленнее, чем RAM, однако процессор имеет возможность выполнять код, записанный в FLASH, напрямую. FLASH хранит прошивку, пока устройство выключено, во время старта системы загрузчик передает управление на нее сразу после того, как сделает все свои необходимые инициализационные действия.

Замечание: для ускорения работы FLASH в pic32 также существует специальный механизм, который называется Prefetch Cache. Он выполняет опережающее чтение инструкций из FLASH и поставляет их в процессор в максимальном темпе так, что скорость выполнения кода из FLASH фактически сравнивается со скоростью выполнения кода из RAM. С деталями работы механизма Prefetch Cache и способом его включения можно изучить в технической документации на pic32 в главе "4.0 Memory Organization".

Прошивка для FLASH представляет собой тот самый двоичный машинный код, который мы тренировались генерировать вручную из программы, написанной на ассемблере MIPS. В реальной жизни его конечно же генерирует специальный компилятор типа gcc или собственного компилятора Microchip наприрмер из кода на Си или на том же ассемблере MIPS. Компилятор записывает сгенерированный машинный код в специальный файл прошивки (hex-файл, который мы уже видели и генерировали сами), а файл прошивки отправляется с компьютера на FLASH память контроллера например через USB-шнурок при помощи специального программного или аппаратного программатора.

Блок памяти BOOT

BOOT - системный загрузчик. Содержит код, который процессор начинает выполнять в момент старта, т.е. при подаче питания на схему.

Следуя спецификации архитектуры MIPS32, первая инструкция загрузчика находится по адресу 0xBFC00000.

Замечание: видим очередное приятное совпадение по этому поводу в двух разных книгах, на которые мы использовали в разных лабах:

В Harris & Harris на полях страницы 365 (там же, где описывается структура простого однотактового процессора) видим замечание о том, что все процессоры MIPS начинают свою инициализацию с адреса 0xBFC00000:




Картинка с изображением адресного пространства виртуальной памяти pic32 на 135й странице Programming 32-bit Microcontrollers in C (которую я уже перерисовал выше) подтверждает то, что в этом пункте реализация процессора pic32 следует спецификациям MIPS - блок BOOT расположен по адресу 0xBFC00000, т.е. там, где нужно (не путать с картинкой с физическими адресами блоков - там блок BOOT находится по адресу 0x1FC00000, но процессор с этим адресом дела не имеет, т.к. модуль FMT решает вопрос соответствия спецификациям как нужно):




В реализации нашего процессора мы, следуя совету из Harris & Harris, для простоты решили от этой спецификации немного отойти и запускать процессор с адреса 0x00000000, но мы с самого начала договорились простить себе это маленькое отступление, помня о том, что его нужно будет исправить тому, кто соберется делать полноценную реализацию процессора с полной совместимостью.

Как уже было сказано выше, главная задача загрузчика в конечном итоге - передать управление коду прошивки в FLASH, чтобы она принялась делать что-то действительно полезное.

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

Замечание: Загрузчики бывают разные - некоторые нужно искать на сайте производителя конкретной платы, внутри которой используется микроконтроллер pic32, а какие-то судя по всему универсальные загрузчики можно найти на сайте производителя pic32 microchip.com (без привязки к особенностям платы - просто для голого pic32). Более того, даже для одной и той же модели платы могут существовать разные версии загрузчиков, которые в значительной степени влияют на их работу - например для нашей платы ChipKIT Uno32 существует как минимум 3 разных загрузчика - один позволяет работать плате в режиме совместимости с платформой Arduino, другой позволяет работать с платой при помощи программатора PICkit 3 из среды MPLAB X, третий позволяет шить плату при помощи того же программатора PICkit 3 из специальной программы PICkit 3 Programmer, который в свою очередь сам обладает набором некоторых полезных и уникальных фукнций. При этом все три режима работы несовместимы между собой и чтобы переключаться между ними, каждый раз приходится перепрошивать новый загрузчик. В общем главное, что нужно запомнить - к выбору загрузчика нужно подходить обстоятельно и изучать всю цепочку инструментов от и до заранее, т.к. на любом шаге можно ожидать любого подвоха (например шили себе спокойно плату из MPLAB X под линуксом, а тут раз - потребовалось нечто специфическое через PICkit 3 Programmer сделать, а он доступен долько для Windows и такое на каждом шагу).

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

Продолжение Лабораторная работа 6: знакомство с промышленной реализацией архитектуры MIPS на примере pic32 (3) >>

цифровая электроника для программистов, лаба 6, microchip, pic32, mips

Previous post Next post
Up