Прерывания

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Interrupt Quick Reference

Tue0900_RISCV-20160712-Interrupts.pdf

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

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

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

Регистр ustatus:

bits

31-2

3

2-1

0

target

UPIE

UIE

Регистр ucause:

bits

31

30-3

3 -0

target

Interrupt

unused

Exception code

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

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

31-9

8

7-5

4

3-1

0

UEI

UTI

USI

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

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

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

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

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

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

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

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

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

   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

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

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

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

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

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

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

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

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

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

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

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

Значения(ID) в регистре utval RARS:

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

Теперь рассмотри пример отложенных прерываний. Разрешим прерывание от клавиатуры и будем вдобавок порождать достаточное количество программных прерываний. Пронаблюдаем содержимое регистра 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

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

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

Д/З

У Консоли RARS есть задокументированное свойство, которого не было на лекции:

When ASCII 7 (bell) is stored in the Transmitter Data register, the cursor in the tool's Display window will be positioned at the (X,Y) coordinate specified by its high-order 3 bytes. Place the X position (column) in bit positions 20-31 of the Transmitter Data register and place the Y position (row) in bit positions 8-19.  The cursor is not displayed but subsequent transmitted characters will be displayed starting at that position. Position (0,0) is at upper left. Why did I select the ASCII Bell character?  Just for fun!

Т. е. если выводить на экран консоли машинное слово, у которого:

Вместо рисования чего-либо изменится знакоместо, в которое в следующий раз будет выводиться очередной байт. Все стандартные терминалы и эмуляторы терминалов так умеют! Но у всех эти вправляющие символы выглядят по-разному…

Во всех задачах допустимо использовать поллинг готовности вывода.

<!> В качестве бонуса можно попробовать реализовать посимвольный вывод из буфера по прерыванию готовности вывода, но это довольно сложно.

  1. (подготовительная) Звёздное небо-2. Написать программу, которая заполняет экран консоли случайными маленькими латинскими буквами в случайном порядке. Для простоты использовать ecall 42 и поллинг готовности вывода.

  2. (подготовительная) Бегущая звёздочка. Написать программу, которая выводит на экране консоли символ «*», и этот символ «движется» от левого края к правому; на краю консоли программа останавливается.

    • «Движение» — это вывод по координатам x+1, y символа «*», а затем вывод по координатам «x, y пробела.

    • Движение происходит согласно таймерному прерыванию (используется и консоль, и Timer Tool)
      • Таймер медленный — не чаще 5 раз в секунду
  3. (собственно задача) Написать программу управления человечком на консоли:
     O
    -+-
    / \
    • Изначально человечек стоит по центру консоли.
    • Если нажата одна из пяти клавиш управления, он начинает двигаться в соответствующую сторону или останавливается
      • Вариант управления (можно другой):
            8
        4 ← 5 → 6
            2
    • Движение происходит согласно таймерному прерыванию (используется и консоль, и Timer Tool)
      • Таймер медленный — не чаще 5 раз в секунду
    • Ввод происходит по прерыванию от клавиатуры консоли
    • На границе экрана человечек останавливается (или появляется с другой стороны — как вам удобнее)
    • Допустимо в качестве «движения» сначала заполнять человечка пробелами, а затем выводить нового в новом месте
      • <!> (бонус) можно предусмотреть затирание только того места, откуда человечек ушёл — тогда экран не будет моргать

LecturesCMC/ArchitectureAssembler2022/09_Interrupts (последним исправлял пользователь FrBrGeorge 2022-04-17 21:52:44)