Продолжаем исследовать Эльбрус путем портирования на него Embox.Данная статья является второй частью технической статьи об архитектуре Эльбрус. В первой части речь шла о стеках, регистрах и так далее. Перед прочтением этой части рекомендуем изучить первую, поскольку в ней рассказывается о базовых вещах архитектуры Эльбрус. В этой части речь пойдет о таймерах, прерываниях и исключениях. Это, опять же, не официальная документация. За ней следует обращаться к разработчикам Эльбруса в МЦСТ.
Приступая к изучению Эльбруса, нам хотелось побыстрее запустить таймер, ведь, как вы понимаете, вытесняющая многозадачность без него не работает. Для этого казалось достаточно реализовать контроллер прерываний и сам таймер, но мы столкнулись с
Код следующий:
#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, определяетсяНа практике же, мы видим глубину = 1, и поэтому используем только регистр TIR0.
TIR_NUM macro, равным количеству стадий конвейера процессора, требуемых для
выдачи всех возможных особых ситуаций. TIR_NUM = 19;”
Специалисты в МЦСТ нам пояснили, что все правильно, и для «точных» прерываний будет только 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. Также можно собрать и запустить, если конечно имеется аппаратная платформа. Правда, для этого нужен компилятор, а его можно получить только в МЦСТ. Официальную документацию можно запросить там же.
