今回は Raspberry Pi Pico W を使って、赤外線リモコンの信号を受信し、RGB LED を光らせる実験に挑戦してみましょう。
テレビのリモコンなどに使われている赤外線(IR)リモコンは、ボタンを押すと「特定のパターンの信号」を赤外線で送信しています。この信号を、赤外線受信モジュール(たとえば VS1838B など)で受け取り、Pico W に接続して解析します。
リモコンのボタンによって異なる信号が届くので、受信したパルスの長さや間隔を読み取り、「どのボタンが押されたか」を判定できます。今回は、その判定結果に合わせて RGB LED の色を変える のが目標です。たとえば
-
1ボタンを押すと白色
-
2ボタンで赤色
3ボタンで緑色
-
4ボタンで青色
というように、ボタンごとに LED の色が変わるようにします。
RGB LED は赤・緑・青の 3 つの LED が 1 つにまとまった部品で、それぞれの色を ON/OFF(あるいは明るさを変える)ことで多彩な色を作れます。まずは 「IR 信号を受け取って LED を光らせる」 という基本動作ができれば OK。慣れてきたら、次のステップとしてなめらかな PWM 制御やグラデーション発光などにも挑戦してみましょう。
回路図
・
PL9823-F5📚 使用ライブラリとモジュール
アイコン | ライブラリ/モジュール | 主な役割 | 本コードで使っている代表的 API |
---|
📡 | UpyIrRx | IR 受信専用の軽量クラス。受信ピンからのパルス列をキャプチャし、キャリブレーション済みリストに変換してくれる | record() , get_calibrate_list() , ERROR_NONE |
🔧 | machine.Pin | Raspberry Pi Pico W の GPIO を制御する MicroPython 標準モジュール | Pin() (モード Pin.IN ) |
⏰ | time | 時刻取得とスリープ用の MicroPython 標準モジュール | ticks_ms() , ticks_diff() , sleep() / sleep_ms() |
ライブラリ:DLして下図のようにRaspbery PI Pico Wにアップロードします。
NEC赤外線リモコン信号デコーダ(MicroPython + UpyIrRx用)
from UpyIrRx import UpyIrRx
from machine import Pin
import time
def decode_nec(raw_data):
if len(raw_data) < 66:
return None
lead_mark = raw_data[0]
lead_space = raw_data[1]
if not (8500 < lead_mark < 9500 and 4000 < lead_space < 5000):
return None
bits = ""
for i in range(2, 66, 2):
mark = raw_data[i]
space = raw_data[i + 1]
if not (400 < mark < 700):
return None
if 400 < space < 700:
bits += "0"
elif 1500 < space < 1800:
bits += "1"
else:
return None
def bits_to_byte(b):
val = 0
for i in range(8):
if b[i] == '1':
val |= (1 << i)
return val
addr = bits_to_byte(bits[0:8])
addr_inv = bits_to_byte(bits[8:16])
cmd = bits_to_byte(bits[16:24])
cmd_inv = bits_to_byte(bits[24:32])
if addr ^ addr_inv != 0xFF or cmd ^ cmd_inv != 0xFF:
return None
return {"address": addr, "command": cmd}
IR_PIN = 16
ir = UpyIrRx(Pin(IR_PIN, Pin.IN))
while True:
err = ir.record(wait_ms=1500, blank_ms=200)
if err == ir.ERROR_NONE:
raw = ir.get_calibrate_list()
result = decode_nec(raw)
if result:
print("✅ NEC信号デコード成功")
print("📮 アドレス: 0x{:02X}".format(result["address"]))
print("🔘 コマンド: 0x{:02X}".format(result["command"]))
# エラーや待機中の表示はなしでスルー
time.sleep(0.1)
>>> %Run -c $EDITOR_CONTENT
MPY: soft reboot
✅ NEC信号デコード成功
📮 アドレス: 0x00
🔘 コマンド: 0x16
✅ NEC信号デコード成功
📮 アドレス: 0x00
🔘 コマンド: 0x0C
✅ NEC信号デコード成功
📮 アドレス: 0x00
🔘 コマンド: 0x18
✅ NEC信号デコード成功
📮 アドレス: 0x00
🔘 コマンド: 0x5E
✅ NEC信号デコード成功
📮 アドレス: 0x00
🔘 コマンド: 0x08
✅ NEC信号デコード成功
📮 アドレス: 0x00
🔘 コマンド: 0x1C
✅ NEC信号デコード成功
📮 アドレス: 0x00
🔘 コマンド: 0x5A
✅ NEC信号デコード成功
📮 アドレス: 0x00
🔘 コマンド: 0x42
✅ NEC信号デコード成功
📮 アドレス: 0x00
🔘 コマンド: 0x52
✅ NEC信号デコード成功
📮 アドレス: 0x00
🔘 コマンド: 0x4A
📝 処理の流れ
⚙️ ライブラリとピンの準備
UpyIrRx, Pin, time をインポートし、IR 受信用ピン番号を決定
🛠 NEC デコード関数 decode_nec() を定義
・パルス列のフォーマット確認(リードバースト長・ビット長)
・“0”/“1” 判定 → 32 bit へ変換 → 補数チェックで妥当性を検証
🔧 IR 受信モジュールを初期化
ir = UpyIrRx(Pin(IR_PIN, Pin.IN))
🔄 メインループ
ir.record() で最長 1.5 s 待機し、パルス列をキャプチャ
受信成功 (ERROR_NONE) なら get_calibrate_list() で μs 単位リストを取得
🧩 NEC デコード
キャプチャしたリストを decode_nec() へ渡し、アドレス&コマンドに変換
✅ デコード成功時の表示
16 進数でアドレス/コマンドをプリントし、判定の可視化
💤 CPU 負荷軽減
0.1 秒スリープでポーリング間隔を調整し省電力化
📌 decode_nec(raw_data)
先頭 9 ms+4.5 ms のリードバーストを確認し、560 µs のマークと 0/1 用スペース長を解析。32 bit の補数ペアで誤受信を排除する
🏷 bits_to_byte()
“01010101” 形式の文字列を整数 0x55 に変換するユーティリティ
📡 UpyIrRx.record()
高速割り込みでパルス幅をバッファリングし、無信号 200 ms で自動終了
🛠 get_calibrate_list()
キャリブレーション済み(π/38 kHz クロック換算済み)の μs リストを返すので解析がシンプル
🚦 デコード結果の利用
今回は print() で確認するだけだが、ここに RGB LED 制御を追加すれば “ボタン → LED 色” が簡単に実現できる
🚀 拡張アイデア
受信コードを辞書にマッピングして多色点灯/PWM グラデーション
受信失敗時のタイムアウト表示やリトライ機能を追加
複数フォーマット(Sony, RC5 など)をデコードする汎用ライブラリ化
🌈 PL9823-F5 用 RGB LED デモプログラム
# pl9823_demo.py
# Raspberry Pi Pico (RP2040) + MicroPython v1.22 以降
from machine import Pin
from neopixel import NeoPixel # Pico 用の組込みモジュール
import time
LED_PIN = 17 # データ線を繋いだ GPIO 番号に合わせる
NUM_LEDS = 1 # PL9823‑F5 を1個だけ接続
BRIGHTNESS = 0.2 # 0.0〜1.0 で輝度スケーリング(省電力)
np = NeoPixel(Pin(LED_PIN, Pin.OUT), NUM_LEDS)
def set_pixel(rgb):
# 0‑255 のタプルを輝度スケーリングして書き込む
r, g, b = rgb
scale = BRIGHTNESS
np[0] = (int(g*scale), int(r*scale), int(b*scale)) # 注: GRB 並び
np.write()
def wheel(pos):
# 0‑255 → レインボー (Adafruit 定番アルゴリズム)
if pos < 85:
return (255 - pos*3, pos*3, 0)
elif pos < 170:
pos -= 85
return (0, 255 - pos*3, pos*3)
else:
pos -= 170
return (pos*3, 0, 255 - pos*3)
print("PL9823-F5 demo start")
while True:
# レインボーグラデーション
for i in range(256):
set_pixel(wheel(i))
time.sleep_ms(20)
# 単色テスト (赤→緑→青→白→消灯)
for color in [(255,0,0), (0,255,0), (0,0,255), (255,255,255), (0,0,0)]:
set_pixel(color)
time.sleep(1)
⚙️ 初期設定
・🔌 GPIO17 を出力モードに設定
・💡 NeoPixel モジュールで LED(1個)を初期化
・🔋 輝度 (BRIGHTNESS) を 0.2 に設定(省電力)
🎨 set_pixel(rgb) 関数
・🎚️ RGB 値を輝度スケーリング(0〜255 → 0〜51)
・🔄 GRB の並びにして NeoPixel に送信
・📝 np.write() で LED に反映
🌈 wheel(pos) 関数
・🔢 入力値(0〜255)を元にレインボーカラーを生成
・🌟 Adafruit の定番アルゴリズムを使用
・🔁 色相:赤 → 緑 → 青 のグラデーションを形成
🔁 メインループの流れ
1.🌈 レインボーグラデーション
・wheel(i) で 0〜255 の色を順番に表示
・⏱️ 20ms ごとに更新(滑らかな色の遷移)
2.🎯 単色表示テスト
・🟥 赤 → 🟩 緑 → 🟦 青 → ⬜ 白 → ⚫ 消灯
・⏱️ 各色を 1秒間表示
IRリモコン信号でPL9823-F5フルカラーLEDを制御
from UpyIrRx import UpyIrRx
from machine import Pin
from neopixel import NeoPixel
import time
import gc
# ==== 設定 ====
IR_PIN, LED_PIN = 16, 17
NUM_LEDS = 1
BRIGHTNESS = 0.25
TOL = 0.45 # パルス許容誤差 45%
# ==== NeoPixel 初期化 ====
np = NeoPixel(Pin(LED_PIN, Pin.OUT), NUM_LEDS)
def set_pixel(rgb):
r, g, b = rgb
scale = BRIGHTNESS
np[0] = (int(g * scale), int(r * scale), int(b * scale)) # GRB順
np.write()
time.sleep_us(50)
def match(val, target):
lo = target * (1.0 - TOL)
hi = target * (1.0 + TOL)
return lo <= val <= hi
def decode_nec(raw):
if len(raw) == 0:
return None, False
if len(raw) < 4:
return None, False
# 明らかに早すぎるノイズパルスは無視(例: <2000us)
if raw[0] < 2000:
return None, False
if 500 <= raw[0] <= 10000:
if match(raw[1], 4500):
pass # 正常なヘッダー
elif match(raw[1], 2250):
return 0xFFFF, True # リピート信号
else:
return None, False
else:
return None, False
bits = 0
count = 0
for i in range(2, min(len(raw) - 1, 66), 2):
mark, space = raw[i], raw[i + 1]
if not match(mark, 560):
return None, False
if match(space, 560):
bits |= (0 << count)
elif match(space, 1690):
bits |= (1 << count)
else:
return None, False
count += 1
if count < 32:
return None, False
addr = bits & 0xFF
addr_i = (bits >> 8) & 0xFF
cmd = (bits >> 16) & 0xFF
cmd_i = (bits >> 24) & 0xFF
if (addr ^ addr_i) != 0xFF or (cmd ^ cmd_i) != 0xFF:
return None, False
return cmd, False
# ==== コマンド → RGB ====
CMD_COLOR = {
0x16: (128, 128, 0), # 白(控えめ)
0x0C: (255, 0, 0), # 赤
0x18: (0, 255, 0), # 緑
0x5E: (0, 0, 255), # 青
0x08: (255, 255, 0), # 黄
0x1C: (0, 255, 255), # シアン
0x5A: (255, 0, 255), # マゼンタ
0x42: (128, 0, 128), # グレー
0x52: (255, 128, 0), # オレンジ
0x4A: (0, 0, 0), # 消灯
}
DEFAULT_COLOR = (32, 32, 32)
# ==== IR 初期化 ====
ir = UpyIrRx(Pin(IR_PIN, Pin.IN, Pin.PULL_UP))
print("IR ⇒ RGB LED demo (final tuned version)")
last_cmd = None
gc_counter = 0
error_counter = 0
while True:
result = ir.record(wait_ms=1000, blank_ms=180)
if result == ir.ERROR_NONE:
raw = ir.get_calibrate_list()
gc.collect()
cmd_val, is_repeat = decode_nec(raw)
del raw
if cmd_val is None:
error_counter += 1
if error_counter >= 10:
print("Resetting IR receiver...")
ir = UpyIrRx(Pin(IR_PIN, Pin.IN, Pin.PULL_UP))
error_counter = 0
time.sleep(0.005)
continue
if is_repeat:
if last_cmd is None:
continue
cmd_val = last_cmd
else:
last_cmd = cmd_val
cmd_val &= 0xFF
color = CMD_COLOR.get(cmd_val, DEFAULT_COLOR)
set_pixel(color)
print("cmd=0x{:02X} → RGB {}".format(cmd_val, color))
gc_counter += 1
if gc_counter >= 100:
gc.collect()
print("Free memory:", gc.mem_free())
gc_counter = 0
error_counter = 0
time.sleep(0.005)
elif result == ir.ERROR_TIMEOUT:
pass # 通常状態:無反応ならスキップ
else:
error_counter += 1
if error_counter >= 10:
print("Resetting IR receiver...")
ir = UpyIrRx(Pin(IR_PIN, Pin.IN, Pin.PULL_UP))
error_counter = 0
time.sleep(0.005)
>>> %Run -c $EDITOR_CONTENT
MPY: soft reboot
IR ⇒ RGB LED demo (final tuned version)
cmd=0x4A → RGB (0, 0, 0)
cmd=0x16 → RGB (128, 128, 0)
cmd=0x0C → RGB (255, 0, 0)
cmd=0x18 → RGB (0, 255, 0)
cmd=0x5E → RGB (0, 0, 255)
cmd=0x08 → RGB (255, 255, 0)
cmd=0x1C → RGB (0, 255, 255)
cmd=0x5A → RGB (255, 0, 255)
cmd=0x42 → RGB (128, 0, 128)
cmd=0x52 → RGB (255, 128, 0)
cmd=0x4A → RGB (0, 0, 0)
cmd=0x0C → RGB (255, 0, 0)
cmd=0x18 → RGB (0, 255, 0)
cmd=0x5E → RGB (0, 0, 255)
🔁 処理の流れ(流れ図スタイル+アイコン)
🔧 初期化
・⚙️ GPIO16 を IR受信機(VS1838B等)用に入力+プルアップ設定
・💡 GPIO17 を NeoPixel(PL9823-F5)データ出力に設定
・🧠 NeoPixel オブジェクトと UpyIrRx 赤外線受信オブジェクトを初期化
・🧹 ガベージコレクションカウンタ・エラーカウンタを初期化
🔄 メインループ処理
1.📡 ir.record() で赤外線信号を受信(最大1000ms待機)
2.🧮 受信成功 → get_calibrate_list() で正規化されたパルス列取得
3.🔍 decode_nec() でNECフォーマット解析:
・📉 不完全やノイズの短い信号は無視
・🔁 リピートコード(0xFFFF)も検出対応
・✅ チェックサム検証付きで信頼性高
4.🎮 コマンド取得成功時:
・📝 最後のコマンドを保存(リピート対応)
・🎨 コマンドに対応するRGB色を辞書から取得
・🌈 NeoPixelへ色を書き込み
・🖨 色とコマンドをログ出力 cmd=0xXX → RGB (r,g,b)
5.🧹 100回ごとに gc.collect() を実行しメモリ管理
6.⚠️ 受信失敗・ノイズ時はエラーカウント増加 → 10回でIRを再初期化
コメント
コメントを投稿