Пишем на Java в Arduino



    В статье расскажу как можно писать на Java для Arduino.

    Почему Java? Если кратко — just for fun!

    Я Java программист и в свободное время играюсь с Arduino и хотелось перенести свои знания Java в мир микроконтроллеров и embedded устройств.

    На данный момент есть несколько возможностей запускать Java на embedded устройствах. В этой статье я рассмотрю их.

    Официальная JVM


    Первое — это официальная JVM для embedded:
    www.oracle.com/technetwork/java/embedded/embedded-se/overview/index.html
    habrahabr.ru/post/243549 Запускаем Java Runtime на 256KB оперативной памяти

    Тут практически настоящая JVM которая исполняет byte-code. Но есть большие минусы — это работает только для Raspberry Pi и Freescale K64F (может я что то упустил, если так — добавьте, пожалуйста в комментариях). Поддержка Raspberry Pi определённо хорошо, но это по сути компьютер, хоть и одноплатный. На нём можно и простую JVM запустить. Да и стоит он от 3 т.р. K64F — это уже dev board с Cortex M4 на борту. Но стоит тоже от 3 т.р. Что гораздо дороже распространённого Arduino Uno.

    JVM с компилированием byte кода


    Есть несколько VM которые позволяют запускать Java на микроконтроллерах — это LeJOS ( www.lejos.org ) и HaikuVM ( haiku-vm.sourceforge.net )
    LeJOS — позволяет запускать Java приложения на Lego MindStorm. HaikuVM — на микрокомпьютерах AVR. Сейчас LeJOS разделён на две части:
    — для последнего, EV3, используется настоящая JVM, от Oracle ( www.oracle.com/technetwork/java/embedded/downloads/javase/javaseemeddedev3-1982511.html ). О ней я сказать больше ничего не могу — просто JVM.
    — для предыдущих версий, NXJ и RCX, используется JVM на основе TinyVM ( tinyvm.sourceforge.net ). Вот о ней стоит рассказать подробнее.

    Т.к. в микроконтроллерах очень мало памяти (в Arduino Uno 28kB Flash и 2kB SRAM) то настоящую JVM, с которая бы интерпретировала class файлы, там не запустить. Но можно преобразовать byte code программы и скомпилировать его в native код, вырезав при этом всё не нужное, весь не используемый runtime. При компиляции теряется часть функциональных возможностей Java (например, reflection). Но программа будет работать!

    HaikuVM работает также — берёт Java код, компилирует его с JRE из LeJOS (альтернативная реализация некоторых стандартных классов — String, StringBuilder, Integer и т.п. — нужна для оптимизации) вместо JRE из оригинальной JVM (rt.jar в HotSpot), получившиеся class файлы преобразует в C++ код, добавляет runtime из HaikuVM (в нём поддержка потоков, GC, exception) и компилирует всё это с помощью avr-gcc. И таким образом удаётся запустить Java программу вплоть до ATMega8 c 8kB flash памяти!

    image
    Алгоритм работы HaikuVM. Картинка взята с сайта haiku-vm.sourceforge.net

    Пример преобразования кода

    Java код:
    public static void setup() {
      Serial.begin(57600);
      while (!Serial.isOpen()) {
      }
    }
    


    Byte code:
    public static setup()V
     L0
      LINENUMBER 140 L0
      GETSTATIC processing/hardware/arduino/cores/arduino/Arduino.Serial : Lprocessing/hardware/arduino/cores/arduino/HardwareSerial;
      LDC 57600
      INVOKEVIRTUAL processing/hardware/arduino/cores/arduino/HardwareSerial.begin (J)V
     L1
      LINENUMBER 141 L1
     FRAME SAME
      GETSTATIC processing/hardware/arduino/cores/arduino/Arduino.Serial : Lprocessing/hardware/arduino/cores/arduino/HardwareSerial;
      INVOKEVIRTUAL processing/hardware/arduino/cores/arduino/HardwareSerial.isOpen ()Z
      IFNE L2
      GOTO L1
     L2
      LINENUMBER 144 L2
     FRAME SAME
      RETURN
      MAXSTACK = 3
      MAXLOCALS = 0
    


    Сгенерированный C код:
    /**
    public static void setup()
    Code(max_stack = 3, max_locals = 0, code_length = 22)
    */
    #undef  JMETHOD
    #define JMETHOD ru_timreset_IrTest_setup_V
    const           ru_timreset_IrTest_setup_V_t JMETHOD PROGMEM ={
    0+(2)+3,    0,    0,    // MaxLocals+(lsp+pc)+MaxStack, purLocals, purParams
    
    OP_GETSTATIC_L,      SADR(processing_hardware_arduino_cores_arduino_Arduino_Serial), 
                                                                           // 0:    getstatic		processing.hardware.arduino.cores.arduino.Arduino.Serial Lprocessing/hardware/arduino/cores/arduino/HardwareSerial; (16)
    OP_LDC2_W_L,         CADR(Const0003),                                  // 3:    ldc2_w		57600 (35)
    OP_INVOKEVIRTUAL,    B(2), LB(MSG_begin__J_V),                         // 6:    invokevirtual	processing.hardware.arduino.cores.arduino.HardwareSerial.begin (J)V (37)
    OP_GETSTATIC_L,      SADR(processing_hardware_arduino_cores_arduino_Arduino_Serial), 
                                                                           // 9:    getstatic		processing.hardware.arduino.cores.arduino.Arduino.Serial Lprocessing/hardware/arduino/cores/arduino/HardwareSerial; (16)
    OP_INVOKEVIRTUAL,    B(0), LB(MSG_isOpen___Z),                         // 12:   invokevirtual	processing.hardware.arduino.cores.arduino.HardwareSerial.isOpen ()Z (38)
    OP_IFNE,             TARGET(21),                                       // 15:   ifne		#21
    OP_GOTO,             TARGET(9),                                        // 18:   goto		#9
    OP_RETURN,                                                             // 21:   return
    };
    


    Как видно из примера выше — HaikuVM практически один в один переносит byte code в C.

    Помимо поддержки Java, HaikuVM позволяет вызывать C функции напрямую — с помощью аннотаций NativeCppFunction/NativeCFunction и содержит методы по работе с памятью и прерываниями.

    В целом проект мне понравился — я даже попробовал перевести его на Gradle ( github.com/TimReset/HaikuVMGradle ), но так как HaikuVM содержит в себе довольно сложную логику в bat/sh файлах, полностью это сделать это пока не удалось.

    Но тут есть минусы — так как в микроконтроллерах памяти и частоты процессора мало, то, пусть даже небольшой, overhead в виде GC (хотя можно GC отключить, но это слабо помогает) и преобразования byte code в C вносит ощутимые задержки. Это выражается, например, в невозможности работать с Serial на больших частотах ( больше 57600 kb/s ) — данные начинают теряться. Поэтому я начал разрабатывать свой (с тестами и поддержкой библиотек) вариант запуска Java в Arduino.

    Преобразования Java кода в Wiring


    Что бы не было overhead в виде GC и native интерпретатора byte code можно преобразовывать Java код напрямую в Wiring (язык программирования в Arduino, тот же C++). Готовых реализаций я не нашёл, поэтому решил написать свою ( github.com/TimReset/arduino-java ), благо синтаксис Java на C очень похож. Для этого использовал анализ AST из Eclipse ( help.eclipse.org/mars/index.jsp?topic=%2Forg.eclipse.jdt.doc.isv%2Freference%2Fapi%2Forg%2Feclipse%2Fjdt%2Fcore%2Fdom%2FASTNode.html )

    Алгоритм преобразования

    Есть абстрактный класс с абстрактными методами loop() и setup() и со служебными константами и методами digitalRead(int), analogRead(int) и т.п. Абстрактные методы loop/setup нужны для обязательного переопределения. Служебные методы и константы должны эмулировать поведение Wiring — в скетчах для Arduino можно так обращаться к этим методам/константам.

    Скетч наследует этот базовый класс (я его назвал BaseArduino) и имплементирует методы setup и loop.

    Далее просто пишем логику. Можно создавать методы, использовать переменные. Для использования сторонних библиотек нужно создать stub классы на Java, которые бы содержали методы из этих библиотек и в коде использовать эти классы. Stub классы должны находится в пакете с названием библиотеки, которую эти классы реализуют. Сами библиотеки должны находиться в папке parser/src/main/c в папке с названием библиотеки. При компиляции уже Wring кода эти библиотеки будут использоваться.

    И наконец, преобразование Java класса происходит с помощью Visitor, наследника класса org.eclipse.jdt.internal.core.dom.NaiveASTFlattener ( www.cs.utep.edu/cheon/download/jml4c/javadocs/org/eclipse/jdt/internal/core/dom/NaiveASTFlattener.html ), в котором переопределены некоторые методы:
    boolean visit(VariableDeclarationStatement), boolean visit(FieldDeclaration), boolean visit(MethodDeclaration) — для отслеживания использования классов из библиотек и удаления всех модификаторов (final, модификаторы видимости и static). Возможно это излишне, но пока работает так.
    Так же заменяет создание объекта:
    decode_results results = new decode_results(); преобразует в decode_results results();

    boolean visit(MethodInvocation) — для отслеживания обращения к классам библиотек и при передаче их в методы передаёт ссылки на них (через &):
    irrecv.decode(results) преобразует в irrecv.decode(&results)

    Если тут будут знатоки C++, подскажите, так всегда нужно передавать объекты или есть какие-нибудь ещё варианты?

    6) Всё это обвёрнуто Gradle скриптом который позволяет запускать верификацию и загрузку скетча.

    Пример:

    Компиляция скетча


    Загрузка скетча

    В качестве примера возьму программу преобразования ИК сигналов для колонок (там долгая история — колонки Microlab Speakers Solo 6C с пультом, пульт через несколько месяцев перестал работать, оригинал не нашёл, пришлось заменить универсальным пультом, но он был большого размера, в итоге сделал преобразователь сигналов на Arduino из маленького пульта chipster.ru/catalog/arduino-and-modules/control-modules/2077.html в сигналы для колонок).

    Java код:
    public class IrReceiverLib extends BaseArduino {
    
        public static final long REMOTE_CONTROL_POWER = 0xFF906F;
        public static final long REMOTE_CONTROL_VOL_UP = 0xFFA857;
        public static final long REMOTE_CONTROL_VOL_DOWN = 0xFFE01F;
        public static final long REMOTE_CONTROL_REPEAT = 0xFFFFFFFF;
    
        public static final long SPEAKER_IR_POWER = 2155823295L;
        public static final long SPEAKER_IR_VOL_DOWN = 2155809015L;
        public static final long SPEAKER_IR_VOL_UP = 2155841655L;
        public static final long SPEAKER_IR_BASS_UP = 2155843695L;
        public static final long SPEAKER_IR_BASS_DOWN = 2155851855L;
        public static final long SPEAKER_IR_TONE_UP = 2155827375L;
        public static final long SPEAKER_IR_TONE_DOWN = 2155835535L;
        public static final long SPEAKER_IR_AUX_PC = 2155815135L;
        public static final long SPEAKER_IR_REPEAT = 4294967295L;
    
        public static final int IR_PIN = A0;
    
        public final IRrecv irrecv = new IRrecv(IR_PIN);
    
        public final IRsend irsend = new IRsend();
    
        long last_value = 0;
    
        @Override
        public void setup() {
            irrecv.enableIRIn();
        }
    
        @Override
        public void loop() {
            decode_results results = new decode_results();
            if (irrecv.decode(results) != 0) {
                final long value = results.value;
                if (value == REMOTE_CONTROL_POWER) {
                    last_value = SPEAKER_IR_POWER;
                    irsend.sendNEC(SPEAKER_IR_POWER, 32);
                    irrecv.enableIRIn();
                } else if (value == REMOTE_CONTROL_VOL_DOWN) {
                    last_value = SPEAKER_IR_VOL_DOWN;
                    irsend.sendNEC(SPEAKER_IR_VOL_DOWN, 32);
                    irrecv.enableIRIn();
                } else if (value == REMOTE_CONTROL_VOL_UP) {
                    last_value = SPEAKER_IR_VOL_UP;
                    irsend.sendNEC(SPEAKER_IR_VOL_UP, 32);
                    irrecv.enableIRIn();
                } else if (value == REMOTE_CONTROL_REPEAT) {
                    if (last_value != 0) {
                        irsend.sendNEC(last_value, 32);
                        irrecv.enableIRIn();
                    } else {
                    }
                } else {
                    last_value = 0;
                }
            }
        }
    
    }
    


    Преобразуется в этот код:
    #include <IRremote.h>
    public static long REMOTE_CONTROL_POWER=0xFF906F;
    public static long REMOTE_CONTROL_VOL_UP=0xFFA857;
    public static long REMOTE_CONTROL_VOL_DOWN=0xFFE01F;
    public static long REMOTE_CONTROL_REPEAT=0xFFFFFFFF;
    
    public static long SPEAKER_IR_POWER=2155823295L;
    public static long SPEAKER_IR_VOL_DOWN=2155809015L;
    public static long SPEAKER_IR_VOL_UP=2155841655L;
    public static long SPEAKER_IR_BASS_UP=2155843695L;
    public static long SPEAKER_IR_BASS_DOWN=2155851855L;
    public static long SPEAKER_IR_TONE_UP=2155827375L;
    public static long SPEAKER_IR_TONE_DOWN=2155835535L;
    public static long SPEAKER_IR_AUX_PC=2155815135L;
    public static long SPEAKER_IR_REPEAT=4294967295L;
    
    public static int IR_PIN=A0;
    
    IRrecv irrecv(IR_PIN);
    IRsend irsend;
    long last_value=0;
    
    void setup(){
      Serial.begin(256000);
      irrecv.enableIRIn();
    }
    
    void loop(){
      decode_results results;
      if (irrecv.decode(&results) != 0) {
      long value=results.value;
        if (value == REMOTE_CONTROL_POWER) {
          last_value=SPEAKER_IR_POWER;
          irsend.sendNEC(SPEAKER_IR_POWER,32);
          irrecv.enableIRIn();
        }
        else
        if (value == REMOTE_CONTROL_VOL_DOWN) {
          last_value=SPEAKER_IR_VOL_DOWN;
          irsend.sendNEC(SPEAKER_IR_VOL_DOWN,32);
          irrecv.enableIRIn();
        }
        else
        if (value == REMOTE_CONTROL_VOL_UP) {
          last_value=SPEAKER_IR_VOL_UP;
          irsend.sendNEC(SPEAKER_IR_VOL_UP,32);
          irrecv.enableIRIn();
        }
        else
        if (value == REMOTE_CONTROL_REPEAT) {
          if (last_value != 0) {
            irsend.sendNEC(last_value,32);
            irrecv.enableIRIn();
          }
          else {
          }
        }
        else {
          last_value=0;
        }
      }
    }
    


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

    И тест на преобразование сигналов:
    @RunWith(Parameterized.class)
    public class IRReceiverTest {
        @Parameterized.Parameters(name = "{index}: Type={0}")
        public static Iterable<Object[]> data() {
            return Arrays.asList(new Object[][]{
                    {"Power", IrReceiverLib.REMOTE_CONTROL_POWER, IrReceiverLib.SPEAKER_IR_POWER},
                    {"Vol down", IrReceiverLib.REMOTE_CONTROL_VOL_DOWN, IrReceiverLib.SPEAKER_IR_VOL_DOWN},
                    {"Vol up", IrReceiverLib.REMOTE_CONTROL_VOL_UP, IrReceiverLib.SPEAKER_IR_VOL_UP}
            });
        }
    
        private final long remoteSignal;
        private final long speakerSignal;
    
        public IRReceiverTest(String type, long remoteSignal, long speakerSignal) {
            this.remoteSignal = remoteSignal;
            this.speakerSignal = speakerSignal;
        }
    
        @Test
        public void test() {
            IrReceiverLib irReceiverLib = new IrReceiverLib();
            irReceiverLib.setup();        
            Assert.assertTrue(irReceiverLib.irrecv.isEnabled());
    
            irReceiverLib.irrecv.receive(remoteSignal);
            irReceiverLib.loop();
            Assert.assertEquals(speakerSignal, irReceiverLib.irsend.getLastSignal());
        }
    }
    


    Для теста я дописал методы в stub классы библиотеки IRremote, что бы можно было эмулировать приём и передачу сигнала. В тесте я инициализирую и передаю сигнал в скетч, далее проверяю, что отправленный из скетча сигнал соответствует ожидаемому.

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

    В общем, пока для себя я остановился на своём варианте преобразования Java в C.

    Ремарка по поводу преобразования Java кода на другие языки. Java код можно конвертировать в JS. Сейчас есть несколько рабочих вариантов: GWT ( www.gwtproject.org ) и TeaVM ( github.com/konsoletyper/teavm ). И они также используют два различных подхода — GWT преобразует исходный код в JS, TeaVM — байт код.

    Полезные ссылки


    Здесь описано, как работать Eclipse AST: habrahabr.ru/post/269129 Разбор Java программы с помощью java программы
    Преобразование Groovy кода в шейдеры: habrahabr.ru/post/269591 Отладка шейдеров на Java + Groovy
    Анализ AST: habrahabr.ru/post/270173 Анализ AST c помощью паттернов
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 12
    • 0
      Выглядит интересно! Как насчет jruby, groovy или scala? как оно будет конвертироваться в байткод?
      • 0
        Если удастся использовать runtime от LeJOS, то будет. Это скорее всего легко будет сделать в Groovy. Но на счёт jRuby и Scala не уверен — хоть они так же генерируют byte code для JVM, но я не знаю, не добавляют ли они туда какие-нибудь свои специфические классы и методы, которые бы HaikuVM не обработал или просто из за сгенерированного кода скомпилированный elf файл будет очень большой.
        • 0
          Я думаю, не потянет. Банально рантайм Groovy не влезет. Не говоря уже о том, что там, по-видимому, байткод-магии внутри хватает.
          • 0
            Сейчас погуглил — действительно, судя по этой статье www.javaworld.com/article/2073502/using-groovyc-to-compile-groovy-scripts.html там много доп. кода, не влезет в Arduino, если даже скомпилируется. И то не факт, что скомпилируется.
            Но зато из Groovy проще Wiring код генерировать :-)
      • +11
        На что люди только не идут, лишь бы не писать под АВР на си)
        • +2
          Есть настоящая vm: www.harbaum.org/till/nanovm/index.shtml
          Сама во flash памяти размещается, java байт код из eeprom исполняет
          • 0
            Да, видел её. Не стал указывать в статье, т.к. последнее обновление в 2006 году. В статье указал только «живые» версии. Так же HaikuVM обновилась пару недель назад.
          • 0
            А как у этих вещей дела обстоят с отладкой?
            • +1
              Всё просто — её нет :-) Вообще, теоретически можно дебажить сгенерированный из HaikuVM код — но там сильно много преобразований — Java -> byte code -> C -> elf и я себе слабо представляю, насколько это будет удобно отлаживать.
              С другой стороны, если генерировать Wiring код из Java (как я делал), то можно применить подход как VisualMicro ( www.visualmicro.com ) — этакие «программные» breakpoint. Работает это примерно так — перед запуском ставятся breakpoint, плагин для IDE при генерации Wiring кода в эти строки добавляет служебный код, который сообщает по Serial состояние программы в этой строке (значение переменных, может что то ещё), далее при запуске программа соединятся с плагином IDE и при переходе на эту строку срабатывает breakpoint. Ну а тогда в IDE можно посмотреть состояние переменных, при срабатывании этого breakpoint.
              Звучит конечно муторно, но что делать, если jtag не работает в Arduino? Ну насколько я знаю. :-) Вообще в самом Arduino проблемы с отладкой. Хотя может меня поправят?
              • 0
                Да, я совсем забыл, что в обычной Ардуино тоже отладки нет :)
            • +1
              Даже страшно представить, с какой скоростью ЭТО будет работать.
              Хотя вы сразу написали — just for fun
              • 0
                В HaikuV в 40 раз медленнее. И я с этим потом столкнулся. Поэтому сейчас просто генерирую Wiring из Java.

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