WebHID API は Firefox・Safari では利用できません。Chrome / Edge で開き直してください。
(Ruby のコンパイルと .ino / .urb のダウンロードはこのブラウザでも利用できますが、デバイス接続・EEPROM 転送・HID コンソールは利用できません)
「Rubyで電子工作をやってみたい」——そう思っても、
高価なボード、複雑なセットアップ、専用ソフト…
とハードルを感じていませんか?
UIAPruby は、そのハードルをぐっと下げます。
UIAPduino(290円、CH32V003ベース)に
I2C EEPROM(24FC256 なら数十円)を合わせて、ハードウェアは数百円。
あとは手持ちのChrome/Edgeブラウザだけ。
普段書いているRubyの文法でLEDを光らせ、ブザーを鳴らし、センサーを読み、
電源を切っても消えない変数($count)やドット絵の配列まで扱えます。
マイコンで Ruby といえば mruby。でも mruby のビルドには Ruby と rake が必要で、
Arduino IDE だけでは完結しません。
URB Lab は、その rake の役割をブラウザに移しました。
コンポーネント(mruby の mrbgems に相当)を選ぶと、
必要な機能だけを組み込んだ単一の .ino が生成され、Arduino IDE でそのまま書き込めます。
PC に mruby のビルド環境(rake の構成やクロスコンパイラ)を整える必要はありません。
Rubyコードのコンパイルもブラウザの中——@ruby/prism でコードを解析し、
独自の軽量バイトコード(.urb)に変換。
WebHIDで EEPROM へ直接転送し、マイコン上のTinyVMが実行します。
インストール不要・クラウド不要。すべてブラウザの中で完結。
Rubyist が電子工作へ踏み出す入口を、
Wakayama.rb
が作りました。mruby ボード GR-CITRUS に続く、二つ目の扉です。
⚡ クイックスタート(4ステップ)
-
1
スケッチをダウンロードして書き込む(初回のみ)
この下の「コンポーネント選択」で使いたい機能を選び、UIAPrubyEeVm***.inoをダウンロード。 Arduino IDEでUIAPduinoに書き込みます。
書き込み時は、UIAPduino のボタンを押しながら USB ケーブルを挿し、 ブートローダーを起動した状態にしてから書き込んでください。
FQBN:UIAP_HID:ch32v:CH32V003:pnum=V14,usb=webhid,opt=oslto -
2
Rubyコードを書く
エディタにRubyコードを入力します。サンプルボタンですぐ試せます。
例:led = GPIO.new(2, GPIO::OUT)→ D2ピンをLED出力に設定
loop do led.toggle; sleep(0.5); end→ 0.5秒ごとに点滅 -
3
「コンパイル」ボタンを押す
ブラウザ内でRubyコードをAST解析し、TinyVM用.urbバイトコードへ変換します。
✓ OK と表示されれば生成完了です。 -
4
EEPROM に送信 → RUN
USBケーブルでUIAPduinoを接続し「📤 EEPROM に送信」をクリック。 未接続ならデバイス選択ダイアログが自動で開きます。転送後「▶ RUN」で実行開始。
💡 EEPROM に書いたプログラムは、 電源投入から 5 秒後に自動実行されます(ファイル名の指定は不要です)。
🔧 必要なハードウェア
- UIAPduino(CH32V003ベース)+ EEPROM 基板
- I2C EEPROM: 24FC256(32KB)または CAT24M01WI(128KB)
- USBケーブル(Micro-B または Type-C)
- LED / タクトスイッチ / ブザーなど(実験用)
💻 必要なソフトウェア
- Google Chrome または Microsoft Edge(最新版)
- このページ — WebHID対応ブラウザのみ動作
- ビルド済みファームを書き込む場合は、以下は要りません。
このページの「ビルド済みファームを直接書き込む」から書き込めば、Arduino IDE もボードパッケージも不要です。 - Arduino IDE(自分でビルドする場合のみ)
- UIAP_HID ボードパッケージ(自分でビルドする場合のみ)
v1.2.13 以降
v1.2.13 でNeoPixelmin::getPixels()が入り、NeoPixel の回転・減光がこれを使います。
ボードマネージャURL:https://raw.githubusercontent.com/tarosay/board_manager_files/main/package_uiap_hid_index.json
UIAPduino EE 版 対応ピン一覧(クリックで開く)
| Arduinoピン番号 | CH32V003ポート | PWM | ADC | 用途・備考 | UIAPruby記法 |
|---|---|---|---|---|---|
| 2 | PC0 | ✅ | — | LED出力(基板上LED) | GPIO.new(2, GPIO::OUT) |
| 3 / 4 | PC1 / PC2 | — | — | ⚠ SDA / SCL — EEPROM 専用(占有) | — |
| 5 | PC3 | ✅ | — | PWMブザー(圧電ブザー推奨) | Tone.new(5) |
| 6 | PC4 | ✅ | ✅ | SD 版から解放 — GPIO / PWM / ADC(A2) | ADC.new(6) / PWM.new(6) |
| 7 | PC5 | — | — | SD 版から解放 — GPIO | GPIO.new(7, ...) |
| 8 | PC6 | — | — | NeoPixel DIN(Np 選択時は専用)。未選択時は GPIO | NeoPixel.new(12) |
| 9 / 10 | PC7 / PD0 | — | — | HC-SR04 推奨(9=TRIG, 10=ECHO)。GPIO 可 | Ultrasonic.new(9, 10) |
| 11 | PD1 | — | — | タクトスイッチ(PULL_UP使用) | GPIO.new(11, GPIO::IN | GPIO::PULL_UP) |
| 0, 1, 12 | PA1/PA2/PD2 | — | ✅ | アナログ入力(ADC対応ピン) | ADC.new(0) 〜 |
| 15 / 16 | PD5 / PD6 | — | ✅ | ADC(A5/A6) ⟷ Se 選択時は UART TX/RX 専用(排他) | ADC.new(15) / Serial.new(9600) |
SD 版で塞がっていた 6・7・8・9 は EE 版で解放されました。8 は Np 選択時のみ NeoPixel 専用になります。
Se(UART)を選ぶと 15/16 が TX/RX になり、ADC は 0, 1, 6, 12 の 4 本になります。
EEPROM の配線(クリックで開く)— アドレス 0x50 / 0x52 の決め方
スレーブアドレスは 1010 A2 A1 A0 R/W で決まります。
A0 / A1 / A2 をすべて GND に落とすと 1010000 =
0x50、A1 だけ 5V に上げると
1010010 = 0x52 です。
24FC256 は 0x50 で使います。CAT24M01WI は 0x50/0x51 と
0x52/0x53 のどちらでも使えるので、上の「搭載する EEPROM」で選んだ側が
ファームに焼き込まれます。24FC256 と CAT24M01WI を同じバスに載せるときは、
ぶつからないよう CAT24M01WI を 0x52 側にしてください。
CAT24M01WI がアドレスを 2 つ使うのは、128KB を 2 バンクに分けて 17 ビット目を
スレーブアドレス側に入れているためです。
| EEPROM 側 | つなぎ先 | 理由 |
|---|---|---|
| SDA (5) | UIAPduino ピン 3(PC1) | ファームが固定で使う。GPIO には開放されない |
| SCL (6) | UIAPduino ピン 4(PC2) | 同上 |
| VCC (8) | 5V | EE 版は全て 5V 動作。SD が無くなり 3.3V 系が不要になった |
| VSS (4) | GND | — |
| A0 (1) | GND | 24FC256 では A0。CAT24M01WI では NC(A16 に使われ外に出ていない)。 どちらも GND でよい |
| A1 (2) | GND なら 0x50 5V なら 0x52 |
ここでアドレスが決まる。上の「搭載する EEPROM」で選んだ側と 合わせること。食い違うと起動時に「EEPROM 検出 FAIL」で止まる |
| A2 (3) | GND | 0x50 / 0x52 のどちらでも GND |
| WP (7) | GND | 24FC256 は未接続にできない(データシートに「VSS または VCC に 接続しなければならない」)。CAT24M01WI は内部プルダウンで 0 |
ボードパッケージ同梱の I2C サンプル(
Wiremin_EEPROM_24FC256 など)には
「4.7 kΩ を SDA/SCL から Vcc へ」と書かれていますが、あれは UIAPduino 以外の環境も含めた
一般的な説明です。Wiremin.h は PC1 / PC2 を AF オープンドレインに設定するだけで、
マイコン内部のプルアップは使っていません。
Wiremin_begin_fast() を呼び、400 kHz で動きます
(5V 動作のため。決定 20)。自作の基板や外部デバイスを長い配線でぶら下げる場合は、この速度で波形が保つかを見てください。
EE 版では I2C スレーブになれません。 自分のプログラムが EEPROM から読めなくなるため、コマンドごと持っていません。
選択内容が下のエディタのサンプルと .ino ファイルに反映されます。
Arduino IDE でビルドしなくても、ビルド済みのものをブラウザから UIAPduino に書き込めば、そのまま UIAPruby が使えます。
選んだファームに入っているコンポーネントには自動でチェックが入ります。
UIAPruby — Ruby構文を @ruby/prism でAST解析し、UIAPduino TinyVM用 URB1 バイトコードへ変換します。
UIAPduino 上では Ruby VM を実行しません。
.rb ファイルをエディタにドラッグ&ドロップするか、「開く」ボタンで読み込めます。
require "name" または require_relative "name" で読み込めます。
⚠ D3(SDA) D4(SCL) は EEPROM 専用です。D6/D7/D8/D9 は SD 版と違い使用できます(D8 は Np 選択時のみ NeoPixel 専用)。
自動実行が始まるのは、電源オンから 5 秒待ってからです。
USB 接続・RUN ボタン操作なしで単体動作するので、完成したスクリプトの運用に便利です。
$変数 の現在値を読み出します。読み取り専用で、値は変更しません。型・要素数・永続属性は EEPROM 先頭の URB1 メタデータから、変数名と値は各スロットから取得します。
未接続のときは、このボタンでデバイス選択ダイアログが開き、接続してからそのまま読み出します。
配列は先頭 20 要素を表示します。「〇番目から全部読む」で末尾まで読み出せます(時間がかかるので途中で「中止」できます)。
「CSV で保存」は画面に出ている数値配列を、絶対添字で揃えて縦に並べて書き出します。開始位置や要素数が変数ごとに違っても、値の無いところは空欄になります。
プログラム実行中は読み出せません。先に「■ STOP」を押してください。
ファイルヘッダー(8 バイト)
変数メタデータ(version = 2 のみ、ヘッダー直後)
$var: 電源を切っても値を保持)TinyVM 命令セット(リトルエンディアン)
Timer.new と t.reset は同じ命令)(Tm)レジスタ R0–R3 は int32_t。Q16.8 固定小数: 1.0=256, 0.5=128。
MUL_Q16 は int32 乗算 — 積が ±32768 未満まで正確(2026-07-12 以前のファームは int16 制限で ±128 以上のオペランドが壊れるバグあり。要再書き込み)。
GPIO / PWM / I2C / その他
| Ruby 記法 | 変換後命令 | Phase |
|---|---|---|
led = GPIO.new(2, GPIO::OUT) | GPIO_MODE pin=2 OUTPUT | 1 |
button = GPIO.new(11, GPIO::IN | GPIO::PULL_UP) | GPIO_MODE pin=11 INPUT_PULLUP | 1 |
led.on / led.high / led.write(1) | GPIO_WRITE pin, 1 | 1 |
led.off / led.low / led.write(0) | GPIO_WRITE pin, 0 | 1 |
led.toggle | GPIO_TOGGLE pin | 1 |
buzzer = Tone.new(5) | GPIO_MODE pin=5 OUTPUT | 1 |
buzzer.frequency(440) | TONE_FREQ pin, 440 | 1 |
buzzer.off / buzzer.frequency(0) | TONE_FREQ pin, 0 | 1 |
buzzer.tone(440, 0.2) | TONE_FREQ 440 → WAIT_MS 200 → TONE_FREQ 0 | 1 |
button.low? / button.high? | GPIO_READ pin, 空きレジスタ + JNZ/JZ | 2 |
sleep(0.2) / wait_ms(200) | WAIT_MS 200 | 1 |
wait_ms n * 100 / sleep(n) — 変数の時間待ち | WAIT_MS_REG reg, 100(掛け算はファームウェア側) | 1 |
print / puts / p / putc | PRINT_STR | 3 |
warn d / warn sonar.read — DEVICE LOG へデバッグ出力(Ec 不要・全構成で使用可) | WARN_REG reg | 3 |
raise "msg" / raise unless cond | PRINT_STR + HALT | 4 |
def name … end | インライン展開 | 5 |
break / next | JMP loop_end / loop_start | 6 |
g = 0.1 * 0.5 / Q16.8 演算 | LOAD_Q16 + MUL_Q16 … | 7 |
if d < 15 — 比較演算(< > == >= <= !=)※Q1 必須 | CMP_LT/GT/EQ_Q16 + JZ/JNZ | Q1 |
case d / when 0, 10 — 値の列挙 ※Q1 必須 | CMP_EQ_Q16 + ジャンプ(OR 展開) | Q1 |
when 0...15 / when 1..3 — レンジ ※Q1 必須 | CMP_LT/GT_Q16 ×2 + ジャンプ(範囲比較) | Q1 |
f = ->(x) { x < 15 } — ラムダ式の定義 | コンパイル時登録(バイトコードなし) | — |
f.call(d) / when f / when ->(x){…} — ラムダ呼び出し | 本体をインライン展開(下記参照) | — |
require_relative "path" | compile-time include | 8 |
motor = PWM.new(2) | GPIO_MODE pin=2 OUTPUT | Pw |
motor.frequency(500) — 基準周波数(省略時 1kHz) | PWM_BASE_FREQ pin, 500 | Pw |
motor.duty(255) — リテラル 0〜255 | PWM_DUTY pin, 255(即値) | Pw |
motor.duty(speed) — 変数 ※Q1 必須 | PWM_DUTY_REG pin, reg | Pw |
servo.angle(90) — リテラル 0〜180 ※要 frequency(50)(50Hz・0.5〜2.4ms、分解能約7°) | PWM_DUTY pin, 19(コンパイル時計算) | Pw |
servo.angle(pos) — 変数 ※Q1 必須・要 frequency(50) | Q16 演算 + PWM_DUTY_REG pin, reg | Pw |
sensor = ADC.new(0) ピン: 0,1,6,12,15,16(Se 選択時は 15/16 不可) | — (初期化なし) | Ad |
puts sensor.read / print sensor.read | ADC_READ pin,空きレジスタ + PRINT_REG | Ad |
sonar = Ultrasonic.new(9, 10) 第1引数=TRIG, 第2引数=ECHO(空いている GPIO ピン。3/4 は EEPROM 専用で不可) | GPIO_MODE trig OUT + GPIO_MODE echo IN | Us |
puts sonar.read / print sonar.read | ULTRASONIC_READ trig,echo,空きレジスタ + PRINT_REG | Us |
$count = $count + 1 — $変数は EEPROM に永続保存 ※Q1 必須 | VAR_LOAD + ADD_Q16 + VAR_STORE | Ev |
| 数値変数の3つ目以降(R0/R1 あふれ)— EEPROM に自動配置 ※Q1 必須 | VAR_LOAD / VAR_STORE | Ev |
数値配列 arr = Array.new(10) / arr[i]($付きで永続) ※Q1 必須 | VAR_LOAD_IDX / VAR_STORE_IDX | Ev |
puts $count / puts g — 数値出力 ※数値変数は Q1 必須(デバッグなら warn が Ev だけで使える) | VAR_LOAD + PRINT_REG / PRINT_REG | Ec |
I2C.master_init / v = I2C.master_get(0x10, 0) / I2C.master_set(0x10, 0, v) — 0x50〜0x53 は EEPROM が占有 | I2C_MASTER_* | BASE |
ser = Serial.new(9600) TX=15, RX=16(ADC の 15/16 と排他) | SERIAL_BEGIN | Se |
ser.write(65) / ser.write(b)(変数) | SERIAL_WRITE / SERIAL_WRITE_REG | Se |
ser.print "x" / ser.puts "x"(puts は CRLF 付き) | SERIAL_PRINT | Se |
if ser.available? / b = ser.read | SERIAL_AVAILABLE / SERIAL_READ | Se |
ser.print t / ser.puts line — 数値・文字変数の送信 | SERIAL_PRINT_REG / SERIAL_PRINT_VAR | Se+Ec |
line = ser.gets(13, 1000) — 1 行受信(デリミタ, タイムアウトms。既定 10, 1000) | SERIAL_READ_LINE | Se+Ec |
np = NeoPixel.new(12) DIN=8 固定・個数は設定値まで | NEO_BEGIN | Np |
np.set(0, 255, 0, 0) / np.hsv(0, 50, 100, 100) / np.fill(r,g,b) / np.clear | NEO_SET_RGB / NEO_SET_HSV / NEO_FILL / NEO_CLEAR | Np |
np.rainbow(20) / np.shift(1) / np.dim(20) / np.brightness(30) — 既定では 1 命令ごとに自動で show | NEO_RAINBOW / NEO_SHIFT / NEO_DIM / NEO_BRIGHTNESS | Np |
np.auto = false — 自動 show を止める。以後は np.show だけが送る(np.auto = true で戻す) | NEO_AUTO | Np |
np.shift(step) — 歩数を数値変数で。負も剰余も実行時に畳む(要 Q1) | NEO_SHIFT_REG | Nr |
np.seen('B', 'G') — テープの色の並びを直す。「赤が B に、緑が G に見える」と答える形('R' / 'G' / 'B' のリテラルのみ) | NEO_ORDER | Nr |
n = rand(6) — 0〜5 の整数 | RAND 0,6,Rn | Rn |
n = rand(1..6) — 1〜6 の整数 | RAND 1,7,Rn | Rn |
srand(42) — シード設定 | SRAND 42 | Rn |
every_ms 100 do … end — 絶対周期でループ。本体が何 ms かかっても周期は 100ms のまま | 本体 + EVERY_MS s0,100 + JMP | Tm |
every_ms 100 — loop do の中に文として置く形。書き方が違うだけで同じ 1 命令 | EVERY_MS s0,100 | Tm |
t = Timer.new — 起点をセット。every_ms の起点とは別物で、周期の境目で区切られることはない | TIMER_RESET s1 | Tm |
d = t.ms / u = t.us — 起点からの経過。読んでも起点は動かないので同じ区間を両方読める | TIMER_MS / TIMER_US | Tm |
t.reset — 起点を今にし直す。区間を区切るのはこれ | TIMER_RESET | Tm |
warn t.ms — 経過時間を DEVICE LOG へ | TIMER_MS + WARN_REG | Tm |
ラムダ式(コンパイル時インライン展開)
| 使い方 | 説明 | 必要 |
|---|---|---|
near = ->(x) { x < 15 } | 定義。本体は式1つだけ。バイトコードは出力されない | — |
case dwhen near / when ->(x) { x < 15 } | case 対象 d が引数 x に渡り、本体が条件として展開される | Q1 |
if between.call(d, 5, 10) | if / while / until / unless / 三項演算子の条件で使用可。複数引数・&& / || も OK | Q1 |
y = double.call(n) | 数値式として使用(戻り値 = 本体の式の値) | Q1 |
wait_ms half.call(500) | 引数がすべて定数ならコンパイル時に計算され、リテラルを書ける場所(sleep / servo.angle 等)ならどこでも使える | — |