Skip to main content

atmos/kernel/
usb_rx_state.rs

1//! L1/L2: USB Ethernet 受信チャネルの状態遷移(純粋ロジック)。
2//!
3//! ## なぜこのモジュールがあるか
4//! OSI 下位層(L1/L2)だけ単体試験が無いまま、L4(TCP)/L7(CSS) を延々と
5//! 追いかけていた。実測の結果、実際の障害はここにあった:
6//!
7//! ```text
8//! [NETBENCH][RES] before=2 rx_frames=14 ...
9//! [NETBENCH][RES] before=5 rx_frames=14   ← 1 回目以降 1 フレームも増えない
10//! ```
11//!
12//! 1 回目の通信の後、USB Ethernet が受信を完全に停止する。
13//! DNS タイムアウト・TCP 接続タイムアウト・受信打ち切りは
14//! **すべてこの単一症状の現れ**だった。
15//!
16//! ## 元の実装の問題
17//! ```text
18//! if RX_IN_FLIGHT {
19//!     if (hcchar & CHENA) != 0 { return; }   // ← break ではなく return
20//!     ...
21//! ```
22//! チャネルが有効なままだとポーリング関数全体を `return` する。
23//! CHENA が落ちない状態に陥ると受信が永久に停止し、
24//! しかも `ints` のチェックに到達しないので**エラーログすら出ない**
25//! (実測でも異常停止カウンタは 0 件だった)。
26//!
27//! 本モジュールはこの判断をレジスタ値から純粋に導出し、単体試験で固定する。
28
29/// 受信ポーリングが次に取るべき動作。
30#[derive(Debug, PartialEq, Eq, Clone, Copy)]
31pub enum RxAction {
32    /// 転送中でまだデータが来ていない。今回は何もせず次のポーリングを待つ。
33    /// **呼び出し側は関数を抜けてよいが、他の処理を止めてはいけない。**
34    WaitInFlight,
35    /// 転送完了。データを取り込む。
36    Harvest,
37    /// 転送は終わったが完了ビットが立っていない(エラー等)。
38    /// 記録したうえで**必ず再アームする**。ここで諦めると受信が永久に止まる。
39    AbnormalHalt,
40    /// 転送が飛んでいない。新規に受信を開始する。
41    Arm,
42    /// NAK 応答。異常ではないので記録せず、ただちに再アームする。
43    Rearm,
44}
45
46/// タイムアウト判定に使う、転送中のまま許容する最大ポーリング回数。
47/// これを超えたら CHENA が落ちなくなったとみなして強制的に再アームする。
48pub const MAX_IN_FLIGHT_POLLS: u32 = 200;
49
50/// DWC2 の HCINT ビット(受信で参照するもの)。
51pub const HCINT_XFER_COMPL: u32 = 1 << 0;
52/// NAK。Bulk IN では「今は送るデータが無い」という**正常な応答**。
53/// デバイスは次のトークンを待っているので、**チャネルを再アームすればよい**。
54pub const HCINT_NAK: u32 = 1 << 4;
55pub const HCINT_STALL: u32 = 1 << 3;
56pub const HCINT_XACT_ERR: u32 = 1 << 7;
57pub const HCINT_BBL_ERR: u32 = 1 << 8;
58pub const HCINT_FRM_OVRUN: u32 = 1 << 9;
59pub const HCINT_DATA_TGL_ERR: u32 = 1 << 10;
60
61/// HCCHAR のチャネル有効ビット。
62pub const HCCHAR_CHENA: u32 = 1 << 31;
63
64/// 受信チャネルの次の動作を決める。
65///
66/// - `in_flight` … 受信転送を発行済みか
67/// - `hcchar` … HCCHAR レジスタ値
68/// - `hcint` … HCINT レジスタ値
69/// - `polls_in_flight` … 転送中のままポーリングした回数
70///
71/// **`WaitInFlight` は `MAX_IN_FLIGHT_POLLS` 回までしか返さない。**
72/// それを超えたら `AbnormalHalt` を返して再アームさせる。
73/// 元の実装は無条件に待ち続けたため、CHENA が落ちない状態に陥ると
74/// 受信が永久に停止していた。
75pub fn decide(in_flight: bool, hcchar: u32, hcint: u32, polls_in_flight: u32) -> RxAction {
76    if !in_flight {
77        return RxAction::Arm;
78    }
79    if (hcchar & HCCHAR_CHENA) != 0 {
80        // 【2026-07-29 実測】CHENA が立ったまま HCINT=0x10(NAK のみ)で
81        // 張り付く状態が実機で観測された(`HCCHAR=0x80b89040 HCINT=0x00000010`)。
82        // Bulk IN の NAK は「今はデータが無い」という**正常な応答**であり、
83        // 異常ではない。デバイスは次のトークンを待っているので、
84        // **ただちに再アームする**のが正しい。
85        // 従来は「CHENA が立っている=転送継続中」と解釈して無条件に待ち続け、
86        // この状態から永久に抜けられず受信が停止していた。
87        if (hcint & HCINT_NAK) != 0 {
88            return RxAction::Rearm;
89        }
90        // NAK 以外で CHENA が落ちない場合も、永久には待たない。
91        if polls_in_flight >= MAX_IN_FLIGHT_POLLS {
92            return RxAction::AbnormalHalt;
93        }
94        return RxAction::WaitInFlight;
95    }
96    if (hcint & HCINT_XFER_COMPL) != 0 {
97        return RxAction::Harvest;
98    }
99    // 【2026-07-29 実測で判明】CHENA が落ちた後も HCINT=0x10(NAK のみ)で
100    // 戻ってくる経路があり、これを「異常停止」に分類していたため
101    // 21504 回もエラーログを出していた。NAK はエラーではないので、
102    // CHENA の有無に関わらず**常に再アーム**へ倒す。
103    if (hcint & HCINT_NAK) != 0 {
104        return RxAction::Rearm;
105    }
106    RxAction::AbnormalHalt
107}
108
109/// 1 回のポーリングで回収できるフレーム数の上限。
110///
111/// 【2026-07-30】受信ポーリングは Core 2 の `net_service_thread` が
112/// `sleep(10)`(=100ms)間隔で呼ぶ。1 回あたり `HARVEST_BUDGET` フレームしか
113/// 取らないと **毎秒 `HARVEST_BUDGET * 10` フレームが上限**になる。
114/// 8 だと毎秒 80 フレーム(1500B 換算で約 120KB/s)で、
115/// 49KB の CSS 取得や DNS 応答の回収に何往復も要してタイムアウトしていた。
116pub const HARVEST_BUDGET: u32 = 64;
117
118/// ポーリング 1 回あたりの理論最大フレーム数を求める。
119///
120/// `poll_interval_ms` が 0 は不正(ゼロ除算)なので、
121/// 黙って戻らずエラーログを出して `None` を返す。
122pub fn frames_per_second(harvest_budget: u32, poll_interval_ms: u32) -> Option<u32> {
123    if poll_interval_ms == 0 {
124        crate::error!("[USB][RX] ポーリング間隔が 0 です");
125        return None;
126    }
127    Some(harvest_budget.saturating_mul(1000 / poll_interval_ms.max(1)))
128}
129
130/// HCINT からエラー要因を人が読める形にする(診断ログ用)。
131pub fn describe_halt(hcint: u32) -> (bool, bool, bool, bool, bool) {
132    (
133        (hcint & HCINT_STALL) != 0,
134        (hcint & HCINT_XACT_ERR) != 0,
135        (hcint & HCINT_DATA_TGL_ERR) != 0,
136        (hcint & HCINT_BBL_ERR) != 0,
137        (hcint & HCINT_FRM_OVRUN) != 0,
138    )
139}
140
141/// 受信バイト数を求める。`rx_len` は要求長、`hctsiz_remaining` は残り転送量。
142///
143/// 残りが要求長を超えるのは異常(レジスタ読み取り失敗等)なので、
144/// 黙って巨大な値を返さず `None` にする。
145pub fn received_len(rx_len: u32, hctsiz_remaining: u32) -> Option<usize> {
146    if hctsiz_remaining > rx_len {
147        return None;
148    }
149    Some((rx_len - hctsiz_remaining) as usize)
150}