🦾 SpecRAG: How We Taught an AI to Read a Datasheet
"Hardware is hard, but it shouldn't be silent."
The Inspiration: The Night a PCB Burned
Every embedded systems developer has a war story. Mine involves a 3:00 AM oscilloscope session, a board that wouldn't boot, and the sinking realization that three weeks of work had just turned into a silent brick. The culprit? A single line of C++:
Wire.write(0x75); // Writing to WHO_AM_I — a read-only register
Wire.write(0x00);
The compiler didn't flinch. The linter didn't flinch. The code was syntactically perfect — and electrically catastrophic. The WHO_AM_I register on the MPU6050 IMU is read-only by hardware definition, documented clearly on page 46 of the datasheet. But no tool in the developer's toolchain speaks "datasheet."
That night became the seed for SpecRAG.
🔥 The Developer Pain Point: The Semantic Gap
Embedded C/C++ compilers are extraordinarily good at catching one class of errors: syntactic violations of the language specification. They are completely blind to semantic violations of the hardware specification.
A developer can write clean, well-typed, lint-passing C++ that:
- Writes to Read-Only registers — silently failing at runtime, causing corrupted sensor state or device lock-up.
- Uses the wrong I2C slave address —
0x68vs0x69for the MPU6050 depends on the logic state of theAD0pin, a hardware fact, not a software fact. - Violates timing constraints — reading a temperature sensor before its 100ms startup delay expires produces garbage data, not an exception.
- Drives 5V logic into a 3.3V pin — potentially destroying silicon, permanently.
The root cause is a semantic gap: the compiler knows C++, but it has no model of the physical world the code is meant to control. The hardware specification lives in a 1,000-page PDF, and no one has ever built a bridge between that PDF and the IDE.
SpecRAG is that bridge.
🏗️ How We Built It: The Four-Node Architecture
We structured SpecRAG as a four-node pipeline, where each node has a single, clean responsibility.
graph TD
User[Developer writes .cpp / .ino] -->|File Save| Node4[Node 4: VS Code Extension]
Node4 -->|JSON-RPC over stdio| Node2[Node 2: MCP Server + AST Parser]
Node2 -->|Semantic Query| Node1[Node 1: RAG Engine — ChromaDB]
Node1 -->|Datasheet Chunks| Node2
Node2 -->|Ground Truth + Filtered Code| Node3[Node 3: LLM Auditor — Gemini 2.5 Flash]
Node3 -->|HardwareValidationResult JSON| Node2
Node2 -->|Diagnostic Payload| Node4
Node4 -->|Red Squiggly + Citation| User
Node 1 — The Ground Truth Engine (RAG Data Pipeline)
The first breakthrough was deciding that the LLM should never rely on its own knowledge of hardware. Pre-trained LLMs carry outdated, incomplete, and occasionally hallucinated register maps. Instead, we built a full RAG pipeline:
- PDF Ingestion (
ingest_pdf.py,ingest.py): Raw datasheets are converted from PDF → Markdown, then sanitized byclean_markdown.pyto strip HTML artifacts, block diagram text, and revision history noise that would pollute the context window. - Semantic Chunking: LangChain's
MarkdownHeaderTextSplitterdivides the cleaned Markdown intelligently, keeping register tables firmly attached to their section headers. A table without its## Register Map > PWR_MGMT_1header is meaningless to the retriever. - Vector Indexing: Chunks are embedded and stored in ChromaDB (
vector_store/). The collection is namedhardware_datasheetsand persists across server restarts. - Retrieval (
rag.py): A query combining the component name and the code snippet retrieves the top 3 most semantically relevant datasheet chunks, formatted with their full header hierarchy.
def retrieve_context(component_name: str, code_snippet: str = "") -> str:
query_string = f"{component_name} {code_snippet.strip()}"
results = collection.query(query_texts=[query_string], n_results=3)
# ... returns formatted chunks with section headers
The mathematical insight here: if the context contains the exact text "Register 0x75 (WHO_AM_I) is read-only", the LLM cannot claim ignorance. The ground truth is in the prompt.
Node 2 — The Smart Router (AST Parsing + MCP Server)
Raw firmware files can be thousands of lines long. Feeding all of it to the LLM wastes tokens and dilutes the relevant signal. Node 2's job is to be a precision filter.
AST Parsing (ast_parser.py): Using Tree-sitter, we parse the incoming C/C++ code into an Abstract Syntax Tree and extract only hardware-relevant operations:
#definepreprocessor directives (register addresses, bitmasks)pinMode(),digitalWrite(),analogRead()callsWire.beginTransmission(),Wire.write(),Wire.requestFrom()I2C callsSPI.transfer()operations
This reduces a 500-line sketch to the 30 lines that actually matter for hardware validation — a 16x token efficiency gain.
MCP Server (MCP_Server.py): Rather than a custom REST API, we chose the Model Context Protocol (MCP) as the communication layer. This decision was deliberate:
- Decoupling: The AI "Brain" is completely separated from the IDE "Body." The server can run on a powerful lab machine; the developer codes on a lightweight laptop.
- Standardization: MCP is an open standard. Any MCP-compatible client (Cursor, Claude Desktop, custom VS Code extension) can consume our tools without modification.
- Dual Tool Exposure: We expose two tools —
validate_hardware_code_mockfor deterministic dry-run testing (zero API cost), andvalidate_hardware_codefor the full live pipeline.
Node 3 — The Zero-Hallucination Auditor (AI Engineering)
This is where the project's core thesis is proven. The challenge: how do you force a generative LLM — which by nature produces probabilistic, open-ended text — to behave like a deterministic hardware auditor?
Answer: Mathematical constraint via Structured Output + Pydantic.
class HardwareValidationResult(BaseModel):
status: str = Field(description="'error' or 'success'")
severity: Optional[str] = Field(None, description="'high', 'medium', 'low', or null")
line: Optional[int] = Field(None, description="Exact line number of bug")
message: str = Field(description="One-sentence explanation of the violation")
citation: str = Field(description="Datasheet section, page, or register name")
Using Gemini's response_mime_type="application/json" with a response_schema, and OpenAI's client.beta.chat.completions.parse() with the Pydantic model as response_format, the LLM is mathematically incapable of returning conversational text. It can only return a valid JSON object matching this schema.
The system prompt (Archives/AGENT.md) defines the auditor persona with surgical precision:
- Conservative Evaluation Rule: If the RAG context is insufficient to prove a violation, default to
success. The LLM must never invent hardware constraints. - Citation Mandate: Every identified bug requires a citation — a specific section name, table name, page number, or register name from the retrieved context. This makes every diagnosis traceable and auditable.
- Few-Shot Examples: Three carefully crafted examples calibrate the model's reasoning for the exact violation types we care about: read-only register writes, logic-level mismatches, and malformed I2C transactions.
The LLM provider is configurable — Azure OpenAI, OpenAI GPT-4o, or Gemini 2.5 Flash — resolved at runtime from environment variables, with Gemini as the fallback.
Node 4 — The Developer Interface (VS Code Extension)
The TypeScript VS Code extension (extension/) closes the loop. It:
- Debounces on text change and file save events (prevents API spam while typing)
- Sends the active file content and detected component name to the MCP server via stdio JSON-RPC
- Parses the returned
HardwareValidationResult - Draws red squiggly diagnostics at the exact
lineof the violation - Renders Markdown hover cards showing the
messageand thecitationfrom the datasheet - Offers Quick Fix code actions — "Comment out line" and "Insert FIXME marker" — directly from the lightbulb
The extension is packaged as a .vsix and installable in any VS Code-compatible editor.
🧠 What We Learned
1. Prompt Engineering as a Determinism Problem
The most counterintuitive lesson: the goal of prompt engineering for a hardware auditor is not to make the model smarter. It's to make the model less creative. The temperature is set to 0.1. The schema is rigid. The persona is "Strict Senior Embedded Auditor," not "helpful assistant." Removing the LLM's freedom is the feature.
2. Context Quality Is the Whole Game
"If your context misses the I2C registers, the AI will miss the I2C bug."
We discovered this the hard way. Early versions of the ingestion pipeline were keeping HTML table artifacts, which the vector embedding model treated as noise. A single cleaning step in clean_markdown.py — stripping those artifacts — increased retrieval precision dramatically. Garbage in, garbage out applies with brutal exactness to RAG systems.
3. MCP Over REST Was the Right Call
Building the server as an MCP tool rather than a FastAPI REST endpoint was a non-obvious choice that paid off. The stdio transport means zero network configuration — no ports to open, no CORS to debug, no firewall rules. The VS Code extension just spawns the server process and pipes stdin/stdout. It "just works" in any environment, including corporate developer machines behind restrictive proxies.
4. Structured Output Has Edge Cases Across Providers
Gemini's response_schema rejects Pydantic default values as an "unknown field," so we had to rewrite the schema as a raw dictionary for Gemini. OpenAI's beta.chat.completions.parse() handles Pydantic natively. Azure OpenAI and OpenAI share the same code path. Abstracting these differences behind a single get_llm_client() factory function was essential for maintainability.
⚡ What Changes for the Developer Who Uses SpecRAG
Before SpecRAG, the firmware development loop looks like this:
$$ \text{Write Code} \rightarrow \text{Compile (syntax only)} \rightarrow \text{Flash to Board} \rightarrow \text{Debug Hardware} \rightarrow \text{Repeat for days} $$
The hardware debug phase — oscilloscopes, logic analyzers, multimeters, forum threads — can consume days to weeks for a single subtle register or addressing bug.
After SpecRAG, the loop becomes:
$$ \text{Write Code} \rightarrow \underbrace{\text{SpecRAG Audit (datasheet-grounded)}}_{\text{< 2 seconds, in IDE}} \rightarrow \text{Fix Squiggly} \rightarrow \text{Flash to Board} $$
The hardware debug phase for the class of bugs SpecRAG catches — register violations, address mismatches, pin mode mismatches, I2C protocol violations — collapses to zero. The developer sees the red squiggly before they ever touch physical hardware. The hover tooltip shows the exact datasheet section that proves why it's wrong.
More importantly, it changes the confidence of the developer. Flashing firmware to a prototype PCB becomes a lower-stakes act when you know your code has been audited against the actual component specifications.
🏆 The Agent Published to the AI Catalog
The SpecRAG Hardware Auditor agent is the core of this system — the Archives/AGENT.md persona combined with the MCP server tooling. It is a stateless, context-driven, structured-output agent that:
- Accepts filtered C++ code and retrieved datasheet context as input
- Returns a guaranteed-schema JSON diagnostic in every case
- Cites its sources for every finding (zero hallucination by design)
- Gracefully degrades to
successwhen the context is insufficient (conservative by mandate)
This agent is published to the AI Catalog as "SpecRAG: Zero-Hallucination Firmware Auditor" and can be connected to any MCP-compatible client with a component datasheet loaded into ChromaDB.
🔮 What's Next
- Broader Component Support: Ingesting datasheets for STM32, ESP32 peripherals, and popular sensors (BME280, BMP390, ADXL345).
- Wokwi Integration: Simulation-time validation before physical hardware ever enters the picture.
- CI/CD Gate: A GitHub Action that runs SpecRAG on every PR containing firmware changes, blocking merges with high-severity hardware violations.
- JTAG / GDB Plugin: Post-flash runtime analysis for violations that require execution context.
Built with ❤️ for the Hackathon. Every red squiggly is a PCB that didn't burn.
Log in or sign up for Devpost to join the conversation.