Прерывания

Базовая статья — слайды доклада 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)

Обработка:

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

Если несколько прерываний возникли актуально одновременно или во время обработки другого прерывания, они «накапливаются» в регистре *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») — воображаемое устройство, осуществляющее побайтовый ввод и вывод. Верхнее окошко — «дисплей», куда выводятся байты, а нижнее — «клавиатура» (для удобства набираемый на клавиатуре текст отображается в этом окошке).

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

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

   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 .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

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

Упражнение: добавьте в пример сохранение и вывод 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. (у этой задачи нет тестов: она требует интерактива с пользователем)

    EJudge: TextRunner 'Бегущий человечек'

    Написать программу управления человечком на консоли:

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

    Input:

    Последовательнcть нажатий в консоли
    Output:

    Человечек бегает

У меня получилось так:

LecturesCMC/ArchitectureAssembler2024/10_Interrupts (последним исправлял пользователь FrBrGeorge 2024-04-20 22:33:29)