Пишем на 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 помощью паттернов

ссылка на оригинал статьи http://habrahabr.ru/post/274571/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *