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