このファイルがどのバスかすら分からない
目標
I2Cのキャプチャ4つと、SPIのキャプチャ3つを自分でデコードし、最後に、名前が付いていないキャプチャを3つ受け取って、どのバスかを見分け、故障に数字の証拠を付けて報告します。
なぜ重要なのか
ドライバーが出す「デバイスが応答しません」の一行には、異なる4つの原因がまとまっています。アドレスが間違っている場合、スレーブが時間を稼いでいる場合、プルアップが弱くて立ち上がりがビット時間を使い切っている場合、バスがまるごとロックされている場合です。この4つは、波形ではまったく違う形をしていて、直す人もそれぞれ違います。ファームウェア、ドライバーの復旧手順、ハードウェアです。波形で見分ければ、報告書の一行が担当者1人を指名します。SPI側は逆に、クロックがあわせて来るので速度の問題がなく、その代わり、どのエッジで読むかと選択線がいつ下がるかが、静かな故障を生みます。
ステップ
- 2本の線のキャプチャを読む: サンプルレート、立ち上がり時間、SCLのロー区間の分布を測ります。
- STARTとSTOPを探す: SCLがハイの間のSDAの変化だけを、条件として数えます。
- アドレスとACKを読み出す: 9ビットの束で、バイトとACKを復元します。
- 誰も応答しなかったアドレス: NACKが、実際には何の不在なのかを確認します。
- マスターが作っていない区間: クロックストレッチングを、長さの分布で見つけ出します。
- 立ち上がりがビット時間を使い切る: 弱いプルアップを、立ち上がり時間と2つのしきい値で証明します。
- 4つの組み合わせのどれか: アイドルレベルと、データが変わるエッジで、モードを決めます。
- 選択線が遅いと個数が先に語る: 8の倍数が崩れることを見ます。
- 名前のないキャプチャ3つを見分ける: バスと故障を、証拠とあわせて報告します。
参考
- 材料は、
/opt/fixtures/serialbus/の下にあります。i2c/read-ok.csv、i2c/nack.csv、i2c/stretch.csv、i2c/weak-pullup.csv、spi/mode0.csv、spi/mode3.csv、spi/cs-late.csv、そしてunknown/capture-1.csvからcapture-3.csvまでです。形式の説明は/opt/fixtures/serialbus/README.mdです。 - チャンネル名は、ファイルごとに異なります。I2Cは
scl・sda、SPIはcs・sclk・mosi・miso、正体のわからないキャプチャはch0からです。名前に意味を期待せず、ヘッダーから読んでください。 - よくあるミスの1つ目は、SCLがハイの間のSDAの変化を探すとき、エッジと重なるサンプルまで数えてしまうことです。
scl[i]とscl[i-1]がどちらもハイのときだけが条件です。 - よくあるミスの2つ目は、最後のバイトのNACKを故障として報告してしまうことです。読み取りの転送では、正常な終了の手順です。
- 供給電圧は3.30Vで、既定の判定のしきい値は、その半分の1.65Vです。VIHを使うよう指示したステップでだけ、0.7倍を使います。
- 追加のインストールもインターネットも必要ありません。python3だけを使います。
- 目安は85分なので、基本の60分が終わる前に、+時間で延長してください(最大180分)。セッションが終わると
/rootは消えるので、残したいものは別に保管してください。
2本の線のキャプチャを読む
mkdir -p /root/bus-triageで作業フォルダーを作り、/opt/fixtures/serialbus/i2c/read-ok.csvを読んで、/root/bus-triage/levels.jsonに、sample_rate_hz、sample_count、duration_us、channels(ヘッダーのチャンネル名のリスト)、scl_rise_time_samples、sda_rise_time_samples、scl_low_runs、scl_low_median_usを保存してください。立ち上がり時間は、キャプチャファイルで最後に起きる完全な立ち上がり(0.33V以下から2.97V以上まで)のサンプル数で、SCLのロー区間は、下がったサンプルから、再び上がったサンプルまでです。
0.33Vと2.97Vは、3.30Vの10%と90%です。ロー区間は、立ち下がりエッジと、その次の立ち上がりエッジの間で、最後までローのまま終わる区間は、長さがわからないので、数えません。
STARTとSTOPを探す
/root/bus-triage/i2c.pyにconditions(scl_v, sda_v, threshold_v=1.65)を実装してください。SCLが2サンプル連続でハイの間に、SDAが下がれば("start", i)、上がれば("stop", i)を、時間の順に格納して返します。SCLがローのときのSDAの変化は、条件ではありません。read-ok.csvに適用して、/root/bus-triage/i2c-frames.jsonに、start_count、stop_count、starts、stops(サンプル番号のリスト)、scl_high_windows(上がってから、再び下がるところまで確認できたSCLのハイ区間の数。最後までハイのまま残った区間は、長さがわからないので、数えません)を保存してください。
条件の判定は、scl[i]とscl[i-1]が、どちらもハイのときだけ行います。エッジと重なる瞬間を除いてはじめて、繰り返しSTARTをデータと勘違いしません。採点ツールは、短い試験表で先に確認します。
アドレスとACKを読み出す
read-ok.csvを最後までデコードしてください。SCLのハイ区間の真ん中でSDAを読んでビットを集め、9ビットごとに、前の8ビットを上位から束ねてバイトに、9番目のビットが0ならACKとみなします。/root/bus-triage/i2c-read.jsonに、bytes(各項目はvalueとack)、address(最初のバイトを1ビット右にずらしたもの)、first_rw、register、second_rw、data(3つ目のバイトのあとの値)、repeated_starts(サンプル番号のリスト)、last_byte_ackedを保存してください。first_rwとsecond_rwは、"read"または"write"です。
繰り返しSTARTは転送を終わらせないので、バイトを数える流れは初期化しますが、転送は続けます。最後のバイトのNACKは、故障ではなく、マスターがもう送らなくてよいと知らせる、正常な手順です。
誰も応答しなかったアドレス
/opt/fixtures/serialbus/i2c/nack.csvをデコードして、/root/bus-triage/i2c-nack.jsonに、start_n、stop_n、byte_count、address、rw、acked(9番目のビットが0だったか)、data_bytes_after_address(全体のバイト数から1を引いた値)を保存してください。
ACKの位置では、マスターはSDAを離します。ハイのまま残ったということは、誰も引き下げなかったという意味で、アドレスの誤り・電源なし・デバイスの故障が、すべて同じ形です。マスターがすぐにSTOPを出していることも確認してください。
マスターが作っていない区間
/opt/fixtures/serialbus/i2c/stretch.csvのSCLのロー区間の長さをすべて測って、/root/bus-triage/i2c-stretch.jsonに、scl_low_runs、median_low_us、max_low_us、stretch_us(最大値から中央値を引いた値)、stretch_start_n(最も長い区間が始まるサンプル番号)、bytes(デコードしたバイトのリスト)、stop_nを保存してください。
中央値を使う理由は、平均が長い区間1つに引っ張られるからです。延びた区間が、何番目のバイトのあとにあるかを、サンプル番号で指し示してみると、スレーブがいつ時間を稼いだかが見えます。
立ち上がりがビット時間を使い切る
/opt/fixtures/serialbus/i2c/weak-pullup.csvを測ってください。/root/bus-triage/i2c-pullup.jsonに、sda_rise_time_us、scl_rise_time_us(どちらも10%から90%まで)、scl_high_windows(ステップ2と同じ定義)、sda_peak_v(各SCLのハイ区間の中のSDAの最大値のうち最大のもの)、ones_at_1v65とones_at_vih(その区間の最大値が、それぞれ1.65VとVIH以上である区間の数)、vih_v(0.7に3.30を掛けた値を、小数第2位に丸めたもの)、stops_at_1v65とstops_at_vih(SDAの判定のしきい値だけを変えたときに検出されるSTOP条件の数)を保存してください。
SDAとSCLの立ち上がり時間を比べると、どちらの線のプルアップが弱いかがすぐにわかります。STOPの個数がしきい値によって変わるなら、遅い立ち上がりがSCLのハイ区間の中にはみ出して、条件のように見えるという意味です。
4つの組み合わせのどれか
/root/bus-triage/spi.pyにdecode(cs_v, sclk_v, mosi_v, miso_v, cpol, cpha, threshold_v=1.65)を実装してください。CSがローの間、cphaが0なら(1−cpol)の値へ向かうエッジで、1ならcpolの値へ向かうエッジで、サンプルします。{"edges": サンプルしたエッジの数, "mosi": バイトのリスト, "miso": バイトのリスト, "leftover_bits": エッジの数を8で割った余り}を返し、ビットは上位から束ねます。/opt/fixtures/serialbus/spi/mode0.csvとmode3.csvをそれぞれ判定して、/root/bus-triage/spi-mode.jsonに、mode0とmode3の2つのブロックで、sclk_idle、cpol、cpha、mode、sampling_edges、mosi_changes_on、mosi、misoを保存してください。
cpolは、最初のサンプルのクロックのレベルです。cphaは、MOSIが変わる時刻の直前(8サンプル以内)の、クロックのエッジの方向で決めます。そのエッジは、読む側ではありません。その方向がアイドルレベルと同じならcphaは0、異なれば1です。modeは、cpol×2+cphaです。
選択線が遅いと個数が先に語る
/opt/fixtures/serialbus/spi/cs-late.csvをモード0でデコードして、/root/bus-triage/spi-cs.jsonに、cs_low_start、cs_low_end(CSが下がったサンプルと、再び上がったサンプル)、sclk_edges_total(キャプチャ全体のクロックのエッジの数)、sampling_edges_total(その半分)、sampling_edges_in_cs(CSがローの間にサンプルしたエッジの数)、leftover_bits、bytes(MOSIのバイトのリスト)を保存してください。
値よりも個数が先におかしくなります。CSの外に抜けたエッジがあれば、8の倍数が崩れて、余るビットが生じます。その状態で束ねたバイトは、すべて1つずつずれています。
名前のないキャプチャ3つを見分ける
/opt/fixtures/serialbus/unknown/のcapture-1.csv、capture-2.csv、capture-3.csvを判定して、/root/bus-triage/triage.jsonに、capture_1、capture_2、capture_3の3つのブロックを保存してください。3つのブロックすべてに、channels(チャンネル数)、bus("uart"・"i2c"・"spi"のいずれか)、fault("polarity-inverted"・"sda-stuck-low"・"miso-idle"・"baud-mismatch"・"address-nack"・"clock-stretch"・"weak-pullup"・"cs-timing"・"mode-mismatch"のいずれか)を入れます。これに加えて、capture_1にはidle_v・baud・textを、capture_2にはstart_count・stop_count・longest_sda_low_usを、capture_3にはmiso_edges_in_cs・miso_bytes・mosi_bytesを入れます。
チャンネル数とアイドルレベルだけで、バスはほぼ決まります。1本なのにローで休んでいるなら、すべてのビットを反転させて読んでみてください。2本なのにSTOPがないなら、何がSTOPを妨げているのかを見てください。4本で、転送の間ずっと一度も変わらない線は、それ自体が証拠です。