- Платформа Ядро Hart
Такое ощущение, что это (бессмысленная классификация) сейчас больше запутывает.
Георгий Курячий Увы, всё это есть в спецификации и довольно часто используется в объяснениях.
- Прерывания в RISC-V могут быть трёх типов:
На самом деле - только одного. Таймер как являлся периферией, так и является, ручная запись в *ip как воспринималась прерыванием от периферии, так и воспринимается. Опять совершенно ненужное разделение одной сущности на несколько.
Георгий Курячий Снова отсылаю к спецификации, где этих типов три. Это не говоря уже о том, что существуют глубокие различия между всеми тремя, о некоторых прямо говорилось в лекциях.
- Основной механизм — т. н. вертикальная обработка, при котором прерывание, возникшее на более низком уровне, обрабатывается на более высоком
Как мы помним, прерывание это всегда событие от периферии, …
Георгий Курячий Или не от периферии, см выше про три типа. Может быть программное или от внутреннего таймера.
…ему без разницы какой инструкции не повезло выполняться в этот момент.
Георгий Курячий или не без разницы, если надо проанализировать эту инструкцию на предмет семихостинга, например.
Тут акцент немножко на другом: драйвер периферии может крутиться не в пространстве ядра (M-mode), а в пространстве операционной системы (S-mode). Соответственно и спустить привилегии нужно именно туда.
Георгий Курячий Подмена причины и следствия. Драйвер написали уже после того, как сделали железо. Он поэтому и крутится уровнем выше, что так написано в спецификации железки.
- Повторный вход в ловушку (double trap, попытка вызвать обработчик во время выполнения кода обработчика) в спецификации RISC-V в целом запрещен, но:
Зачем тогда биты MPIE и подобные? Вряд ли ведь только чтобы руками туда что-то записывать.
Георгий Курячий Это не про повторный вход в ловушку
- Можно по конвенции потребовать в обработчике явно сохранять контекст (куда? — а вот в зависимости от приоритета же) и немедленно выставлять разрешение прерываний в mie.
Скорее, заставить программиста сохранить контекст и руками выставить MIE. А дальше уже аппаратура проверяет в каком состоянии процессор (U-mode, M-mode, M-mode в обработчике, какой приоритет у прерывания) и какое прерывание ждет выполнения.
- В одноуровневых системах (типа RARS или очень маленьких контроллеров) эта иерархия отсутствует
Даже в ch32v003 (а более слабых risc-v контроллеров я не знаю) и то есть приоритеты прерываний.
- Если несколько прерываний возникли актуально одновременно или во время обработки другого прерывания, они «накапливаются» в регистре *ip (в RARS — uip).
Скорее, в регистрах контроллера прерываний. Это внешняя периферия, не входящая в ядро.
Георгий Курячий До тех пор, пока мы этих регистров не видим, нам остаётся только смотреть в *ip
- Для старта работы с прерываниями нужно:
Самое главное забыли - разрешить прерывания в самой периферии. В частности, указать какие именно события будут генерировать прерывания. Например, в UART один и тот же обработчик может вызываться при приему байта, при передаче байта, при ошибке приема. В таймерах все еще веселее: по переполнению, по сравнению каждого канала, по захвату, ... В общем, пока фантазия производителя не успокоится.
- Обработчик прерываний RARS
Здесь можно сказать словами, что примитивный стандарт из Rars, да даже из Risc-v это, конечно, хорошо. Но в реальности опять придется изучать как извращался тот конкретный производитель, с железом которого вам не повезло работать. Стандарт Risc-v либо слишком негибкий, либо слишком молодой, чтобы удовлетворять реальным задачам, вот все и "расширяют" его. В меру своей фантазии.
Георгий Курячий «Стандарт» RARS реально тупой и древний, а вот относительно RISC-V скорее наоборот — все "расширяют" его в меру своей фантазии просто потому, что не осиливают CLIC / PLIC со товарищи
- В RARS автоматически (по спецификации) запрещается обрабатывать повторный вход в обработчик
Это лишнее уточнение. Без дополнительных настроек входв прерывание и так аппаратно блокирует повторный вход. Соответственно, требование проводить в обработчике как можно меньше времени, актуально всегда (ох как на радиофорумах любят любителей delay(), while(flag){}, ...). Ну а если хочется пострелять себе по ногам и разрешить вложенные прерывания - ну флаг в руки.
- Перед выходом из обработчика можно очистить регистр ucause
Ритуал завершения прерывания также может сильно отличаться.
Так, зачастую биты pending прерываний являются всего лишь отображением битов периферии. Сбросили флаг где-нибудь в TIMER8->STATR, сбросился бит прерывания. Еще веселее в случае прерываний по уровню на ножке. Буквально можно настроить, чтобы пока на 14-й ножке микросхемы высокий уровень, бит готовности прерывания равен 1. И никаким способом сбросить его нельзя. Поэтому если в прерывании нет кода запрета самого себя, код так и будет там крутиться. Ну или не запрета, а перенастройки - например, не по высокому уровню, а по спадающему фронту (лог.1 -> лог.0). Это, кстати, к вопросу о битах разрешения прерываний.
- Замечание: как и во время обработки прерывания, во время системного вызова прерывания или вообще запрещены, или накапливаются
Это одно и то же. Единственное, что делает периферия это выставляет бит pending interrupt. А уж дальше контроллер прерываний или ядро этот бит читают, делают AND по маске разрешенных прерываний, проверяют MIE, проверяют приоритеты, ... И только в самом конце вызывают собственно прерывание.
- В обработчике выполняются только действия, необходимые для получения данных по прерыванию (чтение регистров, управление устройствами и т. п.)
Остается добавить: "вот таким способом мы изобрели драйвер устройства. Он занимается обработкой прерываний, подготовкой данных, настройкой периферии. А юзерский код только и может, что складывать в нужное место данные для обмена".
Георгий Курячий Строго наоборот. «Таким образом изобрели драйвер устройства» разве что в Plan5 или GNU HURD. В подавляющем большинстве операционных систем драйвера монолитные с логикой в пространстве ядра.
Кроме шуток, термина "драйвер" я на этой странице не нашел.
Георгий Курячий Потому что это термин операционной системы, не имеющий отношения к архитектуре. И термин, положа руку на сердце, неважнецкий — слишком нечёткий.
- Консоль имеет следующие регистры ввода-вывода
В реальности обычно указывают базовый адрес периферии и смещения отдельных регистров. Для "консоли rars" это будет
.equ TREM_BASE 0xFFFF0000 .equ TERM_CTL_IN 0 .equ TERM_IN 4 .equ TERM_CTL_OUT 8 .equ TERM_OUT 12
Экземпляров периферии обычно бывает больше одного. Тех же UART-ов обычно штуки две-три, таймеров минимум четыре. Поэтому проще указать, что DATA регистр всегда по смещению +4 (или сколько там) от базового адреса. Соответственно для UART1 это будет 0x40013800 + 4, а для UART5 0x40005000 + 4 (числа, если что, реальные, от ch32v307).
Кстати, зачастую DATA-IN и DATA-OUT это один и тот же регистр, просто периферия сама определяет происходит ли чтение или запись и реагирует по-разному. Либо подменяя операцию чтения в DATA чтением из буфера чтения (это штука чисто аппаратная, своего номера у него нет), либо подменяя запись в тот же DATA записью во внутренний буфер. Таким образом, прочитать только что записанное значение уже невозможно.
- а если в управляющем регистре вывода — символ всё ещё выводится, следующий выводить нельзя (ну медленное устройство, в жизни так сплошь и рядом!)
"Терминал Rars" похож на обычные аппаратные терминалы, которые подключались по COM-порту. Соответственно скорость обмена там хорошо если 9600 бод (а бывало и 2400 и даже меньше). Даже у контроллера ядро успевает выполнить сотни тысяч инструкций, пока терминал наконец передаст один байт.
- Чего в документации не написано — так это того, что бит готовности вывода в действительности доступен на запись
Такое в реальности тоже бывает. Вот только если не вовремя "выставить бит готовности", получим вместо осмысленных данных какую-то кашу. Буквально на прошлой неделе наступил на эти грабли: проверял бит готовности USB1, а писал в USB0. Естественно, бит готовности не сбрасывался (я ж не в USB1 писал), вот и получалось, что начинает передаваться один массив, а заканчивает - другой.
- Устанавливая в 1 первый бит в регистре RcC, мы разрешаем консоли возбуждать прерывание всякий раз, как пользователь нажал на клавишу. Устанавливая в 1 первый бит регистра TxC, мы разрешаем прерывание типа «окончание вывода».
Вот на это надо будет потом сделать отсылку при обсуждении DMA. Устройство может сообщить о готовности не только ядру, но и модулю DMA, и тот автоматически выдаст ему следующий байт.
Впрочем, для того же UART (терминал) я и сейчас пользуюсь именно прерываниями по приему отдельных байтов. Тут проблема в том, что терминал заточен под строки, а не под пакеты. То есть нельзя заранее указать сколько байтов ожидается. Вот и приходится использовать ручную реализацию кольцевого буфера. Но UART обычно штука медленная, там этого хватает.
- Это лучше, чем модифицировать регистр или метку, определяемую пользовательской программой.
На ассемблере еще куда ни шло. А вот на Си компилятору не скажешь "ты регистр x23 не трогай, в него прерывание полезет".
- Однако это выглядит некрасиво: то ли программа у нас занимается записью в TxD, то ли ловушка.
В реальности именно так и бывает. Если устройство готово, пишем в него напрямую, если нет, то в буфер. Можно, конечно, и в буфер, а потом разрешать прерывание (см. выше: флаг готовности мы снять не можем, он аппаратно равен 1 все то время, пока буфер на передачу пуст. Можем только запретить прерывание). Но это лишний вход в прерывание, лишнее копирование. Лишние такты.
- Или же мы можем положить байт в буфер, самостоятельно вызвать программное прерывание
Правильнее сказать "сэмулировать аппаратное прерывание". Насколько я помню, в Risv-v, в отличие от некоторых других архитектур, программных прерываний нет. Вместо них ecall?
Георгий Курячий См. выше в плане этой лекции — в RISC-V всего три класса прерываний, и один из них — программные.
