Прерывания
Базовая статья — слайды доклада 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.
Уровни привилегий (Machine → Hypervisor (VS) → 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
Предусмотрен векторный режим вызова ловушек (возможен также вариант с двойной косвенной адресацией, но его нет в спецификациях; вообще фантазия производителей железа безгранична)
В спецификации оставлены определения набор минимально необходимых аппаратных средств. Значительная часть задач возлагается на окружение, возможно и аппаратное — например, контроллер прерываний (ACLIC, 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 |
- Interrupt = 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
Д/З
У Консоли 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!
Т. е. если выводить на экран консоли машинное слово, у которого:
- символ с кодом 7 в младшем байте,
- Y-координата в битах 19-8,
- X-координата в битах 31-20,
…то вместо рисования чего-либо изменится знакоместо, в которое в следующий раз будет выводиться очередной байт. Все стандартные терминалы и эмуляторы терминалов так умеют! Но у всех эти управляющие символы выглядят по-разному…
Во всех задачах допустимо использовать поллинг готовности вывода.
- В моей системе консоль открывается размером 97x10 знакомест. Будем считать, что, когда это требуется, размер консоли именно такой.
В качестве бонуса можно попробовать реализовать посимвольный вывод из буфера по прерыванию готовности вывода, но это довольно сложно.
Все задания необходимо сдать на EJudge, но тестов по ним не будет, ибо консоль интерактивная.
EJudge: AsteriskSky 'Звёздное небо-2'
Input:Написать программу, которая заполняет экран консоли случайными символами ".", "+" и "*" на случайных местах. Для простоты использовать ecall 42 и поллинг готовности вывода.
- Пример вывода:
Output:.
.
- Пример вывода:
EJudge: MovingAsterisk 'Бегущая звёздочка'
Input:Бегущая звёздочка. Написать программу, которая выводит на экране консоли символ «*», и этот символ «движется» от левого края к правому; на краю консоли программа останавливается.
- «Движение» — это вывод по координатам x+1, y символа «*», а затем вывод по координатам «x, y пробела.
- Движение происходит согласно таймерному прерыванию (используется и консоль, и Timer Tool)
- Таймер медленный — не чаще 5 раз в секунду
Output:.
.
- Это т. н. «нанопроект». Он стоит 400% обычной задачи и у него длинный дедлайн.
EJudge: TextRunner 'Бегущий человечек'
Input:Написать программу управления человечком на консоли:
O -+- / \
- Изначально человечек стоит по центру консоли.
- Если нажата одна из пяти клавиш управления, он начинает двигаться в соответствующую сторону или останавливается
- Вариант управления (можно другой):
8 ↑ 4 ← 5 → 6 ↓ 2
- Вариант управления (можно другой):
- Движение происходит согласно таймерному прерыванию (используется и консоль, и Timer Tool)
- Таймер медленный — не чаще 5 раз в секунду
- Ввод происходит по прерыванию от клавиатуры консоли
- На границе экрана человечек останавливается (или появляется с другой стороны — как вам удобнее)
- Допустимо в качестве «движения» сначала заполнять человечка пробелами, а затем выводить нового в новом месте
(бонус) можно предусмотреть затирание только того места, откуда человечек ушёл — тогда экран не будет моргать
Output:Последовательнcть нажатий в консоли
Человечек бегает
- У меня получилось так:
