Сегодня речь пойдет о модном — о RISС-V микроконтроллере. Я давно хотел познакомиться с этим ядром и ждал когда появится что-то похожее на STM32 и вот дождался, встречайте — китайский GigaDevice — GD32V. Инфраструктура для этого микроконтроллера не такая обширная как для STM32, но есть все необходимое для того, чтобы начать с ним работать. Благо отладочные платы можно заказать на аликекспресс, например, вот тут: Longan Nano GD32VF103CBT6 RISC-V MCU Китайцы продвигают для этого микроконтроллера среду разработку Platform IO, которую можно поставить как расширение под Visual Studio Code. Но мы не будем её использовать, это ведь не по инженерным понятиям, мы же инженеры и хотим разобраться во всем сами. Поэтому давайте попробуем запустить плату на IAR, написав все с нуля. Кстати, IAR раздает отладочный комплект (отладочная плата + отладчик I-Jet + 30 Дней полная лицензия) IAR RISC-V GD32V Evaluation kit. Вот тут можно оставить заявку Request for Development Tools. Не уверен, что они посылают комплект всем желающим, но мы получили в течение 5 дней. Спасибо им за это! Ну что же, кто заинтересовался, добро пожаловать под кат Вообще я хотел портировать очень простой планировщик из статьи Переключение контекста и простой вытесняющий планировщик для CortexM, но из-за найденной мною ошибки в компиляторе IAR (но IAR обещал исправить все в ближайшее время), напрямую этого сделать не удалось, пришлось немного переделать стратегию определения задач, да и материала получалось много. Поэтому я решил начать с простого — с моргания светодиодом, точнее двумя. Когда я только сел разбираться, думал, что разберусь за пару часов, но открыв документацию, которая ссылается на другую документацию, которая ссылается на еще другую документацию, которая, в конечном итоге, равномерно разбросана по интернету, понял, что парой часов тут не обойтись и первое впечатление от RISC-V было вот прямо как у Джереми, когда, видимо, он тоже увидел RISC-V микроконтроллер.  Но чуть позже я привык и даже проникся теплыми чувствами к ученым из Калифорнийского университета, которые придумали вот это всё (это я про саму архитектуру RISC-V). И потому попытаюсь донести, что же я понял, и что такое китайский RISC-V. Описывать все детали архитектуры RISC-V не хватит никаких сил, я ограничусь только необходимым минимумом для того, чтобы понять, как правильно поморгать светодиодом через таймер с прерыванием. Но даже, чтобы описать такую, казалось бы простую задачу, мне пришлось написать очень много букв, поэтому, если вы не хотите проходить со мной весь этот тернистый путь, можете сразу мотать на раздел Моргаем светодиодом, там где начинается код. Материала по RISC-V на русском не так много (вот есть обзор Создание процессора со свободной архитектурой RISC-V, вот еще презентация от Syntacore), поэтому, как обычно, начнем с азов. Итак, поехали. Для начала опишу, какими ресурсами я пользовался, возможно кому-то пригодится. Из всего этого будем пользоваться только первыми 4 пунктами, попробуем написать код на С++ без всех этих библиотек — все сами, заглядывая только в документацию. Задача будет такая: RISC-V это уникальная архитектура. Все определения и понятия тут свои, основные понятия, которые вам будут постоянно встречаться на извилистом пути изучения RISC-V, я приведу ниже: Ну хватит… остальное вроде бы привычно для ушей эмбеддеров. Для начала, немного википедии: На данный момент, в архитектуре разделяются следующие наборы команд, который я скрыл под спойлер, так как она довольно большая: Как видно из спрятанной таблицы, архитектура уже подразумевает поддержку довольно большого количества расширений, что делает её очень привлекательной для независимого будущего. Ведь можно делать ядра начиная от простейших микроконтроллеров и заканчивая уже мощными процессорами для научных расчетов. Но нам нужна только небольшая часть из всего этого, так как на самом деле для микроконтроллеров общего назначения используется в основном 32 битная архитектура с очень небольшим количеством расширений, например: Ядро: Расширения: Как видите — вполне себе стандартенький наборчик для обычного общепромышленного микроконтроллера.  Давайте теперь кратенько взглянем на регистры. RISC-V имеет 32 регистра x0-x31. Но обычно к ним обращаются через ABI имена. Рабочие регистры:
Регистры t0-t6(x5-x7, x28-x31) и a0-a7(x10-x17), а также регистр адреса возврата являются рабочими регистрами. Любая функция может изменять содержимое этих регистров и если ей нужно воспользоваться какими-то из этих регистров после вызова другой функции, она должна сохранить их значение на стеке. Сохраняемые регистры:
Регистры s0-s11 (x8, x9, x18-x27 ) должны сохраняться вызываемой функцией на стеке (если функция хочет их использовать) перед входом в функцию и восстанавливаться перед выходом, . Далее табличка из интернета, описывающая каждый регистр, не стал переводить, итак все понятно: А вот теперь моя вольная интерпретация некоторых регистров.  x0/zero:
Регистр хранит всегда 0 и может использоваться в некоторых командах доступа к регистрам CSR(об этом и о многом другом чуть дальше), например, в команде CSRRS (Atomic Read and Set Bits in CSR), при использовании регистра x0 как источника маски, команда будет атомарно только читать CSR регистр без его модификации. Если вы захотите использовать другой регистр в котором хранится ноль, то команда все равно произведет запись в регистр CSR, поэтому если необходимо только прочитать биты, то нужно использовать регистр zero. x1/ra:
(Link register или Return Address регистр). Регистр содержащий адрес возврата из функции. Этот регистр может использоваться как рабочий регистр в функции, поэтому при входе в функцию он должен быть сохранен, а при выходе, перед вызовом инструкции ret, восстановлен. x2/sp:
Указатель стека. Ничего не придумал от себя — просто указатель стека. И он один, не как в CortexM, где их два. x3/gp:
(The global pointer register). Глобальный регистр указателей (gp/x3) используется для эффективного доступа к памяти в пределах области в 4 Кбайта. Компоновщик сравнивает значение адресов памяти со значением которым должен быть проинициализирован gp, и если оно находится в пределах диапазона 4 кбайта, заменяет абсолютную/pc-относительную адресацию на gp-относительную адресацию, что делает код более эффективным. Этот процесс также называется короткой памятью. Область 4K может находиться в любом месте памяти, но для того, чтобы оптимизация была эффективной, она должна предпочтительно охватывать наиболее интенсивно используемую область оперативной памяти. Поэтому обычно в настройках компоновщика для инициализации этого указателя используют адрес на начало сегмента глобальных и статических данных. x4/tp:
(The thread pointer). Указатель потока. Этот регистр используется для реализации механизма Локального хранилища потока (Thread Local Storage (TLS)), например при реализации спецификатора класса thread_local в С++. Не не будем сильно заморачиваться с этими регистрами, все что нам нужно знать про них для нашей задачи — это тот факт, что как минимум все рабочие регистры нужно будет сохранить во время входа в прерывание и восстановить при выходе. Я не буду описывать наборы инструкций и ассемблер, потому что он нам не нужен, но вот про спецификации, описывающие ISA, стоит рассказать. Существует две спецификации набора инструкций: В нашем китайском микроконтроллере используется оба набора. Спецификация на этот набор описывает инструкции и функциональность которые обычно используются во всех режимах привилегий, т.е. общие для всех архитектур набор инструкций и функций. Спецификация на этот набор доступна здесь: Непривилегированный ISA Основное её назначение — это разделение уровня приложений и уровня ядра, а также поддержка операционных систем вплоть до нескольких разных операционных систем типа Linux, работающих через виртуальную машину. Но нас это не особо беспокоит, у нас же небольшой микроконтроллер, который из всего этого дела использует почти самую простую форму привилегированности. Спецификация на привилегированный набор описывает возможную архитектуру привилегированных режимов, в том числе специальные инструкции и дополнительную функциональность для каждого из них. Спецификация доступна здесь: Привилегированная ISA Следует уточнить, что эта спецификация носит рекомендованный характер, и она описывает только одно из возможных решений. Основное её преимущество, в том, что привилегированная архитектура никак не задевает основную непривилегированную функциональность и является её расширением. Далее речь пойдет как раз о некоторых деталях этой привилегированной архитектуры, так как есть кое-какие нюансы, которые нужно знать при работе с ловушками. В RISC-V архитектуре существует 3 уровня привилегий. Уровни привилегий используются для обеспечения защиты между различными компонентами программного обеспечения (например, пользовательским приложением и ядром операционной системы). Любые попытки выполнения операций, не разрешенных текущим режимом привилегий, вызовут исключение.
Ниже показаны значения режима привилегий: Как видно из таблички, для микроконтроллеров, таких как GD32VF103 рекомендованы режимы M или М и U. Собственно он и поддерживает оба режима. И если микроконтроллер работает в пользовательском режиме U, то ему недоступны настройки машинного режима и доступ к машинным регистрам, таким как mtvt, mepc, о них речь пойдет немного ниже. И чтобы обратиться к ним, вам необходимо зайти в ловушку, так как в GD32VF103, при попадании в ловушку ядро переходит в машинный режим.  Т.е. любое прерывание или исключение переводит ядро в машинный режим M и уже внутри него можно обращаться к машинным регистрам. В общем случае алгоритм доступа к машинным регистрам из пользовательского режима выглядит следующим образом — вам надо вызывать инструкцию ecall — запрос среды исполнения, которая переведет микроконтроллер в машинный режим и вызовет обработчик ловушки, в котором вы можете поменять машинные регистры в соответствии с запросом. Переход же из машинного режима в пользовательский происходит после команды mret — возврат из машинного режима.  Забегая вперед скажу, что хотя микроконтроллер и поддерживает два режима, после сброса он находится в машинном режиме, и переводить в пользовательский режим мы его не будет, чтобы не нагружать итак уже большую статью. Примечание:
Согласно стандартной привилегированной архитектуре RISC-V, мы не можем на прямую узнать текущий привилегированный режим (например, машинный режим или режим пользователя). Пусть вас не смущает буква F в названии микроконтроллера GD32VF103 — это просто маркетинговое название, чтобы было похоже на уже существующую линейку GD32F103 на ядре CortexM3. Никакой поддержки инструкций с плавающей точкой здесь нет. Наверное, ставка была на то, что кто-то спутает GD32F103 с STM32F103 и не заметит подвоха… а затем еще спутает и GD32VF103 c GD32F103. Мой продавец попался в эту ловушку (это другая ловушка, если что), и вначале мне пришел микроконтроллер GD32F103, вместо GD32VF103. Этот микроконтроллер построен на архитектуре RV32IMAC — что идентифицирует микроконтроллер как RISC-V 32-битная архитектура с 32-битными регистрами общего назначения, который имеет целочисленные инструкции умножения и атомарные инструкции, инструкции сжаты до 16 бит для уменьшения размера кода. Микроконтроллер может использоваться в защищенных системах, для которых достаточно только два режима: Как я уже говорил выше, привилегированная спецификация это не панацея и производители могут добавлять и даже изменять архитектуру. В данном случае, ребята добавили несколько подрежимов Машинного режима. Почитать о ней можно тут: Nuclei privileged ISA Существует 4 подрежима: Эти подрежимы можно узнать из поля TYP машинного регистра msumbm По умолчанию после сброса ядро находится в машинном режиме в подрежиме 0 (Нормальный подрежим работы) и вообще для большинства применений этого и достаточно, потому как у нас есть полный доступ ко всем регистрам и пользовательским и машинным.
Собственно, в моем примере я буду использовать только такой режим, но если мы сильно хотим ограничить пользователя от настроек ядра, например, запретить пользователю изменять машинные регистры из задач операционной системы, то мы всегда можем перейти в режим пользователя. Для этого, в нормальном подрежиме машинного режима, необходимо просто выполнить инструкцию mret — возврат из машинного режима, предварительно подменив в регистре mstatus поле MPP на пользовательский режим, а также поставив правильный адрес возврата в регистре mepc. Вот про эти странные регистры мы сейчас и узнаем.  Я тут уже вскользь упомянул регистры mstatus, mepc, msumbm, mtvt ..., так что это за регистры? Эти регистры встроены в ядро микроконтроллера, поэтому доступ к ним можно осуществить только с помощью специальных команд ассемблера, например cssr или csrr. Это не очень хорошо, так как я собирался использовать для доступа к ним мою обертку над регистрами, а она не подходит для доступа к регистрам ядра, из-за того, что доступ к ним осуществляется особым образом через эти специальные команды.  Чтобы не трогать уже написанную обертку и генератор регистров, я сделал отдельный класс для их обработки.  На пользователей это никак не повлияло, а я получил возможность удобно обращаться к таким регистрам. Суть класса таже самая — только вместо прямого чтения, все сделано на ассемблере, встроенных в IAR функции доступа к CSR регистрам. (Было лень писать на ассемблере просто взял встроенные функции IAR, но правильно переписать на ассме, чтобы подходило для GCC тоже). Вот так выглядит метод чтения значения такого регистра Пример доступа к специальному регистру на ассемблере и через обертку Регистров целая куча, есть регистры, которые обязательны в соответствии со спецификацией, а есть уже добавленные производителем. CSR регистры существуют для каждого режима, поэтому в общем случае они называются xимярегистра, например xstatus — может быть регистр mstatus — регистр статуса машинного режима, ustatus — регистр статуса пользовательского режима. И, например, доступ к m регистрам запрещен из пользовательского режима, а к u регистрам разрешен.  Под спойлером описание всех регистров статуса и управления нашего микроконтроллера. Для нашей задачи нам не нужны все регистры, мы ограничимся только теми, что нужны для решения конкретно нашей задачи. Напомню её на всякий случай — поморгать светодиодами. Регистр указывающий причину возникновения прерывания. Табличку скрыл под спойлер, чтобы места не занимала. Регистр хранящий адрес общего обработчика прерываний в не-векторном режиме при работе ECLIC контроллера. Специализированный регистр ядра Bumblebee, хранящий текущий машинный подрежим и подрежим, в которой было ядро перед входом в текущую ловушку. Регистр mstatus отслеживает и управляет текущим рабочим состоянием аппаратного потока (hart). Также под спойлер. Регистр содержит настройку того, чему равно значение регистра mnvec, в котором лежит адрес обработчика ловушки NMI. Содержит единственно поле — бит номер 9 (NMI_CAUSE_FF). Да, именно бит номер 9 — в принципе, почему бы и нет. Так вот, если NMI_CAUSE_FF(бит 9) равен 0x0, то значение регистра mnvec будет равен адресу содержащемуся по вектору сброса. Если этот бит равен 0x1, то значение регистра mnvec будет равно значению, лежащему в регистре mtvec, т.е. NMI исключения и прерывания будут обрабатываться через одну ловушку, а номер обработчика будет равен 0xFFF. Регистр содержащий адрес возврата из ловушки. Адрес возврата автоматически сохраняется в этом регистре при возникновении исключения или прерывания. При возврате из ловушки он восстанавливается из это регистра в pc.
Этот регистр можно изменять, что используется в RTOS при переключении на другую задачу. Регистр содержащий адрес ловушки. Может содержать как адрес ловушки прерываний, так и адрес ловушки обработчика исключений, зависит от настроек в регистре mmisc_ctl Ну вот, все нужные регистры изучены. Теперь можно прояснить ситуацию с обработкой прерываний и исключений. Как я уже говорил, существует 3 различные ловушки событий, которые прерывают поток выполнения программы. Эти события разделяются на Каждое из таких событий обрабатывается ядром немного по-разному. Исключения обрабатываются отдельно. Базовый набор исключений ядра Bumblebee нашего микроконтроллера выглядит так: Теперь рассмотрим как обработать прерывания: Прерывания — это асинхронные события прерывающие поток исполнения. У нашего микроконтроллера существует две реализации контроллеров прерывания RISC-V базовый контроллер PLIC(Platform-Level Interrupt Controller)умолчанию и режим CLIC (Core-Local Interrupt Controller). PLIC описан в привилегированной спецификации, а драфт версия для CLIC описана здесь Работа PLIC опирается на регистры mie and mip, которые являются частью привилегированной спецификации RISC-V. Как говорит руководство на ядро, использование этого контроллера рекомендуется для симметричных многопроцессорных систем или для операционных систем типа Linux. А для встроенного ПО и операционных систем реального времени рекомендуется использовать CLIC. Поэтому далее мы будем говорить только про CLIC. После сброса ядро работает с базовым контроллером PLIC и необходимо явно переключиться на работу с CLIC. Это делается с помощью CSR регистра mtvec в двух его младших битах. По умолчанию они стоят в режиме (00b) PLIC в не-векторном режиме. Для перехода в CLIC нужно установить два этих младших бита в 11b. На самом деле микроконтроллер GD32VF103 использует ECLIC (расширенный контроллер прерываний) — немного улучшенная версия CLIC, описанного здесь Но вкратце:
Котроллер поддерживает до 4096 прерываний, все прерывания и исключения, включая стандартные подключены к нему и управляются им. Прерывания, начиная с номера 19 являются внешними, например это может любая периферия. Вот как к ECLIC подключены прерывания.
 Контроллер поддерживает следующие возможности:
Поиск обработчика по номеру прерывания, разрешение/запрещение прерываний, возведение флага прерывания, определение прерывания по его уровню и фронту, приоритизацию прерываний, векторный и невекторный режимы.  Все его режимы рассматривать не будем. Узнаем только про то, что нам надо. Всего в нашем китайском микроконтроллере 87 источников прерываний.  Процесс обработки прерываний и исключений довольно прост. При возникновении таких событий, вход в ловушку Исключения/Прерывания/NMI происходит практически одинаково и включает в себя следующие шаги, которые выполняются одновременно за один цикл: Важно, что обработчик ловушки находится всегда в Машинном режиме.  На рисунке я показал синим — шаги которые одинаковы для всех видов ловушек и шаги, разными цветами уникальные шаги для входа в различные ловушки.  Нам понадобится эта картинка для того, чтобы правильно сделать обработчики прерываний. Картинку рисовать не буду, опишу в общих деталях: Все это дело выполняется за один цикл. В RISC-V нет автоматического stacking и unstacking как в CortexM ядрах, поэтому, как минимум 16 регистров общего назначения придется сохранять и восстанавливать руками. Давайте разберемся как же обрабатывать прерывания. Как видно из картинки существует два режима обработки прерываний — векторный, через таблицу векторов и не-векторный — через единый обработчик прерывания. Также существует несколько способов(например, Interrupt Tail-Chaining) сделать обработку прерываний эффективнее.  Чтобы не раздувать, итак уже офигенно большую статью, я покажу как реализовать только не-векторный режим без оптимизации и шаманства. Но вкратце опишу оба. Контроллер прерываний ECLIC позволяет выбрать режим обработки прерываний и обеспечивает гибкость для выбора поведения каждого отдельного прерывания — либо с использованием аппаратной векторизации, либо без неё. В результате это позволяет пользователям оптимизировать каждое прерывание и пользоваться преимуществом обоих видов поведения. Аппаратная векторизация имеет более быстрый механизм обработки прерывания, но и имеет больший объем кода (из-за сохранения и восстановления контекста для каждого из прерываний). Напротив, невекторный режим имеет преимущество в размере кода, так как используется только один обработчик всех прерываний, но обработка происходит медленнее. Какой режим выгоднее, выбирает разработчик. Я выбрал не-векторный. В этом режиме при возникновении прерывания контроллер прерываний переходит на адрес прерывания, который указан в таблице векторов прерываний в соответствии с номером прерывания (алгоритм работы очень похож на обработку прерываний CortexM). По умолчанию все прерывания настроены в не-векторный режим. Т.е. для обработки прерывания существует только один единый обработчик. Тип обработки прерывания указывается в регистре CLICINTATTR[i] в поле SHV. По умолчанию там записан 0 — в этом случае прерывание настроено на не-векторный режим, т.е. при возникновении прерывания или исключения контроллер всегда вызывает единый обработчик, находящийся по адресу, указанному в регистре mtvec или mtvt2, в зависимости от настроек и типов ловушки. В этом обработчике необходимо определить, какое прерывание произошло и вызвать необходимую функцию обработки прерывания. Узнать, что за прерывание произошло можно с помощью регистра mcause — который хранит в себе номер прерывания в поле EXCCODE. Для настройки контроллера нам понадобится описание его регистров. Ниже я привел табличку с 7 регистрами, но на самом деле их на много больше, так как i — означает номер прерывания. Т.е. существует 87 clicintip, и 87 clicintie, и 87 clicintattr и 87 clicintctl, каждый из которых отвечает за свое прерывание.  Ну а теперь, надо же описать, что это за регистры…  Регистр, который задает уровень срабатывания прерывания. Как видно из картинки, можно управлять не только приоритетом прерывания, но и уровнем прерывания и делать, что-то типа прореживания, мол если уровень прерывания ниже определенной границы, то это и не прерывание вовсе и не нужно его возводить.  Сам уровень конкретного прерывания, как было сказано задается clicintctl[i]. А вот уровень срабатывания — как раз задается регистром mth. Собственно это просто 8-битный регистр, хранящий уровень срабатывания прерывания. Регистр используется для задания уровня и приоритета прерывания. Как будет рассказано ниже, старшие биты(эффективные биты), количество которых задается в регистре CLICCFG указывают уровень прерывания, а младшие — приоритет. Количество эффективных битов также можно считать из регистра CLICINFO в поле CLICINTCTLBITS. Регистр общей конфигурации прерываний. Он задает количество эффективных битов, ответственных за установку уровня и приоритета прерывания. Чтобы было понятнее, приведу картинку.   Непонятно? Тогда следите за описанием бита nlbits в табличке, должно много прояснить. Регистр общей информации о системе прерываний Регистр содержащий единственный флаг запроса прерывания. i — обозначает номер прерывания. Наш контроллер содержит 87 прерываний, поэтому будет 87 таких регистров. Регистр разрешения прерываний. Их тоже 87. Регистр настройки источника прерываний. Как было показано выше, контроллер прерываний может работать в нескольких режимах. Прерывания могут срабатывать по уровню, по фронту переднем или заднему, а также тип прерывания векторный или не векторный. И их тоже 87. Все регистры кончились, осталось описать, как работает машинный таймер и порты… уже немножко и можно будет моргать. Машинный таймер — это как системный таймер в CortexM, но только машинный. Сам машинный таймер является неотъемлемой частью привилегированной архитектуры ядра, доступ к нему должны иметь все аппаратные потоки(hart). И для работы с ним даже выделили регистры mtime и mtimecmp.
Спецификация привилегированной архитектуры рекомендует сделать эти регистры как обычные регистры, а не регистры CSR. Собственно китайские ребята так и сделали, вот только в документациях, я нигде не смог найти на каком адресе находятся эти регистры. Пришлось вытащить его из примеров. Находится регистр mtime по адресу 0xd1000000, а регистр mtimecmp по адресу 0xd1000008. Размер у обоих регистров 64 бита. И отвечают они за: Запись в эти регистры гарантированно сбросит флаг запроса прерывания, но это неточно, потому что он сбрасывается не сразу и возможны паразитные прерывания таймера. Однако, как говорит спецификация-это очень редкое событие, и потому все нормально. В нашем случае, когда таймер досчитает до значения в mtimecmp, мы должны сбросить mtime в 0 в обработчике, чтобы флаг запроса на прерывание тоже сбросился.  В модуль таймера ребята из Китая запихнули еще один регистр msip — с помощью него можно генерировать программное прерывание. Его тоже можно использовать для обращения к машинным регистрам из пользовательского режима. А RTOS используют его для генерации программного прерывания, которое используется для переключения контекста. Порты в нашем китайском микроконтроллер очень похожи на порты в STM32, все тоже самое, только регистры называются по другому и настройки немного по другому сгруппированы. Но суть такая же. Нужно подать тактирование на порты, а затем настроить порты в режим выхода через регистр Port control register 0 (GPIOx_CTL0, x=A..E). И собственно для нас нужен еще один регистр Port output control register (GPIOx_OCTL, x=A..E), который позволит переключить ножку в противоположное состояние, чтобы моргнуть светодиодом.  На самом деле периферийные регистры я сгенерировал из svd файла и обращаться к ним через мою обертку можно точно также, как и регистрам STM32. Поэтому зацикливаться тут я не буду, лучше уже перейду к коду. Фуууух… ну кажется все, думаю этих знаний достаточно, чтобы поморгать светодиодом. Теперь можно перейти и к практике. И первое что нужно сделать, это настроить контроллер прерываний. Как я уже говорил, работать мы будем с ECLIC контроллером в не-векторном режиме. В этом режиме у нас один единственный обработчик прерываний, при входе в него нам нужно сохранить регистры, и еще нужно сохранить регистры mcause, mepc и msubm, потому как нас могут опять прервать, а как вы знаете из разноцветной картинки, которую я показал выше, во время прерывания эти регистры перезаписываются. Соответственно при выходе нужно восстановить все это дело. Сохранять регистры сами мы не будем, за нас это может сделать компилятор, для этого есть специальный атрибут функции __interrupt, собственно когда компилятор его видит, он подставляет пролог и эпилог функции в которых как раз и производится сохранение регистров при входе, и восстановление регистров при выходе и еще добавляет команду mret выхода из машинного режима. Но если сильно нужно, можно написать функцию на ассемблере или stackless функцию на С++ и сохранить регистры самостоятельно… эффект будет тот же самый. Итак, вот наш общий единый обработчик всех прерываний.  Во-первых, при входе мы сохранили все регистры на стеке(это у нас делает сам компилятор, потому что мы указали волшебное слово __interrupt), при выходе они восстанавливаются из стека. Во-вторых, мы сохранили CSR регистры, которые необходимо сохранить в прерываниях, в соответствии с документацией, и при выходе мы их восстановили. В-третьих, мы вызвали нужную нам функцию обработки прерывания в зависимости от его номера. Вот кстати, как эта функция выглядит (подглядел у IAR).  Мы просто берем указатель на нужную функцию из таблицы, которая выглядит как одномерный массив указателей на функции для обработки прерываний. Если вы заметили, под номером 7 стоит адрес функции обработки прерывания от машинного таймера, но следуя, моему внутреннему эго, я по старинке назвал его SystemTimer::HandleInterrupt В нем происходит вызов функции OnSystemTick() сервиса таймеров. Про него немного позже, а пока нужно настроить обработку исключений и NMI.  Для этого, с исключениями мы провернем точно такой же "фокус". Только таблица будет поменьше, исключений всего 12. NMI будем обрабатывать вместе с исключениями. Зададим его номер — 0xFFF.  Теперь нужно указать, что NMI будет обрабатываться той же ловушкой, что используется для исключений, адрес обработчика которой будет указан в mtvec, а номер обработчика NMI будет 0xFFF, см описание регистра MMISC_CTL выше в статье. Настроим в каком регистре будет указан адрес ловушки общего обработчика прерываний Ну a теперь переключимся в режим работы контроллера ECLIC и укажем адрес единого обработчика исключений и NMI в регистре mtvec Собственно все… контроллер и адреса обработчиков настроены. Полный код Теперь нужно настроить машинный таймер и его прерывание. Осталось подать тактирование на порты, к которым подключены светодиоды и настроить эти порты на выход. Светодиоды у нас на портах GPIOC.7 и GPIOB.6 Мы все ближе к морганию светодиода.
Определим сервис таймера и функцию, которая будет запускаться в обработчике прерывания машинного таймера. Она простая. Она будет вызвать функцию OnTick() у программных таймеров Программный таймер — это вот такая штука, которая вызывает метод OnTimeout(), когда он переполнится. Теперь создадим отдельный программный таймер для каждого светодиода, один на 100 ms, второй на 200ms и зарегистрируем программные таймеры в сервисе системного таймера Ну и кульминация, светодиоды Led1 и Led2 — это просто ножки портов GPIOB.6 и GPIOC.7 Функция main, не интересная, она выглядит очень скромно В общем и целом, это работает так:  Как только мы разрешили прерывание машинного таймера и глобальное машинное прерывание. Через 1 мс срабатывает прерывание машинного таймера, которое вызывает метод OnTick() программных таймеров. В момент, когда время программного таймера истекло, вызывается его метод OnTimeout(), в котором переключается ножка порта к которому подключен светодиод.  Вроде бы все, надеюсь кому-то может помочь, если начнете изучать RISC-V. Как обычно, выкладываю код под IAR 1.31 for RISC-V. И ссылка на Гитхаб с исходниками З.Ы. RISC-V оставил двоякое впечатление, с одной стороны это очень расширяемая и гибкая штука, можно делать что угодно, с другой, есть риск, что производителей занесет в строну, и каждый будет городить свой огород. Надеюсь, что все таки все спецификации скоро утвердят и все уляжется. Исправления, по наставлению Ryppka заменил структуру NonVectoredInt на namespace Хоккеист — на микроконтроллерах программист