AI Agents in Embedded: Context Engineering
AI agents write good Yocto recipes but silently corrupt sensor data and misconfigure SPI registers. The fix is context engineering.
On this page
I asked Claude Code for a Yocto recipe and it produced one that ran on the first try. Correct inherit, proper SRC_URI, the systemd service file installed to the right path. Two weeks later, same tool, same kind of session, I asked it to write a DMA callback for an IMU sensor driver on i.MX8M Plus. The code compiled with no errors. But under load the sensor data started corrupting intermittently, and within 30 seconds the whole kernel locked up.
Same model, same engineer. What changed was the context. For the Yocto recipe, my CLAUDE.md spelled out the build system (meson, not cmake), the layer structure, the standard recipe patterns. For the DMA handler? “Write a DMA callback for the sensor data path” was the whole brief. It had no way to know this platform’s cache synchronization requirements or the locking discipline for shared buffers.
The gap between the tasks agents handle cleanly and the tasks where they emit dangerous code is what this post is about. The first post fixed the feedback loop with spec-driven TDD. This one fixes the agent itself. It comes down to one thing. the model is not the bottleneck. Context is.
This post continues to use Claude Code. The CLAUDE.md, scoped rules, and subagent reviewers are Claude Code features, but the principle of layered context carries over to any agentic coding tool.
What Agents Get Right
AI agents are genuinely productive when the right answer is set by software conventions and doesn’t hinge on hardware behavior. Yocto recipes, CMakeLists, systemd unit files, pytest scaffolds, CI/CD pipelines. The agent has seen thousands of .bb recipes in training data, and the differences between them are text you can spell out in a project file. Jacob Beningo reports saving 4 to 20 hours on sensor driver work with Claude Code, with the caveat that a clean hardware abstraction layer is a prerequisite. For build system work and test scaffolding, solo development starts to feel like a small team.
What They Get Wrong
The failures show up at every level. Start at the register level. The agent used 0x11 for CTRL_REG1 on our ICM-42688-P IMU. The correct address is 0x4E. SPI doesn’t throw an error on a wrong register address. It just reads back whatever sits at that location, which happened to be a reserved register returning zero. I spent 40 minutes with a logic analyzer before dumping the register map and spotting the wrong address. One line in CLAUDE.md would have prevented it.
At the kernel level the gap is far more dangerous. A DMA completion callback for a sensor driver on i.MX8M Plus is a textbook case. The agent produces code like this:
/* Agent output: DMA completion callback */
static void sensor_dma_complete(void *arg)
{
struct sensor_dev *sdev = arg;
memcpy(sdev->latest, sdev->dma_buf, SAMPLE_SIZE);
complete(&sdev->data_ready);
}
It compiles. On light traffic it runs fine on the bench. But under sustained load it corrupts data intermittently. Without context, there are two problems the agent has no way to catch:
/* What the hardware actually needs */
static void sensor_dma_complete(void *arg)
{
struct sensor_dev *sdev = arg;
/* SDMA writes to RAM but CPU L1/L2 still holds stale data.
Without this call, memcpy reads garbage when cache lines
haven't been evicted yet. */
dma_sync_single_for_cpu(sdev->dev, sdev->dma_addr,
SAMPLE_SIZE, DMA_FROM_DEVICE);
spin_lock(&sdev->lock); /* pair with spin_lock_bh() in process-context readers */
memcpy(sdev->latest, sdev->dma_buf, SAMPLE_SIZE);
spin_unlock(&sdev->lock);
complete(&sdev->data_ready);
}
The missing dma_sync_single_for_cpu means the CPU reads stale cache lines after the DMA engine writes straight to RAM. On i.MX8M Plus with Cortex-A53, cache eviction is non-deterministic under load, so the corruption comes and goes with memory pressure. The missing spin_lock opens a race between the callback (tasklet/softirq context) and any process thread reading sdev->latest. And the process-context side needs spin_lock_bh, not plain spin_lock, or you deadlock when a softirq fires while the lock is held. That deadlock is exactly what locked up the kernel in the opening example.
The models have seen the Linux DMA API in training data. But they haven’t seen your platform’s cache topology, your driver’s locking discipline, or the fact that this callback runs in softirq context where a sleeping lock is fatal. A Chalmers/University of Gothenburg study (January 2026) surveyed 10 senior software engineers across four companies. They ranked non-determinism and certification gaps as the top barriers to adopting AI agents in embedded. One engineer put it well: “We can check in the compiler from ten years ago, but not the LLM.”
The gap shows up under load, at scale, or when hardware behavior diverges from what the training data implied. Closing it doesn’t take a smarter model. It takes context.
Context Engineering: Three Layers That Work
I converged on three layers of context that catch progressively subtler failures.
Layer 1: CLAUDE.md as Hardware Specification
The single highest-impact change. One markdown file in your project root that survives across every Claude Code session. Thirty minutes of writing pays off on every interaction after that.
An ETH Zurich study (February 2026) tested 138 real-world tasks. LLM-generated context files actually dropped success rates by 3% and raised costs 20%. Human-written context files raised success by 4%, but costs went up 19% too, because agents follow instructions too literally. The takeaway: only include what the agent can’t infer from the repository itself. Obvious project-structure descriptions waste tokens. Hardware constraints the agent can’t discover from code are gold.
My CLAUDE.md focuses on the non-inferable constraints:
- Hardware platform: SoC model (i.MX8M Plus), target boards (RPi CM4), key peripherals and their register quirks
- Register map: critical SPI/I2C register addresses the agent has gotten wrong before
- DMA rules: cache line alignment (64 bytes on Cortex-A53), mandatory sync calls, buffer allocation constraints
- Build system: meson not cmake, which meta-layer owns which recipes, Docker build patterns
Every entry is there because the agent made a specific mistake without it. The pattern that actually works is a “Learned Corrections” section, where each line traces back to a concrete failure:
## Learned Corrections
- 2026-01-15: IMU CTRL_REG1 is 0x4E on ICM-42688-P, not 0x11
- 2026-01-18: DMA buffers must be 64-byte aligned on Cortex-A53
- 2026-02-03: SPI mode 3 (CPOL=1, CPHA=1) for ICM-42688-P
Each line is a bug the agent introduced, diagnosed, and won’t repeat. The file grows organically. In my experience, toolchain and workflow corrections outnumber hardware corrections by about 3 to 1. The most frequent failures are about build environments and deployment paths that the training data doesn’t cover.
For larger projects, Claude Code supports path-based rule files (.claude/rules/*.md) that load only when you touch matching files. Kernel constraints load when the agent edits driver code. Yocto conventions load for .bb files. Each rule file is small and focused. The agent gets exactly the context the current task needs.
Layer 2: Specialized Subagents for Review
A single general-purpose agent reviewing embedded code tries to hold too much at once: code style, concurrency safety, register correctness, blast-radius analysis, coding standards. The context window fills up and the agent starts missing things.
The fix is to split review into specialized subagents, each with a focused system prompt and access only to the tools it needs. My BSP workflow runs five specialized reviewers plus a supervisor that aggregates the findings. Two design decisions matter here:
Conditional invocation. Not every reviewer runs on every change. The register contract reviewer only fires when SPI/I2C code or device tree files change. The concurrency reviewer only fires when kernel driver code changes. Test-only changes skip both and trigger only the test coverage reviewer. A simple file-glob check at the start of each agent decides whether it runs or bails. This halves review time on most commits and avoids the false positives you get from reviewers analyzing code they have no business touching.
Redundant instances for critical reviewers. The concurrency and register contract reviewers each run 2 instances in parallel with the same prompt but independent context. If both instances flag the same issue, confidence is high. If only one flags it, it goes to human review. This handles the probabilistic blind spot where a single LLM pass misses a bug the same model would catch on a second run. The cost is modest (two parallel API calls instead of one), and the reliability gain is big for the two categories that produce the most dangerous bugs. The supervisor deduplicates every finding and ranks it by severity. 2+ flags on the same issue means high confidence (auto-fixable); 1 flag goes to human review. No re-run rounds. The 2-instance design already captures the variation, and extra rounds just add latency.
A Google/MIT scaling study (December 2025, 180 configurations) found a hard ceiling at 3 to 4 agents under fixed token budgets. Independent agents with no coordination amplify errors 17.2x. A centralized supervisor architecture holds errors to 4.4x, the best pattern tested. Scaling agent count beats scaling discussion rounds, and independent analysis followed by centralized aggregation beats inter-agent debate.
The two reviewers that catch the most bugs:
Concurrency reviewer. Its prompt opens with: “You do NOT review code quality, style, or performance. You ONLY answer: Can this code break under concurrent execution?” It checks spinlock context violations, sleeping in IRQ/softirq, lock ordering, memory barriers, and DMA sync discipline. That DMA callback bug from earlier? This reviewer catches it on the first pass. It flags the missing cache sync and the unprotected memcpy with a step-by-step interleaving scenario. A general-purpose reviewer misses both.
Register contract reviewer. It checks that SPI/I2C register addresses, bit field masks, and initialization sequences match the datasheet. The reviewer greps every register access macro, extracts the addresses, and compares them against the register map in docs/hardware/.
The other three reviewers (side-effect/blast-radius, coding standards, test coverage) run as single instances alongside these two. They cover lower-risk concerns where a single pass is enough.
Feeding static analysis output into each reviewer’s prompt amplifies the effect. I run cppcheck and clang-tidy before the review pass, convert their output to JSON, and drop it into each reviewer’s prompt. Static analysis findings combined with LLM review catch more than either one alone.
Layer 3: Compiler-in-the-Loop Feedback
The last layer closes the loop between agent output and real toolchain results. Cross-compile the agent’s output for the target, run static analysis, check flash and RAM usage, and feed the errors back as structured data. Then the agent iterates against a real compiler and real static analysis output instead of guessing.
The first post covered the whole spec-driven testing pipeline: acceptance criteria in spec.md, pytest + labgrid wrapped in a single command, structured JSON back. The build feedback loop applies the same principle earlier, before code reaches the target. Instead of scrolling through compiler warnings, the agent reads {"check": "cppcheck", "severity": "error", "message": "Buffer access out of bounds"}, and instead of scrolling through kernel logs it reads {"test": "dma_cache_sync", "outcome": "failed", "reason": "data mismatch after 10000 transfers"}.
The three layers compound. CLAUDE.md keeps the agent from generating code with known-bad patterns. Subagents catch the domain-specific issues the main agent would miss. The build feedback loop tests against reality and catches the rest.
What This Pipeline Produces
Before context engineering, the agent’s code compiled and then died at runtime. Silent failures like wrong register addresses, missing cache syncs, unprotected shared state each ate 30+ minutes to diagnose, because nothing in the logs pointed at the root cause.
With the three layers, most modules pass acceptance tests on the first or second iteration. The rework that’s left is about incomplete specs, not broken code.
Where Context Engineering Stops
This approach covers roughly half of what matters for production embedded drivers. The rest stays human.
Hardware debugging. When the SPI bus is stuck because the chip select polarity is inverted, no context file helps. Oscilloscopes, logic analyzers, and 20 minutes of staring at a probe before it clicks that the CPOL bit is flipped. The agent will write software fault injection scripts (stress-ng, forced module unload/reload), but physical faults need physical instruments.
Context degradation. Over long sessions (2+ hours), the agent loses track of earlier constraints even with CLAUDE.md loaded. The fix is aggressive use of /clear (Claude Code’s context reset command) to start fresh.
Spec quality. The pipeline is only as good as the spec. Covered in the first post.
Start Here
Write a 20-line CLAUDE.md: target hardware, build system, and three “never do this” rules. Add entries as the agent makes mistakes. Then bolt on one concurrency reviewer subagent. Then close one feedback loop with a bash script that cross-compiles and returns errors as JSON. The TDD post has the full spec-driven testing setup.
Better models will raise the ceiling on what good context makes possible. They won’t remove the need for it. The model will keep getting smarter. Your CLAUDE.md is what tells it where the cliffs are.
The final post in this series puts the complete project anatomy in one place: folder structure, specs, four test layers, review agents, and the end-to-end feedback loop.
Comments
Loading comments...