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

    Продолжаем исследовать Эльбрус путем портирования на него 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. Также можно собрать и запустить, если конечно имеется аппаратная платформа. Правда, для этого нужен компилятор, а его можно получить только в МЦСТ. Официальную документацию можно запросить там же.
    • +32
    • 4,4k
    • 4
    Embox
    56,00
    Открытая и свободная ОС для встроенных систем.
    Поделиться публикацией

    Похожие публикации

    Комментарии 4

      0

      Спасибо за интересную серию.


      Заметил (еще с первой статьи), что в макросах с assembler'ными вставками используются nop'ы с численным аргументом:


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

      #define E2K_ASM_FLUSH_CPU \
      flushr; \
      nop 2;  \
      flushc; \
      nop 3;

      Это "No OPeration" инструкция? Зачем она в приведенных макросах? Зачем ей аргумент?

        +1
        Это «No OPeration» инструкция? Зачем она в приведенных макросах? Зачем ей аргумент?

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

        Еще интересно разбирались ли вы с эльбрусовским защищенным режимом? Будете ли пробовать внедрять его поддержку в Embox?
        Было бы очень интересно почитать про него отдельную статью.

          0
          Да, конечно, планируется работа и с защищенном режимом, и с тегированной памятью, и с 128 битными указателями, у Эльбруса еще много фишек интересных. Но пока делаем стандартные вещи, для того чтобы уже можно было достаточно просто использовать в каких то практических задачах.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое