Raspberry Pi Picoで動的ライティングに対応したUSBデバイスを開発する
Make a USB device for dynamic lighting with Raspberry Pi Pico
ゲーミングデバイスではおなじみのフルカラーで光るLED。Windows 11では「動的ライティング」として、OSから直接制御できるようになっています。Windowsの動的ライティング機能は基本的に「Lighting and Illumination」というUSBデバイスを光らせるための規格に基づいているため、この規格に則ったデバイスをプログラミングすることで、自作のライティングデバイスやLEDコントローラーを制作することができます。
今回はRapberry Pi Pico(RP2040搭載ボード)とArduinoを使って、オリジナルのLEDコントローラーを開発するための手はずをご紹介します。
まずは開発環境の構築から。Visual Studio Codeにマイコン開発環境のPlatformIOをインストールし、新規プロジェクトを作成します。この記事では「
YD-RP2040」という互換ボードを使って開発しているため、Boardの項目は「RP2040(Generic)」を指定しています。
初期状態ではPlatformIOによるArduino環境が導入されますが、この環境は所定のUSBライブラリーとの相性が悪いので、「platformio.ini」ファイルの当該部分を次のように編集して、Arduino-Picoを導入できるようにします。
platformio_1.ini
[env:generic]
platform = https://github.com/maxgerhardt/platform-raspberrypi.git
board = pico
framework = arduino
board_build.core = earlephilhower
続いてiniファイルに追加の定義を記述します。
platformio_2.ini
board_build.f_cpu = 48000000L
lib_deps =
adafruit/Adafruit NeoPixel@^1.12.3
adafruit/Adafruit TinyUSB Library@3.3.4
build_flags=
-DUSE_TINYUSB
-UUSE_TINYUSB_HOST
-DUSBD_HID_OUT_SUPPORT=1
-DCFG_TUH_ENABLED=0
-DCFG_TUD_VIDEO=0
-DCFG_TUD_MSC=0
-DCFG_TUD_MIDI=0
monitor_speed=115200
lib_depsではプログラマブルLED向けのライブラリーとTinyUSBの拡張ライブラリーを指定しています。また、build_flagsではRaspberry Pi Pico公式のTinyUSBライブラリーの定義と、不要なUSBライブラリーの除外が定義されています。monitor_speedではシリアル通信の速度を115200bpsに、board_build.f_cpuではCPUの動作周波数を48MHzにしていますが、これらはお好みの数値に変更しても構いません。
ここからは
Githubに公開しているソースコードの要点を抜粋して解説します。
desc.c
uint8_t const desc_lighting_report[] = {
TUD_HID_REPORT_DESC_LIGHTING(0x01)
};
「Lighting and Illumination」として機能するためのUSBデバイス記述子(descriptor)はTinyUSBにあらかじめ収録されているので、そのデータを取り込んでいます。
lamp.c
#define LAMP_PIN 23
#define LAMP_COUNT 1
#define NEO_PIXEL_TYPE (NEO_GRB + NEO_KHZ800)
YD-RP2040にはフルカラーLED(WS2812B)がGPIO23(LAMP_PIN=23)に繋がっているので、このLED1個(LAMP_COUNT=1)を点灯対象にしています。
size.c
// デバイスの物理的な大きさ
#define BOUND_WIDTH_MM 23
#define BOUND_HEIGHT_MM 53
#define BOUND_DEPTH_MM 2
// ランプが遅延するミリ秒
#define LAMP_UPDATE_LATENCY (0x04)
デバイスの物理的な情報の定義です。ホストに渡す値はマイクロミニメートルですが、細かい数値を指定してもあまり意味がないので、ここでは開発ボード自体のサイズをミリメートル(23x53mm)で指定しています。ライトが更新されるまでの遅延時間は適当です。
initusb.c
TinyUSBDevice.setManufacturerDescriptor("TNK Software");
TinyUSBDevice.setProductDescriptor("Pico Led Controller");
if (!TinyUSBDevice.isInitialized()) {
TinyUSBDevice.begin(0);
}
usb_hid.enableOutEndpoint(true);
usb_hid.setPollInterval(2);
usb_hid.setReportDescriptor(desc_lighting_report, sizeof(desc_lighting_report));
usb_hid.setReportCallback(OnGetReport, OnSetReport);
usb_hid.begin();
if (TinyUSBDevice.mounted()) {
TinyUSBDevice.detach();
delay(10);
TinyUSBDevice.attach();
}
USBの初期設定で「setManufacturerDescriptor」と「setProductDescriptor」に任意の文字列を入れると、その文字列がWindowsの製造者名とデバイス名として反映されます。
setReportCallbackにコールバック関数を指定することで、ホストから要求された処理に応えることができるようになります。OnGetReportでは返信に必要なリポートIDに応じたデータをデータバッファにコピーし、その書き込んだデータサイズを返り値とします。OnSetReportではリポートIDに対応した処理をマイコンで実行させます。割り込み処理で複雑な処理を行うのは好ましくないため、処理量が多くなりそうであればloop()内に記述するようにしましょう。
sendlamparray.c
uint16_t SendLampArrayAttributesReport(LampArrayAttributesReport *report)
{
// 物理情報を返す
report->LampCount = LAMP_COUNT;
report->BoundingBoxWidthInMicrometers = MILLIMETERS_TO_MICROMETERS(BOUND_WIDTH_MM);
report->BoundingBoxHeightInMicrometers = MILLIMETERS_TO_MICROMETERS(BOUND_HEIGHT_MM);
report->BoundingBoxDepthInMicrometers = MILLIMETERS_TO_MICROMETERS(BOUND_DEPTH_MM);
report->LampArrayKind = LampArrayKindPeripheral;
report->MinUpdateIntervalInMicroseconds = MILLISECONDS_TO_MICROSECONDS(33);
return sizeof(LampArrayAttributesReport);
}
USBデバイスとして認識されると、はじめにホストからデバイスの物理情報が求められるので、値を埋めていきます。LampArrayKindで指定した値は、Windowsではその値に応じたアイコンに変化します。
lampid.c
void UpdateLampAttributes(LampAttributesRequestReport *report) noexcept
{
// 対象となるLampId(0~LampCount-1)の送信。無効なLampIdは0として処理する
lastLampIdRequested = (report->LampId < LAMP_COUNT) ? report->LampId : 0;
}
ホストからLED(Lamp)に後述の処理で返すべき最初のIDが返されます。
sendlamp.c
uint16_t SendLampAttributesReport(LampAttributesResponseReport *report) noexcept
{
report->Attributes.LampId = lastLampIdRequested;
report->Attributes.PositionXInMicrometers = MILLIMETERS_TO_MICROMETERS(BOUND_WIDTH_MM / 2);
report->Attributes.PositionYInMicrometers = MILLIMETERS_TO_MICROMETERS(48);
report->Attributes.PositionZInMicrometers = MILLIMETERS_TO_MICROMETERS(0);
report->Attributes.UpdateLatencyInMicroseconds = MILLISECONDS_TO_MICROSECONDS(LAMP_UPDATE_LATENCY);
report->Attributes.LampPurposes = LampPurposeAccent;
report->Attributes.RedLevelCount = 0xFF;
report->Attributes.GreenLevelCount = 0xFF;
report->Attributes.BlueLevelCount = 0xFF;
report->Attributes.IntensityLevelCount = 0x01;
report->Attributes.IsProgrammable = LAMP_IS_PROGRAMMABLE;
report->Attributes.InputBinding = 0x00;
lastLampIdRequested++;
if (lastLampIdRequested >= LAMP_COUNT) lastLampIdRequested = 0; // Reset
return sizeof(LampAttributesResponseReport);
}
次にホストからは個別のLED(Lamp)に関する情報を提供するように促されます。今回はLEDはひとつしかありませんが、複数ある場合、最終的には(0~総数-1)のIDを持つすべてのLEDを定義する必要があります。その際はホスト・デバイスともに無駄な処理を避けるためにも、連番で処理することが望ましいです。
autonomous.c
void UpdateArrayControl(LampArrayControlReport *report) noexcept
{
is_autonomous = !!report->AutonomousMode;
}
ホストから新しいLEDの発光状態が送信される際には、AutonomousMode=0x00が送られてきます。
update.c
// 複数のランプを一度に更新
void UpdateMultipleLamp(LampMultiUpdateReport *report) noexcept
{
for (uint8_t i = 0; i < report->LampCount; i++){
if (report->LampIds[i] < TOTAL_LED_COUNT) {
LampArrayColor *c = &report->UpdateColors[i];
uint32_t pxcolor = leds.Color(c->RedChannel, c->GreenChannel, c->BlueChannel);
setPixelColor(report->LampIds[i], pxcolor);
}
}
// ホストから送られるデータが最後ならこのフラグが立ち、次に「LampArrayControlReport(AutonomousMode: enabled)」が送信されることになる
if (report->LampUpdateFlags & LAMP_UPDATE_FLAG_UPDATE_COMPLETE) is_completed = true;
}
// 2つのIDの範囲内のランプを更新
void UpdateRangeLamp(LampRangeUpdateReport *report) noexcept
{
if (report->LampIdStart >= 0 && report->LampIdStart < TOTAL_LED_COUNT &&
report->LampIdEnd >= 0 && report->LampIdEnd < TOTAL_LED_COUNT &&
report->LampIdStart <= report->LampIdEnd)
{
for (uint8_t i = report->LampIdStart; i <= report->LampIdEnd; i++) {
uint32_t pxcolor = leds.Color(report->UpdateColor.RedChannel, report->UpdateColor.GreenChannel, report->UpdateColor.BlueChannel);
setPixelColor(i, pxcolor);
}
}
// UpdateMultipleLampと同様、ホストから送られるデータが最後ならこのフラグが立つ
if (report->LampUpdateFlags & LAMP_UPDATE_FLAG_UPDATE_COMPLETE) is_completed = true;
}
設定が完了すると、以降はどのLEDを更新すべきかの命令が逐次届くようになります。この2つのどちらが届くかはホスト側の処理に依存します。いずれにしても、更新すべきLEDの情報がすべて届いたのであれば、LampUpdateFlagsの値が0x01になります。
最後に、AutonomousMode=0x01が送られてくるので、このタイミングでデバイスで出力処理を行います。
suspend.c
void tud_suspend_cb(bool remote_wakeup_en)
{
// 消灯
neoPixelShield.clear();
neoPixelShield.show();
__wfi(); // Pythonのlightsleep()と同じ
}
パソコンがスリープ状態になるなどしてUSBが休止状態になるとTinyUSBよりこの関数が呼び出されます。必要に応じてLEDを消灯するなどの休止処理を記述します。
プログラムのビルドと書き込みが正常に完了すれば、Raspberry Pi PicoはWindowsの動的ライティング設定からは、USB HIDデバイスとして認識されるようになります。
2024/12/18