Прерывания

Базовая статья — слайды доклада Krste Asanović

Общие сведения

  • Прерывание — сигнал, сообщающий процессору о наступлении какого-либо события.
    • асинхронны: могут произойти в любое время в любом месте выполнения программы (ср. исключения: могут возникнуть только при выполнении конкретных инструкций)
    • позволяют освободить cpu от активного ожидания: программа спокойно вычисляет (не отвлекаясь на опрос готовности устройства), а когда устройство наконец себя проявит, результаты его активности быстро обрабатываются
    • обрабатываются так же, как и другие события, — обработчиком прерываний (как правило, частью ядра ОС)

  • Проблемы возникающие при обработке прерываний:
    • распознавание природы прерывания — что за устройство и что с ним случилось
      • где-то должна появляться об этом информация (аппаратно!)
    • прерывания нужно быстро обработать
    • в одно и то же время может случиться несколько прерываний
      • про каждое из них надо знать, что оно случилось
  • Маскирование прерываний. Прерывания, в зависимости от возможности запрета, делятся на:
    • маскируемые — прерывания, которые можно запрещать установкой соответствующих битов в регистре маскирования прерываний;
    • немаскируемые — обрабатываются всегда, независимо от запретов на другие прерывания.
  • Приоритеты обслуживания прерываний: если выясняется, что произошло несколько, какое обрабатывать первым? А если за время обработки произойдёт ещё несколько?

Специфика RISC-V

Предварительные замечания

Для понимания организации процесса вычислений на архитектуре RISC-V, определим следующие понятия:

  • платформа (машина, platform, board и т.п.) содержит одно или несколько ядер (core) RISC-V, не RISC-V ядер, «примочки» (accelerators, ускоряющие работу ЦП или выполняющие специальные инструкции, например, DMA и прочие укорители В/В или аппаратное шифрование), память, устройства ввода-вывода и схемотехнику для эффективного взаимодействия всего этого друг с другом (interconnect structure). 1.1 RISC-V Hardware Platform Terminology

  • Ядро (core) - компонент содержащий независимое устройство выборки инструкций.

    • К ядру могу прилагаться сопроцессоры, управляемые непосредственно инструкциями из основной программы, но частично от процессора независимые (например, FPU)
  • HART - набор ресурсов, необходимых для выполнения потока вычислений RISC-V. Для нас важно, что:
    • одно ядро может поддерживать несколько HART (аппаратных потоков)1.1,

    • каждый HART ассоциирован с отдельным адресным пространством из 4096 регистров управления и состояния (CSR))10 Zicsr v2.0

    • сам HART выполняется в некотором окружении (environment), которое даже не обязано быть RISC-V (например, в эмуляторах типа RARS или QEMU)

Дополнительно о происхождении аппаратных потоков lithe-enabling-efficient-composition-of-parallel-libraries, Lithe Enabling Efficient.pdf

Направляющие идеи

  • Унификация обработки непредвиденных ситуаций уже была рассмотрена в лекции про exception и описана в 1.6 Exceptions, Traps, and Interrupts спецификации.

  • Уровни привилегий (M, H, S, U)
  • Регистры контроля и статуса (CSR), как-то *status, *cause, *tvec, *ie/*ip, *epc и некоторые другие на разных уровнях привилегий

    • Например, на уровне Machine регистр статуса называется mstatus, на уровне Hypervisor — (предположительно, потому что спецификация пока в проекте ☺) hstatus, на уровне Supervisor — sstatus, на уровне User — ustatus.

  • Специальные поля в регистрах *status для организации переходов между уровнями 3.1.6.1 Privilege and Global Interrupt-Enable Stack in mstatus register

  • Предусмотрен векторный режим вызова ловушек (возможен также вариант с двойной косвенной адресацией, но его нет в спецификациях)

В спецификации оставлены определения набор "минимально" необходимых аппаратных средств. Значительная часть задач возлагается на окружение, возможно и аппаратное, например контроллер прерываний (CLIC, PLIC).

Прерывания в RISC-V

В спецификации описаны три стандартных (Machine, Supervisor, User) и один дополнительный (Hypervisor — между Machine и Supervisor) уровни выполнения (привилегий). Уровни отличаются

  • Правами доступа к регистрам CSR одного и того же HART (например, кровень Machine можно писать во все RW-регистры всех уровней, а уровень User — только в свои, а некоторые и вообще не видит)
  • Тем, какие именно поля CSR управляют работой HART

Прерывания в RISC-V могут быть трёх типов:

  1. Внешние: приходят от периферийных устройств и направляются контроллером прерываний для обработки в HART
  2. Таймерные: приходят от процессора и его таймеров; возможно, завязаны на внешнее устройство-таймер (например, на уровне Machine есть прерывание от часов), но для каждого HART на более низких уровнях есть своё / свои

  3. Программные: приходят непосредственно из HART, который (если верить спецификации) просто взял и выставил соответствующий флаг в регистре *ip (interrupt pending)

Обработка:

  • Прерывание по умолчанию «ловится» уровнем Machine, но может быть делегировано (аппаратно, установкой специальных битов в CSR mideleg) на более низкий уровень

  • Основной механизм — т. н. вертикальная обработка, при котором прерывание, возникшее на более низком уровне, обрабатывается на более высоком

    • Если нужна горизонтальная, рекомендуется сначала «выпасть» на один из уровней выше, а уже оттуда передать управление обработчику обратно на исходный уровень

    • Однако в одноуровневых системах (типа RARS или небольших контроллеров) эта иерархия не нужна

Отложенные прерывания

Если несколько прерываний возникли актуально одновременно или во время обработки другого прерывания, они «накапливаются» в регистре *ip (в RARS — uip).

  • В RISC-V запоминается только тип прерывания:

    • EXTERNAL_INTERRUPT = 0x100 — внешнее
    • TIMER_INTERRUPT = 0x10 — таймерное
    • SOFTWARE_INTERRUPT = 0x1 — программное
  • Если ничего не сделать с этим регистром, то при выходе из ловушки (после *ret) выберется самый приоритетный тип прерывания (с наибольшим номером), и оно немедленно «приедет» в поток выполнения, т. е. на той же инструкции.

    • ⇒ (1) В обработчике надо проводить как можно меньше времени
    • ⇒ (2) В обработчике можно попробовать обработать сразу все прерывания, а затем сбросить *ip вручную

  • Пункт (2) — довольно рискованный, потому что в действительности могут отложиться несколько прерываний одного типа (например, от внешних устройств). Необходимо аппаратно определять, какое из ожидающих прерываний приоритетнее.

  • Вариант реализации (некоторое время был включён в базовую спецификацию RISC-V, затем вынесен в отдельный документ). Задействованы два вида устройств — контроллер прерываний со стороны процессора, и набор т. н. Interrupt Gateways (порталов) со стороны аппаратуры

    • Портал обеспечивает преобразование сигналов от устройств в унифицированный формат и их агрегацию (несколько сигналов могут быть преобразованы в один запрос прерывания к контроллеру)
    • Контроллер принимает решение, какое из прерываний приоритетнее и договаривается с конкретным HART относительно уровня его обработки
  • Повторный вход в ловушку (попытка вызвать обработчик во время выполнения кода другого обработчика) в спецификации RISC-V запрещен, но:
    • Вполне можно «эскалировать» уровень обработки: например, исключение в режиме Supervisor (какая-нибудь ошибка страницы в ядре) — это вполне рабочее исключение на уровне Hypervisor, и что бы в этот момент ядро не делало (а оно вполне могло выполнять ловушку уровня Supervisor), произойдёт выход в ловушку на уровне H — гипервизор сам разберётся, что делать с глючным ядром под его управлением.

    • В одноуровневых системах всё-таки возникает необходимость в повторном входе, как минимум между прерываниями разного типа. Например, обрабатывая прерывание по таймеру, которое не обязано быть супер точным, важна именно последовательноть, мы можем внезапно захотеть обработать внешнее прерывание от критически важного аппаратного устройства (датчик давления в контроллере котла!). В этом случае приходится изобретать дополнительные пространства адресов для аппаратного сохранения и восстановления контекста, иначе вот этот двойной вызов может всё испортить (спасибо @COKPOWEHEU за примечание)

Алгоритм обработки — упрощенно

* Для старта работы с прерываниями нужно:

  • сохранить адрес обработчика в соответствующем *tvec

  • разрешить наблюдение за нужным источником в регистре *ie

  • глобально разрешить, в регистрах *status поднять соответствующие биты.

  • При возникновении события прерывания происходит следующее:
    • поток выполнения приостанавливается
    • в регистр *epc сохраняется счетчик команд

    • в регистр *cause заносится код причины

    • устанавливается бит в регистре ожидания прерывания *ip.

      • если несколько прерываний ожидают обработки и разрешены одновременно, то порядок их обслуживания определяется фиксированным приоритетом, чем выше бит, тем выше приоритет.
    • управление передается на адрес из *tvec, а в случае векторного режима на BASE + 4* Cause

  • Обработать
  • Восстановить состояние CSR, перейти к исполнению прерванного потока в его режиме привилегий.

Interrupt Quick Reference

Tue0900_RISCV-20160712-Interrupts.pdf

Обработчик прерываний RARS

Прерывания, в отличие от исключений, могут возникать в произвольное время (например, прерывание ввода зависит от того, когда человек нажал на кнопку). Прерывания в RARS обрабатываются тем же кодом, что и исключения — специальным обработчиком.

Адрес обработчика хранится в utvec.

Регистр ustatus:

bits

31-2

3

2-1

0

target

UPIE

UIE

  • UPIE — User Previous Interrupt Enable - устанавливается автоматически при входе в ловушку; предотвращает повторный вход.

  • UIE — User Interrupt Enable - глобальное разрешение обработчики прерываний (0 - отключить)

    • Выполнение кода и переход на обработчик выполняться не будет. В RARS автоматически отключается при наступлении события прерывания.

Регистр ucause:

bits

31

30-3

3 -0

target

Interrupt

unused

Exception code

  • Int = 1, если прерывание
  • Exception code — код исключения или источник прерывания:
    • 0 — Программное прерывание
    • 4 — Таймерное прерывание; уточнённый источник прерывания хранится в utval:

      • 0x100 — прерывание раз в 30 инструкций от «Digital Lab Sim»
      • 0x10 — срабатывание таймера «Timer Tool»
    • 8 — Внешнее прерывание; уточнённый источник прерывания хранится в utval:

      • 0x40 — прерывание ввода с клавиатуры «Keyboard And Display MMIO Simulator»
      • 0x80 — прерывание готовности вывода «Keyboard And Display MMIO Simulator»
      • 0x200 — прерывание с цифровой клавиатуры «Digital Lab Sim»

При обработке прерывания:

  • Нужно сохранять все используемые регистры, включая t*; можно воспользоваться регистром uscrsatch

  • Можно (с большой оглядкой) пользоваться стеком.
    • Можно предусмотреть отдельный и пользоваться им (тогда sp тоже необходимо сохранять и восстанавливать)

  • Нужно различать исключения (поле Int регистра ucause ненулевое) и прерывания (поле Int нулевое)

    • возврат из исключения по uret требует прибавить 4 к значению uepc (ошибочную инструкцию выполнять повторно обычно не надо)

    • возврат из прерывания по uret не требует увеличения uepc (инструкция по этому адресу ещё не выполнена)

  • В RARS автоматически (по спецификации) запрещается обрабатывать повторный вход в обработчик
    • Значит, в обработчике надо проводить как можно меньше времени: глобально прерывания запрещены, то можно пропустить события.

    • Если в регистр uip приехало несколько битов, значит, произошло несколько прерываний, и все надо обработать (или игнорировать)

  • Перед выходом из обработчика можно очистить регистр ucause (старший бит и тип прерывания/исключения), если мы не хотим, чтобы основная программа догадалась о том, что прерывание вообще было

    • При возникновении следующего прерывания ucause будет перезаписан

    • Бит UPIE в регистре ustatus очистится, а бит UIE поднимется инструкцией uret

Значения полей в регистрах uie и uip (структура их одинакова):

31-9

8

7-5

4

3-1

0

UEI

UTI

USI

  • UEI — user external interrupt
  • UTI — user timer interrupt
  • USI — user software interrupt

Эта таблица соответствует устаревшему расширению N спецификации.

Программа, использующая прерывания, должна «настроить прерывания и устройства»:

  • сохранить адрес ловушки в регистре utvec,

  • записать 1 во все нужные позиции маски прерываний в uie,

  • выставить в 1 бит глобального разрешения прерываний (ustatus)

  • перевести используемые внешние устройства в режим работы по прерыванию

На примере «Консоли RARS»

  •    1         li      t0 2 
       2         li      t1 0xffff0000       # переключить клавиатуру в режим работы по прерываниям
       3         sw      t0 (t1)
       4 
       5         la      t0 handler
       6         csrw    t0 utvec            # адрес ловушки
       7 
       8         li      t0 0x100
       9         csrw    t0 uie              # включить внешние прерывания
      10 
      11         csrsi   ustatus 1           # разрешить обработку вех прерываний
    

Сам обработчик расположен по адресу, сохраненному в utvec , таким образом, обычно состоит из следующих частей:

  • Сохранение всех регистров
  • Вычисление типа исключений (0 — прерывание)
    • Переход на обработчик соответствующего исключения или на обработчик прерываний
  • Обработчик прерываний:
    • Выяснение источника прерывания и анализ списка отложенных прерываний (ucause и utval, uip)

    • Обработка или сброс всех случившихся прерываний (порядок определяется программно)
  • Обработчик исключения
    • Выяснение природы исключения (ucause)

    • Обработка исключения :)

    • Вычисление нового uepc

  • Восстановление всех регистров
  • uret

Замечание: как и во время обработки прерывания, во время системного вызова прерывания или вообще запрещены, или накапливаются (делаются «Pending») без вызова обработчика. Поэтому задача обработчика — как можно быстрее принять решение, что делать с прерыванием и вернуть управление пользовательской программе.

  • В обработчике выполняются только действия, необходимые для получения данных по прерыванию (чтение регистров, управление устройствами и т. п.)
  • Вся логика обработки данных остаётся в программе пользователя
  • При соблюдении некоторой дисциплины программирования можно вообще запретить пользовательской программе обращаться к внешним устройствам, оставив эти операции ядру, обрабатывающему прерывания

Пример: консоль RARS

Консоль RARS («Keyboard and Display MMIO Simulator») — воображаемое устройство, осуществляющее побайтовый ввод и вывод. Верхнее окошко — «дисплей», куда выводятся байты, а нижнее — «клавиатура» (для удобства набираемый на клавиатуре текст отображается в этом окошке).

LecturesCMC/ArchitectureAssembler2022/09_Interrupts/Console_RARS.png

Консоль имеет следующие регистры ввода-вывода

0xffff0000

RcC

Управляющий регистр ввода

RW

0 бит — готовность, 1 бит — прерывание

0xffff0004

RcD

Регистр данных ввода

R

введённый байт

0xffff0008

TxC

Управляющий регистр вывода

RW

0 бит — готовность, 1 бит — прерывание

0xffff000c

TxD

Регистр данных вывода

W

необязательные координаты курсора, байт для вывода

Работа посредством поллинга

Операции ввода или вывода в консоли возможны только если бит готовности равен 1. Если бит готовности нулевой в управляющем регистре ввода, значит, клавиша ещё не нажата, а если в управляющем регистре вывода — символ всё ещё выводится, следующий выводить нельзя (ну медленное устройство, в жизни так сплошь и рядом!). Как обычно, устройство заработает только после нажатия кнопки «Connect to RARS». Простой пример чтения с клавиатуры при помощи поллинга. Удобно рассматривать с низкой скоростью работы эмулятора (3-5 тактов в секунду).

   1 loop:   lb      t0 0xffff0000          # готовность ввода
   2         andi    t0 t0 1                # есть?
   3         beqz    t0 loop                # нет — снова
   4         lb      a0 0xffff0004          # считаем символ
   5         li      a7 11                  # выведем его
   6         ecall
   7         b       loop

Чуть более сложный пример с выводом, в котором видна проблема переполнения.

  • Согласно документации, вывод начинает работать (точнее, бит готовности выставляется в первый раз) только если нажать «Reset» или «Сonnect to program» после запуска программы. Это — тоже пример из «реальной жизни»: при включении питания многие устройства находятся в неопределённом состоянии и требуют инициализации.

  • Чего в документации не написано — так это того, что бит готовности вывода в действительности доступен на запись, и мы можем в самом начале программы выставить его в 1

   1         li      t0 1
   2         sb      t0 0xffff0008 t1
   3         li      t1 0
   4 loop:   beqz    t1 noout                # выводить не надо
   5 loopi:  lb      t0 0xffff0008           # готовность вывода
   6         andi    t0 t0 1                 # есть?
   7         beqz    t0 loopi                # а надо! идём обратно
   8         sb      t1 0xffff000c t2        # запишем байт
   9         li      t1 0                    # обнулим данные
  10 noout:  lb      t0 0xffff0000           # готовность ввода
  11         andi    t0 t0 1                 # есть?
  12         beqz    t0 loop                 # нет — снова
  13         lb      t1 0xffff0004           # считаем символ
  14         b       loop      

Выставляя ползунок «Delay Length» в большое значение, мы заставляем консоль долго не давать готовности по выводы (в течение, скажем, 20 инструкций). Пока программа находится в половинке вывода (цикл loopi:), она не успевает за вводом.

Задание: пронаблюдать, что происходит с регистрами ввода, когда пользователь много нажимает на клавиатуре, а программа не успевает считать.

Работа по прерываниям

Главное свойство консоли: она может инициировать прерывания в момент готовности ввода или вывода. Устанавливая в 1 первый бит в регистре RcC, мы разрешаем консоли возбуждать прерывание всякий раз, как пользователь нажал на клавишу. Устанавливая в 1 первый бит регистра TxC, мы разрешаем прерывание типа «окончание вывода». И в том, и в другом случае прерывание возникает одновременно с появлением бита готовности (нулевого) в соответствующем регистре. Таким образом, вместо постоянного опроса регистра мы получаем однократный вызов обработчика в подходящее время. Рассмотрим пример очень грязного обработчика прерывания от клавиатуры, который ничего не сохраняет, не проверяет причину события и номер прерывания. Зато по этому коду хорошо видна асинхронная природа работы прерывания. Рекомендуется выставить ползунок RARS «Run speed» в низкое значение (например, 5 раз в секунду).

   1         li      a0 2                    # разрешим прерывания от клавиатуры
   2         sw      a0 0xffff0000 t0
   3         la      t0 handler 
   4         csrw    t0 utvec                # Инициализируем ловушку
   5         csrsi   uie 0x100               # Разрешим внешние прерывания
   6         csrsi   ustatus 1               # Включим обработку прерываний
   7         li      a0 0
   8 loop:   beqz    a0 loop                 # вечный цикл
   9         li      t0 0x1b
  10         beq     a0 t0 done              # ESC — конец
  11         li      a7 11                   # выведем символ
  12         ecall
  13         li      a0 0                    # затрём a0
  14         j       loop
  15 done:   li      a7 10
  16         ecall
  17 
  18 handler:                                # ОЧЕНЬ грязный код обработчика
  19         lw      a0 0xffff0004           # считаем символ
  20         uret

В примере ниже «полезные вычисления» делает подпрограмма sleep (на самом деле ничего полезного), время от времени проверяя содержимое ячейки 0 в глобальной области. Это лучше, чем модифицировать регистр или метку, определяемую пользовательской программой. Обработчик клавиатурного прерывания (для простоты — не проверяя, клавиатурное ли оно) записывает в эту ячейку код нажатой клавиши.

   1 .text
   2 .globl main
   3 main:   la      t0 handle
   4         csrw    t0 utvec
   5         csrsi   uie 0x100
   6         csrsi   ustatus 1              # enable all interrupts
   7 
   8         li      a0 2                   # enable keyboard
   9         sw      a0 0xffff0000 t0
  10 
  11 here:   jal     sleep
  12         lw      a0 (gp)                # print key stored in (gp)
  13         li      t0 0x1b
  14         beq     a0 t0 done             # ESC terminates
  15         beqz    a0 here                # No input
  16         li      a7 1
  17         ecall
  18         li      a0 '\n'
  19         li      a7 11
  20         ecall
  21         sw      zero (gp)              # Clear input
  22         b       here
  23 done:   li      a7 10
  24         ecall
  25 
  26 .eqv    ZZZ     1000
  27 sleep:  li      t0 ZZZ                 # Do nothing
  28 tormo0: addi    t0 t0 -1
  29         blez    t0 tormo1
  30         b       tormo0
  31 tormo1: ret
  32 
  33 handle: csrw    t0 uscratch
  34         sw      a7 sr1  t0            # We need to use these registers
  35         sw      a0 sr2  t0            # not using the stack
  36 
  37         csrr    a0 ucause             # Cause register
  38         srli    a0 a0 31              # Get interrupt bit
  39         beqz    a0 hexc               # It was an exception
  40                                       # Assume only I/O interrupts enables
  41         lw      a0 0xffff0004         # get the input key
  42         sw      a0 (gp)               # store key
  43         li      a0 '.'                # Show that we handled the interrupt
  44         li      a7 11
  45         ecall
  46         b       hdone
  47 
  48 hexc:   csrr    a7 uepc               # No exceptions in the program, but just in case of one
  49         addi    a7 a7 4               # Return to next instruction
  50         csrw    a7 uepc
  51 
  52 hdone:  lw      a7 sr1                # Restore other registers
  53         lw      a0 sr2
  54         csrr    t0 uscratch
  55         uret
  56 
  57 .data
  58 sr1:     .word 10
  59 sr2:     .word 11
  • Если запускать этот пример на пониженной скорости, надо поменять значение ZZZ на меньшее, например, на 10, иначе вывода можно и не дождаться

  • В обработчике из этого примера есть также не используемая часть, которая определяет, прерывание это или исключение (и соответственно не изменяет или изменяет uepc) — это буквально решение прошедшего домашнего задания ;)

Прерывание готовности вывода

Как самое настоящее устройство вывода, консоль RARS выводить байты тоже медленно. Пока «байт выводится», нулевой бит регистра TxCTxC:0 равен нулю, а когда устройство готово выводить следующий байт, он равен 1. Если выставить в 1 первый бит этого регистра, TxC:1, консоль будет порождать прерывание всякий раз, когда она готова выводить.

В результате мы имеем две ситуации:

  1. Необходимо вывести байт, устройство готово — байт можно записывать в TxD непосредственно

  2. Необходимо вывести байт, устройство не готово — байт нужно кода-то отложить, и его запишет обработчик прерывания готовности вывода

Самая простая реализация — проверить TxC:0, и если готовность есть, записать байт в TxD, а если её нет, записать в специальный буфер вывода, откуда его возьмёт обработчик. Мы можем надеяться на то, что прерывание готовности произойдёт, потому что сейчас-то готовности нет, а когда-то точно будет.

Однако это выглядит некрасиво: то ли программа у нас занимается записью в TxD, то ли ловушка. Однако другие варианты более сложны в реализации:

  • Например, мы можем организовать опрос буфера вывода по таймеру, по наличии там данных выводить их из ловушки. Программа при этом просто записывает данные в буфер.
  • Или же мы можем положить байт в буфер, самостоятельно вызвать программное прерывание, обработчик которого попытается вывести байт сразу, если готовность есть, и ничего не будет делать, если её нет — выведет, когда сработает прерывание готовности.

Для вызова программного прерывания достаточно записать в uip бит USI, то есть нулевой.

Чтобы не усложнять пример ниже, для ввода в нём используется поллинг, а вот для вывода — последняя из описанных процедур (запись в буфер и программное прерывание)

   1 .eqv    RcC     0xffff0000
   2 .eqv    RcD     0xffff0004
   3 .eqv    TxC     0xffff0008
   4 .eqv    TxD     0xffff000c
   5 .text
   6 .globl main
   7 main:   la      t0 handle
   8         csrw    t0 utvec            # Ловушка
   9         csrsi   uie 0x101           # Обработка внешних и программных прерываний
  10         li      t1 3
  11         sw      t1 TxC t0           # Прерывание готовности вывода и «reset»
  12         csrsi   ustatus 1           # Разрешение обработки
  13 
  14         li      s1 27               # ESC
  15 loop:   lb      t0 RcC              # готовность ввода
  16         andi    t0 t0 1             # если нет,
  17         beqz    t0 loop             # ждём дальше
  18         lb      t0 RcD              # введём байт
  19         beq     t0 s1 done          # ESC
  20         sb      t0 stdout t1        # заполним буфер
  21         csrsi   uip 1               # Программное прерывание
  22         b       loop
  23 done:   li      a7 10
  24         ecall
  25         
  26 .data
  27 stdout: .word   0
  28 h.t1:   .word   0
  29 .text
  30 handle: csrw    t0 uscratch         # сохраним t0
  31         sw      t1 h.t1 t0          # сохраним t1
  32         csrr    t0 ucause           # рассмотрим источник прерывания
  33         andi    t0 t0 0x8           # Клавиатура?
  34         bnez    t0 h.out            # не глядя считаем, что готовность вывода
  35 h.soft: lw      t0 TxC              # не глядя считаем, что программное
  36         andi    t0 t0 1             # смотрим готовность
  37         beqz    t0 h.exit           # нет? потом выведем!
  38 h.out:  lb      t0 stdout           # готовность есть (по прерыванию или по проверке)
  39         beqz    t0 h.exit           # но буфер пуст, ничего не делаем
  40         sb      t0 TxD t1           # иначе записываем его
  41         sb      zero stdout t1      # очищаем буфер
  42 h.exit: lw      t1 h.t1             # вспоминаем t1
  43         csrr    t0 uscratch         # вспоминаем t0
  44         uret
  • В этом примере отсутствует код для различения прерываний и исключений
  • Нам достаточно того, что аппаратное прерывание может прийти только по готовности ввода, а программное подразумевает только операцию вывода

Помните домашнее задание с фальшивым syscall-ом? Программное прерывание — официальный способ достичь того же эффекта!

Отложенные прерывания

Теперь рассмотри пример отложенных прерываний. Разрешим прерывание от клавиатуры и будем вдобавок порождать достаточное количество программных прерываний. Пронаблюдаем содержимое регистра uip, в котором отложатся все ещё необработанные к моменту входа в ловушку события, а заодно ucause — во время обработки какого события прервания оказались отложенными.

   1 .text
   2 .macro  printx  %char # число для вывода уже в a0
   3         li      a7 34
   4         ecall
   5         li      a0 %char
   6         li      a7 11
   7         ecall
   8 .end_macro
   9 
  10 .globl main
  11 main:   la      t0 handle       # Устанавливаем обработчик
  12         csrw    t0 utvec
  13         csrsi   uie 0x101       # Включаем программные и внешние прерывания
  14         li      a0 2            # Включаем прерывание от клавиатуры
  15         sw      a0 0xffff0000 t0
  16         csrsi   ustatus 1       # Разрешаем обработку прерываний
  17 
  18 here:   csrsi   uip 1           # Вызываем программное прерывание
  19         lw      a0 (gp)         # Смотрим, были ли отложенные прерывания
  20         beqz    a0 here
  21         printx  ':'             # Выводим uip
  22         lw      a0 4(gp)
  23         printx  '\n'            # Выводим ustatus
  24         sw      zero (gp)       # Затираем сведения
  25         b       here
  26 
  27 .data
  28 h.t1:   .word 0
  29 .text
  30 handle: csrw    t0 uscratch
  31         csrr    t0 uip          # проверим отложенные прерывания
  32         beqz    t0 h.noip       # если были
  33         sw      t0 (gp)         # запомним, какие (uip)
  34         csrr    t0 ucause       
  35         sw      t0 4(gp)        # и какой был ucause
  36 h.noip: csrr    t0 uscratch
  37         uret

Варианты вывода:

  • 0x00000001:0x80000008 (отложено программное прерывание, обрабатывается клавиатурное)

    • Возникает на инструкции (19) «lw      a0 (gp)»

    • Происходит, когда при выполнении этой инструкции возникает два прерывания — для обработки выбирается внешнее, более приоритетное.

  • 0x00000100:0x80000000 (отложено клавиатурное прерывание, обрабатывается программное)

    • Возникает на инструкции (19) «lw      a0 (gp)»

    • Происходит, когда в процессе обработки программного прерывания появляется клавиатурное
  • 0x00000100:0x80000008 (отложено клавиатурное прерывание, обрабатывается тоже клавиатурное)

    • Возникает на произвольной инструкции
    • Происходит, когда в процессе обработки клавиатурного прерывания появляется ещё одно клавиатурное (например, на медленном запуске)

Упражнение: добавьте в пример сохранение и вывод uepc

LecturesCMC/ArchitectureAssemblerProject/19_Interrupts (последним исправлял пользователь FrBrGeorge 2024-08-23 21:46:39)