如何分分钟成为Java嵌入式开发人员
SteWhiteleg
8年前
<p><strong>用Java开发下一代嵌入式产品</strong></p> <p>在我10年的Java布道师生涯里,没有哪次Java新版本发布能让我如此兴奋。Java 8的发布不仅在语言本身加入了些不错的新特性,还在嵌入式开发上加入了很棒的功能,进行了优化,还有简洁的开发文档。如果你是一名Java程序员,并且准备好和我一同加入机器间技术的潮流,或者说开发下一代改变世界的设备,那么就让我们开始学习物联网(IoT)把。</p> <p>在你开始嵌入式开发之前,你需要知道你具体想要开发出什么,以及你打算在哪运行你的程序。这十分重要,因为得根据目的选择不同版本的Embedded Java。</p> <p> </p> <p>如果你想要开发跟桌面应用相似的应用,或者你想要开发出优美的UI,那么你需要选择从Java SE衍生出来的Oracle Java SE Embedded版本。它支持同Java SE一样的平台和功能。此外,它还提供了其它特性,兼容更多平台,小巧的Java运行环境(JREs),支持headless模式配置,以及内存优化。</p> <p>如果你想要更方便地连接如开关、传感器、马达之类的外设,Oracle Java ME Embedded将是你最好的选择。它具有设备访问API,为嵌入式平台最常见的外设提供了接口:通用输入/输出(GPIO)、集成电路总线(IIC)、串行外设接口总线(SPI)、模数转换器(ADC)、数模转换器(DAC)、通用异步收发传输器(UART)、内存映射输入输出(MMIO)、AT命令设备、看门狗定时器、脉冲计数器、脉冲宽度调制器(PWM)和通用设备。</p> <p>至于设备,Embedded Java覆盖了大部分的平台,从传统的Java SE桌面平台与服务器平台到基于STM微处理器的STM32F4DISCOVERY板、树莓派和windows平台。在这篇文章中,我将使用树莓派,不仅仅是因为它是十分强大,且只有卡片大小的计算机,还因为它价格便宜。最新版只要35美元。</p> <p><strong><strong>准备树莓派</strong></strong></p> <p>树莓派需要一张存有Linux镜像的SD卡才能开机。因为树莓派没有硬盘,SD卡就被用来存储运行所需的Linux镜像。该SD卡也被当作存储设备用于加载其它的应用程序。</p> <p>配置SD卡请按以下步骤操作:</p> <ol> <li>格式化SD卡。</li> <li>下载Raspbian(一个专为树莓派优化的基于Debian的免费操作系统)。</li> <li>创建一个可引导的镜像。使用像Win32 Disk Imager这样的应用可以更方便地创建镜像。</li> </ol> <p>当你准备好SD卡之后,树莓派就可以开机了。第一次开机时,树莓派会加载软件配置工具让你进行基本的设置。以下是此时应该注意的选项:</p> <ol> <li>勾选“扩展文件系统(Expand Filesystem)”选项,使操作系统对整个SD存储具有权限。</li> <li>选择“国际化(Internationalisation)”选项中选择与当地对应的语言与区域。</li> <li>在主菜单选择“高级(Advanced)”选项,通过开启SSH将树莓派设置为headless嵌入式设备模式(没有显示器)。</li> <li>设置静态IP地址,确保树莓派总以相同的IP地址接入。虽然这不是必须的,但我发现在树莓派headless模式下总是很有用。设置静态IP需要编辑/etc/network/interfaces文件如下图:</li> </ol> <p><img alt="图1" src="https://simg.open-open.com/show/3718385eb02e4e78127c1b6cc7229b91.jpg"><br> (图1)</p> <p>现在你已经准备好了连接树莓派,你可以选择使用[PuTTY](http://www.putty.org/)连接。如下图:</p> <p><img alt="图2" src="https://simg.open-open.com/show/4b9329d1b93c6535c3eee0dba87fda60.jpg"><br> (图2)</p> <p><strong><strong>在树莓派上安装Embedded Java</strong></strong></p> <p>现在是时候决定你打算在你的设备上运行什么样的应用了。我个人喜欢搞外设,所以在这篇文章中我将使用Oracle Java ME Embedded,这样我才能使用设备访问API。但是你也可以用Oracle Java SE Embedded来开发树莓派应用。</p> <p>在树莓派上安装Oracle Java ME Embedded二进制文件十分简单,只需要通过SSH连接用FTP协议把树莓派版本的zip压缩文件从桌面传输到树莓派,然后再解压到一个新目录就好了。</p> <p><strong><strong>集成开发环境</strong></strong></p> <p>使用Java ME SDK和NetBeans IDE是创建嵌入式应用不错的选择。这两者结合就能在设备上运行之前先在虚拟机中进行测试,并且能够自动地将代码传输到树莓派运行,甚至能在运行时调试。你所需要做的只是确保Java ME SDK是IDE的Java平台的一部分。你需要在 工具->Java平台 点击“添加平台”的选项,然后选择SDK的路径。</p> <p>为了能够远程管理树莓派上的嵌入式应用,你需要运行应用管理系统(AMS)。通过SSH,运行以下代码:</p> <p>pi@raspberrypi sudo<br> javame8ea/bin/usertest.sh</p> <p><strong><strong>第一个嵌入式应用</strong></strong></p> <p>Oracle Java ME Embedded应用与Java ME应用看起来完全一样,就如下例:</p> <pre> <code>public class Midlet extends MIDlet { @Override public void startApp() { System.out.println(“Started…”); } @Override public void destroyApp(boolean unconditional) { System.out.println(“Destroyed…”); } }</code></pre> <p><br> (代码1)</p> <p>你的应用必须继承MIDlet类,并且重写两个生命周期方法:startApp和destroyApp。这两个方法分别在应用启动时和快结束前被调用。以上代码能在控制台输出信息。</p> <p><strong><strong>打开LED灯</strong></strong></p> <p>现在让我们做些更有趣的事,比如通过开关来实现开启和关闭LED灯。首先让我们看下树莓派的通用外设输入输出(GPIO)管脚。</p> <p><img alt="图3" src="https://simg.open-open.com/show/f0dc70cb5e82aca67450d8ea62aa8fb3.jpg"></p> <p>(图3)</p> <p>通用外设输入输出连接器(GPIO connector)上有许多不同的连接类型管脚:</p> <p>– 通用外设输入输出管脚(GPIO)</p> <p>– 集成电路总线管脚(IIC)</p> <p>– 串行外设接口管脚(SPI)</p> <p>– RxTx串口管脚</p> <p>这意味着我们有好几个选择可以连接LED和开关,以上任何一个GPIO管脚都可以,只要记住管脚数字和外设ID,因为你需要这些信息才能用代码指向这些设备。</p> <p>现在按照下图焊接电路。注意我们将LED连接到16管脚(GPIO 23),把开关连接到11管脚(GPIO 17)。同时加上 两个电阻以保证电压在安全范围之内。</p> <p><img alt="图4" src="https://simg.open-open.com/show/d0a70d262ff444220299218b002d4c07.jpg"></p> <p>(图4)</p> <p>现在让我们看下程序。设备访问API中的PeripheralManager类能够让你用外设ID连接到任何类型的外设,这能够极大地简化代码。比如要连接LED,只需要用静态方法open,提供管脚ID 23如下代码:</p> <p> </p> <pre> <code>private static final int LED1_ID = 23; ... GPIOPin led1 = (GPIOPin) PeripheralManager.open(LED_ID);</code></pre> <p>(代码2)</p> <p>要改变LED的值(即开关函数)只要用setValue方法传入相应参数:</p> <p><code>//打开LED<br> led1.setValue(true);</code></p> <p>这实在不能再简单了。</p> <p>我们能用PeripheralManager中同样的open方法来连接开关,但我们将用稍微不同的方法来设置一些配置信息。首先,创建GPIOPinConfig对象(代码3),其中包含了如下信息:</p> <pre> <code>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);</code></pre> <p>(代码3)</p> <p>外设名称</p> <p>– 管脚号<br> – 传输方向:输入、输出还是双向<br> – 模式:上拉、下拉还是开漏<br> – 触发器:无触发、下降沿触发、上升沿触发还是双边沿触发,高电平触发、低电平触发还是双电平触发<br> – 初始值</p> <p>接着我们用该配置对象调用open方法,如下:</p> <pre> <code> GPIOPin button1 = (GPIOPin) PeripheralManager.open(config1);</code></pre> <p>(代码4)</p> <p>我们也可以给管脚添加监听器,这样管脚值一旦发生改变,我们就能够知道。在这个例子中,我们想要知道什么时候开关的值发生了改变,这样我们就能相应的改变LED的值:</p> <p><code>button1.setInputListener(this);</code><br> 然后实现valueChanged方法,当监听器事件发生时就调用该方法。</p> <pre> <code>@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); } }</code></pre> <p>(代码5)</p> <p>在结束时关闭管脚是十分重要的,同时还要保证关掉了LED。</p> <pre> <code>public void stop() throws IOException { if (led1 != null) { led1.setValue(false); led1.close(); } if (button1 != null) { button1.close(); } }</code></pre> <p><br> (代码6)</p> <p>整个类的代码可以在<a href="/misc/goto?guid=4959670969688735992" onclick="javascript:window.open('https://blogs.oracle.com/acaicedo/resource/demos/TestGPIO.zip'); return false;">链接</a>找到。</p> <p>现在,我们剩下的只有MIDlet来启用我们的代码了。代码7中的startApp方法会生成一个对象来控制我们的两个通用输入输出设备(LED和开关),并且监听我们的输入。stopApp方法则保证所有东西都被正确地关闭。</p> <pre> <code>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); } } } }</code></pre> <p><br> (代码7)</p> <p><strong>感知环境</strong></p> <p>做到LED和开关已经十分不错,但感知周围环境才是真正有意思的。在下面的例子中,我将演示如何着手使用IIC协议的传感器。</p> <p>IIC设备可能是最常见的设备,它们最大的有点是设计简单。IIC只有两条双向的开漏线:串行数据线(SDA)和串行时钟线(SCL)。</p> <p>总线上的设备都会有一个特殊的地址。主控制器通过在串行数据线上发出开始请求和设备地址建立通讯连接。如果对应地址的设备空闲,则返回请求。然后数据就在串行数据线上传输,用串行时钟线来控制每一比特的时间。</p> <p>一旦通讯结束,控制器就发出停止请求。这样的协议使得在两条总线上得以增加多个设备。</p> <p><strong><strong>启动树莓派的集成电路总线</strong></strong></p> <p>如果你查看树莓派的管脚图(图3),你会发现两个IIC管脚:管脚3是数据总线,管脚5是时钟总线。IIC默认未开启,所以我们需要采取以下步骤才能让我们的应用使用总线。</p> <p>首先,用终端连接树莓派,然后在/etc/modules文件增加一下两行:</p> <p><code>i2c-bcm2708<br> i2c-dev</code></p> <p>i2c-tools包十分有用,它能够检测设备,保证一切正常运转。可以通过以下命令安装:</p> <p> </p> <p><code>sudo apt-get install python-smbus<br> sudo apt-get install i2c-tools</code></p> <p>最后,树莓派中有个黑名单文件/etc/modprobe.d/raspi-blacklist.conf,默认情况下SPI和IIC都在该名单中。这意味着除非我们移除它们或者把他们设为注释,IIC和SPI在树莓派上是不能用的。编辑该文件去除以下两行:</p> <p><code>blacklist spi-bcm2708<br> blacklist i2c-bcm2708</code></p> <p>重启树莓派,确保应用所有的修改。</p> <p><strong><strong>添加传感器</strong></strong></p> <p>Bosch Sensortec的BMP180传感器是测量大气压和气温的经济解决方案。由于气压随着海拔高度改变,你也可以把它当作海拔高度测量仪。BMP180使用IIC协议,工作电压为3V到5V,十分适合连接到树莓派。</p> <p>按照以下的图5把BMP180焊接到树莓派上。通常情况下,使用IIC设备时需要需要在串行数据线和串行时钟线加上一个上拉电阻。幸运的是,树莓派支持上拉电阻,所以你只需要把它们连接在一起。 <img alt="图5" src="https://simg.open-open.com/show/2ed0b383039444623026cc933115d269.jpg"><br> (图5)</p> <p>在你把传感器连接到树莓派之后,就可以检查是否能看到IIC设备了。在树莓派上运行以下命令:</p> <p><code>sudo i2cdetect -y 1</code></p> <p>你应该能在表格中看到设备。图6中显示了两个IIC设备:一个在地址40,另一个在地址70。</p> <p><img alt="图6" src="https://simg.open-open.com/show/5914d8d41fdae043c938ceb788037dcf.jpg"></p> <p><strong>使用IIC设备来获取温度</strong><br> 在你编程连接IIC设备之前有一些必须知道的事项:</p> <ul> <li>设备地址是多少?IIC使用7位作为设备地址,树莓派使用IIC总线1。</li> <li>寄存器的地址是多少?在我们的例子中,我们将读取温度值,而相应寄存器地址是0xF6。(针对BMP180)</li> <li> 是否需要设置控制寄存器来启动传感器?某些设备默认处于睡眠状态,除非我们启动它,否则它是不会监测任何数据的。此处设备的控制寄存器地址是0xF4。(针对BMP180)</li> <li> 设备的时钟频率是多少?BMP180频率为3.4Mhz。</li> </ul> <p>代码8将BMP180的这些参数设置为静态变量供之后的代码使用:</p> <pre> <code>//Raspberry Pi's I2C bus private static final int i2cBus = 1; // 设备地址 private static final int address = 0x77; // 3.4MHz最大时钟频率 private static final int serialClock = 3400000; // 地址宽度 private static final int addressSizeBits = 7; ... // 温度传感器控制寄存器地址 private static final byte controlRegister = (byte) 0xF4; // 读取温度地址 private static final byte tempAddr = (byte) 0xF6; // 读取温度命令地址 private static final byte getTempCmd = (byte) 0x2E; … // 设备对象 private I2CDevice bmp180;</code></pre> <p><br> (代码8)<br> 编程连接设备依然是使用PeripheralManager类的静态方法open。该处我们将针对IIC设备创建一个I2CDeviceConfig对象(代码9)。该对象能让我们设定设备的总线,地址,地址位数(比特单位)和时钟速度。</p> <pre> <code> ... //设置配置信息 I2CDeviceConfig config = new I2CDeviceConfig(i2cBus, address, addressSizeBits, serialClock); //连接IIC设备 bmp180 = (I2CDevice) PeripheralManager.open(config); ...</code></pre> <p>(代码9)</p> <p>要读取温度,我们需要采取以下步骤:</p> <ol> <li>按代码10a和代码10b从设备读取校准数据。该步只针对BMP180传感器,使用其它温度传感器时不一定需要这一步。</li> </ol> <p> </p> <pre> <code>// EEPROM寄存器——校准数据 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; … // 读取所有的校准数据为一个byte型数组 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; } // 将每组数据读取为带符号short型 calibData.rewind(); AC1 = calibData.getShort(); AC2 = calibData.getShort(); AC3 = calibData.getShort();</code></pre> <p><br> (代码10a)</p> <pre> <code>//unsigned short型 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 short型 B1 = calibData.getShort(); B2 = calibData.getShort(); MB = calibData.getShort(); MC = calibData.getShort(); MD = calibData.getShort();</code></pre> <p>(代码10)</p> <p>2.写入到设备上的一个控制寄存器,初始化温度传感器(代码11)。</p> <p> </p> <p><code>// 将读取温度命令写入到寄存器</code><br> </p> <pre> <code>ByteBuffer command = ByteBuffer.allocateDirect (subAddressSize).put(getTempCmd); command.rewind(); bmp180.write(controlRegister, subAddressSize, command);</code></pre> <p>(代码11)</p> <p>3.读取未补偿温度为两个字节的变量,用校准常量得出真实的温度。代码如下(依然针对BMP180)</p> <pre> <code>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; } // 读取温度为无符号byte型 uncompTemp.rewind(); byte[] data = new byte[2]; uncompTemp.get(data); UT = ((data[0] << 8) & 0xFF00) + (data[1] & 0xFF); // 计算实际温度 // 代码只适用BMP180 int X1 = ((UT – AC6) * AC5) >> 15; int X2 = (MC << 11) / (X1 + MD); B5 = X1 + X2; // 摄氏度为单位的温度 float celsius = (float) ((B5 + 8) >> 4) / 10;</code></pre> <p>(代码12)</p> <p>最后,摄氏度为单位的温度数据就被保存在了celsius变量中。你可以在<a href="/misc/goto?guid=4959670969799861181" onclick="javascript:window.open('https://blogs.oracle.com/acaicedo/resource/demos/i2c.zip'); return false;">链接</a>找到整个程序。</p> <p>作为练习,你也可以把该程序扩展到读取压力、海拔或者两者。</p> <p><strong><strong>总结:</strong></strong></p> <p>我们通过演示如何使用GPIO和IIC设备的真实案例学习了如何创建Java嵌入式应用。现在是时候轮到你自己在树莓派上连接更多设备了,希望你喜欢树莓派嵌入式Java开发。</p> <p>来源: <a href="/misc/goto?guid=4959670969884159867">Angela Caicedo</a></p>