over 1 year ago

Translated from "Interacting with Sensors on Intel’s x86 Galileo Board" by Gastón Hillar, Java Magazine September/October 2016, page 14-23. Copyright Oracle Corporation.

在 Intel x86 Galileo 開發板上與感應器互動

捕捉資料與反應是 IoT 的核心,本文說明如何用 Java 與不貴的 x86 處理器開發板做到

在本篇文章中,我開發並解釋一個量測環境光源與調節紅、綠與藍 LED 作為反應的專案,透過這方式,您可以看到如何使用 Java SE 8 來和類比輸入及脈衝寬度調變 (pulse width modulation, PWM) 輸出互動,我使用最新的 Intel IoT 套件,它包含對 Java 的支援,我善用 upm 及 mraa 兩個函式庫的優點:提供高階的介面控制 Intel Galileo Gen 2,相容 Arduino 的 x86 Pentium 架構處理器,我也會說明如何控制連接的電子元件、感測器 (sensor) 與驅動器 (actuator)。

必要條件

Intel Galileo Gen 2 開發板用 Yocto Poky Linux 映像檔開機,它是 Intel Galileo 最新版的 microSD 記憶卡 Linux 作業系統映像檔,您可以在 Intel 官網下載最新版的 Yocto Poky 映像檔。

為了開發,我使用 Intel System Studio IoT Edition 軟體,它是基於 Eclipse 的 IDE,讓建立以 Java 為主語言的 IoT 專案變簡單,我同時使用最新版的 mraa 與 upm 函式庫,mraa 是 Linux 平台上用來溝通的低階基礎函式庫,而 upm 是與感應器及驅動器互動的函式庫集,兩者皆包含在 Intel IoT 開發套件中,您可以在 Intel IoT 官網下載 Intel System Studio IoT Edition。

剛開始使用 Java 開發 IoT 專案時,您可能會遇到的問題:包含在 Intel System Studio IoT Edition 中的 upm 和 mraa 版本和 Galileo Gen 2 開發板上的 Yocto Poky Linux 所安裝的函式庫版本必須一致。我在使用 IDE 的同步功能時遇到蠻多問題,因此,我建議分成兩步驟:首先更新 IDE 的函式庫,然後再更新開發板上的函式庫,選擇 IDE 主選單上的 Intel(R) IoT | Libraries Update...,然後按指示逐步安裝函式庫的最新版本。

以 microSD 記憶卡 Linux 作業系統映像檔完成開機後,用乙太網路孔連上您的區域網路,DHCP 伺服器將會配發一個 IP 位址給開發板,您稍後將會需要這個 IP 位址,您有幾種方式可以取得配發的 IP 位址,例如,假設您進到 DHCP 伺服器網頁介面,您可以得到一個 IP 位置配發給一個裝置,它的 MAC 位址和開發板上標籤印的一樣。

您同樣可以透過零設定網路 (zero-coniguration networking) 的實作在區域網路中找到開發板及其服務,在 Windows 上,您可以使用免費的 Bonjour Browser for Windows,在 OS X 上,您可以使用免費的 Bonjour Browser for OS X。這兩個應用都能顯示可供使用的 Bonjour 服務,您只需要注意名稱為 galileo 的服務,名為 galileo 的 SSH 服務其 IP 位址將是您能用來建立連線到開發板的 IP 位址。

然後,用您喜好的 SSH 終端機工具,使用剛剛取得的 IP 位址、root 帳號及空白密碼,登入到 Intel Galileo Gen 2 開發板上的 Yocto Linux,接著在 SSH 終端機中執行下列指令:

opkg update
opkg install mraa
opkg install upm

接著,在 IDE 中點擊 Create an IoT Project,然後選擇 Java Project,我將使用 AmbientLightAndLed 作為專案名稱,輸入連線名稱與您的 Intel Galileo Gen 2 開發板 IP 位置,這樣,IDE 將提供您一個上傳專案到指定的開發板並執行的環境,在輸出畫面上,您將會看到專案執行時的輸出結果,且您能在終端機上執行指令。不過,一如往常,當您知道事情如何運作後,您會發現客製化自己的腳本,執行指令上傳編譯後的 JAR 檔並執行它會更簡單。當您開始以 Java 使用 mraa 及 upm 函式庫時,IDE 提供的整合除錯體驗會非常有用。

連接電子元件

光敏電阻 (photoresistor) 是一個電子元件,又稱為光電組 (light-dependent resistor, LDR) 或光導管 (photocell),它不是能以高精準度量測環境光源的最佳元件,但這元件在判斷環境是亮是暗很有用,且在這個例子中,延遲性不是個問題 (延遲亮度調節的反應),如果您要在更複雜的專案中量測環境光源,確保您有考慮到精準度與延遲性。

光敏電阻是一種可變電阻,電阻值會隨著環境光源改變,當環境光源強度增加,光敏電阻的電阻值下降,反之亦然。透過類比輸入接腳,開發板可以讀取電壓值,我將光敏電阻作為電壓分配電路 (voltage divider coniguration) 兩個電阻中的一個,當光敏電阻接收到大量的光,電壓分配電路輸出高電壓,當光敏電阻接收到少量的光或完全沒有光,則輸出低電壓。

Intel Galileo Gen 2 開發板允許您使用六個數位 I/O 接腳作為 PWM 輸出腳,六個接腳以 tilde (~) 符號標示,作為開發板上編號的前綴。我使用標示為 A0 的類比接腳連接到包含光敏電阻的電壓分配電路的正極。此外,我用下列 PWM 接腳控制 RGB LED 的亮度:

  • 接腳 ~6 連接到控制 RGB LED 紅色成分的陽極
  • 接腳 ~5 連接到控制 RGB LED 綠色成分的陽極
  • 接腳 ~3 連接到控制 RGB LED 藍色成分的陽極

您需要下列零件以完成整個範例:

  • 一個光敏電阻
  • 一個允許 5% 誤差的 10,000Ω (10 kΩ) 電阻,色碼為棕、黑、橘、金。
  • 一個共陰極的 5mm RGB LED
  • 三個允許 5% 誤差的 270Ω 電阻,色碼為紅、紫、棕、金。

Figure 1 電子元件連接在麵包板上並與 Intel Galileo Gen 2 開發板連接

Figure 1 呈現剛剛描述的電子元件連接在麵包板上,以及從 Intel Galileo Gen 2 開發板到麵包板的必要連線 (我用廣受歡迎的 Fritzing multiplatform application 建立原始圖,這檔案和本文的程式碼可在 Java Magazine 的下載區取得)。

有三個能作為 PWM 的通用 I/O (GPIO, general-purpose input/output) 接腳:~6~5~3,每一個都接上一個 270Ω 的電阻並接在 LED 每個顏色的陽極接腳,共陰極則是接地 (GND),標示為 A0 的類比輸入接腳接到由光敏電阻與允許 5% 誤差的 10 kΩ 電阻所組成的電壓分配電路,光敏電阻接到 IOREF 接腳,我使用開發板的預設組態,因此 IOREF 是 5V,10 kΩ 則是接地 (GND)。

寫程式用 PWM 調節 LED 顏色

當您完成所有必要的連線,您需要寫 Java 程式判斷是否在暗的環境,及根據環境光源的強弱控制 RGB LED 三個顏色的亮度,程式讀取由電阻值轉換的電壓值,然後將此類比數值轉成數位形式,程式將數位值轉成電壓,然後將電壓轉成量測值。

為了讓程式簡單容易理解,接下來我解釋的 Java 類別不會為每個運算檢查結果,但在範例的最終版本,您應該檢查每次呼叫 mraa 的結果,確保回傳值等於 mraa.Result.SUCCESS

我將建立以下三個類別:

  • VariableBrightnessLed 代表連接到開發板的 LED,允許我控制其亮度。
  • VoltageInput 代表連接到開發板上的類比輸入接腳的電壓源,允許我將從類比輸入讀取的原始值對應成電壓值。
  • SimpleLightSensor 代表一個光感應器,允許我將從 VoltageInput 實體讀取到的電壓值轉成光量測值與描述。

接著,我將建立一個 BoardManager 類別,負責建立 VariableBrightnessLedSimpleLightSensor 類別的實體並與之互動,最後建立一個主類別 AmbientLightAndLed 協調前述類別的運作。

首先,我建立一個新的 VariableBrightnessLed 類別代表連接到開發板的 LED,可以有 0 到 255 階的亮度,接下來的程式碼呈現我所有類別會用到的 imports 及此類別的程式:

import mraa.Aio;
import mraa.Pwm;

class VariableBrightnessLed {

    private final int gpioPin;
    private final String name;
    private final Pwm pwm;
    private int brightnessLevel;

    public VariableBrightnessLed(int gpioPin, String name) {
        this.name = name;
        this.gpioPin = gpioPin;
        this.pwm = new Pwm(gpioPin);
        this.pwm.period_us(700);
        this.pwm.enable(true);
        // Set the initial brightness level to 0

        this.setBrightnessLevel(0);
    }

    public void setBrightnessLevel(int brightnessLevel) {
        int validBrightnessLevel = brightnessLevel;
        if (validBrightnessLevel > 255) {
            validBrightnessLevel = 255;
        } else if (validBrightnessLevel < 0){
            validBrightnessLevel = 0;
        }
        float convertedLevel = validBrightnessLevel / 255f;
        this.pwm.write(convertedLevel);
        this.brightnessLevel = validBrightnessLevel;
        System.out.format("%s LED connected to PWM Pin #%d set to brightness level %d.%n", this.name, this.gpioPin, validBrightnessLevel);
    }

    public int getBrightnessLevel() {
        return this.brightnessLevel;
    }

    public int getGpioPin() {
        return this.gpioPin;
    }
}

當我建立一個 VariableBrightnessLed 實體時,需要用 int pioPin 參數指定連接到 LED 的 GPIO 接腳編號,及用 String name 參數指定 LED 的名稱,建構子以 gpioPin 作為參數建立一個新的 mraa.Pwm 實體,並保存其參考在 pwm 欄位,接著呼叫其 period_us 函數設定 PWM 週期為 700 微秒 (700 μs),這樣輸出的工作週期 (duty cycle) 將決定在 700 μs 的週期中訊號為 on 狀態的比例。例如,百分之十 (0.10) 的輸出工作週期指在 700 μs 的週期中,訊號為 on 的狀態為 70 μs。

接著,建構子以 true 為參數呼叫 pwm.enable 設定 PWM 接腳為開啟的狀態,並允許程式開始用 pwm.write 函式設定 PWM 接腳的輸出工作週期比例。

最後,建構子以 0 作為 brightnessLevel 參數呼叫 setBrightnessLevel 函式,如此會將 LED 亮度設為 0,也就是將指定接腳的 LED 關閉,具體來說,這呼叫關閉 LED 特定的顏色。

此類別宣告一個 setBrightnessLevel 函數,將 0 到 255 的亮度值轉換成給 PWM 接腳相應的輸出工作週期,這函式接收 brightnessLevel 參數作為亮度值。

首先,程式確定亮度值在 0 到 255 (含)之間,如果值超過範圍,程式會以下限或上限作為 validBrightnessLevel 區域變數的值。

然後,計算給 PWM 接腳必要的輸出工作週期比例,以 1.0f (100 %) 到 0.0f (0 %) 的區間代表亮度,將合法的亮度值 (validBrightnessLevel) 除以 255f,結果保存在 convertedLevel 變數,下一行呼叫 this.pwm.write 函式將 convertedLevel 作為百分比參數,將連接的 PWM 輸出工作週期設定為 convertedLevel

此類別宣告一個 setBrightnessLevel 函數,將 0 到 255 的亮度值轉換成給 PWM 接腳相應的輸出工作週期。

最後,程式合法的亮度值存在 brightnessLevel 欄位,只能以 getBrightnessLevel 函式讀取,最後一行將 LED 的亮度值與用來辨識的名稱及接腳編號等細節,用 System.out.format 函式印在畫面上,您若用 IDE 執行產生的 JAR 檔或是透過 SSH 在開發板上執行的 Yocto Linux 中以指令執行程式,就可能看到輸出結果,我稍後會詳述列印有用資訊在畫面上的好處。

透過類比輸入量測環境光源

現在,我建立一個新的 VoltageInput 類別,代表連接在開發板上類比輸入接腳的電壓源,下面為該類別的程式碼:

class VoltageInput {

    private final int analogPin;
    private final Aio aio;

    public VoltageInput(int analogPin) {
        this.analogPin = analogPin;
        this.aio = new Aio(analogPin);
        // Configure the ADC

        // (short for Analog-to-Digital Converter)

        // resolution to 12 bits (0 to 4095)

        this.aio.setBit(12);
    }

    public float getVoltage() {
        long rawValue = this.aio.read();
        float voltageValue = rawValue / 4095f * 5f;
        return voltageValue;
    }

    public int getAnalogPin() {
        return analogPin;
    }
}

建立 VoltageInput 類別實體時,需要以 int analogPin 參數指定電壓源所連接的類比輸入接腳編號,建構子將收到的類比輸入接腳編號保存在 analogPin 唯讀欄位,只能以 getAnalogPin 函式讀取,建構子以 analogPin 為參數建立建立 mraa.Aio 實體,保存在 aio 欄位,然後呼叫其 setBit 函式,設定類比轉數位 (analog-to-digital converter, ADC) 的解析度為 12 bits,如此,ADC 將提供 4,096 (212 = 4096) 個數值來表達 0V 到 5V 的範圍,0 代表 0V,4096 代表 5V。

這需要線性函數將從類比輸入接腳讀取的原始值轉乘對應的輸入電壓值,因此,程式將原始值乘上 5 再除以 4095 已取得輸入電壓值,由於使用 12 bits 的解析度,偵測值的刻度將是 5V / 4095 = 0.001220012V,大概是 1.22 mV。

VoltageInput 類別宣告一個 getVoltage 函式,它呼叫 this.aio.read 函式,從類比輸入接腳讀取原始值,保存在 rawValue 區域變數中,這變數方便除錯與理解程式運作原理,然後將 rawValue 除以 4,095 再乘上 5,然後保存在 voltageValue 變數,最後,此函式回傳 voltageValue 變數的值,如此,此函式能從 this.aio.read 函式讀取的原始值並轉換成電壓值回傳。

現在有一個類別能讓我從電壓源取得電壓值,現在,我將建立一個新的 SimpleLightSensor 類別代表包含在電壓分配電路中且與開發板類比輸入接腳連接的光敏電阻,這新類別用剛完成的 VoltageInput 讀取並轉換類比輸入,允許我將電壓值轉換成光源量測值與描述,下面為新類別的程式碼:

class SimpleLightSensor {

    // Light-level descriptions

    public static final String LL_EXTREMELY_DARK = "Extremely dark";
    public static final String LL_VERY_DARK = "Very dark";
    public static final String LL_JUST_DARK = "Just dark";
    public static final String LL_SUNNY_DAY = "Like a sunny day";

    // Maximum voltages that determine the light level

    private static final float EXTREMELY_DARK = 2.1f;
    private static final float VERY_DARK = 3.1f;
    private static final float JUST_DARK = 4.05f;

    private final VoltageInput voltageInput;
    private float measuredVoltage = 0f;
    private String lightLevel = SimpleLightSensor.LL_SUNNY_DAY;

    public SimpleLightSensor(int analogPin) {
        this.voltageInput = new VoltageInput(analogPin);
        this.measureLight();
    }

    public void measureLight() {
        this.measuredVoltage = this.voltageInput.getVoltage();
        if (this.measuredVoltage < SimpleLightSensor.EXTREMELY_DARK) {
            this.lightLevel = SimpleLightSensor.LL_EXTREMELY_DARK;
        } else if (this.measuredVoltage < SimpleLightSensor.VERY_DARK) {
            this.lightLevel = SimpleLightSensor.LL_VERY_DARK;
        } else if (this.measuredVoltage < SimpleLightSensor.JUST_DARK) {
            this.lightLevel = SimpleLightSensor.LL_JUST_DARK;
        } else {
            this.lightLevel = SimpleLightSensor.LL_SUNNY_DAY;
        }
    }

    public String getLightLevel() {
        return this.lightLevel;
    }
}

這類別定義三個常數,指定每個亮度等級的最大電壓值:

  • EXTREMELY_DARK: 2.1V
  • VERY_DARK: 3.1V
  • JUST_DARK: 4.05V

如果量測到的電壓值低於 2.1V,則環境是極度暗,若低於 3V,環境是非常暗,如果低於 4V,環境是有點暗,這些值適用某些特定的光敏電阻,您也許需要確認您的配置在特定環境的電壓值,只需要調整此類別的常數值。

SimpleLightSensor 類別的主要目的是將定量的值 (電壓值) 轉成定性的值 (環境光源描述),這類別宣告下列常數描述亮度等級:

  • LL_EXTREMELY_DARK
  • LL_VERY_DARK
  • LL_JUST_DARK
  • LL_SUNNY_DAY

當我建立 SimpleLightSensor 類別實體時,需要指定 analogPin 參數,也就是包含光敏電阻的電壓分配電路所連接的類比輸入接腳編號,建構子用 analogPin 參數建立 VoltageInput 實體,並保存在 voltageInput 欄位,下一行程式呼叫 measureLight 函式,將從 VoltageInput 實體 (this.voltageInput) 取得的電壓值轉換成亮度等級的描述。

這新類別允許我將電壓值轉換成光源量測值與描述。

這類別的 measureLight 函式保存從 this.voltageInput.getVoltage 函式取得的電壓值在 measuredVoltage欄位,然後下一行用剛解釋過的常數決定 measuredVoltage 是哪個亮度等級,將根據量測得到的電壓值設定合適的lightLevel,然後可以呼叫 getLightLevel 函式取得亮度等級的描述。

SimpleLightSensor 類別的主要目的是將定量的值 (電壓值) 轉成定性的值 (環境光源描述),這類別宣告下列常數描述亮度等級

透過 Board Manager 控制一個輸入與三個輸出

現在我將建一個新的 BoardManager 類別,它建立一個 SimpleLightSensor 實體與三個 VariableBrightnessLed 實體,如此,RGB LED 的每個顏色對應到一個 VariableBrightnessLed 實體,當環境光源改變這類別會觸發動作,明確來說,這類別根據量測的環境光源調整 RGB LED 三個顏色的亮度,下面為新類別的程式碼:

class BoardManager {

    public final SimpleLightSensor lightSensor;
    public final VariableBrightnessLed redLed;
    public final VariableBrightnessLed greenLed;
    public final VariableBrightnessLed blueLed;

    public BoardManager() {
        this.lightSensor = new SimpleLightSensor(0);
        this.redLed = new VariableBrightnessLed(6, "Red");
        this.greenLed = new VariableBrightnessLed(5, "Green");
        this.blueLed = new VariableBrightnessLed(3, "Blue");
    }

    public void setRGBBrightnessLevel(int value) {
        this.redLed.setBrightnessLevel(value);
        this.greenLed.setBrightnessLevel(value);
        this.blueLed.setBrightnessLevel(value);
    }

    public void updateLedsBasedOnLight() {
        String lightLevel = this.lightSensor.getLightLevel();
        switch (lightLevel) {
            case SimpleLightSensor.LL_EXTREMELY_DARK:
                this.setRGBBrightnessLevel(255);
                break;
            case SimpleLightSensor.LL_VERY_DARK:
                this.setRGBBrightnessLevel(128);
                break;
            case SimpleLightSensor.LL_JUST_DARK:
                this.setRGBBrightnessLevel(64);
                break;
            default:
                this.setRGBBrightnessLevel(0);
                break;
        }
    }
}

BoardManager 宣告三個欄位並初始化:

  • lightSensorSimpleLightSensor 的實體,代表連接到類比輸入接腳編號 A0 包含在電壓分配電路中的光敏電阻。
  • redLedVariableBrightnessLed 的實體,代表連接到 GPIO 接腳編號 ~6 的 RGB LED 紅色成分。
  • greenLedVariableBrightnessLed 的實體,代表連接到 GPIO 接腳編號 ~5 的 RGB LED 綠色成分。
  • blueLedVariableBrightnessLed 的實體,代表連接到 GPIO 接腳編號 ~3 的 RGB LED 藍色成分。

setRGBBrightnessLevel 函式以接收的值作為參數呼叫三個 VariableBrightnessLed 實體的 setBrightnessLevel 函式,如此,透過一次呼叫,RGB LED 的三個顏色都設成相同的亮度。

updateLedsBasedOnLight 函式從 SimpleLightSensor 實體取得亮度等級的描述,然後根據量測的光源,呼叫剛解釋的 setRGBBrightnessLevel 函式設定 RGB LED 三個顏色的亮度,如果環境極度地暗,亮度設為 255,若環境是非常暗,亮度值設為 128,若環境是有點暗,亮度值設為 64,其他情況,亮度值設為 0,也就是關閉 RGB LED。

現在,我將寫程式使用 BoardManager 類別量測環境光源,並根據環境光源設定 RGB LED 三個顏色的亮度值,下面為新類別 AmbientLightAndLed 的程式碼:

public class AmbientLightAndLed {

    public static void main(String[] args) {
        String lastlightLevel = "";
        BoardManager board = new BoardManager();
        while (true) {
            board.lightSensor.measureLight();
            String newLightLevel = board.lightSensor.getLightLevel();
            if (newLightLevel != lastlightLevel) {
                // The measured light level has changed

                lastlightLevel = newLightLevel;
                System.out.format("Measured light level: %s%n", newLightLevel);
                board.updateLedsBasedOnLight();
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.err.format("Sleep interruption: %s", e.toString());
            }
        }
    }
}

這類別宣告 main 函式,當我上傳程式到開發板上並啟動時,會執行這函式,首先,這函式將 lastLightLevel 變數初始化為空字串,建立名為 boardBoardManager 實體,然後執行無窮迴圈。

此迴圈呼叫 board.lightSensor.measureLight 函式以更新環境光源的量測結果,下一行將呼叫 board.lightSensor.getLightLevel 所取得的亮度等級描述保存在 newLightLevel 區域變數中,如果新的亮度等級和最後紀錄的環境光源亮度等級不同,程式更新 lastLightLevel 變數的值,列印量測到的亮度等級,然後呼叫 board.updateLedsBasedOnLight 函式。

您能執行這範例,然後用手電筒或您的智慧型手機作為光源在光敏電阻上移動,您應該可以看到列印的訊息,並看到 RGB 變暗,最後關閉,在您減少環境的光源後,RGB LED 亮度增加。

Figure 2 終端機訊息

Figure 2 是在 IDE 的終端機視窗輸出的例子,終端機視窗顯示所有我印的訊息,讓我容易了解元件發生什麼發事。

結論

這簡單的例子說明您可以利用 Java 8 的優點,用高階的程式碼在 Intel Galileo Gen 2 開發板上與 IoT 元件互動,mraa 與 upm 函式庫會定期更新,將它們與 Java 結合,能讓您善用既有知識,在進行與不同輸入、輸出、感測器、顯示器與驅動器互動的 IoT 專案變容易。

Learn More
Yocto Poky Linux image from the Yocto Project
Intel System Studio IoT Edition User Guide for Java

譯者的告白
這篇文章真的很長,作者不但描述每個類別的目的,也接近以逐行說明的方式說明程式碼,也因此重複的部分蠻多的,不過整體來說很有意思,當初轉職就是想玩相關的東西,結果公司被併購後,變成沒機會玩了,只能說命運捉弄人... 不過命運這種事,長遠來看誰知道會怎樣呢?

會選擇這篇文章翻譯,除了自己想玩,還想起一件有趣的往事,想當初大三時一門課的作業是要寫個類嵌入式系統上的程式,那時我用 Java 寫個類似聊天室的東西,不過授課老師問為什麼用 Java 時,我那時說未來不能跑 Java 的裝置都該淘汰,如果以現在來看,手機、機上盒、手錶都能執行 Java (Android),甚至連 IoT 裝置都能執行 Java,當時說的話也許真的成真了,不過,現在想想,如果那時的產品執行 Java,效能及電池續航力的表現肯定奇差無比,選用任何語言或技術,最新的技術不一定適合當時的硬體,還是要斟酌很多面向才行。

路人旁白:因為本文很長,所以譯者的告白也變長了嗎? (笑)

← 欣賞程式語言有限的選擇 介面本質上的進化 →