С/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, после чего программа загрузчика готова к использованию;
  • динамическая инициализация (вызов конструкторов объектов классов).

Следует обратить внимание на две особенности:

  1. отказ от использования библиотечных функций до готовности загрузчика (т.е. до момента, когда все его части не будут размещены в DRAM на своих местах);
  2. действия по копированию и проверке копирования выполняются при выключенных кэша данных и кода.

Отказ от использования библиотечных функций – например, 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(). Функция состоит из двух логических частей:

  1. Подготовка данных.
  2. Инициализация.

Подготовка данных сводится к выбору в соответствии с ревизией используемой целевой микросхемы 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) генерирует последовательность слов:

  1. 0x00000033: опкод и количество аргументов
  2. 0xf8000008: адрес регистра
  3. 0x0000ffff: маска, накладываемая на записываемое в регистр слово данных
  4. 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 просто формирует задержку, длительность которой определяется значением аргумента инструкции. Задержка задаётся в миллисекундах. Задержка формируется путём сравнения значения счётного регистра глобального таймера с вычисленным значением, соответствующим указанной задержке в миллисекундах. Таймер останавливается, его счётный регистр обнуляется, затем таймер запускается и производится опрос счётного регистра таймера на предмет превышения порогового значения, после чего выполнение этой инструкции завершается. Инструкция используется для формирования задержек реального времени.