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

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
26-Авг-2020 22:31


Сегодня речь пойдет о модном — о 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 микроконтроллер.

Но чуть позже я привык и даже проникся теплыми чувствами к ученым из Калифорнийского университета, которые придумали вот это всё (это я про саму архитектуру RISC-V). И потому попытаюсь донести, что же я понял, и что такое китайский RISC-V.
Описывать все детали архитектуры RISC-V не хватит никаких сил, я ограничусь только необходимым минимумом для того, чтобы понять, как правильно поморгать светодиодом через таймер с прерыванием. Но даже, чтобы описать такую, казалось бы простую задачу, мне пришлось написать очень много букв, поэтому, если вы не хотите проходить со мной весь этот тернистый путь, можете сразу мотать на раздел Моргаем светодиодом, там где начинается код.
Материала по RISCV на русском не так много (вот есть обзор Создание процессора со свободной архитектурой RISC-V, вот еще презентация от Syntacore), поэтому, как обычно, начнем с азов. Итак, поехали.
Какая поддержка уже существует у GD32VF103
Для начала опишу, какими ресурсами я пользовался, возможно кому-то пригодится.

Из всего этого будем пользоваться только первыми 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

SPL
Сокращение
Наименование
Версия
Статус
Базовые наборы команд
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-x11, x12-x17), а также регистр адреса возврата являются рабочими регистрами. Любая функция может изменять содержимое этих регистров и если ей нужно воспользоваться какими-то из этих регистров после вызова другой функции, она должна сохранить их значение на стеке.
Сохраняемы регистры::
Регистры s0-s11 (x8, x9, x18-x27 ) должны сохраняться вызываемой функцией на стеке (если функция хочет их использовать) перед входом в функцию и восстанавливаться перед выходом, .
Далее табличка из интернета, описывающая каждый регистр, не стал переводить, и так все понятно:

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

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

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

SPL
Адрес
Доступ
Имя
Описание
Стандартные регистры машинного режима, соответствующие спецификации привилегированной архитектуры 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

SPL
Полн
Биты
Описание
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

SPL
Поле
Бит
Описание
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 нашего микроконтроллера выглядит так:

Таблица исключений

SPL
Код исключения
Тип Исключения/Прерывания
Синхронное/Асинхронное
Описание
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

SPL
Номер прерывания
Имя прерывания
Адрес вектора прерывания
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 ядрах, поэтому все 31 регистр общего назначения придется сохранять и восстанавливать руками.
Давайте разберемся как же обрабатывать прерывания. Как видно из картинки существует два режима обработки прерываний — векторный, через таблицу векторов и не-векторный — через единый обработчик прерывания. Также существует несколько способов(например, Interrupt Tail-Chaining) сделать обработку прерываний эффективнее.
Чтобы не раздувать, и так уже офигенно большую статью, я покажу как реализовать только не-векторный режим без оптимизации и шаманства. Но вкратце опишу оба.
Векторный и невекторный режиме работы прерываний
Контроллер прерываний ECLIC позволяет выбрать режим обработки прерываний и обеспечивает гибкость для выбора поведения каждого отдельного прерывания — либо с использованием аппаратной векторизации, либо без неё. В результате это позволяет пользователям оптимизировать каждое прерывание и пользоваться преимуществом обоих видов поведения. Аппаратная векторизация имеет более быстрый механизм обработки прерывания, но и имеет больший объем кода (из-за сохранения и восстановления контекста для каждого из прерываний). Напротив, невекторный режим имеет преимущество в размере кода, так как используется только один обработчик всех прерываний, но обработка происходит медленнее. Какой режим выгоднее, выбирает разработчик. Я выбрал не-векторный.
Векторный режим
В этом режиме при возникновении прерывания контроллер прерываний переходит на адрес прерывания, который указан в таблице векторов прерываний в соответствии с номером прерывания (алгоритм работы очень похож на обработку прерываний CortexM).
Не-векторный режим обработки прерываний
По умолчанию все прерывания настроены в не-векторный режим. Т.е. для обработки прерывания существует только один единый обработчик.
Тип обработки прерывания указывается в регистре CLICINTATTR в поле 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
8-bit
0x1001+4*i
RW
clicintie
8-bit
0x1002+4*i
RW
clicintattr
8-bit
0x1003+4*i
RW
clicintctl
8-bit
Ну а теперь, надо же описать, что это за регистры…
Регистр MTH
Регистр, который задает уровень срабатывания прерывания. Как видно из картинки, можно управлять не только приоритетом прерывания, но и уровнем прерывания и делать, что-то типа прореживания, мол если уровень прерывания ниже определенной границы, то это и не прерывание вовсе и не нужно его возводить.

Сам уровень конкретного прерывания, как было сказано задается clicintctl.
А вот уровень срабатывания — как раз задается регистром mth. Собственно это просто 8-битный регистр, хранящий уровень срабатывания прерывания.
Регистр CLICINTCTL
Регистр используется для задания уровня и приоритета прерывания. Как будет рассказано ниже, старшие биты(эффективные биты), количество которых задается в регистре CLICCFG указывают уровень прерывания, а младшие — приоритет. Количество эффективных битов также можно считать из регистра CLICINFO в поле CLICINTCTLBITS.
Регистр CLICCFG
Регистр общей конфигурации прерываний. Он задает количество эффективных битов, ответственных за установку уровня и приоритета прерывания. Чтобы было понятнее, приведу картинку.

Непонятно? Тогда следите за описанием бита nlbits в табличке, должно много прояснить.
Поле
Биты
Доступ
Значение по умолчанию
Описание
Резерв
7
R
N/A
Зарезервировано, значение 0
nmbits
6:5
R
N/A
Режим привилегий прерываний. Для нашего микроконтроллера он всегда 0: машинный.
nlbits
4:1
RW
0
Используется для указания эффективной разрядности значения уровня в регистре clicintctl. Т.е. если в этом регистре стоит значение 4, то при задании уровня в регистре clicintctl можно использовать только 4 старших бита, остальные биты используются для задания приоритета. Обычно используется значение от 2 до 8.
nvbits
0
R
N/A
Для нашего микроконтроллера всегда 1: Поддерживает векторный режим. А если бы не поддерживал, был бы 0
Регистр CLICINFO
Регистр общей информации о системе прерываний
Поле
Биты
Разрешение
значение по умолчанию
Описание
Резерв
31:25
R
N/A
Зарезервировано, все значения в 0
CLICINTCTLBITS
24:21
R
N/A
Эффективная разрядность регистра clicintctl.
VERSION
20:13
R
N/A
Номер версии аппаратной реализации контроллера прерываний.
NUM_INTERRUPT
12:0
R
N/A
Количество источников прерываний, поддерживаемых микроконтроллером.
CLICINTIP
Регистр содержащий единственный флаг запроса прерывания. i — обозначает номер прерывания. Наш контроллер содержит 87 прерываний, поэтому будет 87 таких регистров.
Поле
Биты
Разрешение
значение по умолчанию
Описание
Резерв
7:1
RO
N/A
Зарезервировано, все значения в 0
IP
0
RW
0
Флаг ожидания источника прерывания. 1 — означает что прерывание сработало. Если контроллер настроен на работу с прерываниями по уровню, то программно его очистить нельзя. Он будет очищен автоматически когда будет очистен исходный источник прерывания.
CLICINTIE
Регистр разрешения прерываний. Их тоже 87.
Поле
Биты
Разрешение
значение по умолчанию
Описание
Резерв
7:1
RO
N/A
Зарезервировано, все значения в 0
IE
0
RW
0
1 — означает что прерывание разрешено
CLICINTATTR
Регистр настройки источника прерываний. Как было показано выше, контроллер прерываний может работать в нескольких режимах. Прерывания могут срабатывать по уровню, по фронту переднем или заднему, а также тип прерывания векторный или не векторный. И их тоже 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 — с помощью него можно генерировать программное прерывание. Его тоже можно использовать для обращения к машинным регистрам из пользовательского режима.
Поле
Биты
Разрешение
значение по умолчанию
Описание
Резерв
7:1
RO
N/A
Зарезервировано, все значения в 0
MSIP
0
RW
0
1 — сгенерировать программное прерывание
Порты и регистры периферии
Порты в нашем китайском микроконтроллер очень похожи на порты в ST32, все тоже самое, только регистры называются по другому и настройки немного по другому сгруппированы. Но суть такая же.
Нужно подать тактирование на порты, а затем настроить порты в режим выхода через регистр Port control register 0 (GPIOx_CTL0, x=A..E).
И собственно для нас нужен еще один регистр Port output control register (GPIOx_OCTL, x=A..E), который позволит переключить ножку в противоположное состояние, чтобы моргнуть светодиодом.
На самом деле периферийные регистры я сгенерировал из svd файла и обращаться к ним через мою обертку можно точно также, как и регистрам ST32. Поэтому зацикливаться тут я не буду, лучше уже перейду к коду.
Моргаем светодиодом
Фуууух… ну кажется все, думаю этих знаний достаточно, чтобы поморгать светодиодом. Теперь можно перейти и к практике. И первое что нужно сделать, это настроить контроллер прерываний. Как я уже говорил, работать мы будем с ECLIC контроллером в не-векторном режиме.
В этом режиме у нас один единственный обработчик прерываний, при входе в него нам нужно сохранить все 31 регистр, и еще нужно сохранить регистры mcause, mepc и msubm, потому как нас могут опять прервать, а как вы знаете из разноцветной картинки, которую я показал выше, во время прерывания эти регистры перезаписываются. Соответственно при выходе нужно восстановить все это дело.
Сохранять регистры сами мы не будем, за нас это может сделать компилятор, для этого есть специальный атрибут функции __interrupt, собственно когда компилятор его видит, он подставляет пролог и эпилог функции в которых как раз и производится сохранение регистров при входе, и восстановление регистров при выходе и еще добавляет команду mret выхода из машинного режима.
Но если сильно нужно, можно написать функцию на ассемблере или stackless функцию на С++ и сохранить регистры самостоятельно… эффект будет тот же самый.
Итак, вот наш общий единый обработчик всех прерываний.
__interrupt void NonVectoredInt::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 регистры, которые необходимо сохранить в прерываниях, в соответствии с документацией, и при выходе мы их восстановили.
В-третьих, мы вызвали нужную нам функцию обработки прерывания в зависимости от его номера. Вот кстати, как эта функция выглядит.
struct NonVectoredInt
{
  static void  HandleInterrupt(std::uint32_t interruptId)
  {
    // проверим, что код прерывания не больше размера таблицы прерываний
    assert(interruptId < InterruptVectorTable.size());
    // ищем указатель на функцию обработчика в таблице прерываний
    tInterruptFunction fp = InterruptVectorTable[interruptId];
    if (fp != nullptr)
    {
      fp(); // вызываем обработчик
    }
  }
  static __interrupt void IrqEntry();
} ;

Мы просто берем указатель на нужную функцию из таблицы, которая выглядит как одномерный массив указателей на функции для обработки прерываний.
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()
    {
        AppTimerService::OnSystemTick() ;
        MACHINETIMER::MTIME::Write(0U); //Сбросить счетчик
    }
};

В нем происходит вызов функции 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
};

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

NMI будем обрабатывать вместе с исключениями. Зададим его номер — 0xFFF.
__interrupt void NonVectoredInt::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();

Полный код

SPL
#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
struct 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();
        }
    }
    static __interrupt void ExceptionEntry();
    static __interrupt void IrqEntry();
} ;
__interrupt void NonVectoredInt::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 NonVectoredInt::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::APB2EN::PCEN::Enable::Set();
    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 RISCV.
И ссылка на Гитхаб с исходниками
З.Ы. RISC-V оставил двоякое впечатление, с одной стороны это очень расширяемая и гибкая штука, можно делать что угодно, с другой есть риск, что производителей занесет в строну, и каждый будет громоздить свой огород. Надеюсь, что все таки все спецификации скоро утвердят и все уляжется.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_programmirovanie (Программирование), #_c++, #_programmirovanie_mikrokontrollerov (Программирование микроконтроллеров), #_s++17 (с++17), #_riscv, #_riscv, #_gd32vg103, #_mikrokontrollery (микроконтроллеры), #_microcontrollers, #_programmirovanie (
Программирование
)
, #_c++, #_programmirovanie_mikrokontrollerov (
Программирование микроконтроллеров
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 23-Ноя 00:25
Часовой пояс: UTC + 5