momo が 2026年02月19日12時10分45秒 に編集
コメント無し
本文の変更
# はじめに USBで接続する簡単なディスプレイを作りました。ディスプレイといっても128x64ピクセルの小さなOLED([SSD1306](https://akizukidenshi.com/catalog/g/g112031/))です。制御マイコンも数十円の[CH32V003J4M6](https://akizukidenshi.com/catalog/g/g118062/)です(物価高で値上がりしましたがそれでも50円くらい:2026/02/01現在秋月価格)。 ホストPC側ではドライバは不要で簡単にアプリが作れるので、ちょっとしたインフォメーション表示など応用範囲は広いかと思います。  # システム構成 ホストPCとマイコンはUSB1.1で接続しHIDクラスを使ってコマンド+表示データを送ります。マイコンは、そのコマンドを解釈してしOLED用の表示コマンドに変換します。変換されたコマンドはOLEDにI2Cで送信されます。 表示する画像はホストPC側で生成して、デバイスが側では表示制御だけに特化するシステムにしています。それにより、貧弱なマイコン&少ない部品でいろいろな画像が素早く表示できるデバイスになっています。ホスト側ががんばればアニメーションも表示できます。 :::plantuml @startuml node host [ ホストPC\n - Win\n - Linux\n - Ras-P ...etc ] rectangle "USB2OLED" { node cpu [ マイコン\n(CH32V003) ] node oled [ ディスプレイ\n(SSD1306) ] } host =r= cpu: USB 1.1\nHID Class cpu =r= oled: I2C @enduml ::: # 回路構成 主要な部品は激安マイコンの[CH32V003J4M6](https://akizukidenshi.com/catalog/g/g118062/)と[SSD1306](https://akizukidenshi.com/catalog/g/g112031/)くらいです。 3.3Vに降圧するレギュレータ(AMS1117)は、もっと入手しやすいドロップが小さいモノに変更した方がいいかもしれません、もしくは、CH32V003とSSD1306は5Vでも動作するので、そのまま直結しても動作すると思われます。ただし、その場合はI2Cライン(SCL/SDA)は分圧などして3.3Vにする必要があります。あと、パスコンのコンデンサ容量はあまり詳しくないので適当です...。無くても動くかも。 I2Cラインは、SSD1306モージュール側でプルアップされているようなので別途プルアップ抵抗は必要ないようです。 CH32V003の`pin:8 (SWIO)`はでライタ(WCH-LinkE)接続用に開けています。`pin:7`はPowerOn→Bootloader起動→FW起動した時にUSBバスを再接続するためにD-ラインをリセットする為に使っています。Bootloaderを使わない場合はプルアップ固定してしておけば`pin:7`を他の用途でも使えると思います。 Type-cを使う場合はC1/C2端子を5.1kΩでプルダウンしてください。USBコネクタ基板によってはプルダウン抵抗が既に付いているモノもあるようです。  ユニバーサル基板&チップ抵抗を使って手半田で実装したので裏は結構ごちゃごちゃ...。SSD1306は取り外しできるようにピンヘッダを介して接続。CH32V003J4M6は失敗しても安いので気楽に直付け。(でもマイコンチップよりブレイクアウト基板の方が高価だったりする)  裏側。  動いたのでとりあえずヨシ!  ### ケースの作成 3Dプリンタでケースも作成して基板を収めました。実はこれが一番時間がかかった...。  裏側、  試行錯誤の残骸。  # デバイス側のファームウェア ### 開発環境 ビルド環境は[ch32fun](https://github.com/cnlohr/ch32fun) を使うので、とりあえず */examples/blink* とかで makeできるようにセットアップしてください。ちなみに、私は **Windows11 + WSL2** で環境構築しました。
CH32V003は激安すぎてUSBコントローラ回路は搭載されていないので全てソフトウェアでUSB制御を実装します。ライブラリは [rv003usb](https://github.com/cnlohr/rv003usb) を利用します。Bootloaderもあるので一度書き込んでしまえば以降はライタ不要でこの回路のUSB経由でFirmwareのアップデートができるので便利です。その際は、BootloaderのソースコードのUSB端子(D+、D-)設定を、この回路構成と同じに変更しておく必要があります。
CH32V003は激安すぎてUSBコントローラ回路は搭載されていないので全てソフトウェアでUSB制御を実装します。ライブラリは [rv003usb](https://github.com/cnlohr/rv003usb) を利用します。Bootloaderもあるので一度書き込んでしまえば以降はライタ不要でこの回路のUSB経由でFirmwareアップデートできるので便利です。その際は、BootloaderのソースコードのUSB端子(D+、D-)設定を、この回路構成と同じに変更しておく必要があります。
### ソースコード 新たに作るが必要があるファイルは下記の4つだけです。 - Makefile - usb2oled.c - funconfig.h - usb_config.h Makefile内のパス設定を環境に合わせて変更すれば、どんな構成でもよいですが、参考としてフォルダ構成を載せておきます。*/tools/* に*ch32fun* と *rv003usb* を git cloneしています。 ```:開発フォルダの構成(参考) ./ ├── host/ ├── src/ │ └─ usb2oled/ │ ├── Makefile │ ├── usb2oled.c │ ├── funconfig.h │ └── usb_config.h └── tools ├── ch32fun/ └── rv003usb/ ``` */src/usb2oled/* で、`make`すればビルドできます。`make flash`で書き込み。`make debug`でデバッグ実行(printfの表示など) できるようにしています。 ```shell:Makefile TARGET:=usb2oled TARGET_MCU?=CH32V003 PATH_TOOLS:=../../tools PATH_CH32FUN:=$(PATH_TOOLS)/ch32fun/ch32fun DEBUGER:=$(PATH_TOOLS)/ch32fun/minichlink/minichlink RV003USB:=$(PATH_TOOLS)/rv003usb ADDITIONAL_C_FILES+=$(RV003USB)/rv003usb/rv003usb.S $(RV003USB)/rv003usb/rv003usb.c EXTRA_CFLAGS:=-I$(RV003USB)/lib -I$(RV003USB)/rv003usb include $(PATH_CH32FUN)/ch32fun.mk all : $(TARGET).bin flash : cv_flash clean : cv_clean debug : $(DEBUGER) -b -T ``` 起動時のロゴ表示などはファームウェアで行っていますが、それ以降の描画制御はUSB経由で受信したデータを [ch32fun](https://github.com/cnlohr/ch32fun)のSSD1306用の関数に変換して渡しているだけです。**ch32fun::SSD1306のAPI**には線や円の描画、円や矩形の塗りつぶしなどの関数がありますが、この貧弱なマイコンではパフォーマンス的に許容できないと考え実装していません。このシステムの思想としては、ホスト側で画像データ(フレームバッファの内容)を作成して、デバイス側では、それをOLEDに転送するだけに徹する事にしました。但し、フォントデータの扱いは面倒なのでデバイス側で処理して欲しい。なので、テキスト描画は**ch32fun::ssd1306_drawstr_sz**を利用しています。 ```c:usb2oled.c #include <stdio.h> #include <string.h> #include "ch32fun.h" #include "rv003usb.h" #include "ch32v003_GPIO_branchless.h" #define SSD1306_128X64 #include "ssd1306_i2c.h" #include "ssd1306.h" #define min(a, b) ((a) < (b) ? (a) : (b)) #define VER "0.0.5" #define ICON_W 112 #define ICON_H 32 const unsigned char IconBmp[] = { 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x27, 0x80, 0xE8, 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x27, 0x20, 0xFE, 0xFF, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF0, 0xFF, 0xE7, 0x07, 0x8F, 0x0F, 0x08, 0xF8, 0x00, 0x00, 0xF0, 0x1F, 0x00, 0x00, 0xF0, 0xFF, 0xE7, 0x03, 0x8E, 0x0F, 0x08, 0xF0, 0xF8, 0xFF, 0xE0, 0x8F, 0xFF, 0xFF, 0x03, 0xE0, 0xE7, 0x03, 0x8E, 0x0F, 0x88, 0xE1, 0xFC, 0xFF, 0xE3, 0xC7, 0x00, 0x00, 0xF2, 0xEF, 0xE7, 0x71, 0x8C, 0x8F, 0x8F, 0xE3, 0xFC, 0xFF, 0xE0, 0x63, 0x00, 0x00, 0xF2, 0xEF, 0xE7, 0xF9, 0x8C, 0x8F, 0x8F, 0xE3, 0xFC, 0x03, 0xF0, 0x23, 0x00, 0x00, 0x82, 0xEF, 0xE7, 0xF9, 0x8C, 0x0F, 0x8C, 0xE3, 0xFC, 0x00, 0xF8, 0x31, 0x00, 0x00, 0xB2, 0xEF, 0xE7, 0xF9, 0x8C, 0x0F, 0x8C, 0xE3, 0x7C, 0xF0, 0xFF, 0x11, 0x00, 0x00, 0xB2, 0xEF, 0xE7, 0xF9, 0x8C, 0x8F, 0x8F, 0xE3, 0x7C, 0xF8, 0xFF, 0x19, 0x00, 0x00, 0xB2, 0xEF, 0xE7, 0x71, 0x8C, 0x8F, 0x8F, 0xE3, 0x7C, 0x00, 0x00, 0x18, 0x00, 0x00, 0xB2, 0xEF, 0xE7, 0x01, 0x0E, 0x08, 0x88, 0xE1, 0xFC, 0x00, 0x00, 0x18, 0x00, 0x00, 0xB2, 0xEF, 0xE7, 0x03, 0x0E, 0x08, 0x08, 0xF0, 0xFC, 0x01, 0x00, 0x18, 0x00, 0x00, 0xB2, 0xEF, 0xE7, 0x07, 0x0F, 0x08, 0x08, 0xFC, 0xFC, 0xFF, 0xFF, 0x11, 0x00, 0x00, 0xB2, 0xEF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0x33, 0x00, 0x00, 0x82, 0xEF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0x63, 0x00, 0x00, 0xF2, 0xEF, 0xE7, 0xFF, 0xFF, 0xFF, 0x3F, 0x84, 0xFC, 0xFF, 0xFF, 0xC7, 0x00, 0x00, 0xF2, 0xEF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0x03, 0xE0, 0xE7, 0xFF, 0xFF, 0xFF, 0x17, 0x81, 0xFC, 0xFF, 0xFF, 0x1F, 0x00, 0x00, 0xF0, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0xF0, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xF9, 0xFF, 0xE4, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }; uint8_t CmdBuf[128 + 4]; volatile uint8_t FlgRcv = 0; void usb_setup_upd() { #define PIN_USB_DPU GPIOv_from_PORT_PIN(GPIO_port_C, 4) // PC4 GPIO_port_enable(GPIO_port_C); GPIO_pinMode(PIN_USB_DPU, GPIO_pinMode_O_pushPull, GPIO_Speed_10MHz); GPIO_digitalWrite_hi(PIN_USB_DPU); } void oled_setup(void) { Delay_Ms(100); // Wait for OLED setup time if(ssd1306_i2c_init()) { while(1); // I2C initialization failed. } ssd1306_init(); ssd1306_setbuf(0); ssd1306_drawFastHLine(0, 0, SSD1306_W, 1); ssd1306_drawFastHLine(0, SSD1306_H - 1, SSD1306_W, 1); ssd1306_drawFastVLine(0, 0, SSD1306_H, 1); ssd1306_drawFastVLine(SSD1306_W - 1, 0, SSD1306_H, 1); ssd1306_drawstr_sz(0, 4, "USB2OLED", 1, fontsize_16x16); // 16 ssd1306_drawstr_sz(22,20, "ver. " VER, 1, fontsize_8x8); // 36 ssd1306_drawImage(8, 30, IconBmp, ICON_W, ICON_H, 1); ssd1306_refresh(); } void oled_main(uint8_t dat[]) { uint32_t x, y, w, h; switch (dat[0]) { case 0xa0: // clear ssd1306_setbuf(0); break; case 0xa1: // bitblt x = min(((dat[1] >> 0) & 0xf) * 8, SSD1306_W - 1); y = min(((dat[1] >> 4) & 0xf) * 8, SSD1306_H - 1); w = min((((dat[2] >> 0) & 0xf) + 1) * 8, SSD1306_W); // 0:8pix, 1:16pixel, ... 15:128pix h = min((((dat[2] >> 4) & 0xf) + 1) * 8, SSD1306_H); // 0:8pix, 1:16pixel, ... 7:64pix ssd1306_drawImage(x, y, &dat[4], w, h, 0); break; case 0xa2: // text char str[16 + 1] = {"\0"}; x = min(((dat[1] >> 0) & 0xf) * 8, SSD1306_W - 1); y = min(((dat[1] >> 4) & 0xf) * 8, SSD1306_H - 1); uint32_t font = (1 << (dat[2] & 0x3)); // 0:8x8(1), 1:16x16(2), 2:32x32(4), 3:64x64(8) uint32_t len = min(dat[3], sizeof(str) - 1); memcpy(str, &dat[4], len); ssd1306_drawstr_sz(x, y, str, 1, font); break; case 0xaf: // refresh ssd1306_refresh(); break; default: break; } dat[0] = 0x00; } int main() { SystemInit(); // Initialize ch32fun library Delay_Ms(1); // Ensures USB re-enumeration after bootloader or reset; Spec demand >2.5µs ( TDDIS ) usb_setup(); // Initialize rv003usb library usb_setup_upd(); // Reset USB Port oled_setup(); // Initialize OLED while(1) { if(0 != FlgRcv) { oled_main(CmdBuf); // Uodate OLED FlgRcv = 0; } } } void usb_handle_user_data(struct usb_endpoint * e, int current_endpoint, uint8_t * data, int len, struct rv003usb_internal * ist ) { int offset = e->count; int torx = e->max_len - offset; // Remaining bytes that can be received (len = packet size) torx = min(torx, len); memcpy(&CmdBuf[offset], data, torx); e->count += torx; if (e->count >= e->max_len) { FlgRcv = 1; } } void usb_handle_hid_get_report_start(struct usb_endpoint * e, int reqLen, uint32_t lValueLSBIndexMSB ) { e->opaque = CmdBuf; e->max_len = min(reqLen, sizeof(CmdBuf)); e->count = 0; } void usb_handle_hid_set_report_start(struct usb_endpoint * e, int reqLen, uint32_t lValueLSBIndexMSB ) { e->max_len = min(reqLen, sizeof(CmdBuf)); e->count = 0; } ``` [rv003usb](https://github.com/cnlohr/rv003usb)のサンプルコードをベースにしています。デバッグが必要なければ*FUNCONF_USE_DEBUGPRINTF* は不要かも。 ```c:funconfig.h #ifndef _FUNCONFIG_H #define _FUNCONFIG_H #define FUNCONF_USE_DEBUGPRINTF 1 #define CH32V003 1 #define FUNCONF_SYSTICK_USE_HCLK 1 #endif ``` [rv003usb](https://github.com/cnlohr/rv003usb)のサンプルコードをベースにしています。**HID_FEATURE**構造を使ってホストPC⇔デバイス間のコマンドフォーマットを規定しています。(USBは初めてだったので、このディスクリプタの構造を理解するのが難しかった...。) 今回は4つのコマンドを実装しています。 - CLEAR: フレームバッファを全消去 - BITBLT: 画像データをフレームバッファに転送 - TEXT: テキストをフレームバッファに転送 - REFRESH: フレームバッファの内容を表示 ```c:usb_config.h #ifndef _USB_CONFIG_H #define _USB_CONFIG_H //Defines the number of endpoints for this device. (Always add one for EP0). For two EPs, this should be 3. #define ENDPOINTS 2 #define USB_PORT A // [A,C,D] GPIO Port to use with D+, D- and DPU #define USB_PIN_DP 2 // [0-4] GPIO Number for USB D+ Pin #define USB_PIN_DM 1 // [0-4] GPIO Number for USB D- Pin #define RV003USB_DEBUG_TIMING 0 #define RV003USB_OPTIMIZE_FLASH 1 #define RV003USB_EVENT_DEBUGGING 0 #define RV003USB_HANDLE_IN_REQUEST 0 #define RV003USB_OTHER_CONTROL 0 #define RV003USB_HANDLE_USER_DATA 1 #define RV003USB_HID_FEATURES 1 #ifndef __ASSEMBLER__ #include <tinyusb_hid.h> #ifdef INSTANCE_DESCRIPTORS //Taken from http://www.usbmadesimple.co.uk/ums_ms_desc_dev.htm static const uint8_t device_descriptor[] = { 18, //Length 1, //Type (Device) 0x10, 0x01, //Spec 0x0, //Device Class 0x0, //Device Subclass 0x0, //Device Protocol (000 = use config descriptor) 0x08, //Max packet size for EP0 (This has to be 8 because of the USB Low-Speed Standard) 0x09, 0x12, //ID Vendor 0x11, 0x11, //ID Product 0x02, 0x00, //ID Rev 1, //Manufacturer string 2, //Product string 3, //Serial string 1, //Max number of configurations }; static const uint8_t special_hid_desc[] = { HID_USAGE_PAGE_N( HID_USAGE_PAGE_VENDOR, 2 ), // ベンダー固有 HID_USAGE ( 0x00 ), HID_REPORT_SIZE ( 8 ), HID_COLLECTION ( HID_COLLECTION_LOGICAL ), // CMD: Clear screen HID_REPORT_COUNT ( 4 ), HID_REPORT_ID ( 0xa0 ) HID_USAGE ( 0x01 ), HID_FEATURE ( HID_DATA | HID_VARIABLE | HID_ABSOLUTE ) , // CMD: Bitblt image HID_REPORT_COUNT ( 4 + 128), HID_REPORT_ID ( 0xa1 ) HID_USAGE ( 0x01 ), HID_FEATURE ( HID_DATA | HID_VARIABLE | HID_ABSOLUTE ) , // CMD: Write test HID_REPORT_COUNT ( 4 + 16), HID_REPORT_ID ( 0xa2 ) HID_USAGE ( 0x01 ), HID_FEATURE ( HID_DATA | HID_VARIABLE | HID_ABSOLUTE ) , // CMD: Refresh screen HID_REPORT_COUNT ( 4 ), HID_REPORT_ID ( 0xaf ) HID_USAGE ( 0x01 ), HID_FEATURE ( HID_DATA | HID_VARIABLE | HID_ABSOLUTE ) , HID_COLLECTION_END, }; static const uint8_t config_descriptor[] = { // configuration descriptor 9, // bLength; 2, // bDescriptorType; 34, 0x00, // wTotalLength (9+9+9+7) 0x01, // bNumInterfaces (Normally 1) 0x01, // bConfigurationValue 0x00, // iConfiguration 0x80, // bmAttributes (was 0xa0) 0x64, // bMaxPower (200mA) //Joystick (It is unusual that this would be here) 9, // bLength 4, // bDescriptorType 0, // bInterfaceNumber = 1 instead of 0 -- well make it second. 0, // bAlternateSetting 1, // bNumEndpoints 0x03, // bInterfaceClass (0x03 = HID) 0x00, // bInterfaceSubClass 0xff, // bInterfaceProtocol 0, // iInterface 9, // bLength 0x21, // bDescriptorType (HID) 0x10,0x01, // bcd 1.1 0x00, //country code 0x01, // Num descriptors 0x22, // DescriptorType[0] (HID) sizeof(special_hid_desc), 0x00, 7, // endpoint descriptor (For endpoint 1) 0x05, // Endpoint Descriptor (Must be 5) 0x81, // Endpoint Address 0x03, // Attributes 0x08, 0x00, // wMaxPacketSize (8 bytes) 100, // Interval (We don't use it.) }; #define STR_MANUFACTURER u"serebent lab." #define STR_PRODUCT u"USB2OLED(powered by rv003usb)" #define STR_SERIAL u"20260201" struct usb_string_descriptor_struct { uint8_t bLength; uint8_t bDescriptorType; uint16_t wString[]; }; const static struct usb_string_descriptor_struct string0 __attribute__((section(".rodata"))) = { 4, 3, {0x0409} }; const static struct usb_string_descriptor_struct string1 __attribute__((section(".rodata"))) = { sizeof(STR_MANUFACTURER), 3, STR_MANUFACTURER }; const static struct usb_string_descriptor_struct string2 __attribute__((section(".rodata"))) = { sizeof(STR_PRODUCT), 3, STR_PRODUCT }; const static struct usb_string_descriptor_struct string3 __attribute__((section(".rodata"))) = { sizeof(STR_SERIAL), 3, STR_SERIAL }; // This table defines which descriptor data is sent for each specific // request from the host (in wValue and wIndex). const static struct descriptor_list_struct { uint32_t lIndexValue; const uint8_t *addr; uint8_t length; } descriptor_list[] = { {0x00000100, device_descriptor, sizeof(device_descriptor)}, {0x00000200, config_descriptor, sizeof(config_descriptor)}, {0x00002200, special_hid_desc, sizeof(special_hid_desc)}, {0x00002100, config_descriptor + 18, 9 }, // Not sure why, this seems to be useful for Windows + Android. {0x00000300, (const uint8_t *)&string0, 4}, {0x04090301, (const uint8_t *)&string1, sizeof(STR_MANUFACTURER)}, {0x04090302, (const uint8_t *)&string2, sizeof(STR_PRODUCT)}, {0x04090303, (const uint8_t *)&string3, sizeof(STR_SERIAL)} }; #define DESCRIPTOR_LIST_ENTRIES ((sizeof(descriptor_list))/(sizeof(struct descriptor_list_struct)) ) #endif // INSTANCE_DESCRIPTORS #endif #endif ``` ++**ch32funのの不具合** [ch32fun](https://github.com/cnlohr/ch32fun)の`/ch32fun/extralibs/ssd1306.h`に不具合があるようです。[issue:249](https://github.com/cnlohr/ch32fun/issues/249)参照。 ssd1306_drawImage()を下記の様に変更しないと正常に表示できません。++ ```c:ssd1306.h - 301: x_absolute = x + 8 * (bytes_to_draw - byte) + pixel; + 301: x_absolute = x + 8 * byte + pixel; ``` # ホスト側のソフトウェア マウスやキーボードの接続で使われているHIDクラスを利用するので基本的にドライバなどは不要です。アプリ層だけ作ればよいのでホスト側の実装は比較的簡単です。 USBデバイスIDは`VID:0x1209` ` PID:0x1111`にしていますので、ホスト側では、このIDのデバイスを探してアクセスします。 コマンドの仕様はソースコードを参照してください。
プログラム環境としては、USB HIDクラスでデータ通信ができれば何でもよいですが、Python で`hidapi`ライブラリを使うのが一番お手軽かと思いましたので今回はこれで作りました。 import する時は `hid` だけど install は `hidapi`なので注意。'pip install hid' とすると別のライブラリがインストールされてしまいます。
プログラム環境としては、USB HIDクラスでデータ通信ができれば何でもよいですが、Python で`hidapi`ライブラリを使うのが一番お手軽かと思いましたので今回はこれで作りました。但し、import する時は `hid` だけど install は `hidapi`なので注意。'pip install hid' とすると別のライブラリがインストールされてしまいます。
```shell:pip-install pip insatll hidapi ``` # デモ ホストPCと接続すると先ずBootloaderに接続されます(Bootloaderを書き込んでいる場合)。Firmwareを更新する際はこのタイミングで書き込みます。数秒後、自動的に再接続されUSB2OLEDに切り替わり初期画面が表示されます。その後に**usb2oled.py**を実行した様子です。動確用に"Hello World !"とチェックカーパターンが表示するようにしています。
基本的な制御を使いまわし易いようにUsb2Oled Classを実装しましたので、これをImportすると簡単に扱えると思います。
基本的な制御を使いまわし易いようにUsb2Oled Classを実装しましたので、これをImportして使うと簡単に扱えると思います。
```python:usb2oled.py import hid import time # libusbのセットアップ # sudo apt install libusb-1.0-0-dev libudev-dev pkg-config # pip insatll hidapi # # *Note: import は 'hid' だけどinsatll は 'hidapi' # Linux では、USB HID デバイスはデフォルトで root 権限が必要 # sudo vim /etc/udev/rules.d/99-usb2oled.rules # 下記内容を書き込む # SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="1111", MODE="0666" # sudo udevadm control --reload-rules # sudo udevadm trigger # ルールを適用するために対象デバイスを一度抜き差しする class Usb2Oled: WIDTH = 128 HEIGHT = 64 PAGE_SIZE = WIDTH * 8 // 8 def __init__(self, vid=0x1209, pid=0x1111): self.dev = None self.vid = vid self.pid = pid def open(self): try: self.dev = hid.device() self.dev.open(self.vid, self.pid) except Exception as e: raise RuntimeError(f"Failed to open HID device: {e}") def close(self): if self.dev is not None: self.dev.close() self.dev = None @staticmethod def hid_list(): for d in hid.enumerate(): print(f"0x{d['vendor_id']:04x} - 0x{d['product_id']:04x} : {d['product_string']} : {d['serial_number']} : {d['manufacturer_string']}") CMD_CLEAR = 0xa0 CMD_IMAGE_ROW = 0xa1 CMD_TEXT = 0xa2 CMD_REFRESH = 0xaf def send_cmd(self, cmd, param=[0, 0, 0], img=None): if self.dev is None: raise RuntimeError("Device not opened") data = [cmd] + param if img is not None: data += img #print("send_cmd:", "".join([f"{d:02x} " for d in data[:16]])) self.dev.send_feature_report(data) def clear(self): self.send_cmd(self.CMD_CLEAR) self.send_cmd(self.CMD_REFRESH) time.sleep(0.03) def refresh(self): self.send_cmd(self.CMD_REFRESH) FONT_8x8 = 0 FONT_16x16 = 1 FONT_32x32 = 2 FONT_64x64 = 3 def draw_txt(self, txt, col, row, font=FONT_8x8, refresh=False): if len(txt) > 16: print("cmd_txt: text length over 16") txt = txt[:16] txt = txt.ljust(16, '\0') data = txt.encode('ascii') self.send_cmd(self.CMD_TEXT, [(col & 0xf) | (row & 0xf) << 4, font, len(txt)], data) time.sleep(0.1) if refresh: self.refresh() def draw_image_row(self, row, img): if len(img) != self.PAGE_SIZE: raise ValueError(f"Image row size over, got {len(img)}") self.send_cmd(self.CMD_IMAGE_ROW, [(row & 0xf) << 4, 0x0f, 0x00], img) def draw_image(self, img, refresh=False): for row in range(8): st = row * self.PAGE_SIZE ed = st + self.PAGE_SIZE #print(f"{row=} {st=} {ed=}") self.draw_image_row(row, img[st:ed]) if refresh: self.refresh() time.sleep(0.01) def __enter__(self): self.open() return self def __exit__(self, exc_type, exc_value, traceback): self.close() if __name__ == "__main__": # Listup connected devices Usb2Oled.hid_list() with Usb2Oled(vid=0x1209, pid=0x1111) as u2o: # Draw text example u2o.clear() u2o.draw_txt("1:Hello World!", 0, 0) u2o.draw_txt("2:Hello World!", 0, 1) u2o.draw_txt("3:Hello World!", 0, 2) u2o.draw_txt("Hello!", 0, 3, font=Usb2Oled.FONT_16x16) u2o.draw_txt("World!", 0, 5, font=Usb2Oled.FONT_16x16) u2o.refresh() time.sleep(3) # Draw image example row_a = bytes([0xFF, 0x00] * 8) row_b = bytes([0x00, 0xFF] * 8) img = bytearray().join(row_a if ((y // 8) & 1) == 0 else row_b for y in range(64)) u2o.clear() u2o.draw_image(img, refresh=True) time.sleep(3) u2o.clear() ``` @[twitter](https://twitter.com/serebent/status/2024020498548437131)
アニメーション表示の様子。上記の'usb2oled.py'と同じフォルダに下記ファイルをおいて実行します。思ったよりも表示の更新速度が速いので結構ヌルヌル動く感じです。撮影条件の影響でフリッカが見えていますが肉眼ではほとんど気になりません。もっときれいに見えます。
アニメーション表示の様子。上記の **usb2oled.py** と同じフォルダに下記ファイルをおいて実行します。思ったよりも表示の更新速度が速いので結構ヌルヌル動く感じです。撮影条件の影響でフリッカが見えていますが肉眼ではほとんど気になりません。実物はもっときれいに見えます。
```python:sample_anime.py from usb2oled import Usb2Oled import time import math import numpy as np CX, CY = Usb2Oled.WIDTH / 2, Usb2Oled.HEIGHT / 2 def set_pix(fb, x, y): if x < 0 or x >= Usb2Oled.WIDTH or y < 0 or y >= Usb2Oled.HEIGHT: return pos = y * Usb2Oled.WIDTH + x byte_index = pos >> 3 if 0 <= byte_index < len(fb): # fb[byte_index] |= 1 << (7 - (pos & 7)) # MSB first fb[byte_index] |= 1 << (pos & 7) # LSB first def set_frame(fb, u2o): u2o.draw_image(fb, refresh=True) time.sleep(0.016) # ≒1/60 ※オーバーヘッドがあるので実際のfpsはもっと落ちる def anime_circle_wave(n=100, step=0.3): for t in np.arange(0.0, n * step, step): fb = bytearray(Usb2Oled.WIDTH * Usb2Oled.HEIGHT // 8) for y in range(Usb2Oled.HEIGHT): for x in range(Usb2Oled.WIDTH): if math.sin(math.hypot(x - CX, y - CY) / 4 - t) > 0: set_pix(fb, x, y) set_frame(fb, u2o) def amine_time_tonnel(n=100, step=0.25): for t in np.arange(0.0, n * step, step): fb = bytearray(Usb2Oled.WIDTH * Usb2Oled.HEIGHT // 8) for y in range(Usb2Oled.HEIGHT): for x in range(Usb2Oled.WIDTH): dx = x - CX dy = y - CY angle = math.atan2(dy, dx) dist = math.hypot(dx, dy) if math.sin(dist*0.15 - t + angle) > 0: set_pix(fb, x, y) set_frame(fb, u2o) def anime_wave_feild(n=100, step=0.2): for t in np.arange(0.0, n * step, step): fb = bytearray(Usb2Oled.WIDTH * Usb2Oled.HEIGHT // 8) for y in range(Usb2Oled.HEIGHT): for x in range(Usb2Oled.WIDTH): if 0 < math.sin(x*0.12 + t) + math.cos(y*0.15 - t): set_pix(fb, x, y) set_frame(fb, u2o) def ball_xy(offset_x, offset_y, angle_x, angle_y): return CX + offset_x * math.cos(angle_x), CY + offset_y * math.sin(angle_y) def anime_meta_ball(n=100, step=0.2, scale=1.0): for t in np.arange(0.0, n * step, step): fb = bytearray(Usb2Oled.WIDTH * Usb2Oled.HEIGHT // 8) x1, y1 = ball_xy(30, 20, t, t * 1.3) x2, y2 = ball_xy(30, 20, t * 0.7, t) for y in range(Usb2Oled.HEIGHT): for x in range(Usb2Oled.WIDTH): d1 = scale / (math.hypot(x - x1, y - y1) + 1) d2 = scale / (math.hypot(x - x2, y - y2) + 1) if (d1 + d2) > 0.06: set_pix(fb, x, y) set_frame(fb, u2o) def anime_meta_ball_x3(n=100, step=0.16, scale=0.7, threshold=0.095): for t in np.arange(0.0, n * step, step): fb = bytearray(Usb2Oled.WIDTH * Usb2Oled.HEIGHT // 8) x1, y1 = ball_xy(42, 26, t * 0.8, t * 1.1) x2, y2 = ball_xy(40, 24, t * 1.3, t * 0.7) x3, y3 = ball_xy(38, 22, t * 1.6, t * 1.4) for y in range(Usb2Oled.HEIGHT): for x in range(Usb2Oled.WIDTH): d1 = scale / (math.hypot(x - x1, y - y1) + 1) d2 = scale / (math.hypot(x - x2, y - y2) + 1) d3 = scale / (math.hypot(x - x3, y - y3) + 1) v = d1 + d2 + d3 if v > threshold: if (v - threshold) < 0.03: # 境界付近だけディザを入れる if ((x + y) & 1) == 0: # 疑似ハイライト(縞) set_pix(fb, x, y) else: set_pix(fb, x, y) # 中心部はベタ白 set_frame(fb, u2o) with Usb2Oled(vid=0x1209, pid=0x1111) as u2o: anime_circle_wave() amine_time_tonnel() anime_wave_feild() anime_meta_ball() anime_meta_ball_x3() u2o.clear() ``` @[twitter]https://twitter.com/serebent/status/2024294749038137718) # 応用例 ### Raspberry Piの情報表示 ヘッドレスでRasPiを運用しているとIPアドレスが変わった時にリモートでアクセスできなくなります。USB接続した時に下記のスクリプトを実行するようにしておけば別途モニタやキーボード等を接続する必要はありません。RasPi基板のヘッピンにOLEDを直付けしてもできるけどこの方がスマートでしょ。
@[twitter](https://x.com/serebent/status/2024294177501303194)
@[twitter](https://twitter.com/serebent/status/2024294177501303194)
### アナログ時計 時計。この小さい画面だどちょっと見にくいかも。
@[twitter](https://x.com/serebent/status/2024294924137746782)
@[twitter](https://twitter.com/serebent/status/2024294924137746782)
### ワイヤーフレーム 3次元空間座標を浮動小数計算していますがホストPC側で全て計算しているので、こんな表示も可能。IMU(3次元加速度センサ)からの入力をリアルタイムに表示させてもおもしろいかも。
@[twitter](https://x.com/serebent/status/2024294557350068422)
@[twitter](https://twitter.com/serebent/status/2024294557350068422)
### 何かのカウントダウン 発射10前!! 9、8、・・・3、2、1、0
@[twitter](https://x.com/serebent/status/2024295152630845681)
@[twitter](https://twitter.com/serebent/status/2024295152630845681)
### fps測定 ざっくり測定ですが、100フレーム表示するのに **5.07[ms]** かかったので **19.7[fps]** くらいはでいるようです。 表示する際に[ch32fun](https://github.com/cnlohr/ch32fun)の中でフレームバッファのデータを毎回SSD1306用の画素配置に変換しているようなので、ホスト側で直接SSD1306用の配列で画像データを作るようにすれば、もっと速くなる気がします。
@[twitter](https://x.com/serebent/status/2024294308657189022)
@[twitter](https://twitter.com/serebent/status/2024294308657189022)
# まとめ CH32系チップでUSB接続するなら数十円しか違わない**CH32V203/103**を使えという指摘は置いておくとして。 安い部品数点と数百行程度のコードを書くだけでUSB接続ディスプレイができました。USB接続できるので汎用性も高くいろいろなモノに使えそうです。性能的としても満足の出来です、 なんだかんだでUSB仕様の調査と3Dプリンタでのケース作成の時間がほとんどでした。 ---- ---- # 備忘録