はじめに
引き出しの整理をしていたら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
ライブラリで簡単に済ませます。
#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
型にして成否の判断ができるようにしています。
#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
を作成します。
#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
として扱うことができます。
エディタ上で、上記スクリプトに次のコードを追加します。
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」をクリック後、目的のプロジェクトに追加します。
#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)
を挿入し、待機時間を設けています。
これがないと連続した点灯指示に対してマイコン側の処理が追いつかなくなります。
#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()
スクリプトを追加しましょう。
let myLED: DigiLED = DigiLED()
上の行にカーソルを置いてAlt + Enter
でデバイスのインスタンスmyLED
を作成します。
インスタンス作成時に動作確認を兼ねて、NeoPixelが1秒間弱く白色点灯します。
動作確認ができたらmyLED
を使って少し遊んでみます。
点灯例1
// グラデーション表示(以下を全て選択し、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()
で消灯します。
myLED.TurnOff()
点灯例2
用途に合わせた関数作成も自在です。
8球についてRGBごとにそれぞれ "0" "1" 文字列で記述し「8色 + 明るさ指定」で配列をセットする関数です。
// 関数化(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
は明るさです。
("00111100", "11110000", "00001111") |> easy8colors 10uy
myLED.TurnOn()
点灯例3
最後は「ナイトライダー」っぽく流れる関数です。
// 関数化(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
を実行します。
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"(共に製造元不明)