С/C++ Startup🔗
Старт программы🔗
Функция _start🔗
После выполнения низкоуровневой инициализации управление передаётся подпрограмме _start (исходный файл startup.c). Как правило, такой который выполняет функции стандартного стартапа любой C/C++ программы:
- статическую инициализацию (инициализация глобальных переменных);
- динамическую инициализацию (вызов конструкторов глобальных объектов).
ВАЖНОЕ ЗАМЕЧАНИЕ
Но в данном случае производятся специальные действия по загрузке кода и данных программы. Загрузчик собран так, что часть его выполняется из OCM, а часть из DRAM. Выполнение из DRAM необходимо из-за того, что в дальнейшем загрузчик производит перемещение трёх сегментов OCM, расположенных по адресу 0x00000000 в адрес 0xfffc0000, чтобы все четыре сегмента образовали единое адресное пространство размером 256к. Код перемещения и все части программы, которые требуются для работы после перемещения, не могут после этого оставаться в адресах 0x00000000-0x0002ffff (три сегмента OCM), т.к. OCM по этим адресам больше нет.
В младших адресах OCM располагается только код начальной загрузки (boot) и функции _start с её зависимостями. Все остальные части загрузчика, включая таблицу векторов прерываний, обслуживание периферии, печати лога на терминал, загрузки битстрима PL и загрузки образа целевой программы, располагаются в DRAM.
BootROM не может загрузить код bootloader сразу непосредственно в DRAM – для этого необходимо, чтобы контроллер памяти был проинициализирован, а это выполняется в процессе иниациализации периферии – функция ps7_init. Только после этого можно загрузить код в DRAM.
Исходный код функции:
__attribute__ ((section ("._start"))) // place function at determine address to
void _start() // specify breakpoint for JTAG loading via xsct
{
ps7_init();
for(size_t i = 0; i < __bss_end - __bss_start; ++i) // zero-fill uninitialized variables
{
__bss_start[i] = 0;
}
const size_t CODE_SIZE = __code_end - __code_start;
const size_t DATA_SIZE = __data_end - __data_start;
const size_t RODATA_SIZE = __rodata_end - __rodata_start;
memcopy(__code_start, __code_src_start, CODE_SIZE);
memcopy(__data_start, __data_src_start, DATA_SIZE);
memcopy(__rodata_start, __rodata_src_start, RODATA_SIZE);
copy_code_err_count = check_memcopy(__code_src_start, __code_start, CODE_SIZE);
copy_data_err_count = check_memcopy(__data_src_start, __data_start, DATA_SIZE);
copy_rodata_err_count = check_memcopy(__rodata_src_start, __rodata_start, RODATA_SIZE);
Xil_DCacheEnable();
Xil_ICacheEnable();
__libc_init_array(); // low-level init & ctor loop
main();
}
Действия функции:
- инициализация периферии – выполнение
ps7_init(), см. ниже. После этого становится доступной DRAM и все разрешённые периферийные устройства – например, UART, используемый для вывода сообщений на терминал, и QSPI контроллер, с помощью которого загрузчик извлекает образы бистрима PL и целевой программы из загрузочной ПЗУ; - обнуление глобальных переменных, не имеющих инициализаторов. Они располагаются в DRAM, как и все остальные части загрузчика;
- копирование секций кода, данных и данных только для чтения на свои места в DRAM, после чего программа загрузчика готова к использованию;
- динамическая инициализация (вызов конструкторов объектов классов).
Следует обратить внимание на две особенности:
- отказ от использования библиотечных функций до готовности загрузчика (т.е. до момента, когда все его части не будут размещены в DRAM на своих местах);
- действия по копированию и проверке копирования выполняются при выключенных кэша данных и кода.
Отказ от использования библиотечных функций – например, memcpy для копирования, – обусловлен стремлением предоставить библиотеки основной части загрузчика, которая начинается с функции main – очевидно, что невозможно одну и ту же библиотеку разместить в разных областях памяти.
Копирование и проверка выполняются при отключенных кэшах, т.к. кэши тут почти не дают никакого выигрыша, но их использование порождает ряд трудностей – например, использование функций работы с кэшами пришлось бы размещать в OCM (иначе их использование из DRAM без очистки кэша данных и инвалидации кэша инструкций является некорректным), и это привело бы к невозможности их использования после релокации сегментов OCM. Копирование при отключенных кэшах избавляет от этих проблем – весь код и данные гарантировано находятся в DRAM. После этого кэши включаются, и основная часть загрузчика работает эффективно.
Проверка правильности копирования имеет результатом переменные-счётчики количества ошибок копирования. Значения этих счётчиков выводятся в лог на терминал во время выполнения функции main после готовности функции печати к работе.
ПОЯСНЕНИЕ
Функция _start помещается в отдельной секции. Это сделано для того, чтобы разместить функцию по детерминированному адресу, который используется при загрузке с помощью консоли xsct. Дело в том, что когда в QSPI присутствует валидный образ загрузчика, SoC по команде сброса начинает загрузку этого образа в OCM с последующим запуском. Этот процесс конфликтует с загрузкой через xsct.
Для предотвращения конфликта в SoC Zynq-7000 предусмотрены разные режиме загрузки, задаваемые с помощью специальных выводов (bootstrap pins). К сожалению, на целевой плате приборе переключение этих режимов затруднено.
Поэтому используется следующий приём: скрипт загрузки устанавливает точку останова на адрес функции _start, который специально сделан детерминированным, после чего выполняет сброс – при этом стартует BootROM, который обнаруживает валидный образ загрузчика в QSPI, копирует его в OCM и запускает на выполнение. Выполнение доходит до точки останова и инициализация дальше не идёт. Этот момент отслеживает скрипт загрузки, который после этого загружает исполняемый (elf) в память микросхемы и запускает загруженную программу.
Таким образом удаётся предотвратить конфликт между загрузкой образа из QSPI и загрузкой средствами консоли xsct.
Обычное для этой функции действие – инициализация стека, – не производится, т.к. указатели стеков (для всех режимов процессора) проинициализированы ранее.
После этого управление передаётся функции main.
Инициализация периферийных устройств PS🔗
Общие сведения🔗
Инициализация периферийных устройств выпоняется путём вызова функции ps7_init, которая сгенерирована инструментальными средствами Vivado: САПР генерируют файлы, код которых отвечает за инициализацию всей периферии:
| File | Description |
|---|---|
ps7_init.h |
Определения макросов и прототипов функций |
ps7_init.c |
Код и данные для инициализации |
ps7_init.tcl |
Код для IDE для эмуляции инициализации загрузчиком, необходим для отладки пользовательского приложения |
Процесс инициализации🔗
Краткое описание🔗
Процесс инициализации осуществляется путём вызова функции ps7_init(). Функция состоит из двух логических частей:
- Подготовка данных.
- Инициализация.
Подготовка данных сводится к выбору в соответствии с ревизией используемой целевой микросхемы SoC набора данных, это осуществляется путём присвоения соответствующих значений указателям на структуры данных.
Собственно инициализация выполняется путём загрузки данных в регистры периферийных устройств. Процесс загрузки организован как выполнение простых инструкций неким виртуальным процессором. Сами инструкции "записаны" в структурах данных. Такой подход обеспечивает относительную простоту реализации и универсальность, хотя и даёт заметные накладные расходы как по размеру данных, так и по времени выполнения.
Структуры данных🔗
В файле ps7_init.h определены следующие макросы:
#define OPCODE_EXIT 0U
#define OPCODE_CLEAR 1U
#define OPCODE_WRITE 2U
#define OPCODE_MASKWRITE 3U
#define OPCODE_MASKPOLL 4U
#define OPCODE_MASKDELAY 5U
#define EMIT_EXIT() ( (OPCODE_EXIT << 4 ) | 0 )
#define EMIT_CLEAR(addr) ( (OPCODE_CLEAR << 4 ) | 1 ) , addr
#define EMIT_WRITE(addr,val) ( (OPCODE_WRITE << 4 ) | 2 ) , addr, val
#define EMIT_MASKWRITE(addr,mask,val) ( (OPCODE_MASKWRITE << 4 ) | 3 ) , addr, mask, val
#define EMIT_MASKPOLL(addr,mask) ( (OPCODE_MASKPOLL << 4 ) | 2 ) , addr, mask
#define EMIT_MASKDELAY(addr,mask) ( (OPCODE_MASKDELAY << 4 ) | 2 ) , addr, mask
Они используются для формирования значений элементов структур данных. Собственно, структуры данных представляют собой массивы, состоящие из цепочек слов, образующих "инструкции" некоего виртуального процессора. Длина цепочек-инструкций варьируется от 1 до 4. Первое слово всегда комбинация опкода (старший нибл младшего байта) и количества аргументов (младший нибл младшего байта). Из определения макросов видно, что инструкция EXIT имеет длину в одно слово, CLEAR - 2 слова, MASKWRITE - 4 слова, остальные - по 3 слова.
Все массивы инструкций организованы одинаково - см. пример (фрагмент (начало и окончание) массива для инициализации устройства ФАПЧ):
unsigned long ps7_pll_init_data_3_0[] = {
// START: top
// .. START: SLCR SETTINGS
// .. UNLOCK_KEY = 0XDF0D
// .. ==> 0XF8000008[15:0] = 0x0000DF0DU
// .. ==> MASK : 0x0000FFFFU VAL : 0x0000DF0DU
// ..
EMIT_MASKWRITE(0XF8000008, 0x0000FFFFU ,0x0000DF0DU),
// .. FINISH: SLCR SETTINGS
// .. START: PLL SLCR REGISTERS
// .. .. START: ARM PLL INIT
// .. .. PLL_RES = 0x4
// .. .. ==> 0XF8000110[7:4] = 0x00000004U
// .. .. ==> MASK : 0x000000F0U VAL : 0x00000040U
// .. .. PLL_CP = 0x2
// .. .. ==> 0XF8000110[11:8] = 0x00000002U
// .. .. ==> MASK : 0x00000F00U VAL : 0x00000200U
// .. .. LOCK_CNT = 0xfa
// .. .. ==> 0XF8000110[21:12] = 0x000000FAU
// .. .. ==> MASK : 0x003FF000U VAL : 0x000FA000U
// .. ..
EMIT_MASKWRITE(0XF8000110, 0x003FFFF0U ,0x000FA240U),
...
...
...
...
//
EMIT_EXIT(),
};
EMIT_MASKWRITE(0XF8000008, 0x0000FFFFU ,0x0000DF0DU) генерирует последовательность слов:
0x00000033: опкод и количество аргументов0xf8000008: адрес регистра0x0000ffff: маска, накладываемая на записываемое в регистр слово данных0x0000df0d: слово данных
При применении этой "инструкции" про указанном адресу будет записано число 0xdf0d в младшие 16 бит регистра, старшие 16 бит останутся без изменений, это обеспечивается применением маски - обновляются только те биты, в позиции которых в значении маски стоят 1. В данном конкретном случае осуществляется запись в регистр SCLR_UNLOCK специального кода, который разблокирует доступ по записи к группе регистров sclr.
Остальные инструкции генерируются аналогично. Для удобства в комментариях к инструкциям поясняется, какие биты, флаги, поля регистров модифицируются.
Размер файла ps7_init.c достаточно большой (более полумегабайта), но страшного в этом ничего нет - львиную долю этого объёма занимаются массивы инструкций и комментарии к ним, а таких массивов там полтора десятка: три ревизии, для каждой инициализация MIO, PLL, Clock, DDR, Peripherals.
Загрузка🔗
Весь процесс инициализации сводится к выполнению инструкций из массивов:
int
ps7_init()
{
// Get the PS_VERSION on run time
unsigned long si_ver = ps7GetSiliconVersion ();
int ret;
// инициализация указателей на массивы инструкций
// в соответствии с ревизией кристалла
// ...
// ...
// ...
// MIO init
ret = ps7_config (ps7_mio_init_data);
if (ret != PS7_INIT_SUCCESS) return ret;
// PLL init
ret = ps7_config (ps7_pll_init_data);
if (ret != PS7_INIT_SUCCESS) return ret;
// Clock init
ret = ps7_config (ps7_clock_init_data);
if (ret != PS7_INIT_SUCCESS) return ret;
// DDR init
ret = ps7_config (ps7_ddr_init_data);
if (ret != PS7_INIT_SUCCESS) return ret;
// Peripherals init
ret = ps7_config (ps7_peripherals_init_data);
if (ret != PS7_INIT_SUCCESS) return ret;
//xil_printf ("\n PCW Silicon Version : %d.0", pcw_ver);
return PS7_INIT_SUCCESS;
}
MIO, затем PLL, дерево тактовых частот, DDR, периферийных устройств. Сам процесс загрузки данных в регистры во всех случаях один и тот же и выполняется с помощью функции ps7_config(). Фрагмент этой функции, отвечающий за собственно выполнение инструкций:
...
...
...
switch ( opcode ) {
case OPCODE_EXIT:
finish = PS7_INIT_SUCCESS;
break;
case OPCODE_CLEAR:
addr = (unsigned long*) args[0];
*addr = 0;
break;
case OPCODE_WRITE:
addr = (unsigned long*) args[0];
val = args[1];
*addr = val;
break;
case OPCODE_MASKWRITE:
addr = (unsigned long*) args[0];
mask = args[1];
val = args[2];
*addr = ( val & mask ) | ( *addr & ~mask);
break;
case OPCODE_MASKPOLL:
addr = (unsigned long*) args[0];
mask = args[1];
i = 0;
while (!(*addr & mask)) {
if (i == PS7_MASK_POLL_TIME) {
finish = PS7_INIT_TIMEOUT;
break;
}
i++;
}
break;
case OPCODE_MASKDELAY:
addr = (unsigned long*) args[0];
mask = args[1];
int delay = get_number_of_cycles_for_delay(mask);
perf_reset_and_start_timer();
while ((*addr < delay)) {
}
break;
default:
finish = PS7_INIT_CORRUPT;
break;
}
...
...
...
OPCODE_EXIT завершает циклический процесс выполнения инструкций. Такая инструкция помещается в конец массива.
Инструкция с опкодом OPCODE_CLEAR загружает в регистр по адресу значение 0.
Инструкция с опкодом OPCODE_WRITE загружает в регистр по адресу значение аргумента, переданного вслед за опкодом.
Инструкция с опкодом OPCODE_MASK_WRITE загружает в регистр по адресу значение аргумента в соответствии с маской: загружаются только те биты значения, в позиции которых в значении маски стоят 1.
Инструкция с опкодом OPCODE_MASKPOLL выполняет следующие действия: в цикле читает значение регистра по адресу, накладывает на это значение маску и проверяет на ноль. Как только значение регистра с маской дадут ноль, опрос прекращается. Количество циклов в опросе задаётся макросом PS7_MASK_POLL_TIME и составляет 100000000. Инструкция служит для опроса значений регистров с целью определить, установился ли тот или иной бит, флаг и поле.
Инструкция с опкодом OPCODE_MASKDELAY просто формирует задержку, длительность которой определяется значением аргумента инструкции. Задержка задаётся в миллисекундах. Задержка формируется путём сравнения значения счётного регистра глобального таймера с вычисленным значением, соответствующим указанной задержке в миллисекундах. Таймер останавливается, его счётный регистр обнуляется, затем таймер запускается и производится опрос счётного регистра таймера на предмет превышения порогового значения, после чего выполнение этой инструкции завершается. Инструкция используется для формирования задержек реального времени.