Pull to refresh

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

Reading time41 min
Views25K


Сегодня речь пойдет о модном — о 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), поэтому, как обычно, начнем с азов. Итак, поехали.


Какая поддержка уже существует у 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
Сокращение Наименование Версия Статус
Базовые наборы команд
RV32I 32-битный базовый набор с целочисленными операциями с 32 регистрами общего назначения 2.1 Ratified
RV32E 32-битный базовый набор с целочисленными операциями для встраиваемых систем с 16 регистрами общего назначения 1.9 Draft
RV64I 64-битный базовый набор с целочисленными операциями с 32 регистрами общего назначения 2.1 Ratified
RV128I 128-битный базовый набор с целочисленными операциями 1.7 Draft
Стандартные расширенные наборы команд
M Целочисленное умножение и деление (Integer Multiplication and Division) 2.0 Ratified
A Атомарные операции (Atomic Instructions) 2.1 Ratified
F Арифметические операции с плавающей запятой над числами одинарной точности (Single-Precision Floating-Point) 2.2 Ratified
D Арифметические операции с плавающей запятой над числами двойной точности (Double-Precision Floating-Point) 2.2 Ratified
G Сокращенное обозначение для комплекта из базового и стандартного наборов команд н/д н/д
Q Арифметические операции с плавающей запятой над числами четвертной точности 2.2 Ratified
L Арифметические операции над числами с фиксированной запятой (Decimal Floating-Point) 0.0 Open
C Сокращённые имена для команд (Compressed Instructions) 2.2 Ratified
B Битовые операции (Bit Manipulation) 0.36 Open
J Двоичная трансляция и поддержка динамической компиляции (Dynamically Translated Languages) 0.0 Open
T Транзакционная память (Transactional Memory) 0.0 Open
P Короткие SIMD-операции (Packed-SIMD Instructions) 0.1 Open
V Векторные расширения (Vector Operations) 0.2 Open
N Инструкции прерывания (User-Level Interrupts) 1.1 Open

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


Но нам нужна только небольшая часть из всего этого, так как на самом деле для микроконтроллеров общего назначения используется в основном 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-x17), а также регистр адреса возврата являются рабочими регистрами. Любая функция может изменять содержимое этих регистров и если ей нужно воспользоваться какими-то из этих регистров после вызова другой функции, она должна сохранить их значение на стеке.


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


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


Все 32 регистра в одной таблице
Register ABI Name Description Saver
x0 zero Hard-wired zero
x1 ra Return address Caller
x2 sp Stack pointer Callee
x3 gp Global pointer
x4 tp Thread pointer
x5 t0 Temporary/alternate link register Caller
x6–7 t1–2 Temporaries Caller
x8 s0/fp Saved register/frame pointer Callee
x9 s1 Saved register Callee
x10–11 a0–1 Function arguments/return values Caller
x12–17 a2–7 Function arguments Caller
x18–27 s2–11 Saved registers Callee
x28–31 t3–6 Temporaries Caller
pc pc Program counter

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


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 уровня привилегий. Уровни привилегий используются для обеспечения защиты между различными компонентами программного обеспечения (например, пользовательским приложением и ядром операционной системы). Любые попытки выполнения операций, не разрешенных текущим режимом привилегий, вызовут исключение.
Ниже показаны значения режима привилегий:


Уровень Код режима Имя Сокращенное название Описание
0 00 User/Application U Самый низкий уровень привилегий
1 01 Supervisor S
2 10 Reserved
3 11 Machine M Самый высокий уровень привилегий

Поддерживаемые режимы Предполагаемое использование
M Системы со встроенным ПО
M, U Защищенные системы со встроенным ПО и операционными системами реального времени
M,S,U Системы с Unix подобными операционными системами

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


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


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


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


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


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


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


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


  • Машинный Режим (Machine Mode), повторюсь, режим который имеет наивысший уровень привилегий и который является обязательным.
  • Пользовательский режим (User Mode), который можно конфигурировать.

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


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


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


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

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


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


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


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


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


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


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


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


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


 //Метод Get возвращает целое значение регистра, будет работать только для регистров, которые можно считать
 template<typename T = AccessMode,
        class = typename std::enable_if_t<std::is_base_of<ReadMode, T>::value ||
                                          std::is_base_of<ReadWriteMode, T>::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 регистры микроконтроллера
Адрес Доступ Имя Описание
Стандартные регистры машинного режима, соответствующие спецификации привилегированной архитектуры RISC-V CSR (Machine Mode)
0xF11 MRO mvendorid (Machine Vendor ID Register) Регистр содержащий код производителя ядра, который выдается JEDEC ассоциацией
0xF12 MRO marchid (Machine Microacrhitecture ID Register) Идентификатор микроархитектуры ядра
0xF13 MRO mimpid (Machine Implementation ID Register) Идентификатор номера версии ядра.
0xF14 MRO mhartid (Hart ID Register) Идентификатор аппаратного потока, который выполняет код.
0x300 MRW mstatus (Machine Status Register) Регистр содержит текущее состояние и управляет текущим состоянием аппаратного потока
0x301 MRO misa (Machine ISA Register) Идентификатор набора команд, собственно в нем закодирован поддерживаемый набор команд
0x304 MRW mie (Machine Interrupt Enable Register) Регистр отвечает за включение прерываний при использовании PLIC (platform-level interrupt controller)
0x305 MRW mtvec (Machine Trap-Vector Base-Address Register) Регистр содержит адрес обработчика(ловушки) исключений.
0x307 MRW mtvt (ECLIC Interrupt Vector Table Base Address) Регистр содержит базовый адрес вектора прерываний для ECLIC контроллера. На самом деле спецификация на контроллер прерываний еще не утверждена, поэтому это не совсем стандартный регистр.
0x340 MRW mscratch (Machine Scratch Register) Назовем его регистр-записная книжка, обеспечивает механизм сохранения и восстановления специфических данных для ограничения доступа к данным более высокого уровня привилегий из низкого уровня привилегий. Например, после входа в режим прерывания или обработки исключений регистр указателя стека приложения (sp) временно сохраняется в регистре mscratch. Перед выходом из обработчика исключений значение в регистре-записная книжка используется для восстановления регистра указателя стека (sp). Программное обеспечение может получить доступ к этому регистру только из машинного режима.
0x341 MRW mepc (Machine Exception Program Counter) Регистр, который содержит в себе адрес инструкции, которая была прервана исключением или прерыванием. Регистр может быть явно изменен программой в машинном режиме. Младший бит этого регистра всегда равен 0.
0x342 MRW mcause (Machine Cause Register) Этот регистр индицирует событие, которое стало причиной исключения.
0x343 MRW mtval (Machine Trap Value Register). Регистр содержащий специфическую информацию, чтобы помочь с обработкой исключения, например, может хранить код инструкции вызвавшей исключение или адрес в котором произошла ошибка.
0x344 MRW mip (Machine Interrupt Pending Register). Содержит информацию об ожидающих прерываниях, при использовании PLIC (platform-level interrupt controller).
Ox345 MRW mnxti (Next Interrupt Handler Address and Interrupt-Enable CSR) Регистр, содержащий адрес следующего обработчика прерываний. Может использоваться программным обеспечением для обработки следующего прерывания, когда оно находится в том же режиме привилегий, без очистки конвейера прерываний и затрат на сохранения/восстановления контекста. Тоже регистр из неутвержденной спецификации на контроллер прерываний
0x346 MRO mintstatus (Current Interrupt Levels). Регистр содержащий уровень активного прерывания в машинном режиме. Регистр из неутвержденной спецификации на контроллер прерываний
0x348 MRW mscratchcsw (Scratch swap register for privileged mode). Этот регистр используется для того, чтобы выполнить обмен значения хранящиеся в одном из регистров ядра с регистром mscratch (например для обмена значений указателя на стек sp и mscratch). Используется при входе в прерывание и смене режима привилегий для разграничения доступа к данным между уровнями привилегий. Регистр из неутвержденной спецификации на контроллер прерываний
0x348 MRW mscratchcswl (Scratch swap register for interrupt levels). Этот регистр также используется для обмена значений между регистром ядра и регистром mscratch, но в случае когда уровень привилегий не меняется. В частности он используется для ускорения обработки прерывания при переключении между несколькими уровнями прерываний. Регистр из неутвержденной спецификации на контроллер прерываний
0xB00 MRW mcycle (Lower 32 bits of Cycle counter). Младшие 32 бита счетчика циклов
0xB80 MRW mcycleh (Upper 32 bits of Cycle counter). Старшие 32 бита счетчика циклов
0xB02 MRW minstret (Lower 32 bits of Instructions-retired counter). Младшие 32 бита счетчика успешно выполненных инструкций.
0xB82 MRW minstreth (Lower 32 bits of Instructions-retired counter). Старшие 32 бита счетчика успешно выполненных инструкций.
Стандартные регистры пользовательского режима. RISC-V Standard CSR (User Mode)
0xC00 URO cycle Копия регистра mсycle, для чтения из пользовательского режима
0xC01 URO time Копия регистра mtime, содержащий младшие 32 бита счетчика машинного таймера.
0xC02 URO instret Копия регистра minstret, для чтения из пользовательского режима
0xC80 URO cycleh Копия регистра mcycleh, для чтения из пользовательского режима.
0xC81 URO timeh Копия регистра mtimeh, содержащий старшие 32 бита счетчика машинного таймера.
0xC82 URO instreth Копия регистра minstreth, для чтения из пользовательского режима
0x810 MRW wfe Регистр для управления низко-потребляющим режимом
**Специализированные регистры ядра Bumblebee. Bumblebee Customized CSR&&
0x320 MRW mcountinhibit (Customized register for counters on & off). Регистр для управления включением отключением подсчета тактов (регистр mcycle) и количества успешных команд (minstret).
0x7c3 MRO mnvec (NMI Entry Address). Адрес обработчика NMI.
0x7c4 MRW msubm (Customized Register Storing Type of Trap). Регистр хранит тип текущей ловушки и ловушки до входа в текущую ловушку.
0x7d0 MRW mmisc_ctl (Customized Register holding NMI Handler Entry Address). Адрес обработчика прерываний NMI.
0x7d6 MRW msavestatus (Customized Register holding the value of mstatus). Регистр хранит значения регистров mstatus и msubm, что гарантирует, что эти регистры не будут сброшены исключением или NMI.
0x7d7 MRW msaveepc1 (Customized Register holding the value of mepc for the first-level preempted NMI or Exception). Регистр хранит значение регистра mepc.
0x7d8 MRW msavecause1 (Customized Register holding the value of mcause for the first-level preempted NMI or Exception). Регистр хранит значение регистра mcause.
0x7d9 MRW msaveepc2 (Customized Register holding the value of mepc for the second-level preempted NMI or Exception). Регистр хранит значение регистра mepc.
0x7eb MRW pushmsubm (Push msubm to stack). Вспомогательный регистр, обеспечивает метод сохранения регистра msubm в стеке.
0x7ec MRW mtvt2 (ECLIC non-vectored interrupt handler address register). Регистр хранит адрес единого обработчика прерывания в режиме не-векторной обработки.
0x7ed MRW jalmnxti (Jumping to next interrupt handler address and interrupt-enable register). Вспомогательный регистр, используется для того, чтобы уменьшить задержки прерываний и ускорить обработку цепочки последовательно происходящих прерываний.
0x7ee MRW pushmcause (Push mcause to stack). Вспомогательный регистр, обеспечивает метод сохранения регистра mcause в стеке.
0x7ef MRW pushmepc (Push mepc to stack). Вспомогательный регистр, обеспечивает метод сохранения регистра mepc в стеке.
0x810 MRW wfe (Wait for Event Control Register) Регистр настройки способа пробуждения микроконтроллера от прерывания, NMI или от события .
0x811 MRW sleepvalue (WFI Sleep Mode Register). Регистр содержащий настройку режима пониженного энергопотребления
0x812 MRW txevt (Send Event Register). Регистр настройки события

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


Регистр mcause


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


Описание полей регистра mcause
Полн Биты Описание
INTERRUPT 31 Тип ловушки: 0x0: Исключение или NMI 0x1: Прерывание
MINHV 30 Указывает на, что микроконтроллер находится состоянии чтения таблицы векторов прерываний. Это поле доступно только в при работе ECLIC контроллера.
MPP 29:28 Режим привилегий значение регистра mstastus.MIE до входа в ловушку: 0x0: Привилегии пользователя, 0x1: Привилегии супервизора, 0x2: Зарезервировано, 0x3: Режим машинных привилегий.
MPIE 27 Значение регистра mstastus.MIE перед входом в ловушку: 0x1: Машинное прерывание было разрешено. 0x0: Машинное прерывание было запрещено
Reserved 26:24 Reserved 0
MPIL 23:16 Уровень прерывания до входа в обработчик прерывания
Reserved 15:12 Reserved 0
EXCCODE 11:0 Номер(ID) прерывания. EXCCODE для NMI может быть 0x1 или 0xfff. Значение управляется регистром mmisc_ctl.

Регистр mtvt2


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


Поле Биты Описание
CMMON-CODE-ENTRY 31:2 Когда mtvt2.MTVT2EN=1, это поле определяет адрес общего обработчика в не-векторном режиме ECLIC контроллера.
Резерв 1 Значение 0
MTVT2EN 0 Бит активации mtvt2. Если он равен 0x0: то адрес общего обработчика прерывания в не-векторном режиме ECLIC контроллера определяется регистром mtvec. Если он равен 0x1: то адрес общего обработчика прерывания в не-векторном режиме ECLIC контроллера определяется регистром полем mtvt2.CMMON-CODE-ENTRY

msumb


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


Поле Бит Описание
Резерв 31:10 Все биты установлены в 0
PTYP 9:8 Машинный подрежим перед входом в ловушку. 0x0: Нормальный Машинный режим, 0x1: Подрежим обработки прерываний 0x2: Подрежим обработки исключения 0x3: Подрежим обработки NMI
TYP 7:6 Текущий машинный подрежим. 0x0: Нормальный Машинный режим, 0x1: Подрежим обработки прерываний x02: Подрежим обработки исключения 0x3: Подрежим обработки NMI
Резерв 5:0 Все биты установлены в 0

mstatus


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


Описание полей регистра mstatus
Поле Бит Описание
SD 31 Бит SD — это бит только для чтения, который служит для того, чтобы определить сигнализирует ли поле FS или поле XS о наличии Dirty состояния, которое потребует сохранения контекста расширений микроконтроллера в памяти. По сути этот бит определяется следующей логической операцией: SD = (((FS == 0x3)) or (DS == 0x3)). SD можно проверить при переключения контекста, чтобы быстро определить, требуется ли сохранение или восстановление состояния в блоке FPU или дополнительных расширений
XS 16:15 Бит XS кодирует состояние пользовательских расширений, включая дополнительные регистры и CSR регистры и используется для снижения затрат на сохранение и восстановление контекста. 0x0: — (Off) расширение отключено, любая вызванная инструкция этого расширения вызовет исключение, 0x1: (Initial) когда состояние является начальным и имеет некое постоянное значение, 0x2: (Clear) соответствующее состояние потенциально отличается от начального значения, но соответствует последнему сохраненному значению контекста. 0x3: (Dirty)соответствующее состояние потенциально было изменено с момента последнего сохранения контекста и требуется его сохранение или восстановление.
FS 13:14 Бит XS кодирует состояние модуля FPU, включая дополнительные регистры(f0-f31) и CSR регистры и используется для снижения затрат на сохранение и восстановление контекста. 0x0: — (Off) FPU отключен, любая вызванная инструкция FPU вызовет исключение, 0x1: (Initial) когда состояние является начальным и имеет некое постоянное значение, 0x2 — (Clear) соответствующее состояние потенциально отличается от начального значения, но соответствует последнему сохраненному значению контекста. 0x3: (Dirty)соответствующее состояние потенциально было изменено с момента последнего сохранения контекста и требуется его сохранение или восстановление.
MPP 11:12 Хранит текущий режим привилегий перед входом в ловушку. 0x0: Пользовательский режим, 0x1: Режим Супервизора, 0x3: — Машинный режим
MPIE 7 Значение MIE перед входом в ловушку.
MIE 3 Глобальное разрешение машинного прерывания 0x0: — Машинные прерывания запрещены. 0x1: — Машинные прерывания разрешены.

mmisc_ctl


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


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


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


mepc


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


mtvec


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


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


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


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


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

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


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


Исключения обрабатываются отдельно. Базовый набор исключений ядра Bumblebee нашего микроконтроллера выглядит так:


Таблица исключений
Код исключения Тип Исключения/Прерывания Синхронное/Асинхронное Описание
0 Адрес инструкции не выровнен Синхронное Адрес в PC не выровнен. Это тип исключения не возникает в ядрах с сокращенными командами (С расширение ядра).
1 Ошибка доступа к инструкции Синхронное
2 Недопустимая инструкция Синхронное
3 Точка останова Синхронное Архитектура RISC-V определяет инструкцию BREAK. При выполнении этой инструкции ядро войдет в обработчик исключений. Эта инструкция обычно используется отладчиком, для установки точек останова.
4 Доступ по не выровненному адресу при операции чтения Синхронное(исключение) Ядро Bumblebee не поддерживает невыровненный доступ к памяти, поэтому доступ к памяти по невыровненным адресам вызовет исключение.
5 Ошибка доступа к памяти при операции чтения Асинхронный
6 Доступ по не выровненному адресу при операции записи Синхронное(исключение) Ядро Bumblebee не поддерживает невыровненный доступ к памяти, поэтому доступ к памяти по невыровненным адресам вызовет исключение.
7 Ошибка доступа к памяти при операции записи Асинхронный
8 Вызов окружения (команды ecall) из Пользовательского режима Синхронное RISC-V архитектура определяет инструкцию ECALL. При выполнении этой инструкции ядро войдет в обработчик исключений. Эта инструкция обычно используется программным обеспечением для принудительного перехода ядра в режим обработки исключений.
11 Вызов окружения (команды ecall) из машинного режима Синхронное RISC-V архитектура определяет инструкцию ECALL. При выполнении этой инструкции ядро войдет в обработчика исключений. Эта инструкция обычно используется программным обеспечением для принудительного перехода ядра в режим обработки исключений.

Теперь рассмотрим как обработать прерывания:


Прерывания


Прерывания — это асинхронные события прерывающие поток исполнения. У нашего микроконтроллера существует две реализации контроллеров прерывания 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 источников прерываний.


87 прерываний микроконтроллера GD32VF103
Номер прерывания Имя прерывания Адрес вектора прерывания
3 CLIC_INT_SFT 0x0000_000C
7 CLIC_INT_TMR 0x0000_001C
17 CLIC_INT_BWEI 0x0000_0044
18 CLIC_INT_PMOVI 0x0000_0048
19 WWDGT interrupt 0x0000_004C
20 LVD from EXTI interrupt 0x0000_0050
21 Tamper interrupt 0x0000_0054
22 RTC global interrupt 0x0000_0058
23 FMC global interrupt 0x0000_005C
24 RCU global interrupt 0x0000_0060
25 EXTI Line0 interrupt 0x0000_0064
26 EXTI Line1 interrupt 0x0000_0068
27 EXTI Line2 interrupt 0x0000_006C
28 EXTI Line3 interrupt 0x0000_0070
29 EXTI Line4 interrupt 0x0000_0074
30 DMA0 channel0 global interrupt 0x0000_0078
31 DMA0 channel1 global interrupt 0x0000_007C
32 DMA0 channel2 global interrupt 0x0000_0080
33 DMA0 channel3 global interrupt 0x0000_0084
34 DMA0 channel4 global interrupt 0x0000_0088
35 DMA0 channel5 global interrupt 0x0000_008C
36 DMA0 channel6 global interrupt 0x0000_0090
37 ADC0 and ADC1 global interrupt 0x0000_0094
38 CAN0 TX interrupts 0x0000_0098
39 CAN0 RX0 interrupts 0x0000_009C
40 CAN0 RX1 interrupts 0x0000_00A0
41 CAN0 EWMC interrupts 0x0000_00A4
42 EXTI line[9:5] interrupts 0x0000_00A8
43 TIMER0 break interrupt 0x0000_00AC
44 TIMER0 update interrupt 0x0000_00B0
45 TIMER0 trigger and channel commutation interrupts 0x0000_00B4
46 TIMER0 channel capture compare interrupt 0x0000_00B8
47 TIMER1 global interrupt 0x0000_00BC
48 TIMER2 global interrupt 0x0000_00C0
49 TIMER3 global interrupt 0x0000_00C4
50 I2C0 event interrupt 0x0000_00C8
51 I2C0 error interrupt 0x0000_00CC
52 I2C1 event interrupt 0x0000_00D0
53 I2C1 error interrupt 0x0000_00D4
54 SPI0 global interrupt 0x0000_00D8
55 SPI1 global interrupt 0x0000_00DC
56 USART0 global interrupt 0x0000_00E0
57 USART1 global interrupt 0x0000_00E4
58 USART2 global interrupt 0x0000_00E8
59 EXTI line[15:10] interrupts 0x0000_00EC
60 RTC alarm from EXTI interrupt 0x0000_00F0
61 USBFS wakeup from EXTI interrupt 0x0000_00F4
62 Reserved 0x0000_00F8
63 Reserved 0x0000_00FC
64 Reserved 0x0000_0100
65 Reserved 0x0000_0104
66 Reserved 0x0000_0108
67 Reserved 0x0000_010C
68 Reserved 0x0000_0110
69 TIMER4 global interrupt 0x0000_0114
70 SPI2 global interrupt 0x0000_0118
71 UART3 global interrupt 0x0000_011C
72 UART4 global interrupt 0x0000_0120
73 TIMER5 global interrupt 0x0000_0124
74 TIMER6 global interrupt 0x0000_0128
75 DMA1 channel0 global interrupt 0x0000_012C
76 DMA1 channel1 global interrupt 0x0000_0130
77 DMA1 channel2 global interrupt 0x0000_0134
78 DMA1 channel3 global interrupt 0x0000_0138
79 DMA1 channel4 global interrupt 0x0000_013C
80 Reserved 0x0000_0140
81 Reserved 0x0000_0144
82 CAN1 TX interrupt 0x0000_0148
83 CAN1 RX0 interrupt 0x0000_014C
84 CAN1 RX1 interrupt 0x0000_0150
85 CAN1 EWMC interrupt 0x0000_0154
86 USBFS global interrupt 0x0000_0158

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


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


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


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

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


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



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


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


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


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

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


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


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


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


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


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


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


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


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


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


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


В этом обработчике необходимо определить, какое прерывание произошло и вызвать необходимую функцию обработки прерывания. Узнать, что за прерывание произошло можно с помощью регистра mcause — который хранит в себе номер прерывания в поле EXCCODE.


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


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


Смещение Доступ Названия Длина
0x0000 RW cliccfg 8-bit
0x0004 R clicinfo 32-bit
0x000b RW mth 8-bit
0x1000+4*i RW clicintip[i] 8-bit
0x1001+4*i RW clicintie[i] 8-bit
0x1002+4*i RW clicintattr[i] 8-bit
0x1003+4*i RW clicintctl[i] 8-bit

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


Регистр MTH


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



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


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


Регистр CLICINTCTL[i]


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


Регистр CLICCFG


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



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


Поле Биты Доступ Значение по умолчанию Описание
Резерв 7 R N/A Зарезервировано, значение 0
nmbits 6:5 R N/A Режим привилегий прерываний. Для нашего микроконтроллера он всегда 0: машинный.
nlbits 4:1 RW 0 Используется для указания эффективной разрядности значения уровня в регистре clicintctl[i]. Т.е. если в этом регистре стоит значение 4, то при задании уровня в регистре clicintctl[i] можно использовать только 4 старших бита, остальные биты используются для задания приоритета. Обычно используется значение от 2 до 8.
nvbits 0 R N/A Для нашего микроконтроллера всегда 1: Поддерживает векторный режим. А если бы не поддерживал, был бы 0

Регистр CLICINFO


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


Поле Биты Разрешение значение по умолчанию Описание
Резерв 31:25 R N/A Зарезервировано, все значения в 0
CLICINTCTLBITS 24:21 R N/A Эффективная разрядность регистра clicintctl[i].
VERSION 20:13 R N/A Номер версии аппаратной реализации контроллера прерываний.
NUM_INTERRUPT 12:0 R N/A Количество источников прерываний, поддерживаемых микроконтроллером.

CLICINTIP[i]


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


Поле Биты Разрешение значение по умолчанию Описание
Резерв 7:1 RO N/A Зарезервировано, все значения в 0
IP 0 RW 0 Флаг ожидания источника прерывания. 1 — означает что прерывание сработало. Если контроллер настроен на работу с прерываниями по уровню, то программно его очистить нельзя. Он будет очищен автоматически когда будет очистен исходный источник прерывания.

CLICINTIE[i]


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


Поле Биты Разрешение значение по умолчанию Описание
Резерв 7:1 RO N/A Зарезервировано, все значения в 0
IE 0 RW 0 1 — означает что прерывание разрешено

CLICINTATTR[i]


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


Поле Биты Разрешение По умолчанию Описание
Резерв 7:6 R N/A Зарезервировано, значение 11b
Резерв 5:3 R N/A Зарезервировано, все значения 0
TRIG 2:1 RW 0 00b и 10b: Прерывание срабатывает по уровню. 01b: Прерывание срабатывает по положительному фронту. 11b: Прерывание срабатывает по отрицательному фронту.
SHV 0 RW 0 0x0: Прерывание обрабатывается в невекторном режиме. 0x1: Прерывание обрабатывается в векторном режиме.

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


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


Машинный таймер — это как системный таймер в 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 handler increments mtimecmp then immediately returns, because MTIP might not yet have fallen in the interim. All software should be
written to assume this event is possible, but most software should assume this event is extremely unlikely. It is almost always more performant to incur an occasional spurious timer interrupt than to poll MTIP until it falls.

В нашем случае, когда таймер досчитает до значения в mtimecmp, мы должны сбросить mtime в 0 в обработчике, чтобы флаг запроса на прерывание тоже сбросился.


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


Поле Биты Разрешение значение по умолчанию Описание
Резерв 7:1 RO N/A Зарезервировано, все значения в 0
MSIP 0 RW 0 1 — сгенерировать программное прерывание

Порты и регистры периферии


Порты в нашем китайском микроконтроллер очень похожи на порты в 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 void IrqEntry()
{
  const auto mcause = CSR::MCAUSE::Get(); 
  const auto mepc = CSR::MEPC::Get(); 
  const auto msubm = CSRCUSTOM::MSUBM::Get(); 
  //номер прерывания сохранен в mcause
  const auto exceptionCode =  mcause & 0xFFF ; 
  //вызываем обработчик нужного прерывания
  NonVectoredInt::HandleInterrupt(exceptionCode); 

  __disable_interrupt();
  CSR::MCAUSE::Write(mcause); 
  CSR::MEPC::Write(mepc); 
  CSRCUSTOM::MSUBM::Write(msubm) ; 
}

Во-первых, при входе мы сохранили все регистры на стеке(это у нас делает сам компилятор, потому что мы указали волшебное слово __interrupt), при выходе они восстанавливаются из стека.


Во-вторых, мы сохранили CSR регистры, которые необходимо сохранить в прерываниях, в соответствии с документацией, и при выходе мы их восстановили.


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


namespace NonVectoredInt
{
  static void  HandleInterrupt(std::uint32_t interruptId)
  {
    // проверим, что код прерывания не больше размера таблицы прерываний
    assert(interruptId < InterruptVectorTable.size());
    // ищем указатель на функцию обработчика в таблице прерываний 
    tInterruptFunction fp = InterruptVectorTable[interruptId];
    if (fp != nullptr)
    {
      fp(); // вызываем обработчик
    }
  }
} ;

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


using tInterruptFunction = void(*)() ;

inline constexpr std::array<tInterruptFunction,87> InterruptVectorTable
{
  nullptr,
  nullptr,
  nullptr,
  DummyModule::HandleInterrupt,//программное прерывание
  nullptr,
  nullptr,
  nullptr,
  SystemTimer::HandleInterrupt, //А вот и наш обработчик машинного таймера
  nullptr,
  nullptr,
  nullptr,
  nullptr,
  nullptr,
  nullptr,
  nullptr,
  nullptr,
  nullptr,
  DummyModule::HandleInterrupt,//eclic_bwei_handler,
  DummyModule::HandleInterrupt, //eclic_pmovi_handler,
  DummyModule::HandleInterrupt, //WWDGT_IRQHandler,
  DummyModule::HandleInterrupt,//LVD_IRQHandler,
....
} ;

Если вы заметили, под номером 7 стоит адрес функции обработки прерывания от машинного таймера, но следуя, моему внутреннему эго, я по старинке назвал его SystemTimer::HandleInterrupt


struct SystemTimer
{
    static void HandleInterrupt()
    {
        auto mtime = MACHINETIMER::MTIME::Get() - 
                     MACHINETIMER::MTIMECMP::Get();
        MACHINETIMER::MTIME::Write(mtime);
        AppTimerService::OnSystemTick() ;        
    }
};

В нем происходит вызов функции OnSystemTick() сервиса таймеров. Про него немного позже, а пока нужно настроить обработку исключений и NMI.


Для этого, с исключениями мы провернем точно такой же "фокус". Только таблица будет поменьше, исключений всего 12.


inline constexpr std::array<tInterruptFunction,12> ExceptionVectorTable
{
  DummyModule::HandleInterrupt,  //0 - Instruction address misaligned
  DummyModule::HandleInterrupt,  //1 - Instruction access fault
  DummyModule::HandleInterrupt,  //2 - Illegal instruction
  DummyModule::HandleInterrupt,  //3 - Breakpoint
  DummyModule::HandleInterrupt,  //4 - Load address misaligned
  DummyModule::HandleInterrupt,  //5 - Load access fault
  DummyModule::HandleInterrupt,  //6 - Store/AMO address  misaligned
  DummyModule::HandleInterrupt,  //7 - Store/AMO access fault
  EnvironmentCall::HandleInterrupt,  //8 - Environment call from  U-mode
  nullptr,
  nullptr,
  EnvironmentCall::HandleInterrupt,  //11 - Environment call from  M-mode
};

namespace NonVectoredInt
{    
  static void  HandleException(std::uint32_t exceptiontId)
  {
    assert(exceptiontId < ExceptionVectorTable.size());
    tInterruptFunction fp = ExceptionVectorTable[exceptiontId];
    if (fp != nullptr)
    {
      fp();
    }
  }
} ;

NMI будем обрабатывать вместе с исключениями. Зададим его номер — 0xFFF.


__interrupt void ExceptionEntry()
{
  const auto mcause = CSR::MCAUSE::Get();
  const auto mepc = CSR::MEPC::Get();
  const auto msubm = CSRCUSTOM::MSUBM::Get();

  const auto exceptionCode =  mcause & 0xFFF ;
  if (exceptionCode != 0xFFF) // если не NMI
  {
    NonVectoredInt::HandleException(exceptionCode);
  } else
  {
    DummyModule::HandleInterrupt() ; // а это если NMI
  }

  __disable_interrupt();
  CSR::MCAUSE::Write(mcause); 
  CSR::MEPC::Write(mepc); 
  CSRCUSTOM::MSUBM::Write(msubm) ; 
}

Теперь нужно указать, что NMI будет обрабатываться той же ловушкой, что используется для исключений, адрес обработчика которой будет указан в mtvec, а номер обработчика NMI будет 0xFFF, см описание регистра MMISC_CTL выше в статье.


 // Устанавливаем указание адреса обработчика NMI через обработчик, 
 // адрес которого указан в mtvec. Номер обработчика NMI будет 0xFFF
 CSRCUSTOM::MMISC_CTL::NMI_CAUSE_FFF::MnvecIsMtvecNmiIsFFF::Set();

Настроим в каком регистре будет указан адрес ловушки общего обработчика прерываний


// Настраиваем адрес единого обработчика прерываний. 
// Указываем, что он будет находится в регистре MTVT2
CSRCUSTOM::MTVT2::Write(
           CSRCUSTOM::MTVT2::MTVT2EN::Mtvt2IsTrapAddress::Value |
           reinterpret_cast<std::uintptr_t>(&NonVectoredInt::IrqEntry));

Ну a теперь переключимся в режим работы контроллера ECLIC и укажем адрес единого обработчика исключений и NMI в регистре mtvec


 // Переключаемся на режим работы с ECLIC и задаем адрес единого обработчика исключений
 CSR::MTVEC::Write(
      CSR::MTVEC::MODE::Eclic::Value |
      reinterpret_cast<std::uintptr_t>(&NonVectoredInt::ExceptionEntry));        

Собственно все… контроллер и адреса обработчиков настроены. Полный код


extern "C"
{
int __low_level_init(void)
{
 {
   CriticalSection cs;        
   // Устанавливаем указание адреса обработчика NMI через общий обработчик, 
   // адрес которого указан в mtvec. Номер обработчика NMI будет 0xFFF
   CSRCUSTOM::MMISC_CTL::NMI_CAUSE_FFF::MnvecIsMtvecNmiIsFFF::Set(); 

   // Настраиваем адрес единого обработчика прерываний. 
   // Указываем, что он будет находится в регистре MTVT2
   CSRCUSTOM::MTVT2::Write(
              CSRCUSTOM::MTVT2::MTVT2EN::Mtvt2IsTrapAddress::Value |
              reinterpret_cast<std::uintptr_t>(&NonVectoredInt::IrqEntry));

   // Переключаемся на режим работы с ECLIC 
   // и задаем адрес единого обработчика исключений
   CSR::MTVEC::Write(CSR::MTVEC::MODE::Eclic::Value |
       reinterpret_cast<std::uintptr_t>(&NonVectoredInt::ExceptionEntry));

   // Включаем подсчет циклов и счетчика инструкций mycycle_minstret
   CSRCUSTOM::MCOUNTINHIBITPack<CSRCUSTOM::MCOUNTINHIBIT::IR::MinstretOn,
                CSRCUSTOM::MCOUNTINHIBIT::CY::McyclesOn
                >::Set();
   }
}

Теперь нужно настроить машинный таймер и его прерывание.


// Настраиваем количество бит отвечающих  за уровень прерывания 
// в регистре CLICINTCTL_7. Пусть будет 3 бита
ECLIC::CLICCFG::NLBITS::MaxBitsForLevel3::Set();
//Ставим уровень срабатывания прерывания в 0
ECLIC::MTH::Write<0U>();
//Ставим невекторный режим для обработки прерывания таймера
ECLIC::CLICINTATTR_7::SHV::NonVectored::Set();
//Ставим уровень прерывания в 1, приоритет не будем трогать
ECLIC::CLICINTCTL_7::Write<
       1U << (8U - ECLIC::CLICCFG::NLBITS::MaxBitsForLevel3::Value)>();

//Настраиваем машинный таймер. Таймер будет переполнятся раз в 1 мс.     
MACHINETIMER::MTIMECMP::MTIMEField::Value<SystemTimerPeriod>::Write() ;
MACHINETIMER::MTIME::Write<0U>();

//Разрешить прерывание таймера - прерывание номер 7
ECLIC::CLICINTIE_7::IE::Enable::Write();

//Разрешаем глобальное машинное прерывание
CSR::MSTATUSPack<CSR::MSTATUS::MIE::InterruptEnabled>::SetValueBitsAtomic();

Осталось подать тактирование на порты, к которым подключены светодиоды и настроить эти порты на выход. Светодиоды у нас на портах GPIOC.7 и GPIOB.6


RCU::APB2EN::PCEN::Enable::Set(); 
RCU::APB2EN::PBEN::Enable::Set();
GPIOC::CTL0::CTLMD7::GpioOutputPushPull50Mhz::Set();
GPIOB::CTL0::CTLMD6::GpioOutputPushPull50Mhz::Set();

Полный код
#include "gpiocregisters.hpp"
#include "gpiobregisters.hpp"
#include "rcuregisters.hpp"  //for RCU
#include "csrregisters.hpp" //for CSR
#include "eclicregisters.hpp" // for ECLIC
#include "machinetimerregisters.hpp"
#include "systemconfig.hpp" // for SystemTimerPeriod
#include "criticalsection.hpp" // for CriticalSection
#include "csrcustomregisters.hpp"
#include "vectortable.hpp" //for InterruptVectorTable

namespace NonVectoredInt
{
    static void HandleInterrupt(std::uint32_t interruptId)
    {
        assert(interruptId < InterruptVectorTable.size());
        tInterruptFunction fp = InterruptVectorTable[interruptId];
        if (fp != nullptr)
        {
            fp();
        }
    }

    static void HandleException(std::uint32_t exceptiontId)
    {
        assert(exceptiontId < ExceptionVectorTable.size());
        tInterruptFunction fp = ExceptionVectorTable[exceptiontId];
        if (fp != nullptr)
        {
            fp();
        }
    }

    __interrupt void ExceptionEntry()
    {
        const auto mcause = CSR::MCAUSE::Get();
        const auto mepc = CSR::MEPC::Get();
        const auto msubm = CSRCUSTOM::MSUBM::Get();
        const auto exceptionCode = mcause & 0xFFF;

        if (exceptionCode != 0xFFF) // if not NMI
        {
            NonVectoredInt::HandleException(exceptionCode);
        }
        else
        {
            DummyModule::HandleInterrupt(); // for NMI handling
        }

        __disable_interrupt();
        CSR::MCAUSE::Write(mcause);
        CSR::MEPC::Write(mepc);
        CSRCUSTOM::MSUBM::Write(msubm);
    }

    __interrupt void IrqEntry()
    {
        const auto mcause = CSR::MCAUSE::Get();
        const auto mepc = CSR::MEPC::Get();
        const auto msubm = CSRCUSTOM::MSUBM::Get();
        const auto exceptionCode = mcause & 0xFFF;

        NonVectoredInt::HandleInterrupt(exceptionCode);

        __disable_interrupt();
        CSR::MCAUSE::Write(mcause);
        CSR::MEPC::Write(mepc);
        CSRCUSTOM::MSUBM::Write(msubm);
    }
}

extern "C"
{
int __low_level_init(void)
{
    {
        CriticalSection cs;

        // Устанавливаем указание адреса обработчика NMI через общий обработчик, 
        // адрес которого указан в mtvec. Номер обработчика NMI будт 0xFFF
        CSRCUSTOM::MMISC_CTL::NMI_CAUSE_FFF::MnvecIsMtvecNmiIsFFF::Set(); 

        // Настраиваем адрес единого обработчика прерываний. 
        // Указываем, что он будет находится в регистре MTVT2
        CSRCUSTOM::MTVT2::Write(
                   CSRCUSTOM::MTVT2::MTVT2EN::Mtvt2IsTrapAddress::Value |
                   reinterpret_cast<std::uintptr_t>(&NonVectoredInt::IrqEntry));

        // Переключаемся на режим работы с ECLIC и устанавливаем 
        // адрес единого обработчика исключений
        CSR::MTVEC::Write(
                    CSR::MTVEC::MODE::Eclic::Value |
                    reinterpret_cast<std::uintptr_t>(&NonVectoredInt::ExceptionEntry));

        // Включаем подсчет циклов и счетчика инструкций mycycle_minstret
        CSRCUSTOM::MCOUNTINHIBITPack<CSRCUSTOM::MCOUNTINHIBIT::IR::MinstretOn,
                                     CSRCUSTOM::MCOUNTINHIBIT::CY::McyclesOn
                                    >::Set();
    }
    ECLIC::CLICCFG::NLBITS::MaxBitsForLevel3::Set();

    //Ставим уровень срабатывания прерывания в 0
     ECLIC::MTH::Write(0U);
    //Ставим невекторный режим для обработки прерывания таймера
     ECLIC::CLICINTATTR_7::SHV::NonVectored::Set();

    //Ставим уровень прерывания в 1, приоритет не будем трогать
    ECLIC::CLICINTCTL_7::Write<
           1U << (8U - ECLIC::CLICCFG::NLBITS::MaxBitsForLevel3::Value)>();

    MACHINETIMER::MTIMECMP::MTIMECMPField::Value<SystemTimerPeriod>::Write() ;
    MACHINETIMER::MTIME::Write<0U>();

    //Разрешить прерывание таймера - прерывание номер 7
    ECLIC::CLICINTIE_7::IE::Enable::Write();

    //Enable machine interrupt
    CSR::MSTATUSPack<CSR::MSTATUS::MIE::InterruptEnabled>::SetValueBits();

    RCU::APB2ENPack<RCU::APB2EN::PCEN::Enable,
                    RCU::APB2EN::PBEN::Enable>::Set();
    GPIOC::CTL0::CTLMD7::GpioOutputPushPull50Mhz::Set();
    GPIOB::CTL0::CTLMD6::GpioOutputPushPull50Mhz::Set();

    return 1;
}
}

int main()
{
    while (true)
    {
        asm volatile(" ");
    }

    return 0;
}

Мы все ближе к морганию светодиода.
Определим сервис таймера и функцию, которая будет запускаться в обработчике прерывания машинного таймера. Она простая. Она будет вызвать функцию OnTick() у программных таймеров


template<typename ...Timers>
struct TimerService
{
  static void OnSystemTick()
  {
    (Timers::OnTick(), ...);
  }
};

Программный таймер — это вот такая штука, которая вызывает метод OnTimeout(), когда он переполнится.


template <std::uint32_t TimerFrequency, std::uint32_t msPeriod, typename ... Subscribers>
class SoftwareTimer 
{
public:
  static void OnTick()
  {
    --ticksRemain ;
    if (ticksRemain == 0U)
    {       
      ticksRemain = ticksReload ;
      (Subscribers::OnTimeout(),...) ;
    }
  }

private:

  static constexpr std::uint32_t msInSec = 1000UL ;
  static constexpr std::uint32_t ticksReload =  
         static_cast<std::uint32_t>((msPeriod * TimerFrequency) / msInSec) ;

  static inline volatile std::uint32_t ticksRemain = ticksReload;
} ;

Теперь создадим отдельный программный таймер для каждого светодиода, один на 100 ms, второй на 200ms и зарегистрируем программные таймеры в сервисе системного таймера


//Настройка таймеров для светодиодов
using Led1Timer = SoftwareTimer<SystemTimerPeriod, 100UL, Led1> ;
using Led2Timer = SoftwareTimer<SystemTimerPeriod, 200UL, Led2> ;
//регистрация таймеров
using AppTimerService = TimerService<Led1Timer, Led2Timer> ; 

Ну и кульминация, светодиоды Led1 и Led2 — это просто ножки портов GPIOB.6 и GPIOC.7


template<typename Pin>
struct Leds
{
  static void OnTimeout()
  {
    // Переключение ножки, которое вызывается в программных таймерах 
    Pin::Toggle(); 
  }
};

template<typename Port, uint32_t num>
struct DummyPin
{
  static void Toggle()
  {
    Port::OCTL::Toggle(1 << num);
  }
};

using Led1 = Leds<DummyPin<GPIOC, 7>>;
using Led2 = Leds<DummyPin<GPIOB, 6>>;

Функция main, не интересная, она выглядит очень скромно


int main()
{
  while (true)
  {
    asm volatile(" ");
  }
  return 0;
}

В общем и целом, это работает так:


Как только мы разрешили прерывание машинного таймера и глобальное машинное прерывание. Через 1 мс срабатывает прерывание машинного таймера, которое вызывает метод OnTick() программных таймеров. В момент, когда время программного таймера истекло, вызывается его метод OnTimeout(), в котором переключается ножка порта к которому подключен светодиод.


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


Как обычно, выкладываю код под IAR 1.31 for RISC-V.


И ссылка на Гитхаб с исходниками


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


Исправления, по наставлению Ryppka заменил структуру NonVectoredInt на namespace

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+49
Comments31

Articles