Прерывания
Базовая статья — слайды доклада Krste Asanović
Общие сведения
Прерывание — сигнал, сообщающий процессору о наступлении какого-либо события.
- асинхронны: могут произойти в любое время в любом месте выполнения программы (ср. исключения: могут возникнуть только при выполнении конкретных инструкций)
- позволяют освободить cpu от активного ожидания: программа спокойно вычисляет (не отвлекаясь на опрос готовности устройства), а когда устройство наконец себя проявит, результаты его активности быстро обрабатываются
обрабатываются так же, как и другие события, — обработчиком прерываний (как правило, частью ядра ОС, то есть минимум одним уровнем выше — для доступа к памяти и периферии)
Проблемы возникающие при обработке прерываний:
- распознавание природы прерывания — что за устройство и что с ним случилось
- где-то должна появляться об этом информация (аппаратно!)
- прерывания нужно быстро обработать
- в одно и то же время может случиться несколько прерываний
- про каждое из них надо знать, что оно случилось
Маскирование прерываний. Прерывания, в зависимости от возможности запрета, делятся на:
- маскируемые — прерывания, которые можно запрещать установкой соответствующих битов в регистре маскирования прерываний;
(нажмите «Комментарии» в шапке страницы, чтобы прочитать комментарии от COKPOWEHEU)
немаскируемые — обрабатываются всегда, независимо от запретов на другие прерывания.
Вытеснение прерываний: если во время обработки некоторого прерывания возникло ещё одно прерывание, более срочное, его нужно немедленно обработать (например, таймер таймером, а пакет от сетевой карты надо 💯 принять).
Специфика RISC-V
Предварительные замечания
Для понимания организации процесса вычислений на архитектуре RISC-V, определим следующие понятия:
Платформа (машина, platform, board и т.п.) содержит одно или несколько ядер (core) RISC-V, не RISC-V ядер, «примочки» (accelerators, ускоряющие работу ЦП или выполняющие специальные инструкции, например, DMA и прочие укорители В/В или аппаратное шифрование), память, устройства ввода-вывода и схемотехнику для эффективного взаимодействия всего этого друг с другом (interconnect structure). RISC-V Hardware Platform Terminology
Ядро (core) - компонент содержащий независимое устройство выборки инструкций.
К ядру могу прилагаться сопроцессоры, управляемые непосредственно инструкциями из основной программы, но частично от процессора независимые (например, FPU)
Hart — набор ресурсов, необходимых для выполнения потока вычислений RISC-V. Для нас важно, что:
каждый HART ассоциирован с отдельным адресным пространством из 4096 регистров управления и состояния (CSR)Zicsr
одно ядро (core) на уровне M может поддерживать несколько аппаратных потоков, собственно Hardware Threads
на уровне U hart-ом является адресное пространство и CSR отдельного процесса, а уж как они преобразуются в аппаратные потоки — дело более высокого уровня)
Сам hart выполняется в некотором окружении (software execution environment), которое даже не обязано быть RISC-V (например, в эмуляторах типа RARS или QEMU)
Дополнительно о происхождении аппаратных потоков lithe-enabling-efficient-composition-of-parallel-libraries, Lithe Enabling Efficient.pdf
Направляющие идеи
Унификация обработки непредвиденных ситуаций уже была рассмотрена в лекции про exception и описана в спецификации Exceptions, Traps, and Interrupts.
h Уровни привилегий (Machine → Hypervisor → Supervisor → User)
Регистры контроля и статуса (CSR), как то *status, *cause, *tvec, *ie/*ip, *epc и некоторые другие на разных уровнях привилегий
Например, на уровне Machine регистр статуса называется mstatus, на уровне Hypervisor — (предположительно, потому что спецификация пока в проекте ☺) hstatus, на уровне Supervisor — sstatus, на уровне User — ustatus.
Специальные поля в регистрах *status для организации переходов между уровнями 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 могут быть трёх типов:
- Внешние: приходят от периферийных устройств и направляются контроллером прерываний для обработки в HART
Таймерные: приходят от процессора и его таймеров; возможно, завязаны на внешнее устройство-таймер (например, на уровне Machine есть прерывание от часов), но для каждого HART на более низких уровнях есть своё / свои
Программные: приходят непосредственно из HART, который (если верить спецификации) просто взял и выставил соответствующий флаг в регистре *ip (interrupt pending)
Обработка:
Прерывание по умолчанию «ловится» уровнем Machine, но может быть делегировано (аппаратно, установкой специальных битов в CSR mideleg) на более низкий уровень
Основной механизм — т. н. вертикальная обработка, при котором прерывание, возникшее на более низком уровне, обрабатывается на более высоком
Если нужна горизонтальная, рекомендуется сначала «выпасть» на один из уровней выше, а уже оттуда передать управление обработчику обратно на исходный уровень
Повторный вход в ловушку (double trap, попытка вызвать обработчик во время выполнения кода обработчика) в спецификации RISC-V в целом запрещен, но:
- Можно эскалировать обработку на уровень выше по принципу «там разберутся».
Например, исключение в режиме Supervisor (какая-нибудь ошибка страницы в ядре) — это вполне рабочее исключение на уровне Hypervisor, и что бы в этот момент ядро не делало (а оно вполне могло выполнять ловушку уровня Supervisor), произойдёт выход в ловушку на уровне H — гипервизор сам разберётся, что делать с глючным ядром под его управлением. Или не глючным, потому что это был т. н. overcommit.
- Однако уровней прерывания обычно сильно больше, чем уровней привилегий процессора (обработка прерываний — одна из основных задач микроконтроллеров)
В некоторых случаях (например, при обработке сбоев аппаратуры) double trap предлагается обрабатывать на уровне M по определённым правилам; в разработке также расширение Ssdbltrp, описывающее повторный вход в обработчик на уровне S.
- Можно эскалировать обработку на уровень выше по принципу «там разберутся».
Вытеснение прерываний в реальных архиектурах может требоваться чаще:
Можно по конвенции потребовать в обработчике явно сохранять контекст (куда? — а вот в зависимости от приоритета же) и немедленно выставлять разрешение прерываний в mie. Аппаратура будет сравнивать уровень возникшего прерывания с уровнем обрабатывемого, и в случае вытеснения повторно входить в обработчик. При выходе из обработчика надо будет эту цепочку распутать.
Можно не мудрствуя лукаво под каждый уровень прерывания выделать свой, допустим, mepc (а может быть, и другие регистры)
- …
- В одноуровневых системах (типа RARS или очень маленьких контроллеров) эта иерархия отсутствует
Отложенные прерывания
Если несколько прерываний возникли актуально одновременно или во время обработки другого прерывания, они «накапливаются» в регистре *ip (в RARS — uip).
В RISC-V запоминается только тип прерывания:
EXTERNAL_INTERRUPT = 0x100 — внешнее
TIMER_INTERRUPT = 0x10 — таймерное
SOFTWARE_INTERRUPT = 0x1 — программное
Если ничего не сделать с этим регистром, то при выходе из ловушки (после *ret) выберется самый приоритетный тип прерывания (с наибольшим номером), и оно немедленно «приедет» в поток выполнения, т. е. на той же инструкции.
⇒ В обработчике надо проводить как можно меньше времени
⇒ В обработчике можно попробовать обработать сразу все прерывания, а затем сбросить *ip вручную
(нажмите «Комментарии» в шапке страницы, чтобы прочитать комментарии от COKPOWEHEU)
Пункт (2) — довольно рискованный, потому что в действительности могут отложиться несколько прерываний одного типа (например, от внешних устройств). Необходимо аппаратно определять, какое из ожидающих прерываний приоритетнее.
Вариант реализации (некоторое время был включён в базовую спецификацию RISC-V, затем вынесен в отдельный документ). Задействованы два вида устройств — контроллер прерываний со стороны процессора, и набор т. н. Interrupt Gateways (порталов) со стороны аппаратуры
- Портал обеспечивает преобразование сигналов от устройств в унифицированный формат и их агрегацию (несколько сигналов могут быть преобразованы в один запрос прерывания к контроллеру)
- Контроллер принимает решение, какое из прерываний приоритетнее и договаривается с конкретным HART относительно уровня его обработки
Алгоритм обработки — упрощенно
* Для старта работы с прерываниями нужно:
сохранить адрес обработчика в соответствующем *tvec
разрешить наблюдение за нужным источником в регистре *ie
глобально разрешить, в регистрах *status поднять соответствующие биты.
- При возникновении события прерывания происходит следующее:
- поток выполнения приостанавливается
в регистр *epc сохраняется счетчик команд
в регистр *cause заносится код причины (в частности, номер возникшего прерывания)
устанавливается бит в регистре ожидания прерывания *ip.
- если несколько прерываний ожидают обработки и разрешены одновременно, то порядок их обслуживания определяется фиксированным приоритетом: чем выше бит, тем выше приоритет.
управление передается на адрес из *tvec, а в случае векторного режима на BASE + 4* Cause
- Обработать
Восстановить состояние CSR, перейти к исполнению прерванного потока в его режиме привилегий.
Tue0900_RISCV-20160712-Interrupts.pdf
Обработчик прерываний RARS
Прерывания, в отличие от исключений, могут возникать в произвольное время (например, прерывание ввода зависит от того, когда человек нажал на кнопку). Прерывания в RARS обрабатываются тем же кодом, что и исключения — специальным обработчиком.
Адрес обработчика хранится в utvec.
Регистр ustatus:
bits |
31-2 |
3 |
2-1 |
0 |
target |
|
UPIE |
|
UIE |
UIE — User Interrupt Enable - глобальное разрешение обработчики прерываний (0 - отключить)
В RARS автоматически отключается при входе в обработчик.
UPIE — User Previous Interrupt Enable - устанавливается автоматически при входе в ловушку. Описывает состояние UIE перед входом в ловушку и копируется туда во время uret.
Регистр 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 будет перезаписан
После выполнения uret бит UPIE (предыдущее разрешение прерываний) в регистре ustatus скопируется в бит UIE, после чего обнулится.
Значения полей в регистрах 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»
Сам обработчик расположен по адресу, сохраненному в utvec , таким образом, обычно состоит из следующих частей:
- Сохранение всех регистров
- Вычисление типа исключений (0 — прерывание)
- Переход на обработчик соответствующего исключения или на обработчик прерываний
- Обработчик прерываний:
Выяснение источника прерывания и анализ списка отложенных прерываний (ucause и utval, uip)
- Обработка или сброс всех случившихся прерываний (порядок определяется программно)
- Обработчик исключения
Выяснение природы исключения (ucause)
Обработка исключения
Вычисление нового uepc
- Восстановление всех регистров
uret
Замечание: как и во время обработки прерывания, во время системного вызова прерывания или вообще запрещены, или накапливаются (делаются «Pending») без вызова обработчика. Поэтому задача обработчика — как можно быстрее принять решение, что делать с прерыванием и вернуть управление пользовательской программе.
- В обработчике выполняются только действия, необходимые для получения данных по прерыванию (чтение регистров, управление устройствами и т. п.)
- Вся логика обработки данных остаётся в программе пользователя
- При соблюдении некоторой дисциплины программирования можно вообще запретить пользовательской программе обращаться к внешним устройствам, оставив эти операции ядру, обрабатывающему прерывания
Пример: консоль RARS
Консоль RARS («Keyboard and Display MMIO Simulator») — воображаемое устройство, осуществляющее побайтовый ввод и вывод. Верхнее окошко — «дисплей», куда выводятся байты, а нижнее — «клавиатура» (для удобства набираемый на клавиатуре текст отображается в этом окошке).
Консоль имеет следующие регистры ввода-вывода
0xffff0000 |
RcC |
Управляющий регистр ввода |
RW |
0 бит — готовность, 1 бит — прерывание |
0xffff0004 |
RcD |
Регистр данных ввода |
R |
введённый байт |
0xffff0008 |
TxC |
Управляющий регистр вывода |
RW |
0 бит — готовность, 1 бит — прерывание |
0xffff000c |
TxD |
Регистр данных вывода |
W |
необязательные координаты курсора, байт для вывода |
Работа посредством поллинга
Операции ввода или вывода в консоли возможны только если соответствующий бит готовности равен 1. Если бит готовности нулевой в управляющем регистре ввода, значит, клавиша ещё не нажата, а если в управляющем регистре вывода — символ всё ещё выводится, следующий выводить нельзя (ну медленное устройство, в жизни так сплошь и рядом!). Как обычно, устройство заработает только после нажатия кнопки «Connect to RARS». Простой пример чтения с клавиатуры при помощи поллинга. Удобно рассматривать с низкой скоростью работы эмулятора (3-5 тактов в секунду).
Чуть более сложный пример с выводом, в котором видна проблема переполнения.
Согласно документации, вывод начинает работать (точнее, бит готовности выставляется в первый раз) только если нажать «Reset» или «Сonnect to program» после запуска программы. Это — тоже пример из «реальной жизни»: при включении питания многие устройства находятся в неопределённом состоянии и требуют инициализации.
- Чего в документации не написано — так это того, что бит готовности вывода в действительности доступен на запись, и мы можем в самом начале программы выставить его в 1 (будем считать, что это такой программный «Reset»)
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 .eqv RcC 0xffff0000
2 .eqv RcD 0xffff0004
3 .text
4 .globl main
5 main: la t0 handle
6 csrw t0 utvec
7 csrsi uie 0x100
8 csrsi ustatus 1 # enable all interrupts
9
10 li a0 2 # enable keyboard
11 sw a0 RcC t0
12
13 here: jal sleep
14 lw a0 (gp) # print key stored in (gp)
15 li t0 0x1b
16 beq a0 t0 done # ESC terminates
17 beqz a0 here # No input
18 li a7 1
19 ecall
20 li a0 '\n'
21 li a7 11
22 ecall
23 sw zero (gp) # Clear input
24 b here
25 done: li a7 10
26 ecall
27
28 .eqv ZZZ 1000
29 sleep: li t0 ZZZ # Do nothing
30 tormo0: addi t0 t0 -1
31 blez t0 tormo1
32 b tormo0
33 tormo1: ret
34
35 handle: csrw t0 uscratch
36 sw a7 sr1 t0 # We need to use these registers
37 sw a0 sr2 t0 # not using the stack
38
39 csrr a0 ucause # Cause register
40 srli a0 a0 31 # Get interrupt bit
41 beqz a0 hexc # It was an exception
42 # Assume only I/O interrupts enables
43 lw a0 RcD # get the input key
44 sw a0 (gp) # store key
45 li a0 '.' # Show that we handled the interrupt
46 li a7 11
47 ecall
48 b hdone
49
50 hexc: csrr a7 uepc # No exceptions in the program, but just in case of one
51 addi a7 a7 4 # Return to next instruction
52 csrw a7 uepc
53
54 hdone: lw a7 sr1 # Restore other registers
55 lw a0 sr2
56 csrr t0 uscratch
57 uret
58
59 .data
60 sr1: .word 10
61 sr2: .word 11
Если запускать этот пример на пониженной скорости, надо поменять значение ZZZ на меньшее, например, на 10, иначе вывода можно и не дождаться
В обработчике из этого примера есть также не используемая часть, которая определяет, прерывание это или исключение (и соответственно не изменяет или изменяет uepc) — это буквально решение прошедшего домашнего задания
Прерывание готовности вывода
Как самое настоящее устройство вывода, консоль RARS выводит байты тоже медленно. Пока «байт выводится», нулевой бит регистра TxC — TxC:0 равен нулю, а когда устройство готово выводить следующий байт, он равен 1. Если выставить в 1 первый бит этого регистра, TxC:1, консоль будет порождать прерывание всякий раз, когда она готова выводить.
В результате мы имеем две ситуации:
Необходимо вывести байт, устройство готово — байт можно записывать в TxD непосредственно
Необходимо вывести байт, устройство не готово — байт нужно кода-то отложить, и его запишет обработчик прерывания готовности вывода
Самая простая реализация — проверить 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
Д/З
Задание всего одно: это т. н. «нанопроект» — обязательное задание, которое стоит как 4 обычных Д/З. его надо сдавать в EJudge, но там проверяется только сборка, а работу покажете на экзамене.
Написать примитивную игру в «Змея» с использованием:
- Графического дисплея в качестве устройства отображения
- «Keyboard and Display MMIO Simulator» для ввода команд с клавиатуры
Обязательно читать с клавиатуры при помощи обработчика прерывания
«Timer Tool» и обязательная обработка прерывания по таймеру для регулярного обновления картинки
Алгоритм должен поддерживать:
- Случайное появление «еды» на экране
- Рост змея на 1 после съедания еды
- Управление змеем с клавиатуры
- Завершение программы при пересечении границы экрана (выводится количество съеденной еды)
Рекомендуемые настройки:
- Обновление 10 раз в секунду
- Экран 512×256, размер пикселя 4 (т. е. реальный размер экрана 128×64)
Моё решение занимает 165 строк, из которых 50 — это макросы и .eqv