「Electronut LABS」のBLE開発ボード「papyr」の紹介

144Labの入江田です。

Tindieで見つけたE-Ink付きnRF52840ボード「papyr」を紹介します。

f:id:irieda:20190418185628j:plain

  • Nordic製nRF52840とBLEアンテナをモジュール化したMDBT50Q搭載
  • NFCアンテナ付き
  • USB端子はファーム次第でUART/HID/MSDになる
  • ボタンx1搭載
  • フルカラーLEDx1搭載
  • E-Inkディスプレイx1搭載
  • 電源供給をUSBからかボタン電池からかを選べるスイッチがある

デフォルトのファームはスマホアプリと連携して E-Inkディスプレイを書き換えできます。

ボード単体で$39です。 ファームウェアソース

基板は4〜5mmくらいで薄いんだけど、

f:id:irieda:20190418185701j:plain

CR2477の電池がごっつい!

技適あるよ!

f:id:irieda:20190418185624j:plain

この写真拡大してみればちゃんと技適もとってあるので日本で電波吹いても問題なし。

開発キットで購入すると、このようなSWDデバッガが付属します。 「Bumpy(SWDデバッガー)」と「PogoProg(スプリングコンタクト)」です。

f:id:irieda:20190418185640j:plain

こんな風にギュッと押し当てつつデバッグしたりファーム書き換えたりします。

f:id:irieda:20190418185649j:plain

書き込み

「Bumpy」をPCに挿すと2つのUSBシリアルデバイスが見えます。 (番号が若い方がデバッガ)

  1. デバッガ
  2. 汎用シリアル(ピンにそのまま外だし)

書き込みはgdbコマンド経由で1.のシリアルポートを使って行います。

flash.sh

#!/bin/sh
DEV=/dev/cu.usb####
arm-none-eabi-gdb -nx --batch \
  -ex "target extended-remote $DEV" \
  -ex 'monitor swdp_scan' \
  -ex 'attach 1' \
  -ex 'load' \
  -ex 'compare-sections' \
  -ex 'kill' \
  $1
$ ./flash.sh <書き込むhexまたはelf>

ブートローダ

ブートローダーを入れて開発を行うと、開発サイクルが効率的になります。

メモリマップの後方にブートローダーを置いておき、 開発するアプリケーションはブートローダーに書き込んでもらう方式です。

ガチ勢はブートローダーはサンプルコードから自作するものらしいけれど、 最初の頃は既存のブートローダーで便利そうなのを選んで入れると良いでしょう。

nRF-Connect用ファーム

nRF-Connect用ファームダウンロード

  • Nordicのnrfjprogに対応、nRF-Connect/ProgrammerでもOK
  • ProgrammerのUIはメモリマップを学ぶのに最適
  • 対応デバイスは少なめ(比較的最近のnRF5x系のみ)
  • パワーオンリセットでアプリケーションは実行される
  • リセットボタンでファームウェアリーダライター待機に戻る

f:id:irieda:20190418185908p:plain

Adafruit_nRF52_Bootloader

https://github.com/electronut/Adafruit_nRF52_Bootloader

  • PCとUSB端子を繋ぐとUSB-MSDとして認識される
  • マウントデバイス名が「NRF52BOOT」
  • そのドライブにUF2形式のバイナリを投げ込めば書き込みできる
  • リセットボタンダブルクリックでドライブマウント状態に戻る

CircuitPython

組み込み向けPython(MicroPythonのフォーク)

  • ランタイムを含みブートローダー役もこなす
  • Adafruit_nRF52_Bootloader経由で書き込んで使う
  • リセットボタンでどちらのブートローダーを利用するのかが選べる
  • PCとUSB端子を繋ぐとUSB-MSDとして認識される
  • マウントデバイス名が「CIRCUITPY」
  • そのドライブに投げ込めばどんなファイルでも書き込みできる
  • main.pyというファイル名でPythonスクリプトを書き込むと起動時にそれを実行する
  • 「CIRCUITPY/lib」 配下にPythonモジュールを置いておくとユーザー実装からimport可能になる

Adafruitが公開しているライブラリ このzipアーカイブを解凍して、必要なものだけをターゲットのlibフォルダに複製しよう。

Electronut LABS製papyr向けのCircuitPythonは最新のブランチにはマージされました。ビルド済みUF2ファイルがここで公開されるのももうすぐでしょう。

nRF52840を使ったシンプルなボードとは互換性がありますので、 それまでは、adafruit-circuitpython-sparkfun_nrf52840_mini-en_US-4.0.0-beta.7.uf2こちらを書き込むと良いでしょう。

異なるボードのCircuitPythonでは「board」モジュールが使えませんので、 「board.XXX」は「microcontroller.pin.XX」という表現に置き換えてください。

ファームウェアソースのcode/circuitpython/code.pyにある実装でE-Inkの書き換えができます。

これで、Pythonを使ってBLEやE-Inkを使ったアプリケーションが書けます。

以下のコマンドでCircuitPythonの対話シェルを利用することができます。 シリアルをつないだらENTERキーを押せば対話シェルが起動します。

$ screen /dev/tty.usbserial### 115200

対話シェルでは動的にターゲットをコントロールできます。

Bumpy

開発キットに付属しているBumpyはblackmagic-probeという製品の互換品で、GDBサーバー機能を一通り実装したものです。 これを用いて実際のデバッグgdbコマンドを通じて行えます。 SWD関連の2端子のあるターゲットであればデバッグ可能ですが、 nRF52系ICは最初からこれらの端子を備えています。

PythonでE-Ink書き換え

papyrのサンプルコードを元に、 boardモジュール依存を無くしたものが以下のコード。

import time

import busio
import digitalio
import microcontroller
import pulseio
from adafruit_bus_device.spi_device import SPIDevice

red = pulseio.PWMOut(microcontroller.pin.P0_14, frequency=50000, duty_cycle=65535)
blue = pulseio.PWMOut(microcontroller.pin.P0_15, frequency=50000, duty_cycle=65535)
green = pulseio.PWMOut(microcontroller.pin.P0_13, frequency=50000, duty_cycle=65535)

EPD_WIDTH = 200
EPD_HEIGHT = 200

PANEL_SETTING = 0x00
POWER_SETTING = 0x01
POWER_OFF = 0x02
POWER_OFF_SEQUENCE_SETTING = 0x03
POWER_ON = 0x04
POWER_ON_MEASURE = 0x05
BOOSTER_SOFT_START = 0x06
DEEP_SLEEP = 0x07
DATA_START_TRANSMISSION_1 = 0x10
DATA_STOP = 0x11
DISPLAY_REFRESH = 0x12
DATA_START_TRANSMISSION_2 = 0x13
PLL_CONTROL = 0x30
TEMPERATURE_SENSOR_COMMAND = 0x40
TEMPERATURE_SENSOR_CALIBRATION = 0x41
TEMPERATURE_SENSOR_WRITE = 0x42
TEMPERATURE_SENSOR_READ = 0x43
VCOM_AND_DATA_INTERVAL_SETTING = 0x50
LOW_POWER_DETECTION = 0x51
TCON_SETTING = 0x60
TCON_RESOLUTION = 0x61
SOURCE_AND_GATE_START_SETTING = 0x62
GET_STATUS = 0x71
AUTO_MEASURE_VCOM = 0x80
VCOM_VALUE = 0x81
VCM_DC_SETTING_REGISTER = 0x82
PROGRAM_MODE = 0xA0
ACTIVE_PROGRAM = 0xA1
READ_OTP_DATA = 0xA2

lut_vcom0 = bytearray(
    [
        0x0E,
        0x14,
        0x01,
        0x0A,
        0x06,
        0x04,
        0x0A,
        0x0A,
        0x0F,
        0x03,
        0x03,
        0x0C,
        0x06,
        0x0A,
        0x00,
    ]
)

lut_w = bytearray(
    [
        0x0E,
        0x14,
        0x01,
        0x0A,
        0x46,
        0x04,
        0x8A,
        0x4A,
        0x0F,
        0x83,
        0x43,
        0x0C,
        0x86,
        0x0A,
        0x04,
    ]
)

lut_b = bytearray(
    [
        0x0E,
        0x14,
        0x01,
        0x8A,
        0x06,
        0x04,
        0x8A,
        0x4A,
        0x0F,
        0x83,
        0x43,
        0x0C,
        0x06,
        0x4A,
        0x04,
    ]
)

lut_g1 = bytearray(
    [
        0x8E,
        0x94,
        0x01,
        0x8A,
        0x06,
        0x04,
        0x8A,
        0x4A,
        0x0F,
        0x83,
        0x43,
        0x0C,
        0x06,
        0x0A,
        0x04,
    ]
)

lut_g2 = bytearray(
    [
        0x8E,
        0x94,
        0x01,
        0x8A,
        0x06,
        0x04,
        0x8A,
        0x4A,
        0x0F,
        0x83,
        0x43,
        0x0C,
        0x06,
        0x0A,
        0x04,
    ]
)

lut_vcom1 = bytearray(
    [
        0x03,
        0x1D,
        0x01,
        0x01,
        0x08,
        0x23,
        0x37,
        0x37,
        0x01,
        0x00,
        0x00,
        0x00,
        0x00,
        0x00,
        0x00,
    ]
)

lut_red0 = bytearray(
    [
        0x83,
        0x5D,
        0x01,
        0x81,
        0x48,
        0x23,
        0x77,
        0x77,
        0x01,
        0x00,
        0x00,
        0x00,
        0x00,
        0x00,
        0x00,
    ]
)

lut_red1 = bytearray(
    [
        0x03,
        0x1D,
        0x01,
        0x01,
        0x08,
        0x23,
        0x37,
        0x37,
        0x01,
        0x00,
        0x00,
        0x00,
        0x00,
        0x00,
        0x00,
    ]
)

busy = digitalio.DigitalInOut(microcontroller.pin.P0_03)
res = digitalio.DigitalInOut(microcontroller.pin.P0_02)
dc = digitalio.DigitalInOut(microcontroller.pin.P0_28)
eink_en = digitalio.DigitalInOut(microcontroller.pin.P0_11)

cs = digitalio.DigitalInOut(microcontroller.pin.P0_30)
comm_port = busio.SPI(
    microcontroller.pin.P0_31,
    MOSI=microcontroller.pin.P0_29,
    MISO=microcontroller.pin.P1_01,
)
device = SPIDevice(comm_port, cs)

# for framebuffer
frame_black = bytearray(round(EPD_WIDTH * EPD_HEIGHT / 8))
frame_red = bytearray(round(EPD_WIDTH * EPD_HEIGHT / 8))


def epd_send_cmd(cmd):
    dc.value = 0
    with device as bus_device:
        bus_device.write(bytes([cmd]))


def epd_send_data(data):
    dc.value = 1
    with device as bus_device:
        bus_device.write(bytes([data]))


def epd_wait_until_idle():
    while True:
        if busy.value != 0:
            break
        time.sleep(0.1)


def epd_set_lut_bw():
    epd_send_cmd(0x20)  # g vcom
    for count in range(15):
        epd_send_data(lut_vcom0[count])

    epd_send_cmd(0x21)  # g ww --
    for count in range(15):
        epd_send_data(lut_w[count])

    epd_send_cmd(0x22)  # g bw r
    for count in range(15):
        epd_send_data(lut_b[count])

    epd_send_cmd(0x23)  # g wb w
    for count in range(15):
        epd_send_data(lut_g1[count])

    epd_send_cmd(0x24)  # g bb b
    for count in range(15):
        epd_send_data(lut_g2[count])


def epd_set_lut_red():
    epd_send_cmd(0x25)
    for count in range(15):
        epd_send_data(lut_vcom1[count])

    epd_send_cmd(0x26)
    for count in range(15):
        epd_send_data(lut_red0[count])

    epd_send_cmd(0x27)
    for count in range(15):
        epd_send_data(lut_red1[count])


def epd_reset():
    res.value = 0
    time.sleep(0.2)
    res.value = 1
    time.sleep(0.2)


def epd_display_frame():
    epd_send_cmd(DATA_START_TRANSMISSION_1)
    time.sleep(0.2)
    for i in range(EPD_HEIGHT * EPD_WIDTH / 8):
        temp = 0x00
        for bit in range(4):
            if (frame_black[i] & (0x80 >> bit)) != 0:
                temp |= 0xC0 >> (bit * 2)
        epd_send_data(temp)

        temp = 0x00
        for bit in range(4, 8):
            if (frame_black[i] & (0x80 >> bit)) != 0:
                temp |= 0xC0 >> ((bit - 4) * 2)
        epd_send_data(temp)
    time.sleep(0.2)

    epd_send_cmd(DATA_START_TRANSMISSION_2)
    time.sleep(0.2)
    for i in range(EPD_HEIGHT * EPD_WIDTH / 8):
        epd_send_data(frame_red[i])
    time.sleep(0.2)

    epd_send_cmd(DISPLAY_REFRESH)
    epd_wait_until_idle()


def epd_init():
    epd_reset()
    epd_send_cmd(POWER_SETTING)
    epd_send_data(0x07)
    epd_send_data(0x00)
    epd_send_data(0x08)
    epd_send_data(0x00)
    epd_send_cmd(BOOSTER_SOFT_START)
    epd_send_data(0x07)
    epd_send_data(0x07)
    epd_send_data(0x07)
    epd_send_cmd(POWER_ON)
    epd_wait_until_idle()

    epd_send_cmd(PANEL_SETTING)
    epd_send_data(0xCF)
    epd_send_cmd(VCOM_AND_DATA_INTERVAL_SETTING)
    epd_send_data(0x17)
    epd_send_cmd(PLL_CONTROL)
    epd_send_data(0x39)
    epd_send_cmd(TCON_RESOLUTION)
    epd_send_data(0xC8)
    epd_send_data(0x00)
    epd_send_data(0xC8)
    epd_send_cmd(VCM_DC_SETTING_REGISTER)
    epd_send_data(0x30)

    epd_set_lut_bw()
    epd_set_lut_red()

    time.sleep(0.2)


def paint_clear(fb, colored):
    if colored:
        val = 0x00
    else:
        val = 0xFF
    for i in range(len(fb)):
        fb[i] = val


def epd_sleep():
    epd_send_cmd(VCOM_AND_DATA_INTERVAL_SETTING)
    epd_send_data(0x17)
    epd_send_cmd(VCM_DC_SETTING_REGISTER)  # to solve Vcom drop
    epd_send_data(0x00)
    epd_send_cmd(POWER_SETTING)  # power setting
    epd_send_data(0x02)  # gate switch to external
    epd_send_data(0x00)
    epd_send_data(0x00)
    epd_send_data(0x00)
    epd_wait_until_idle()
    epd_send_cmd(POWER_OFF)


def epaper_pins_off():
    busy.direction = digitalio.Direction.OUTPUT
    res.direction = digitalio.Direction.OUTPUT
    dc.direction = digitalio.Direction.OUTPUT
    cs.direction = digitalio.Direction.OUTPUT
    eink_en.direction = digitalio.Direction.OUTPUT
    res.value = 0
    dc.value = 0
    cs.value = 0
    eink_en.value = 1


def epaper_pins_init():
    busy.direction = digitalio.Direction.INPUT
    res.direction = digitalio.Direction.OUTPUT
    res.value = 0
    dc.direction = digitalio.Direction.OUTPUT
    dc.value = 0
    cs.direction = digitalio.Direction.OUTPUT
    cs.value = 0

    time.sleep(0.1)

    eink_en.direction = digitalio.Direction.OUTPUT
    eink_en.value = 1


def epaper_on():
    time.sleep(0.5)
    eink_en.value = 0
    time.sleep(0.5)


def epaper_off():
    time.sleep(0.5)
    eink_en.value = 1


def paint_draw_pixel(fb, x, y, colored):
    if x < 0 or x >= EPD_WIDTH or y < 0 or y >= EPD_HEIGHT:
        return

    if colored:
        fb[int((x + y * EPD_WIDTH) / 8)] &= ~(0x80 >> (x % 8))
    else:
        fb[int((x + y * EPD_WIDTH) / 8)] |= 0x80 >> (x % 8)


def paint_circle(fb, x, y, radius, colored):
    # Bresenham algorithm
    x_pos = -radius
    y_pos = 0
    err = 2 - 2 * radius
    e2 = 0

    while True:
        paint_draw_pixel(fb, x - x_pos, y + y_pos, colored)
        paint_draw_pixel(fb, x + x_pos, y + y_pos, colored)
        paint_draw_pixel(fb, x + x_pos, y - y_pos, colored)
        paint_draw_pixel(fb, x - x_pos, y - y_pos, colored)
        e2 = err
        if e2 <= y_pos:
            y_pos = y_pos + 1
            err = err + y_pos * 2 + 1
            if -x_pos == y_pos and e2 <= x_pos:
                e2 = 0

        if e2 > x_pos:
            x_pos = x_pos + 1
            err = err + x_pos * 2 + 1

        if x_pos > 0:
            break


red.duty_cycle = 0  # on

epaper_pins_init()
epaper_on()
epd_init()

paint_clear(frame_black, False)
paint_clear(frame_red, False)

for i in range(0, 100, 20):
    paint_circle(frame_black, 100, 100, i, True)
    paint_circle(frame_red, 100, 100, i + 4, True)

epd_display_frame()
epd_sleep()

red.value = 65535  # off

実行結果

f:id:irieda:20190418185921j:plain

まとめ

ARM系開発環境自体はカジュアルにしか触ってこなかったので勘所を掴むのに時間を取られました。

なんとかファームをゼロから書き込む方法とアプリケーションだけを効率よく書き込むブートローダーによる方法を確立することができました。

Nordicはプロプライエタリな実装をうまくsoftdevice領域に逃がしてくれているので、更新頻度の異なるユーザーアプリケーションとは分離して扱って別々に更新できるという仕組みは大変開発効率がいいし、後方互換性も高い水準をキープし続けています。

なので将来的にLoRaやNB-IoTモジュールに置き換わっても後方互換製がある程度保たれるであろうという期待が持てますね。