Восхождение на Эльбрус — Разведка боем. Техническая Часть 2. Прерывания, исключения, системный таймер

ic4z5olelesc04boln85goculk4.pngПродолжаем исследовать Эльбрус путем портирования на него Embox.
Данная статья является второй частью технической статьи об архитектуре Эльбрус. В первой части речь шла о стеках, регистрах и так далее. Перед прочтением этой части рекомендуем изучить первую, поскольку в ней рассказывается о базовых вещах архитектуры Эльбрус. В этой части речь пойдет о таймерах, прерываниях и исключениях. Это, опять же, не официальная документация. За ней следует обращаться к разработчикам Эльбруса в МЦСТ.

Приступая к изучению Эльбруса, нам хотелось побыстрее запустить таймер, ведь, как вы понимаете, вытесняющая многозадачность без него не работает. Для этого казалось достаточно реализовать контроллер прерываний и сам таймер, но мы столкнулись с неожиданными ожидаемыми трудностями, куда же без них. Стали искать возможности отладки и выяснили, что разработчики об этом позаботились, введя несколько команд, которые позволяют вызывать различные исключительные ситуации. Например, можно сгенерировать исключение специального вида через регистры PSR (Processor Status Register) и UPSR (User processor status register). Для PSR бит exc_last_wish — флаг генерации исключительной ситуации exc_last_wish при возврате из процедуры, а для UPSR — exc_d_interrupt, это флаг отложенного прерывания, которые вырабатываются операцией VFDI (Проверка флага отложенного прерывания).
Код следующий:

    #define UPSR_DI (1 << 3) /* Определен в .h файле */

    rrs %upsr, %r1
    ors %r1, UPSR_DI, %r1 /* upsr |= UPSR_DI; */
    rws %r1, %upsr
    vfdi  /* Вот здесь должно выработаться исключение */


Запустили. Но ничего не произошло, система где-то висела, в консоль ничего не выводилось. Собственно это мы и видели, когда пытались запустить прерывание от таймера, но тогда было много составляющих, а тут было понятно, что произошло нечто прерывающее последовательный ход выполнения нашей программы, и управление передалось на таблицу исключений (в терминах архитектуры Эльбрус правильнее говорить не о таблице прерываний, а о таблице исключений). Мы предположили, что процессор все-таки выработал исключение, но там, куда он передал управление, лежит какой-то «мусор». Как оказалось, передает он управление в то самое место куда мы положили образ Embox, а значит там лежала точка входа — функция entry.

Для проверки мы сделали следующее. Завели счетчик входов в entry (). Изначально стартуют все CPU с выключенными прерываниями, заходят в entry (), после чего мы оставляем активным только одно ядро, все остальные уходят в бесконечный цикл. После того как счетчик сравнялся с количеством CPU, считаем что все последующие попадания в entry — это исключения. Напоминаю, что раньше было так, как описано в нашей самой первой статье про Эльбрус

  cpuid = __e2k_atomic32_add(1, &last_cpuid);

    if (cpuid > 1) {
        /* XXX currently we support only single core */
        while(1);
    }

    /* copy of trap table */
    memcpy((void*)0, &_t_entry, 0x1800);

    kernel_start();


Сделали так

    /* Since we enable exceptions only when all CPUs except the main one
     * reached the idle state (cpu_idle), we can rely that order and can
     * guarantee exceptions happen strictly after all CPUS entries. */
    if (entries_count >= CPU_COUNT) {
        /* Entering here because of expection or interrupt */
        e2k_trap_handler(regs);
   ...
    }

    /* It wasn't exception, so we decide this usual program execution,
     * that is, Embox started on CPU0 or CPU1 */

    e2k_wait_all();

    entries_count = __e2k_atomic32_add(1, &entries_count);

    if (entries_count > 1) {
        /* XXX currently we support only single core */
        cpu_idle();
    }

    e2k_kernel_start();
}


И наконец увидели реакцию на вход в прерывание (просто с помощью printf вывели строчку).

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


    .text : {
        _start = .;
         _t_entry = .;
        /* Interrupt handler */
        *(.ttable_entry0)
        . = _t_entry + 0x800;
        /* Syscall handler */
        *(.ttable_entry1)
        . = _t_entry + 0x1000;
        /* longjmp handler */
        *(.ttable_entry2)
        . = _t_entry + 0x1800;
        _t_entry_end = .;

        *(.e2k_entry)
        *(.cpu_idle)

        /* text */
    }


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

Вот так выглядит функция входа для нашего активного ядра, на котором будет выполняться Embox:

static void e2k_kernel_start(void) {
    extern void kernel_start(void);
    int psr;

    /* Ждем пока остальные CPU "уснут” */
    while (idled_cpus_count < CPU_COUNT - 1)
        ;

...

    /* Отключаем операции с плавающей точкой, они разрешены по умолчанию */
    e2k_upsr_write(e2k_upsr_read() & ~UPSR_FE);

    kernel_start(); /* Входим в Embox */
}


Отлично, по инструкции VFDI выработалось исключение. Теперь нужно получить его номер, чтобы убедиться, что это правильное исключение. Для этого в Эльбрусе есть регистры информации о прерываниях TIR (Trap Info registers). Они содержат информацию о нескольких последних командах, то есть заключительной части трассы выполнения (trace). Trace собирается во время выполнения программы и «замораживается» при входе в прерывание. TIR включает в себя младшую (64 бита) и старшую (64 бита) части. В младшем слове содержатся флаги исключений, а в старшем слове указатель на инструкцию приведшую к исключению и номер текущего TIR«a. Соответственно, в нашем случае exc_d_interrupt это 4-ый бит.
Примечание у нас еще осталось некоторое непонимание касательно глубины (кол-ва) TIR«ов. В документации приводится:

«Глубина памяти TIR, то есть количество регистров Trap Info, определяется
TIR_NUM macro, равным количеству стадий конвейера процессора, требуемых для
выдачи всех возможных особых ситуаций. TIR_NUM = 19;»


На практике же, мы видим глубину = 1, и поэтому используем только регистр TIR0.
Специалисты в МЦСТ нам пояснили, что все правильно, и для «точных» прерываний будет только TIR0, а для других ситуаций может быть и другое. Но поскольку пока речь идет только о прерываниях от таймера, нам это не мешает.

Хорошо, теперь разберем, что нужно для правильного входа/выхода из обработчика исключения. На самом деле необходимо сохранять на входе и восстанавливать на выходе 5 следующих регистров. Три регистра подготовки передачи управления — ctpr[1,2,3], и два регистра управления циклами — ILCR (Регистр исходного значения счетчика циклов) и LSR (Регистр состояния цикла).

.type ttable_entry0,@function
ttable_entry0:
    setwd   wsz = 0x10, nfx = 1;
    rrd %ctpr1, %dr1
    rrd %ctpr2, %dr2
    rrd %ctpr3, %dr3
    rrd %ilcr,  %dr4
    rrd %lsr,   %dr5

    /* sizeof pt_regs */
    getsp -(5 * 8), %dr0

    std %dr1, [%dr0 + PT_CTRP1] /* regs->ctpr1 = ctpr1 */
    std %dr2, [%dr0 + PT_CTRP2] /* regs->ctpr2 = ctpr2 */
    std %dr3, [%dr0 + PT_CTRP3] /* regs->ctpr3 = ctpr3 */

    std %dr4, [%dr0 + PT_ILCR]  /* regs->ilcr = ilcr */
    std %dr5, [%dr0 + PT_LSR]   /* regs->lsr = lsr */

    disp    %ctpr1, e2k_entry
    ct        %ctpr1


Собственно, на этом все, после выхода из обработчика исключения нужно восстановить эти 5 регистров.
Мы делаем это с помощью макроса

#define RESTORE_COMMON_REGS(regs)                   \
({                                  \
    uint64_t ctpr1 = regs->ctpr1, ctpr2 = regs->ctpr2,   \
        ctpr3 = regs->ctpr3, lsr = regs->lsr,       \
        ilcr = regs->ilcr;                  \
    /* ctpr2 is restored first because of tight time constraints    \
     * on restoring ctpr2 and aaldv. */             \
    E2K_SET_DSREG(ctpr1, ctpr1);                    \
    E2K_SET_DSREG(ctpr2, ctpr2);                    \
    E2K_SET_DSREG(ctpr3, ctpr3);                    \
    E2K_SET_DSREG(lsr, lsr);                    \
    E2K_SET_DSREG(ilcr, ilcr);                  \
})


Еще важно не забыть уже после восстановления регистров вызвать операцию DONE (Возврат из аппаратного обработчика прерываний). Эта операция нужна в частности для того, чтобы корректно обработать прерванные операции передачи управления. Это мы делаем с помощью макроса

#define E2K_DONE \
do { \
    asm volatile ("{nop 3} {done}" ::: "ctpr3"); \
} while (0)


Собственно возврат из прерывания мы делаем прямо в Си коде с помощью этих двух макросов.

        /* Entering here because of expection or interrupt */
        e2k_trap_handler(regs);

        RESTORE_COMMON_REGS(regs);

        E2K_DONE;


Внешние прерывания.


Начнем с того, как же разрешить внешние прерывания. В Эльбрусe в качестве контроллера прерываний используется APIC (точнее его аналог), в Embox уже был этот драйвер. Поэтому можно было подцепить к нему системный таймер. Есть два таймера, один какой-то очень похожий на PIT, другой LAPIC Timer, тоже достаточно стандартный, поэтому рассказывать о них не имеет смысла. И то, и то выглядело просто, и то и то в Embox уже было, но драйвер LAPIC-таймера выгляден более перспективно, к тому же реализация PIT таймера нам показалась более нестандартной. Следовательно, доделать казалось проще. К тому же в официальной документации были описаны регистры APIC и LAPIC, которые немного отличались от оригиналов. Приводить их нет смысла, поскольку можно посмотреть в оригинале.

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

Мы используем следующее решение. Регистр PSR позволяет включить управление через UPSR, который уже является глобальным (что нам и нужно). Поэтому мы разрешаем управление через UPSR непосредственно (важно!) перед функцией входа в ядро Embox:

    /* PSR is local register and makes sense only within a function,
    * so we set it here before kernel start. */
    asm volatile ("rrs %%psr, %0" : "=r"(psr) :);
    psr |= (PSR_IE | PSR_NMIE | PSR_UIE);
    asm volatile ("rws %0, %%psr" : : "ri"(psr));

    kernel_start();


Как-то случайно после рефакторинга я взял и вынес эти строчки в отдельную функцию… А регистр-то локальный для функции. Понятно, что все сломалось :)

Итак, в процессоре все необходимое, вроде бы, включили, переходим к контроллеру прерываний.

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

После включения таймера последовало пару дней мучений, так как никакого прерывания получить не удалось. Причина была достаточной забавной. В Эльбрусе 64-битные указатели, а адрес регистра в APIC влазил в uint32_t, поэтому мы их и использовали. Но оказалось, что если вам нужно, например, привести 0xF0000000 к указателю, то вы получите не 0xF0000000, а 0xFFFFFFFFF0000000. То есть, компилятор расширит ваш unsigned int знаком.

Здесь конечно нужно было использовать uintptr_t, поэтому как, как выяснилось, в стандарте C99 такого рода приведения типов implementation defined.

После того как мы наконец-то увидели поднятый 32-ой бит в TIR«e, стали искать как получить номер прерывания. Это оказалось довольно просто, хотя и совсем не так как на x86, это одно из отличий реализаций LAPIC. Для Эльбруса, чтобы достать номер прерывания, нужно залезть в специальный регистр LAPIC:

    #define APIC_VECT   (0xFEE00000 + 0xFF0)


где 0xFEE00000 — это базовый адрес регистров LAPIC.

На этом все, получилось подцепить и системный таймер и LAPIC таймер.

Заключение.


Информации, приведенной в первых двух технических частях статьи про архитектуру Эльбрус, достаточно для того, чтобы реализовать в любой ОС аппаратные прерывания и вытесняющую многозадачность. Собственно, приведенные скриншоты об этом свидетельствуют.
qmqz3pbvuvohscm_qon1edf5jno.png

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

Все, что написано в статье можно найти в репозитории Embox. Также можно собрать и запустить, если конечно имеется аппаратная платформа. Правда, для этого нужен компилятор, а его можно получить только в МЦСТ. Официальную документацию можно запросить там же.

© Habrahabr.ru