Java Magazine 徽标
最初发表于 Java Magazine 2014 年 3/4 月刊。立即订阅

如何在几分钟内成为嵌入式开发人员

作者:Angela Caicedo

使用嵌入式 Java 编程快速入门。

在作为 Java 宣讲师的 10 年里,从未有一个 Java 版本令我如此激动。Java 8 不仅语言本身是一个非常棒的版本,拥有很酷的新特性,而且它还在嵌入式方面表现非常棒,有大量优化,且体积非常小。如果您是 Java 开发人员,并准备与我一起加入新的一波机器到机器技术(说得更好听些,是下一个大编程项目),那么,让我们从物联网 (IoT) 开始吧。

开始嵌入式编程之前,您需要准确了解您计划构建的是什么,您要在哪里运行您的应用?这很关键,因为有各种不同风格的嵌入式 Java,将会完美适合您的需要。

如果您要构建与桌面上运行的应用类似的应用,或者想要精美的 UI,您需要看看源自 Java SE 的 Oracle Java SE Embedded。Java SE 支持的平台和功能,它都支持。此外,它还提供了特定特性并支持其他平台,其 Java 运行时环境 (JRE) 占用空间小,支持无头配置,并且优化了内存。

Oracle Java SE Embedded 支持的平台和功能与 Java SE 一样。

另一方面,如果您要寻找一种可以轻松连接各种外围设备(如开关、传感器、电机等)的办法,Oracle Java ME Embedded 是您最好的选择。它拥有的 Device Access API 定义了针对嵌入式平台中一些最常见外围设备的 API:通用输入/输出 (GPIO)、内部集成电路 (I2C) 总线、串行外围接口 (SPI) 总线、模拟数字转换器 (ADC)、数字模拟转换器 (DAC)、通用异步接收器/发射器 (UART)、内存映射输入/输出 (MMIO)、AT 命令设备、监视定时器、脉冲计数器、脉冲宽度调制 (PWM) 发生器和通用设备。

Oracle Java SE Embedded 中没有(至少墓前还没有)Device Access API,因此,如果您仍然希望使用 Oracle Java SE Embedded 并且还要使用外围设备,就必须依赖外部 API,如 Pi4J

在设备方面,嵌入式 Java 所覆盖范围极其广泛,从传统的 Java SE 桌面和服务器平台,到 STMicroelectronics STM32F4DISCOVERY 板、Raspberry Pi 和 Windows。在本文中,我将使用 Raspberry Pi,不仅因为它是一个非常强大的信用卡大小的单板计算机,而且还因为它非常实惠。最新型号仅需 35 美元。

准备好 Raspberry Pi

为了引导,Raspberry Pi 要求安全数字 (SD) 内存卡中有 Linux 映像。这种计算机没有硬盘驱动器。而是用一个 SD 卡存储 Linux 映像,以供开机时计算机运行所用。此 SD 内存卡还充当卡上加载的其他应用的存储。

要配置 SD 卡,请执行以下步骤:

  1. 格式化 SD 卡。
  2. 下载 Raspbian,这是一个基于 Debian 的免费操作系统,专门针对 Raspberry Pi 硬件进行了优化。
  3. 创建一个可引导映像。您可以使用 Win32 Disk Imager 等应用轻松创建映像。

准备好 SD 卡之后,即可打开 Raspberry Pi。首次引导 Raspberry Pi 时,它将带您进入 Raspberry Pi Software Configuration Tool 执行一些基本配置。下面是您应执行的其他任务:

  1. 选择 Expand Filesystem 选项,确保所有 SD 卡存储对操作系统可用。
  2. 选择 Internationalisation 选项,按照您所在的位置设置语言和区域。
  3. 允许通过安全 Shell (SSH) 访问,将 Raspberry Pi 设置为无头(即不连接监视器)嵌入式设备。为此,请选择主菜单中的 Advanced 选项。
  4. 设置一个静态 IP 地址,确保 Raspberry Pi 始终具有相同的 IP 地址。虽然这不是必需的,但我发现这在以无头方式使用 Raspberry Pi 时非常有用。要设置静态 IP 地址,请编辑 /etc/network/interfaces 文件。图 1 显示了一个这种文件的示例。

    ntj-embedded-f1

    图 1

现在已准备好连接 Raspberry Pi 了。一个选择是使用 PuTTY图 2 显示了如何连接的示例。

ntj-embedded-f2

图 2

在 Raspberry Pi 上安装嵌入式 Java

现在,您该决定要在设备上运行何种应用。我个人喜欢利用外围设备做些有趣的事情,因此本文中我将使用 Oracle Java ME Embedded,这样我就可以利用 Device Access API。但请记住,您还可以在 Raspberry Pi 上运行 Oracle Java SE Embedded。

在 Raspberry Pi 上安装 Oracle Java ME Embedded 二进制文件非常简单。只需使用 FTP 通过 SSH 连接将 Raspberry Pi 分发 zip 文件从桌面传输到 Raspberry Pi。然后将文件解压缩到新目录中,就大功告成了。

整合

创建嵌入式应用的一个很好的选择是结合使用 NetBeans IDE 与 Java ME SDK。这二者结合使用,您甚至可以在设备上运行应用之前使用仿真器进行测试。您将能够自动传输代码并在 Raspberry Pi 上执行,甚至可以动态调试代码。您只需确保 Java ME SDK 是 IDE 上的 Java 平台的一部分。您需要选择 Tools->Java Platforms,单击 Add Platform,然后指定包含 SDK 的目录,即可启用 NetBeans IDE 中的 SDK。

为了远程管理 Raspberry Pi 上的嵌入式应用,您需要运行 Application Management System (AMS)。只需通过 SSH 执行以下命令:

pi@raspberrypi sudo 
javame8ea/bin/usertest.sh

您的第一个嵌入式应用

Oracle Java ME Embedded 应用看起来与其他 Java ME 应用完全一样。清单 1 显示了您能够拥有的最简单的示例。

public class Midlet extends MIDlet {

    @Override
    public void startApp() {
        System.out.println("Started...");
    }

    @Override
    public void destroyApp(boolean unconditional) {
        System.out.println("Destroyed...");
    }
}

清单 1

应用必须继承自 MIDlet 类,且应重写两个生命周期方法:startAppdestroyApp。当应用启动时和即将销毁之前,将调用这两个方法。清单 1 中的代码只是在设备控制台上打印一条文本消息。

开灯!

下面做点儿更有趣的事,比如,按开关点亮和熄灭 LED。首先,我们来看看 Raspberry Pi 上的 GPIO 引脚(参见图 3)。

ntj-embedded-f3


图 3

GPIO 连接器上有一些不同类型的连接:

  • GPIO 引脚
  • I2C 引脚
  • SPI 引脚
  • 串行 Rx 和 Tx 引脚

这意味着到底在哪里连接 LED 和开关有多种选择;任何 GPIO 引脚都可以。只需记下每个设备的引脚号和 ID,因为在代码中引用每个设备时,您将需要此信息。

如果您是 Java 开发人员,并准备加入新的一波机器到机器技术,那么,让我们从物联网 (IoT) 开始吧。

我们来做一些基本的焊接,创建一个如图 4 所示的电路。注意,我们将 LED 连接到引脚 16 (GPIO 23),将开关连接到引脚 11 (GPIO 17)。添加了几个电阻,以确保电压在所需范围内。

ntj-embedded-f4

图 4

现在,我们来看看程序。在 Device Access API 中,有一个名为 PeripheralManager 的类,允许您使用外设 ID 连接到任何外设(无论是什么),大大简化了编码。例如,要连接到 LED,只需使用静态方法 open 并提供引脚 ID 23,如清单 2 所示。完成!

private static final int LED1_ID = 23;
...
GPIOPin led1 = (GPIOPin) PeripheralManager.open(LED_ID);

清单 2

要更改 LED 的值(打开或关闭),请使用 setValue 方法和所需的值:

// Turn the LED on 
 led1.setValue(true);

实在是简单至极。

要连接开关,我们可以对 PeripheralManager 使用相同的 open 方法,但因为我们想设置一些配置信息,因此将使用一个稍微不同的方法。首先,创建一个 GPIOPinConfig 对象(参见清单 3),包含如下信息:

private static final int Button_Pin = 17;
 ...
 GPIOPinConfig config1 = new GPIOPinConfig("BUTTON 1", 
                      Button_Pin, 
                      GPIOPinConfig.DIR_INPUT_ONLY,
                      PeripheralConfig.DEFAULT,  
                      GPIOPinConfig.TRIGGER_RISING_EDGE, 
                      false);

清单 3 

  • 设备名称
  • 引脚号
  • 方向:输入、输出或双向
  • 模式:上拉、下拉、推拉或漏极开路
  • 触发:无、下降沿、上升沿、双边沿、高电平、低电平或双电平
  • 初始值

然后,我们使用此配置对象调用 open 方法,如清单 4 所示。

GPIOPin button1 = (GPIOPin) PeripheralManager.open(config1);

清单 4

我们还可以向引脚添加监听器,这样就可以在每次引脚值变化时得到通知。在本例中,我们希望在开关值更改时得到通知,因此可以相应地设置 LED 值:

button1.setInputListener(this);

然后,实现发生事件时将调用的 value Changed 方法,如清单 5 所示。

@Override
public void valueChanged(PinEvent event) {
    GPIOPin pin = (GPIOPin) event.getPeripheral();
    try {
      if (pin == button1) {
          // Toggle the value of the led
          led1.setValue(!led1.getValue()); 
      }
    }catch (IOException ex) {
          System.out.println("IOException: " + ex); 
      }
}

清单 5

完成后关闭引脚并确保关闭 LED 也很重要。(参见清单 6)。

public void stop() throws IOException {
   if (led1 != null) {
       led1.setValue(false);
       led1.close();
   }
   if (button1 != null) {
       button1.close();
   }
}

清单 6

这里提供整个类。

现在,只缺调用代码的主 MIDlet 了。清单 7 所示的 startApp 方法将创建一个对象来控制这两个 GPIO 设备(LED 和开关)并监听输入,stopApp 方法将确保一切正常关闭(停止)。

public class Midlet extends MIDlet{

 private MyFirstGPIO gpioTest;

 @Override
 public void startApp() {
   gpioTest = new MyFirstGPIO();
   try {
     gpioTest.start();
   } catch (PeripheralTypeNotSupportedException | 
            PeripheralNotFoundException|
            PeripheralConfigInvalidException | 
            PeripheralExistsException ex) {
      System.out.println("GPIO error:"+ex.getMessage());
   } catch (IOException ex) {
      System.out.println("IOException: " + ex);
   }
 }

 @Override
 public void destroyApp(boolean unconditional) {
   try {
      gpioTest.stop();
   } catch (IOException ex) {
       System.out.println("IOException: " + ex);
     }
   }
 }
}

清单 7

感知环境

LED 和开关不错,但真正有趣的是开始感知周围环境时。在下面的示例中,我想展示如何开始运用采用 I2C 协议的传感器。

I2C 设备可能是应用最广泛的设备了,它最大的优点是设计简单。I2C 设备只使用两个双向漏极开路线:串行数据线 (SDA) 和串行时钟线 (SCL)。

总线上的设备将具有一个特定的地址。主控制器在 SDA 线上发出一个启动请求,接着是设备地址,由此建立与总线上各组件的通信。如果具有该地址的设备就绪,就会响应一个确认请求。然后在 SDA 线上发送数据,使用 SCL 线控制每个数据位的定时。

与一个设备的通信完成后,主控制器发送停止请求。这个简单协议允许一个双线总线上有多个 I2C 设备。

在 Raspberry Pi 上使用 I2C

如果您再看 Raspberry Pi 引脚(参见图 3),您将看到有两个引脚用于 I2C:引脚 3 是数据总线 (SDA),引脚 5 是时钟 (SCL)。默认情况下,I2C 未启用,所以我们需要执行几个步骤使其可用于本应用。

首先,使用一个终端连接 Raspberry Pi,然后在 /etc/modules 文件中添加下面几行代码:

i2c-bcm2708
i2c-dev

安装 i2c-tools 软件包很有用,这些工具将方便检测设备和确保一切正常工作。可以使用以下命令安装软件包:

sudo apt-get install 
python-smbus
sudo apt-get install i2c-tools

最后,有一个名为 /etc/modprobe.d/raspi-blacklist.conf 的黑名单文件;默认情况下,SPI 和 I2C 都在这个黑名单上。这意味着,只有删除或注释掉这些行,I2C 和 SPI 才能在您的 Raspberry Pi 上工作。编辑该文件,删除以下几行代码:

blacklist spi-bcm2708
blacklist i2c-bcm2708

重新启动 Raspberry Pi,确保应用所有更改。

添加传感器

Bosch Sensortec 的 BMP180 板是测量气压和温度的低成本传感解决方案,。因为压力随海拔变化,也可将其用作高度计。它使用 I2C 协议,电压范围 3V 到 5V,非常适合连接到我们的 Raspberry Pi。

我们返回焊接步骤,按照图 5 所示示意图,将 BMP180 板连接到 Raspberry Pi。正常情况下,使用 I2C 设备时,SDA 和 SCL 线需一个上拉电阻。幸运的是,Raspberry Pi 提供了上拉电阻器,因此只需简单连接即可。

ntj-embedded-f5
图 5

将板连接到 Raspberry Pi 之后,即可检查是否可见 I2C 设备。在 Raspberry Pi 上运行以下命令:

sudo i2cdetect -y 1 

您应能够在表中看到您的设备。图 6 显示两个 I2C 设备:一个在地址 40,一个在地址 70。

ntj-embedded-f6

图 6

使用 I2C 设备获取温度

以编程方式连接到 I2C 设备之前,有几件事需要了解:

  • 设备的地址是什么?I2C 有 7 位用于设备地址,Raspberry Pi 使用 I2C 总线 1。
  • 寄存器地址是什么?
  • 在本例中,我们将读取温度值,该寄存器位于地址 0xF6。(这是 BMP180 板所特有的。)
  • 是否需要配置任何控制寄存器来启动感知?默认情况下,有些设备处于睡眠模式,这意味着在唤醒它们之前,它们不会感知任何数据。本设备的控制寄存器地址 0xF4。(这是 BMP180 板所特有的。)
  • 设备的时钟频率是多少?在本例中,BMP180 板使用 3.4 Mhz。

清单 8 将 BMP180 值定义为静态变量,以便稍后在代码中使用。

 //Raspberry Pi's I2C bus
    private static final int i2cBus = 1; 
    // Device address 
    private static final int address = 0x77; 
    // 3.4MHz Max clock
    private static final int serialClock = 3400000; 
    // Device address size in bits
    private static final int addressSizeBits = 7; 
    ...
    
    // Temperature Control Register Data
    private static final byte controlRegister = (byte) 0xF4; 
    // Temperature read address
    private static final byte tempAddr = (byte) 0xF6; 
    // Read temperature command
    private static final byte getTempCmd = (byte) 0x2E; 
    
    ...
    // Device object
    private I2CDevice bmp180;

清单 8

更多信息


 开始在 Keil 板上使用 Oracle Java ME Embedded 3.3

同样,使用 PeripheralManager 的静态方法 open 以编程方式连接到设备。在本例中,我们将提供 I2C 设备特有的 I2CDeviceConfig 对象(参见清单 9)。I2CDeviceConfig 对象允许指定设备的总线、地址、地址大小(以位为单位)和时钟速度。

    ...        
    //Setting up configuration details
    I2CDeviceConfig config = new I2CDeviceConfig(i2cBus,  
                      address, addressSizeBits, serialClock);
    //Opening a connection the I2C device
    bmp180 = (I2CDevice) PeripheralManager.open(config);
    ...

清单 9

要读取温度,我们需要执行以下三个步骤:

  1. 从设备读取校准数据,如清单 10a10b 所示。这是 BMP180 板所特有的,使用其他温度传感器时,您可能不需要执行此步骤。

     

       // EEPROM registers - these represent calibration data
        private short AC1;
        private short AC2;
        private short AC3;
        private int AC4;
        private int AC5;
        private int AC6;
        private short B1;
        private short B2;
        private short MB;
        private short MC;
        private short MD;
        private static final int CALIB_BYTES = 22;
    
        ...        
        // Read all of the calibration data into a byte array
           ByteBuffer calibData=
                           ByteBuffer.allocateDirect(CALIB_BYTES);
            int result = bmp180.read(EEPROM_start, 
                                 subAddressSize, calibData);
            if (result < CALIB_BYTES) {
                System.out.format("Error: %n bytes read/n", 
                                  result);
                return;
            }
            // Read each of the pairs of data as a signed short
            calibData.rewind();
            AC1 = calibData.getShort();
            AC2 = calibData.getShort();
            AC3 = calibData.getShort();
    

    清单 10a

     // Unsigned short values
            byte[] data = new byte[2];
            calibData.get(data);
            AC4 = (((data[0] << 8) & 0xFF00) + (data[1] & 0xFF));
            calibData.get(data);
            AC5 = (((data[0] << 8) & 0xFF00) + (data[1] & 0xFF));
            calibData.get(data);
            AC6 = (((data[0] << 8) & 0xFF00) + (data[1] & 0xFF));
    
            // Signed sort values
            B1 = calibData.getShort();
            B2 = calibData.getShort();
            MB = calibData.getShort();
            MC = calibData.getShort();
            MD = calibData.getShort();
    

    清单 10b

  2. 写入设备上的控制寄存器,启动温度测量(参见清单 11)。

     

    // Write the read temperature command to the command register
     ByteBuffer command = ByteBuffer.allocateDirect
                                  (subAddressSize).put(getTempCmd);
     command.rewind();
     bmp180.write(controlRegister, subAddressSize, command);
    

    清单 11

  3. 以双字节字形式读取未补偿温度,并使用校准常数确定真实温度,如清单 12 所示。(同样,这是此传感器所特有的。)

     

    ByteBuffer uncompTemp = ByteBuffer.allocateDirect(2);
     int result = bmp180.read(tempAddr, subAddressSize, 
                              uncompTemp);
     if (result < 2) {
        System.out.format("Error: %n bytes read/n", result);
        return 0;
     }
    
     // Get the uncompensated temperature as an unsigned two byte   
     // word
     uncompTemp.rewind();
     byte[] data = new byte[2];
     uncompTemp.get(data);
     UT = ((data[0] << 8) & 0xFF00) + (data[1] & 0xFF);
    
     // Calculate the actual temperature
     // This is device specific again!
     int X1 = ((UT - AC6) * AC5) >> 15;
     int X2 = (MC << 11) / (X1 + MD);
     B5 = X1 + X2;
     // Temperature in celsius
     float celsius = (float) ((B5 + 8) >> 4) / 10;
    

    清单 12

最后,摄氏温度将存储在 celsius 变量中。这里提供完整的程序。

作为练习,您可以将程序扩展为读取压力、高度或二者同时读取。

总结

本文通过介绍如何使用 GPIO 和 I2C 设备的真实示例,引导您完成开始创建嵌入式 Java 应用所需的步骤。现在轮到您发现要连接到 Raspberry Pi 的更多设备,在 Raspberry Pi 上开心试用嵌入式 Java。


angela-headshot

Angela Caicedo 是 Oracle 技术宣讲师。她擅长的领域包括 Java ME、Java SE 和 Java EE。她喜欢涉猎各种新酷技术,如游戏开发、3-D、蓝牙和智能微尘,并曾出席许多开发人员大会。她毕业于哥伦比亚麦德林 EAFIT 大学。