144Labの入江田です。
Tindieで見つけたE-Ink付きnRF52840ボード「papyr」を紹介します。
- Nordic製nRF52840とBLEアンテナをモジュール化したMDBT50Q搭載
- NFCアンテナ付き
- USB端子はファーム次第でUART/HID/MSDになる
- ボタンx1搭載
- フルカラーLEDx1搭載
- E-Inkディスプレイx1搭載
- 電源供給をUSBからかボタン電池からかを選べるスイッチがある
デフォルトのファームはスマホアプリと連携して E-Inkディスプレイを書き換えできます。
ボード単体で$39です。 ファームウェアソース
基板は4〜5mmくらいで薄いんだけど、
CR2477の電池がごっつい!
技適あるよ!
この写真拡大してみればちゃんと技適もとってあるので日本で電波吹いても問題なし。
開発キットで購入すると、このようなSWDデバッガが付属します。 「Bumpy(SWDデバッガー)」と「PogoProg(スプリングコンタクト)」です。
こんな風にギュッと押し当てつつデバッグしたりファーム書き換えたりします。
書き込み
「Bumpy」をPCに挿すと2つのUSBシリアルデバイスが見えます。 (番号が若い方がデバッガ)
- デバッガ
- 汎用シリアル(ピンにそのまま外だし)
書き込みは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用ファーム
- Nordicのnrfjprogに対応、nRF-Connect/ProgrammerでもOK
- ProgrammerのUIはメモリマップを学ぶのに最適
- 対応デバイスは少なめ(比較的最近のnRF5x系のみ)
- パワーオンリセットでアプリケーションは実行される
- リセットボタンでファームウェアリーダライター待機に戻る
- オレンジ: MBR(マスターブートレコード)
- ブルー: softdevice(電波関連ライブラリ)
- グリーン: ユーザーアプリケーション
- レッド: ブートローダー
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
実行結果
まとめ
ARM系開発環境自体はカジュアルにしか触ってこなかったので勘所を掴むのに時間を取られました。
なんとかファームをゼロから書き込む方法とアプリケーションだけを効率よく書き込むブートローダーによる方法を確立することができました。
Nordicはプロプライエタリな実装をうまくsoftdevice領域に逃がしてくれているので、更新頻度の異なるユーザーアプリケーションとは分離して扱って別々に更新できるという仕組みは大変開発効率がいいし、後方互換性も高い水準をキープし続けています。
なので将来的にLoRaやNB-IoTモジュールに置き換わっても後方互換製がある程度保たれるであろうという期待が持てますね。