Операционная система ЗОСРВ «Нейтрино» > Руководство разработчика > Основные принципы системной разработки > Запуск системы, быстрая активация устройств > Построение встраиваемых систем > Статьи руководства > Руководство по разработке модуля startup



Руководство по разработке модуля startup

Разработка инициализирующего кода в составе загрузочного образа

В этой статье:

Введение
Инициализация оборудования
Инициализация системной страницы
Инициализация callout-ов
Файлы модуля startup
Структура модуля startup
Создание нового модуля startup
Структура системной страницы
Деревья адресных пространств
HW-теги и сведения об аппаратной платформе
Теги
Элементы
Деревья устройств
Создание раздела
Другие функции
Параметры по умолчанию
Предопределенные элементы и теги
Элемент "Group"
Элемент "Bus"
Элемент "Device"
Теги "location", "irq", "diskgeometry" и "pad"
Информация о процессорах
Информация о векторах прерываний
Сведения о callout-ах
Отладочный интерфейс
Интерфейс часов/таймеров
Интерфейс контроллера прерываний
Интерфейс контроллера кеш-памяти
Callout перезагрузки системы
Callout управления питанием
Библиотека libstartup
Сохранение отладочного вывода в системном журнале
Написание собственных callout-ов ядра
Готовые функции
Почему callout-ы написаны на ассемблере?
Начало разработки
Корректировка кода callout
Выделение памяти для чтения/записи данных
Исключение, которое подтверждает правило
Поддержка процессоров с архитектурой PPC
Добавление нового процессора в библиотеку libstartup

Введение

Первая программа, которая находится в загрузочном образе ЗОСРВ «Нейтрино», называется модулем startup-* и выполняет следующие задачи:

  1. инициализирует оборудование
  2. инициализирует системную страницу
  3. инициализирует callout-ы
  4. загружает следующую программу образа и передает ей управление.

Изменяя модуль startup-*, можно адаптировать ЗОСРВ «Нейтрино» к различным аппаратным платформам встраиваемых систем.

Инициализация оборудования

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

В модуле startup-* не требуется инициализировать стандартные периферийные устройства (такие как интерфейс IDE) или указывать скорость работы последовательных портов) — это делают соответствующие драйверы после запуска операционной системы.

Инициализация системной страницы

Собранные сведения о системе помещаются в структуру данных, которая находится в оперативной памяти и называется системной страницей. Системная страница содержит данные о типах процессора и шины, а также размере и местоположении ОЗУ системы.

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

Инициализация callout-ов

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

Файлы модуля startup

В состав ЗОСРВ «Нейтрино», а также в соответствующие BSP, входят модули 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

Каждый модуль 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() для вывода отладочной информации
}


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

Создание нового модуля startup

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

HW-теги и сведения об аппаратной платформе

В области hwinfo указываются сведения об аппаратной платформе (тип шины, устройства, линии запроса прерываний и др.) Эта область заполняется функцией init_system_private() системной библиотеки libstartup и является одним из самых сложных компонентов системной страницы. Раздел hwinfo представляет собой не одну структуру или массив определенного типа, а последовательность структур, которые отмечены символьными тегами и в совокупности описывают компоненты аппаратной платформы встраиваемой системы. Все указанные ниже типы и константы определены в заголовочном файле <hw/sysinfo.h>.


Note: В разделе hwinfo не обязательно описывать все аппаратные компоненты. Например, модуль startup-* может не опрашивать шину PCI, если ей не требуется обнаруживать подключенные к ней устройства. Разработчик самостоятельно определяет, какие сведения размещать в разделе hwinfo. Как правило, в hwinfo описываются несъемные компоненты аппаратной платформы.

Теги

Каждая структура (или тег) раздела начинается со структуры struct hwi_prefix. В поле size указывается размер структуры в количестве 4-байтовых блоков (с учетом struct hwi_prefix). Поле name представляет собой смещение относительно начала раздела strings системной страницы и содержит строку с именем структуры с нулевым символом в конце.

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

Элементы

Каждое устройство описывается последовательностью тегов. Совокупность тегов образует элемент, который соответствует одному устройству. Первый тег каждого элемента всегда начинается со следующей структуры (обратите внимание, что ее первым элементом является структура struct hwi_prefix). Элемент определяется структурой struct hwi_item.


Note: Текущий код требует, чтобы имя тега любой структуры элемента начиналось с заглавной буквы, а имена тегов, которые не относятся к элементам — со строчной буквы.

Деревья устройств

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

/hw/шина/класс_устройства/устройство

где:

hw
корень дерева устройств
шина
шина, к которой подключено устройство (pci, eisa и др.)
класс_устройства
общий класс устройства (serial, rtc и др.)
устройство
микросхема устройства (8250, mc146818 и др.)

Создание раздела

Для добавления информации в раздел 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 корректируется соответствующим образом.

Чтобы создать элемент, выполните следующие действия:

  1. Вызовите функцию hwi_alloc_item(), чтобы создать элемент верхнего уровня (с владельцем HWI_NULL_OFF).
  2. Добавьте в элемент все необходимые структуры тегов.
  3. Создайте новый элемент с помощью функции hwi_alloc_item(). Он может являться как вторым элементом верхнего уровня, так и дочерним элементом по отношению к первому.

Можно создавать элементы в любом порядке; достаточно создавать родительский элемент раньше, чем дочерний.

Предположим, что при создании дочернего элемента вы сохранили его владельца в переменной, или вам известно только имя его элемента. Чтобы определить корректное значение параметра 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 );
#endif
tag = 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;
#else
tag->addrspace.len = (uint64_t)1 << 32;
#endif
}

Предопределенные элементы и теги

Файл <hw/sysinfo.h> содержит ряд предопределенных элементов и тегов. Разработчик также может создавать дополнительные элементы для собственных нужд. Все предопределенные элементы и теги имеют имена вида HWI_TAG_NAME_*, HWI_TAG_ALIGN_* и struct hwi_*. Эти имена подобраны так, что макрос HWI_TAG_INFO() в коде запуска работает корректно.

Элемент "Group"

Элемент Group группирует несколько элементов и является аналогом каталога в файловой системе. Например, он используется на уровне devclass дерева /hw. Подробнее см. struct hwi_group.

Элемент "Bus"

Элемент 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 описывает отдельное устройство системы (уровень device раздела Trees — уровень devclass задается с помощью тега Group). Подробнее см. struct hwi_device.

Теги "location", "irq", "diskgeometry" и "pad"

Обратите внимание, что 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 показана на следующем рисунке:

caches.png
Рисунок 1. Двухпроцессорная система с раздельным L1-кешем инструкций и данных

Для вышеописанной структуры памяти поля разделов 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.


Note: Эту структуру необходимо изменять почти на всех платформах, отличных от x86.

Каждая группа callout-ов в struct intrinfo_entry (id, eoi, mask, unmask) для каждого уровня контроллера прерываний работает с множеством векторов прерываний, которые начинаются с 0. Каждому уровню прерываний назначаются соответствующие callout-ы.

Номера векторов прерываний передаются в callout-ы без смещений. Связь векторов прерываний, которые отсчитываются от 0 и используются callout-ами, с векторами прерываний системного уровня задается в структурах intrinfo, которые инициализируются функцией init_intrinfo().

Поле cpu_intr_base

Интерпретация поля cpu_intr_base зависит от процессора:

x86
Запись в таблице дескрипторов прерываний (Interrupt Descriptor Table, IDT) — как правило, 0x30.
PPC
Смещение относительно начала таблицы исключений, которому передается управление при возникновении внешнего прерывания. Например, оно может быть равно 0x0140 (результат 0x0500 / 4).
PPC (BE)
Прерывания больше не размещаются по фиксированному адресу в нижней области памяти; в настоящее время используется набор регистров смещений векторов прерываний (Interrupt Vector Offset Register, IVOR). Каждый класс исключений имеет собственный IVOR. При настройке прерываний в модуле startup-* необходимо задавать конкретный IVOR, который будет использоваться при возникновении прерывания. Например, регистр PPCBKE_SPR_IVOR4 используется для обычных внешних прерываний, а регистр PPCBKE_SPR_IVOR10 — для прерываний декрементного счетчика. Пример настройки процессоров bookE см. в файле startup/boards/440rb/init_intrinfo.c.
PPC (не BE)
MIPS
Значение регистра "причины" при возникновении внешнего прерывания. Стандартное значение равно 0.
ARM
Это значение должно быть равно 0, поскольку все прерывания ARM обрабатываются посредством исключения IRQ.

Сведения о callout-ах

Все 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 перезагрузки системы

Callout reboot() вызывается ядром для перезагрузки системы. С его помощью разработчики задают действия, которые выполняются перед перезагрузкой — отключение сторожевого таймера, операции над нужными регистрами и др.

При завершении исполняемого файла вызывается функция sysmgr_reboot(), которая, в свою очередь, запускает callout reboot().

Callout управления питанием

Callout power() выполняется при каждой активации системы управления питанием и зависит от процессора и целевой системы.

Существуют следующие основные режимы энергопотребления процессора:

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

В этих базовых режимах можно определять подмножества состояний процессора. Кроме того, некоторые из вышеперечисленных режимов энергопотребления нецелесообразно или даже невозможно реализовать на конкретном процессоре или плате.

Библиотека libstartup

Библиотека 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" );
else
kprintf( "Error: slog initialization failed\n" );
} else
kprintf( "Error: slog initialization failed (memory access issue)\n" );
}
...
}

Написание собственных callout-ов ядра

Для обеспечения совместимости микроядра ЗОСРВ «Нейтрино» с любыми платами, все действия с конкретным оборудованием выполняются за его пределами. Эти действия называются 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, где категория может принимать следующие значения:

cache
функции управления кеш-памятью
debug
функции отладочного ввода и вывода в ядре
interrupt
функции обработки прерываний
timer
функция обработки сигналов микросхемы таймера
reboot
перезагрузка системы

Параметр устройство задает конкретное устройство, для которого предназначены callout-ы. Как правило, все функции, которые находятся в конкретном файле исходного кода, образуют группу, которая используется (или не используется) ядром. Например, файл callout_debug_8250.s содержит функции display_char_8250(), poll_key_8250() и break_detect_8250() для работы с микросхемой UART 8250.

Почему callout-ы написаны на ассемблере?

Поскольку операционная система освобождает память, которая была выделена модулю 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, 0
CALLOUT_END timer_load_8254

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

Корректировка кода callout

Иногда необходимо разрабатывать 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, %edx
shll %cl, %edx
movl %edx, 0x6(%eax)
ret
CALLOUT_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 4
patch_interrupt:
add a1, a1, a2
j ra
sh 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-ы принимают и передают данные на конкретном процессоре. Также следует иметь в виду, что обычный возврат из середины функции невозможен — ее код выполняется целиком.

Поддержка процессоров с архитектурой PPC

В библиотеку 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 Кбайт меньше исходной.

Добавление нового процессора в библиотеку libstartup

Для процессора с именем 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).

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

ppc600_init_features()
ppc600_init_caches()
ppc600_flush_caches()
Автоматически вызываются библиотекой.
ppc7450_init_l2_cache()
Вместо этой функции используется функция ppc700_init_l2_cache().




Предыдущий раздел: перейти