Светодиод, таймер и прерывания на RISC-V с нуля (на примере GD32VF103 и IAR C++)

zgkixtk0-cd4pnvkfl5hiapdvr8.png

Сегодня речь пойдет о модном — о 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, напрямую этого сделать не удалось, пришлось немного переделать стратегию определения задач, да и материала получалось много.

Поэтому я решил начать с простого — с моргания светодиодом, точнее двумя. Когда я только сел разбираться, думал, что разберусь за пару часов, но открыв документацию, которая ссылается на другую документацию, которая ссылается на еще другую документацию, которая, в конечном итоге, равномерно разбросана по интернету, понял, что парой часов тут не обойтись и первое впечатление от RISC-V было вот прямо как у Джерими, когда, видимо, он тоже увидел RISC-V микроконтроллер.

-czjjh30_iaojm1wjllpcuyrd_4.png

Но чуть позже я привык и даже проникся теплыми чувствами к ученым из Калифорнийского университета, которые придумали вот это всё (это я про саму архитектуру RISC-V). И потому попытаюсь донести, что же я понял, и что такое китайский RISC-V.

Описывать все детали архитектуры RISC-V не хватит никаких сил, я ограничусь только необходимым минимумом для того, чтобы понять, как правильно поморгать светодиодом через таймер с прерыванием. Но даже, чтобы описать такую, казалось бы простую задачу, мне пришлось написать очень много букв, поэтому, если вы не хотите проходить со мной весь этот тернистый путь, можете сразу мотать на раздел Моргаем светодиодом, там где начинается код.

Материала по RISC-V на русском не так много (вот есть обзор Создание процессора со свободной архитектурой RISC-V, вот еще презентация от Syntacore), поэтому, как обычно, начнем с азов. Итак, поехали.


Какая поддержка уже существует у GD32VF103

Для начала опишу, какими ресурсами я пользовался, возможно кому-то пригодится.


  1. Описание ISA RISC-V с официального сайта RISC-V организации:
    Непривилегированная ISA
    Привилегированная ISA
  2. Описание микроконтроллера GD32VF103. Открывается очень медленно, а иногда и не открывается вовсе. Поэтому вот еще ссылка прямо на производителя GD32VF103CBT6 — GD32 RISC-V Microcontroller
  3. Ядро микроконтроллера GD32VF103 сделано на ядре Bumblebee, которое в свою очередь использует архитектуру Nuclei processor core
  4. Описание контроллера прерываний ECLIC.
  5. Вот тут чувак пытался разобраться как работает наш микроконтроллер и ему почти это удалось. Советую почитать.
  6. Есть общие библиотеки ядра Nuclei RISC-V на Си, например можно посмотреть здесь в репозитории IAR или тут в оригинале n200 drivers
  7. Есть библиотека периферии от самой GigaDevice, например можно взять тут же в репозитории IAR или на сайте производителя Библиотека от производителя
  8. Так же есть порты операционных систем FreeRTOS и uCOS II n200 sdk
  9. Нет, ну есть еще всякие Platform IO примеры, но это совсем не про нас. Поэтому даже ссылок давать не буду.

Из всего этого будем пользоваться только первыми 4 пунктами, попробуем написать код на С++ без всех этих библиотек — все сами, заглядывая только в документацию. Задача будет такая:


Моргать двумя светодиодами раз в 100 мс и 200 мс соответственно от прерывания системного машинного таймера


Немного определений

RISC-V это уникальная архитектура. Все определения и понятия тут свои, основные понятия, которые вам будут постоянно встречаться на извилистом пути изучения RISC-V, я приведу ниже:


  • Hart (Аппаратный поток) — архитектура поддерживает многопоточность, поэтому может быть несколько аппаратных потоков исполнения кода. Под потоком (hart) подразумевается аппаратный поток. Микроконтроллер как минимум должен иметь один поток (hart) с ID равным 0. Вот наш микроконтроллер именно такой с одним единственный hartом.
  • Trap (Ловушка) — ловушка это совокупное объедение смысла таких слов, как прерывание и исключение. Я буду постоянно путаться и называть ловушку прерыванием, или исключение — ловушкой, или прерывание — ловушкой, знайте, я не со зла. Ловушки бывают нескольких типов:
    • Ловушка исключения (exception) — это понятие означает синхронное событие, которое прерывает исполнения кода. Исключение может прерываться другим исключением, или NMI.
    • Ловушка прерывания (interrupt) — внешнее асинхронное событие, которое может привести к тому, что поток неожиданно может передать управление. Прерывание может прерываться другим прерыванием, NMI, или исключением.
    • Ловушка немаскируемого прерывания (NMI) — немаскируемое прерывание. NMI не может прерываться другим NMI, но может перейти из обработчика NMI в режим обработки исключения, если в момент обработки NMI произойдет исключение. В нашем микроконтроллере, например, отказ высокоскоростного кварцевого генератора, заведен на немаскируемое прерывание.
  • Machine (машинный) — В ядре все машинное — регистры, таймер, режим. Поэтому все что связано со словом machine(машинный) должно поддерживается на уровне ядра. Наверное, можно позволить своему внутреннему Я, заменить это на слово системный, но лучше так не делать.

Ну хватит… остальное вроде бы привычно для ушей эмбеддеров.


Краткий обзор возможностей архитектуры RISC-V

Для начала, немного википедии:


RISC-V (риск-пять) — открытая и свободная система команд (ISA — Instruction Set Architecture) и процессорная архитектура на основе концепции RISC для микропроцессоров и микроконтроллеров. Спецификация доступна для свободного и бесплатного использования, включая коммерческие реализации непосредственно в кремнии или конфигурировании ПЛИС. Имеет встроенные возможности для расширения списка команд и подходит для широкого круга применений.

На данный момент, в архитектуре разделяются следующие наборы команд, который я скрыл под спойлер, так как она довольно большая:


Таблица расширений RISC-V

Как видно из спрятанной таблицы, архитектура уже подразумевает поддержку довольно большого количества расширений, что делает её очень привлекательной для независимого будущего. Ведь можно делать ядра начиная от простейших микроконтроллеров и заканчивая уже мощными процессорами для научных расчетов.

Но нам нужна только небольшая часть из всего этого, так как на самом деле для микроконтроллеров общего назначения используется в основном 32 битная архитектура с очень небольшим количеством расширений, например:

Ядро:


  • RV32Е: 32 битная архитектура с 16 регистрами общего назначения
  • RV32I: 32 битная архитектура с 32 регистрами общего назначения

Расширения:


  • M: целочисленные инструкции по умножению и делению
  • C: сжатые до 16 бит инструкции для уменьшения размера кода
  • А: Атомарные Инструкции
  • F: Инструкции С Плавающей Запятой Одиночной Точности
  • D: Инструкции С Плавающей Запятой Двойной Точности

Как видите — вполне себе стандартенький наборчик для обычного общепромышленного микроконтроллера.

Давайте теперь кратенько взглянем на регистры.


Регистры общего назначения

RISC-V имеет 32 регистра x0-x31. Но обычно к ним обращаются через ABI имена.

Рабочие регистры:
Регистры t0-t6(x5-x7, x28-x31) и a0-a7(x10-x11, x12-x17), а также регистр адреса возврата являются рабочими регистрами. Любая функция может изменять содержимое этих регистров и если ей нужно воспользоваться какими-то из этих регистров после вызова другой функции, она должна сохранить их значение на стеке.

Сохраняемы регистры:
Регистры s0-s11 (x8, x9, x18-x27) должны сохраняться вызываемой функцией на стеке (если функция хочет их использовать) перед входом в функцию и восстанавливаться перед выходом, .

Далее табличка из интернета, описывающая каждый регистр, не стал переводить, и так все понятно:


Все 32 регистра в одной таблице

А вот теперь моя вольная интерпретация некоторых регистров.

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, стоит рассказать. Существует две спецификации набора инструкций:


  1. Непривилегированный набор инструкций
  2. Привилегированный набор инструкций

В нашем китайском микроконтроллере используется оба набора.


Непривилегированный набор инструкций

Спецификация на этот набор описывает инструкции и функциональность которые обычно используются во всех режимах привилегий, т.е. общие для всех архитектур набор инструкций и функций. Спецификация на этот набор доступна здесь: Непривилегированный ISA


Привилегированный набор инструкций

Основное её назначение — это разделение уровня приложений и уровня ядра, а также поддержка операционных систем вплоть до нескольких разных операционных систем типа Linux, работающих через виртуальную машину.

Но нас это не особо беспокоит, у нас же небольшой микроконтроллер, который из всего этого дела использует почти самую простую форму привилегированности.

Спецификация на привилегированный набор описывает возможную архитектуру привилегированных режимов, в том числе специальные инструкции и дополнительную функциональность для каждого из них. Спецификация доступна здесь: Привилегированная ISA

Следует уточнить, что эта спецификация носит рекомендованный характер, и она описывает только одно из возможных решений. Основное её преимущество, в том, что привилегированная архитектура никак не задевает основную непривилегированную функциональность и является её расширением.
Далее речь пойдет как раз о некоторых деталях этой привилегированной архитектуры, так как есть кое-какие нюансы, которые нужно знать при работе с ловушками.


Уровни привилегий

В RISC-V архитектуре существует 3 уровня привилегий. Уровни привилегий используются для обеспечения защиты между различными компонентами программного обеспечения (например, пользовательским приложением и ядром операционной системы). Любые попытки выполнения операций, не разрешенных текущим режимом привилегий, вызовут исключение.
Ниже показаны значения режима привилегий:

Как видно из таблички, для микроконтроллеров, таких как GD32VF103 рекомендованы режимы M или М и U. Собственно он и поддерживает оба режима. И если микроконтроллер работает в пользовательском режиме U, то ему недоступны настройки машинного режима и доступ к машинным регистрам, таким как mtvt, mepc, о них речь пойдет немного ниже. И чтобы обратиться к ним, вам необходимо зайти в ловушку, так как в GD32VF103, при попадании в ловушку ядро переходит в машинный режим.

Т.е. любое прерывание или исключение переводит ядро в машинный режим M и уже внутри него можно обращаться к машинным регистрам. В общем случае алгоритм доступа к машинным регистрам из пользовательского режима выглядит следующим образом — вам надо вызывать инструкцию ecall — запрос среды исполнения, которая переведет микроконтроллер в машинный режим и вызовет обработчик ловушки, в котором вы можете поменять машинные регистры в соответствии с запросом. Переход же из машинного режима в пользовательский происходит после команды mret — возврат из машинного режима.

Забегая вперед скажу, что хотя микроконтроллер и поддерживает два режима, после сброса он находится в машинном режиме, и переводить в пользовательский режим мы его не будет, чтобы не нагружать итак уже большую статью.

Примечание:
Согласно стандартной привилегированной архитектуре RISC-V, мы не можем на прямую узнать текущий привилегированный режим (например, машинный режим или режим пользователя).


Режим привилегий микроконтроллера GD32VF103

Пусть вас не смущает буква F в названии микроконтроллера GD32VF103 — это просто маркетинговое название, чтобы было похоже на уже существующую линейку GD32F103 на ядре CortexM3. Никакой поддержки инструкций с плавающей точкой здесь нет. Наверное, ставка была на то, что кто-то спутает GD32F103 с ST32F103 и не заметит подвоха…, а затем еще спутает и GD32VF103 c GD32F103. Мой продавец попался в эту ловушку (это другая ловушка, если что), и вначале мне пришел микроконтроллер GD32F103, вместо GD32VF103.

Этот микроконтроллер построен на архитектуре RV32IMAC — что идентифицирует микроконтроллер как RISC-V 32-битная архитектура с 32-битными регистрами общего назначения, который имеет целочисленные инструкции умножения и атомарные инструкции, инструкции сжаты до 16 бит для уменьшения размера кода.

Микроконтроллер может использоваться в защищенных системах, для которых достаточно только два режима:


  • Машинный Режим (Machine Mode), повторюсь, режим который имеет наивысший уровень привилегий и который является обязательным.
  • Пользовательский режим (User Mode), который можно конфигурировать.
    Как я уже говорил выше, привилегированная спецификация это не панацея и производители могут добавлять и даже изменять архитектуру. В данном случае, ребята добавили несколько подрежимов Машинного режима. Почитать о ней можно тут: Nuclei privileged ISA


Подрежимы Машинного режима

Существует 4 подрежима:


  • Нормальный подрежим (Normal Mode — 0×0)
    Ядро будет находиться в этом подрежиме после сброса и работать в нем до тех пора пока не произойдет прерывание, немаскируемое прерывание (NMI) или исключение.
  • Подрежим обработки исключения (Exception Handling Mode — 0×2)
    Ядро находится в этом режиме когда оно обрабатывает исключение.
  • Подрежим обработки немаскируемого прерывания (NMI Handling Mode — 0×3)
    Ядро находится в этом подрежиме когда оно обрабатывает немаскируемое прерывание NMI.
  • Подрежим обработки прерывания (Interrupt Handling Mode — 0×1)
    Ядро находится в этом подрежиме когда оно обрабатывает прерывание.

Эти подрежимы можно узнать из поля TYP машинного регистра msumbm

По умолчанию после сброса ядро находится в машинном режиме в подрежиме 0 (Нормальный подрежим работы) и вообще для большинства применений этого и достаточно, потому как у нас есть полный доступ ко всем регистрам и пользовательским и машинным.
Собственно, в моем примере я буду использовать только такой режим, но если мы сильно хотим ограничить пользователя от настроек ядра, например, запретить пользователю изменять машинные регистры из задач операционной системы, то мы всегда можем перейти в режим пользователя. Для этого, в нормальном подрежиме машинного режима, необходимо просто выполнить инструкцию mret — возврат из машинного режима, предварительно подменив в регистре mstatus поле MPP на пользовательский режим, а также поставив правильный адрес возврата в регистре mepc. Вот про эти странные регистры мы сейчас и узнаем.


Регистры статуса и управления CSR (Control and Status Registers)

Я тут уже вскользь упомянул регистры mstatus, mepc, msumbm, mtvt …, так что это за регистры?

Эти регистры встроены в ядро микроконтроллера, поэтому доступ к ним можно осуществить только с помощью специальных команд ассемблера, например cssr или csrr.

Это не очень хорошо, так как я собирался использовать для доступа к ним мою обертку над регистрами, а она не подходит для доступа к регистрам ядра, из-за того, что доступ к ним осуществляется особым образом через эти специальные команды.

Чтобы не трогать уже написанную обертку и генератор регистров, я сделал отдельный класс для их обработки.
На пользователей это никак не повлияло, а я получил возможность удобно обращаться к таким регистрам. Суть класса таже самая — только вместо прямого чтения, все сделано на ассемблере, встроенных в IAR функции доступа к CSR регистрам. (Было лень писать на ассемблере просто взял встроенные функции IAR, но правильно переписать на ассме, чтобы подходило для GCC тоже).

Вот так выглядит метод чтения значения такого регистра

 //Метод Get возвращает целое значение регистра, будет работать только для регистров, которые можно считать
 template::value ||
                                          std::is_base_of::value>>
 inline static Type Get()
 {
   return __read_csr(address) ;
 }

Пример доступа к специальному регистру на ассемблере

unsigned long get_mstatus()
{
  unsigned long value;
  asm volatile("csrr %0, 0x300" : "=r"(value));
  return value;
}

auto mstatus = get_mstatus() ;

и через обертку

auto mstatus = CSR::MSTATUS::Get() ;

Регистров целая куча, есть регистры, которые обязательны в соответствии со спецификацией, а есть уже добавленные производителем. CSR регистры существуют для каждого режима, поэтому в общем случае они называются xимярегистра, например xstatus — может быть регистр mstatus — регистр статуса машинного режима, ustatus — регистр статуса пользовательского режима. И, например, доступ к m регистрам запрещен из пользовательского режима, а к u регистрам разрешен.

Под спойлером описание всех регистров статуса и управления нашего микроконтроллера.


CSR регистры микроконтроллера

Для нашей задачи нам не нужны все регистры, мы ограничимся только теми, что нужны для решения конкретно нашей задачи. Напомню её на всякий случай — поморгать светодиодами.


Регистр mcause

Регистр указывающий причину возникновения прерывания. Табличку скрыл под спойлер, чтобы места не занимала.


Описание полей регистра mcause


Регистр mtvt2

Регистр хранящий адрес общего обработчика прерываний в не-векторном режиме при работе ECLIC контроллера.


msumb

Специализированный регистр ядра Bumblebee, хранящий текущий машинный подрежим и подрежим, в которой было ядро перед входом в текущую ловушку.


mstatus

Регистр mstatus отслеживает и управляет текущим рабочим состоянием аппаратного потока (hart). Также под спойлер.


Описание полей регистра mstatus


mmisc_ctl

Регистр содержит настройку того, чему равно значение регистра mnvec, в котором лежит адрес обработчика ловушки NMI.

Содержит единственно поле — бит номер 9 (NMI_CAUSE_FF). Да, именно бит номер 9 — в принципе, почему бы и нет.

Так вот, если NMI_CAUSE_FF(бит 9) равен 0×0, то значение регистра mnvec будет равен адресу содержащемуся по вектору сброса. Если этот бит равен 0×1, то значение регистра mnvec будет равно значению, лежащему в регистре mtvec, т.е. NMI исключения и прерывания будут обрабатываться через одну ловушку, а номер обработчика будет равен 0xFFF.


mepc

Регистр содержащий адрес возврата из ловушки. Адрес возврата автоматически сохраняется в этом регистре при возникновении исключения или прерывания. При возврате из ловушки он восстанавливается из это регистра в pc.
Этот регистр можно изменять, что используется в RTOS при переключении на другую задачу.


mtvec

Регистр содержащий адрес ловушки. Может содержать как адрес ловушки прерываний, так и адрес ловушки обработчика исключений, зависит от настроек в регистре mmisc_ctl

Ну вот, все нужные регистры изучены. Теперь можно прояснить ситуацию с обработкой прерываний и исключений.


Исключения и прерывания

Как я уже говорил, существует 3 различные ловушки событий, которые прерывают поток выполнения программы. Эти события разделяются на


  • исключения (синхронные события),
  • NMI(асинхронное немаскируемое событие),
  • прерывания(асинхронные маскируемые события).

Каждое из таких событий обрабатывается ядром немного по-разному.


Исключения и таблица исключений

Исключения обрабатываются отдельно. Базовый набор исключений ядра 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 подключены прерывания.
cl8o2hytrue-1wzjl5wxuyxvia8.png

Контроллер поддерживает следующие возможности:
Поиск обработчика по номеру прерывания, разрешение/запрещение прерываний, возведение флага прерывания, определение прерывания по его уровню и фронту, приоритизацию прерываний, векторный и невекторный режимы.

Все его режимы рассматривать не будем. Узнаем только про то, что нам надо. Всего в нашем китайском микроконтроллере 87 источников прерываний.


87 прерываний микроконтроллера GD32VF103


Обработка прерываний

Процесс обработки прерываний и исключений довольно прост. При возникновении таких событий, вход в ловушку Исключения/Прерывания/NMI происходит практически одинаково и включает в себя следующие шаги, которые выполняются одновременно за один цикл:


Вход в ловушку


  • При входе в ловушку ядро обновляет CSR контрольные регистры
    • mcause
    • mepc
    • mstatus
    • mintstatus для прерывания или исключения
  • Одновременно ядро переходит в Машинный Привилегированный режим и в соответствующий подрежим машинного режима
  • В это же время останавливается выполнение текущей программы и PC загружается адрес обработчика ловушки в зависимости от того, какое событие произошло — Исключением, Прерывание или NMI. Адрес обработчика может браться из разных регистров.

Важно, что обработчик ловушки находится всегда в Машинном режиме.

На рисунке я показал синим — шаги которые одинаковы для всех видов ловушек и шаги, разными цветами уникальные шаги для входа в различные ловушки.

6sneibnbwow5krdv0y87efbccom.png

Нам понадобится эта картинка для того, чтобы правильно сделать обработчики прерываний.


Выход из прерывания

Картинку рисовать не буду, опишу в общих деталях:


  • При выходе из ловушки ядро прекращает работу текущей программы, загружает в PC адрес, который записан в регистр mepc и переходит на него
  • Обновляет следующие CSR регистры:
    • mstatus
    • mcause
    • mintstatus
  • Обновляет режим привилегий и машинные подрежимы, возвращаясь в те режимы, что были до входа в ловушку.

Все это дело выполняется за один цикл.

В RISC-V нет автоматического stacking и unstacking как в CortexM ядрах, поэтому все 31 регистр общего назначения придется сохранять и восстанавливать руками.

Давайте разберемся как же обрабатывать прерывания. Как видно из картинки существует два режима обработки прерываний — векторный, через таблицу векторов и не-векторный — через единый обработчик прерывания. Также существует несколько способов (например, Interrupt Tail-Chaining) сделать обработку прерываний эффективнее.
Чтобы не раздувать, и так уже офигенно большую статью, я покажу как реализовать только не-векторный режим без оптимизации и шаманства. Но вкратце опишу оба.


Векторный и невекторный режиме работы прерываний

Контроллер прерываний ECLIC позволяет выбрать режим обработки прерываний и обеспечивает гибкость для выбора поведения каждого отдельного прерывания — либо с использованием аппаратной векторизации, либо без неё. В результате это позволяет пользователям оптимизировать каждое прерывание и пользоваться преимуществом обоих видов поведения. Аппаратная векторизация имеет более быстрый механизм обработки прерывания, но и имеет больший объем кода (из-за сохранения и восстановления контекста для каждого из прерываний). Напротив, невекторный режим имеет преимущество в размере кода, так как используется только один обработчик всех прерываний, но обработка происходит медленнее. Какой режим выгоднее, выбирает разработчик. Я выбрал не-векторный.


Векторный режим

В этом режиме при возникновении прерывания контроллер прерываний переходит на адрес прерывания, который указан в таблице векторов прерываний в соответствии с номером прерывания (алгоритм работы очень похож на обработку прерываний CortexM).


Не-векторный режим обработки прерываний

По умолчанию все прерывания настроены в не-векторный режим. Т.е. для обработки прерывания существует только один единый обработчик.

Тип обработки прерывания указывается в регистре CLICINTATTR[i] в поле SHV. По умолчанию там записан 0 — в этом случае прерывание настроено на не-векторный режим, т.е. при возникновении прерывания или исключения контроллер всегда вызывает единый обработчик, находящийся по адресу, указанному в регистре mtvec или mtvt2, в зависимости от настроек и типов ловушки.
В этом обработчике необходимо определить, какое прерывание произошло и вызвать необходимую функцию обработки прерывания. Узнать, что за прерывание произошло можно с помощью регистра mcause — который хранит в себе номер прерывания в поле EXCCODE.


Регистры контроллера прерываний ECLIC

Для настройки контроллера нам понадобится описание его регистров. Ниже я привел табличку с 7 регистрами, но на самом деле их на много больше, так как i — означает номер прерывания. Т.е. существует 87 clicintip, и 87 clicintie, и 87 clicintattr и 87 clicintctl, каждый из которых отвечает за свое прерывание.

Ну, а теперь, надо же описать, что это за регистры…


Регистр MTH

Регистр, который задает уровень срабатывания прерывания. Как видно из картинки, можно управлять не только приоритетом прерывания, но и уровнем прерывания и делать, что-то типа прореживания, мол если уровень прерывания ниже определенной границы, то это и не прерывание вовсе и не нужно его возводить.

2m1qxbhexzi1oy5hdnnr6khs0zi.png

Сам уровень конкретного прерывания, как было сказано задается clicintctl[i].

А вот уровень срабатывания — как раз задается регистром mth. Собственно это просто 8-битный регистр, хранящий уровень срабатывания прерывания.


Регистр CLICINTCTL[i]

Регистр используется для задания уровня и приоритета прерывания. Как будет рассказано ниже, старшие биты (эффективные биты), количество которых задается в регистре CLICCFG указывают уровень прерывания, а младшие — приоритет. Количество эффективных битов также можно считать из регистра CLICINFO в поле CLICINTCTLBITS.


Регистр CLICCFG

Регистр общей конфигурации прерываний. Он задает количество эффективных битов, ответственных за установку уровня и приоритета прерывания. Чтобы было понятнее, приведу картинку.

vurthszaxwvh_un6sg1qs03fwjq.png

Непонятно? Тогда следите за описанием бита nlbits в табличке, должно много прояснить.


Регистр CLICINFO

Регистр общей информации о системе прерываний


CLICINTIP[i]

Регистр содержащий единственный флаг запроса прерывания. i — обозначает номер прерывания. Наш контроллер содержит 87 прерываний, поэтому будет 87 таких регистров.


CLICINTIE[i]

Регистр разрешения прерываний. Их тоже 87.


CLICINTATTR[i]

Регистр настройки источника прерываний. Как было показано выше, контроллер прерываний может работать в нескольких режимах. Прерывания могут срабатывать по уровню, по фронту переднем или заднему, а также тип прерывания векторный или не векторный. И их тоже 87.

Все регистры кончились, осталось описать, как работает машинный таймер и порты… уже немножко и можно будет моргать.


Машинный таймер

Машинный таймер — это как системный таймер в CortexM, но только машинный. Сам машинный таймер является неотъемлемой частью привилегированной архитектуры ядра, доступ к нему должны иметь все аппаратные потоки (hart). И для работы с ним даже выделили регистры mtime и mtimecmp.
Спецификация привилегированной архитектуры рекомендует сделать эти регистры как обычные регистры, а не регистры CSR. Собственно китайские ребята так и сделали, вот только в документациях, я нигде не смог найти на каком адресе находятся эти регистры.

Пришлось вытащить его из примеров. Находится регистр mtime по адресу 0xd1000000, а регистр mtimecmp по адресу 0xd1000008.

Размер у обоих регистров 64 бита. И отвечают они за:


  • mtime — регистр содержащий счетчик таймера
  • mtimecmp — регистр сравнения. Когда значение таймера в регистре mtime будет равно значению с регистре mtimecmp таймер поставит флаг запроса на прерывание в регистре CLICINTIP[7].

Запись в эти регистры гарантированно сбросит флаг запроса прерывания, но это неточно, потому что он сбрасывается не сразу и возможны паразитные прерывания таймера. Однако, как говорит спецификация-это очень редкое событие, и потому все нормально.


Writes to mtime and mtimecmp are guaranteed to be reflected in MTIP eventually, but not necessarily immediately.

A spurious timer interrupt might occur if an interrupt

© Habrahabr.ru