Convergence Lab.株式会社 CEOの木村 優志です。

今回は Zephyr RTOSを使って BLE通信をして、サーモグラフィーの画像をPCに送る方法について説明します。今、最も熱いRTOSである Zephryの魅力の一端を説明できれば幸いです。

このエントリーをはてなブックマークに追加
 

はじめに

Zephyr RTOSを用いて、BLE通信でサーモグラフィーの画像をPCに送る方法について説明します。

実際のところ、今回の例では、Zephyrの魅力を十分に引き出せているとは言えないのですが、入門用の参考とはなると思います。 Zephyr RTOSに関する記事は国内ではあまりないようですし。

技適未取得機器を利用するため、警告文を掲載しておきます。なお、弊社は 「技適未取得機器の実験等の特例制度」に基づく届出を済ませています。

この無線設備は、電波法に定める技術基準への適合が確認されておらず、
法に定める特別な条件の下でのみ使用が認められています。この条件に
違反して無線設備を使用することは、法に定める罰則その他の措置の対
象となります。

また、今回のコードは以下から取得できます。

https://github.com/Convergence-Lab-Inc/zephyr_amg_ble

Zephyr RTOS

Zephyr RTOSは、その名の通り、RTOSの一種です。RTOSは、Real Time Operating Systemの略です、リアルタイムOSですね。RTOSは、非常にざっくりと正確でない表現をするなら、マイコン用のOSであると思えば良いと思います。読売新聞によると、RTOSのデファクト・スタンダードはTRON(μITRON)らしいです。しかし、私はそもそも組み込み開発には明るくなく、300ページ以上あるμITRONの仕様書を読む気が失せてしまったのもあり、μITRONは利用しないことにしました。

一方、Zephyrは、Wikipediaによると2020年8月現在で最もコントリビューターとコミット数が多いRTOSプロジェクトであるとのことです。そんな、現在最も熱い RTOSである Zephyrに入門してみようと思います。

なぜRTOSをつかうのか

マイコンのプログラムにはRTOSは必須では有りません。RTOSを使わずにマイコンのプログラムを書くことを、ベアメタルプログラムとよびます。もちろん、ベアメタルで書いてもよいのですが、マイコンの種類を変えるとプログラムが動かなくなったり、半導体メーカーから提供されているライブラリの種類が少ないために、多くのコードを書く必要があったりとたいへんではあります。そこで、RTOSをつかって、共通部分をライブラリとして使用したり、ドライバを共有してプログラムの可搬性を高めたりするわけです。

TechFactoryの記事では、RTOSの特徴として以下が挙げられています(一部改変)。

  1.  マルチスレッドのサポート
  2. 最悪応答時間の保証
  3. タスク間通信のサポート
  4. 機能の取捨選択が可能

RTOSを使わない場合、マルチスレッドやタスク間通信すら自力で書かなければならないことが多いということですね。

Zephyr RTOSの特徴 

 Zephyrは、Linux Founddationが開発する RTOSです。そのため、ZephyrのカーネルはLinux Kearnelとの類似性をいくつか持ちます。Linuxにも RTLinuxというリアルタイムOSが存在するのですが、RTLinuxはほぼPCと同じレベルの性能や消費電力をデバイスに要求します。Zephyrはマイコン向けOSらしく、軽量な設計となっています。とはいえ、Zephyrを利用する場合は Cortex-Mなどの32bit Armマイコンを利用するのが普通ではないかな、と思います。

Zephryの特徴には以下があります。

  1. スレッド間通信や割り込みなどのサポート
  2. 複数のタスクスケジューリング方法のサポート
  3. メモリ保護
  4. DeviceTreeのサポート
  5. Bluetooth Low Energy (BLE) 5.0のサポート
  6. Windows, Mac, Linux環境での開発が可能
  7.  モダンな開発ツール

DeviceTreeというのは、Linux Kernelでも採用されている、デバイス管理用法です。Linxuカーネルに慣れている人がいれば(いるのか?)、同じ方法でデバイスの管理ができます。このあたりがZephyrがLinux Foundationのプロジェクトらしいところです。私が最も特徴的だと思うのが、開発環境ツール west の存在です。ZephyrのプロジェクトはCMakeで管理されます。そのビルドや書き込みには westというツールを使います。これが非常に優秀で、様々な組み込み開発に必要なツールを見事に隠蔽しています。例えば、チップへのプログラムの書き込みは、

west flash

とコマンドを打てば良いようになっています。通常はチップメーカーによって様々なコマンドがあるのですが、それを覚える必要がなく便利です。まあ、私が組み込みを知らないだけで他にも便利なものがあるのかもしれませんが。

Zephyr開発環境の準備

Zephyrの開発環境の準備方法は公式の手順を参考にするのが最も良いと思います。ここに転載してしまうと、変更になった時にこまるのでやめておきます。注意点としては、ボードとPCのインターフェイスである、ST-LinkJ-Linkの最新のドライバを入れるのを忘れないようにしてください。この記事のサンプルに必要なのは、J-Linkの方です。わたしは、J-Linkのドライバが最新でないために起こったエラーのせいでしばらく時間が溶けました。

今回使用するボード・部品

今回使用するボード部品は以下のとおりです。

Nordic nRF52-DKは、Arm Cortex-MマイコンにBluetooth Low Energy, Bluetooth mesh, NFCが搭載されたNordic製のSoCが載った開発用ボードです。こちらのボードなのですが、技適未取得です。今回は、「技適未取得機器の実験等の特例制度」を利用して実験を行います。手続きを行わずに電波を出すと電波法違反となるため、くれぐれも注意してください。特例制度自体は個人での取得も可能です。技適未取得なのもあって、国内では販売されていません。私は DigiKeyから購入しました。 開発ボードですので、ジャンパワイヤーがさせたりと便利です。J-Linkに対応しているため、PCからボードへのプログラムの書き込みはUSBケーブルを利用して行えます。電波を出さない実験をする場合は、こちらの開発ボードではなく、Nucleoなどを利用するほうが、入手もしやすく簡単だと思います。

Conta™ サーモグラフィー は Panasonic製の8x8 赤外線センサアレイ AMG8833 を搭載したセンサーモジュールです。I2Cを利用した赤外線センサアレイデータの読み込みができます。


接続

nRF52 DKとAMG8833センサーモジュールを以下のように接続します。

nRF52 DK AMG8833
VDD 3.3V
GND GND

p0.17(D6)

INT
p0.27(SCL) SCL
p0.26(SDA) SDA

基板のシルクにはかかれていませんが、nRF52-DKの p0.27とp0.26はI2Cの SCL, SDAになっています。写真では、SCLとSDAに 4.7kΩのプルアップ抵抗を接続しています。これはなくても動くと思います。基板の右上に電源スイッチがあります。書き込む際や利用する際はこれをONにしてください。

設定ファイル

まず、Zephyrで必要になる設定ファイルを見ていきましょう。まず、使うデバイスの設定ファイルを見ていきましょう。今回は、app.overlayで設定されています。これは、先述の通り、DeviceTree形式になっています。先程の例で、INTを p0.17(D6)につないだのは、このように設定されているためです。以下のようにして、i2cでamg88xxを使うよと設定しています。arduinoのGPIO定義を利用するため、 arduino_i2cとなるようです。

&arduino_i2c {
	status = "okay";

	amg88xx@68 {
		compatible = "panasonic,amg88xx";
		reg = <0x68>;
		label = "AMG88XX";
		/* Pin D6 from Arduino Connector */
		int-gpios = <&arduino_header 12 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
		status = "okay";
	};
};

マイコン側プログラム

以下が、マイコン側のコードの全体です。C言語で書かれています。多くの箇所で、Zephyrのサンプルコードを参考にしています。

#include <zephyr.h>
#include <device.h>
#include <drivers/sensor.h>
#include <sys/printk.h>
#include <sys/byteorder.h>
#include <settings/settings.h>

#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/conn.h>
#include <bluetooth/uuid.h>
#include <bluetooth/gatt.h>

static struct sensor_value temp_value[64];

void print_buffer(void *ptr, size_t l)
{
	struct sensor_value *tv = ptr;
	int ln = 0;

	printk("---|");
	for (int i = 0; i < 8; i++) {
		printk("  %02d  ", i);
	}
	printk("\n");

	printk("%03d|", ln);
	for (int i = 0; i < l; i++) {
		printk("%05d ", (tv[i].val1 * 100 + tv[i].val2 / 10000));
		if (!((i + 1) % 8)) {
			printk("\n");
			ln++;
			printk("%03d|", ln);
		}
	}
	printk("\n");
}


void convert_value(void *ptr, size_t l, uint16_t* buf) {
	struct sensor_value *tv = ptr;
	for (int i = 0; i < l; i++) {
		buf[i] = (tv[i].val1 * 100 + tv[i].val2 / 10000);
	}
}



/* Custom Service Variables */
static struct bt_uuid_128 vnd_uuid = BT_UUID_INIT_128(
	0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12,
	0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12);

static struct bt_uuid_128 vnd_thermo_uuid = BT_UUID_INIT_128(
	0xf1, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12,
	0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12);

static uint16_t vnd_value[64];

static ssize_t read_vnd(struct bt_conn *conn, const struct bt_gatt_attr *attr,
			void *buf, uint16_t len, uint16_t offset)
{
	const char *value = attr->user_data;

	return bt_gatt_attr_read(conn, attr, buf, len, offset, value,
				 strlen(value));
}

static ssize_t write_vnd(struct bt_conn *conn, const struct bt_gatt_attr *attr,
			 const void *buf, uint16_t len, uint16_t offset,
			 uint8_t flags)
{
	uint8_t *value = attr->user_data;

	if (offset + len > sizeof(vnd_value)) {
		return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
	}

	memcpy(value + offset, buf, len);

	return len;
}

static uint8_t simulate_vnd;

static void vnd_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value)
{
	simulate_vnd = (value == BT_GATT_CCC_INDICATE) ? 1 : 0;
}


/* Vendor Primary Service Declaration */
BT_GATT_SERVICE_DEFINE(vnd_svc,
	BT_GATT_PRIMARY_SERVICE(&vnd_uuid),
	BT_GATT_CHARACTERISTIC(&vnd_thermo_uuid.uuid,
				   BT_GATT_CHRC_READ |
			       BT_GATT_CHRC_WRITE | BT_GATT_CHRC_EXT_PROP,
			       BT_GATT_PERM_READ | BT_GATT_PERM_WRITE |
			       BT_GATT_PERM_PREPARE_WRITE,
			       read_vnd, write_vnd, vnd_value),
	BT_GATT_CCC(vnd_ccc_cfg_changed,
		    BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT),
);

static const struct bt_data ad[] = {
	BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
	BT_DATA_BYTES(BT_DATA_UUID128_ALL,
		      0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12,
		      0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12),
};

static void connected(struct bt_conn *conn, uint8_t err)
{
	if (err) {
		printk("Connection failed (err 0x%02x)\n", err);
	} else {
		printk("Connected\n");
	}
}

static void disconnected(struct bt_conn *conn, uint8_t reason)
{
	printk("Disconnected (reason 0x%02x)\n", reason);
}

static struct bt_conn_cb conn_callbacks = {
	.connected = connected,
	.disconnected = disconnected,
};

static void bt_ready(void)
{
	int err;

	printk("Bluetooth initialized\n");

	if (IS_ENABLED(CONFIG_SETTINGS)) {
		settings_load();
	}

	err = bt_le_adv_start(BT_LE_ADV_CONN_NAME, ad, ARRAY_SIZE(ad), NULL, 0);
	if (err) {
		printk("Advertising failed to start (err %d)\n", err);
		return;
	}

	printk("Advertising successfully started\n");
}

void main(void)
{
	int ret;
	const struct device *dev = device_get_binding(
				DT_LABEL(DT_INST(0, panasonic_amg88xx)));

	if (dev == NULL) {
		printk("Could not get AMG88XX device\n");
		return;
	}

	printk("device: %p, name: %s\n", dev, dev->name);

	int err;
	err = bt_enable(NULL);
	if (err) {
		printk("Bluetooth init failed (err %d)\n", err);
		return;
	}


	bt_ready();
	bt_set_name(CONFIG_BT_DEVICE_NAME);

	bt_conn_cb_register(&conn_callbacks);

	/* Implement notification. At the moment there is no suitable way
	 * of starting delayed work so we do it here
	 */
	while (1) {
		k_sleep(K_MSEC(100));

		ret = sensor_sample_fetch(dev);
		if (ret) {
			printk("Failed to fetch a sample, %d\n", ret);
			return;
		}

		ret = sensor_channel_get(dev, SENSOR_CHAN_AMBIENT_TEMP,
					 (struct sensor_value *)temp_value);
		if (ret) {
			printk("Failed to get sensor values, %d\n", ret);
			return;
		}

		// printk("new sample:\n");
		// print_buffer(temp_value, ARRAY_SIZE(temp_value));

		convert_value(temp_value, 64, vnd_value);
		// printk("%d\n", sizeof(vnd_value));
	}
}

プログラムのうち、重要な部分について抜き出してみていきましょう。

以下の部分で、BLEのUUIDを定義しています。 

vnd_uuidがサービスのUUIDです。リトルエンディアンなので、"12345678-12345678-123456789abcdef0"となります。

vnd_thermo_uuidがキャラクタリスティックのUUIDです。"12345678012345678-123456789abcdef1"となります。

ここでは、サンプルなのでこの値ですが、実際に製品に利用するには世界で一位のUUIDを使う必要があります。 uuidgenコマンドや、Online UUID Generator を利用して生成することができます。

/* Custom Service Variables */
static struct bt_uuid_128 vnd_uuid = BT_UUID_INIT_128(
	0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12,
	0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12);

static struct bt_uuid_128 vnd_thermo_uuid = BT_UUID_INIT_128(
	0xf1, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12,
	0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12);

 これらを以下のコードを利用して設定します。

/* Vendor Primary Service Declaration */
BT_GATT_SERVICE_DEFINE(vnd_svc,
	BT_GATT_PRIMARY_SERVICE(&vnd_uuid),
	BT_GATT_CHARACTERISTIC(&vnd_thermo_uuid.uuid,
				   BT_GATT_CHRC_READ |
			       BT_GATT_CHRC_WRITE | BT_GATT_CHRC_EXT_PROP,
			       BT_GATT_PERM_READ | BT_GATT_PERM_WRITE |
			       BT_GATT_PERM_PREPARE_WRITE,
			       read_vnd, write_vnd, vnd_value),
	BT_GATT_CCC(vnd_ccc_cfg_changed,
		    BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT),
);

そして、 bt_ready()関数で、BLE通信のアドバタイズをします。

static void bt_ready(void)
{
	int err;

	printk("Bluetooth initialized\n");

	if (IS_ENABLED(CONFIG_SETTINGS)) {
		settings_load();
	}

	err = bt_le_adv_start(BT_LE_ADV_CONN_NAME, ad, ARRAY_SIZE(ad), NULL, 0);
	if (err) {
		printk("Advertising failed to start (err %d)\n", err);
		return;
	}

	printk("Advertising successfully started\n");
}

AMG8833センサモジュールからのデータの読み込みは以下のようにしています。まず、デバイスの初期化を行います。

void main(void) {
	// .. snip
	const struct device *dev = device_get_binding(
				DT_LABEL(DT_INST(0, panasonic_amg88xx)));
	if (dev == NULL) {
		printk("Could not get AMG88XX device\n");
		return;
	}

センサーの値を取り出すのは以下のように sensor_sample_fetch(), sensor_channel_get()を呼ぶだけです。print_buffer()をコメントインすると、シリアルコンソールにセンサー値を書き出します。

		ret = sensor_sample_fetch(dev);
		if (ret) {
			printk("Failed to fetch a sample, %d\n", ret);
			return;
		}

		ret = sensor_channel_get(dev, SENSOR_CHAN_AMBIENT_TEMP,
					 (struct sensor_value *)temp_value);
		if (ret) {
			printk("Failed to get sensor values, %d\n", ret);
			return;
		}
		// print_buffer(temp_value, ARRAY_SIZE(temp_value));

次に、convert_valueで、読み込んだセンサー値をBLEのキャラクタリスティックに書き込みます。何か他にいい方法があるような気もするのですが、今回は直接メモリに書き込んでいます。8x8のセンサーなので値は64個の uint16 になります。

		convert_value(temp_value, 64, vnd_value);
// snip

void convert_value(void *ptr, size_t l, uint16_t* buf) {
	struct sensor_value *tv = ptr;
	for (int i = 0; i < l; i++) {
		buf[i] = (tv[i].val1 * 100 + tv[i].val2 / 10000);
	}
}

PC側プログラム

PC側のプログラムはPythonで書きました。本当はスマホのほうがそれっぽいのですが、やや面倒だったので今回はPCで受信することにします。BLE通信用のパッケージとしてBleakを使用します。ここでは、実行時に与えられたコマンドライン引数のMACアドレスのBLE機器から、キャラクタリスティックを読み込みます。後述しますが、今回はMACアドレスをIEEEに申請していないこともあり、デバイスのMACアドレスはランダム値となります。

import platform
import sys
import asyncio
import logging
import struct
import time
import matplotlib.pyplot as plt
import numpy as np

from bleak import BleakClient


async def run(address, debug=False):
    log = logging.getLogger(__name__)

    async with BleakClient(address) as client:
        log.info(f"Connected: {client.is_connected}")

        plt.ion()
        graph = plt.imshow(np.reshape(np.repeat(0,64),(8,8)),cmap=plt.cm.hot,interpolation='lanczos')
        plt.draw()
        while True:
            value = bytes(await client.read_gatt_char("12345678-1234-5678-1234-56789abcdef1"))
            print(len(value))
            print(value)
            value_ints = struct.unpack('<HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH', value)
            value_ints = np.array(value_ints).reshape(8, 8)
            graph = plt.imshow(value_ints,cmap=plt.cm.hot,interpolation='lanczos')
            plt.pause(0.01)


if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("python amg88xx_ble.py [MAC ADDRESS]")

    print(sys.argv)
    address = (
        sys.argv[1]
    )
    loop = asyncio.get_event_loop()
    loop.set_debug(True)
    loop.run_until_complete(run(address, True))

特筆すべきコードは以下の部分でしょうか。

 value_ints = struct.unpack('<HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH', value)

BLEのキャラクタリスティックはバイナリ値ですので、structを利用して、64個のunsined shortにunpackする必要があります。


マイコンへのコードの書き込み

さて、コードを実行してみましょう。まず、環境を設定します。コードをgithubからクローンして、以下のようにコマンドを実行します。west updateにはしばらく時間がかかります。

git clone https://github.com/Convergence-Lab-Inc/zephyr_amg_ble.git
cd zephyr_amg_ble
west init
west update

 つぎに、マイコン側コードをビルドします。ここで、nrf52dk_nrf52832 はZephyrに予め定義されている nRF52-DKのボード名です。

west build -b nrf52dk_nrf52832 .  

そして、コードをマイコンに書き込みます。USBでボードを接続して、以下のようにコマンドヲ入力します。

west flash

実行

まず、シリアルコンソールを表示しましょう。以下のようにします。 ACM0の部分は環境によって変わるはずです。なお、screenを閉じるには Ctrl-a Ctrl-\ と入力します。

screen /dev/ttyACM0 115200

nRF52 DKのリセットボタンを押すと、コンソールに以下のように表示されるはずです。これで通信待ちになっています

*** Booting Zephyr OS build v2.6.0-rc3-42-g39354d92bbdc  ***
device: 0x2000089c, name: AMG88XX
Bluetooth initialized
Advertising successfully started
[00:00:00.286,895] <inf> fs_nvs: 6 Sectors of 4096 bytes
[00:00:00.286,926] <inf> fs_nvs: alloc wra: 0, f80
[00:00:00.286,926] <inf> fs_nvs: data wra: 0, b0
[00:00:00.288,452] <inf> bt_hci_core: HW Platform: Nordic Semiconductor (0x0002)
[00:00:00.288,452] <inf> bt_hci_core: HW Variant: nRF52x (0x0002)
[00:00:00.288,482] <inf> bt_hci_core: Firmware: Standard Bluetooth controller (0x00) Version 2.6 Build 0
[00:00:00.288,757] <inf> bt_hci_core: No ID address. App must call settings_load()
[00:00:00.291,198] <inf> bt_hci_core: Identity: D5:99:B8:25:6A:AE (random)
[00:00:00.291,229] <inf> bt_hci_core: HCI: version 5.2 (0x0b) revision 0x0000, manufacturer 0x05f1
[00:00:00.291,229] <inf> bt_hci_core: LMP: version 5.2 (0x0b) subver 0xffff

 つぎに、MAC Addressを取得します。

python/ディレクトリの中の scan_gattlib.pyを実行します。 sudoが必要です。

# sudo python scan_gattlib.py
name: BLE Thermography, address: 6F:B2:C3:1F:56:1B

前述したようにこの値は、起動するたびに変わります。

次に、取得したMacアドレスを引数にして、amg_ble.pyを実行します。

python amg_ble.py 6F:B2:C3:1F:56:1B

 以下のような画像が表示されれば成功です。

ぼんやりと人型の形が見えますね。

次に、ホットコーヒーを入れたコーヒーカップを赤外線カメラの前においてみました。

人よりホットコーヒーのほうがだいぶあついのでホットコーヒーが明るくなっていますね。

まとめ

今回はZephyr RTOSをつかって、サーモグラフィーの画像をBLEでPCに送るコード作成しました。マルチスレッドや割り込みな度は使っていないませんが、BLEをつかったデバイスを簡単に作ることができることがわかります。

また、今後も更新していきたいと思います。

 

このエントリーをはてなブックマークに追加
 

お問い合わせ

 

Related Articles/Posts