ООП и Java-синтаксис в 2 КБ ОЗУ: как я пишу компилятор для 8-битных МК без виртуальной машины
В эмбеддед-разработке для 8-битных микроконтроллеров исторически правит бал Си и изредка ассемблер. А что, если писать прошивки на языке с Java-подобным синтаксисом, убрать целый класс багов, знакомых каждому Си-разработчику — утечки памяти, выход за границы массива, молчаливые зависания, — и при этом укладываться в те же спартанские 2 КБ ОЗУ?
Я назвал этот язык J8B (Java для 8-бит)
Первая реакция любого практикующего инженера: «Это бред! Под Java нужна виртуальная машина, она сожрет все ресурсы, ничего серьезного из этого не выйдет».
И вы будете абсолютно правы… если мы говорим о классическом подходе. Но я предлагаю взглянуть на задачу иначе. Я начинаю цикл статей о своем проекте vm5277 — тулките для разработки прошивок под микроконтроллеры, которая не имеет ничего общего с Си, и транслирует объектно-ориентированный код напрямую в нативный, оптимизированный ассемблер.
Кто я и зачем это пишу
Возможно, год назад вы видели мою первую, еще очень раннюю публикацию на эту тему vm5277, пример компиляции для AVR. За прошедшее время было написано огромное количество кода, исправлено не меньшее число багов и найдено множество не очевидных архитектурных решений. Изменился и инструмент: я успешно отложил в сторону плагин для NetBeans, который выпил из меня немало крови, и полностью переключился на IntelliJ IDEA.
Это первая статья из планируемого цикла. Чтобы у нас сразу сложилось правильное понимание, обозначу пару моментов:
Я не филолог и не академик по компиляторостроению. Я системотехник. Мой подход сугубо прагматичный: идти от конкретной инженерной задачи к её реализации. В моих статьях не будет заумной теории, только чистая практика.
Цель проекта — создать законченный тулкит, который кардинально снизит порог входа в embedded-разработку для маломощных МК и заметно сократит трудозатраты на написание и отладку кода. При этом — без потери производительности «на железе».
Текущий статус: Живая, но суровая Альфа
Проект находится в стадии активной альфы. Проделан колоссальный путь, но впереди работы еще больше. Для продакшена использовать тулкит, конечно, рано, но для ознакомления и экспериментов — самое оно.
Где посмотреть минусы: Список известных ограничений — по ссылке. Он будет обновляться по мере развития.
Где посмотреть код: В репозитории проекта на GitHub доступны десятки живых примеров — от банального мигания светодиодом до сложных драйверов периферии - ссылка.
Вот один из примеров ссылка
import boards.ArduinoUno; class Main { static public class Blink implements Timer { @Override public void run() { Gpio.invert(Gpio.PB5); } } public static void main() { Gpio.modeOut(ArduinoUno.LED_BUILTIN); Timer blink = new Blink(); while(true) { blink.start(100); Thread.sleep(3000); blink.start(30); Thread.sleep(3000); } } }
А здесь результат компиляции
; vm5277.avr:atmega328p, opt:size v0.5.0 .set OS_DRIVER_EXCEPTION_ID = 10 .set OS_BOUNDS_EXCEPTION_ID = 9 .set OS_NATIVE_EXCEPTION_ID = 8 .set OS_NULL_POINTER_EXCEPTION_ID = 11 .set OS_CALLBACK_IFACE_ID = 26 .equ CORE_FREQ = 16000 ;KHz .set STDIN_PORT_REGID = 11 .set STDIN_DDR_REGID = 10 .set STDIN_PIN_REGID = 9 .set STDIN_PORTNUM = 3 .set STDIN_PINNUM = 0 .set STDOUT_PORT_REGID = 11 .set STDOUT_DDR_REGID = 10 .set STDOUT_PIN_REGID = 9 .set STDOUT_PORTNUM = 3 .set STDOUT_PINNUM = 1 .set OS_FT_BLDR_API_REUSE = 1 .set OS_FT_DRAM = 1 .set OS_FT_MULTITHREADING = 1 .include "devices/atmega328p.def" .include "core/core.asm" .include "sys/mcu_halt.asm" .include "dmem/dram.asm" .include "j8b/class_refcount.asm" .include "core/dispatcher.asm" .include "j8b/mfin.asm" .include "j8b/new_thread.asm" .include "core/wait_ms.asm" Main: ldi r16,35 ldi r17,0x00 call j8bproc_new_thread std z+0x05,c0x02 movw pid_l,zl ldi r19,low(_OS_TASK_ENDPOINT) push r19 ldi r19,high(_OS_TASK_ENDPOINT) push r19 jmp j8b_CMainMmain _j8b_meta_518: .db 46,0 _j8b_meta_521: .db 47,1,33,1 .dw j8b_CMainCBlinkMrun_527 j8b_CMainCBlinkMrun_527: sbi pinb, 5 ret j8b_CMainCBlinkMBlink_532: ldi r16,low(35) ldi r17,high(35) call j8bproc_new_thread ldi r19,low(_j8b_meta_521*2) std z+0x03,r19 ldi r19,high(_j8b_meta_521*2) std z+0x04,r19 std z+0x05,c0x00 ldi r19,low(j8b_cmaincblinkmrun_527) std z+0x1d,r19 ldi r19,high(j8b_cmaincblinkmrun_527) std z+0x1e,r19 movw r16,r30 jmp j8bproc_mfin j8b_CMainMmain: sbi ddrb, 5 push r30 push r31 rcall j8b_CMainCBlinkMBlink_532 movw r20,r16 _j8b_loop_540: ldi r18,100 ldi r19,0 movw r16,r20 call os_timer_start_nr ldi r16,184 ldi r17,11 call os_wait_ms ldi r18,30 ldi r19,0 movw r16,r20 call os_timer_start_nr ldi r16,184 ldi r17,11 call os_wait_ms rjmp _j8b_loop_540
Вы можете лично запустить компилятор, оценить потребление ресурсов, скорость работы. А главное — посмотреть промежуточный ASM-код, чтобы оценить его оптимальность. Да, сейчас мой приоритет — расширение функционала, а не микрооптимизации. Но даже в текущем виде нативный выхлоп показывает более чем достойные результаты.
Поскольку готового комьюнити вокруг проекта пока нет, у вас есть возможность повлиять на вектор развития архитектуры и расширить инструмент, которого нам так не хватало.
За счет чего это работает? Архитектура экосистемы
Чтобы подружить строгий ООП-синтаксис с жесткими рамками 8-битного «железа», мне пришлось отказаться от идеи классических абстракций и построить экосистему на четырех базовых концептах:
Высокоуровневый J8B-язык: Для написания бизнес-логики используется язык с Java-подобным синтаксисом. Однако он имеет ряд критически важных отличий от стандартной Java, поскольку изначально спроектирован под МК с экстремально малым объемом ресурсов.
Платформонезависимый Runtime: Все базовые библиотеки и даже драйверы написаны на самом высокоуровневом языке. Они реализуют всю необходимую логику, но абстрагированы от конкретного железа. Написав один раз драйвер дисплея или протокола, вы без изменений переносите его между AVR, PIC, STM8 и т.д.
Системное ядро на чистом Ассемблере: Нижний уровень экосистемы пишется вручную под каждую архитектуру. Ассемблерное ядро берет на себя прямое взаимодействие с периферией, критичные к таймингам функции и предоставляет встроенную RTOS. Диспетчеризация потоков (как в кооперативном, так и в вытесняющем режимах) и управление динамической памятью здесь работают с минимально возможным оверхедом.
Собственный монолитный тулкит: Вместо зоопарка сторонних утилит я разрабатываю весь инструментарий с нуля на Java — компилятор, собственный ассемблер, прошивальщик, бутлоадер, LSP-сервер для подсветки синтаксиса и плагины для Maven, IDEA и NetBeans. За счет сквозной интеграции всех компонентов на этапе сборки удается проводить такие оптимизации кода, которые недоступны стандартным компиляторам, а сама сборка происходит существенно быстрее существующий решений.
Главные отличия vm5277 от C и Java: Разрушаем мифы
А теперь перейдем к деталям, из-за которых Си-разработчики обычно заявляют, что «это невозможно». Давайте наглядно сравним, как базовые задачи решаются в классическом Си, стандартной Java и в моей экосистеме.
Управление памятью: new без free и почему подсчет ссылок на 8-битном МК — это не больно
В мире Java разработчик не думает о памяти — там всем заправляет Garbage Collector (GC), который на микроконтроллере с 2 КБ ОЗУ устроил бы катастрофу из-за пауз и аппетита к ресурсам. В Си же мы пишем malloc / free вручную, регулярно ловя утечки памяти или fragmentation fault.
Как это решено в vm5277:
Вы используете привычный оператор new для создания объектов, но оператора free или delete в языке просто нет. Память освобождается автоматически прямо в момент, когда на объект больше никто не ссылается. Достигается это за счет подсчета ссылок (Reference Counting).
Но подсчет ссылок на AVR/PIC? Это же оверхед по памяти на хранение счетчиков и куча лишних тактов на каждый чих!
От части: Да, каждый объект имеет счётчик ссылок в HEAP. Но счётчик 8-битный, его обновление вынесено в отдельные ассемблерные процедуры и выполняется нечасто. Производительность получается сопоставимой с Си. Более того - этот язык спроектирован для 8 бит МК, а значит в основном в коде будут использоваться примитивы для которых счетчики не нужны.
Мы вынуждены платить за автоматическое освобождение памяти, я считаю данную реализацию вполне допустимой ценой.
В результате мы убираем целый класс ошибок, типичных для Си: утечки памяти, use-after-free, повторное освобождение. Мы получаем удобство разработки кода как в Java, где нам не нужно тратить большие усилия на управление памятью, при этом у нас нет Garbage Collector’а, что гарантирует детерминированное время выполнения.
Пример:
import boards.ArduinoUno; import drivers.sound.Buzzer; class Main { public static void main() { Buzzer buzzer = new Buzzer(ArduinoUno.PIN_D7, 150); buzzer.play(0b11101010100000000101); } }
j8b_CMainMmain: ;Формируем стек для вызова конструктора push r30 push r31 ldi r19,55 push r19 ldi r19,150 push r19 push c0x00 rcall j8b_CBuzzerMBuzzer_534 <-- Здесь создается объект Buzzer movw r20,r16 <-- Записываем ссылку в переменную (регистры r20,r21) ;Формируем стек для вызова метода play push r30 push r31 ldi r19,5 push r19 ldi r19,168 push r19 ldi r19,14 push r19 push c0x00 movw r30,r20 rcall j8b_CBuzzerMplay_571 <-- Вызов метода play ;Жизненный цикл переменной завершен, выполняем декремент счетчика movw r16,r20 call j8bproc_class_refcount_dec_nr jmp mcu_halt
Процедура RTOS с освобождением памяти
;----------------------------------------------------------- J8BPROC_CLASS_REFCOUNT_DEC_NR: ;----------------------------------------------------------- ;Декремент счетчика ссылок объекта ;IN: ACCUM_L/H-адрес HEAP ;MOD: ACCUM_L/H/EL/EH ;----------------------------------------------------------- CP ACCUM_L,C0x00 CPC ACCUM_H,C0x00 BREQ J8BPROC_CLASS_REFCOUNT_DEC__END MCALL OS_DISPATCHER_LOCK PUSH_Z MOVW ZL,ACCUM_L LDD ACCUM_EH,Z+0x02 CPI ACCUM_EH,0x00 BREQ J8BPROC_CLASS_REFCOUNT_DEC__SKIP2 DEC ACCUM_EH BRNE J8BPROC_CLASS_REFCOUNT_DEC__SKIP1 LDD ACCUM_L,Z+0x00 LDD ACCUM_H,Z+0x01 MCALL OS_DRAM_FREE J8BPROC_CLASS_REFCOUNT_DEC__SKIP1: STD Z+0x02,ACCUM_EH J8BPROC_CLASS_REFCOUNT_DEC__SKIP2: POP_Z MCALL OS_DISPATCHER_UNLOCK J8BPROC_CLASS_REFCOUNT_DEC__END: RET
ООП без оверхеда на ОЗУ: почему в языке нет наследования классов, а полиморфизм интерфейсов спрятан во Flash
Любой эмбеддер знает: тащить классическое наследование классов C++ на 8-битный микроконтроллер — это боль. Виртуальные функции требуют таблиц виртуальных методов (VMT) в ОЗУ, а приведение типов раздувает структуры.
Как это решено в vm5277: В нашем Java-подобном языке наследование классов запрещено на уровне архитектуры. Но полиморфизм нам необходим, поэтому язык поддерживает наследование интерфейсов (как в Go или Rust) и поощряет композицию.
Что это дает «в железе»?
Сам объект в ОЗУ остается плоской структурой, содержащей только его реальные переменные. Но как тогда работает полиморфизм без VMT в оперативке?
Компилятор берет всю тяжелую работу на себя и генерирует минимально необходимый RTTI (информация о типах во время выполнения), полностью упаковывая его в компактные метаданные во Flash-памяти. В ОЗУ под это не тратится ни одного байта.
Вот как выглядят реальные сгенерированные метаданные во Flash для точки входа Main и классов, реализующих интерфейсы дисплея HD44780 и расширителя портов PCF8574:
; Метаданные для драйвера HD44780 _j8b_meta_525: .db 45,3,10,0,37,1,38,2 ; Компактные байты идентификаторов (RTTI) .dw j8b_CHD44780Minit_656, j8b_CHD44780MsetCursor_719, j8b_CHD44780MputChar_726 ; Прямые адреса функций во Flash ; Метаданные для слоя PCF8574 _j8b_meta_529: .db 47,1,46,3 .dw j8b_CPcf8574LayerMsetNibble_554, j8b_CPcf8574LayerMpulse_566, j8b_CPcf8574LayerMsetRS_578
В чем профит:
Вы получаете привычную по Java гибкость интерфейсов, возможность легко подменять реализацию железа (например, переключить дисплей с параллельного интерфейса на I2C через PCF8574), но платите за это немного Flash-памятью. Оперативная память микроконтроллера не задействована.
Безопасность рантайма без оверхеда: как проверить деление на ноль и переполнение, не раздувая Flash
Первая реакция любого Си-разработчика: «Проверки переполнения и индексов в рантайме? Да это же дико замедлит процессор и сожрет копеечную Flash-память на кучу условных переходов!»
Написание прошивок на Си для 8-битных МК — это ходьба по минному полю. Ошиблись в индексе массива? Переполнился счетчик? Программа молча затрет соседние переменные в ОЗУ, и вы проведете пару ночей с осциллографом, пытаясь понять, почему железка зависает раз в сутки.
В Java безопасность абсолютная, но механизм исключений (Exception) весит слишком много для микроконтроллера.
Давайте снимем этот вопрос сразу. Никто не собирается бездумно пихать тяжелые проверки в каждый такт 8-битного процессора.
В vm5277 этот компромисс решен с помощью трех инженерных оптимизаций:
Аппаратные флаги вместо простыни кода: Для проверки арифметического переполнения на том же AVR зачастую достаточно всего одной копеечной инструкции — условного перехода по флагу C.
Вынос логики в RTOS: Более сложные проверки (например, выход за границы массива) не дублируются в коде каждый раз. Они вынесены в виде компактных системных функций в ассемблерное ядро RTOS. На месте проверки во Flash тратится лишь несколько байт на обычную инструкцию вызова подпрограммы (CALL).
**Проверки не безусловны (Главная фишка): ** Вы можете полностью отключить часть проверок на уровне компиляции для релизной прошивки, когда тестирование завершено.
Но самый хитрый фокус компилятора кроется в интеграции с легковесной системой try-catch. Компилятор не встраивает проверки переполнения во весь код подряд. Он делает это точечно — только там, где программист явно обернул блок в try-catch и ожидает потенциальную ошибку!
Код без try-catch: Компилятор работает в режиме «тихого Си» — переполнения игнорируются, оверхеда по скорости и Flash нет вообще.
Код внутри try-catch: Компилятор видит, что вы хотите обработать ошибку арифметики, и только в этот локальный участок внедряет быструю проверку флага.
В итоге вы сами решаете, где вам нужна абсолютная безопасность Java, а где — максимальная скорость и легкость чистого Си.
try { System.out("\nСложение с переполнением" + " в обработчике исключения" + "(runtime): " + (b2+0xffffffff)); } catch(MathOverflowException ex) { System.out("MathOverflowException"); }
ldi r16,255 ldi r17,255 ldi r18,255 ldi r19,255 add r16,r20 adc r17,c0x00 adc r18,c0x00 adc r19,c0x00 brcc _j8b_throwskip_10015 ldi r16,7 ldi r17,0x00 ldi r18,1 call j8bproc_etrace_addfirst rjmp _j8b_catch_554 _j8b_throwskip_10015:
Исключения (try-catch) на 8-битном МК: быстрая обработка ошибок и микростектрейс
Исключения в Java хороши тем, что при аварии мы получаем детальный стек-трейс: какая функция вызвала какую и где именно всё упало. В Си на МК при ошибке мы в лучшем случае получаем бесконечный цикл while(1), а в худшем — просто внезапный перезапуск по ватчдогу.
Как это решено в vm5277:
Под капотом в легковесной системе исключений нет никаких объектов в куче и тяжелой раскрутки стека. Вместо этого компилятор использует аппаратный статус-регистр процессора (флаг T в регистре SREG для архитектуры AVR).
Передача исключения наверх занимает минимум тактов на проверку инструкции brts. Но как понять, какой именно путь прошла ошибка до того, как упасть в catch или уйти в системную панику?
Для этого в ОЗУ выделяется крошечный кольцевой буфер. Когда метод возвращает взведенный флаг ошибки, компилятор подмешивает вызов j8bproc_etrace_add, который закидывает в буфер компактный ID точки прохода (всего 1-2 байта).
Когда система ловит критический сбой, ассемблерное ядро нашей RTOS раскручивает этот кольцевой буфер и выводит в цепочку вызовов.
Stacktrace for TestException, code:SECOND PROJECT/Main.j8b:120 TestException.throw(byte) PROJECT/Main.j8b:117 Main.method8() PROJECT/Main.j8b:114 Main.method7() PROJECT/Main.j8b:111 Main.method6() PROJECT/Main.j8b:108 Main.method5() ... PROJECT/Main.j8b:86 Main.method1() End stacktrace
В чем профит:
На выходе в терминале разработчик видит лаконичный лог ошибки в стиле 05.01>0A>02 (а прошивальщик в интерактивном режиме конвертирует его в человекочитаемый стектрейс с именами файлов и номерами строк), который мгновенно указывает на цепочку вызовов методов, приведших к сбою. При этом накладные расходы — это фиксированный кольцевой буфер на несколько байт в ОЗУ и запись ID и кода при возврате из методов. Полная безопасность и читаемость логов без ущерба для 8-битного «железа».
Процедура RTOS
.IFNDEF J8BPROC_ETRACE_ADD .include "j8b/etrace_clear.asm" ;----------------------------------------------------------- J8BPROC_ETRACE_ADDFIRST: ;----------------------------------------------------------- ;Устанавливаем в буфер ид типа исключения и код ;IN:ACCUM_L-ИД типа исключения, ACCUM_H-код, ACCUM_EL-ид ;строки в исходном коде (+ACCUM_EH в 15 бит режиме) ;----------------------------------------------------------- MCALL J8BPROC_ETRACE_CLEAR RCALL _J8BPROC_ETRACE_ADDFIRST__SET ;----------------------------------------------------------- J8BPROC_ETRACE_ADD: ;----------------------------------------------------------- ;Добавляем в буфер точку прохода(или переписываем последнюю) ;----------------------------------------------------------- PUSH_X PUSH TEMP_L LDI_X _OS_ETRACE_BUFFER+0x02 ;Перемещаемся на первую точку .IF OS_ETRACE_POINT_BITSIZE==0x07 PUSH ACCUM_L PUSH ACCUM_EL LDI TEMP_L,OS_ETRACE_BUFFER_SIZE-0x02 ;Количество итераций без последнего элемента _J8BPROC_ETRACE_ADD__LOOP: LD ACCUM_L,X+ ;Считываем первую точку CPI ACCUM_L,0x00 ;Проверяем, если свободна - переходим на запись BREQ PC+0x04 DEC TEMP_L ;Иначе продолжаем итерации BRNE _J8BPROC_ETRACE_ADD__LOOP ORI ACCUM_EL,0x80 ;Все элементы заполнены, пишем в последний включив признак переполнения SBIW XL,0x01 ST X,ACCUM_EL POP ACCUM_EL POP ACCUM_L .ELSE PUSH ACCUM_H PUSH ACCUM_L PUSH ACCUM_EH LDI TEMP_L,(OS_ETRACE_BUFFER_SIZE-0x02)/2 ;Аналогичная логика только для 15 битных элементов _J8BPROC_ETRACE_ADD__LOOP: LD ACCUM_H,X+ LD ACCUM_L,X+ CPI ACCUM_H,0x00 BRNE PC+0x03 CPI ACCUM_L,0x00 BREQ PC+0x05 DEC TEMP_L BRNE _J8BPROC_ETRACE_ADD__LOOP ORI ACCUM_EH,0x80 SBIW XL,0x02 ST X+,ACCUM_EH ST X,ACCUM_EL POP ACCUM_EH POP ACCUM_L POP ACCUM_H .ENDIF POP TEMP_L POP_X RET ;----------------------------------------------------------- _J8BPROC_ETRACE_ADDFIRST__SET: ;----------------------------------------------------------- ;Фиксируем Ид первого исключения и код ;----------------------------------------------------------- STS _OS_ETRACE_BUFFER+0x00,ACCUM_L ;В начале буфера записываем ид типа исключений STS _OS_ETRACE_BUFFER+0x01,ACCUM_H ;И код SET RET .ENDIF
Типы данных: почему все примитивы беззнаковые и зачем языку встроенный тип fixed (Q7.8)
В стандартной Java все числовые типы знаковые (signed), а результирующий тип данных в большинстве выражений будет как минимум int (4 байта), поскольку byte, short и char автоматически расширяются до int при участии в операциях. В эмбеддеде это порождает лишний расход памяти и проблемы: для работы с регистрами и битовыми масками нам критически необходимы беззнаковые типы (unsigned). Кроме того, работа с дробными числами через float на 8-битном МК без аппаратного математического сопроцессора — это гарантированный способ задушить производительность и раздуть Flash-память библиотеками программной эмуляции.
Как это решено в vm5277:
Я упростил систему типов Java, сделав её максимально близкой к аппаратуре микроконтроллера.
Во-первых, все базовые примитивные типы в языке являются беззнаковыми:
boolean (1 байт)
byte (1 байт) — аналог uint8_t в Си
char (1 байт) — экономный ASCII+KOI8-R-символ
short (2 байта) — аналог uint16_t в Си
int (4 байта) — аналог uint32_t в Си
Во-вторых, вместо тяжеловесного float я ввел уникальный для языков высокого уровня встроенный двухбайтовый знаковый примитив — fixed формата Q7.8 (1 байт на целую часть со знаком + 1 байт на дробную часть).
В-третьих, арифметические операции расширяют тип до минимально возможного, т.е. компилятор анализирует количество необходимых бит для результата и подбирает минимальный тип данных.
По умолчанию работают с тихим переполнением (как в C/C++) — для максимальной производительности
При оборачивании выражения в try-catch (ArithmeticOverflowException) выполняется проверка переполнения с генерацией исключения
Это дает разработчику выбор между скоростью и безопасностью в зависимости от задачи
Зачем нужен fixed?
Знак часто необходим для вычислений (например, показания температуры или вычисления дельты). Но знаковый float на 8 битах — это катастрофа по тактам. Тип fixed под капотом — это обычная 16-битная целочисленная математика. Для Q7.8 подходит тот же функционал арифметики, что и для 16-битной целочисленной, нужно только учитывать знак. В итоге арифметика fixed выполняется так же быстро, как обычные целочисленные операции — процессор щелкает эти вычисления мгновенно, а экономия тактов и Flash-памяти по сравнению с программным float получается многократной.
Тотальный Dead Code Elimination: в прошивку попадает только то, что реально вызвано
**На ПК мы привыкли не экономить:** если нам нужен один метод из библиотеки, мы подключаем её целиком, а неиспользуемые мегабайты просто лежат в памяти или на диске. На 8-битных МК такой подход упрется в потолок Flash-памяти в первые же дни разработки. Си-разработчики используют дефайны и флаги оптимизации линковщика, но они не всегда идеально справляются с ООП-абстракциями и внутренними зависимостями библиотек.
Как это решено в vm5277:
За счет того, что вся цепочка инструментов (компилятор → собственный ассемблер → ядро) написана с нуля как единый монолитный тулкит, я смог реализовать эффективный механизм оптимизации.
На уровне ООП-языка: Если вы создали огромный класс с десятками методов и драйверов, но вызвали только один метод — компилятор физически не будет генерировать код и метаданные для остальных методов. Аналогичная ситуация и с полями классов. Подключение «тяжелого» класса ради одной фичи стоит вам очень близко к размеру кода этой фичи.
На уровне ассемблерного ядра RTOS: Если ваша прошивка не использует, например, вытесняющую многозадачность, динамический диспетчер памяти или определенные системные функции ядра — эти куски ассемблерного кода просто не попадут в итоговый бинарник.
Мой тулкит собирает прошивку буквально по кирпичикам, отслеживая каждую живую связь от точки входа. Почти всё неиспользуемое безжалостно вырезается. Вы можете смело использовать богатые возможностями библиотеки, писать понятный и расширяемый ООП-код, зная, что итоговый нативный выхлоп во Flash будет чистым, компактным и избавленным от «мертвого» кода.
Двойная модель вызова и инлайнинг: как ООП-метод превращается в одну инструкцию ассемблера
В классической Java все аргументы при вызове методов гоняются через стек. На микроконтроллерах с архитектурой вроде AVR стек — ресурс дефицитный, а постоянное заталкивание (PUSH) и выталкивание (POP) регистров ради простого изменения состояния ножки процессора похоронило бы всю производительность. С другой стороны, писать всю прошивку на «голом» ассемблере — удовольствие сомнительное.
Как я решил эту проблему в vm5277:
Я заложил в компилятор двойную модель вызова методов (которая работает в том числе и через сгенерированный RTTI), разделив их по назначению:
Стековая модель: Используется для обычных высокоуровневых методов бизнес-логики. Она обеспечивает гибкость, понятную структуру программы и привычную ООП-разработку.
Регистровая модель: Применяется для вызова нативных методов ассемблерного ядра RTOS. Мой компилятор на этапе сборки точно знает спецификацию каждого нативного метода — какие аргументы он ждет и в каких конкретно регистрах процессора. Он не тратит такты на стек, а раскладывает значения напрямую по регистрам МК непосредственно перед вызовом. Таким образом оверхед на прыжок в нативный код становится меньше.
Но важный инструмент для эмбеддера — это инлайнинг кода. Компилятор умеет схлопывать высокоуровневые вызовы (там где это может дать профит). А также может заменить вызов нативного метода на небольшой блок инструкций если аргументы не используются либо выражены в виде констант.
Вот самый яркий пример. Я пишу красивый и понятный объектно-ориентированный код для управления периферией:
// Код на моем языке J8B Gpio.setHigh(Gpio.PD1);
Компилятор видит, что этот метод нативный и на входе константа, и вместо генерации вызова, передачи параметров и возврата из функции, он превращает эту строчку в одну единственную нативную инструкцию процессора:
; Сгенерированный компилятором нативный ASM для AVR sbi PORTD, 0x01 ; Выполняется за 1 такт, занимает 2 байта во Flash!
В чем профит:
Я получаю удобный синтаксический сахар Java, где работа с «железом» выглядит удобно и безопасно, но в итоговом бинарнике это работает с близкой к той же максимальной скорости и эффективности, как если бы я вручную писал этот кусок прошивки на чистом ассемблере.
Инфраструктура, экосистема и взгляд в будущее
Помимо ключевых низкоуровневых оптимизаций, в проекте реализовано множество архитектурных и сервисных фич. Они не требуют глубокого погружения в ассемблер, но именно они превращают компилятор в полноценную и удобную экосистему. Чтобы не раздувать первую статью, я объединю их в несколько наглядных блоков:
Синтаксис и сахар
Знакомая многопоточность: Поддержка таймеров и потоков, максимально близкая по своей логике и API к стандартной Java (Thread, Timer, TimerTask).
Бесплатные enum: Статическая реализация перечислений, которая вообще ничего не стоит для рантайма и итогового размера прошивки.
Удобные синтаксические фичи: Я добавил в язык конструкции, которых мне самому не хватало: цикл for с блоком else, операторы is (аналог instanceof) и as для безопасного приведения, а также switch с поддержкой диапазонов значений.
Аннотации-мосты: На аннотации возложена критическая задача — они связывают высокоуровневый код с ассемблерным ядром (это активно используется в драйверах, где часть логики написана на J8B, а тайминговые участки — на ассемблере).
Архитектура тулкита
Кроссплатформенная кодогенерация: Модель разделена на общий фронтенд-транслятор и сменные Java-библиотеки бэкенда, реализующие кодогенерацию под конкретные архитектуры МК.
Модульная изоляция: Экосистема жестко разделена на независимые модули. Это сделано специально, чтобы новые участники проекта могли комфортно подключиться к интересным им задачам — будь то развитие компилятора, написание ассемблерного ядра под новое семейство чипов или расширение высокоуровневого рантайма.
Встроенное логирование: Из коробки доступна продвинутая база для вывода логов, которую можно перенаправить на любой порт МК, поддерживающий обычный GPIO.
Инструментарий и Роадмап
Прошивальщик: Мой собственный прошивальщик с удобным интерактивным режимом для работы с кристаллами.
Плагин для IntelliJ IDEA: Написан богатый по функционалу плагин для комфортной разработки в привычной IDE (подсветка, автодополнение, интеграция со сборщиком), хотя плагин еще активно дорабатывается.
Отладка по 1–2 пинам (В планах): В будущем я планирую реализовать полноценную отладку высокоуровневого кода прямо на железе, задействуя всего 1 или 2 вывода микроконтроллера (это уже используется для прошивки).
Виртуальная машина (В планах): Имя проекта VM изначально закладывалось не просто так. Для мощного «железа» (32 и 64 бита) в будущем планируется разработка полноценной компактной виртуальной машины.
О недостатках честно: с чем придется смириться на этапе Альфы
Проект масштабный, и я не собираюсь скрывать его текущие ограничения. На Хабре ценят честность, поэтому давайте сразу обозначим болевые точки экосистемы в её нынешнем состоянии:
Это НЕ Java: Это самостоятельный язык с Java-подобным синтаксисом. Многие привычные фичи JDK здесь либо неактуальны, либо просто не поместятся в микроконтроллер. У меня нет (и не будет в привычном виде) тяжелых коллекций с объектами в качестве ключей, Generics, Stream API или автоматического приведения любых выражений к типу int. Синтаксис Java я выбрал исключительно ради высокой читаемости кода и низкого порога входа для прикладных разработчиков.
Проекту 1.5 года, и я пишу его в одиночку: Вокруг платформы пока нет комьюнити, полноценной хорошей документации и готовой базы ответов на StackOverflow. Всё создается с нуля.
Реализация пока только для платформы AVR: Архитектура заложена гибкая, но «в железе» компилятор пока умеет работать лишь с небольшим семейством чипов AVR (в основном тестирую на ATmega328).
Высокая цена переноса на новые архитектуры: Чтобы запустить экосистему на условном STM8, PIC или Z80, для каждой платформы нужно написать (а точнее портировать существующие) три вещи: поддержку её ассемблера в тулките, Java-библиотеку бэкенд-кодогенератора и системное ядро (RTOS) на нативном ассемблере этого чипа.
Это жесткая Альфа: Я развиваю проект прагматично, отталкиваясь от своих текущих задач. Это значит, что вы легко можете наткнуться на глупые баги, недоделанный функционал или методы, которые покрывают только базовые сценарии. Сейчас я мало сил уделяю микрооптимизациям, хотя сгенерированный нативный код уже получается вполне приличным, а главное — он легко читается человеком.
Все не так плохо, как кажется.
Да, объем работы для поддержки новых платформ выглядит пугающе, но под капотом всё устроено максимально дружелюбно к разработчику.
Во-первых, прикрутить бэкенд для условного STM8 не так сложно: я постарался вынести максимум общей логики в общий бэкенд. Библиотеку под новую платформу можно писать по аналогии, взяв за основу готовую библиотеку от AVR.
Во-вторых, ассемблерное ядро RTOS тоже не придется придумывать с нуля. Его логика, структура диспетчеризации и алгоритмы управления памятью будут максимально отлажены (когда я завершу платформу AVR). Задача портирования сводится к переносу этой логики на систему команд и регистры другого процессора (архитектурно те же STM8 или старые добрые Z80 во многом концептуально понятны).
Отсутствие комьюнити на этапе альфы — это нормально. Я надеюсь, что опытные инженеры оценят заложенные преимущества и архитектуру vm5277, увидят здесь перспективу и помогут проекту вырасти.
Анатомия тулкита: из чего состоит экосистема сборки
Чтобы получить максимальную интеграцию и независимость от стороннего софта, я написал всю цепочку утилит с нуля на Java. Архитектурно экосистема разделена на несколько ключевых компонентов, которые ведут проект от исходного кода до прошивки в чипе:
1. Компилятор и Ассемблер
j8bc (компилятор): Берет ваш исходный код, автоматически подключает к нему рантайм (и внешние библиотеки, если они используются) и транслирует всё это в читаемый ассемблерный файл. Этот файл генерируется под конкретный чип с его специфичными характеристиками. При желании вы можете открыть его в IDE или текстовом редакторе, изучить каждую строчку или вручную что-то подправить.
avrasm (ассемблер для AVR): Принимает сгенерированный ассемблерный файл и собирает его в финальный бинарник прошивки для выбранного кристалла. Именно на этом этапе подгружаются необходимые низкоуровневые библиотеки ассемблерного ядра RTOS.
2. Флешер и Бутлоадер
j8bf (флешер): Работает в паре с моим бутлоадером на чипе. Флешер в полностью автоматическом режиме определяет метод подключения, считывает характеристики и идентификаторы МК, после чего выполняет быструю заливку прошивки.
Интерактивный режим флешера (Фишка): После прошивки утилита не закрывается, а может перейти в интерактивную консоль. Она перенаправляет ввод-вывод программного UART микроконтроллера в консоль ПК. Более того, флешер на лету подхватывает те самые служебные данные исключений от МК (о которых я писал в блоке про try-catch) и преобразует их в человекочитаемый стек-трейс прямо в консоли.
Bootloader: Прошивается в МК один раз. Помимо связи с флешером, в будущем он будет активно задействован для реализации сквозной отладки высокоуровневого кода.
3. Интеграция и IDE
Maven-плагин: Интегрирует компилятор в стандартный жизненный цикл сборки. Разработка и компиляция проекта для микроконтроллера выглядит и управляется точно так же, как сборка классического Java-приложения на ПК.
LSP (Language Server Protocol): Сердце языковой поддержки (еще не все реализовано). Этот сервер интегрирует лексер, AST-парсер и семантический анализатор моего компилятора с редактором кода. Сейчас он используется в плагине для IDEA, но архитектура позволяет в будущем легко перенести поддержку языка в сторонние редакторы (VS Code, Kate, Neovim и т.д.).
Плагин для IntelliJ IDEA: Универсальное рабочее пространство, которое я сейчас активно развиваю. Он обеспечивает полноценную и удобную работу в одной IDE как с высокоуровневым языком J8B (подсветка, автодополнение благодаря LSP), так и с нативным ассемблером (в будущем будут дополнительные возможности, например навигация).
Заключение: мне нужны евангелисты
Давайте подведем итог. Напомню то, с чего я начал этот разговор: я не профессиональный писатель, не компиляторщик с академическим бэкграундом и не филолог. Я инженер-системотехник, который привык идти от конкретной боли к её осязаемому решению. Мне не хватало удобного инструментария для 8-битных микроконтроллеров — и я создаю его сам, таким, каким я его вижу.
Сейчас проект vm5277 подошел к черте, когда двигаться в одиночку дальше не имеет особого смысла. Я не верю в сухой маркетинг — считаю его вторичным.
Мне нужны живые люди, которые увидят в этом проекте смысл и ценность. Люди, которым будет близка сама идея удобного, безопасного ООП-подхода для скромного, но надежного и широко распространенного 8-битного «железа». Проекту нужны технари-экстраверты — настоящие евангелисты платформы, которые:
Захотят крутить этот тулкит, тестировать его и ломать в хвост и в гриву.
Будут искать, где применить этот стек в реальных задачах.
Помогут мне выстраивать приоритеты развития архитектуры и проекта в целом.
Станут рассказывать о проекте другим, привлекая новых участников.
При этом я бы хотел остаться на текущем месте - т.е. продолжать дорабатывать проект надеясь, что он станет полезен обществу.
Экосистема изначально спроектирована модульно. Если вы хотите приложить руку к коду — для вас открыты любые направления: от оптимизации компилятора до портирования ассемблерного ядра RTOS под новые платформы (например, под те же STM8 или PIC), расширения рантайм-библиотек или шлифовки плагина для IDEA.
Их пока нет — тех людей, которые вдохнут в этот проект полноценную жизнь.
И я не умею выстраивать сообщество. Я одиночка, который последние полтора года писал код. Комьюнити-менеджмент, организация вклада, модерация обсуждений — это отдельная работа, на которую у меня нет ни времени, ни компетенции.
Поэтому мне нужен не просто пользователь, а человек, который захочет стать связующим звеном между проектом и людьми. Технарь, который понимает ценность идеи, но при этом умеет общаться, объяснять, вовлекать и выстраивать процессы. Я пишу код, он — сообщество.
Забегая вперед. Я понимаю, что после этой статьи возникнет много вопросов — справедливой критики и не очень. Требования показать бенчмарки, разобрать конкретные сценарии, сравнить с Си — всё это закономерно.
Это не последняя статья. Я постараюсь ответить на ваши вопросы в следующих материалах.
Ваши комментарии под этим постом — они помогут определить, о чём писать дальше.
Полезные ссылки:
Точка входа — предлагаю начать с этой инструкции
vm5277.ru — основной сайт
github.com/w5277c/vm5277 — весь проект, включая компилятор, ассемблер, ядро RTOS и тулкит.
github.com/w5277c/vm5277/tree/main/examples/j8b — примеры программ на J8B.
github.com/w5277c/vm5277/tree/main/docs — документация (пока сырая и местами не актуальная)
Telegram группа - группа в телеграмме
P.S. Я написал этот текст с помощью нейросети.
Потому что это логичный инструмент, позволяющий экономить время и выдавать структурированный материал. Не вижу смысла притворяться писателем.
Если у проекта появится сообщество, я готов делегировать академическую часть и документацию тем, кому это интересно.
Update:
Важно понимать: я не создаю новый концептуальный язык программирования и не соревнуюсь с Zig или Rust в выразительности типов. Моя цель — взять уже готовый, максимально популярный синтаксис (Java) и адаптировать его под жесткие рамки 8-битных МК.
Любые фичи современных языков, какими бы красивыми и полезными они ни были, сознательно отсекаются, если они нарушают эту главную концепцию.
Комментарии (54)

Jijiki
31.08.2026 11:11ну тоесть получается на десктопе вся та портянка зависимости получается ляжет в память по счетчику ссылок или только родители/дети, я не знаю как в ембединге вежет себя ява и не до конца понимаю внутрянку языка, но там помойму внутрянка решающая, тоесть это возможно придётся переписать весь язык, ведь нью может запускать каскад нью под капотных типо нетривиальных, они тоже подцепляются на счетчики интересно, если так, то на интрузивном счетчике ссылок на С++ можно целый аналог явы чтоли сварганить ?) мне кажется это было бы проще, хотя то как ява оптимизирована в плане запуска байт-кодов восхищает конеш........ интересно очень, но ничо не понятно )))

lgorSL
31.08.2026 11:11Вопрос про подсчёт ссылок: кольцо из ссылающихся объектов приведёт к утечке? Сборщик мусора двигает объекты в памяти при сборке или нет? Если нет - как решается проблема фрагментации? Есть ли escape analysis, когда объект создаётся внутри функции, гарантированно не утекает и тут же выкидывается? (Тогда можно вообще счётчик ссылок не заводить)
И ещё одно замечание про оверхед со ссылками и прочее - в С# в системе типов изначально были ещё и структуры (а в java их всё никак не приделают), и кажется что для МК они нужны. Идея структуры в том, что она передаётся по значению (прям как си), и позволяет создавать меньше объектов и плотнее хранить данные в памяти.
P.s. я сам пробовал задизайнить язык, но я правда пришёл к ещё более радикальной идее - у меня один и тот же класс может передаваться и по ссылке и по значению. У меня у ссылок на объект в куче тип Gc[Smth], а у объекта по-значению - Smth, и можно один и тот же класс точечно и на куче создавать и на стеке (например, если я как программист знаю, что он временный).
P.p.s. какие-то мысли про язык и процесс разработки (сам язык сильно отличается, но надеюсь какие-то вещи могут пригодиться) : https://kright.me/2026/07/06/delaiu-svoi-iazyk-programmirovaniia/

5277 Автор
31.08.2026 11:111. Кольцевые ссылки: Да, в текущей реализации это приведет к утечке. На данном этапе я сознательно не усложнял архитектуру (нет ни weak-ссылок, ни trace-компонента), так как в рамках 8-битных систем сложные цикличные графы объектов практически не встречаются — логика приложений пишется максимально плоско. Избегание циклов сейчас — это ответственность прикладного программиста.
2. Перемещение объектов: Нет, никакого перемещения и полноценного Garbage Collector здесь нет. Механизм максимально детерминированный: чистый Reference Counting с мгновенным освобождением памяти при обнулении счетчика. Поэтому адрес объекта в куче (HEAP) является его постоянным и уникальным идентификатором.
3. Фрагментация: Проблема решается на уровне кастомного менеджера памяти. Аллокатор работает по принципу битовой маски, где 1 бит отвечает за блок в 8 байт (дискретная сетка/слоты). Для 8-битной архитектуры с небольшими объектами такой подход сводит внешнюю фрагментацию к минимуму при крайне низком расходе памяти на метаданные аллокатора.
4. Escape analysis: На данный момент отсутствует и будет рассмотрен как задача оптимизации. И таких задач не мало — Альфа версия. Вы правы: если объект гарантированно не покидает пределы метода, то подсчет ссылок не нужен - это вопрос оптимизации кода.
Важный нюанс, который часто упускают при обсуждении Escape-анализа для 8-битных систем:Статический анализ компилятора никогда не покрывает 100% кейсов. Если алгоритм не может гарантированно доказать, что объект "не убегает", он обязан свалиться в безопасный режим и включить подсчет ссылок в рантайме.
На масштабах прошивок для 8-битных МК, где методы и жизненный цикл объектов экстремально короткие, а память сильно ограничена, доля успешно доказанных и при этом эффективных оптимизаций через Escape-анализ будет ничтожной. Усложнение компилятора получается значительным, а реальный выигрыш в байтах или тактах — копеечным. В условиях жестких физических ограничений внедрение таких тяжелых механизмов оптимизации просто экономически и инженерно нецелесообразно. Особенно когда есть пласт более приоритетных задач.

rukhi7
31.08.2026 11:11Кольцевые ссылки: Да, в текущей реализации это приведет к утечке.
Интересно! А вы зачем вообще ссылки считаете, у вас же вот даже в вашем примере:
Timer blink = new Blink(); while(true) {программа никогда не кончается (while(true) !!!)? И это нормальная ситуация для 8-битных контроллеров. Получается вы это как-то не учли что ли? На 8-ми битных процессорах сборщик мусора просто не имеет смысла, по факту, какая нафик утечка, куда? Мусора просто не должно быть! При очень ограниченных ресурсах это непозволительная рокошь.

5277 Автор
31.08.2026 11:11Извините, я не понимаю что Вы хотели этим сказать?
Я просто оставлю это здесь:

или это из коллекции


kmatveev
31.08.2026 11:11Какая связь между незавершающейся программой и подсчётом ссылок? Ну сделал он в примере new до цикла, а в примере посложнее сделал бы в каком-нибудь методе, который вызывается в цикле и возвращает это значение. В C для этого возвращался бы указатель.
Динамическое выделение памяти не является непозволительной роскошью, реально используемой динамической памяти в каждый момент времени обычно сильно меньше, чем если бы память под все используемые данные была бы выделена статически.
И утечки тут не при чём, утечка - это когда память выделяется и не освобождается, в языках с динамической памятью и сборкой мусора нет явного освобождения, и если ссылок не осталось, то утечек не будет.

rukhi7
31.08.2026 11:11ну не знаю что для вас является аргументом. Ну вот например:
Если вы освобождаете память динамически, значит вы ее где-то и выделяете динамически, значит у нас какой-то алгоритм должен работать, индексации-распределения блоков этой памяти. Но на скажем 4-килобайтах оперативной памяти и скажем 20 МГц тактовой частоты процессора это выглядит как совсем лишняя работа, которую вообще говоря всегда(!) можно избежать-решить на уровне проектирования программы с помощью какой-то простейшей индексации например, тех же linked list техник...
Тащить в такую систему универсальный сборщик мусора, на все случаи жизни, мне кажется большой перебор. По крайней мере я всегда находил способ решить это примерно так как я описал. Утечки на таких ограниченных системах должны исключаться на этапе проектирования программы, иначе вам там ничто не поможет добиться хотя бы стабильности.

5277 Автор
31.08.2026 11:11Вы практически описали концепцию J8B, только предложили делать её руками. Ваши кастомные связанные списки (linked list) и ручные индексы в Си - это и есть динамическое распределение ресурсов, на которое процессор потратит ровно те же такты. Разница лишь в том, что в Си Вы пишете это вручную под каждую задачу (регулярно ловя баги с указателями), а в vm5277 этот алгоритм написан один раз, вылизан на ассемблере и скрыт в рантайме.
Но самое забавное - динамика в J8B добровольная. Если Вы так сильно её отвергаете, вас никто не заставляет использовать
new. Вы вольны объявлять переменные глобально, делать методы статическими и писать всю программу на чистой статике, как Вы привыкли в Си. Язык дает выбор: хотите - пишите в старом процедурном стиле, хотите - используете мощь безопасного ООП с экономией ОЗУЗачем вообще использовать ООП и интерфейсы на микроконтроллерах?
В Си-подходе, если вам нужно перенести код экрана с аппаратного I2C микроконтроллера ATmega328 на программный ногодрыг (Bit-bang) для ATtiny (где аппаратного I2C нет в принципе), вам придется переписать всю библиотеку дисплея, обмазав её макросами
#ifdef. А если у вас в системе 5 разных дисплеев и 3 разных датчика - Си-код превратится в нечитаемый ад.В ООП-модели J8B эта проблема решается через полиморфизм интерфейсов. Вы пишете драйвер дисплея один раз в жизни. Ему всё равно, какая под ним железка. В конструктор экрана вы можете передать объект
HardwareI2cна одном чипе, илиSoftwareI2cна другом. Код самого экрана не изменится ни на одну строчку.ООП на 8 битах нужно не ради абстракций ради абстракций. Оно нужно, чтобы избавиться от бесконечного переписывания драйверов, защитить ОЗУ от багов с указателями и собирать прошивки из готовых, безопасных и на 100% переносимых кубиков как в LEGO - все это дико экономит время и нервы разработчика а также снижает его порог вхождения в данную отрасль.

5277 Автор
31.08.2026 11:11Этот проект родился не на пустом месте. У меня есть собственный масштабный проект умного дома, где десятки прошивок (датчики, шлюзы, исполнительные устройства) написаны мной на чистом ассемблере на базе моей же RTOS (
core5277).Я знаю регистры и ассемблер досконально. И именно на практике я упёрся в стену: когда устройств много, а их бизнес-логика усложняется, нативный кодинг (даже на Си) превращается в ад из-за нулевой переносимости кода и постоянного риска словить скрытый баг с памятью.
vm5277 - это закономерная эволюция моей ассемблерной RTOS. Если вам нужно один раз написать простую мигалку — пишите на Си, моё решение вам не нужно. Но если вы строите распределенную экосистему устройств, где бизнес-логика постоянно растет, а чипы из-за дефицита или рынка приходится менять, - безопасность, читаемость и кроссплатформенность J8B окупают себя на 100%

rukhi7
31.08.2026 11:11Если вам нужно один раз написать простую мигалку — пишите на Си, моё решение вам не нужно.
Когда есть С++ я пишу на С++, но new там идет в огромной ран-тайм библиотеке стандартной и я очень замечательно обхожусь без этой библиотеки и без
new, соответственно. Слава богу чтобы вызвать деструктор для объекта на стеке при выходе из функцииnewне нужен и это очень удобно. Но С++ на мелких процах конечно никто не делает, наверно, но я уже давно не писал на мелких.Кстати на процах посерьезнее уже не обойтись без библиотек. Например, мы использовали LWIP в исходниках - я там даже что-то тоже поправил на уровне работы с регистрами почти. Вы LWIP тоже на Джаву переведете :) ? Библиотеку c TCP/IP стеком тоже напишете под MAC контроллер переферийный?

5277 Автор
31.08.2026 11:11"Вы LWIP тоже на Джаву переведете :)" - Уже была такая мысль. Может быть. Не забывайте, что TCP/IP это уже существующий и множество раз реализованный стек. Его не нужно изобретать.
Разработчику способному создать RTOS на ассемблере такая задача вполне по зубам. Особенно если будет команда. Особенно, если проект наберет популярность. Может быть даже мой любимый SCTP реализую - ведь это не про 8-бит, это про vm5277 который я планирую и на десктопы развернуть.
P.S. Вы меня пытаетесь напугать MAC'ом? Серьезно?

5277 Автор
31.08.2026 11:11Знаете почему я люблю писать на Java и на ассемблере но никак не на Си?
Потому что все эти наработки на Си превратились в тонны спагетти-кода, с кучей дефайнов, с кучей полностью не читабельного кода. Там чтобы понять чужую бизнес логику нужно прочитать талмуды размером с войну и мир, потому что все перемешано и связано, даже комменты не помогают.
И в итоге вместо нескольких минут ты уже сидишь пятый час чтобы понять элементарную бизнес логику.
Реализация сети? Не спорю, если реализовать все до крайности - на это годы на Си уйдут. Но я, на своем опыте прекрасно знаю, что если бы у этих библиотек был бы универсальный HAL - они были бы значительно меньше. А реализовать ethernet, ip, udp - это уровень второго курса универа. TCP да гораздо сложнее - но там и половины функционала в рамках МК не нужно.
Так что да, запилить поддержку пары Ethernet микросхем для МК (для начала только с UDP) задача тривиальная. Я больше сил потрачу на раскуривание сишного кода.
Попробуйте поработать с Java - напишите несколько сетевых утилит, серверов, протоколов. Попробуйте на ней помигать светодиодами на той-же Raspberry. А потом вернитесь к Си с его фишками и оцените как удобно на нем писать портируемый код скажем на avr и stm32

rukhi7
31.08.2026 11:11Попробуйте поработать с Java - напишите несколько сетевых утилит, серверов, протоколов.
ну я тут как то LDPC декодер написал на Java для статистических испытаний, потом переписал на C#. C# мне болльше нравится. Вы бы хоть поинтересовались прежде чем предлагать что-то попробовать. Вы вот можете сказать в чем принципиальное отличие Java от C#? Я вот особой принципиальной разницы не заметил если наличие функциональности в библиотеках не сравнивать.

5277 Автор
31.08.2026 11:11Это больше комментарий для тех кто читает, не к Вам лично.
Да, С# это переписанная и доработанная Java для Windows, и он гораздо ближе к Java чем к C.
Тогда я видимо не правильно Вас понял посчитав за критика и сторонника Си.
Вероятно я запутался в комментах, извините.

mozg37
31.08.2026 11:11Вот сугубо практическая задача. dma принимает с внешнего источника пакеты - кольцевой буфер, прерывания на середине и конце буфера - ну как обычно. Как это на этой недожаве реализовывать?

5277 Автор
31.08.2026 11:11В vm5277 работа с прерываниями, DMA и регистрами железа пишется исключительно на нативном ассемблере внутри ядра RTOS, а не на прикладном языке. Оверхед там близок к абсолютному нулю.
Прикладной язык J8B используется только как безопасная ООП-обертка над этой логикой. Ассемблерный обработчик DMA (по прерываниям) точно так же складывает данные в кольцевой буфер в куче, а из J8B вы просто вызываете потоковый
read().Посмотрите, как это уже сделано для обычного аппаратного UART:
https://github.com/w5277c/vm5277/blob/main/rtos/avr/drivers/uart/hw_full.asm
https://github.com/w5277c/vm5277/blob/main/runtime/drivers/ifaces/UartHwFd.j8b
Для DMA можно сделать аналогично. Но программный кольцевой буфер не обязателен — можно писать/читать напрямую в созданные в HEAP массивы на основе
new byte[]. А еще есть механизм RtosCallback, через который низкий уровень может вызывать J8B-код. Пример: https://github.com/w5277c/vm5277/blob/main/examples/j8b/timer1/src/Main.j8bКакое конкретно будет принято решение — зависит от задачи. Когда проект дойдет до поддержки DMA (а это актуально для 32-битных архитектур, а не 8-битных), гибкость архитектуры позволит выбрать максимально эффективное решение. И под конкретную задачу оно, вероятнее всего, окажется чище и оптимальнее, чем на C++.
А самое важное - работа с DMA - это не прикладной уровень. Разработчику нечего там делать - это задача ядра системы.
Я понимаю, что мир Си заставляет разработчика писать код буквально на всех уровнях единой простыней. В этом очередное важное отличие моего решения - прикладной разработчик не изучает даташиты, регистры периферии МК, особенности компилятора. Он просто создает объект нужного класса - например BMP580 и работает с его методами не вникая что там лежит уровнем ниже.

rukhi7
31.08.2026 11:11В этом очередное важное отличие моего решения - прикладной разработчик не изучает даташиты, регистры периферии МК, особенности компилятора. Он просто создает объект нужного класса - например BMP580 и работает с его методами не вникая что там лежит уровнем ниже.
Так вы собрались сделать библиотеку классов объектов на все случаи жизни получается? Если не изучать даташиты, регистры периферии МК, особенности компилятора то, видимо, придется изучать описание методов и поведения ваших объектов и все равно компилятора этих объектов в объектный код, только вашего(!) компилятора, что-то очень сомнительно что это может в принципе быть намного проще, а главное что этого будет достаточно для решения любой эмбедед задачи.

5277 Автор
31.08.2026 11:11Я разделяю функционал:
1 - то что не требует работы на низком уровне (не критично по времени, не требует работы с IO регистрами (кроме GPIO), не требует максимальной оптимизации) - пишется на j8b (в том числе и драйвера) - это дает полную переносимость.
2 - драйвера, которые не привязаны к железу а только к интерфейсам (i2c, uart, spi и прочие) - работают также на j8b - опять-же полная переносимость (может быть также реализовано на программных интерфейсах)
3 - остальной код реализуется на ассемблере и должен быть реализован в ядре (но часть его также может быть на j8b)
4 - рантайм будет и уже содержит некоторые драйвера конечных устройств (типа bmp580) - прикладнику достаточно просто использовать класс датчика подставляя ему нужные классы реализации интерфейсов. Для этого действительно не нужно знать даташиты.
5 - прикладник может также написать сам драйвер устройства если он основан на популярных и поддерживаемых в vm5277 интерфейсах и даже сами интерфейсы если им достаточно GPIO. И здесь ему также не нужно знать даташиты на МК - как реализовать интерфейс (прерывания, IO регистры и прочее)
6 - реализация стандартных интерфейсов и прочей периферии конкретного семейства МК - задача разработчиков нативной части (ядро и RTOS на ассемблере) - но это задача не прикладника.
Да - прикладнику нужно будет знать названия классов устройств и методов. Для этого по большей части достаточно простого описания методов. например:
public SpiSwHd(byte dataPort, byte clkPort, boolean fastMode, byte delay, boolean cpol, boolean cpha, boolean lsbFirst) {...}
public short comm(byte[] sendBuf, short sendLen, byte[] recvBuf, short recvLen, short timeout) throws DriverException {...}
Это далеко не то-же самое что изучение даташита МК на предмет IO регистров работы с SPI
Вот еще пример:
// Регистрируем обработчик (менеджер сам определит оптимальный механизм детектирования) regId = PinEventManager.register(ArduinoUno.PIN_D2, inst, PinEventManager.TRIGGER_MODE_ANY);
Прикладнику даже не нужно особо заморачиваться как именно реализуется обработка смены состояния пина (прерывания INT или PCINT и т.п.). Конечно если он захочет оптимизировать все по максимум - ему придется спуститься на уровень ниже - мое решение это позволяет - вплоть до ассемблера. (PinEventManager - еще в разработке)

mozg37
31.08.2026 11:11Не попытка ли это плодить сущности? Настроить таймер/уарт/etc это лишь записать насколько регистров. Это довольно просто и интуитивно. А плодить rtos, и еще сверху оверхед - разумно ли это? Я не критикую, просто дискуссия.

5277 Автор
31.08.2026 11:11Только на atmega328 три таймера, где-то WD еще подключают - настраиваются по-разному (элементарно что-то 8 бит, что-то 16). На других avr таймеров другое количество. Где-то UART, где-то USART, где-то USI, где-то вообще ничего. А еще и имена регистров отличаются а также их биты и в принципе их порядок и функционал. И это только Atmega и UART, а еще ATtiny - может сильно отличаться. И это только платформа AVR, а добавьте туда PIC, STM8 и не дай бог STM32.
Сколько у Вас займет времени перенести работу I2C экрана с Atmega328 на attiny где нет I2C? Я даже не буду говорить, что примеры которые я видел на си подобной реализации были с существенными ошибками.
А если Вам нужно 10 таймеров? И пять UART'ов? Будете покупать в несколько раз дороже чип?
Вы просто не представляете о чем Вы говорите - переносимость - это очень серьезная проблема. Попробуйте сделать самое элементарное - прослойку которая будет просто по пину предоставлять возможность вызова процедуры при смене любого выбранного пина как при работе с INT для простого семейства Atmega. Опытный системный разработчик потратит несколько дней на такую задачу. А для Вас я смотрю это просто и интуитивно.
Я даже уже не говорю о том, что работа с регистрами это не безопасно в отличии от вызова метода.

rukhi7
31.08.2026 11:11Сколько у Вас займет времени перенести работу I2C экрана с Atmega328 на attiny где нет I2C?
интересно было бы посмотреть на некий универсальный класс-объект для I2C экрана который каким то чудом будет одинаково работать
и на Atmega328
на attiny где нет I2C !!!
Вы просто не представляете о чем Вы говорите - переносимость - это очень серьезная проблема.
но до переносимости еще нужно решить проблему возможности реализации с заданными характеристиками. Обычно нужен не какой-то абстрактный I2C экран, а I2C экран с четко заданными характеристиками - таймингами например, и он не в сферическом вакууме должен работать, а в жестких условиях конкуренции за очень ограниченные ресурсы с другими пользовательскими функциями программы.
Вы что, собираетесь сделать универсальные-божественные классы, которые подходят под любую задачу? Мне кажется, это похоже на задачу алхимии, вы пытаетесь создать лекарство от всех болезней и ключ от всех дверей, не повторяйте ошибок прошлого.

5277 Автор
31.08.2026 11:11Этот проект начат не с чистого листа. У меня есть проект RTOS для AVR на ассемблере https://github.com/w5277c/core5277
Вот в нем есть программные драйвера, например https://github.com/w5277c/core5277/blob/devel/core/drivers/i2c_su.inc - I2C Slave основанный на USI
В ядре текущего проекта для AVR будет набор программных драйверов. Конечно они будут более медленные, чем аппаратные и будут больше потреблять ресурсов, но они будут.
При этом огромное преимущество дает верхний уровень - можно писать драйвера (не зависящие напрямую от железа) на j8b один раз с полной переносимостью кода. Пример https://github.com/w5277c/vm5277/blob/main/runtime/drivers/ifaces/SpiSwHd.j8b
Вот пример реализации в моем проекте светодиодной матрицы 8x8 на базе MAX7219 https://github.com/w5277c/vm5277/blob/main/runtime/drivers/display/LedMatrixMax7219.j8b в его конструкторе создается программный интерфейс (аналогично будет и для любого экрана с I2C) iface = new SpiSwHd(dinPort, clkPort, true, 0, false, false, false);
А вот кстати и I2C символьный экран HD44780: https://github.com/w5277c/vm5277/blob/main/runtime/drivers/display/HD44780.j8b там вообще заложен layer как прослойка реализующая работу с разными интерфейсами.
Конечно это все черновые наработки (проект в Альфе, и я недавно поломал I2C и часть примеров перестала работать) но все что у меня есть в примерах (а там разные устройства) почти все они работоспособны (а то что сломал я скоро починю). Но тем не менее архитектура вполне рабочая.
Просто не забываем про мощь ООП модели.




Ну и на закуску, пример работы с ws2812b который максимально привередлив к ресурсам и таймингам МК.

P.S. Конечно в реальности есть куча жестких ограничений по ресурсам, которые не дадут к примеру запустить тот-же ws2812B на частоте менее 8МГц (на 8 - это уже предел возможностей). Но здесь мы уже бессильны.
P.P.S. Еще можно сюда заглянуть https://github.com/w5277c/vm5277/blob/main/examples/j8b/max7219/src/Main.j8b - там вообще магия и она работает прямо сейчас.

rukhi7
31.08.2026 11:11ну вот! Что и требовалось доказать! По моему гораздо проще изучать даташиты, регистры периферии МК, особенности компилятора, чем то разнообразие надстроек над этим богатством, которое вы (еще только) собираетесь предложить, учитывая что:
Конечно это все черновые наработки (проект в Альфе, и я недавно поломал I2C и часть примеров перестала работать)
Но успехов вам, конечно. Большая-продолжительная работа в конце концов находит практическое применение, хоть и не всегда там на что изначально была цель.

5277 Автор
31.08.2026 11:11Это целиком Ваше право и Ваше мнение. Спорить с этим никакого смысла не имеет.
"которое вы (еще только) собираетесь предложить" - сейчас я привлекаю разработчиков к Альфе, а не пользователей к релизу. Вы же меня упрекаете что на Альфе не все готово - естественно не все готово - именно об этом я и пишу в статье.

mozg37
31.08.2026 11:11Разумно ли вообще сейчас что-то пилить на древних устаревших и дорогих mcu? Китайские кортексы ххх32 дешевы, мощны и куча периферии.

5277 Автор
31.08.2026 11:11Конечно разумно.
1 - 8 бит имеют свой сегмент и он очень не маленький и у них есть свои преимущества которых нет у чипов на базе Cortex
2 - 8-бит - это старт проекта - если проект зайдет, то он придет и на 32 бита и на десктопы.
3 - вообще тема использования мощных МК достаточно сомнительная, потому что их можно заменить на SoC с еще большими преимуществами.

smt_one
31.08.2026 11:11Наивный подсчёт ссылок менее эффективен, чем tracing GC (источник - https://www.researchgate.net/publication/225216046_Uniprocessor_Garbage_Collection_Techniques). Если собираетесь останавливаться на подсчёте ссылок, посмотрите на Perceus (https://dl.acm.org/doi/10.1145/3453483.3454032).

5277 Автор
31.08.2026 11:11Я не претендую на гениальность, и тем более не считаю, что научное сообщество просто так ест свой хлеб.
Но есть два важных момента:
1 - 8-битный МК - это просто, здесь нет многоядерных решений, нет кэша процессора и очень простая архитектура процессора в целом.
Кроме этого, сильно ограниченные ресурсы, где лишняя логика в рантайме - серьёзный оверхед.
2 - наивный подсчёт ссылок без оптимизации со стороны компилятора - это действительно не оптимально, потому что обязательно найдутся условия, в которых умный компилятор сможет доказать определённые кейсы и оптимизировать код, убрав лишние операции работы со ссылками - я это понимаю.
Но такие алгоритмы мной сейчас не рассматриваются, потому что главная цель - наработать функционал.
Оптимизация кода будет позже и будет затрагивать разные аспекты. Возможно, вообще будет только в коммерческой версии.
К тому-же бэкенд кодогенератор и ядро не обязательно должно быть идентичным на всех платформах, более того, они действительно будут сильно отличаться если сравнить 8 бит и 32 бита (например менеджер динамической памяти) - что не повлияет на код верхнего уровня.
Спасибо за информацию. Когда я подойду к вопросу оптимизации (а в проекте явно есть что оптимизировать) - Ваши ссылки мне будут очень полезны.

Filipp42
31.08.2026 11:11Добрый день!
Сердечно поздравляю вас с таким замечательным успехом!
Я сам очень интересуюсь разработкой компиляторов и разработкой ОС.Думаю, что я готов был бы попробовать поддержать ваш проект, но у меня есть несколько пожеланий по поводу его развития.
Очень прошу не составлять о них первого категоричного мнения, а попробовать беспристрастно разобрать плюсы и минусы.
1) Очень хотелось бы иметь маркированное объединение с возможностью сопоставления с образцом. Это позволяет гораздо изящнее строить абстракции. Кроме того, я слышал, что прошивки для микроконтроллера часто представляют из себя конечный автомат. А то, что я предложил (алгебраические типы данных) очень хорошо подходят для этого дела.
Подробнее можно прочитать вот здесь: https://habr.com/ru/articles/1033910/
2) В Java объявить ещё один тип - это достаточно муторное занятие. Нужно создавать отдельный файл, ну и так далее. Но опыт показывает, что полезно создавать не мало больших типов, а много маленьких. А в Java это не удобно.
Я бы хотел, чтобы вы добавили возможность создавать типы в любом лексическом окружении. Это просто сделать, но это очень поможет программистам.
3) Очень хотелось бы, чтобы вы попробовали несколько разных языков: Zig, Lean, Clojure, Prolog если не сделали этого раньше. Это поможет прекрасно расширить кругозор!4) Не могли бы вы попробовать программу для проверки моделей Alloy model checker? Она позволяет выявлять баги в архитектуре.
Скажите пожалуйста, а почему вы ориентируетесь именно на синтаксис Java, а не на синтаксис Kotlin?
У меня есть ещё много вопросов, но не всё ведь сразу!
Спасибо вам за вашу разработку!
5277 Автор
31.08.2026 11:11Спасибо за ваши предложения и ссылку на статью! Однако я хотел бы пояснить, почему наши взгляды на проектирование языков программирования принципиально расходятся.
Исходная точка: физические ограничения против теории типов Мой подход исходит из физических ограничений целевого железа (8-битный МК и 2 КБ ОЗУ). Это не абстракция, а жесткая реальность. Каждая фича языка в vm5277 должна платить за свое существование байтами Flash/RAM и тактами процессора. Ваш подход, как я понял, идет от выразительности и корректности на уровне типов. Это прекрасно там, где есть гигабайты ОЗУ и JIT-компилятор, но избыточно для микроконтроллеров. Я выбрал ООП-модель Java в качестве основы, потому что она узнаваема миллионами, достаточна для задач embedded и поддается глубокой оптимизации под 8 бит (наш RTTI упакован во Flash, VMT в ОЗУ отсутствует, а объекты остаются “плоскими”).
Про маркированные объединения и pattern matching: В J8B полиморфизм интерфейсов уже решает задачи подмены реализаций. Внедрение ADT потребует кардинальной перестройки парсера и семантического анализатора, что нецелесообразно. А текущий enum потребляет 0 ресурсов рантайма.
Про конечные автоматы: Вы предлагаете реализовывать их через ADT и match. В моей же модели автомат — это класс, инкапсулирующий состояние. При необходимости он может управляться отдельным легким потоком (Thread), что дает изоляцию и параллельность, либо работать в рамках общего цикла.
Мы смотрим на язык с разных сторон: вы — со стороны теории типов, я — со стороны эффективной кодогенерации под ассемблер.
По поводу локального объявления типов: Объявить новый тип в лексере и парсере, да и в семантике — это как раз легкая задача. А вот с кодогенерацией существенно сложнее — для каждого нового типа нужно с нуля прописать логику сравнений, арифметики, присваивания и приведение типов и их взаимодействия друг с другом на уровне регистров.
По поводу расширения кругозора (Zig, Lean, Clojure и др.): В моем инженерном багаже: C, C++, Java, C#, Bash, Assembler, Basic, Python, Kotlin, Dart и многое другое. Мне не нужно абстрактно расширять кругозор ради самого кругозора. Я решаю конкретную практическую задачу, стремясь реализовать её оптимально, а не универсально.
По поводу Alloy model checker: Формальная верификация моделей потребует от меня колоссального количества сил и времени. Я предпочитаю потратить этот ресурс на написание кода, развитие рантайма и исправление живых багов альфа-версии. Прагматизма в Alloy для хобби-проекта на одного человека я сейчас не вижу.
Почему Java, а не Kotlin? Если отбросить маркетинговое продвижение Kotlin, то в реальном мире первыми по популярности среди прикладных языков идут C и Java.
Именно по этой причине мой язык максимально похож на синтаксис Java. Изменено по большому счету только то, что экономит ресурсы или банально удобнее в разработке и при этом ничего особо не стоит для рантайма.
Буду рад вашим дальнейшим вопросам, но очень прошу учесть: я практик и всегда исхожу от конкретной инженерной задачи. Чистая теория, как и синтаксический сахар, мне не особо интересна.

lgorSL
31.08.2026 11:11Исходная точка: физические ограничения против теории типов Мой подход исходит из физических ограничений целевого железа (8-битный МК и 2 КБ ОЗУ). Это не абстракция, а жесткая реальность. Каждая фича языка в vm5277 должна платить за свое существование байтами Flash/RAM и тактами процессора. Ваш подход, как я понял, идет от выразительности и корректности на уровне типов. Это прекрасно там, где есть гигабайты ОЗУ и JIT-компилятор, но избыточно для микроконтроллеров.
У вас ложная дихотомия. Корректность и сложность системы типов ортогональны размеру скомпилированного бинарника. Они могут усложнить компилятор, но не требуют сложности от рантайма. Например, явная nullability в котлине позволяет на этапе компиляции явно доказать, что какие-то ссылки нулевыми не будут и спасает от ошибок. Или, например, лайфтаймы в расте существуют только во время компиляции как доказательство корректности, в скомпилированном коде их нет. (Конкретно в Расте кстати не всё хорошо, авторы игнорировали теорию типов, в итоге в языке оказались проблемы, которые просто так не выкорчевать). И всякие constexpr и consteval фичи в с++ это как раз про то, чтобы помочь компилятору вынести какие-то вычисления на этап компиляции, а не в рантайм.

5277 Автор
31.08.2026 11:11Дело не в самом усложнении рантайма, а в усложнении библиотеки бэкенд кодогенератора без какой-либо существенной практической пользы (который требуется реализовывать под каждую платформу отдельно) - я не достаточно развернуто ответил на этот вопрос ранее.
Более того, я объяснил существенную причину почему выбран Java синтаксис. Да и в принципе вопрос выбора языка и его ситнаксиса я не рассматриваю, кроме изменений который действительно будут существенно полезны для разработки прошивок на 8 бит Мк, как например замена signed типов на unsigned.
Все перечисленные вами примеры требуют нетривиальной системы зависимостей между типами, подстановки, обобщений и проверок на уровне AST/семантики. Это всё ложится на этап компиляции и кодогенерацию. Для моего компилятора под 8 бит это означает не просто "ещё одна проверка", а перестройку всей инфраструктуры вывода кода которая (изначально достаточно сбалансирована) — и это я считаю неоправданным без явной, измеримой выгоды для конечной прошивки

Azeront
31.08.2026 11:11Конкретно в Расте кстати не всё хорошо, авторы игнорировали теорию типов, в итоге в языке оказались проблемы, которые просто так не выкорчевать
Извините за оффтоп, но можете ли вы порекомендовать источники, в которых сруктурировано изложены упомянутые вами проблемы? Мне для общего кругозора.

lgorSL
31.08.2026 11:11Есть такое видео, но его сложновато слушать: https://www.youtube.com/watch?v=1iPWt1gvT_w
И ссылки на почитать. https://habr.com/ru/articles/1033328/ https://github.com/rust-lang/rust/issues/25860 https://github.com/rust-lang/rust/issues/135011
Поискать можно по словам типа “Rust type system unsoundness”
Ещё я не нашёл ссылки, но суть в том, что некоторые стандартные классы в языке можно было бы сделать монадами, но так сложилось, что их написали как набор отдельных самостоятельных классов со своими уникальными названиями методов.

kmatveev
31.08.2026 11:112) В Java объявить ещё один тип - это достаточно муторное занятие. Нужно создавать отдельный файл, ну и так далее. Но опыт показывает, что полезно создавать не мало больших типов, а много маленьких. А в Java это не удобно.
В Java публичный класс должен находиться в отдельном файле, а package-private и private классы не обязаны, могут жить в файле публичного класса, я часто это использую.
Я бы хотел, чтобы вы добавили возможность создавать типы в любом лексическом окружении. Это просто сделать, но это очень поможет программистам.
Java сто лет уже это умеет, вы можете объявлять классы внутри классов и внутри методов. Умеет ли язык автора - хз.

kmatveev
31.08.2026 11:11Я не сильно понял, что вы выиграли, отказавшись от наследования. С наследованием реализация была бы такой: у объекта есть указание на его класс, у класса таблица виртуальных функций. Вместо этого у вас каждый объект содержит поле-ссылку на родителя-делегата, по динамической памяти расход больше. Поэтому вот этот пассаж мне не ясен:
Компилятор берет всю тяжелую работу на себя и генерирует минимально необходимый RTTI (информация о типах во время выполнения), полностью упаковывая его в компактные метаданные во Flash-памяти. В ОЗУ под это не тратится ни одного байта.
Как это вы сделали, что у объекта нет информации о его классе?
Ещё хотел спросить: а что будет, если счётчик ссылок переполнится?

5277 Автор
31.08.2026 11:11Не очень удобно развернуто отвечать в комментариях - ответы урезаны, это может вносить недопонимание.
Я планирую написать несколько статей где смогу развернуто ответить на вопросы как формируются HEAP данные класса , что лежит в метаданных Flash и тому подобное.
Также планирую рассказать как я работаю со ссылками и почему циклические ссылки и дефрагментация памяти не так критична в моей модели.
Забегая вперед приведу заголовок класса(для Thread и Timer заголовок расширен) в HEAP:
CLASS_HEAP_OFFSET__SIZE = 0x0000;2B-total size
CLASS_HEAP_OFFSET__LINK_CNTR = 0x0002;1B-Link counter
СLASS_HEAP_OFFSET__METADATA_ADDR = 0x0003;2B-flash address
Мета-данные в flash:
CLASS_META_OFFSET__CLASS_ID = 0x0000;1B-ИД типа класса
CLASS_META_OFFSET__PAIRS_QNT = 0x0001;1B-Количество пар
CLASS_META_OFFSET__PAIRS_TABLE = 0x0002;xB-таблица пар (ID интерфейса + количество методов)
Этого достаточно для выполнения динамической диспетчеризации вызовов. При наследовании классов боюсь пришлось бы хранить более большие таблицы (и возможно даже использовать RAM), плюс поиск реализации стал бы более затратным. Также усложняется процесс определения мертвого кода.
Счетчик ссылок дорастает до значения 255 и блокируется - память освободиться не сможет. Крайне маловероятно что это произойдет на 8 битах. Но можно будет это учесть - сделать исключение или если компилятор докажет - расширять счетчик до 16 бит.

kmatveev
31.08.2026 11:11Да, в комментах не очень удобно форматировать текст. Спасибо за ответ. На основе этого я бы сказал, что та цитата из статьи, к которой я прицепился, содержит неверную информацию: на RTTI в ОЗУ тратится два байта.
Насчёт наследования классов - нифига, принципиальной разницы с наследованием интерфейсов нет.
Заодно новый вопрос: а вам сильно помогает хранить размер объекта в ОЗУ ? Он же одинаков для всех объектов одного класса, и его можно получать, обратившись по METADATA_ADDR. Я ещё могу понять массивы, но предполагаю, что длина массива у вас хранится явно и отдельно.

5277 Автор
31.08.2026 11:11"содержит неверную информацию" - я так упростил ответ, коряво вышло.
"Насчёт наследования классов - нифига, принципиальной разницы с наследованием интерфейсов нет." - не хочу сейчас об этом спорить, тем более я знаю единственный способ это доказать или опровергнуть - это реализовать. И может быть я к этому приду - так как такая доработка не поломает существующую модель. Сейчас меня вполне устраивает композиция, вроде-бы она сейчас даже в тренде.
"а вам сильно помогает хранить размер объекта в ОЗУ" - Это размер всего HEAP. У меня нет отдельного хранения размера блока выделенной памяти. В теории этот размер можно было бы хранить во flash, но я посчитал что там оптимальней.
Аналогично и массивы - есть нечто общее в заголовках. В нем есть размерность на базе которой вычисляется размер выделенной памяти.

BoriskovKB
31.08.2026 11:11Где вы видели динамическую память на 8 бит МК ?

5277 Автор
31.08.2026 11:11У себя. И оно работает.
У vm5277 есть конечно ограничения - запрет на оператор new на чипах менее 256 байт - таким образом я блокирую возможность использования HEAP и динамической памяти. Все что от 256 может иметь DRAM и HEAP
И да, самые простые программы я запускаю даже на attiny13a, например:



EvgeniyKo88
31.08.2026 11:11Извиняюсь за банальный интерес, но что означает total time на скрине? Время цикла или что то другое?

5277 Автор
31.08.2026 11:11Это вывод ассемблер сборщика. Т.е. время за которое avr ассемблер собрал из asm файла и его инклудов hex прошивку.
Вот пример 'hello' вывода работы компилятора (j8b->asm)

работы ассемблера (asm->hex)

И прошивальщика (hex->МК)

Конечно большие проекты будут собираться медленней. Но я еще вернусь к оптимизации процесса сборки - знаю что можно соптимизировать.
P.S. На первом скрине можно заметить, что total time заметно больше чем сумма работы компилятора - при сборке компилятор не показывает затраченное время на обработку автоматически подгружаемых библиотек из runtime. Но в total time это учитывается.

sami777
31.08.2026 11:11Помнится, сколько то лет назад здесь на хабре читал статью какого то чувака, который решил C# использовать для программирования под эмбендинг. Тоже как и в этой статье очень много было заделок на будущее и готовых идей. Если мне память не изменяет, была всего одна статья. C#, как и Java всегда шли плечом друг к другу. Так что дерзайте!

5277 Автор
31.08.2026 11:11Спасибо. К вопросу выгорания - я этот проект веду почти каждый день уже как полтора года. Давно бы уже выгорел если бы мог. Причина в том, что я интроверт, и выгораю от социума, а в подобных проектах я отдыхаю, наверное это как рыбалка.
Да, безусловно, он может остаться никому не известным. Но причина в этом будет не техническая и не архитектурная - потому, что уже (даже в Альфе) существующие мои доработки доказывают реальность архитектуры и ее преимущества - по сути она закрывает сегмент рынка где особых конкурентов просто нет. Вопрос только в сообществе - удастся ли мне набрать критическую массу этого сообщества.

aao_1965
31.08.2026 11:11кажется, что в статье нет ответа на главный вопрос - ЗАЧЕМ все это нужно. абстракция от железа и многопоточность на уровне языка? а.. зачем? если есть ртос? это все красивые идеи, но жизнеспособные только в стране розовых единорогов (имхо). не вижу необходимости в подобном подходе и инструментарии даже на 32бит гигагерцовых процессорах с десятками мб озу. а у вас речь про 8бит с десятками КИЛОБАЙТ. попробуйте переубедить меня

5277 Автор
31.08.2026 11:11Я не знаю чем Вы занимаетесь, чтобы Вас переубедить. Это инструмент - достоинства которого я описал. Можно еще почитать https://github.com/w5277c/vm5277/blob/main/FAQ.md для дополнительной информации и посетить сайт-визитку https://vm5277.ru/
На самом деле этот вопрос очень прост. У него ровно такие-же ответы как и на вопрос 'зачем писать на Java когда есть Си' - не думаю, что я смогу написать ответ лучше чем уже существующие ответы.
Лично от себя приведу пример:
У меня есть свой проект умного дома, со значительным набором своих устройств (датчики, управление, шлюзы и прочее).
Все прошивки реализованы на чистом ассемблере на базе моей RTOS (core5277). vm5277 - это следующий этап развития core5277 - так как ему не хватало функционала для легкого описания прикладной (бизнес) логики и не хватало переносимости.
Когда мы говорим об одной конкретной прошивке с простым функуионалом - да тратить силы на какое-то стороннее решение (да и еще от него зависеть) - это глупо.
Но как только у вас появляются задачи где нужно реализовать кучу бизнес логики а потом, вполне вероятно ситуация переноса прошивки на другие чипы - вот здесь удобство vm5277 сильно перевешивает нативный кодинг даже на Си.
Плюс, мой язык дает безопасность и легкую читабельность, которой нет в Си.
Я предлагаю инструмент который в будущем позволит Вам писать бизнес логику гораздо с меньшими трудозатратами, с легко читаемым синтаксисом, с существенно меньшим порогом вхождения в embedded, с большей безопасностью итогового кода и с легкой переносимостью на другие чипы. Если для Вас эти характеристики не существенны - то да, Вам мой проект не нужен. А вот бизнесу такой проект нужен, так как существенно сокращает цену разработки и позволяет нанимать менее дорогостоящих специалистов.

aao_1965
31.08.2026 11:11да, именно этот вопрос. зачем писать на java (которая мне нравится на РС) если есть С? каких средств языка вам не хватает в эмбеддед С? переносимость (что кстати спорно - С - весьма переносим) и низкий порог входа это справедливо при умалчиваемых вами допущениях, что низкоуровневый слой для данной платформы уже кем-то написан. а с этим "все очень сложно" даже во вселенной С с ее терабайтами исх текстов: _нормальных низкоуровневых библиотек днем с огнем поискать. и да, по указанным выше ссылкам я побродил ранее.
кстати почему вы ориентируетесь при написании низкого уровня на ассемблер? кажется, что это многих отпугнет сложностью самостоятельной доработки библиотек низкого уровня. почему не С? насколько я помню семейство АВР (из 8бит ников) как раз и разрабатывалось так, чтоб на нем максимально эффективно работали программы на С
aao_1965
31.08.2026 11:11а про "безопасность" С- кода отвечу бородатым анекдотом:
-доктор, помогите! когда я ВОТ ТАК делаю - мне больно.
-голубчик! а вы ТАК - не делайте!

5277 Автор
31.08.2026 11:11Судя по вашим аргументам, наше обсуждение вышло из технического русла и перешло в область идеологического фанатизма.
Ваши вопросы и тезисы («зачем всё это нужно», «перенубедите меня», «всё сложно») имеют исключительно холиварный характер. Вы требуете от меня универсальных ответов на субъективные вопросы, на которые embedded-сообщество спорит десятилетиями.
Дискутировать в таком ключе непродуктивно. Я не планирую тратить силы на то, чтобы переубеждать лично вас или доказывать очевидную ценность HAL, переносимости кода и безопасности памяти для бизнеса. Все технические ответы, исходный код Альфы и примеры работы на реальном железе (включая WS2812B и экраны) уже выложены в моем репозитории. Кому интересно - тот изучает и пробует. Всего вам доброго
AnthonyDS
Классно, нужно будет попробовать)