Разработка инициализирующего кода в составе загрузочного образа
В этой статье:
Первая программа, которая находится в загрузочном образе ЗОСРВ «Нейтрино», называется модулем startup-* и выполняет следующие задачи:
Изменяя модуль startup-*, можно адаптировать ЗОСРВ «Нейтрино» к различным аппаратным платформам встраиваемых систем.
На этом этапе инициализируются основные аппаратные компоненты. Содержание инициализации зависит от действий, которые были выполнены начальным загрузчиком IPL.
В модуле startup-* не требуется инициализировать стандартные периферийные устройства (такие как интерфейс IDE) или указывать скорость работы последовательных портов) — это делают соответствующие драйверы после запуска операционной системы.
Собранные сведения о системе помещаются в структуру данных, которая находится в оперативной памяти и называется системной страницей. Системная страница содержит данные о типах процессора и шины, а также размере и местоположении ОЗУ системы.
Ядро и приложения могут считывать эту информацию, а аппаратно-зависимый код, который запрашивает ее у конкретной платформы, находится в модуле startup-*. После выполнения этот код удаляется из оперативной памяти системы.
Еще одной важной задачей модуля startup-* является регистрация callout-ов в системной странице. С помощью этих вызовов ядро выполняет различные функции, которые специфичны для аппаратной платформы и реализуются системным интегратором.
В состав ЗОСРВ «Нейтрино», а также в соответствующие BSP, входят модули startup-* для различных плат; их спектр расширяется с каждой версией. Чтобы узнать, какие платы поддерживаются на текущий момент, обратитесь к следующим источникам:
boards
каталога bsp/src/hardware/startup
Каждый модуль startup-* предоставляется как в виде исполняемого файла, так и в виде исходного кода, который пригодны для адаптации и модификации. Файлы хранятся в следующей структуре каталогов:
bsp/src/hardware/ |--> ipl/ |--> startup/ | |--> boards/ | | |--> xzynq | | |--> rockchip/ | | |--> imx6x/ | | |--> imx8x/ | | |--> orangepi/ | | |--> p3041/ | | |--> p5040/ | | `--> ... | | | |--> bootfile/ | | |--> aarch64le | | |--> armv7le | | |--> ppcbe | | `--> x86 | | | `--> lib/ | `--> flash/
Обычно в этой структуре находится исходный код модуля startup-* startup-имя_платы:
bsp/src/hardware/startup/boards/имя_платы
Каждый модуль startup-* состоит из функции main() со следующей структурой (в виде псевдокода):
Глобальные переменныеmain(){Вызов add_callout_array()Анализ аргументов (вызов handle_common_option())Вызов init_raminfo()Освобождение памяти, которую использовали модули, входящие в состав образаесли (virtual) инициализация MMU с помощью init_mmu()Вызов init_intrinfo()Вызов init_qtime()Вызов init_cacheattr()Вызов init_cpuinfo()Присвоение имени аппаратной платформе системыВызов init_system_private()Вызов print_syspage() для вывода отладочной информации}
![]() | Следует изучить закомментированный исходный код каждой библиотечной функции и определить, требуется ли заменять ее на собственную. |
Чтобы разработать новый модуль startup-*, следует создать новый подкаталог в каталоге bsp/src/hardware/startup/boards
и скопировать в него файлы одной из существующих модулей startup-*. Например, чтобы создать startup-* для платы my_new_board, которая схожа с платой Intel PXA250TMDP, выполните следующие команды:
cd bsp/src/hardware/startup/lib mkdir my_new_board cp -r pxa250tmdp/* my_new_board cd my_new_board make clean
Описания всех функций запуска см. в параграфе "Библиотека libstartup" далее.
Как было отмечено ранее (см. параграф "Инициализация системной страницы"), одной из главных задач модуля startup-* является инициализация системной страницы.
Структура системной страницы struct syspage_entry определена в заголовочном файле <sys/syspage.h>
. Она содержит ряд констант, ссылок на другие структуры и объединение, которое относится к конкретной процессорной архитектуре, на которой исполняется ЗОСРВ «Нейтрино».
Важно понимать, что существуют два метода доступа к данным системной страницы. Один из них используется при загрузке системы для добавления данных в системную страницу, а другой — после загрузки системы для считывания данных системной страницы, в том числе прикладными программами. В обоих методах доступа используются одни и те же поля.
Структура системной страницы содержит как минимум следующие поля:
#include <sys/syspage.h>struct syspage_entry {uint16_t size; /* Размер структуры */uint16_t total_size; /* Размер системной страницы */uint16_t type;uint16_t num_cpu;syspage_entry_info system_private;syspage_entry_info asinfo;syspage_entry_info meminfo;syspage_entry_info hwinfo;syspage_entry_info cpuinfo;syspage_entry_info cacheattr;syspage_entry_info qtime;syspage_entry_info callout;syspage_entry_info callin;syspage_entry_info typed_strings;syspage_entry_info strings;syspage_entry_info intrinfo;syspage_entry_info smp;syspage_entry_info pminfo;syspage_entry_info mdriver;union {struct x86_syspage_entry x86;struct ppc_syspage_entry ppc;struct mips_syspage_entry mips;struct arm_syspage_entry arm;struct e2k_syspage_entry e2k;struct sparc_syspage_entry sparc;struct aarch64_syspage_entry aarch64;} un;};
Следует иметь в виду, что одни из вышеперечисленных полей можно инициализировать с помощью кода библиотеки libstartup, а другие — лишь при разработке собственного модуля startup-*. Трудоемкость инициализации зависит от количества необходимых модификаций.
Детализация полей структуры представлена на странице struct syspage_entry.
Раздел asinfo содержит деревья адресных пространств, которые определяют местоположение различных компонентов системы (ОЗУ, ПЗУ, флеш-памяти и др.). Как правило, иерархия адресных пространств имеет следующий вид:
/memory/класс_памяти/.... /io/класс_памяти/.... /memory/io/класс_памяти/....
Компоненты memory и io определяют тип адресного пространства — памяти или ввода/вывода (последняя форма применяется в системах, где отсутствуют инструкции in*() / out() и все устройства отображены в память). Поле класс_памяти содержит значения ram, rom, flash и др. На более низких уровнях вводится дополнительная классификация, с помощью которой менеджер процессов реализует поддержку типизированной памяти.
В области hwinfo указываются сведения об аппаратной платформе (тип шины, устройства, линии запроса прерываний и др.) Эта область заполняется функцией init_system_private() системной библиотеки libstartup и является одним из самых сложных компонентов системной страницы. Раздел hwinfo представляет собой не одну структуру или массив определенного типа, а последовательность структур, которые отмечены символьными тегами и в совокупности описывают компоненты аппаратной платформы встраиваемой системы. Все указанные ниже типы и константы определены в заголовочном файле <hw/sysinfo.h>.
![]() | В разделе hwinfo не обязательно описывать все аппаратные компоненты. Например, модуль startup-* может не опрашивать шину PCI, если ей не требуется обнаруживать подключенные к ней устройства. Разработчик самостоятельно определяет, какие сведения размещать в разделе hwinfo. Как правило, в hwinfo описываются несъемные компоненты аппаратной платформы. |
Каждая структура (или тег) раздела начинается со структуры struct hwi_prefix. В поле size указывается размер структуры в количестве 4-байтовых блоков (с учетом struct hwi_prefix). Поле name представляет собой смещение относительно начала раздела strings системной страницы и содержит строку с именем структуры с нулевым символом в конце.
Может показаться, что использовать ASCII-строку вместо перечисляемого типа в качестве идентификатора структуры нерационально, однако это не так. Поскольку память для системной страницы обычно выделяется блоками по 4 Кбайт, дополнительная область для хранения строк не требует каких-либо накладных расходов. Кроме того, разработчики могут самостоятельно добавлять в этот раздел новые структуры, не обращаясь в компанию-разработчика для выдачи порядковых номеров типов. При обработке этого раздела следует игнорировать любые неизвестные теги, пропуская их с помощью поля size.
Каждое устройство описывается последовательностью тегов. Совокупность тегов образует элемент, который соответствует одному устройству. Первый тег каждого элемента всегда начинается со следующей структуры (обратите внимание, что ее первым элементом является структура struct hwi_prefix). Элемент определяется структурой struct hwi_item.
![]() | Текущий код требует, чтобы имя тега любой структуры элемента начиналось с заглавной буквы, а имена тегов, которые не относятся к элементам — со строчной буквы. |
Раздел hwinfo содержит деревья, которые описывают различные устройства платы. Иерархия устройств обычно имеет следующий вид:
/hw/шина/класс_устройства/устройство
где:
Для добавления информации в раздел hwinfo используются две основных функции библиотеки libstartup:
void * hwi_alloc_tag( const char *name, unsigned size, unsigned align );
Этот вызов создает тег размера size с именем name. Если структура содержит хотя бы одно 64-разрядное целочисленное поле, следует присвоить полю align значение 8
, в противном случае — значение 4
. Эта функция возвращает указатель на память, в которую можно помещать необходимую информацию. Обратите внимание, что поля struct hwi_prefix автоматически заполняются функцией hwi_alloc_tag().
void * hwi_alloc_item( const char *name, unsigned size, unsigned align,const char *itemname, unsigned owner );
Этот вызов создает структуру item. Первые три его параметра совпадают с параметрами функции hwi_alloc_tag().
Параметры itemname и owner задают значения полей itemname и owner структуры struct hwi_item. Все вызовы hwi_alloc_tag(), которые выполняются после вызова hwi_alloc_item(), относятся к созданному элементу, и поле itemsize корректируется соответствующим образом.
Чтобы создать элемент, выполните следующие действия:
HWI_NULL_OFF
). Можно создавать элементы в любом порядке; достаточно создавать родительский элемент раньше, чем дочерний.
Предположим, что при создании дочернего элемента вы сохранили его владельца в переменной, или вам известно только имя его элемента. Чтобы определить корректное значение параметра owner, можно использовать следующую функцию, которая определена в системной библиотеке, поскольку полезна при обработке раздела:
unsigned hwi_find_item( unsigned start, ... );
Параметр start указывает начальную позицию для поиска заданного элемента. При первом вызове функции следует задать его равным HWI_NULL_OFF
. Если обнаруженный элемент не является искомым, значение, которое возвращено первым вызовом hwi_find_item(), используется в качестве параметра start второго вызова. Последующий поиск начинается с позиции, на которой завершился предыдущий. Можно выполнять поиск любое количество раз (передавая третьему вызову значение параметра start, возвращенное вторым вызовом, и т.д.). Искомый элемент идентифицируется последовательностью параметров char *
, которые следуют за параметром start и завершаются NULL
. Последняя строка перед NULL
является именем самого нижнего itemname, участвующего в поиске; строка, предшествующая этой строке — именем элемента, который является владельцем самого нижнего элемента, и т.д.
Например, следующий вызов ищет первый элемент с именем "foobar":
item_off = hwi_find_item( HWI_NULL_OFF, "foobar", NULL );
Следующий вызов ищет первый элемент с именем "foobar", который принадлежит элементу "sam":
item_off = hwi_find_item( HWI_NULL_OFF, "sam", "foobar", NULL );
При отсутствии этих элементов функция возвращает значение HWI_NULL_OFF
.
Следующие функции определены в системной библиотеке и предназначены для обработки раздела hwinfo:
unsigned hwi_tag2off( void * );
Эта функция принимает указатель на начало тега и возвращает смещение в байтах относительно начала раздела hwinfo.
void *hwi_off2tag( unsigned );
Эта функция принимает смещение в байтах относительно начала раздела hwinfo и возвращает указатель на начало тега.
unsigned hwi_find_tag( unsigned start, int curr_item, const char *tagname );
Эта функция выполняет поиск тега с именем tagname. Параметр start действует так же, как в функции hwi_find_item(). Если параметр curr_item не равен нулю, поиск прекращается в конце текущего элемента независимо от элемента, на который указывает параметр start. Если параметр curr_item равен нулю, поиск выполняется до конца раздела. При отсутствии искомого тега функция возвращает значение HWI_NULL_OFF
.
Перед вызовом функции main() в модуле startup-* библиотека добавляет ряд исходных записей, которые являются основой для последующих элементов.
В заголовочном файле <startup.h>
определен макрос HWI_TAG_INFO()
, который раскрывается в три параметра name, size и align функций hwi_alloc_tag() и hwi_alloc_item() с помощью нескольких удобных имен.
void hwi_default(){hwi_tag *tag;hwi_tag *tag;hwi_alloc_item( HWI_TAG_INFO( group ), HWI_ITEM_ROOT_AS, HWI_NULL_OFF);tag = hwi_alloc_item( HWI_TAG_INFO( group ), HWI_ITEM_ROOT_HW, HWI_NULL_OFF );hwi_alloc_item( HWI_TAG_INFO( bus ), HWI_ITEM_BUS_UNKNOWN, hwi_tag2off( tag ) );loc = hwi_find_item( HWI_NULL_OFF, HWI_ITEM_ROOT_AS, NULL );tag = hwi_alloc_item( HWI_TAG_INFO( addrspace ), HWI_ITEM_AS_MEMORY, loc );tag->addrspace.base = 0;tag->addrspace.len = (uint64_t)1 << 32;#ifndef __X86__loc = hwi_tag2off( tag );#endiftag = hwi_alloc_item( HWI_TAG_INFO( addrspace ), HWI_ITEM_AS_IO, loc );tag->addrspace.base = 0;#ifdef __X86__tag->addrspace.len = (uint64_t)1 << 16;#elsetag->addrspace.len = (uint64_t)1 << 32;#endif}
Файл <hw/sysinfo.h>
содержит ряд предопределенных элементов и тегов. Разработчик также может создавать дополнительные элементы для собственных нужд. Все предопределенные элементы и теги имеют имена вида HWI_TAG_NAME_*
, HWI_TAG_ALIGN_*
и struct hwi_*
. Эти имена подобраны так, что макрос HWI_TAG_INFO()
в коде запуска работает корректно.
Элемент Group группирует несколько элементов и является аналогом каталога в файловой системе. Например, он используется на уровне devclass дерева /hw
. Подробнее см. struct hwi_group.
Элемент Bus указывает, какая шина используется в системе. Могут использоваться следующие (а также не указанные здесь) имена:
#define HWI_ITEM_BUS_PCI "pci"#define HWI_ITEM_BUS_ISA "isa"#define HWI_ITEM_BUS_EISA "eisa"#define HWI_ITEM_BUS_MCA "mca"#define HWI_ITEM_BUS_PCMCIA "pcmcia"#define HWI_ITEM_BUS_UNKNOWN "unknown"
Подробнее см. struct hwi_bus.
Элемент Device описывает отдельное устройство системы (уровень device раздела Trees — уровень devclass задается с помощью тега Group). Подробнее см. struct hwi_device.
Обратите внимание, что location является обычным тегом, а не элементом. Он задает местоположение регистров устройства в отдельном пространстве ввода/вывода или отображенной области памяти. Описание элемента может состоять из нескольких тегов location, если устройство имеет несколько групп регистров. Подробнее см. struct hwi_location.
irq, geometry и pad являются обычными тегами, а не элементами. Подробнее см. struct hwi_irq, struct hwi_diskgeometry и struct hwi_pad соответственно.
Тег pad вставляет заполнение для выравнивания следующего тега.
Тег diskgeometry используется только в системах с архитектурой x86 для считывания информации о геометрии диска из BIOS.
Область cpuinfo содержит информацию о каждой микросхеме процессора (тип, скорость, функции, производительность и размер кеша ЦП). Количество элементов в структуре struct cpuinfo_entry равно значению num_cpu структуры struct syspage_entry (например, в двухпроцессорной системе существуют две записи struct cpuinfo_entry).
Эта таблица автоматически заполняется библиотечной функцией init_cpuinfo().
В области cacheattr содержится информация о конфигурации внутрисхемной и внесхемной кеш-памяти, а также callout control(), который используется для управления кеш-памятью. Запись cacheattr формируется функциями init_cpuinfo() и init_cacheattr().
Следует иметь в виду, что функция init_cpuinfo() работает с кеш-памятью самого процессора, а функция init_cacheattr() — с кеш-памятью платы.
Суммарное количество байт, описываемых записью cacheattr, равно line_size × num_lines.
Записи cacheattr организованы в виде связанного списка, в котором элемент next содержит индекс следующей записи в кеш-памяти более низкого уровня. Такая структура применяется потому, что в некоторых архитектурах кеш-память данных и инструкций разделены на одном уровне, но объединены на другом уровне. Связанный список позволяет эффективно хранить информацию в системной странице. Обратите внимание, что для входа в таблицы cacheattr используются параметры ins_cache и data_cache структуры cpuinfo. Поскольку в SMP-системах cpuinfo является массивом, который индексируется по номеру ЦП, можно настраивать кеш-буферы процессоров, которые имеют различную архитектуру кеш-памяти. Схема двухпроцессорной системы с раздельными кешами инструкций и данных уровня L1 и общим кешем уровня L2 показана на следующем рисунке:
Для вышеописанной структуры памяти поля разделов cpuinfo и cacheattr будут выглядеть следующим образом:
/* CPUINFO */cpuinfo[0].ins_cache = 0;cpuinfo[0].data_cache = 1;cpuinfo[1].ins_cache = 0;cpuinfo[1].data_cache = 1;/* CACHEATTR */cacheattr[0].next = 2;cacheattr[0].linesize = linesize;cacheattr[0].numlines = numlines;cacheattr[0].flags = CACHE_FLAG_INSTR;cacheattr[1].next = 2;cacheattr[1].linesize = linesize;cacheattr[1].numlines = numlines;cacheattr[1].flags = CACHE_FLAG_DATA;cacheattr[2].next = CACHE_LIST_END;cacheattr[2].linesize = linesize;cacheattr[2].numlines = numlines;cacheattr[2].flags = CACHE_FLAG_UNIFIED;
Значения параметров linesize и numlines зависят от конфигурации кеш-памяти системы.
Область intrinfo используется для хранения информации о системе обработки прерываний, а также содержит callout-ы, которые управляют аппаратным контроллером прерываний.
В многоядерной системе каждое прерывание направляется на один (любой) процессор одной или несколькими программируемыми микросхемами контроллеров прерываний. При инициализации контроллера прерываний в процессе запуска системы можно связывать прерывания с процессором, который будет обрабатывать их; некоторые контроллеры прерываний даже позволяют чередовать процессоры, на которые поступают прерывания от одного источника.
Как правило, в собственных модулях startup-* мы направляем все прерывания, кроме межпроцессорных, на процессор с номером 0, что позволяет использовать один модуль startup-* как с procnto, так и procnto-smp. Несколько лет назад компания Sun провела исследование, которое показало, что передача всех прерываний на один ЦП наиболее эффективна благодаря рациональному использованию кеш-памяти.
Область intrinfo автоматически заполняется функцией init_intrinfo().
Если требуется изменять параметры по умолчанию, которые задает функция init_intrinfo(), либо невозможно использовать ее в нестандартной среде, можно вызывать функцию add_interrupt_array() непосредственно и передавать ей структуру struct intrinfo_entry.
![]() | Эту структуру необходимо изменять почти на всех платформах, отличных от x86. |
Каждая группа callout-ов в struct intrinfo_entry (id, eoi, mask, unmask) для каждого уровня контроллера прерываний работает с множеством векторов прерываний, которые начинаются с 0
. Каждому уровню прерываний назначаются соответствующие callout-ы.
Номера векторов прерываний передаются в callout-ы без смещений. Связь векторов прерываний, которые отсчитываются от 0
и используются callout-ами, с векторами прерываний системного уровня задается в структурах intrinfo, которые инициализируются функцией init_intrinfo().
Поле cpu_intr_base
Интерпретация поля cpu_intr_base зависит от процессора:
0x30
. 0x0140
(результат 0x0500 / 4
). startup/boards/440rb/init_intrinfo.c
. 0
. 0
, поскольку все прерывания ARM обрабатываются посредством исключения IRQ.
Все callout-ы обладают следующими свойствами:
Callout-ы представляют собой отдельные фрагменты кода, которые вызываются ядром, но статически не скомпонованы с ним.
Необходимость писать callout-ы на ассемблере вытекает из второго требования — они должны быть позиционно-независимыми. Callout-ы входят в состав кода запуска и перезаписываются после того, как ядро начинает работу. Модуль startup-* копирует их в безопасное место; для того, чтобы callout-ы продолжали выполняться после переноса из места начальной загрузки, они должны быть написаны в относительных адресах.
Последнее требование, которому должны соответствовать callout-ы — отсутствие статических областей для чтения/записи данных. Существует механизм, который предоставляет callout небольшую область памяти в системной странице, однако callout-ы не могут создавать статические хранилища для чтения/записи в своем адресном пространстве.
Отладочный интерфейс включает в себя три callout-а:
Ядро использует их для взаимодействия с последовательным портом, консолью или другим устройством (например, при выводе внутренней отладочной информации или сбое). Обязательным является только callout display_char().
Интерфейс часов/таймеров включает в себя следующие callout-ы:
Ядро использует их для работы с микросхемой таймера.
Callout timer_load() записывает в микросхему таймера/счетчика значение делителя частоты, которое указывает ядро. Поскольку ядру не известны характеристики микросхемы таймера, вызов timer_load() принимает и анализирует переданное значение. Ядро использует новое значение во всех внутренних вычислениях. Новое значение можно считывать из элемента qtime_entry системной страницы или с помощью функции ClockPeriod().
Callout timer_reload() выполняется после генерации прерывания микросхемой таймера и решает две задачи:
Callout timer_value() возвращает прирост значения внутреннего счетчика микросхемы таймера с момента последнего прерывания и используется на процессорах без встроенного счетчика (например, 80486).
Интерфейс контроллера прерываний включает в себя callout-ы
и две "заглушки":
Callout-ы mask() и unmask() маскируют и демаскируют конкретный вектор прерываний, а config() определяет конфигурацию уровня прерывания.
Дополнительную информацию об этих callout-ах см. в описании структуры intrinfo системной страницы ранее в этой статье, а также в описании типа struct intrinfo_entry.
В зависимости от архитектуры контроллера кеш-памяти может требоваться разработка callout-а, который обеспечивает взаимодействие контроллера кеш-памяти с ядром.
Поскольку в архитектуре x86 контроллер кеш-памяти тесно интегрирован с процессором, ядру не требуется взаимодействовать с ним. В других архитектурах (например, MIPS и PowerPC) при выполнении некоторых функций в ядре необходимо передавать контроллерам команды, которые объявляют недействительными фрагменты кеш-памяти.
Управления кеш-памятью осуществляется с помощью callout-а control(), который принимает следующие аргументы:
Callout control() возвращает количество измененных строк кеш-памяти, что позволяет ядру многократно обращаться к нему на более высоких уровнях. Нулевое значение указывает, что все строки кеш-памяти были изменены и объявлены недействительными.
Callout reboot() вызывается ядром для перезагрузки системы. С его помощью разработчики задают действия, которые выполняются перед перезагрузкой — отключение сторожевого таймера, операции над нужными регистрами и др.
При завершении исполняемого файла вызывается функция sysmgr_reboot(), которая, в свою очередь, запускает callout reboot().
Callout power() выполняется при каждой активации системы управления питанием и зависит от процессора и целевой системы.
Существуют следующие основные режимы энергопотребления процессора:
В этих базовых режимах можно определять подмножества состояний процессора. Кроме того, некоторые из вышеперечисленных режимов энергопотребления нецелесообразно или даже невозможно реализовать на конкретном процессоре или плате.
Библиотека libstartup содержит большое количество высокоуровневых функций, которые вызываются функцией main() и обращаются к вспомогательным функциям для опроса устройств, инициализации системной страницы, загрузки следующего процесса в образе и переключения в защищенный режим. Разработчику доступен исходный код всех функций библиотеки libstartup, который позволяет создавать их локальные копии с незначительными изменениями в каталоге startup/
целевой системы.
Ниже перечислены функции библиотеки libstartup в алфавитном порядке:
Как на этапе отладки, так и в процессе эксплуатации, полезно иметь унифицированный механизм получения отладочной информации из модуля startup-*. В некторых стандартных модулях эта функциональность доступна через опцию -k (см. документацию на startup-*).
Чтобы воспользоваться данной возможностью, на этапе инициализации необходимо выполнить ряд действий:
Если все эти шаги выполнены успешно, после запуска системы менеджер slogger вычитает промежуточный буфер и включит его в системный журнал. В дальнейшем его можно будет прочитать с помощью утилиты sloginfo.
Пример инициализации промежуточного буфера системного журнала:
int main( int argc, char **argv, char **envv ){size_t slog_buffer_size = 65536;...init_raminfo();...if ( slog_buffer_size ) {char *slogbuffer = alloc_slog_buffer( slog_buffer_size, 0 );if ( slogbuffer != NULL ) {if ( init_slog( slogbuffer, slog_buffer_size ) == 0 )kprintf( "Info: slog initialization done\n" );elsekprintf( "Error: slog initialization failed\n" );} elsekprintf( "Error: slog initialization failed (memory access issue)\n" );}...}
Для обеспечения совместимости микроядра ЗОСРВ «Нейтрино» с любыми платами, все действия с конкретным оборудованием выполняются за его пределами. Эти действия называются callout-ами ядра и должны реализовываться в модуле startup-*.
Модуль startup-* может включать в себя различные версии одного callout-а и предоставлять ядру доступ к версии, которая подходит для конкретной платы. В то же время в глубоко встроенной системе модуле startup-* заранее известно обо всех имеющихся устройствах, и каждый callout присутствует в ней в единственном экземпляре. Модуль startup-* лишь сообщает о callout-ах ядру и не сканирует аппаратную конфигурацию системы.
Код callout-а копируется из модуля startup-* в системную страницу, а затем память модуля startup-* (код и данные) освобождается.
В момент callout-а перезагрузки:
Поскольку код корректировки выполняется во время работы модуля startup-*, он не влияет на обычные вызовы.
Скопированный код должен быть полностью независим и написан в относительных адресах. Цель процедур корректировки заключается в настройке констант, доступа к области хранения данных и т.д. так, чтобы обеспечить независимость кода и создать все необходимые отображения виртуальной памяти в физическую.
Библиотека libstartup содержит ряд готовых callout-ов. Перед тем, как приступать к разработке собственного callout-а, рекомендуется изучить дерево исходного кода (которое изначально располагалось в каталоге bsp/src/hardware/startup/lib/
), чтобы выяснить, имеется ли готовая функция для вашего устройства/платы. Этот каталог содержит как общий код, так и подкаталоги для конкретных процессоров.
На уровне процессора найдите все файлы исходного кода с именем вида callout_*.[sS]
. Они содержат библиотечные callout-ы. Расширение файла (.s
или .S
) зависит от того, обрабатывался ли он препроцессором C перед передачей транслятору. В этом разделе мы будем использовать термин файлы *.s
.
Имена делятся на типы callout_категория_устройство.s
, где категория может принимать следующие значения:
Параметр устройство задает конкретное устройство, для которого предназначены callout-ы. Как правило, все функции, которые находятся в конкретном файле исходного кода, образуют группу, которая используется (или не используется) ядром. Например, файл callout_debug_8250.s
содержит функции display_char_8250(), poll_key_8250() и break_detect_8250() для работы с микросхемой UART 8250.
Поскольку операционная система освобождает память, которая была выделена модулю startup-*, после ее завершения, callout-ы невозможно использовать в месте их размещения. Библиотека копирует callout-ы в безопасную область. Поскольку весь код callout-ов должен быть написан в относительных адресах, он реализован на ассемблере. Нам необходимо знать местоположение начала и окончания callout-а; не существует портируемого способа определения места завершения C-функции.
Еще одна проблема заключается в отсутствии портируемого способа управления созданием начальной/конечной частей и генерации кода. При изменении двоичного интерфейса приложения или наличии проблем с конфигурацией сборки могут возникать скрытые ошибки.
Ядро использует callout-ы путем стандартного обращения к функциям; позже мы познакомимся с двумя исключениями из этого правила — функциями interrupt_id() и interrupt_eoi().
Найдите файл с кодом callout-а, который похож на разрабатываемый вызов, и скопируйте его в новый файл. Если новые функции можно использовать на нескольких платах, целесообразно сохранить файл с исходным кодом в вашей собственной копии библиотеки libstartup. В противном случае можно скопировать исходный код в каталог с файлами для конкретной платы.
Теперь отредактируйте новый файл с исходным кодом. Он начинается со строки вида
#include "callout.ah"
или
.include "callout.ah"
Различия обусловлены особенностями используемого синтаксиса ассемблера.
В этом заголовочном файле определены макросы CALLOUT_START
и CALLOUT_END
. Макрос CALLOUT_START
принимает три параметра и указывает начало одного callout-а. Первым параметром является имя функции callout-а; второй и третий параметр будут рассмотрены позже.
Макрос CALLOUT_END
указывает окончание функции callout-а. Он принимает один параметр, который должен совпадать с первым параметром предшествующего вызова CALLOUT_START
. Если ядро выбирает эту функцию для использования, библиотека libstartup копирует код, который находится между CALLOUT_START
и CALLOUT_END
, в безопасное и доступное ядру место. Точный синтаксис указанных двух макросов зависит от ассемблера. Часто используется следующий синтаксис:
CALLOUT_START( timer_load_8254, 0, 0 )CALLOUT_END( timer_load_8254 )
или
CALLOUT_START timer_load_8254, 0, 0CALLOUT_END timer_load_8254
Следует соблюдать синтаксис исходного файла. В исходном файле также имеются комментарии с C-прототипами функций, которые позволяют определять передаваемые в них параметры.
Иногда необходимо разрабатывать callout-ы для работы с устройством, которое может отображаться в различные области памяти в зависимости от платы. В таких случаях можно "корректировать" код callout-а при его копировании в конечное местоположение. Третий параметр макроса CALLOUT_START
равен нулю или адресу функции patcher(). Эта функция вызывается немедленно после копирования callout-а в конечное местоположение.
Пример функции корректировки для процессора x86:
patch_debug_8250:movl 0x4(%esp), %eax /* получение paddr функции */addl 0xc(%esp), %eax /* ... */movl 0x14(%esp), %edx /* получение базового адреса */movl DDI_BASE(%edx), %ecx /* корректировка кода - реальный последовательный порт */movl %ecx, 0x1(%eax)movl DDI_SHIFT(%edx), %ecx /* корректировка кода - сдвиг регистра */movl $REG_LS, %edxshll %cl, %edxmovl %edx, 0x6(%eax)retCALLOUT_START( display_char_8250, 0, patch_debug_8250 )movl $0x12345678, %edx /* считывание скорректированного базового адреса последовательного порта */movl $0x12345678, %ecx /* считывание скорректированного смещения последовательного порта */....CALLOUT_END( display_char_8250 )
После копирования функции display_char_8250() вызывается функция patch_debug_8250(), которая изменяет константы в первых двух инструкциях в соответствии с местоположением последовательного порта и смещением его регистров на конкретной плате. Функции корректировки обычно написаны на ассемблере, поскольку находятся в одном файле с корректируемым исходным кодом, однако реализовывать их на ассемблере не обязательно. Группировка первых инструкций схожих callout-ов (например, debug_char_*(), poll_key_*(), break_detect_*()) позволяет корректировать их одной функцией.
Иногда в callout-ах необходимо использовать статическую память для чтения и записи данных. Поскольку callout-ы должны быть позиционно-независимыми, в них невозможно выделять такие области стандартным образом, однако можно использовать функции корректировки и второй параметр CALLOUT_START
, в котором указывается адрес четырехбайтной переменной с требуемым размером области. Пример:
rw_interrupt:.long 4patch_interrupt:add a1, a1, a2j rash a3, 0+LOW16(a1)/* Маскирование указанного прерывания */CALLOUT_START( interrupt_mask_mips, rw_interrupt, patch_interrupt )/* Входные параметры :* a0 - syspage_ptr* a1 - номер прерывания* Возвращаемое значение:* v0 - код ошибки *//* Отключение прерывания */la t3, 0x1234(a0) # считывание скорректированного адреса разрешенных прерыванийli t1, MIPS_SREG_IMASK0....CALLOUT_END( interrupt_mask_mips )
Адрес rw_interrupt во втором параметре указывает библиотеке libstartup, что функции требуется область памяти для чтения/записи размером 4 байта (поскольку по этому адресу записано значение 4). Библиотека libstartup выделяет область в конце системной страницы и передает указатель на нее в функцию корректировки через параметр rw_offset. Затем функция корректировки задает требуемое смещение в начальной инструкции callout-а. Во время выполнения callout-а регистр t3 содержит указатель на область памяти для чтения/записи данных. Здесь неизбежно возникает вопрос: почему параметр CALLOUT_START
содержит адрес памяти, в которой хранится размер области данных? Почему нельзя передавать размер области данных непосредственно?
Это хороший вопрос. Так задумано специально. Группе схожих callout-ов может потребоваться общая область памяти для обмена данными друг с другом. Библиотека передает в функцию корректировки одно и то же значение rw_offset для всех функций, которые имеют одинаковый второй параметр CALLOUT_START
. Другими словами, все нижеперечисленные вызовы
CALLOUT_START( interrupt_mask_mips, rw_interrupt, patch_interrupt )....CALLOUT_END( interrupt_mask_mips )CALLOUT_START( interrupt_unmask_mips, rw_interrupt, patch_interrupt )....CALLOUT_END( interrupt_unmask_mips )CALLOUT_START( interrupt_eoi_mips, rw_interrupt, patch_interrupt )....CALLOUT_END( interrupt_eoi_mips )CALLOUT_START( interrupt_id_mips, rw_interrupt, patch_interrupt )....CALLOUT_END( interrupt_id_mips )
получают одно и то же значение параметра rw_offset, которое передается в функцию patch_interrupt(), а, следовательно, могут пользоваться общим доступом к области хранения данных.
Последний аспект заключается в том, что функции interrupt_id() и interrupt_eoi() вызываются иначе, чем обычные функции. Для увеличения производительности они интегрированы в код ядра, и обращение к ним выполняется не по стандартным правилам вызова функций. В файлах callout_interrupt_*.s
в библиотеке libstartup указаны регистры, через которые callout-ы принимают и передают данные на конкретном процессоре. Также следует иметь в виду, что обычный возврат из середины функции невозможен — ее код выполняется целиком.
В библиотеку libstartup для процессоров PPC внесены изменения, которые:
Все новые функции и переменные для PPC начинаются с префикса ppcv_ и группируются в одну функцию или переменную в каждом файле с исходным кодом для многократного использования без дублирования.
Введены две новые структуры данных:
Первая структура имеет вид:
struct ppcv_chip {unsigned short chip;uint8_t paddr_bits;uint8_t cache_lsize;unsigned short icache_lines;unsigned short dcache_lines;unsigned cpu_flags;unsigned pretend_cpu;const char *name;void (*setup)( void );}
Каждому поддерживаемому процессору соответствует статически инициализируемая переменная указанного типа, которая находится в файле с исходным кодом для процессора (например, <ppvc_chip_603e7.c>
).
Если поле chip совпадает со старшими 16 битами регистра PVR, выбирается эта структура ppcv_chip
, и указатель на нее присваивается библиотечной глобальной переменной pccv. Поскольку проверяются только старшие 16 бит, при инициализации поля можно использовать константы, которые определены в заголовочном файле <ppc/cpu.h>
(PPC_750 и др.).
Поле paddr_bits содержит количество физических линий адреса в микросхеме (как правило, 32).
В поле cache_lsize указывается разрядность размера строки кеш-памяти микросхемы (обычно 5, но иногда 4).
Поля icache_lines и dcache_lines содержат количество строк в кеш-памяти инструкций и данных соответственно.
В поле cpu_flags хранятся флаги PPC_CPU_*
, которые определены в заголовочном файле <ppc/syspage.h>
для соответствующего процессора. Следует отметить, что флаги типа PPC_CPU_HW_HT
не указывались в прежних модулях startup-*; ядро проверяло регистр PVR и самостоятельно устанавливало их при необходимости. В настоящее время ядро устанавливает эти флаги ТОЛЬКО при обнаружении устаревшего модуля startup-*.
Поле pretend_cpu копируется в поле ppc_kerinfo_entry.pretend_cpu системной страницы, чтобы ядро воспринимало процессор с неизвестным PVR как известный ему процессор.
Поле name содержит строку с именем процессора, которая помещается в раздел cpuinfo.
Функция setup вызывается, когда библиотека выбирает для использования определенную структуру ppcv_chip
, и продолжает процесс настройки библиотеки, заполняя вторую новую структуру данных:
struct ppcv_config {unsigned family;void (*cpuconfig1)( int cpu );void (*cpuconfig2)( int cpu );void (*cpuinfo)( struct cpuinfo_entry *cpu );void (*qtime)( void );void *(*map)( unsigned size,paddr_t phys,unsigned prot_flags );void (*unmap)( void * );int (*mmu_info)( enum mmu_info info,unsigned tlb );/* в процессе реализации: tlb_read/write */}
Библиотека содержит единственную переменную этого типа с именем ppcv_config
. Функция настройки, которая указана в выбранной переменной типа ppcv_chip
, задает функции для соответствующей микросхемы в вышеперечисленных полях. Переменная ppcv_config
статически инициализируется пустыми функциями; если не требуется выполнять над микросхемой какие-либо действия (обычно посредством функций cpuconfig[1/2]), функция настройки может ничего не заполнять.
Общий подход к разработке функций заключается в том, что в них принято выполнять действия, которые специфичны для микросхемы, но не специфичны для платы. Например, функции main() устаревших модулей startup-* иногда отключают преобразование данных, поскольку оно включается некоторыми IPL. В новых модулях startup-* библиотека выполняет это действие автоматически. С другой стороны, как в прежних, так и в новых модулях startup-* функция ppc700_init_l2_cache() вызывается вручную из main(), поскольку биты, которые необходимо устанавливать в регистре L2CR, зависят от конкретной платы. Следует модифицировать библиотечные функции так, чтобы они корректно взаимодействовали с IPL и инициализировали процессор, а не создавать "костыли" в коде для платы (как в вышеупомянутом примере с отключением преобразования данных).
Функция настройки также может инициализировать еще ряд глобальных переменных, которые избавляют другие функции (например, ppc600_set_clock_freqs() и ppcv_setup_7450()) от необходимости повторно проверять значения PVR.
Новому модулю startup-* (и ядру, которое настраивается с ее помощью) больше не требуется определять семейство микросхемы по значению регистра PVR. Вместо этого в поле семейства указывается одно из значений PPC_FAMILY_*
, которые определены в заголовочном файле <ppc/syspage.h>
. Это значение копируется в поле ppc_kerinfo_entry.family системной страницы, и с его помощью ядро проверяет корректность используемой версии модуля procnto-*.
Если системная страница содержит значение PPC_FAMILY_UNKNOWN
(ноль), ядро предполагает, что используется устаревший модуль startup-*, и пытается самостоятельно определить значения полей семейства процессора (cpuinfo->flags). НЕ ПРИМЕНЯЙТЕ эту функцию к новым модулям startup-*.
Присваивайте корректные значения полям ppcv_config.family и ppcv_chip.cpu_flags. Функция cpuconfig1 подготавливает процессор к выполнению модуля startup-* и вызывается незадолго до функции main(). Например, она отключает преобразование инструкций и данных, заполняет таблицу исключений указателями на нижнюю область памяти и т.п. Эта функция вызывается один раз для каждого процессора SMP-системы, при этом номер инициализируемого процессора задается параметром cpu.
Функция cpuconfig2 вызывается непосредственно перед передачей управления от модуля startup-* первому загрузочному исполняемому файлу в файловой системе образа. Он настраивает процессор на работу в среде загрузки — например, включает его специфичные функции, такие как биты HID0 и HID1. Этот файл также выполняется однократно для каждого процессора SMP-системы, а параметр cpu определяет номер конкретного процессора.
Функция cpuinfo вызывается функцией init_one_cpuinfo() и заполняет структуру struct cpuinfo_entry для каждого процессора. Функция qtime вызывается функцией init_qtime() и настраивает раздел qtime системной страницы.
Функции map и unmap создают/удаляют отображения памяти, которые используются в модуле startup-* и callout-ах. Они вызываются следующими функциями:
Также следует иметь в виду переменную ppcv_list, которая представляет собой статически инициализируемый массив указателей на структуры ppcv_chip
. Поскольку ее версия по умолчанию включает в себя все переменные ppcv_chip
, которые определены в библиотеке, библиотека по умолчанию может работать с микросхемами PPC любого типа.
Если определить переменную ppcv_list в каталоге платы и включить в нее только переменные ppcv_chip_*, которые можно использовать на этой плате, из модуля startup-* будет исключен весь код, специфичный для микросхем и относящийся к процессорам, которые не могут использоваться в составе платы.
Например, новый модуль startup-* shasta-ssc примерно на 1 Кбайт больше прежней версии, если используется значение переменной ppcv_list по умолчанию. Если включить в состав ppcv_list только переменную ppcv_chip_750, новый модуль startup-* становится на 1 Кбайт меньше исходной.
Для процессора с именем xyz создайте файл <ppcv_chip_xyz.c>
и поместите в него корректно инициализированную переменную ppcv_chip
ppcv_chip_xyz. Добавьте переменную ppcv_chip_xyz в список ppcv_list по умолчанию (в файле <ppcv_list.c>
).
Если можно использовать существующую функцию ppcv_setup_*() для инициализации ppcv_chip_xyz, дальнейшие действия не нужны. В противном случае создайте файл <ppcv_setup_xyz.c>
с корректной функцией ppcv_setup_xyz() (не забудьте добавить ее прототип в заголовочный файл <cpu_startup.h>
).
Если можно использовать существующие функции ppcv_*() в функции ppcv_setup_xyz(), дальнейшие действия не нужны. В противном случае создайте эти функции в соответствующих файлах <ppcv_*_xyz.c>
(не забудьте добавить их прототипы в заголовочный файл <cpu_startup.h>
). При написании функций по возможности используйте объектно-ориентированный стиль программирования и вызывайте существующие функции для заполнения общих данных (например, функция ppcv_cpuconfig2_700() вызывает функцию ppcv_cpuconfig2_600(), которая выполняет основную часть работы, а затем лишь задает параметры, специфичные для процессора серии 700).
На текущий момент следующие функции считаются устаревшими и возвращают соответствующее сообщение при вызове:
Предыдущий раздел: перейти