В статье расскажу как можно писать на 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 памяти!
Алгоритм работы 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 помощью паттернов
Автор: TimReset