Hello dear readers. In this article I will try to demonstrate how to run a Java Virtual Machine and Java applications on microcontrollers. This idea may sound quite outlandish by itself: why use Java on microcontrollers where each byte of RAM and each CPU cycle are precious commodity? There's nothing like native C for microcontrollers — even C++ is rarely used! Yes, I've heard that discussion (and took part in it) for many years. Well, I will try to explain «why», as well as «how», in this article. So, anyone who wants to understand how MCU implementation of JVM works; see an unusual approach to multi-platform project development; take part in Open Source project; or just have fun — welcome onboard, uJVM is ready to take off! People who are ready to criticize can stay near the runway and watch us climb (or crash) from a safe distance
Intro
As a starting point, please watch the video below.
As far as I understand, this video describes something that consists of OS, JVM and libraries. It can be run on a variety of microcontroller boards (you can find and watch other videos published by the same company). However, this solution is not open-source, and I wanted to make some experiments, so I started to look for alternatives. I've chosen uJVM — source code is available, build instructions are good, and anyone who wants to participate in development is welcome.
In this article I will describe overall architecture, byte code loading process, environment setup and getting first results: compiled JVM and a traditional HelloWorld.java application that we will run on x86_64/Linux platform. In subsequent articles we will see how to run uJVM on various microcontroller boards, port uJVM to new platforms, add new Java classes and native classes. So let's start.
uJVM
uJVM is designed in accordance to overall architecture requirements described in The Java Virtual Machine Specification Java SE 7 Edition and consists of 3 parts:
- Class loader uJVM is responsible for loading classes stored in .class or .jar files into RAM, linking and initialization.
- Managed memory areas uJVM supports threads, storing of special registers, and memory allocation for stack, heap and thread-specific storage.
- Execution engine. Due to stringent RAM limitations, uJVM offers only bytecode interpreter as an execution engine — there are no JIT- or AOT-compilers. Engine uJVM can access Java source code, H/W drivers (e.g., UART, GPIO and SysTick for most of supported platforms) and native methods.
As of now uJVM is available for the following H/W platforms:
uJVM can be ported as an application for a variety of RTOSes, for example:
— however, we will not discuss use of uJVM under RTOSes in this article.
uJVM is an open source implementation of JVM for embedded systems with stringent resource limitations. In a minimal configuration, it can run in about 6 kB of RAM and requires less than 45 kB of ROM for storage. Build system allows the developers to configure uJVM at a build time, disabling unneeded features to conserve resources or enabling support of specific H/W devices (for example, FPU support to accelerate calculations). Let's see how to build and run it…
Setting up the environment and downloading the source code
1. Installing the necessary Linux packages
To build uJVM from source code, I use Ubuntu 16.04, but there are no strict Linux version requirements. Packages listed below should be installed:
$ sudo apt-get install git gcc make openjdk-8-jdk-headless gperf flex bison libncurses5-dev texinfo g++ curl pkg-config autoconf libtool libtool-bin libc6:i386 libc6-dev:i386 gcc-multilib doxygen doxygen-gui
2. Specifying Java compiler location (value of JAVA_HOME
variable)
Please note that Java compiler (javac) directory must be known to build system. To achieve that, you need (according to Oracle Java documentation) set the value of environment variable specifying the compiler location. A simple way to set this variable is to use shell commands:
$ export JAVA_HOME = _java_compiler_directory_
Here _java_compiler_directory_
must not include last bin
directory in the path; e.g., if compiler is located in /usr/bin
directory, JAVA_HOME
value must be /usr
. If you want to assign JAVA_HOME
value permanently for every user in the system, you can add this variable into /etc/environment
file by using the following command:
$ echo "JAVA_HOME="/usr"" | sudo tee -a /etc/environment
Same effect can be achieved by adding JAVA_HOME
variable into /etc/profile
file by using the following command:
$ echo "export JAVA_HOME="/usr"" | sudo tee -a /etc/profile
After executing either of these commands, you need to log out of shell and log in again.
3. Building kconfig-frontends
for Linux (optional)
Building kconfig-frontends
for Linux is a simple and straightforward process using autotools
. Usually you just need to download the source archive, unpack it and start build:
$ ./configure && make && sudo make install
3.1 Downloading and unpacking the source code archive
$ curl -O http://ymorin.is-a-geek.org/download/kconfig-frontends/kconfig-frontends-3.12.0.0.tar.xz
$ tar -xf kconfig-frontends-3.12.0.0.tar.xz
$ cd kconfig-frontends-3.12.0.0
3.2 Patching
If your system uses gperf 3.0.4
or earlier, just go to part 3.3. Otherwise, read on.
gperf 3.1
(released on January 5 2017) changed the type used for specifying data length in generated functions — instead of unsigned int
, it started to use size_t
. This causes the build to fail with a following message:
CC libkconfig_parser_la-yconf.lo
In file included from yconf.c:234:0:
hconf.gperf:141:1: error: conflicting types for 'kconf_id_lookup'
hconf.gperf:12:31: note: previous declaration of 'kconf_id_lookup' was here
static const struct kconf_id *kconf_id_lookup(register const char *str, register unsigned int len);
^~~~~~~~~~~~~~~
make[3]: *** [Makefile:456: libkconfig_parser_la-yconf.lo] Error 1
make[2]: *** [Makefile:350: all] Error 2
make[1]: *** [Makefile:334: all-recursive] Error 1
make: *** [Makefile:385: all-recursive] Error 1
To fix this issue, execute the following commands:
$ curl -O https://gist.githubusercontent.com/KamilSzczygiel/d16a5d88075939578f7bd8fadd0907aa/raw/1928495cfb6a6141365d545a23d66203222d28c0/kconfig-frontends.patch
$ patch -p1 -i kconfig-frontends.patch
$ autoreconf -fi
3.3 Configuring kconfig-frontends
Recommended configuration for kconfig-frontends
is:
$ ./configure --enable-conf --enable-mconf --disable-shared --enable-static
3.4 Compiling and installing kconfig-frontends
$ make
$ sudo make install
$ sudo strip /usr/local/bin/kconfig-*
4. Cloning the uJVM repository:
$ git clone https://github.com/Samsung/uJVM.git
Compiling the uJVM
Select PLATFORM x86_64_linux
. After that, create default build context by executing following command:
$ cd uJVM/
$ make PLATFORM=x86_64_linux create_context
Include HelloWorld.java into build. To do that, open configuration util:
$ make menuconfig
Go to Java application Configuration menu
and select «Hello world example»
This will include HelloWorld.java
into build sequence:
/**
* @file japps/hello_world/HelloWorld.java
* @brief Example how to use String and SysLog classes
*
* @copyright Copyright (c) 2018 Samsung Electronics Co., Ltd. All Rights Reserved.
* @author Taras Drozdovskyi t.drozdovsky@samsung.com
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import ujvm.lang.*;
public class HelloWorld {
public static void main() {
SysLog.log("Hello world!n");
String hello = "Hello world!n";
SysLog.log(hello);
byte[] bytes = { 'H', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '!', 'n' };
String str = new String(bytes);
SysLog.log(str);
}
}
Don't let the SysLog.log(str)
confuse you — it's simply a low-level analog of System.out.println(str)
. Standard Java library is not fully implemented in uJVMyet, but I hope someone will do that someday.
Save and compile:
$ make
And, finally, run the application:
$ make run
Other applications listed in build system can be built and run in the same fashion.
Next article will cover building and running uJVM on microcontroller boards. However, if you're impatient to try, just read the documentation — it covers the usual sequence of operations pretty well.
Links:
The Journal of Open Source Software — uJVM: Lightweight Java Virtual Machine for embedded systems;
uJVM Documentation.
Автор: TarasDrozdovsky