Пишем на Java в Arduino

в 21:19, , рубрики: arduino, AST, C, c++, haikuvm, java, lejos

Пишем на Java в Arduino - 1

В статье расскажу как можно писать на 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 скриптом который позволяет запускать верификацию и загрузку скетча.

Пример:
Пишем на Java в Arduino - 3
Компиляция скетча

Пишем на Java в Arduino - 4
Загрузка скетча

В качестве примера возьму программу преобразования ИК сигналов для колонок (там долгая история — колонки 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 помощью паттернов

Автор: TimReset

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js