Pull to refresh
0
Embox
Открытая и свободная ОС для встроенных систем

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

Reading time8 min
Views7K
Продолжаем исследовать Эльбрус путем портирования на него 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 таймер.

Заключение


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



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

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

Articles

Change theme settings

Information

Website
emboxing.ru
Registered
Founded
Employees
2–10 employees
Location
Россия