Claudeでデータシートのレジスタ表をC構造体に変換 — リセット値の照合まで
データシートのレジスタ表PDFをいったん仕様データに起こし、ビットフィールドのC構造体を生成して、リセット値とアドレスをスクリプトとCのテストで機械照合する手順です。
マイコンやペリフェラルのデータシートには、レジスタ一覧とビットフィールドの表が何十ページも続きます。これを手でC構造体に写すと、ビット位置の1つのずれが、実機で動かして初めて分かる不具合になります。
この記事の方針は1つです。PDFから構造体を直接作らせず、いったん仕様データ(JSON)に起こし、リセット値とアドレスを機械で照合してから構造体を生成する。Claudeには「読む」仕事だけを任せ、正しさの判定はスクリプトとCのテストに回します。
PDFを渡す前に、範囲と読み取り経路を決める
レジスタ表は、小さな文字の表が密に並ぶページです。Claude APIでは、PDFの各ページが画像に変換され、抽出したテキストが画像と一緒に渡されます。画像として読むので、視覚機能全般の制約がそのまま効きます。
- 低品質・回転・200ピクセル未満の小さな画像では、誤読や幻覚が起こることがある
- 小さな文字の多いページ、複雑な表、重い図版のPDFは、ページ上限の手前でコンテキストウィンドウを使い切ることがある
- 1リクエストの上限は600ページ。コンテキストウィンドウが1Mトークン未満なら100ページ
- パスワードや暗号化のかかったPDFは扱えない。リクエスト全体のサイズ上限は32MBで、同じリクエストに添えた他の内容も数えられる
数百ページ・数十MBになりがちなデータシートは、この上限に当たりやすい資料です。配布元のPDFにパスワードがかかっているなら、先に外したものを用意します。対策は、データシート全体ではなく対象ペリフェラルの章だけを渡すことです。性能面の指針にも、PDFを質問文の前に置く、ページを正しい向きに直す、PDFビューアーのページ番号で指示する、大きなPDFは分割する、が並びます。
Claude Codeで読ませる場合、10ページを超えるPDFはpagesパラメーターで範囲を区切って読まれ、1回に20ページまでです。範囲指定の読み取りはpoppler-utilsのpdftoppmでページを描画するため、先に入れておきます。
brew install poppler # macOS
sudo apt-get install poppler-utils # Debian / UbuntuAPIから同じPDFを何度も使うなら、Files APIで一度アップロードしてfile_idで参照できます。アップロードしたファイルはワークスペース全体から見える点に注意が要ります。社外秘のデータシートは、キーの権限が分かれたワークスペースに置きます。
claude.aiで使う場合は、プロジェクトのナレッジにデータシートを置くと、同じ部品について何度も質問できます。有料プランではナレッジが大きくなるとRAGモードで容量が広がります。
PDFから起こすのは構造体ではなく仕様データ
構造体を直接書かせると、ビット位置の誤りとリセット値の誤りが、Cのコードに溶けて見えなくなります。仕様データにしておけば、同じデータを検査にも生成にも使えます。
PDFから照合済みの構造体まで
- 1
仕様データに起こす
レジスタごとのオフセット、フィールドのビット範囲、フィールド別のリセット値、一覧表のリセット値、載っているページをJSONにします。
- 2
仕様データを検査する
ビットの重なり、リセット値の合成、アドレスの足し算をスクリプトで確かめます。
- 3
構造体を生成する
検査を通ったデータだけから、ヘッダーを機械的に生成します。
- 4
Cのテストで照合する
生成した構造体にリセット値を詰め、一覧表の値と一致するかを実行して確かめます。
Claudeへの指示は、次のように「何を、どの形で、書いていないときはどうするか」まで決めておきます。
添付PDFの p.40-44 から TMR0 のレジスタを JSON に起こしてください。
- レジスタごとに name / offset / page / summary_reset(一覧表のリセット値)
- フィールドごとに name / msb / lsb / reset(詳細表のリセット値)
- フィールドごとに access(RO / RW / W1C など、PDFの属性欄の表記どおり)
- Reserved の行は fields に入れない
- 読み取れない値、表をまたいで途切れた行は、推測せず "unknown" と書く
- アドレスマップの表は address_map に別で写す「推測せずunknown」の一文が要です。空欄を埋めさせると、検査で拾えない形で数字が混ざります。
仕様データの例(架空のタイマー)
以下は説明用に作った架空のタイマーTMR0です。実在の部品の値ではありません。ベースアドレスは0x40001000で、CNTの前に0x0Cの空きがあります。アクセス属性は、後述のとおり機械では照合できないので、各フィールドにaccessを持たせて人が見る列にします(検査と生成のスクリプトはaccessを読みません。例ではCTRL.ENがRW、STAT.OVFがW1C、CNT.VALUEがRO)。
{
"peripheral": "TMR0",
"base": "0x40001000",
"registers": [
{"name": "CTRL", "offset": "0x00", "page": 41, "summary_reset": "0x00000040",
"fields": [
{"name": "EN", "msb": 0, "lsb": 0, "reset": "0x0", "access": "RW"},
{"name": "MODE", "msb": 2, "lsb": 1, "reset": "0x0", "access": "RW"},
{"name": "IRQEN", "msb": 3, "lsb": 3, "reset": "0x0", "access": "RW"},
{"name": "PRESC", "msb": 11, "lsb": 4, "reset": "0x04", "access": "RW"}]},
{"name": "STAT", "offset": "0x04", "page": 42, "summary_reset": "0x00000000",
"fields": [
{"name": "BUSY", "msb": 0, "lsb": 0, "reset": "0x0", "access": "RO"},
{"name": "OVF", "msb": 1, "lsb": 1, "reset": "0x0", "access": "W1C"}]},
{"name": "LOAD", "offset": "0x08", "page": 42, "summary_reset": "0x0000FFFF",
"fields": [
{"name": "VALUE", "msb": 15, "lsb": 0, "reset": "0xFFFF", "access": "RW"}]},
{"name": "CNT", "offset": "0x10", "page": 43, "summary_reset": "0x00000000",
"fields": [
{"name": "VALUE", "msb": 15, "lsb": 0, "reset": "0x0", "access": "RO"}]}
],
"address_map": {"CTRL": "0x40001000", "STAT": "0x40001004",
"LOAD": "0x40001008", "CNT": "0x40001010"}
}リセット値は2か所に登場します。一覧表のsummary_resetと、詳細表のフィールド別resetです。データシートでは別々の表に載っているので、この2つは独立した読み取り結果になります。食い違いは、どちらかの読み違いのしるしです。
機械照合1: 仕様データの検査スクリプト
次のスクリプトは、構造体を作る前に3つを確かめます。
- フィールドのビットが重ならず、リセット値がビット幅に収まる
- フィールド別のリセット値を合成した値が、一覧表の値と等しい
- ベースアドレス+オフセットが、アドレスマップの表の値と等しい
import json, sys
spec = json.load(open(sys.argv[1]))
base = int(spec["base"], 16)
errors = []
prev_offset = -4
for reg in spec["registers"]:
name, off = reg["name"], int(reg["offset"], 16)
if off <= prev_offset or off % 4:
errors.append(f"{name}: オフセット {off:#x} が昇順でない/4の倍数でない")
prev_offset = off
used, composed = 0, 0
for f in reg["fields"]:
width = f["msb"] - f["lsb"] + 1
if width < 1:
errors.append(f"{name}.{f['name']}: msb<lsb ({f['msb']}:{f['lsb']})")
continue
mask = ((1 << width) - 1) << f["lsb"]
if used & mask:
errors.append(f"{name}.{f['name']}: 他フィールドとビットが重なる")
used |= mask
value = int(f["reset"], 16)
if value >> width:
errors.append(f"{name}.{f['name']}: リセット値 {f['reset']} が{width}ビットに入らない")
composed |= (value << f["lsb"]) & mask
summary = int(reg["summary_reset"], 16)
if composed != summary:
errors.append(f"{name}: フィールド合成 {composed:#010x} != 一覧表 {summary:#010x} (p.{reg['page']})")
addr = int(spec["address_map"][name], 16)
if addr != base + off:
errors.append(f"{name}: base+offset {base + off:#x} != アドレスマップ {addr:#x}")
print("\n".join(errors) or "OK")
sys.exit(1 if errors else 0)手元でpython3 -I check_spec.py tmr0.jsonを実行するとOKが出ます。エラーにページ番号を含めているのは、直す箇所のPDFをすぐ開くためです。
わざと読み違いを混ぜると、何が出るかが分かります。PRESCのビット範囲を[11:4]から[12:5]にずらした場合の出力です。
CTRL: フィールド合成 0x00000080 != 一覧表 0x00000040 (p.41)詳細表のビット位置を1つ読み違えても、一覧表のリセット値は別の場所にあるので、合成した値が食い違って落ちます。CNTのアドレスマップを0x4000100Cに変えた場合は、次の1行です。
CNT: base+offset 0x40001010 != アドレスマップ 0x4000100cブロック全体のサイズ(最後のレジスタのオフセット+4)も、データシートに記載があれば同じ要領で足せます。
構造体はスクリプトで生成する
検査を通った仕様データから、ヘッダーを機械的に作ります。Claudeに構造体を手書きさせない理由は、生成ならビット幅の計算とReservedの埋めが毎回同じになり、差分レビューが仕様データだけで済むからです。
import json, sys
spec = json.load(open(sys.argv[1]))
p = spec["peripheral"]
out = ["#include <stdint.h>", "#include <stddef.h>", ""]
for reg in spec["registers"]:
pos, lines = 0, []
for f in sorted(reg["fields"], key=lambda f: f["lsb"]):
if f["lsb"] > pos:
lines.append(f" uint32_t _rsvd{pos} : {f['lsb'] - pos};")
lines.append(f" uint32_t {f['name']} : {f['msb'] - f['lsb'] + 1};")
pos = f["msb"] + 1
if pos < 32:
lines.append(f" uint32_t _rsvd{pos} : {32 - pos};")
out += ["typedef union {", " uint32_t raw;", " struct {",
*[" " + l for l in lines], " } f;",
f"}} {p}_{reg['name']}_t;", ""]
out.append("typedef struct {")
cursor = 0
for reg in spec["registers"]:
off = int(reg["offset"], 16)
if off > cursor:
out.append(f" uint32_t _gap{cursor:02X}[{(off - cursor) // 4}];")
out.append(f" volatile {p}_{reg['name']}_t {reg['name']};")
cursor = off + 4
out += [f"}} {p}_regs_t;", ""]
for reg in spec["registers"]:
out.append(f'_Static_assert(offsetof({p}_regs_t, {reg["name"]}) == {reg["offset"]}, '
f'"{p}.{reg["name"]} offset");')
print("\n".join(out))生成されるヘッダーの先頭は次の形です。各レジスタがrawとfの共用体になり、末尾の_Static_assertで、構造体のオフセットが仕様データのオフセットと一致しない場合にコンパイルが止まります。
typedef union {
uint32_t raw;
struct {
uint32_t EN : 1;
uint32_t MODE : 2;
uint32_t IRQEN : 1;
uint32_t PRESC : 8;
uint32_t _rsvd12 : 20;
} f;
} TMR0_CTRL_t;CNTの手前の0x0Cには_gap0Cというパディングが入ります。アドレスの空きを構造体に写し忘れると、CNTが0x0Cにずれる典型的な事故になります。静的アサートはこれをコンパイル時に止めます。
落とし穴
ビットフィールドの並び順はコンパイラー依存
Cの規格上、ビットフィールドの割り当て順は実装定義です。上のヘッダーは、下位ビットから割り当てるコンパイラー(GCCなど)を前提にしています。別のコンパイラーやビッグエンディアンの環境へ移すときは、次のテストを必ずその環境で実行します。
機械照合2: リセット値をCのテストで照合する
最後に、生成した構造体が一覧表のリセット値を再現できるかを実行して確かめます。フィールドごとのリセット値を詰めた共用体のrawが、一覧表の値と一致すれば、構造体のビット配置と表が合っています。
#include <stdio.h>
#include "tmr0_regs.h"
int main(void) {
int bad = 0;
{ TMR0_CTRL_t r = { .f = { .EN = 0x0, .MODE = 0x0, .IRQEN = 0x0, .PRESC = 0x04 } };
if (r.raw != 0x00000040u) { printf("NG CTRL: %08x\n", (unsigned)r.raw); bad++; } }
{ TMR0_LOAD_t r = { .f = { .VALUE = 0xFFFF } };
if (r.raw != 0x0000FFFFu) { printf("NG LOAD: %08x\n", (unsigned)r.raw); bad++; } }
/* STAT・CNT も同じ形。仕様データから生成する */
puts(bad ? "FAIL" : "PASS");
return bad != 0;
}テストも、仕様データから生成します。生成スクリプトgen_test.pyは、各レジスタについて「全フィールドのリセット値を代入した共用体を作り、rawをsummary_resetと比べる」ブロックを出力するだけの短いものです。
import json, sys
spec = json.load(open(sys.argv[1]))
p = spec["peripheral"]
print(f'#include <stdio.h>\n#include "tmr0_regs.h"\nint main(void) {{ int bad = 0;')
for reg in spec["registers"]:
init = ", ".join(f'.{f["name"]} = {f["reset"]}' for f in reg["fields"])
t = f'{p}_{reg["name"]}_t'
print(f' {{ {t} r = {{ .f = {{ {init} }} }};')
print(f' if (r.raw != {reg["summary_reset"]}u) {{ printf("NG {reg["name"]}: %08x\\n", (unsigned)r.raw); bad++; }} }}')
print(' puts(bad ? "FAIL" : "PASS"); return bad != 0; }')期待値に使うのは一覧表側のsummary_resetで、代入するのは詳細表側の値です。生成側と検査側が同じ列を見ると、読み違いも同じ形で通ってしまうので、あえて別の列を突き合わせます。手元ではGCCでコンパイルしてPASSを確認しました。PRESCを[12:5]に読み違えた仕様データから生成し直すと、同じテストが次のように落ちます。
NG CTRL: 00000080
FAILスクリプトによる検査とCのテストは、重なる部分があります。それでも両方を置くのは、前者が仕様データの内部矛盾を、後者が生成した構造体の実際の並びを見るからです。生成スクリプトに不具合があれば、前者を通ってもCのテストで落ちます。
検査が拾える読み違い、拾えないもの
表の構造から考えられる読み違いの型を、どの検査が捕まえるかで並べます。
| 読み違えの型 | 拾う検査 |
|---|---|
ビット範囲の1つのずれ([11:4]を[12:5]) | 拾う検査リセット値の合成が一覧表と食い違う |
| 同じビットを2つのフィールドに割り当てる | 拾う検査ビットの重なり検査 |
| 空きアドレスの見落とし | 拾う検査アドレスマップとの足し算、_Static_assert |
| 16進と2進、桁数の取り違え | 拾う検査ビット幅に収まるかの検査 |
| 表が複数ページにまたがり、行が欠ける | 拾う検査合成値の食い違い(欠けた行のリセット値が非ゼロのとき) |
| リセット値がゼロのフィールドの欠落 | 拾う検査拾えない |
| アクセス属性(RO・W1Cなど)の読み違い | 拾う検査拾えない |
| データシート自体の誤記 | 拾う検査一覧表と詳細表が同じ誤りを持てば拾えない |
下の3行は機械で守れない範囲です。リセット値がゼロのフィールドが丸ごと欠けても、合成値は変わりません。アクセス属性は、リセット値にもアドレスにも現れません。
アクセス属性は、読み違えても検査もテストも通ってしまいます。たとえばSTAT.OVFをW1C(1を書くとクリアされる)でなくRWと起こすと、後段のドライバーが0を書いてフラグを消そうとする実装になります。仕様データにaccessの列を持たせておけば、PDFの属性欄とJSONの列を並べて見比べるだけで済みます。
この3行は、仕様データを人が読む工程に残します。レビューの観点は、フィールド数を詳細表の行数と数え合わせること、RO・W1Cなどの属性をPDFの該当ページで見ること、の2点です。pageを仕様データに持たせてあるので、見る箇所はすぐ開けます。
Claude Codeに任せる運用
仕様データの検査と生成を、コマンド1つにまとめておくと、Claudeが修正のたびに自分で回せます。
python3 -I check_spec.py tmr0.json \
&& python3 -I gen_header.py tmr0.json > tmr0_regs.h \
&& python3 -I gen_test.py tmr0.json > test.c \
&& gcc -Wall -o t test.c && ./tCLAUDE.mdには、この流れを守る規約を書いておきます。
# レジスタ定義のルール
- レジスタ表は tmr0.json に起こし、構造体を手で書かない
- 読み取れない値は "unknown" にし、推測で埋めない
- 変更後は check_spec.py から ./t までを実行し、PASS を確認してから報告する
- 検査が落ちたら、仕様データの該当ページ(page)を読み直して原因を直す検査が落ちたときの直し方が肝です。Claudeに「テストが通るようにリセット値を直して」と頼むと、合成値を合わせるために仕様データのほうを都合よく書き換える余地が生まれます。直す前にpageのPDFページを読み直させ、表の値と照らして根拠を示させます。
同じ「原本の表を読み、別の場所の値で検算する」型は、決算書PDFから勘定科目を抽出する手順の貸借の検算や、データ移行の項目マッピングの件数照合にも通じます。出力を先に固定してから変えていく進め方は、特性テストの考え方と重なります。2つの部品の定格を比べたいときは、データシートの比較と代替品の同等性判定の手順が使えます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。