Copilot先生と学ぶ!SnowRunnerのゲーム画面からAIに車種を判別させてみた

1. はじめに(AI学習のきっかけ)

  • 大好きなゲーム『SnowRunner』を題材にAIを勉強したい!
  • 実行環境:macOS Tahoe 26.5.2
  • 相棒のAI:まずは一番身近だった「Microsoft Copilot」を選択。

SnowRunnerとは?

トラックやオフロードSUVなどで悪路を指定された荷物を目的地に運んだり、災害で流された車両や機械を探したりするゲームです。

今回はSwitch2版の4K画像を使います。

2. 提案された3つのアプローチと、最初の一歩

Copilot先生から提示された3つの難易度:

  1. 車種判別(分類) ➡️ 「これ画面に映ってるの何?」(★今回はこれに決定!)
  2. 位置特定(物体検出) ➡️ 「画面のどこに車がある?」
  3. マップ上の位置推定 ➡️ 「背景からして、ここミシガンのどこ?」

3. 車種判別の5ステップと「Teachable Machine」の罠

開発のステップ

  • ステップ1:プレイ動画を撮影(4K)
  • ステップ2:動画から1秒ごとに静止画を抽出(FullHD)
  • ステップ3:フォルダ分けしてラベル付け(各150枚程度)
  • ステップ4:ノーコードで簡単な「Teachable Machine」で学習
  • ステップ5:Copilotに書いてもらったPythonコードで推論実行

ステップ1:

プレイ動画を撮りながら、途中、左右が開けた場所で、車両をぐるりと回るように視野を動かしていろいろな角度から車を撮影しました。

ステップ2:

ffmpegで動画から1秒ごとに静止画(FullHD)を抽出。


// 4Kの長い動画から切り出す部分を指定して4Kの短い動画に切り出して、その動画からFHDの静止画を切り出す(2段階)
ffmpeg -ss 00:06:28 -i input4k.mp4 -t 00:00:40 -c copy clip01.mp4
ffmpeg -i clip01.mp4 -vf "scale=1920:1080,fps=1" frames/frame_0%04d.jpg

// 4Kの長い動画から切り出す部分を指定して、直接FHDの静止画を切り出す(1段階でOK)
ffmpeg -ss 00:05:10 -i input4k.mp4 -t 00:02:30 -vf "scale=1920:1080,fps=1" frames/frame_%04d.jpg

ステップ3:

フォルダーに分けてラベル付け。

次のようなフォルダー構成にしました。トラックには「アオリ付き荷台(Sideboard)」というフレーム装備を明記。

dataset/
 ├── Chevrolet_CK1500/
 ├── Fleetstar_7020A_Sideboard/
 ├── GMC_MH9500_Sideboard/
 └── Scout_800/

実際に各フォルダーの内容は次のようになります。(トリミングされています。)

ステップ4:

Teachable Machineで学習させて、学習したモデルデータをダウンロードします。

「使ってみる」→「画像プロジェクト」→「標準の画像モデル」→それぞれのクラスを定義してそれぞれの画像をアップロード→トレーニング→モデルをエクスポート→TensorflowタブでSavedmodel形式を選んで「モデルをダウンロード」。

ダウンロードしたconverted_savedmodel.zipを解凍すると、次のようなフォルダ構成になっています。

converted_savedmodel/
 ├── labels.txt         ← クラス名のラベル
 └── model.savedmodel/  ← モデルデータのフォルダ(内容を変更しないこと!)
      ├── assets/
      ├── saved_model.pb
      └── variables/

ステップ5:

推論するコード(classify.py)は最終的なコードを最後に載せます。

対象にした4車種と「フレーム装備」の標準化

カスタマイズで見た目が変わる対策として、ルールを固定:

  • スカウト車(ノーマル状態):Chevrolet CK1500 / Scout 800
  • トラック車(アオリ付き荷台装備):GMC MH9500 / Fleetstar 7020A

4. 【トラブル発生】認識率が半分!?AI先生たちのバトンリレー

試行錯誤①:Copilotの提案「もっと車を大きく映そう」

一律で画面中央やや下をトリミングするコードを追加 ➡️ 多少改善するも、なぜかScout 800に誤認識されまくる。

緑の枠内でトリミングしています。
rawフォルダー内の画像を上の画像のように一律でトリミングしてtrimmedフォルダーに保存するコード(trimming.py)です。推論コード内にも同じトリミングを行う部分があります。

import cv2
import glob
import os

input_dir = "raw"
output_dir = "trimmed"
os.makedirs(output_dir, exist_ok=True)

for path in glob.glob(input_dir + "/*.jpg"):
    img = cv2.imread(path)
    h, w, _ = img.shape

    # 画面中央を基準に車体が写っている領域を切り出す
    x1 = int(w * 0.2)
    x2 = int(w * 0.8)
    y1 = int(h * 0.3)
    y2 = int(h * 0.9)

    crop = img[y1:y2, x1:x2]
    cv2.imwrite(os.path.join(output_dir, os.path.basename(path)), crop)

試行錯誤②:Gemini先生のファインプレー!コードの致命的バグを発見

原因はまさかの「色の反転」と「正規化の計算ミス」。

  • 誤:BGR(Python標準) ➡️ 正:RGB(Teachable Machine標準)への変換漏れ。
  • AIモデルの前提条件にコードが合っていなかったことが判明!

試行錯誤③:用途に合わせたAIの使い分け

Gemini先生の提案が少し難易度高め(PyTorchや本格的な環境構築)になりかけたため、Teachable Machineの親玉である「Google AI」に相談役をチェンジ。より実践的な機能拡張へ。

5. 機能拡張と、驚きの最終結果!

正解ラベルと比較して、フレームごとの「正答率」を自動算出する機能を推論コードに追加。最終的な推論コードは最後にあります。

📊 最終評価レポート

  • 総評価フレーム数: 3,179枚
  • 全体正答率 (Overall Accuracy)91.95%

車種別の結果:

  • 🚗 Chevrolet CK1500: 99.31% (ほぼ完璧!)
  • 🚛 GMC MH9500: 99.68% (ほぼ完璧!)
  • 🚙 Scout 800: 87.77% (黄色い車体がGMCに見えた?)
  • 🚚 Fleetstar 7020A: 84.40% (GMCの荷台と見間違えた?)

6. まとめと次のステップ

  • 身近なゲームを使うことで、モチベーションを保ったまま画像認識の基本(データ前処理、カラーチャネル、評価ログ)が学べた!
  • AIはそれぞれ得意分野(コード生成、デバッグ、検索)があるので、複数を使い分けるのが正解。
  • 次は「位置特定(物体検出)」か、YOLOを使った本格的な学習にチャレンジしてみたい。

📝 著作権・免責事項

本プロジェクトで使用しているゲーム画像および動画は、ゲーム『SnowRunner』(© Saber Interactive / Focus Entertainment)のものを使用しています。著作権はすべて権利所有者に帰属します。

車種判別に入力した動画

プレイ動画から各車種ごとに学習用とは違う部分を切り出したシーンを単純につないだものです。

最終的な推論コード

以下は最終的な推論コード(classify.py)です。正解ファイル(gt.txt)とかレポートファイル(accuracy_report.csv)とかを入出力しているので複雑になっています。

import tensorflow as tf
import numpy as np
import cv2
import sys
import csv
from collections import defaultdict

MODEL_PATH = "model.savedmodel"
VIDEO_PATH = "snowrunner.mp4"
USE_CAMERA = False
LABELS_PATH = "labels.txt"
GT_PATH = "gt.txt"
CONF_THRESHOLD = 0.5

# --- labels.txt をロード ---
try:
    with open(LABELS_PATH, "r", encoding="utf-8") as f:
        class_names = [line.strip() for line in f.readlines() if line.strip()]
except FileNotFoundError:
    print(f"エラー: {LABELS_PATH} が見つかりません。")
    sys.exit(1)

# --- モデルのロード ---
try:
    model = tf.keras.layers.TFSMLayer(MODEL_PATH, call_endpoint="serving_default")
except Exception as e:
    print(f"モデルの読み込みに失敗しました: {e}")
    sys.exit(1)

# --- ビデオキャプチャの設定 ---
cap = cv2.VideoCapture(0 if USE_CAMERA else VIDEO_PATH)
if not cap.isOpened():
    print("エラー: ビデオソースを開けませんでした。")
    sys.exit(1)

# --- 出力動画・FPSの設定 ---
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
fps = cap.get(cv2.CAP_PROP_FPS)
if fps <= 0 or np.isnan(fps):
    fps = 30.0

width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
out = cv2.VideoWriter("result.mp4", fourcc, fps, (width, height))

# --- 正解ファイル (gt.txt) のパース関数(修正完了) ---
def parse_gt_file(file_path, fps):
    timeline = []
    try:
        with open(file_path, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                # 最初に見つかるスペースで、タイムスタンプ(左)と中身(右)に2分割
                parts = line.split(maxsplit=1)
                if len(parts) < 2:
                    continue
                
                time_str = parts[0]
                # parts[1] の文字列からダブルクォーテーションをきれいに削除
                label_name = parts[1].replace('"', '').strip()

                # タイムスタンプをフレーム数に変換 (hh:mm:ss:ff または hh:mm:ss)
                time_parts = time_str.split(":")
                hh = int(time_parts[0])
                mm = int(time_parts[1])
                ss = int(time_parts[2])
                ff = int(time_parts[3]) if len(time_parts) > 3 else 0
                
                total_frames = int(((hh * 3600) + (mm * 60) + ss) * fps) + ff
                timeline.append((total_frames, label_name))
    except FileNotFoundError:
        print(f"警告: 正解ファイル {file_path} が見つかりません。正誤判定はスキップします。")
        return None
    
    timeline.sort(key=lambda x: x[0])
    return timeline

gt_timeline = parse_gt_file(GT_PATH, fps)

def get_current_gt(frame_idx, timeline):
    if not timeline:
        return "N/A"
    current_label = "不明"
    for start_frame, label in timeline:
        if frame_idx >= start_frame:
            current_label = label
        else:
            break
    return current_label

# --- CSVレポートの準備 ---
csv_file = open("accuracy_report.csv", "w", newline="", encoding="utf-8")
csv_writer = csv.writer(csv_file)
csv_writer.writerow(["Frame", "Timestamp(sec)", "AI_Prediction", "Confidence", "Ground_Truth", "Is_Correct"])

# --- 集計用カウンターの初期化 ---
total_evaluated_frames = 0
correct_predictions_count = 0

class_total_counts = defaultdict(int)
class_correct_counts = defaultdict(int)

print("処理を開始します。'q' キーで終了します。")

frame_count = 0

try:
    while True:
        ret, frame = cap.read()
        if not ret:
            break

        h, w, _ = frame.shape
        x1, x2 = int(w * 0.2), int(w * 0.8)
        y1, y2 = int(h * 0.3), int(h * 0.9)
        frame_trimmed = frame[y1:y2, x1:x2]

        if frame_trimmed.size == 0:
            frame_count += 1
            continue

        # --- 前処理 ---
        img_rgb = cv2.cvtColor(frame_trimmed, cv2.COLOR_BGR2RGB)
        img = cv2.resize(img_rgb, (224, 224))
        img = img.astype(np.float32)
        img = (img / 127.5) - 1.0
        img = np.expand_dims(img, axis=0)

        # --- 推論 ---
        pred_dict = model(img)
        output_key = list(pred_dict.keys())[0]
        pred = pred_dict[output_key].numpy()

        idx = np.argmax(pred)
        confidence = pred[0][idx]

        # --- AIラベル判定 ---
        if confidence < CONF_THRESHOLD:
            ai_label = "不明"
        else:
            ai_label = class_names[idx] if idx < len(class_names) else "不明"

        # --- 正解データとの照合 ---
        gt_label = get_current_gt(frame_count, gt_timeline)
        
        is_correct = (ai_label == gt_label)
        status_text = "OK" if is_correct else "NG"
        
        # --- 全体集計の更新 ---
        total_evaluated_frames += 1
        if is_correct:
            correct_predictions_count += 1
            
        # --- 車種別集計の更新 ---
        class_total_counts[gt_label] += 1
        if is_correct:
            class_correct_counts[gt_label] += 1
        
        # --- CSVへの書き出し ---
        timestamp_sec = frame_count / fps
        csv_writer.writerow([frame_count, f"{timestamp_sec:.2f}", ai_label, f"{confidence:.4f}", gt_label, status_text])

        # --- 画面描画 ---
        text_ai = f"AI: {ai_label} ({confidence:.2f})"
        text_gt = f"GT: {gt_label} [{status_text}]"
        text_x, text_y = 60, 180

        cv2.rectangle(frame, (text_x - 20, text_y - 140), (text_x + 2700, text_y + 160), (0, 0, 0), -1)
        cv2.putText(frame, text_ai, (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 4, (255, 255, 255), 3)
        color_status = (0, 255, 0) if is_correct else (0, 0, 255)
        cv2.putText(frame, text_gt, (text_x, text_y + 120), cv2.FONT_HERSHEY_SIMPLEX, 4, color_status, 3)
        cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)

        out.write(frame)
        cv2.imshow("SnowRunner AI - Evaluation", frame)

        frame_count += 1

        if cv2.waitKey(1) == ord('q'):
            break

finally:
    cap.release()
    out.release()
    
    if total_evaluated_frames > 0:
        incorrect_predictions_count = total_evaluated_frames - correct_predictions_count
        overall_accuracy = (correct_predictions_count / total_evaluated_frames) * 100
        
        print("\n" + "="*30)
        print("     OVERALL SUMMARY")
        print("="*30)
        print(f"Total Frames Evaluated: {total_evaluated_frames}")
        print(f"Correct Predictions:    {correct_predictions_count}")
        print(f"Incorrect Predictions:  {incorrect_predictions_count}")
        print(f"Overall Accuracy:       {overall_accuracy:.2f}%")
        
        print("\n" + "="*30)
        print("   CLASSWISE ACCURACY REPORT")
        print("="*30)
        
        csv_writer.writerow([])
        csv_writer.writerow(["--- Overall Summary ---"])
        csv_writer.writerow(["Metric", "Value"])
        csv_writer.writerow(["Total Frames Evaluated", total_evaluated_frames])
        csv_writer.writerow(["Correct Predictions", correct_predictions_count])
        csv_writer.writerow(["Incorrect Predictions", incorrect_predictions_count])
        csv_writer.writerow(["Overall Accuracy (%)", f"{overall_accuracy:.2f}"])
        
        csv_writer.writerow([])
        csv_writer.writerow(["--- Classwise Accuracy Report ---"])
        csv_writer.writerow(["Class Name", "Total Frames", "Correct", "Incorrect", "Accuracy (%)"])

        for cls in sorted(class_total_counts.keys()):
            total = class_total_counts[cls]
            correct = class_correct_counts[cls]
            incorrect = total - correct
            accuracy = (correct / total) * 100
            
            print(f"Class: {cls:<30} | Total: {total:<5} | Correct: {correct:<5} | Incorrect: {incorrect:<5} | Accuracy: {accuracy:.2f}%")
            csv_writer.writerow([cls, total, correct, incorrect, f"{accuracy:.2f}"])
            
        print("="*30)
        
    csv_file.close()
    cv2.destroyAllWindows()
    print(f"\n処理が終了しました。検証データと集計レポートは 'accuracy_report.csv' に保存されました。")

カテゴリー: AI, 学習 | タグ: , , | コメントする

M5StickCが起動しない

ちょっとブザーを鳴らすテストをしようとして、出来るだけ小さくしたいと調べていたら、M5StickC Plus2が電源もあって、ブザーが入っているのでコレでいいやと思っていたら、「そういえばM5StickCがあったな…」と出してみたら、USBケーブルで接続して電源ボタンを押しても起動しないので、「長いこと放置していたからバッテリーがダメになったかな?」と調べてみたら、次のような記事が見つかった。

https://qiita.com/kazuhidet/items/7c0c6fa7995fa5da6e9a

「なるほど…」とArduino IDEで確認したら起動はしているようだった。FactoryTestスケッチを書き込んでみたらLCDも表示されて起動した。

ただし、次のイメージのようにLow Battery please charge!と表示される。

Low Battery please charge!と表示される

追記:一晩、この状態で放っておいたけど、回復せず…

追記:BATGNDを短絡してUSBにつないで充電するというのも試すけど、どうも手応えがない。

追記:「電源管理用ICチップをリセット」の部分に書かれていた、1kΩの抵抗でG03V3を接続して充電するというのも試すけど、手応えがない。

追記:今度は「M5StickC low battery please charge!と表示される」と検索すると、「M5StickCの内蔵バッテリーが充電されない問題 → 解決」という記事が出てきたので、読んでみたらコメント欄のアドバイスで充電できたとのこと。

コメント欄には、「AXP192のスケッチを実行した状態で、BAT3V3を短絡すると一度画面が消えて、再度電源を入れるとバッテリー電圧が0じゃなくなる」ということなので、まずはAXP192のスケッチを…と探したら、今はM5StickC > Advanced > AXP192 > PowerWakeというスケッチを書き込んで実行させたら、vbat: 0Vだった。

そこで、BAT3V3を短絡すると一度画面が消えるので、そのまま数分放置して、USBケーブルを抜いて、再度挿したらvbat:が0Vじゃなくて、増えていく状態になったので、短絡しているジャンパーを抜いて放置中。

充電中

追記:vbat:は3Vを越えるとだんだん遅くなって、4V以降はなかなか増えない。と思ったら4.19Vになったらいいらしい。

追記:USBケーブルを抜いて、電源OFFして電源ONで動くことを確認できた。でも、みるみるうちにvbat:が減っていき1分くらいで落ちた(電源OFF)。
多分、先ほどの記事の追記部分のように徐々に回復していくんだと思う。

カテゴリー: M5StickCシリーズ | タグ: | コメントする

ADAML2000

2021年6月に購入したのに記事を投稿していなかったので、メモ的に投稿。

ADALM2000はAnalog Devices社のPCに接続して使う計測機器です。 専用アプリのScopyを使って、オシロスコープやシグナル・ジェネレータ、ロジック・アナライザ、他として使えます。 今回は、パッケージの内容の紹介から、PCへの接続。専用アプリScopyでシグナル・ジェネレータ機能で波形を生成して、それをオシロスコープ機能で波形を見るまでを紹介します。

■ 公式の動画

ADALM2000 再生リスト
https://www.youtube.com/playlist?list=PLE6soOeVPOJ0Pj5sMui4KPDiTa7HY50y3

■ 製品ページ

https://www.analog.com/jp/design-center/evaluation-hardware-and-software/evaluation-boards-kits/ADALM2000.html

■ ADALM2000 Overview (Wiki)

https://wiki.analog.com/university/tools/m2k

カテゴリー: 備忘録, 測定装置 | タグ: , | コメントする

MP3再生モジュール(DFPlayer mini)を8ピンPIC(PIC12F1822)で動かす

前回、DFPlayer miniをArduinoで動かしましたが、今回は8ピンのPIC12F1822で動かしました。

前回使ったDFRobotDFPlayerMiniというライブラリは使えないので、同ライブラリを参考にPIC用の関数を書きました。(今はソースコードにテストコードとかがゴチャゴチャ入っているので公開していませんが、それらを整理したら公開したいと思います。)

動作は前回のGetStartedと同じで、ボリュームを20にして、001~004の4曲を8秒間ずつ繰り返し再生するというモノです。

配線図

注意!:単三乾電池は2本ではなく3本使っています。

カテゴリー: PIC, 部品 | タグ: , , | コメントする

MPLAB X IDEをアップデートしたら…

現在、MPLAB X IDE v.6.05で、最新版がv.6.20だったのでxc8(v.2.41→v.2.46)も合わせてアップデートしたら、MCC(MPLAB Code Configurator)も様子が変わっていて、今回使いたかったPIC12F1822の内蔵クロック8MHzのx4 PLLの設定が出来なくて32MHzでの動作が出来なかった(というか、わからなかった)。

また、今回、EUSARTが使いたかったけど、ボーレートの9600とか設定するのもレジスタでの設定しかなくて、EUSART_is_rx_ready()とかEUSART_is_tx_ready()等のサポート関数の生成もされなかったので、旧版に戻した。

カテゴリー: PIC | タグ: , , , , , | コメントする

MP3再生モジュール(DFPlayer mini)をArduinoで動かす

音楽を再生する小さなデバイスを作りたくて、MP3の再生できる小さなモジュールを探していたら、DFPlayer miniというmicroSDカード内のMP3ファイルを再生できるモジュールを見つけたので、今回はそれを紹介します。

今回、購入したのはスピーカーが2つ付属したmicroSDカードスロット大の小さなモジュールです。他にもスピーカーの付属しない3つセットとかもありました。

KeeYees DFPlayer Mini MP3プレイヤー モジュール ミニ 音声モジュール 音源再生モジュール + マグネットスピーカー 磁石内蔵 8オーム 2W 金属シェル

使い方としては「DFPlayer mini」で検索するといろいろ出てきますが、Arduino IDEでライブラリを検索するとDFRobotoDFPlayerMiniというライブラリが出てきたので、それをインストールしました。

サンプルの中にGetStartedというのがあったので、それで試しました。

ソースコードの18行目に回路図のURLが書いてあったので、それを見ながらArduinoと接続して動かしました。

https://wiki.dfrobot.com/DFPlayer_Mini_SKU_DFR0299#Connection_Diagram

DFRobotの解説ページ
DFRobotの解説ページより

変更点は、27行目のソフトシリアルに使うピンを(rx)10と(tx)11にして、さらに68行目の3000を8000にしました。

カテゴリー: Arduino, 部品 | タグ: , , | コメントする

PICkit3が使えなくなった

しばらくPICkit3を使っていなかったら、MPLAB X IDE v6.05で使えなくなった。
そもそも、MPLAB X IDEもしばらく使っていなかったので、最新版(v6.05)をインストールして、xc8コンパイラもv2.41をインストール。

環境はM1 iMac 24インチにVentura 13.3.1で、USB Aポートが無いので、直に接続できないのでAnker製のUSBハブ経由で接続。以前は、Intel iMacだったのでUSB Aポートがあって直挿ししてた。

macOSのシステムレポートのUSBにはPICkit3が出ているので、USB的には認識されていると思うのだけど、MPLAB X IDEから認識されていないっぽい。

USBに挿すタイミングとMPLAB X IDEを起動するタイミングをいろいろ変えても認識されないので、PICkit3をUSBに接続したままでiMacを再起動して、MPLAB X IDEを起動すると認識された。

ただし、一度でもUSBから抜くと再度iMacを再起動しないと認識されないので、とても不便。

カテゴリー: PIC | タグ: , | コメントする

抵抗分圧

抵抗分圧(抵抗器による分圧回路)は、直列につないだ2つの抵抗器の両端にかかる電圧を、2つの抵抗器の全体と片方の抵抗値の比率により分割する回路の事です。

下の回路図のようにVinにかけた電圧がR1, R2の抵抗器によって分圧されてVoutに出てくるというものです。

抵抗分圧の回路図

次にR1, R2によってどのようにVoutが決まる計算式は次のようになります。

計算式

応用編

抵抗分圧の応用として、抵抗値の変化を電圧の変化へ変換するというものがあります。

これはセンサーの一部には抵抗値が変化するものがあり、そのセンサーの値をマイコンで扱う場合、マイコンには電圧を読み取る機能があるので、この抵抗分圧回路に組み込んで使います。

今回はR2にボリューム(可変抵抗器)を使います。

R2を可変抵抗器にした回路図

考察

Google スプレッドシートでR1とR2の比率を1:10, 1:5, 1:2, 1:1の4つのパターンのR2の変化によるVoutをグラフにしてみました。

R1:R2のグラフ

1:10はVoutのレンジが広いですが、最初の部分での変化が大きいです。

1:5, 1:2はVoutのレンジもまあまあ確保できて、変化が穏やか。

1:1はVoutのレンジが半分しか無いですが、変化が直線に近いです。

どのくらいのレンジでVoutが欲しいか、またセンサーの取りうる値の範囲、またどの部分の変化を重視するかでR1:R2を決めるといいと思います。

動画

動画はこちらです。

カテゴリー: 基礎知識 | タグ: | コメントする

PIC16F1508+4PWM(LED)のICSP回路

PIC16F1508の4つのPWMにLEDをつないだ状態のICSP回路図をKiCadで描いた。

PIC16F1508+4PWM(LED)のICSP回路図

カテゴリー: PIC | タグ: , , , | コメントする

PICで複数のPWMを使う(どのチップを使う?)

複数のLEDの光量を制御したいと思って、プロトタイプとしてはArduinoを使うけど、最終的にはPICで実装したいと思って、PWMを4つくらい使えるPICを探していたらPWMが4つあるPIC16F1508を見つけたのでメモ。

カテゴリー: PIC | タグ: , , | コメントする