WindowsでUSB MSCのファイル更新を即時反映する方法|SCSI Unit Attention実装ガイド

USBマスストレージクラス(MSC)のファームウェアで内部的にファイルを生成・削除しても、Windows エクスプローラーは即座に変化を拾ってくれません。ケーブルを抜き差しせずに更新を反映させたい――本記事は、その最短手段である SCSI「ユニットアテンション(Unit Attention)」の実装ポイントを、Windowsの挙動・注意点・検証方法まで含めて丁寧に解説します。

目次

問題の本質と「なぜ更新が見えないのか」

Windows は USB MSC を SCSI デバイスとして扱い、ファイルシステム(FAT/exFAT 等)は OS 側がブロックデバイスをマウントして解釈します。エクスプローラーでディレクトリを開いた後、ホスト側は頻繁に再スキャンしません。さらに、OS はパフォーマンスのためにメタデータをキャッシュします。結果として、デバイス内部でファイルを変更しても、ホストから見た「メディアの中身が変わった」というイベントが発生しない限り、一覧は更新されません。

この「メディアが変わった」を公式に通知するのが、SCSI のユニットアテンション(以下 UA)です。UA はプロトコル標準に記載された正攻法であり、Windows / macOS / Linux を含む主要 OS が理解する共通メカニズムです。

最短の答え:Unit Attention を一度だけ返す

ファイルやディレクトリを変更し、ファイルシステムのメタデータをフラッシュした直後に、次に受け取るホストコマンド(TEST UNIT READY / READ(10) / WRITE(10) 等)に対し「UA が発生した」ことを通知します。ホストは直後に REQUEST SENSE を発行するため、その応答で「メディアが変わった」ことを示すセンスデータを返せば、Windows は自動的にボリュームを再スキャンします。

実装全体像(BOT と UAS の両対応を俯瞰)

手順ホスト ↔ デバイス返すべき値(要点)実装ポイント
変更検知(デバイス内でファイル/ディレクトリ更新完了)FAT/exFAT のメタデータ(FAT テーブル、ディレクトリエントリ、FSInfo 等)を 確実にフラッシュした後、該当 LUN に「UA ペンディング」フラグを立てる。
次コマンドで通知Host → TEST UNIT READY(または READ(10) など)SCSI ステータス=CHECK CONDITION
(※BOT では CSW bCSWStatus=0x01(Command Failed)で通知し、センス取得を促す)
BOT の CSW には SCSI ステータスは載らない。
UAS の場合は SCSI ステータス 0x02 を返し、IU でセンスを後続提供。
センス取得Host → REQUEST SENSE(直後)Sense Key=0x06(UNIT_ATTENTION)
ASC=0x28, ASCQ=0x00
(意味:Not-Ready-to-Ready transition, media changed)
代替として ASC/ASCQ=0x3F/0x03(Inquiry Data Changed)も OS が理解するが、0x28/0x00 が最も汎用的
フラグ解除Device → REQUEST SENSE 応答Good / CSW Passedこのタイミングで UA フラグをクリア。以降は通常応答。UA を出し続けると OS がデバイスを不安定と判断する恐れ。
Windows の動作Host → 再スキャンエクスプローラーに新規/変更/削除が即時反映。ユーザー操作(F5)不要。

センスコードの選択基準と意味

Sense KeyASC/ASCQ意味用途と備考
0x06(UNIT_ATTENTION)0x28 / 0x00Not-Ready → Ready へ遷移(メディア変更)推奨。Windows が確実に「メディア内容変更」と解釈し、再スキャンされる。
0x060x3F / 0x03Inquiry Data Changed識別情報が変わったことを表す。再スキャンはされるが、0x28/0x00 の方が意図が明確。
0x060x29 / 0x00Power On, Reset, or Bus Device Resetリセット通知。意図以上に重い扱いになり得る。通常の更新通知には不向き。

BOT と UAS の違い(実装で混同しやすいポイント)

  • BOT(Bulk-Only Transport):USB スタックが CBW(Command Block Wrapper)を受け取り、CSW(Command Status Wrapper)で結果を返します。
    UA 通知時は CSW の bCSWStatus=0x01(Command Failed)を返し、ホストに REQUEST SENSE を促します。SCSI ステータス 0x02(CHECK CONDITION)は CSW には載らず、センスデータで意味付けされます。
  • UAS(USB Attached SCSI):SCSI ステータスとセンス IU を使ってよりリッチに通知します。マイコン実装は BOT が主流ですが、UAS を実装している場合は SCSI ステータス 0x02 を返し、続くセンス IU で 0x06/0x28/0x00 を渡します。

コード例(最小実装の雛形)

1) 共通:LUN 状態管理

// LUN ごとに UA の有無と内容を保持
typedef struct {
    bool ua_pending;
    uint8_t sense_key; // 0x06
    uint8_t asc;       // 0x28
    uint8_t ascq;      // 0x00
} lun_state_t;

static lun_state_t lun[NUM_LUNS];

// ファームウェア内部でファイル更新が完了したら呼ぶ
void msc_notify_media_changed(uint8_t lun_id) {
// 先にファイルシステムのメタデータを必ずフラッシュ
fs_flush_all(lun_id);
lun[lun_id].ua_pending = true;
lun[lun_id].sense_key  = 0x06;
lun[lun_id].asc        = 0x28;
lun[lun_id].ascq       = 0x00;
} 

2) BOT:SCSI デコーダの要所

// BOT のコマンド入口(CBW 受信後)
msc_status_t scsi_dispatch(uint8_t lun_id, const scsi_cdb_t* cdb) {


// REQUEST SENSE 以外のコマンドで UA が立っているなら、まず UA を知らせる
if (lun[lun_id].ua_pending && cdb->opc != SCSI_OPCODE_REQUEST_SENSE) {
    // センスバッファに UA をセット(デバイス側に保持)
    scsi_set_sense(lun_id, lun[lun_id].sense_key, lun[lun_id].asc, lun[lun_id].ascq);

    // BOT の流儀:このコマンドは「失敗」で返し、ホストに REQUEST SENSE を促す
    bot_send_csw(CSW_STATUS_FAILED); // bCSWStatus = 0x01
    return MSC_STATUS_HANDLED;
}

switch (cdb->opc) {
case SCSI_OPCODE_TEST_UNIT_READY:
    bot_send_csw(CSW_STATUS_PASSED);
    return MSC_STATUS_HANDLED;

case SCSI_OPCODE_REQUEST_SENSE: {
    uint8_t buf[18];
    size_t n = scsi_build_sense_buffer(lun_id, buf, sizeof(buf));
    usb_bulk_in(buf, n);
    bot_send_csw(CSW_STATUS_PASSED);
    // <重要> UA をここでクリア(1回限りの通知)
    lun[lun_id].ua_pending = false;
    return MSC_STATUS_HANDLED;
}

// ... READ(10), WRITE(10), INQUIRY, MODE SENSE など通常処理 ...
default:
    return scsi_handle_default(lun_id, cdb);
}


} 

3) TinyUSB の場合(コールバック例)

// TinyUSB: tud_msc_scsi_cb でカスタム応答
int32_t tud_msc_scsi_cb(uint8_t lun_id, uint8_t const scsi_cmd[16],
                        void* buffer, uint16_t bufsize) {
    uint8_t opc = scsi_cmd[0];


if (lun[lun_id].ua_pending && opc != SCSI_OPCODE_REQUEST_SENSE) {
    // UA をセットして "Command Failed" を示す
    tud_msc_set_sense(lun_id, 0x06, 0x28, 0x00); // Unit Attention / Media Changed
    // <戻り値 -1 で "Command Failed" を示し、ホストに REQUEST SENSE を促せる実装に調整>
    return -1;
}

if (opc == SCSI_OPCODE_REQUEST_SENSE) {
    // TinyUSB の API でセンスバッファは自動構築されることが多い
    lun[lun_id].ua_pending = false;
    // 実データ長を返す
    return build_request_sense(buffer, bufsize);
}

// 以降は通常の READ/WRITE 等にフォールバック
return handle_scsi_default(lun_id, scsi_cmd, buffer, bufsize);


} 

4) STMicroelectronics USB Device Library の場合

// usbd_msc_scsi.c / BOT 層の分岐で UA を返す
int8_t SCSI_ProcessCmd(USBD_HandleTypeDef *pdev, uint8_t lun_id, uint8_t *params) {
    uint8_t opc = params[0];


if (lun[lun_id].ua_pending && opc != SCSI_OPCODE_REQUEST_SENSE) {
    SCSI_SenseCode_SET(pdev, lun_id, 0x06, 0x28, 0x00);
    MSC_BOT_SendCSW(pdev, CSW_CMD_FAILED); // bCSWStatus = 0x01
    return 0;
}

if (opc == SCSI_OPCODE_REQUEST_SENSE) {
    SCSI_RequestSense(pdev, lun_id, params);
    MSC_BOT_SendCSW(pdev, CSW_CMD_PASSED);
    lun[lun_id].ua_pending = false;
    return 0;
}

// 通常処理...
return SCSI_DefaultCmd(pdev, lun_id, params);


} 

フラッシュとキャッシュ整合性:UA の前に必ずやること

  • FAT テーブル/ディレクトリエントリの書き込み完了:クラスタ割り当て、ファイルサイズ、タイムスタンプ、属性(Archive ビット)を更新。
  • FSInfo(FAT32):空きクラスタ数・次探索クラスタを更新。Windows はここも参照するため、ズレがあると整合性検査のトリガーになることがあります。
  • ライトキャッシュのフラッシュ:ストレージ層(NAND/SD 等)に対して実書き込みを完了させる。電源断耐性を意識。
  • 排他制御:ホストからの WRITE と競合させない。UA 送出直前・直後のクリティカルセクションでホスト I/O とデバイス内 I/O が交差しないようにする。

Windows の挙動とポリシーの影響

  • 再スキャンのトリガー:UA(0x06/0x28/0x00)を受け取ると、Windows はボリュームを再解析し、エクスプローラー一覧が自動更新されます。
  • デバイスの「取り外しポリシー」:既定の「クイック削除」でも UA は有効。ポリシーは主に書き込みキャッシュの有効/無効に影響しますが、UA による再スキャン自体は機能します。
  • ハンドル保持中の挙動:ホストアプリが対象ファイル/ディレクトリにオープンハンドルを持つ場合、表示更新が遅れることがあります。UA は「メディアの変化」を知らせるもので、開放されていないハンドルを強制的に更新するものではありません。
  • RMB(Removable Media Bit):INQUIRY データの RMB=1(リムーバブル)であることは、多くの実装で前提。固定ディスク風の振る舞いにすると、OS のリカバリー挙動が変わる場合があります。

ありがちな誤解・落とし穴

  • 「CSW で 0x02 を返す」:BOT の CSW は 0x00(Passed)、0x01(Failed)、0x02(Phase Error)の 3 種であり、CHECK CONDITION を直接表すフィールドはありません。UA を知らせるときは CSW=Failed とし、センスで 0x06/0x28/0x00 を返すのが正しい流儀です。
  • UA の出しっぱなし:UA は 1 回の通知で十分。REQUEST SENSE 応答後にフラグをクリアしないと、Windows がデバイスを故障と判断する恐れがあります。
  • 未フラッシュで UA:メタデータが不整合のまま再スキャンさせるのは厳禁。表示は更新されても、読み出した内容が壊れて見える・チェックディスクの対象になる等の副作用が出ます。
  • インターフェースの瞬断でごまかす:MSC インターフェースを Disable→Enable で再枚挙させる方法はデバッグ向けの荒技。本番では UA を使うこと。

INQUIRY と UA の関係(識別情報を変えるべきか)

「Inquiry Data Changed(0x3F/0x03)」は、INQUIRY で返す識別情報(Vendor/Product/Revision 等)が変わったことを示すコードです。実際には識別情報を変えずに 0x28/0x00 を使うだけで十分です。識別情報を頻繁に変えると、OS のデバイス管理(ドライババインド、キャッシュ)に余計な影響を与える可能性があります。

タイミングと状態遷移をもう一度整理

[デバイス内の更新完了] 
   ↓ (fs_flush_all)
[UA フラグ = true]
   ↓ (Windows が周期的に TUR: TEST UNIT READY を投げてくる)
[Host: TEST UNIT READY]
   ↓
[Device: CSW=Failed(BOT)/ SCSI=CHECK CONDITION(UAS)]
   ↓
[Host: REQUEST SENSE]
   ↓
[Device: Sense = 0x06/0x28/0x00, UA フラグ = false]
   ↓
[Host: ボリューム再スキャン → エクスプローラー更新]

複数 LUN / 複数更新の扱い

  • UA は LUN 単位:1 つの物理デバイスに複数 LUN がある場合、更新が発生した LUN にだけ UA を立てる。関係ない LUN に連鎖させない。
  • 連続更新:更新のたびに UA を立てればよいが、ホストが REQUEST SENSE を取り終える前に UA を再度上書きしないよう注意。必要であれば「UA 発火→クリア→再更新で再度 UA」という順序を守る。

ファイルシステム実装のチェックリスト

  • 時刻スタンプ:ローカル時刻/UTC の扱いを決め、DOS タイムスタンプを正しくエンコードする。
  • ロングファイル名(LFN):短名(8.3)と整合。途中で中断しない。
  • ディレクトリエントリの Archive ビット:新規/更新で立てる(バックアップや同期ツールが見る)。
  • FAT32 の FSInfo:空きクラスタ数と Next Free を適切に更新。
  • リードオンリー属性・隠し属性:エクスプローラー表示への影響を理解。

検証方法:Windows での動作確認と USB トレース

  1. デバイスを接続し、エクスプローラーで対象ドライブを開く。
  2. ファームウェア機能(ボタン/シリアルコマンド等)でファイルを新規作成・削除。
  3. UA が正しく実装されていれば、数百 ms~数秒で一覧が自動更新される。
  4. うまくいかない場合は USB トレースを取得(USBPcap + Wireshark 等)。
    直前の TUR → CSW Failed(BOT)→ REQUEST SENSE → センス 0x06/0x28/0x00 の往復が見えることを確認。
  5. ファイルシステムの整合性は Windows の「プロパティ → ツール → エラーチェック」や、実ファイルのハッシュ比較で点検。

UASP 実装時の補足

  • キューイングが効くため、UA の通知→センス返却の流れがパイプライン化されることがあります。SCSI ステータスとセンス IU の整合性に注意。
  • Windows が UASP で接続した場合でも、0x06/0x28/0x00 の意味は同じ。テストには UASP/MSC BOT の両モードを確認すると安心です。

代替策の是非

方法可否解説
インターフェースの再起動(Disable→Enable)避けたい再枚挙で更新は反映されるが、ホスト側から見て瞬断。マウント/ドライブレターの再取得やアプリの I/O 失敗を誘発しやすい。
START STOP UNIT / PREVENT-ALLOW の操作限定的メディア入れ替えの概念に紐づくが、「内容が変わった」の明示としては UA の方が意図が明確。
独自ベンダークラスで通知不可エクスプローラーの再スキャンは MSC/SCSI の世界で完結しており、ベンダークラス通知では標準の挙動にならない。

実運用のベストプラクティス

  • 「更新ブロック」を設ける:デバイス内の更新中はホストからの書き込みを一時的に拒否(WRITE(10) に対し Busy/Not Ready を返す等)し、更新完了後に UA を 1 回だけ発火。
  • 更新の粒度をまとめる:高頻度の UA はホスト負荷になる。数秒単位で変更をバッチングして 1 回の UA に集約。
  • 障害復旧のルール化:USB リセットやサスペンド/レジューム後、UA の再通知が必要なケース(例:電源断からの復帰)を定義。
  • ログを埋め込む:UA 発火・REQUEST SENSE 受信・クリアをデバイスログに記録し、フィールド障害解析を容易にする。

トラブルシュート早見表

症状考えられる原因対処
エクスプローラーが更新されないUA を出していない/出しっぱなしで OS に無視された/センスコードが誤り0x06/0x28/0x00 を 1 回だけ通知。
REQUEST SENSE 返答後に UA を確実にクリア。
「ディスクのエラーをチェックしてください」メタデータ未フラッシュ/FSInfo 不整合/LFN 破損更新前後でフラッシュ徹底、FSInfo 更新、LFN の整合確認。
ホストが繰り返し REQUEST SENSE を送るUA をクリアしていない/別のエラーセンスを返し続けているREQUEST SENSE 応答後に UA フラグを必ず false に。
たまにドライブが消える再枚挙やリセットを使っている/UAS でのエラー処理の不整合再枚挙に頼らず UA で通知。UAS の IU とステータス整合を見直す。

テストスクリプト例(組み込み側トリガ)

// 疑似 API:ファイル生成→メタデータ flush→UA 通知
void create_log_file_and_notify(uint8_t lun_id) {
    file_t f = fs_open("/LOG/NEW_001.TXT", "w");
    if (!f) return;


// 何かデータを書き込む
fs_write(f, "hello, world\r\n", 14);
fs_close(f);

// メタデータと媒体のフラッシュ
fs_flush_all(lun_id);
storage_sync(lun_id);

// UA で Windows に知らせる
msc_notify_media_changed(lun_id);


} 

QA:現場でよく出る疑問

  • Q. TUR(TEST UNIT READY)以外でも UA を返せる?
    A. 返せます。最初に来たコマンド(READ/WRITE など)に対して CSW=Failed(BOT)→ REQUEST SENSE で十分です。ただし WRITE の途中で返すのは避け、境界で返すと安全です。
  • Q. 0x3F/0x03 と 0x28/0x00 のどちらを使うべき?
    A. 一般には 0x28/0x00(Media Changed)が意図に合致し、互換性も高い。0x3F/0x03 は識別情報変更の意味合いが強く、頻繁使用は推奨しません。
  • Q. 一覧は更新されたがファイルが空/壊れている
    A. フラッシュの不足が濃厚。クラスタ割当・サイズ・タイムスタンプ・FSInfo の同期を再確認してください。
  • Q. macOS/Linux でも効く?
    A. UA は SCSI 標準動作のため、主要 OS で概ね同様に認識されます(実装差による再スキャンのタイミング差はあり)。

チェックリスト(導入前の最終確認)

  • 更新完了後に UA を 1 回だけ発火している。
  • BOT:CSW=0x01(Failed)で REQUEST SENSE を促している。
  • Sense=0x06/0x28/0x00 を返している。
  • REQUEST SENSE 応答の直後に UA フラグをクリアしている。
  • FAT/exFAT のメタデータを 確実にフラッシュしている。
  • READ/WRITE と内部更新が衝突しないよう 排他制御している。
  • USB ログで TUR→Failed→REQUEST SENSE→Good の流れを確認済み。

まとめ

USB MSC デバイスで、Windows に「内容が変わった」と認識させる正攻法は SCSI のユニットアテンションです。変更をフラッシュ→次コマンドで UA 告知→REQUEST SENSE に 0x06/0x28/0x00 を返す――これだけで、エクスプローラーは自動的に再スキャンし、ユーザーは抜き差しや F5 連打から解放されます。BOT/UAS の違いを押さえ、UA は 1 回だけ、センスは標準コードで、という基本を守れば堅牢に動きます。ファイルシステムの整合性と排他制御を徹底し、テストで動作を見える化すれば、現場投入後のトラブルも大幅に低減できます。


付録:参考の小さな実装メモ

  • INQUIRY:Peripheral Device Type=0(Direct-Access)、RMB=1(推奨)。
  • MODE SENSE/MODE SELECT:書き込みキャッシュ設定は OS のポリシーと整合を取る。
  • REQUEST SENSE のデータ長:古典的 18 バイトに合わせておくと安全(OS は短くても解釈可能だが互換性を狙う)。
  • 時間管理:RTC が無い場合は固定時刻でもよいが、タイムスタンプが更新されないと「変わった感」が薄く見える場合がある。
  • エラー復帰:Mass Storage Reset を乱用せず、まずは UA→再スキャンで復旧を図る。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次