Библиотека генератора ассемблерного кода для микроконтроллеров AVR
Часть 1. Первое знакомство
Добрый день, уважаемые хабровчане. Хочу предложить Вашему вниманию очередной (из имеющегося великого множества) проект, для программирования популярных микроконтроллеров серии AVR.
Можно было бы потратить много текста для объяснения зачем это понадобилось, но вместо этого просто посмотрим на примерах чем он отличается от других решений. А все пояснения и сравнения с имеющимися системами программирования будут по мере необходимости в процессе разбора примеров. Библиотека сейчас находится в процессе доработки, поэтому реализация некоторых функций может показаться не вполне оптимальной. Так же часть задач, которые в этой версии возложены на программиста, предполагается в дальнейшем оптимизировать или автоматизировать.
Итак, приступим. Сразу хочу уточнить, что представленный материал ни в коем случае не стоит рассматривать как полноценное описание, а только как демонстрацию некоторых возможностей разработанной библиотеки для того, чтобы помочь понять насколько интересным для читателей может оказаться данный подход.
Не будем отступать от сложившийся практики и начнем с классического примера, своеобразного «Hello world» для микроконтроллеров. А именно помигаем светодиодом, подключенным к одной из ножек процессора. Откроем VisualStudio от Microsoft (подойдет любой выпуск) и создадим консольное приложение для C#. Тем, кто не в курсе — достаточный для работы Community Edition абсолютно бесплатен.
Собственно сам текст выглядит следующим образом:
using NanoRTOSLib;
using System;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
var m = new Mega328();
m.PortB[0].Mode = ePinMode.OUT;
m.PortB.Activate();
m.LOOP(m.TempL, (r, l) => m.GO(l), (r) => { m.PortB[0].Toggle();});
Console.WriteLine(AVRASM.Text(m));
}
}
}
Разумеется, чтобы все заработало и нужна та самая библиотека, которую я и представляю.
После компиляции и запуска программы, в выводе консоли увидим следующий результат работы данной программы.
#include “common.inc”
RESET: ldi r16, high(RAMEND)
out SPH,r16
ldi r16, low(RAMEND)
out SPL,r16
outi DDRB,0x1
L0000:
in TempL,PORTB
ldi TempH,1
eor TempL,TempH
out PORTB,TempL
xjmp L0000
.DSEG
Если скопировать результат в любую среду, умеющую работать с AVR ассемблером и подключить библиотеку макросов Common.inc (макробиблиотека так же является одной из составных элементов представляемой системы программирования и работает совместно с NanoRTOSLib), то эту программу можно скомпилировать и проверить на эмуляторе или реальном кристалле и убедиться что все работает.
Рассмотрим подробнее исходный код программы. Первым делом назначаем переменной m тип используемого кристалла. Далее устанавливаем для нулевого бита порта B кристалла режим цифрового выхода и активируем порт. Следующая строка выглядит немного странно, но ее смысл совсем простой. В ней мы говорим, что хотим организовать бесконечный цикл в теле которого мы меняем значение нулевого бита порта B на противоположное. Последняя строка программы собственно и визуализирует результат всего написанного ранее в виде ассемблерного кода. Все предельно просто и компактно. А результат практически не отличается от того, что можно было бы написать на ассемблере. Вопросов к выходному коду может возникнуть только два: первый — зачем инициализировать стек, если его все равно не используем и что за xjmp? Ответом на первый вопрос и одновременно объяснением почему выводится ассемблер, а не готовый HEX будет следующий: результат в виде ассемблера позволяет дополнительно анализировать и оптимизировать программу, позволяя программисту выделять и изменять фрагменты кода, которые ему не нравятся. А инициализация стека оставлена хотя бы из тех соображений, что без использования стека можно придумать не так много программ. Впрочем, если не нравится — смело убирайте. Вывод в ассемблер для этого и предназначен. Что касается xjmp — это пример использования макросов для увеличения читабельности выходного ассемблера. Конкретно xjmp — замена для jmp и rjmp с правильной подстановкой в зависимости от длины перехода.
Если залить программу на кристалл, то мигания диодом мы конечно не увидим, несмотря на то, что состояние пина меняется. Просто это происходит слишком быстро для того для того, чтобы увидеть это глазами. Поэтому рассмотрим следующую программу в которой продолжим мигать диодом, но уже так, чтобы это можно было увидеть. Для примера вполне подойдет задержка в 0.5 секунды: не слишком быстро и не слишком медленно. Можно было бы сделать множество вложенных циклов с NOP для формирования задержки, но мы пропустим этот этап, как не добавляющий ничего к описанию возможностей библиотеки, и сразу воспользуемся возможностью использовать имеющиеся аппаратные средства. Изменим наше приложение следующим образом.
using System;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
var m = new Mega328();
m.PortB[0].Mode = ePinMode.OUT;
m.PortB.Activate();
m.WDT.Clock = eWDTClock.WDT500ms;
m.WDT.OnTimeout = () => m.PortB[0].Toggle();
m.WDT.Activate();
m.EnableInterrupt();
var loop = AVRASM.newLabel();
m.GO(loop);
Console.WriteLine(AVRASM.Text(m));
}
}
}
Очевидно, что программа похожа на предыдущую, поэтому рассмотрим только то, что изменилось. Во-первых в этом примере мы задействовали WDT(watchdog timer). Для работы с большими и не требующими особой точности задержками это наилучший вариант. Все, что нужно для его использования — это установить требуемую периодичность, путем установки делителя через свойство WDT.Clock и определить действия, которые необходимо выполнить в момент срабатывания события, путем определения кода через свойство WDT.OnTimeout. Так как для работы нам нужны прерывания, их необходимо разрешить командой EnableInterrupt. А вот основной цикл можно заменить пустышкой. В нем мы все равно ничего делать не планируем. Поэтому объявим и установим метку и сделаем на нее безусловный переход для организации пустого цикла. Если больше нравится LOOP — пожалуйста. Результат от этого не изменится.
Ну и в финале посмотрим на получившийся код.
#include “common.inc”
jmp RESET
reti ; IRQ0 Handler
nop
reti ;IRQ1 Handler
nop
reti ;PC_INT0 Handler
nop
reti ;PC_INT1 Handler
nop
reti ;PC_INT2 Handler
nop
jmp WDT ;Watchdog Timer Handler
RESET: ldi r16, high(RAMEND)
out SPH,r16
ldi r16, low(RAMEND)
out SPL,r16
outi DDRB,0x1
ldi TempL, (1<<WDCE) | (1<<WDE)
sts WDTCSR,TempL
ldi TempL, 0x42
sts WDTCSR,TempL
sei
L0000:
xjmp L0000
WDT:
push r17
push r16
in r16,SREG
push r16
in TempL,PORTB
ldi TempH,1
eor TempL,TempH
out PORTB,TempL
pop r16
out SREG,r16
pop r16
pop r17
reti
.DSEG
У тех, кто знаком с этим процессором безусловно возникнет вопрос куда делись еще несколько векторов прерываний. Здесь мы использовали следующую логику — если код не используется — код не нужен. Поэтому таблица прерываний и заканчивается на последнем используемом векторе.
Несмотря на то, что программа прекрасно справляется с поставленной задачей, самым придирчивым может не понравится то, что набор возможных задержек ограничен, а шаг слишком грубый. Поэтому рассмотрим еще один способ, а заодно и посмотрим, как в библиотеке организована работа с таймерами. В кристалле Mega328, который взят за образец, их целых 3 штуки. 2 8-битных и один 16-битный. Архитекторы очень постарались вложить в эти таймеры как можно больше возможностей, поэтому и их настройка достаточно объемна.
Сначала посчитаем какой счетчик следует применить для нашей задержки в 0,5 сек. Если взять тактовую частоту кристалла 16 Мгц, то даже с максимальным предделителем периферии не получается уложится в 8-разрядный счетчик. Поэтому не будем усложнять и воспользуемся единственным доступным для нас 16 -разрядным счетчиком Timer1.
В результате программа приобретает следующий вид:
using NanoRTOSLib;
using System;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{var m = new Mega328();
m.FCLK = 16000000;
m.CKDIV8 = false;
var bit1 = m.PortB[0];
bit1.Mode = ePinMode.OUT;
m.PortB.Activate();
m.Timer1.Mode = eWaveFormMode.CTC_OCRA;
m.Timer1.Clock = eTimerClockSource.CLK256;
m.Timer1.OCRA = (ushort)((0.5 * m.FCLK) / 256);
m.Timer1.OnCompareA = () => bit1.Toggle();
m.Timer1.Activate();
m.EnableInterrupt();
m.LOOP(m.TempH, (r, l) => m.GO(l), (r) => { });
Console.WriteLine(AVRASM.Text(m));
}
}
}
Так как мы используем в качестве источника тактирования для нашего таймера основной генератор, для правильного расчета задержки необходимо указать частоту тактирования процессора, настройку делителя и фьюза тактирования периферии. Основной текст программы занимает установка таймера в нужный режим. Здесь сознательно для тактирования выбран предделитель на 256 а не максимальный, так как при выборе предделителя 1024 для требуемой тактовой частоты в 500ms, которую мы хотим получить, получается дробное число.
Результирующий ассемблерный код нашей программы будет выглядеть следующим образом:
#include “common.inc”
jmp RESET
reti ; IRQ0 Handler
nop
reti ;IRQ1 Handler
nop
reti ;PC_INT0 Handler
nop
reti ;PC_INT1 Handler
nop
reti ;PC_INT2 Handler
nop
reti ;Watchdog Timer Handler
nop
reti ;Timer2 Compare A Handler
nop
reti ;Timer2 Compare B Handler
nop
reti ;Timer2 Overflow Handler
nop
reti ;Timer1 Capture Handler
nop
jmp TIM1_COMPA ;Timer1 Compare A Handler
RESET: ldi r16, high(RAMEND)
out SPH,r16
ldi r16, low(RAMEND)
out SPL,r16
outi DDRB,0x1
outiw OCR1A,0x7A12
outi TCCR1A,0
outi TCCR1B,0xC
outi TCCR1C,0x0
outi TIMSK1,0x2
outi DDRB,0x1
sei
L0000:
xjmp L0000
TIM1_COMPA:
push r17
push r16
in r16,SREG
push r16
in TempL,PORTB
ldi TempH,1
eor TempL,TempH
out PORTB,TempL
pop r16
out SREG,r16
pop r16
pop r17
reti
.DSEG
Здесь уже вроде больше нечего комментировать. Инициализируем устройства, настраиваем прерывания и наслаждаемся работой программы.
Работа через прерывания — наиболее простой вариант создания программ для работы в режиме реального времени. К сожалению переключение между выполняемыми параллельно задачами с использованием только обработчиков прерываний для выполнения этих задач не всегда возможно. Ограничением является запрет на вложенную обработку прерываний, что приводит к тому, что до тех пор, пока не произойдет выход из обработчика, процессор не реагирует на все остальные прерывания, что может привести к потере событий, если обработчик будет выполняться слишком долго.
Вариантом решения является разделение кода регистрации событий и их обработки. Ядро многопоточной обработки Parallel из библиотеки организовано таким образом, что при возникновении события обработчик прерывания только регистрирует данное событие и при необходимости выполняет минимально необходимые операции по захвату данных, а вся обработка производится в основном потоке. Ядро последовательно проверяет наличие необработанных флагов и в случае их нахождения переходит к выполнению соответствующей задачи.
Использование этого подхода упрощает проектирование систем с несколькими асинхронными задачами, позволяя рассматривать каждую из них изолированно, не концентрируясь на проблемах переключения ресурсов между задачами. В качестве примера рассмотрим реализацию двух независимых задач, каждая из которых переключает свой вывод с определенной задержкой.
using NanoRTOSLib;
using System;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
var m = new Mega328();
m.FCLK = 16000000;
m.CKDIV8 = false;
m.PortB.Direction(0x07);
var bit1 = m.PortB[1];
var bit2 = m.PortB[2];
m.PortB.Activate();
var tasks = new Parallel(m, 4);
tasks.Heap = new StaticHeap(tasks, 64);
var t1 = tasks.CreateTask((tsk) =>
{
var loop = AVRASM.NewLabel();
bit1.Toggle();
tsk.Delay(32);
tsk.TaskContinue(loop);
},"Task1");
var t2 = tasks.CreateTask((tsk) =>
{
var loop = AVRASM.NewLabel();
bit2.Toggle();
tsk.Delay(48);
tsk.TaskContinue(loop);
}, "Task2");
var ca = tasks.ContinuousActivate(tasks.AlwaysOn, t1);
tasks.ActivateNext(ca, tasks.AlwaysOn, t2);
ca.Dispose();
m.EnableInterrupt();
tasks.Loop();
Console.WriteLine(AVRASM.Text(m));
}
}
}
В этой задаче мы настраиваем нулевой и первый выводы порта В на вывод и меняем значение с 0 на 1 и обратно с периодом 32ms для нулевого и 48ms для первого вывода. За управление каждым из портов отвечает отдельная задача. Первое, на что следует обратить внимание, это определение экземпляра Parallel. Этот класс представляет из себя ядро управления задачами. В его конструкторе мы определяем максимально допустимое количество одновременно работающих потоков. Далее следует выделение памяти для хранения данных потоков. Использованный в примере класс StaticHeap выделяет под каждый поток фиксированное количество байт. Для решения нашей задачи это допустимо, а использование фиксированного распределения памяти по сравнению с динамическим упрощает алгоритмы и делает код более компактным и быстрым. Далее в коде мы описываем набор задач, которые предназначены для запуска под управлением ядра. Следует обратить внимание на асинхронную функцию Delay, которую мы используем для формирования задержки. Ее особенность состоит в том, что при вызове этой функции в настройках потока устанавливается требуемая задержка, а управление передается в ядро. По истечении установленного интервала ядро возвращает управление задаче с команды, следующей за командой Delay. Другой особенностью задачи является программирование поведения потока задачи по ее завершению в последней команде задачи. В нашем случае обе задачи настроены на выполнение в бесконечном цикле с возвратом управления ядру в конце каждого цикла. В случае необходимости, завершение задачи может освобождать поток или передавать его для выполнения другой задачи.
Основанием для вызова задачи является активация сигнала, назначенного потоку задачи. Сигнал может активироваться как программным путем, так и аппаратно по прерываниям от периферийных устройств. Вызов задачи сбрасывает сигнал. Исключением является предопределенный сигнал AlwaysOn, который всегда находится в активном состоянии. Это дает возможность создавать задачи, которые будут получать управление в каждом цикле опроса. Функция LOOP необходима для вызова основного цикла исполнения. К сожалению размер выходного кода при использовании Parallel уже становится существенно больше чем в предыдущих примерах (примерно 600 команд) и не может быть целиком приведен в статье.
И на сладкое — нечто более похожее на живой проект, а именно цифровой термометр. Все как всегда просто. Цифровой датчик с интерфейсом SPI, 7-сегментный 4-х разрядный индикатор и несколько потоков обработки для того, чтобы все крутилось. В одном гоняем цикл для динамической индикации, в другом события по которым запускается цикл считывания температуры, в третьем читаем принятые c датчика значения и преобразуем из бинарного кода в BCD а затем в сегментный код для буфера динамической индикации.
Сама программа выглядит следующим образом.
using NanoRTOSLib;
using System;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
var m = new Mega328();
m.FCLK = 16000000;
m.CKDIV8 = false;
var led7s = new Led_7();
led7s.SegPort = m.PortC;
led7s.Activate();
m.PortD.Direction(0xFF);
m.PortD.Activate();
m.PortB[0].Mode = ePinMode.OUT;
var tc77 = new TC77();
tc77.CS = m.PortB[0];
tc77.Port = m.SPI;
m.Timer0.Clock = eTimerClockSource.CLK64;
m.Timer0.Mode = eWaveFormMode.Normal;
var reader = m.DREG("Temperature");
var bcdRes = m.DREG("digits");
var tmp = m.BYTE();
var bcd = new BCD(reader, bcdRes);
m.subroutines.Add(bcd);
var os = new Parallel(m, 4);
os.Heap = new StaticHeap(os, 64);
var tmrSig = os.AddSignal(m.Timer0.OVF_Handler);
var spiSig = os.AddSignal(m.SPI.Handler, () =>
{
m.SPI.Read(m.TempL);
m.TempL.MStore(tmp);
});
var actuator = os.CreateTask((tsk) =>
{
var loop = AVRASM.NewLabel();
tc77.ReadTemperatureAsync();
tsk.Delay(16);
tsk.TaskContinue(loop);
}, "actuator");
var treader = os.CreateTask((tsk) =>
{
var loop = AVRASM.NewLabel();
tc77.ReadTemperatureCallback(os, reader, tmp);
reader >>= 7;
m.CALL(bcd);
tsk.TaskContinue(loop);
}, "reader");
var display = os.CreateTask((tsk) =>
{
var loop = AVRASM.NewLabel();
m.PortD.Write(0xFE);
m.TempQL.Load(bcdRes.Low);
m.TempQL &= 0x0F;
led7s.Show(m.TempQL);
os.AWAIT();
m.PortD.Write(0xFD);
m.TempQL.Load(bcdRes.Low);
m.TempQL >>= 4;
led7s.Show(m.TempQL);
os.AWAIT();
m.PortD.Write(0xFB);
m.TempQL.Load(bcdRes.High);
m.TempQL &= 0x0F;
led7s.Show(m.TempQL);
os.AWAIT();
m.PortD.Write(0xF7);
m.TempQL.Load(bcdRes.High);
m.TempQL >>= 4;
led7s.Show(m.TempQL);
os.AWAIT();
tsk.TaskContinue(loop);
}, "display");
var ct = os.ContinuousActivate(os.AlwaysOn, actuator);
os.ActivateNext(ct, spiSig, treader);
os.ActivateNext(ct, tmrSig, display);
tc77.Activate();
m.Timer0.Activate();
m.EnableInterrupt();
os.Loop();
Console.WriteLine(AVRASM.Text(m));
}
}
}
Понятно, что это не рабочий проект, а только технологическое демо, призванное продемонстрировать возможности библиотеки NanoRTOS. Но во всяком случае меньше 100 строк исходного и меньше 1кб выходного кода вполне неплохой результат для работоспособного приложения.
В следующих статьях я планирую в случае интереса к данному проекту более подробно остановится уже на принципах и особенностях программирования с использованием данной библиотеки.
Комментарии (20)
FForth
04.08.2019 08:11| Можно было бы потратить много текста для объяснения зачем это понадобилось,
| но вместо этого просто посмотрим на примерах чем он отличается от других решений
Стоило потратить, так как в приведённом коде есть некоторые отсылки к синтаксису Си языка. Чем Си тогда перестал устраивать для решения задач AVR микроконтроллера?
Кроме своего псевдосинтаксиса ассемблера, не проще ли было взять синтаксис, например, TCL языка? Могло бы быть и сравнение с другими ассемблерами доступными для AVR.
Как пример c АБ (Algoritm Builder) Здесь его активно применяют пользователи
P.S. Определение вручную ещё и меток в данном «ассемблере», это где то, немного за гранью разумного программирования.
Вот ещё вариант дизайна ассемблера.idv2013 Автор
04.08.2019 10:26При написании статьи я много раз переписывал начало со сравнением различных систем и подходов к программированию AVR. Тема эта настолько объемная, что требует даже не одной полноценной публикации, а целого цикла. Если бы я начал с полноценного сравнения, то до описания библиотеки дело даже бы не дошло. Если кратко — я в курсе существования всех перечисленных продуктов и еще многих других, Вами не перечисленных. Более подробный сравнительный анализ предполагался в последующих статьях, посвященных описанию работы с библиотекой. Вероятно мне не удалось донести основную мысль данного проекта: Это библиотека на языке C# (к языку C этот проект не имеет никакого отношения), предназначенная для автоматизации написания программы на ассемблере (не «ассемблер», а стандартный и единственный описанный в документации производителя AVR assembler) без использования проприетарных библиотек. То, что это с точки зрения программы С# просто библиотека, позволяет использовать при программировании все возможности уже имеющейся продвинутой IDE, в отличии от проектов с полноценной реализацией языка. Это и ответ на замечание по — поводу меток. У программиста, который хочет написать программу на ассемблере должны быть все возможности использовать ассемблер напрямую, если высокоуровневая реализация его чем-то не устраивает. В том числе метки. Что касается графических языков программирования — у них свои сторонники и круг задач. Это же относится и к моему проекту. Если и рассматривать тех, кого он может заинтересовать, то это скорее те, кому нравится Arduino, но не устраивает его быстродействие.
FForth
04.08.2019 10:56Насколько данный проект масштабируем для использования с контроллерами другой архитектуры (ARM, MSP430, PIC, STM32, 8051)
(в каком месте проекта это масштабируется?)
P.S. И что из возможностей Visual Studio помогает в отладке кода на AVR контроллере?
(есть ли в этом варианте бесшовная интеграция?)idv2013 Автор
04.08.2019 11:48Давайте рассматривать эту библиотеку как сильно выросшие ассемблерные макросы. С этой точки зрения неважно о какой архитектуре идет речь. Масштабирование, если я правильно понял вопрос, это описание отличающихся объектов для каждой из архитектур и уникальный код ассемблерной реализации. С точки зрения написания универсального кода для всех архитектур этот продукт не очень подходит. В балансе оптимальность/универсальность здесь существенный перекос в сторону оптимальность.
Что касается отладки — библиотека заканчивается текстом ассемблера (не кодом, а именно исходным текстом, без всяких изменений приведенным в примерах в статье). Помощь в отладке заключается в возможности в исходном тексте указывать именованные метки, имена регистров, комментарии для размещения в выходном ассемблере.FForth
04.08.2019 14:40Ok,
Но, можете просвятить, чем полезна Visual Studio IDE по сравнению с доступными другими IDE при использовании Вами для решения озвученного решения?
И что именно в IDE для Вас имеет первостепенное значение?
P.S. Сильно ли данное решение выиграет у Notepad++?idv2013 Автор
04.08.2019 16:57Советую ознакомится с visualstudio.microsoft.com/ru/vs/community/?rr=https%3A%2F%2Fwww.microsoft.com%2Fru-ru%2Fsearch%3Fq%3Dvisual%2Bstudio%2B2013. Заодно еще раз обратите внимание на то, что версия Community абсолютно бесплатна. Все возможности, которые там описаны, справедливы и для этого решения, так как это просто программа на C#. Для того, чтобы пользоваться библиотекой, желательно знать, что такое C# и как на нем программировать. В этом случае все Ваши знания об удобстве и предпочтительности различных IDE для C# будут справедливы и для данного проекта
sbnur
04.08.2019 09:02Заголовок своей первой публикации стоило бы вычитать более внимательно
idv2013 Автор
04.08.2019 10:27-1Уточните пожалуйста, что Вам кажется неправильным в заголовке.
Vadimatorikda
04.08.2019 10:46Лично мне из названия было ничего не понятно до тех пор, пока я не открыл статью и не дочитал до примера кода на C#. В заголовке и тегах об этом ни слова.
Ну в целом впечатления смешанные. Но безусловно стоит продолжать. Пока еще не совсем ясно, что и зачем. Да, интересно. Да, на вид круто. Но что и куда — не ясно. К тому же не ясно, как даже попробовать это все (тут многие используют Linux...).idv2013 Автор
04.08.2019 10:51-1Спасибо за оценку. Что касается Linux — то здесь все просто. Библиотека собирается и под Core (спасибо Microsoft за то, что не обходит вниманием и другие ОС). Что касается остального — дождитесь пожалуйста следующей публикации.
AndyKorg
05.08.2019 13:02Когда-то программист должен был сделать три вещи: свой компилятор, свою ОС, свою файловую систему. По моему это первый пункт.
safari2012
05.08.2019 17:09Да, только это не совсем компилятор, а скорее интерпретатор. На выходе ассемблер, а не машинный код. Хотя, википедия говорит, что ассемблер (иногда) тоже является результатом работы компилятора, но в нашем случае автор предлагает скопировать ассемлерный код в другую среду и уже там скомпилировать.
assembled
05.08.2019 23:53Интерпретатор (в чистом виде) ничего не компилирует — он читает и сразу исполняет команды.
В вашем случае это компилятор — транслирует программу с одного языка на другой.safari2012
06.08.2019 02:19Точно, транслятор!
assembled
06.08.2019 03:35Транслятор более общее понятие. Интерпретатор тоже транслятор.
Я всё-таки прочитал статью и посмотрел примеры, тут вообще нет никакого «транслятора» кроме самого сишарпа. Тут просто генератор кода (да и название статьи намекает ;). Код генерируется вызовами методов из этой либы. Вот если писать компилятор, то эта либа может пригодиться для бэкэнда.
Забавная цепочка: C# компилируется в дотнетовский байткод, затем этот байткод JIT'ится в нативный и исполняется, им генерируется ассемблерный листинг, который сперва препроцессируется и только после транслируется в код для AVR.
P.S. Почему-то принял вас за автора, видимо из-за похожего строения ника :)
u_235
А где сама библиотека NanoRTOSLib?
idv2013 Автор
Я планировал выложить библиотеку в следующей статье с описанием ее использования. К сожалению без этого ее использование может вызвать затруднения и создать неверное впечатление о ее работе.