0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[F#]USBマイコンと通信する<Digispark編>

Last updated at Posted at 2023-08-14

はじめに

引き出しの整理をしていたらDigisparkのストックが出てきました。
前回の購入時(21年1月頃)はクローン品が$1.2程、さらに円高だったので多めに買った残り物です。
最近は円建てで300円以上するので、買う気になりませんが...

キーボード・マウスとしての活用事例が多いようですが、DigiUSBでお手軽な汎用USBデバイスになります。

ただし、汎用デバイスとして使うには柔軟なホスト環境も必要です。
GitHubにはデバイスやホストのサンプルプログラムが実行ファイルも併せて公開されています。

検索するとホストとして上記サンプルプログラム中のmonitor.exeを使用する例が散見されます。
上記をzipダウンロードして展開し、Python\DigiUSB\windows\まで下りてmonitor.exeを実行すればそのまま動きます。

上は実際に動かした際のスクショです。
使用したDigisparkは大文字小文字を変換して返すようにプログラムされている(詳細は後述)ので正常に機能しています。
GUIかつ文字列での通信ということもあって、動作確認の他には使う場面が限定されそうです。

サンプルプログラムを眺めると、核になるのは次のファイルということがわかります。
このファイルをimportすればPythonで簡単に通信ができてしまいます。(要PyUSB

本家Pythonはもちろんのこと、PyUSBをインストールしたIronPython 3.4でも動作します。

「このままPythonで動かせばいいのに、今さら...」の感は否めませんが、今回はホスト側をF#スクリプトで記述し、自由度の高い汎用USBデバイスとホストの製作例を記事にします。

サンプルプログラムの焼き直しで特に目新しい内容はありませんので、一つの事例として読み流してください。

具体的にはDigisparkにNeoPixel(8LED スティック)を接続してホストPCで操作してみます。

やることは単純に次の2つです。

  • F#スクリプトでホストPCのUSBから24バイトのデータ(8LED分)をDigisparkに流す。
  • DigisparkはUSBから受け取った24バイトのデータでNeoPixelを点灯させる。

Python同様、F#もREPL環境が使えるので、イルミネーションのパターンをすぐに試作できます。

環境

  • Windows 11 Pro 22H2(ThinkPad L15 Core i5-10210U)
  • .NET SDK 7.0.400 <要注意!>
  • VSCode(拡張機能として PlatformIO IDE + Ionide for F#)
    (なお、Ionide for F#は、C#拡張機能に依存するので、こちらもインストール)
  • Digispark開発時にはドライバが必要(下記から入手)
    https://github.com/digistump/DigistumpArduino/blob/master/tools
    Windowsの場合はmicronucleus-2.0a4-win.zipを展開し、DPinst64.exeを実行すればよい。

注意:.NET7 SDKの一部バージョンにはバグがあり、記事中のF#スクリプトが動作しないものがあります。(#r "nuget: ...."がエラーになる。)
ただし、執筆時最新の SDK 7.0.400(Runtime 7.0.10)(2023/8/8リリース)は動作します

VSCodeに拡張機能を追加すると、マイコンのコード編集・アップロード、ホスト側のF#スクリプト編集・実行が同一環境下で完結し、軽量かつ強力な統合環境が実現できます。
ワークスペースによって、マイコンのプロジェクトフォルダとホスト側スクリプトのフォルダを一括管理できるのも大きなメリットです。

まずはループバックテスト

USBからDigisparkにデータを送信し、USBでDigisparkからのデータを受信します。
今回は少し手を加えて、半角英字コードを受信した場合に大文字小文字を変換して返すことにします。

マイコン側

PlatformIO IDEを使ったDigisparkプロジェクトの作成はとても簡単です。
新規プロジェクトで、ボードのドロップダウンリストから"Digispark USB (Digistump)"を選択するだけです。

今回はプロジェクト名をDigispark_ChangeCaseとしておきました。
既定のフォルダ(ドキュメント内のPlatformIO\Projects)内に同名のフォルダができます。

Digispark側のコード(スケッチ)はmain.cppを書き換えます。
PlatformIOを利用しているので#include <Arduino.h>が必要なことを除けば特に補足はありません。
USB周りはDigiUSBライブラリで簡単に済ませます。

main.cpp
#include <Arduino.h>
#include <DigiUSB.h>

#define LED 1  // Built-in red LED (Model A)
uint8_t changeCase(uint8_t);

void setup() {
  DigiUSB.begin();
  pinMode(LED, OUTPUT);
}

void loop() {
  while (DigiUSB.available()) {
    digitalWrite(LED, HIGH);
    DigiUSB.write(changeCase((uint8_t)DigiUSB.read()));
  }
  digitalWrite(LED, LOW);
  DigiUSB.refresh();
}

uint8_t changeCase(uint8_t c) {
  if ('A' <= c && c <= 'Z') {
    c += 0x20;
  } else if ('a' <= c && c <= 'z') {
    c -= 0x20;
  }
  return c;
}

VSCode + PlatformIO IDEの場合、エディタ上でショートカットキーAlt + Ctrl + Uによりデバイスへのアップロードを実行します。
(ビルドだけならAlt + Ctrl + B

Digisparkのお約束で、アップロードの前にUSBから抜いておきます。
抜き忘れた場合でも下記メッセージが出てから抜いて大丈夫です。

ターミナルに

> Please plug in the device (will time out in 60 seconds) ... 

が表示されたら準備が整ったので、Digisparkを挿すと書き込みが始まります。

以下に実際のスクショを載せますが、DigiUSBはかなりのリソースを消費します。

書き込みが成功すれば、挿し直すことなく新たなUSBデバイスとして認識されます。

DigiUSBで作成したUSBデバイスは上図のとおりlibusb-win32デバイスです。
前述のとおり開発環境はすでにドライバがインストールされていますが、デバイスを他のWindows PCで使う場合はドライバ(libusb-win32)のインストールが必要です。

ホスト側

F#のスクリプトはGitHubで公開されているコードを参考(ほぼ引用)にして書いてみました。

PyUSBに替わるUSB用のライブラリとして、libusb-win32をサポートしているLibUsbDotNetを使います。
こちらもUSBホストは簡潔に記述できます。下は公式ドキュメントへのリンクです。

VID,PIDによるデバイスの認識、1バイトの送信・受信も数行のコードで済みます。

モデルにしたusbdevice.py同様の作りを目指しましたが、使い勝手をよくするために複数バイトの入出力、文字列の入出力なども実装しておきます。

以下、見てのとおりエラーチェックや例外処理は施していません。終了処理も省略しました。
(ただし、LibUsbDotNet.UsbDeviceクラスにアクセスできるようにUsbDeviceプロパティを実装しています。)
1バイトを読み取るためのReadByteの結果はoption型にして成否の判断ができるようにしています。

DigiUSB.fsx
#r "nuget: LibUsbDotNet, 2.2.29"

open LibUsbDotNet
open LibUsbDotNet.Main
open System.Collections.Generic

type DigiUsbDevice() =
    let USBRQ_HID_GET_REPORT = 0x01uy
    let USBRQ_HID_SET_REPORT = 0x09uy
    let USB_HID_REPORT_TYPE_FEATURE = 0x03

    let REQUEST_TYPE_SEND =
        UsbCtrlFlags.RequestType_Class
        ||| UsbCtrlFlags.Recipient_Device
        ||| UsbCtrlFlags.Direction_Out
        |> byte

    let REQUEST_TYPE_RECEIVE =
        UsbCtrlFlags.RequestType_Class
        ||| UsbCtrlFlags.Recipient_Device
        ||| UsbCtrlFlags.Direction_In
        |> byte

    let Device: UsbDevice =
        (0x16c0, 0x05df) // (VID, PID) of Digispark
        |> UsbDeviceFinder
        |> UsbDevice.OpenUsbDevice

    // 次の3つのプロパティはデバイス情報確認用
    member this.UsbDevice = Device
    member this.Manufacturer = Device.Info.ManufacturerString
    member this.Product = Device.Info.ProductString

    // 送信メソッド(1バイト・バイト列・文字列)
    member this.WriteByte(data: byte) =
        let mutable packet: UsbSetupPacket =
            UsbSetupPacket(REQUEST_TYPE_SEND, USBRQ_HID_SET_REPORT, USB_HID_REPORT_TYPE_FEATURE <<< 8, int data, 0)
        let mutable bytesWritten: int = 0
        Device.ControlTransfer(&packet, null, 0, &bytesWritten)

    member this.WriteBytes(data: seq<byte>) =
        data |> Seq.map this.WriteByte |> Seq.forall id

    member this.WriteString(str: string) =
        str |> Seq.map byte |> this.WriteBytes

    // 受信メソッド(1バイト・バイト列・文字列)
    member this.ReadByte() : byte option =
        let mutable packet: UsbSetupPacket =
            UsbSetupPacket(REQUEST_TYPE_RECEIVE, USBRQ_HID_GET_REPORT, USB_HID_REPORT_TYPE_FEATURE <<< 8, 0, 1)
        let readValue: byte array = [| 0uy |]
        let mutable bytesRead: int = 0
        let result: bool = Device.ControlTransfer(&packet, readValue, 1, &bytesRead)
        if result && (bytesRead = 1) then
            Some(readValue[0])
        else
            None

    member this.ReadBytes() =
        let data: Queue<byte> = Queue<byte>()
        let mutable flag: bool = true
        while flag do
            let result: byte option = this.ReadByte()
            if result.IsSome then
                data.Enqueue(result.Value)
            else
                flag <- false
        data :> seq<byte>

    member this.ReadString() =
        this.ReadBytes()
        |> Seq.map char
        |> System.String.Concat

完成したスクリプトDigiUSB.fsxは保存して、以降は他のスクリプトから呼び出して使います。
ここまでで、目的の95%は達成です。

続いて、ループバックテスト用のスクリプトDigiUSB_ChangeCase.fsxを書きます。
こちらは、上記DigiUSB.fsxのロードと、送信と受信を1つのメソッドにまとめただけのスクリプトです。
末尾でデバイスのインスタンスmyDigiを作成します。

DigiUSB_ChangeCase.fsx
#load "DigiUSB.fsx"
open DigiUSB

type ChangeCaseDevice() =
    let Digi: DigiUsbDevice = DigiUsbDevice()

    member this.sendByte(data: byte) =
        data |> Digi.WriteByte |> ignore
        (Digi.ReadByte()).Value

    member this.sendBytes(data: seq<byte>) =
        data |> Digi.WriteBytes |> ignore
        Digi.ReadBytes()

    member this.sendString str =
        str |> Digi.WriteString |> ignore
        Digi.ReadString()

    member this.Manufacturer = Digi.Manufacturer   // 接続確認用
    member this.Product = Digi.Product             // 接続確認用

let myDigi: ChangeCaseDevice = ChangeCaseDevice()  // インスタンス作成

Ionide for F#がインストールされていればVSCodeのエディタ上で上記を全選択し、Alt + Enterで選択範囲を実行します。

以降は、Digisparkを「送信データを加工(大文字小文字の変換)して返すUSBデバイス」myDigiとして扱うことができます。

エディタ上で、上記スクリプトに次のコードを追加します。

DigiUSB_ChangeCase.fsx(追加分)
myDigi.UsbDevice.IsOpen  // UsbDeviceクラスのテスト
myDigi.Manufacturer      // 接続確認
myDigi.Product           // 接続確認
65uy |> myDigi.sendByte
[ 65uy; 66uy; 97uy; 98uy ] |> myDigi.sendBytes
"aBc123XyZ" |> myDigi.sendString

下の3行がループバックテストになります。
1バイト、バイト列、文字列データを送信すると受信結果を返します。

追加分について、エディタ上で1行ごとにAlt + Enterで実行します。

大文字小文字も変換されており、ループバックテストは成功です。
Digisparkがデータを受信するとボード上の赤色LEDが微かに点滅するので、目視でも動作が確認できます。

ちなみに、DigiUSBのバッファは128バイトです。
128バイト以上のデータを送った場合、127バイトを返しました。

NeoPixelの制御例

最後にNeoPixelを接続してみます。
ポイントはDigisparkの負担を軽くすることマイコン側の処理時間を考慮することです。

DigisparkとNeoPixelの接続

接続は冒頭の写真のとおりで、Digispark PB0 <---> NeoPixel DIN と GND <---> GND の2本です。
マイコンとNeoPixelの接続に関しては次の記事を一読することをおすすめします。

信号線には300~500Ωの抵抗挿入が推奨されていますが今回は省略しています。
また、NeoPixelの電源(+5V)は別電源にしています。

マイコン側

Digisparkに使われているATtiny85は低スペックの8ビットマイコンです。
ブートローダ(Micronucleus)とDigiUSBでかなりのリソースが消費されています。
NeoPixel用のライブラリにはリソース不足でビルドに失敗するものもありますが、FastLEDはビルドが通りました。
残りのリソースは少ないので、DigisparkにはNeoPixelを点灯させる仕事だけを担ってもらいます。

PlatformIO IDEからライブラリの検索・追加も容易にできます。
ライブラリを追加する様子です。
「Add to Project」をクリック後、目的のプロジェクトに追加します。

main.cpp
#include <Arduino.h>
#include <DigiUSB.h>
#include <FastLED.h>

#define LED_PIN 0 // To Neopixel DIN
#define NUM_LEDS 8
#define DATA_LEN (NUM_LEDS * 3)

union npdata {
  CRGB leds[NUM_LEDS];
  uint8_t usbdata[DATA_LEN];
} data;

uint8_t count;

void setup() {
  DigiUSB.begin();
  FastLED.addLeds<NEOPIXEL, LED_PIN>(data.leds, NUM_LEDS);
  count = 0;
}

void loop() {
  while (DigiUSB.available()) {
    data.usbdata[count++] = DigiUSB.read();
  }
  if (count >= DATA_LEN) {
    DigiUSB.refresh();  // 外すと不安定になる
    FastLED.show();
    count = 0;
  }
  DigiUSB.refresh();
}

24バイト受信したらそのまま点灯(FastLED.show)させるだけのスケッチになります。
注意点は、こまめにDigiUSB.refreshを呼ばないと接続が切れることです。
上のスケッチもコメントのとおりで、FastLED.showに時間がかかるので直前に一つ挿入しました。

以下はビルド時に表示されたリソース使用率です。

RAM:   [========= ]  90.8% (used 465 bytes from 512 bytes)
Flash: [========  ]  82.3% (used 4948 bytes from 6012 bytes)

参考までに16 LEDに変更(#define NUM_LEDS 16)した場合です。

RAM:   [==========]  95.5% (used 489 bytes from 512 bytes)
Flash: [========  ]  82.3% (used 4948 bytes from 6012 bytes)

RAMの使用量がきっちり8球分の24バイト増加しています。LEDを24球にするとRAMは100%超えになります。

ホスト側

ホスト側のスクリプトは、先のDigiUSB.fsxを使って24バイトを送信すればいいだけですが、使い勝手を考えて以下の機能を持つDigiLEDクラスを作成します。

  • インスタンス作成時の動作確認(1秒間のテスト点灯)
  • RGB各8バイトの配列を用意
  • 上記配列のクリア(配列を0埋めする)
  • 点灯(RGB配列データの送信)
  • 全消灯(0を24バイト送信)

点灯TurnOnと全消灯TurnOffにはFastLEDの処理時間(実質はNeoPixelへのデータ転送時間)を考慮してSystem.Threading.Thread.Sleep(1)を挿入し、待機時間を設けています。
これがないと連続した点灯指示に対してマイコン側の処理が追いつかなくなります。

DigiUSB_NeoPixel.fsx
#load "DigiUSB.fsx"
open DigiUSB
open System.Threading

type DigiLED() =
    let Device = DigiUsbDevice()

    do
        Array.create 24 7uy |> Device.WriteBytes |> ignore
        Thread.Sleep(1000)
        Array.zeroCreate<byte> 24 |> Device.WriteBytes |> ignore

    member val R = Array.zeroCreate<byte> 8 with get, set
    member val G = Array.zeroCreate<byte> 8 with get, set
    member val B = Array.zeroCreate<byte> 8 with get, set

    member this.TurnOff() =
        Array.zeroCreate<byte> 24
        |> Device.WriteBytes
        |> ignore
        Thread.Sleep(1)

    member this.TurnOn() =
        Array.zip3 this.R this.G this.B
        |> Seq.collect (fun (r: byte, g: byte, b: byte) -> [ r; g; b ])
        |> Device.WriteBytes
        |> ignore
        Thread.Sleep(1)

    member this.ClearR() = Array.fill this.R 0 8 0uy
    member this.ClearG() = Array.fill this.G 0 8 0uy
    member this.ClearB() = Array.fill this.B 0 8 0uy

    member this.Clear() =
        this.ClearR()
        this.ClearG()
        this.ClearB()

スクリプトを追加しましょう。

DigiUSB_NeoPixel.fsx(追加分)
let myLED: DigiLED = DigiLED()

上の行にカーソルを置いてAlt + EnterでデバイスのインスタンスmyLEDを作成します。
インスタンス作成時に動作確認を兼ねて、NeoPixelが1秒間弱く白色点灯します。
動作確認ができたらmyLEDを使って少し遊んでみます。

点灯例1

DigiUSB_NeoPixel.fsx(追加分)
// グラデーション表示(以下を全て選択し、Alt + Enter)
myLED.Clear()
myLED.R <- [| 2uy .. 2uy .. 16uy |]
myLED.G <- [| 2uy .. 2uy .. 16uy |] |> Array.rev
myLED.B <- Array.create 8 8uy
myLED.TurnOn()

グラデーション表示されました。

myLED.TurnOff()で消灯します。

DigiUSB_NeoPixel.fsx(追加分)
myLED.TurnOff()

点灯例2

用途に合わせた関数作成も自在です。
8球についてRGBごとにそれぞれ "0" "1" 文字列で記述し「8色 + 明るさ指定」で配列をセットする関数です。

DigiUSB_NeoPixel.fsx(追加分)
// 関数化(8色 + 明るさ指定)(以下を選択し、Alt + Enter)
let easy8colors (brightness: byte) ((r: string, g: string, b: string): string * string * string) =
    let setCells (bitstr: string) =
        bitstr
        |> Seq.map (string >> byte >> (*) brightness)
        |> Seq.toArray
    myLED.R <- setCells r
    myLED.G <- setCells g
    myLED.B <- setCells b

F#になじみのない方にはSeq.map (string >> byte >> (*) brightness)...の辺りがわかりにくいかと思います。

抜き出して適当な値を入れて実行した結果を示します。
ここでは文字列を重みのついたバイト配列に変換しています。

点灯させてみましょう。
3要素タプルの左からRGBで、R要素を中央4球、G要素を左4球、B要素を右4球に与えています。
末尾の10uyは明るさです。

DigiUSB_NeoPixel.fsx(追加分)
("00111100", "11110000", "00001111") |> easy8colors 10uy
myLED.TurnOn()

上記で点灯させたものが記事冒頭2枚目の写真です。(再掲)

点灯例3

最後は「ナイトライダー」っぽく流れる関数です。

DigiUSB_NeoPixel.fsx(追加分)
// 関数化(Knight Rider 風)(以下を選択し、Alt + Enter)
let knightRider() =
    let data: byte array array =
        "0, 0, 0, 0, 0, 0, 0, 0, 64, 32, 16, 8, 4, 2, 1, 0"
            .Split(',')
        |> Array.map byte
        |> Array.windowed 8
    myLED.Clear()
    for _ in 0..4 do
        for cells: byte array in data do
            myLED.R <- Array.copy cells
            myLED.G <- [| for i: byte in cells -> i / 2uy |]
            myLED.TurnOn()
            Thread.Sleep(5)
        myLED.TurnOff()
        Thread.Sleep(500)

関数の冒頭がわかりにくいのですが、配列の配列を作っています。
冒頭部分を抜き出して実行した結果を示します。

これを順番に表示させて、LEDが尾を引きながら流れる感じにしています。
このデータを赤と緑(値を1/2)にセットするとオレンジで光ります。

関数knightRiderを実行します。

DigiUSB_NeoPixel.fsx(追加分)
knightRider()

(左端LEDが点灯していませんが、動画をgifに変換した際にコマ落ちしたようです。実際は左端まで流れています。)

おわりに

蒸し返しになりますが、単に24バイトを送信するだけならusbdevice.pyを利用し、Pythonでホストを書いても十分です。
F#で書いてみたのはコレクション操作のバリエーションが豊富で、NeoPixelのパターンデータを作成しやすかった点にあります。
用途に応じて使い分けできればいいかと思います。

F#でもPythonでもREPLでマイコン操作(というよりはマイコンを介した周辺機器操作)ができると何かと便利です。
ただし、Digisparkは別にして、Arduinoの場合、通常はUSBシリアルで通信ができるので実務的にはそちらを選択する方が楽でしょう。

さて、タイトルに<Digispark編>と付けましたが、机の引き出しからはATmega32U4を使用したUSB直挿しマイコン("Pro Micro Beetle" とか "Arduino Leonardo Beetle" とかで検索すると引っかかるヤツ)も出てきました。

こちらは、 USB CDC(シリアル) に加えて、USB Raw HID でも通信できるので、あらためて記事にする予定です。

左:Digispark クローン品  右:ATmega32U4 "Pro Micro Beetle"(共に製造元不明)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?