<?xml version="1.0" encoding="UTF-8"?>        <rss version="2.0"
             xmlns:atom="http://www.w3.org/2005/Atom"
             xmlns:dc="http://purl.org/dc/elements/1.1/"
             xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
             xmlns:admin="http://webns.net/mvcb/"
             xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
             xmlns:content="http://purl.org/rss/1.0/modules/content/">
        <channel>
            <title>
									EDGEMTech Forum - Recent Topics				            </title>
            <link>https://edgemtech.ch/community/</link>
            <description>EDGEMTech Discussion Board</description>
            <language>en-US</language>
            <lastBuildDate>Sat, 13 Jun 2026 06:43:56 +0000</lastBuildDate>
            <generator>wpForo</generator>
            <ttl>60</ttl>
							                    <item>
                        <title>EDGEM·AI: A Local, RAG‑Powered Assistant for Our Embedded Projects</title>
                        <link>https://edgemtech.ch/community/general-discussions/edgem%c2%b7ai-a-local-rag-powered-assistant-for-our-embedded-projects/</link>
                        <pubDate>Wed, 10 Jun 2026 15:05:32 +0000</pubDate>
                        <description><![CDATA[Over the last few months, we’ve been reshaping our workflows at EDGEMTech to put AI directly in the loop of our daily engineering work, especially around embedded systems and edge computing....]]></description>
                        <content:encoded><![CDATA[<p class="my-2 :mt-4 :block :pb-2 :hidden">Over the last few months, we’ve been reshaping our workflows at EDGEMTech to put AI directly in the loop of our daily engineering work, especially around embedded systems and edge computing.</p>
<p class="my-2 :mt-4 :block :pb-2 :hidden">Like many teams, we see clear gains in development speed and bug resolution, and it strengthens our<span> </span><strong>specification-driven</strong><span> </span>approach and software component integration. At the same time, our projects often involve industrial, medical, or otherwise critical environments, which come with strict constraints on confidentiality, digital sovereignty, and long‑term control over our infrastructure.</p>
<p class="my-2 :mt-4 :block :pb-2 :hidden">Cloud-first AI tooling is not acceptable for a large part of our work. This is why we started building our own AI stack, designed from day one to be<span> </span><strong>local, privacy-preserving, and specialized for embedded and edge software development</strong>.</p>
<h2>What we are building</h2>
<p class="my-2 :mt-4 :block :pb-2 :hidden">We’re developing an internal AI coding assistant, currently used on:</p>
<ul class="marker:text-quiet list-disc pl-8">
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">our edgem1/verdin build system including TorizonOS</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">The SO3/AVZ hypervisor</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">LVGL projects and other UI stacks</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">generic embedded and Linux-based repos</p>
</li>
</ul>
<p class="my-2 :mt-4 :block :pb-2 :hidden">The assistant runs<span> </span><strong>fully local</strong>: no source code or customer data ever leaves the machine. It is built around a modern open model (Qwen 3.6 35B MoE, quantized) serving as the core, with a harness that adds:</p>
<ul class="marker:text-quiet list-disc pl-8">
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden"><strong>RAG over code and docs</strong><span> </span>(per-project ChromaDB collections)</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden"><strong>corpus-aware context</strong><span> </span>(one “corpus” per project/repo)</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden"><strong>persistent memory and skills</strong>, allowing the assistant to learn from real usage across sessions</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">optional fine-tuning via lightweight adapters when it becomes useful again</p>
</li>
</ul>
<p class="my-2 :mt-4 :block :pb-2 :hidden">In other words, it is not “just a model on a GPU”, but an AI<span> </span><strong>tooling layer</strong><span> </span>tailored to our codebases and workflows.</p>
<h2>Architecture, in practice</h2>
<p class="my-2 :mt-4 :block :pb-2 :hidden">Our current deployment runs on a developer laptop (RTX 4060 8GB + 62GB RAM). This configuration is<span> </span><strong>deliberately constrained</strong>: it forces us to focus, to rationalize the scope, and to validate the architecture and tooling on realistic, everyday hardware.</p>
<ul class="marker:text-quiet list-disc pl-8">
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">Model server: Qwen3.6‑35B‑A3B in Q8_0 via llama.cpp, with hybrid offload</p>
<ul class="marker:text-quiet list-disc">
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">attention on GPU, experts in RAM</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">~7 tokens/s in typical usage, offline only</p>
</li>
</ul>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">Front-end: a CLI chat client aware of the current working directory</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">Tools exposed to the model:</p>
<ul class="marker:text-quiet list-disc">
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden"><code>bash</code>,<span> </span><code>edit_file</code>,<span> </span><code>append_file</code>,<span> </span><code>write_file</code><span> </span>for code edits</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden"><code>web_search</code><span> </span>for explicit internet queries when allowed</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden"><code>remember</code>,<span> </span><code>save_skill</code>,<span> </span><code>search_history</code><span> </span>for long-term memory and procedure learning</p>
</li>
</ul>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">Knowledge layers:</p>
<ul class="marker:text-quiet list-disc">
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">system prompts and project rules</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">RAG corpora per repo</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">persistent memories and skills</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">cross-session history search</p>
</li>
</ul>
</li>
</ul>
<p class="my-2 :mt-4 :block :pb-2 :hidden">Thanks to our collaboration with the <a href="https://heig-vd.ch/recherche/instituts/reds" target="_blank" rel="noopener">REDS (Reconfigurable Embedded Digital Institute)</a> from HEIG‑VD (University of Applied Sciences in Yverdon-les-Bains), we also have access to<span> </span><strong>high‑end GPU infrastructure</strong><span> </span>(for example RTX 6000‑class GPUs with 96 GB of VRAM). This will allow us to:</p>
<ul class="marker:text-quiet list-disc pl-8">
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">run heavier models or higher‑throughput instances when needed</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">perform faster and more extensive fine‑tuning runs</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">~48 tokens/s in typical usage, offline only</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">deploy a dedicated server‑grade instance of the assistant in the near future, while preserving the same architecture and local‑first design</p>
</li>
</ul>
<p class="my-2 :mt-4 :block :pb-2 :hidden">Everything is orchestrated so that the assistant can read, navigate, search, and edit code in a controlled way, with guardrails on file size, number of tool calls per turn, and explicit confirmation for any non‑read‑only actions.</p>
<h2>Corpora: how we structure knowledge</h2>
<p class="my-2 :mt-4 :block :pb-2 :hidden">Central to this approach is the notion of a<span> </span><strong>corpus</strong>: each project or workspace is its own corpus with:</p>
<ul class="marker:text-quiet list-disc pl-8">
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">its own RAG index</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">its own memories and skills</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">project-specific rules and conventions</p>
</li>
</ul>
<p class="my-2 :mt-4 :block :pb-2 :hidden">For example:</p>
<ul class="marker:text-quiet list-disc pl-8">
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden"><code>edgem1</code><span> </span>is a curated corpus built from our build system checkout (verdin, virt64, scripts, doc, etc.)</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">any other repo (LVGL, SO3, u-boot, …) becomes a<span> </span><strong>generic</strong><span> </span>corpus indexed with a standard directory walker</p>
</li>
</ul>
<p class="my-2 :mt-4 :block :pb-2 :hidden">The CLI automatically detects the active corpus from the current directory, or lets you pick one:</p>
<ul class="marker:text-quiet list-disc pl-8">
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden"><code>edgem-chat</code><span> </span>— open a chat bound to the current repo</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden"><code>edgem-chat --corpus lvgl</code><span> </span>— explicitly pick a known corpus</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden"><code>edgem-chat --here</code><span> </span>— treat the current directory as an ad-hoc corpus</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden"><code>edgem-reindex</code><span> </span>/<span> </span><code>edgem-index</code><span> </span>— (re)build the index for a given tree</p>
</li>
</ul>
<p class="my-2 :mt-4 :block :pb-2 :hidden">Large multi-component workspaces can be split into multiple corpora so that, for example, a huge upstream tree like u‑boot doesn’t dilute the relevance of RAG for a smaller in‑house component.</p>
<h2>Four learning loops, different time scales</h2>
<p class="my-2 :mt-4 :block :pb-2 :hidden">One important lesson from our experiments is that<span> </span><strong>the model is only one part of the system</strong>. The real performance comes from how we manage knowledge and learning around it.</p>
<p class="my-2 :mt-4 :block :pb-2 :hidden">We currently combine four learning loops, each with its own “clock”:</p>
<ol class="marker:text-quiet list-decimal pl-8">
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden"><strong>RAG</strong></p>
<ul class="marker:text-quiet list-disc">
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">On‑the‑fly retrieval of code and documentation from the indexed corpora</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">No retraining required, index refresh via reindexing</p>
</li>
</ul>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden"><strong>Memory and skills</strong></p>
<ul class="marker:text-quiet list-disc">
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">The assistant can persist facts and procedures (with confirmation)</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">These are re‑used in later sessions, effectively encoding habits and best practices</p>
</li>
</ul>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden"><strong>Curated datasets from real usage</strong></p>
<ul class="marker:text-quiet list-disc">
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">We log selected good exchanges as training samples</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">We also build datasets from git history, documentation, “how‑to” procedures, and teacher‑student distillation on our own codebases</p>
</li>
</ul>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden"><strong>Fine-tuning adapters (QLoRA)</strong></p>
<ul class="marker:text-quiet list-disc">
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">When needed, we can train small LoRA adapters on external GPUs (RunPod, or HEIG‑VD infrastructure)</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">Only a small fraction of weights is adapted, which keeps costs and deployment simple</p>
</li>
</ul>
</li>
</ol>
<p class="my-2 :mt-4 :block :pb-2 :hidden">Interestingly, our latest tests with Qwen3.6‑35B show that, on a focused benchmark of our LVGL/SO3/Verdin questions, the<span> </span><strong>stock model</strong><span> </span>already outperforms our previous fine‑tuned coder model. In that context, a well‑designed RAG + memory + skills setup matters more than aggressive fine‑tuning, at least for now.</p>
<h2>Safety and reliability</h2>
<p class="my-2 :mt-4 :block :pb-2 :hidden">Because this assistant can read and modify code, we’ve built in several safety mechanisms:</p>
<ul class="marker:text-quiet list-disc pl-8">
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">automatic approval only for read‑only commands</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">confirmation required for any write, run, or destructive action</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">protection against full rewrites of large files</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">per‑turn tool call budget and caching to avoid repeated commands</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">explicit mechanisms to mark answers as “good” or “bad” so they are (or are not) used in future training</p>
</li>
</ul>
<p class="my-2 :mt-4 :block :pb-2 :hidden">We also keep the model’s “thinking mode” disabled in this context, as it tends to waste tokens without bringing benefits for our type of tasks.</p>
<h2>Why this matters for our services</h2>
<p class="my-2 :mt-4 :block :pb-2 :hidden">For our customers, this infrastructure is a way to:</p>
<ul class="marker:text-quiet list-disc pl-8">
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">accelerate development and debugging on complex embedded and edge platforms</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">keep all source code and sensitive data within a trusted Swiss‑based infrastructure</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">leverage the latest open models (Qwen, DeepSeek, Apertus, etc.) while retaining full control over deployment</p>
</li>
<li class="py-0 my-0 prose-p:pt-0 prose-p:mb-2 prose-p:my-0 :pt-0 :mb-2 :my-0">
<p class="my-2 :mt-4 :block :pb-2 :hidden">build project‑specific assistants that really “know” the codebase and its conventions</p>
</li>
</ul>
<p class="my-2 :mt-4 :block :pb-2 :hidden">Our collaboration and proximity with REDS is a key asset here. It gives us access to cutting‑edge research around open models and agent frameworks, and allows us to validate ideas quickly on real industrial use cases. It also paves the way for a server‑grade deployment of this assistant on powerful, on‑premise GPU hardware in the near future.</p>
<p class="my-2 :mt-4 :block :pb-2 :hidden">We will continue to evolve this platform, refine our datasets, and specialize the assistant further for embedded and edge scenarios.</p>
<p class="my-2 :mt-4 :block :pb-2 :hidden">If you are interested in local AI assistants, RAG over real codebases, or agent frameworks for embedded systems, we’d be very happy to exchange experiences and ideas.</p>]]></content:encoded>
						                            <category domain="https://edgemtech.ch/community/"></category>                        <dc:creator>Daniel Rossier</dc:creator>
                        <guid isPermaLink="true">https://edgemtech.ch/community/general-discussions/edgem%c2%b7ai-a-local-rag-powered-assistant-for-our-embedded-projects/</guid>
                    </item>
				                    <item>
                        <title>Discovering the home assistant project</title>
                        <link>https://edgemtech.ch/community/general-discussions/discovering-the-home-assistant-project/</link>
                        <pubDate>Sun, 31 May 2026 07:29:44 +0000</pubDate>
                        <description><![CDATA[Recently I have been tasked with discovering and understanding the Home Assistant open source projet. The objective is to make it run on the Toradex Verdin SoM.I am very new to home automati...]]></description>
                        <content:encoded><![CDATA[<p><br />Recently I have been tasked with discovering and understanding the Home Assistant open source projet. The objective is to make it run on the Toradex Verdin SoM.<br /><br />I am very new to home automation, I lived in rented apartments all my life... &#x1f622;  But if I were to own a house then home assistant will surely be my pick.<br />Simply because privacy is one of the top concerns of the home assistant contributors <a href="https://www.home-assistant.io/blog/2016/01/19/perfect-home-automation/" target="_blank" rel="noopener">https://www.home-assistant.io/blog/2016/01/19/perfect-home-automation/</a></p>
<p>I also think that the projet features a nice architecture which makes great use of Docker containers. Smart choice, as it enables them to run on large variety of platforms, on GNU/Linux servers, on NASes or any appliance that supports OCI containers.<br /><br />Home assistant is split into components, here are the main ones:<br /><br /><strong>core</strong> - which is the central container, provides the web UI that is accessible on port 8123<br />and provides many integrations out of the box, Zigbee, KNX, etc..<br /><br /><strong>supervisor</strong> - It sits in between the core container and the operating system, the core container communicates with the supervisor container over HTTP. The supervisor communicates with the services over Dbus.<br /><br /><strong>operating system</strong> - The operating system is built with buildroot and is extended with external packages provided by the home assistant contributors. The kernel is also compiled by buildroot, popular ARM boards are supported Raspberry Pi, ODROID and the Asus Tinker board.<br /><br />I've used the generic aarch64 configuration everything compiled without errors and it took around 2 hours. It was then very straight forward to get it to run in QEMU mainly due to the developer documentation being of good quality and pleasant to read. <a href="https://developers.home-assistant.io/docs/architecture_index" target="_blank" rel="noopener">https://developers.home-assistant.io/docs/architecture_index</a><br /><br />Upon first boot, I was surprised to see that the GRUB bootloader is used by default, which has an entry for A slot and the B slot. However, our objective at EDGEMTech is to use it with TorizonOS so the home assistant operating system will only be used as a reference.<br /><br />The next step was to understand the scripts in <em>buildroot-external/package/hassio</em> which make use of the skopeo utility to fetch the container images and save them as tarballs.<br /><br />Didn't know about the skopeo utility, it allows to inspect containers without needlessly pulling the image, copy images between image stores, convert images. It is worth exploring further if one works with docker often.<br /><br />Stay tuned for more news about home assistant on TorizonOS in a couple of weeks</p>]]></content:encoded>
						                            <category domain="https://edgemtech.ch/community/"></category>                        <dc:creator>Erik Tagirov</dc:creator>
                        <guid isPermaLink="true">https://edgemtech.ch/community/general-discussions/discovering-the-home-assistant-project/</guid>
                    </item>
				                    <item>
                        <title>Zephyr: Start testing your system</title>
                        <link>https://edgemtech.ch/community/general-discussions/zephyr-start-testing-your-system/</link>
                        <pubDate>Mon, 25 May 2026 11:20:45 +0000</pubDate>
                        <description><![CDATA[Writing Unit Tests for Zephyr: A Starter Guide
Zephyr ships with its own unit test framework (Ztest) and an orchestrator (Twister) that builds and runs those tests across emulators, simulat...]]></description>
                        <content:encoded><![CDATA[<h1 class="md-end-block md-heading md-focus"><span class="md-plain md-expand">Writing Unit Tests for Zephyr: A Starter Guide</span></h1>
<p class="md-end-block md-p"><span class="md-plain">Zephyr ships with its own unit test framework (</span><span class="md-pair-s "><strong><span class="md-plain">Ztest</span></strong></span><span class="md-plain">) and an orchestrator (</span><span class="md-pair-s "><strong><span class="md-plain">Twister</span></strong></span><span class="md-plain">) that builds and runs those tests across emulators, simulators, and real hardware. This post walks through what a test looks like, how to wire one into your project, and — in the final chapter — how to make the same suite run across several board variants.</span></p>
<p>You can see this as a starter tutorial to write tests and make your projects more robust! </p>
<div class="md-hr md-end-block"><hr /></div>
<h2><span class="md-plain">1. Why Ztest + Twister?</span></h2>
<p><span class="td-span"><span class="md-plain"><strong>Ztest</strong>: The in-tree unit test framework (</span><span class="md-pair-s"><code>#include &lt;zephyr/ztest.h&gt;</code></span><span class="md-plain">). Provides </span><span class="md-pair-s"><code>ZTEST_SUITE</code></span><span class="md-plain">, </span><span class="md-pair-s"><code>ZTEST</code></span><span class="md-plain">, </span><span class="md-pair-s"><code>zassert_*</code></span><span class="md-plain"> macros, fixtures, and setup/teardown hooks.</span></span></p>
<p><span class="td-span"><span class="md-plain"><strong>Twister</strong>: The test runner. Discovers test cases via </span><span class="md-pair-s"><code>testcase.yaml</code></span><span class="md-plain">, builds each one for every allowed platform, flashes/runs it, and produces a report.</span></span></p>
<p class="md-end-block md-p"><span class="md-plain">Both ship with Zephyr, you do not need a third-party framework.</span></p>
<div class="md-hr md-end-block"><hr /></div>
<h2><span class="md-plain">2. Anatomy of a Test Suite</span></h2>
<p class="md-end-block md-p"><span class="md-plain">A minimal test suite is a small Zephyr application with three files:</span></p>
<pre contenteditable="false">tests/
└── my_feature/
    ├── CMakeLists.txt   # builds the test binary
    ├── prj.conf         # Kconfig for the test build
    ├── testcase.yaml    # tells Twister what to do with it
    └── src/
        └── main.c       # the actual tests</pre>
<p class="md-end-block md-p"><span class="md-plain">That layout — one folder per suite, sitting under </span><span class="md-pair-s"><code>tests/</code></span><span class="md-plain"> at the repo root — is the convention every Zephyr-based project I have worked with follows.</span></p>
<div class="md-hr md-end-block"><hr /></div>
<h2><span class="md-plain">3. Writing the Test Code</span></h2>
<p class="md-end-block md-p"><span class="md-plain">Open </span><span class="md-pair-s"><code>src/main.c</code></span><span class="md-plain"> and start with a suite declaration, then add cases:</span></p>
<pre contenteditable="false"><span><span class="cm-meta">#include &lt;zephyr/ztest.h&gt;</span></span><br /><span><span class="cm-meta">#include &lt;errno.h&gt;</span></span><br /><span><span class="cm-meta">#include "pid_controller.h"</span></span><br /><span>​</span><br /><span><span class="cm-comment">/* ZTEST_SUITE(name, predicate, setup, before, after, teardown) */</span></span><br /><span><span class="cm-variable">ZTEST_SUITE</span>(<span class="cm-variable">pid_controller</span>, <span class="cm-variable">NULL</span>, <span class="cm-variable">NULL</span>, <span class="cm-variable">NULL</span>, <span class="cm-variable">NULL</span>, <span class="cm-variable">NULL</span>);</span><br /><span>​</span><br /><span><span class="cm-variable">ZTEST</span>(<span class="cm-variable">pid_controller</span>, <span class="cm-variable">test_compute_output</span>)</span><br /><span>{</span><br /><span>    <span class="cm-comment">/* identical setpoint and measurement -&gt; no correction */</span></span><br /><span>    <span class="cm-variable">zassert_equal</span>(<span class="cm-variable">PID_Compute_Output</span>(<span class="cm-comment">/*setpoint=*/</span><span class="cm-number">100</span>, <span class="cm-comment">/*measurement=*/</span><span class="cm-number">100</span>), <span class="cm-number">0</span>);</span><br /><span>​</span><br /><span>    <span class="cm-comment">/* positive error -&gt; positive (clamped) output */</span></span><br /><span>    <span class="cm-variable">zassert_equal</span>(<span class="cm-variable">PID_Compute_Output</span>(<span class="cm-comment">/*setpoint=*/</span><span class="cm-number">100</span>, <span class="cm-comment">/*measurement=*/</span>  <span class="cm-number">0</span>), <span class="cm-number">100</span>);</span><br /><span>​</span><br /><span>    <span class="cm-comment">/* invalid gain configuration -&gt; -EINVAL */</span></span><br /><span>    <span class="cm-variable">zassert_equal</span>(<span class="cm-variable">PID_Compute_Output</span>(<span class="cm-comment">/*setpoint=*/</span><span class="cm-operator">-</span><span class="cm-number">1</span>,  <span class="cm-comment">/*measurement=*/</span>  <span class="cm-number">0</span>), <span class="cm-operator">-</span><span class="cm-variable">EINVAL</span>);</span><br /><span>}</span></pre>
<h3><span class="md-plain">Common assertions</span></h3>
<p><strong>- Boolean checks:</strong> <span class="td-span"><span class="md-pair-s"><code>zassert_true(x)</code></span><span class="md-plain"> / </span><span class="md-pair-s"><code>zassert_false(x)<br /></code></span></span><span class="td-span"><span class="md-plain"><strong>- Integer / enum equality:</strong> <span class="md-pair-s"><code>zassert_equal(a, b)<br /></code></span></span></span><span class="td-span"><span class="md-plain"><strong>- Pointer equality:</strong> <span class="md-pair-s"><code>zassert_equal_ptr(a, b)<br /></code></span></span></span><span class="td-span"><span class="md-plain"><strong>- NULL checks:</strong> <span class="md-pair-s"><code>zassert_is_null(p)</code></span> / <span class="md-pair-s"><code>zassert_not_null(p)<br /></code></span></span></span><span class="td-span"><strong><span class="md-plain">- </span><span class="md-plain">Compare </span><span class="md-pair-s"><code>n</code></span></strong><span class="md-plain"><strong> bytes:</strong> <span class="md-pair-s"><code>zassert_mem_equal(p, q, n)</code></span><br /></span></span><span class="td-span"><span class="md-plain"><strong>- Floating-point / tolerance:</strong> <span class="md-pair-s"><code>zassert_within(a, b, tol)</code></span></span></span></p>
<h3><span class="md-plain">Fixtures</span></h3>
<p class="md-end-block md-p"><span class="md-plain">When several cases need the same state, use a fixture:</span></p>
<pre contenteditable="false"><span><span class="cm-keyword">struct</span> <span class="cm-def">fixture</span> {</span><br /><span>    <span class="cm-variable-3">size_t</span> <span class="cm-variable">size</span>;</span><br /><span>    <span class="cm-variable-3">uint8_t</span> <span class="cm-variable">buf</span>;</span><br /><span>};</span><br /><span>​</span><br /><span><span class="cm-keyword">static</span> <span class="cm-variable-3">void</span> <span class="cm-variable-3">*</span><span class="cm-def">suite_setup</span>(<span class="cm-variable-3">void</span>)</span><br /><span>{</span><br /><span>    <span class="cm-keyword">static</span> <span class="cm-keyword">struct</span> <span class="cm-def">fixture</span> <span class="cm-def">f</span>;</span><br /><span>    <span class="cm-keyword">return</span> <span class="cm-operator">&amp;</span><span class="cm-variable">f</span>;</span><br /><span>}</span><br /><span>​</span><br /><span><span class="cm-keyword">static</span> <span class="cm-variable-3">void</span> <span class="cm-def">before_each</span>(<span class="cm-variable-3">void</span> <span class="cm-variable-3">*</span><span class="cm-variable">f</span>)</span><br /><span>{</span><br /><span>    <span class="cm-keyword">struct</span> <span class="cm-def">fixture</span> <span class="cm-operator">*</span><span class="cm-variable">fix</span> <span class="cm-operator">=</span> <span class="cm-variable">f</span>;</span><br /><span>    <span class="cm-variable">memset</span>(<span class="cm-variable">fix</span><span class="cm-operator">-&gt;</span><span class="cm-variable">buf</span>, <span class="cm-number">0xff</span>, <span class="cm-keyword">sizeof</span>(<span class="cm-variable">fix</span><span class="cm-operator">-&gt;</span><span class="cm-variable">buf</span>));</span><br /><span>    <span class="cm-variable">fix</span><span class="cm-operator">-&gt;</span><span class="cm-variable">size</span> <span class="cm-operator">=</span> <span class="cm-number">0</span>;</span><br /><span>}</span><br /><span>​</span><br /><span><span class="cm-variable">ZTEST_SUITE</span>(<span class="cm-variable">my_suite</span>, <span class="cm-variable">NULL</span>, <span class="cm-variable">suite_setup</span>, <span class="cm-variable">before_each</span>, <span class="cm-variable">NULL</span>, <span class="cm-variable">NULL</span>);</span><br /><span>​</span><br /><span><span class="cm-variable">ZTEST_F</span>(<span class="cm-variable">my_suite</span>, <span class="cm-variable">test_buffer_initialized</span>)   <span class="cm-comment">/* note: ZTEST_F, not ZTEST */</span></span><br /><span>{</span><br /><span>    <span class="cm-variable">zassert_equal</span>(<span class="cm-variable">fixture</span><span class="cm-operator">-&gt;</span><span class="cm-variable">size</span>, <span class="cm-number">0</span>);</span><br /><span>    <span class="cm-variable">zassert_equal</span>(<span class="cm-variable">fixture</span><span class="cm-operator">-&gt;</span><span class="cm-variable">buf</span>, <span class="cm-number">0xff</span>);</span><br /><span>}</span></pre>
<p class="md-end-block md-p"><span class="md-pair-s"><code>ZTEST_F</code></span><span class="md-plain"> automatically gives you a </span><span class="md-pair-s"><code>fixture</code></span><span class="md-plain"> pointer inside the test body.</span></p>
<div class="md-hr md-end-block"><hr /></div>
<h2><span class="md-plain">4. Wiring it into the Build</span></h2>
<h3><span class="md-pair-s"><code>prj.conf</code></span></h3>
<p class="md-end-block md-p"><span class="md-plain">The bare minimum:</span></p>
<pre contenteditable="false"><span>CONFIG_ZTEST=y</span><br /><span>CONFIG_NEWLIB_LIBC=y           # for printf/floats in assertions</span><br /><span>CONFIG_DEBUG_OPTIMIZATIONS=y</span></pre>
<p class="md-end-block md-p"><span class="md-plain">Add whatever Kconfig your code-under-test needs (drivers, subsystems, etc.).</span></p>
<h3><span class="md-pair-s"><code>CMakeLists.txt</code></span></h3>
<pre contenteditable="false"><span><span class="cm-def">cmake_minimum_required</span><span class="cm-bracket">(</span>VERSION <span class="cm-number">3</span>.20.0<span class="cm-bracket">)</span></span><br /><span><span class="cm-def">find_package</span><span class="cm-bracket">(</span>Zephyr REQUIRED HINTS <span class="cm-variable-2">$ENV</span>{ZEPHYR_BASE}<span class="cm-bracket">)</span></span><br /><span>​</span><br /><span><span class="cm-def">project</span><span class="cm-bracket">(</span>my_feature<span class="cm-bracket">)</span></span><br /><span>​</span><br /><span><span class="cm-def">target_sources</span><span class="cm-bracket">(</span>app PRIVATE</span><br /><span>    src/main.c</span><br /><span>    <span class="cm-variable-2">${CMAKE_SOURCE_DIR}</span>/../../src/modules/control/pid_controller.c</span><br /><span><span class="cm-bracket">)</span></span><br /><span>​</span><br /><span><span class="cm-def">target_include_directories</span><span class="cm-bracket">(</span>app PRIVATE</span><br /><span>    <span class="cm-variable-2">${CMAKE_SOURCE_DIR}</span>/../../src/modules/control</span><br /><span><span class="cm-bracket">)</span></span></pre>
<blockquote>
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">Tip — share common test plumbing.</span></strong></span><span class="md-plain"> If you have repository-wide includes, mocks, or compile flags, factor them into a </span><span class="md-pair-s"><code>tests/&lt;your_project&gt;_test.cmake</code></span><span class="md-plain"> file and </span><span class="md-pair-s"><code>include(...)</code></span><span class="md-plain"> it at the top of each suite's </span><span class="md-pair-s"><code>CMakeLists.txt</code></span><span class="md-plain">. It keeps every suite's CMake to a single screen.</span></p>
</blockquote>
<h3><span class="md-pair-s"><code>testcase.yaml</code></span></h3>
<p class="md-end-block md-p"><span class="md-plain">This is Twister's manifest. The simplest possible version:</span></p>
<pre contenteditable="false"><span><span class="cm-atom">tests</span><span class="cm-meta">:</span></span><br /><span><span class="cm-atom">  acme.my_feature</span><span class="cm-meta">:</span></span><br /><span><span class="cm-atom">    platform_allow</span><span class="cm-meta">:</span></span><br /><span><span class="cm-meta">      - </span>mps2/an521/cpu0</span><br /><span><span class="cm-atom">    tags</span><span class="cm-meta">:</span></span><br /><span><span class="cm-meta">      - </span>acme</span></pre>
<p class="md-end-block md-p"><span class="md-plain">The test name is </span><span class="md-pair-s"><code>&lt;group&gt;.&lt;suite&gt;</code></span><span class="md-plain"> and is what you'll see in reports.</span></p>
<div class="md-hr md-end-block"><hr /></div>
<h2><span class="md-plain">5. Running the Tests</span></h2>
<p class="md-end-block md-p"><span class="md-plain">From the project root, with the Zephyr environment activated:</span></p>
<pre contenteditable="false"><span><span class="cm-comment"># Run a single suite</span></span><br /><span>west twister <span class="cm-attribute">--clobber-output</span> <span class="cm-attribute">--jobs</span> <span class="cm-number">1</span> <span class="cm-attribute">-T</span> tests/my_feature</span><br /><span>​</span><br /><span><span class="cm-comment"># Run everything under tests/</span></span><br /><span>west twister <span class="cm-attribute">--clobber-output</span> <span class="cm-attribute">--jobs</span> <span class="cm-number">1</span> <span class="cm-attribute">-T</span> tests</span><br /><span>​</span><br /><span><span class="cm-comment"># Run on real hardware</span></span><br /><span>west twister <span class="cm-attribute">--hardware-map</span> hardware-map.yaml \</span><br /><span>             <span class="cm-attribute">--device-testing</span> <span class="cm-attribute">--inline-logs</span> \</span><br /><span>             <span class="cm-attribute">--clobber-output</span> <span class="cm-attribute">--jobs</span> <span class="cm-number">1</span> <span class="cm-attribute">-T</span> tests</span></pre>
<p class="md-end-block md-p"><span class="md-plain">Twister deposits build artifacts and logs under </span><span class="md-pair-s"><code>twister-out/</code></span><span class="md-plain">. Open </span><span class="md-pair-s"><code>twister-out/twister.log</code></span><span class="md-plain"> (or the per-suite </span><span class="md-pair-s"><code>handler.log</code></span><span class="md-plain">) when something fails — Ztest prints the file, line, and the values that didn't match.</span></p>
<p class="md-end-block md-p"><span class="md-plain">For quick local iteration without Twister, you can </span><span class="md-pair-s"><code>west build -b &lt;board&gt; tests/my_feature</code></span><span class="md-plain"> like any other Zephyr app and flash/run it directly.</span></p>
<div class="md-hr md-end-block"><hr /></div>
<h2><span class="md-plain">6. Targeting Multiple Boards (the Multi-Target Chapter)</span></h2>
<p class="md-end-block md-p"><span class="md-plain">Real projects rarely have </span><span class="md-pair-s "><em><span class="md-plain">one</span></em></span><span class="md-plain"> board. You typically have:</span></p>
<ul class="ul-list" data-mark="-">
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">An emulator</span></strong></span><span class="md-plain"> for tests with no hardware dependencies (</span><span class="md-pair-s"><code>native_sim</code></span><span class="md-plain">, </span><span class="md-pair-s"><code>mps2/an521/cpu0</code></span><span class="md-plain">, </span><span class="md-pair-s"><code>qemu_x86</code></span><span class="md-plain">, …). These run in CI in seconds.</span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">One or more real board revisions</span></strong></span><span class="md-plain"> (</span><span class="md-pair-s"><code>acme_node@0.1.0</code></span><span class="md-plain">, </span><span class="md-pair-s"><code>acme_node@0.1.1</code></span><span class="md-plain">, …) for tests that need actual peripherals (sensors, LEDs, flash).</span></p>
</li>
</ul>
<p class="md-end-block md-p"><span class="md-plain">Twister's </span><span class="md-pair-s"><code>platform_allow</code></span><span class="md-plain"> field is what controls this per test. Pick the narrowest set that still covers the test's intent:</span></p>
<pre contenteditable="false"># Pure logic — runs anywhere with a libc. Put it on the emulator.
tests:
  acme.pid_controller:
    platform_allow:
      - mps2/an521/cpu0
      - acme_node@0.1.0
      - acme_node@0.1.1
    tags: testing
# Driver / peripheral test — on-target only.
tests:
  acme.gpio_isr_ordering:
    platform_allow:
      - acme_node@0.1.0
      - acme_node@0.1.1
    tags:
      - testing
      - gpio
# Pure emulator (heavy logic that doesn't touch any DTS nodes).
tests:
  acme.signal_filter_stress:
    platform_allow: mps2/an521/cpu0
    tags: acme
    timeout: 180</pre>
<h3><span class="md-plain">Sharing per-board configuration</span></h3>
<p class="md-end-block md-p"><span class="md-plain">When </span><span class="md-pair-s "><em><span class="md-plain">every</span></em></span><span class="md-plain"> on-target test needs the same Kconfig or DTS overlays (for example, routing the test console out a USB CDC ACM port so Twister can read it), don't repeat it in each suite. Centralize it:</span></p>
<pre contenteditable="false"><span>tests/</span><br /><span>├── acme_test.cmake            # common CMake helper</span><br /><span>├── acme_test.conf             # common Kconfig (on-target only)</span><br /><span>└── boards/</span><br /><span>    └── acme_console.overlay   # common DTS overlay (on-target only)</span></pre>
<p class="md-end-block md-p"><span class="md-plain">Then in the shared CMake helper, append them only when the target is a real board, not the emulator:</span></p>
<pre contenteditable="false"><span><span class="cm-def">if</span><span class="cm-bracket">(</span>NOT <span class="cm-variable-2">${BOARD}</span> MATCHES <span class="cm-string">"^mps2"</span><span class="cm-bracket">)</span></span><br /><span>    <span class="cm-def">list</span><span class="cm-bracket">(</span>APPEND CONF_FILE  <span class="cm-variable-2">${PROJECT_ROOT}</span>/tests/acme_test.conf<span class="cm-bracket">)</span></span><br /><span>    <span class="cm-def">list</span><span class="cm-bracket">(</span>APPEND DTC_OVERLAY_FILE <span class="cm-variable-2">${PROJECT_ROOT}</span>/tests/boards/acme_console.overlay<span class="cm-bracket">)</span></span><br /><span><span class="cm-def">endif</span><span class="cm-bracket">()</span></span><br /><span>​</span><br /><span><span class="cm-def">list</span><span class="cm-bracket">(</span>APPEND CONF_FILE <span class="cm-variable-2">${CMAKE_SOURCE_DIR}</span>/prj.conf<span class="cm-bracket">)</span></span></pre>
<p class="md-end-block md-p"><span class="md-plain">The benefit: each individual </span><span class="md-pair-s"><code>testcase.yaml</code></span><span class="md-plain"> only declares </span><span class="md-pair-s "><strong><span class="md-plain">what platforms the test is valid on</span></strong></span><span class="md-plain">, and CMake takes care of supplying the right overlays and configs for each one.</span></p>
<h3><span class="md-plain">Per-board overlays (when you actually need them)</span></h3>
<p class="md-end-block md-p"><span class="md-plain">If a single board revision needs a different pin or a tweaked node, drop a board-specific overlay next to the suite:</span></p>
<pre contenteditable="false">tests/my_feature/
├── boards/
│   ├── acme_node_0_1_0.overlay
│   └── acme_node_0_1_1.overlay
├── prj.conf
└── ...</pre>
<p class="md-end-block md-p"><span class="md-plain">Zephyr automatically picks </span><span class="md-pair-s"><code>boards/&lt;normalized_board_name&gt;.overlay</code></span><span class="md-plain"> when building. No CMake changes required.</span></p>
<h3><span class="md-plain">Rules of thumb</span></h3>
<ol class="ol-list" start="">
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">Default to the emulator.</span></strong></span><span class="md-plain"> If the code under test does not touch DTS nodes or peripherals, put it on the QEMU/native platform only — those tests run in CI without hardware.</span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">Hardware-only when unavoidable.</span></strong></span><span class="md-plain"> Driver mutex ordering, LED animation, real flash, ADC scans — these belong on the on-target list.</span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">List every supported board revision.</span></strong></span><span class="md-plain"> It is tempting to list one and trust the others "work the same." They don't. List </span><span class="md-pair-s"><code>acme_node@0.1.0</code></span><span class="md-plain"> </span><span class="md-pair-s "><em><span class="md-plain">and</span></em></span><span class="md-plain"> </span><span class="md-pair-s"><code>acme_node@0.1.1</code></span><span class="md-plain"> so a DTS regression on one revision is caught immediately.</span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">Tag aggressively.</span></strong></span><span class="md-plain"> Tags let you slice runs in CI (</span><span class="md-pair-s"><code>--tag testing</code></span><span class="md-plain">, </span><span class="md-pair-s"><code>--tag gpio</code></span><span class="md-plain">) without editing yaml.</span></p>
</li>
</ol>
<p class="md-end-block md-p"> </p>
<h2><span class="md-plain">8. Conclusion</span></h2>
<p class="md-end-block md-p"><span class="md-plain">The mental model that makes Zephyr testing click is this: </span><span class="md-pair-s "><strong><span class="md-plain">a Ztest suite is its own standalone Zephyr application</span></strong></span><span class="md-plain"> — same build system, same Kconfig, same devicetree — that just happens to call </span><span class="md-pair-s"><code>ZTEST</code></span><span class="md-plain"> functions instead of </span><span class="md-pair-s"><code>main()</code></span><span class="md-plain">. When it runs on </span><span class="md-pair-s"><code>mps2/an521/cpu0</code></span><span class="md-plain">, it's a tiny firmware image running inside QEMU. When it runs on </span><span class="md-pair-s"><code>acme_node@0.1.0</code></span><span class="md-plain">, it's a tiny firmware image running on the silicon, talking to real peripherals over real buses.</span></p>
<p class="md-end-block md-p"><span class="md-plain">That framing is what makes the approach so powerful. You can exercise a single module, a single driver, or a single piece of business logic </span><span class="md-pair-s "><strong><span class="md-plain">lifted out of the full application flow</span></strong></span><span class="md-plain">, with nothing else running around it to muddy the picture. No state machine in another thread, no UI events firing, no background work — just the unit under test and the assertions you wrote against it. If a sensor driver behaves correctly in its standalone suite but misbehaves in the application, you now know the bug is in integration, not in the driver itself. That is an enormous amount of information for very little code.</span></p>
<p class="md-end-block md-p md-focus"><span class="md-plain">Whether you are validating a pure algorithm on the emulator or proving that a specific board revision wires up its LEDs the way you expect, the recipe is the same: a small folder, a </span><span class="md-pair-s"><code>main.c</code></span><span class="md-plain"> with a few </span><span class="md-pair-s"><code>ZTEST</code></span><span class="md-plain"> cases, and a one-line </span><span class="md-pair-s"><code>platform_allow</code></span><span class="md-plain md-expand">. Build that habit once and every feature you add gets a matching safety net for free.</span></p>
<h2><span class="md-plain">Stay tuned</span></h2>
<p class="md-end-block md-p">A more advanced guide will come soon, covering some of the remaining more complex features proposed by Zephyr! </p>]]></content:encoded>
						                            <category domain="https://edgemtech.ch/community/"></category>                        <dc:creator>Gabriel CATEL TORRES</dc:creator>
                        <guid isPermaLink="true">https://edgemtech.ch/community/general-discussions/zephyr-start-testing-your-system/</guid>
                    </item>
				                    <item>
                        <title>TorizonOS: TEZI &amp; Manual Image Customization</title>
                        <link>https://edgemtech.ch/community/orchestration-and-services/torizonos-tezi-and-image-json/</link>
                        <pubDate>Sun, 10 May 2026 06:45:49 +0000</pubDate>
                        <description><![CDATA[In today&#039;s post, we&#039;ll cover how Toradex Easy Installer TEZI installs a TorizonOS image. We&#039;ll also learn how to modify it to add customs components and to be able to automatically install a...]]></description>
                        <content:encoded><![CDATA[<p>In today's post, we'll cover how Toradex Easy Installer TEZI installs a TorizonOS image. We'll also learn how to modify it to add customs components and to be able to automatically install an image. This post also extends the one written by my colleague <a href="https://edgemtech.ch/community/general-discussions/customization-of-torizonos/" target="_blank" rel="noopener">here</a>.</p>
<p>It is to note that the image customization part of this post is not officially supported by Toradex and is more of a "dive deeper" in TEZI than an official guide. A future post will cover a more production ready procedure, while this one is more of a discovery/custom guide. </p>
<h2>TEZI introduction</h2>
<p>TEZI is the official way to flash an Torizon OS image in Toradex ecosystem. It is a simple Linux running in RAM, flashed over USB after booting the board into recovery. The Toradex boards all come pre-flashed with TEZI in their eMMC, removing the need of setting up the recovery process.</p>
<p>TEZI's job is simple:</p>
<ol>
<li>Set up the storage and network</li>
<li>Scan Toradex official feeds (network) and any storage peripherals (USB, SD Card)</li>
<li>Launch a UI over the HDMI listing the images and some settings</li>
<li>Flash the image once it is selected by the user</li>
<li>Ask to reboot</li>
</ol>
<p>And voilà, TEZI's job is done! So what does TEZI look at to flash the image?</p>
<h2>TEZI bundled files</h2>
<p>After a successful Yocto build, the TorizonOS image is packaged as a tarball ready for TEZI:</p>
<pre contenteditable="false">torizon/build/deploy/images/verdin-imx8mp/torizon-docker-verdin-imx8mp-Tezi.tar</pre>
<p>Extracting it gives:</p>
<pre contenteditable="false">.
├── image.json
├── imx-boot
├── torizon-docker-verdin-imx8mp.ota.tar.zst
├── u-boot-initial-env-sd
├── prepare.sh
├── wrapup.sh
├── marketing.tar
├── toradexlinux.png
└── LA_OPT_NXP_SW.html</pre>
<p>The key files are:</p>
<ul>
<li><strong><code>image.json:</code></strong>the descriptor that tells TEZI how to handle everything</li>
<li><strong><code>imx-boot: </code></strong>U-Boot + SPL, combined with LPDDR firmware and ARM Trusted Firmware</li>
<li><strong><code>torizon-docker-verdin-imx8mp.ota.tar.zst:</code></strong> the OSTree rootfs archive</li>
<li><strong><code>u-boot-initial-env-sd:</code></strong> the initial U-Boot environment</li>
</ul>
<h2><code>image.json</code></h2>
<p>This file is what TEZI reads first. It describes the image metadata and, how each block device should be partitioned and populated. A standard, unmodified file looks like this:</p>
<div>
<pre contenteditable="false">{
   "config_format": "4",
   "autoinstall": false,
   "name": "Torizon OS",
   "description": "Torizon OS Linux with no containers pre-provisioned.",
   "version": "7.4.0-devel-20250930075455+build.0",
   "release_date": "2025-09-30",
   "u_boot_env": "u-boot-initial-env-sd",
   "prepare_script": "prepare.sh",
   "wrapup_script": "wrapup.sh",
   "supported_product_ids": ,
   "blockdevs": [
      {
         "name": "emmc",
         "partitions": 
      },
      {
         "name": "emmc-boot0",
         "erase": true,
         "content": {
            "filesystem_type": "raw",
            "rawfiles": 
         }
      }
   ]
}</pre>
<p>&nbsp;</p>
</div>
<p>Breaking down the important fields:</p>
<ul>
<li><strong><code>autoinstall: </code></strong>when<span> </span><code>true</code>, TEZI will install the image automatically upon detection, without any user interaction. Useful for headless provisioning.</li>
<li><strong><code>supported_product_ids:</code></strong> the list of Toradex product IDs this image supports. TEZI uses this to validate hardware compatibility before flashing.</li>
<li><strong><code>blockdevs:</code></strong> the core of the file. Describes what goes where on each block device.
<ul>
<li><code>emmc</code>: the main eMMC partition, formatted as ext4, receives the OSTree rootfs.</li>
<li><code>emmc-boot0</code>: the hardware boot partition (4 MiB), written in raw mode at offset 0 with<span> </span><code>imx-boot</code>.</li>
</ul>
</li>
<li><strong><code>prepare_script</code><span> </span>/<span> </span><code>wrapup_script:</code></strong> shell scripts that TEZI runs before and after flashing, respectively. Used for board-specific setup.</li>
</ul>
<h2>Customizing the flash</h2>
<p>The standard TorizonOS image boots using Toradex's own U-Boot and device tree. In our project, we need to boot from our own <strong>ITB</strong> (Image Tree Blob), a U-Boot FIT image that bundles our custom kernel, device tree, and initramfs into a single signed artifact, using a custom boot script to load it.</p>
<p>TEZI supports copying additional files into the ext4 partition through the <strong><code>filelist</code></strong> key inside a partition's <code>content</code> block. We use this to inject our ITB and boot script at flash time, alongside the OSTree rootfs:</p>
<div>
<pre contenteditable="false">"content": {
   "label": "otaroot",
   "filesystem_type": "ext4",
   "mkfs_options": "-E nodiscard",
   "filename": "torizon-docker-verdin-imx8mp.ota.tar.zst",
   "uncompressed_size": 674.7265625,
   "filelist": 
}</pre>
<p>&nbsp;</p>
</div>
<p>TEZI extracts the rootfs archive first, then copies each file listed in <code>filelist</code> directly into the partition root. Both <code>verdin-imx8mp.itb</code> and <code>boot.edgem.scr</code> must be present alongside <code>image.json</code> on the USB drive.</p>
<p>The second change is setting <code>"autoinstall": true</code>. This instructs TEZI to skip the selection UI and flash immediately upon detecting the USB drive, making it possible to provision boards with no display and no operator interaction.</p>
<p>Once again, this is not the official way of modifying the TorizonOS image. To customize an image it without manually modifying those files, one must use TorizonCore Builder.</p>
<h2>Flashing the board</h2>
<p>Preparing a USB drive for deployment comes down to:</p>
<ol>
<li>Extracting the Yocto-produced TEZI tarball onto an ext4-formatted USB stick.</li>
<li>Replacing<span> </span><code>imx-boot</code><span> </span>with our custom build (U-Boot + SPL + ATF).</li>
<li>Adding<span> </span><code>verdin-imx8mp.itb</code><span> </span>and<span> </span><code>boot.edgem.scr</code><span> </span>next to<span> </span><code>image.json</code>.</li>
<li>Editing<span> </span><code>image.json</code><span> </span>to add the<span> </span><code>filelist</code><span> </span>entries and set<span> </span><code>autoinstall</code><span> </span>to<span> </span><code>true</code>.</li>
</ol>
<p>Once plugged in, TEZI handles the rest: it partitions the eMMC, writes the bootloader to <code>emmc-boot0</code>, extracts the OSTree rootfs, and copies the ITB and boot script into the ext4 partition. On the next power cycle, our custom U-Boot finds <code>verdin-imx8mp.itb</code> at the root of the partition and hands off control to it.</p>
<p>The result is a board fully provisioned through TEZI  in a single step, without even needing a display!</p>
<p>&nbsp;</p>]]></content:encoded>
						                            <category domain="https://edgemtech.ch/community/"></category>                        <dc:creator>David Truan</dc:creator>
                        <guid isPermaLink="true">https://edgemtech.ch/community/orchestration-and-services/torizonos-tezi-and-image-json/</guid>
                    </item>
				                    <item>
                        <title>Zephyr Device Power Management</title>
                        <link>https://edgemtech.ch/community/general-discussions/zephyr-device-power-management/</link>
                        <pubDate>Sun, 12 Apr 2026 15:17:37 +0000</pubDate>
                        <description><![CDATA[Zephyr Device Power Management &amp; Power Domains: A Hands-On Guide
If you&#039;re building a battery-powered product on Zephyr, you&#039;ll spend more time on power management than almost anything ...]]></description>
                        <content:encoded><![CDATA[<h1 class="md-end-block md-heading md-focus"><span class="md-plain md-expand">Zephyr Device Power Management &amp; Power Domains: A Hands-On Guide</span></h1>
<p class="md-end-block md-p"><span class="md-plain md-expand">If you're building a battery-powered product on Zephyr, you'll spend more time on power management than almost anything else. Controlling the CPU idle state is only half the battle - the real wins come from suspending individual peripherals (sensors, buses, radios, displays) when they're not in use, and cutting power to entire groups of devices through power domains.</span></p>
<p class="md-end-block md-p"><span class="md-plain">This post is a practical, code-first walkthrough of </span><span class="md-pair-s "><strong><span class="md-plain">Device PM</span></strong></span><span class="md-plain"> and </span><span class="md-pair-s "><strong><span class="md-plain">Power Domains</span></strong></span><span class="md-plain"> in the latest Zephyr. We'll go from zero to a working setup with full Devicetree, Kconfig, and C driver examples.</span></p>
<div class="md-hr md-end-block"><hr /></div>
<h2><span class="md-plain">The Big Picture</span></h2>
<p class="md-end-block md-p"><span class="md-plain">Zephyr device PM has two modes:</span></p>
<p>- System-Managed</p>
<p>- Device Runtime PM</p>
<p>The preferred approach for anything non-trivial is Device Runtime PM</p>
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">Power Domains</span></strong></span><span class="md-plain"> layer on top of either mode. They model a shared power rail (a regulator, a load switch, an SoC internal domain) and propagate power on/off events to all child devices.</span></p>
<p class="md-end-block md-p"><span class="md-plain">We'll focus on </span><span class="md-pair-s "><strong><span class="md-plain">Device Runtime PM + Power Domains</span></strong></span><span class="md-plain">, since that's what real-world products use.</span></p>
<div class="md-hr md-end-block"><hr /></div>
<h2><span class="md-plain">Step 1: Kconfig - Enabling the Subsystem</span></h2>
<pre contenteditable="false"><span># prj.conf</span><br /><span>​</span><br /><span># Core device PM support</span><br /><span>CONFIG_PM_DEVICE=y</span><br /><span>​</span><br /><span># Reference-counted runtime PM (get/put API)</span><br /><span>CONFIG_PM_DEVICE_RUNTIME=y</span><br /><span>​</span><br /><span># Power domain support</span><br /><span>CONFIG_PM_DEVICE_POWER_DOMAIN=y</span><br /><span>​</span><br /><span># Optional: helpful during development</span><br /><span>CONFIG_PM_DEVICE_SHELL=y</span><br /><span>CONFIG_DEVICE_SHELL=y</span></pre>
<p class="md-end-block md-p"> </p>
<h2><span class="md-plain">Step 2: Devicetree - Defining Your Power Domain and Devices</span></h2>
<p class="md-end-block md-p"><span class="md-plain">Let's say you have a board with a load switch on GPIO P0.14 that powers an I2C bus with a temperature sensor and an accelerometer. Here's the complete DTS:</span></p>
<pre contenteditable="false"><span>/ {</span><br /><span>    /*</span><br /><span>     * A GPIO-controlled load switch powering the sensor rail.</span><br /><span>     * 'power-domain-gpio' is a built-in Zephyr driver.</span><br /><span>     */</span><br /><span>    sensor_rail: sensor-power-rail {</span><br /><span>        compatible = "power-domain-gpio";</span><br /><span>        #power-domain-cells = &lt;0&gt;;</span><br /><span>​</span><br /><span>        enable-gpios = &lt;&amp;gpio0 14 GPIO_ACTIVE_HIGH&gt;;</span><br /><span>        startup-delay-us = &lt;1500&gt;;   /* time for rail to stabilize */</span><br /><span>        off-on-delay-us = &lt;10000&gt;;   /* minimum off time before re-enabling */</span><br /><span>​</span><br /><span>        /*</span><br /><span>         * Let Zephyr auto-enable runtime PM for this domain.</span><br /><span>         * Without this, you'd need to call pm_device_runtime_enable()</span><br /><span>         * manually during init.</span><br /><span>         */</span><br /><span>        zephyr,pm-device-runtime-auto;</span><br /><span>    };</span><br /><span>};</span><br /><span>​</span><br /><span>/* The I2C bus behind the load switch */</span><br /><span>&amp;i2c1 {</span><br /><span>    status = "okay";</span><br /><span>    clock-frequency = &lt;I2C_BITRATE_FAST&gt;;</span><br /><span>​</span><br /><span>    /* Declare this bus is powered by our domain */</span><br /><span>    power-domains = &lt;&amp;sensor_rail&gt;;</span><br /><span>    zephyr,pm-device-runtime-auto;</span><br /><span>​</span><br /><span>    /* Temperature sensor on the bus */</span><br /><span>    temp_sensor: tmp117@48 {</span><br /><span>        compatible = "ti,tmp117";</span><br /><span>        reg = &lt;0x48&gt;;</span><br /><span>        power-domains = &lt;&amp;sensor_rail&gt;;</span><br /><span>    };</span><br /><span>​</span><br /><span>    /* Accelerometer on the same bus */</span><br /><span>    accel: lis2dh@19 {</span><br /><span>        compatible = "st,lis2dh";</span><br /><span>        reg = &lt;0x19&gt;;</span><br /><span>        irq-gpios = &lt;&amp;gpio0 22 GPIO_ACTIVE_HIGH&gt;;</span><br /><span>        power-domains = &lt;&amp;sensor_rail&gt;;</span><br /><span>    };</span><br /><span>};</span></pre>
<p class="md-end-block md-p"><span class="md-plain">Key things to notice:</span></p>
<ol class="ol-list" start="">
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">Both the bus and the devices</span></strong></span><span class="md-plain"> declare </span><span class="md-pair-s"><code>power-domains = &lt;&amp;sensor_rail&gt;</code></span><span class="md-plain">. When all children release their references, the domain powers down automatically.</span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-pair-s"><code>zephyr,pm-device-runtime-auto</code></span></strong></span><span class="md-plain"> on the domain and bus means Zephyr enables runtime PM right after successful init - no extra code needed.</span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-pair-s"><code>startup-delay-us</code></span></strong></span><span class="md-plain"> gives the rail time to stabilize before child devices try to communicate. This prevents I2C NACK storms after power-up.</span></p>
</li>
</ol>
<div class="md-hr md-end-block"><hr /></div>
<h2><span class="md-plain">Step 3: The PM Action Callback - Heart of Every PM-Aware Driver</span></h2>
<p class="md-end-block md-p"><span class="md-plain">Every driver that supports power management must implement a callback that handles four actions:</span></p>
<pre contenteditable="false"><span>TURN_ON  ──► SUSPENDED  ──► ACTIVE</span><br /><span>                 ▲              │</span><br /><span>                 │              │</span><br /><span>              SUSPEND ◄────────┘</span><br /><span>                 │</span><br /><span>                 ▼</span><br /><span>             TURN_OFF</span></pre>
<ul class="ul-list" data-mark="-">
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-pair-s"><code>TURN_ON</code></span></strong></span><span class="md-plain">: Power domain came alive. Configure pins/hardware into a known </span><span class="md-pair-s "><strong><span class="md-plain">suspended</span></strong></span><span class="md-plain"> state.</span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-pair-s"><code>RESUME</code></span></strong></span><span class="md-plain">: Move from suspended to active. Initialize hardware, enable interrupts.</span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-pair-s"><code>SUSPEND</code></span></strong></span><span class="md-plain">: Move from active to suspended. Disable interrupts, put hardware to sleep.</span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-pair-s"><code>TURN_OFF</code></span></strong></span><span class="md-plain">: Power domain is going down. Disconnect pins to prevent backpowering through GPIOs.</span></p>
</li>
</ul>
<p class="md-end-block md-p"><span class="md-plain">Here's a complete example for a fictional gas sensor driver:</span></p>
<pre contenteditable="false"><span><span class="cm-meta">#include &lt;zephyr/device.h&gt;</span></span><br /><span><span class="cm-meta">#include &lt;zephyr/drivers/i2c.h&gt;</span></span><br /><span><span class="cm-meta">#include &lt;zephyr/drivers/gpio.h&gt;</span></span><br /><span><span class="cm-meta">#include &lt;zephyr/pm/device.h&gt;</span></span><br /><span><span class="cm-meta">#include &lt;zephyr/pm/device_runtime.h&gt;</span></span><br /><span>​</span><br /><span><span class="cm-meta">#define DT_DRV_COMPAT acme_gas_sensor</span></span><br /><span>​</span><br /><span><span class="cm-keyword">struct</span> <span class="cm-def">gas_sensor_config</span> {</span><br /><span>    <span class="cm-keyword">struct</span> <span class="cm-def">i2c_dt_spec</span> <span class="cm-def">bus</span>;</span><br /><span>    <span class="cm-keyword">struct</span> <span class="cm-def">gpio_dt_spec</span> <span class="cm-def">alert_gpio</span>;</span><br /><span>};</span><br /><span>​</span><br /><span><span class="cm-keyword">struct</span> <span class="cm-def">gas_sensor_data</span> {</span><br /><span>    <span class="cm-keyword">struct</span> <span class="cm-def">gpio_callback</span> <span class="cm-def">alert_cb</span>;</span><br /><span>    <span class="cm-variable-3">uint16_t</span> <span class="cm-variable">last_reading</span>;</span><br /><span>};</span><br /><span>​</span><br /><span><span class="cm-comment">/* ──────────────── PM helpers ──────────────── */</span></span><br /><span>​</span><br /><span><span class="cm-keyword">static</span> <span class="cm-variable-3">int</span> <span class="cm-def">gas_sensor_hw_resume</span>(<span class="cm-keyword">const</span> <span class="cm-keyword">struct</span> <span class="cm-def">device</span> <span class="cm-operator">*</span><span class="cm-variable">dev</span>)</span><br /><span>{</span><br /><span>    <span class="cm-keyword">const</span> <span class="cm-keyword">struct</span> <span class="cm-def">gas_sensor_config</span> <span class="cm-operator">*</span><span class="cm-variable">cfg</span> <span class="cm-operator">=</span> <span class="cm-variable">dev</span><span class="cm-operator">-&gt;</span><span class="cm-variable">config</span>;</span><br /><span>    <span class="cm-keyword">struct</span> <span class="cm-def">gas_sensor_data</span> <span class="cm-operator">*</span><span class="cm-variable">data</span> <span class="cm-operator">=</span> <span class="cm-variable">dev</span><span class="cm-operator">-&gt;</span><span class="cm-variable">data</span>;</span><br /><span>    <span class="cm-variable-3">int</span> <span class="cm-variable">ret</span>;</span><br /><span>​</span><br /><span>    <span class="cm-comment">/* Get the bus so we can talk to the device */</span></span><br /><span>    <span class="cm-variable">ret</span> <span class="cm-operator">=</span> <span class="cm-variable">pm_device_runtime_get</span>(<span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">bus</span>.<span class="cm-variable">bus</span>);</span><br /><span>    <span class="cm-keyword">if</span> (<span class="cm-variable">ret</span> <span class="cm-operator">&lt;</span> <span class="cm-number">0</span>) {</span><br /><span>        <span class="cm-keyword">return</span> <span class="cm-variable">ret</span>;</span><br /><span>    }</span><br /><span>​</span><br /><span>    <span class="cm-comment">/* Wake the sensor from its internal sleep mode */</span></span><br /><span>    <span class="cm-variable-3">uint8_t</span> <span class="cm-variable">wake_cmd</span> <span class="cm-operator">=</span> <span class="cm-number">0x01</span>;</span><br /><span>    <span class="cm-variable">ret</span> <span class="cm-operator">=</span> <span class="cm-variable">i2c_write_dt</span>(<span class="cm-operator">&amp;</span><span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">bus</span>, <span class="cm-operator">&amp;</span><span class="cm-variable">wake_cmd</span>, <span class="cm-number">1</span>);</span><br /><span>    <span class="cm-keyword">if</span> (<span class="cm-variable">ret</span> <span class="cm-operator">&lt;</span> <span class="cm-number">0</span>) {</span><br /><span>        <span class="cm-variable">pm_device_runtime_put</span>(<span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">bus</span>.<span class="cm-variable">bus</span>);</span><br /><span>        <span class="cm-keyword">return</span> <span class="cm-variable">ret</span>;</span><br /><span>    }</span><br /><span>​</span><br /><span>    <span class="cm-comment">/* Enable the alert interrupt */</span></span><br /><span>    <span class="cm-variable">gpio_add_callback</span>(<span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">alert_gpio</span>.<span class="cm-variable">port</span>, <span class="cm-operator">&amp;</span><span class="cm-variable">data</span><span class="cm-operator">-&gt;</span><span class="cm-variable">alert_cb</span>);</span><br /><span>    <span class="cm-variable">gpio_pin_interrupt_configure_dt</span>(<span class="cm-operator">&amp;</span><span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">alert_gpio</span>, <span class="cm-variable">GPIO_INT_EDGE_TO_ACTIVE</span>);</span><br /><span>​</span><br /><span>    <span class="cm-comment">/* Release bus -- we only need it during transactions */</span></span><br /><span>    <span class="cm-variable">pm_device_runtime_put</span>(<span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">bus</span>.<span class="cm-variable">bus</span>);</span><br /><span>​</span><br /><span>    <span class="cm-keyword">return</span> <span class="cm-number">0</span>;</span><br /><span>}</span><br /><span>​</span><br /><span><span class="cm-keyword">static</span> <span class="cm-variable-3">int</span> <span class="cm-def">gas_sensor_hw_suspend</span>(<span class="cm-keyword">const</span> <span class="cm-keyword">struct</span> <span class="cm-def">device</span> <span class="cm-operator">*</span><span class="cm-variable">dev</span>)</span><br /><span>{</span><br /><span>    <span class="cm-keyword">const</span> <span class="cm-keyword">struct</span> <span class="cm-def">gas_sensor_config</span> <span class="cm-operator">*</span><span class="cm-variable">cfg</span> <span class="cm-operator">=</span> <span class="cm-variable">dev</span><span class="cm-operator">-&gt;</span><span class="cm-variable">config</span>;</span><br /><span>    <span class="cm-keyword">struct</span> <span class="cm-def">gas_sensor_data</span> <span class="cm-operator">*</span><span class="cm-variable">data</span> <span class="cm-operator">=</span> <span class="cm-variable">dev</span><span class="cm-operator">-&gt;</span><span class="cm-variable">data</span>;</span><br /><span>    <span class="cm-variable-3">int</span> <span class="cm-variable">ret</span>;</span><br /><span>​</span><br /><span>    <span class="cm-comment">/* Disable the alert interrupt */</span></span><br /><span>    <span class="cm-variable">gpio_pin_interrupt_configure_dt</span>(<span class="cm-operator">&amp;</span><span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">alert_gpio</span>, <span class="cm-variable">GPIO_INT_DISABLED</span>);</span><br /><span>    <span class="cm-variable">gpio_remove_callback</span>(<span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">alert_gpio</span>.<span class="cm-variable">port</span>, <span class="cm-operator">&amp;</span><span class="cm-variable">data</span><span class="cm-operator">-&gt;</span><span class="cm-variable">alert_cb</span>);</span><br /><span>​</span><br /><span>    <span class="cm-comment">/* Put sensor into its internal low-power mode */</span></span><br /><span>    <span class="cm-variable">ret</span> <span class="cm-operator">=</span> <span class="cm-variable">pm_device_runtime_get</span>(<span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">bus</span>.<span class="cm-variable">bus</span>);</span><br /><span>    <span class="cm-keyword">if</span> (<span class="cm-variable">ret</span> <span class="cm-operator">&lt;</span> <span class="cm-number">0</span>) {</span><br /><span>        <span class="cm-keyword">return</span> <span class="cm-variable">ret</span>;</span><br /><span>    }</span><br /><span>​</span><br /><span>    <span class="cm-variable-3">uint8_t</span> <span class="cm-variable">sleep_cmd</span> <span class="cm-operator">=</span> <span class="cm-number">0x00</span>;</span><br /><span>    <span class="cm-variable">ret</span> <span class="cm-operator">=</span> <span class="cm-variable">i2c_write_dt</span>(<span class="cm-operator">&amp;</span><span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">bus</span>, <span class="cm-operator">&amp;</span><span class="cm-variable">sleep_cmd</span>, <span class="cm-number">1</span>);</span><br /><span>​</span><br /><span>    <span class="cm-variable">pm_device_runtime_put</span>(<span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">bus</span>.<span class="cm-variable">bus</span>);</span><br /><span>​</span><br /><span>    <span class="cm-keyword">return</span> <span class="cm-variable">ret</span>;</span><br /><span>}</span><br /><span>​</span><br /><span><span class="cm-keyword">static</span> <span class="cm-variable-3">int</span> <span class="cm-def">gas_sensor_hw_turn_on</span>(<span class="cm-keyword">const</span> <span class="cm-keyword">struct</span> <span class="cm-def">device</span> <span class="cm-operator">*</span><span class="cm-variable">dev</span>)</span><br /><span>{</span><br /><span>    <span class="cm-keyword">const</span> <span class="cm-keyword">struct</span> <span class="cm-def">gas_sensor_config</span> <span class="cm-operator">*</span><span class="cm-variable">cfg</span> <span class="cm-operator">=</span> <span class="cm-variable">dev</span><span class="cm-operator">-&gt;</span><span class="cm-variable">config</span>;</span><br /><span>​</span><br /><span>    <span class="cm-comment">/*</span></span><br /><span>     <span class="cm-comment">* Power domain is ON. Configure pins into a known state.</span></span><br /><span>     <span class="cm-comment">* The device starts suspended -- we just need safe pin states.</span></span><br /><span>     <span class="cm-comment">*/</span></span><br /><span>    <span class="cm-variable">gpio_pin_configure_dt</span>(<span class="cm-operator">&amp;</span><span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">alert_gpio</span>, <span class="cm-variable">GPIO_INPUT</span>);</span><br /><span>​</span><br /><span>    <span class="cm-keyword">return</span> <span class="cm-number">0</span>;</span><br /><span>}</span><br /><span>​</span><br /><span><span class="cm-keyword">static</span> <span class="cm-variable-3">int</span> <span class="cm-def">gas_sensor_hw_turn_off</span>(<span class="cm-keyword">const</span> <span class="cm-keyword">struct</span> <span class="cm-def">device</span> <span class="cm-operator">*</span><span class="cm-variable">dev</span>)</span><br /><span>{</span><br /><span>    <span class="cm-keyword">const</span> <span class="cm-keyword">struct</span> <span class="cm-def">gas_sensor_config</span> <span class="cm-operator">*</span><span class="cm-variable">cfg</span> <span class="cm-operator">=</span> <span class="cm-variable">dev</span><span class="cm-operator">-&gt;</span><span class="cm-variable">config</span>;</span><br /><span>​</span><br /><span>    <span class="cm-comment">/*</span></span><br /><span>     <span class="cm-comment">* Power domain is going OFF.</span></span><br /><span>     <span class="cm-comment">* Disconnect GPIOs to prevent backpowering the sensor</span></span><br /><span>     <span class="cm-comment">* through its alert pin when the rail is dead.</span></span><br /><span>     <span class="cm-comment">*/</span></span><br /><span>    <span class="cm-keyword">if</span> (<span class="cm-variable">gpio_pin_configure_dt</span>(<span class="cm-operator">&amp;</span><span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">alert_gpio</span>, <span class="cm-variable">GPIO_DISCONNECTED</span>)) {</span><br /><span>        <span class="cm-comment">/* Fallback if the GPIO driver doesn't support DISCONNECTED */</span></span><br /><span>        <span class="cm-variable">gpio_pin_configure_dt</span>(<span class="cm-operator">&amp;</span><span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">alert_gpio</span>, <span class="cm-variable">GPIO_INPUT</span>);</span><br /><span>    }</span><br /><span>​</span><br /><span>    <span class="cm-keyword">return</span> <span class="cm-number">0</span>;</span><br /><span>}</span><br /><span>​</span><br /><span><span class="cm-comment">/* ──────────────── PM action callback ──────────────── */</span></span><br /><span>​</span><br /><span><span class="cm-keyword">static</span> <span class="cm-variable-3">int</span> <span class="cm-def">gas_sensor_pm_action</span>(<span class="cm-keyword">const</span> <span class="cm-keyword">struct</span> <span class="cm-def">device</span> <span class="cm-operator">*</span><span class="cm-variable">dev</span>,</span><br /><span>                                <span class="cm-keyword">enum</span> <span class="cm-variable">pm_device_action</span> <span class="cm-variable">action</span>)</span><br /><span>{</span><br /><span>    <span class="cm-keyword">switch</span> (<span class="cm-variable">action</span>) {</span><br /><span>    <span class="cm-keyword">case</span> <span class="cm-variable">PM_DEVICE_ACTION_RESUME</span>:</span><br /><span>        <span class="cm-keyword">return</span> <span class="cm-variable">gas_sensor_hw_resume</span>(<span class="cm-variable">dev</span>);</span><br /><span>    <span class="cm-keyword">case</span> <span class="cm-variable">PM_DEVICE_ACTION_SUSPEND</span>:</span><br /><span>        <span class="cm-keyword">return</span> <span class="cm-variable">gas_sensor_hw_suspend</span>(<span class="cm-variable">dev</span>);</span><br /><span>    <span class="cm-keyword">case</span> <span class="cm-variable">PM_DEVICE_ACTION_TURN_ON</span>:</span><br /><span>        <span class="cm-keyword">return</span> <span class="cm-variable">gas_sensor_hw_turn_on</span>(<span class="cm-variable">dev</span>);</span><br /><span>    <span class="cm-keyword">case</span> <span class="cm-variable">PM_DEVICE_ACTION_TURN_OFF</span>:</span><br /><span>        <span class="cm-keyword">return</span> <span class="cm-variable">gas_sensor_hw_turn_off</span>(<span class="cm-variable">dev</span>);</span><br /><span>    <span class="cm-keyword">default</span>:</span><br /><span>        <span class="cm-keyword">return</span> <span class="cm-operator">-</span><span class="cm-variable">ENOTSUP</span>;</span><br /><span>    }</span><br /><span>}</span><br /><span>​</span><br /><span><span class="cm-comment">/* ──────────────── Public API ──────────────── */</span></span><br /><span>​</span><br /><span><span class="cm-keyword">static</span> <span class="cm-variable-3">int</span> <span class="cm-def">gas_sensor_read</span>(<span class="cm-keyword">const</span> <span class="cm-keyword">struct</span> <span class="cm-def">device</span> <span class="cm-operator">*</span><span class="cm-variable">dev</span>, <span class="cm-variable-3">uint16_t</span> <span class="cm-variable-3">*</span><span class="cm-variable">ppm</span>)</span><br /><span>{</span><br /><span>    <span class="cm-keyword">const</span> <span class="cm-keyword">struct</span> <span class="cm-def">gas_sensor_config</span> <span class="cm-operator">*</span><span class="cm-variable">cfg</span> <span class="cm-operator">=</span> <span class="cm-variable">dev</span><span class="cm-operator">-&gt;</span><span class="cm-variable">config</span>;</span><br /><span>    <span class="cm-keyword">struct</span> <span class="cm-def">gas_sensor_data</span> <span class="cm-operator">*</span><span class="cm-variable">data</span> <span class="cm-operator">=</span> <span class="cm-variable">dev</span><span class="cm-operator">-&gt;</span><span class="cm-variable">data</span>;</span><br /><span>    <span class="cm-variable-3">int</span> <span class="cm-variable">ret</span>;</span><br /><span>​</span><br /><span>    <span class="cm-comment">/* Activate the sensor (and implicitly, the bus + power domain) */</span></span><br /><span>    <span class="cm-variable">ret</span> <span class="cm-operator">=</span> <span class="cm-variable">pm_device_runtime_get</span>(<span class="cm-variable">dev</span>);</span><br /><span>    <span class="cm-keyword">if</span> (<span class="cm-variable">ret</span> <span class="cm-operator">&lt;</span> <span class="cm-number">0</span>) {</span><br /><span>        <span class="cm-keyword">return</span> <span class="cm-variable">ret</span>;</span><br /><span>    }</span><br /><span>​</span><br /><span>    <span class="cm-comment">/* Now the device is ACTIVE -- perform the read */</span></span><br /><span>    <span class="cm-variable">ret</span> <span class="cm-operator">=</span> <span class="cm-variable">pm_device_runtime_get</span>(<span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">bus</span>.<span class="cm-variable">bus</span>);</span><br /><span>    <span class="cm-keyword">if</span> (<span class="cm-variable">ret</span> <span class="cm-operator">&lt;</span> <span class="cm-number">0</span>) {</span><br /><span>        <span class="cm-keyword">goto</span> <span class="cm-variable">out</span>;</span><br /><span>    }</span><br /><span>​</span><br /><span>    <span class="cm-variable">ret</span> <span class="cm-operator">=</span> <span class="cm-variable">i2c_burst_read_dt</span>(<span class="cm-operator">&amp;</span><span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">bus</span>, <span class="cm-number">0x10</span>, (<span class="cm-variable-3">uint8_t</span> <span class="cm-variable-3">*</span>)<span class="cm-variable">ppm</span>, <span class="cm-keyword">sizeof</span>(<span class="cm-operator">*</span><span class="cm-variable">ppm</span>));</span><br /><span>​</span><br /><span>    <span class="cm-variable">pm_device_runtime_put</span>(<span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">bus</span>.<span class="cm-variable">bus</span>);</span><br /><span>​</span><br /><span><span class="cm-variable">out</span>:</span><br /><span>    <span class="cm-comment">/* Release. If no other users, sensor suspends, domain may power off. */</span></span><br /><span>    <span class="cm-variable">pm_device_runtime_put</span>(<span class="cm-variable">dev</span>);</span><br /><span>    <span class="cm-keyword">return</span> <span class="cm-variable">ret</span>;</span><br /><span>}</span><br /><span>​</span><br /><span><span class="cm-comment">/* ──────────────── Init &amp; instantiation ──────────────── */</span></span><br /><span>​</span><br /><span><span class="cm-keyword">static</span> <span class="cm-variable-3">int</span> <span class="cm-def">gas_sensor_init</span>(<span class="cm-keyword">const</span> <span class="cm-keyword">struct</span> <span class="cm-def">device</span> <span class="cm-operator">*</span><span class="cm-variable">dev</span>)</span><br /><span>{</span><br /><span>    <span class="cm-keyword">const</span> <span class="cm-keyword">struct</span> <span class="cm-def">gas_sensor_config</span> <span class="cm-operator">*</span><span class="cm-variable">cfg</span> <span class="cm-operator">=</span> <span class="cm-variable">dev</span><span class="cm-operator">-&gt;</span><span class="cm-variable">config</span>;</span><br /><span>    <span class="cm-keyword">struct</span> <span class="cm-def">gas_sensor_data</span> <span class="cm-operator">*</span><span class="cm-variable">data</span> <span class="cm-operator">=</span> <span class="cm-variable">dev</span><span class="cm-operator">-&gt;</span><span class="cm-variable">data</span>;</span><br /><span>​</span><br /><span>    <span class="cm-keyword">if</span> (<span class="cm-operator">!</span><span class="cm-variable">i2c_is_ready_dt</span>(<span class="cm-operator">&amp;</span><span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">bus</span>)) {</span><br /><span>        <span class="cm-keyword">return</span> <span class="cm-operator">-</span><span class="cm-variable">ENODEV</span>;</span><br /><span>    }</span><br /><span>​</span><br /><span>    <span class="cm-keyword">if</span> (<span class="cm-operator">!</span><span class="cm-variable">gpio_is_ready_dt</span>(<span class="cm-operator">&amp;</span><span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">alert_gpio</span>)) {</span><br /><span>        <span class="cm-keyword">return</span> <span class="cm-operator">-</span><span class="cm-variable">ENODEV</span>;</span><br /><span>    }</span><br /><span>​</span><br /><span>    <span class="cm-variable">gpio_init_callback</span>(<span class="cm-operator">&amp;</span><span class="cm-variable">data</span><span class="cm-operator">-&gt;</span><span class="cm-variable">alert_cb</span>, <span class="cm-variable">gas_sensor_alert_handler</span>,</span><br /><span>                       <span class="cm-variable">BIT</span>(<span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">alert_gpio</span>.<span class="cm-variable">pin</span>));</span><br /><span>​</span><br /><span>    <span class="cm-comment">/*</span></span><br /><span>     <span class="cm-comment">* MUST be the last call in init.</span></span><br /><span>     <span class="cm-comment">* This initializes the PM context and puts the device into the</span></span><br /><span>     <span class="cm-comment">* correct initial state via the action callback (TURN_ON → RESUME).</span></span><br /><span>     <span class="cm-comment">*/</span></span><br /><span>    <span class="cm-keyword">return</span> <span class="cm-variable">pm_device_driver_init</span>(<span class="cm-variable">dev</span>, <span class="cm-variable">gas_sensor_pm_action</span>);</span><br /><span>}</span><br /><span>​</span><br /><span><span class="cm-comment">/* Define the PM context for instance 0 */</span></span><br /><span><span class="cm-variable">PM_DEVICE_DT_INST_DEFINE</span>(<span class="cm-number">0</span>, <span class="cm-variable">gas_sensor_pm_action</span>);</span><br /><span>​</span><br /><span><span class="cm-variable">DEVICE_DT_INST_DEFINE</span>(</span><br /><span>    <span class="cm-number">0</span>,</span><br /><span>    <span class="cm-variable">gas_sensor_init</span>,     <span class="cm-comment">/* init */</span></span><br /><span>    <span class="cm-variable">PM_DEVICE_DT_INST_GET</span>(<span class="cm-number">0</span>),  <span class="cm-comment">/* pm */</span></span><br /><span>    <span class="cm-operator">&amp;</span><span class="cm-variable">gas_sensor_data_0</span>,  <span class="cm-comment">/* data */</span></span><br /><span>    <span class="cm-operator">&amp;</span><span class="cm-variable">gas_sensor_cfg_0</span>,   <span class="cm-comment">/* config */</span></span><br /><span>    <span class="cm-variable">POST_KERNEL</span>,</span><br /><span>    <span class="cm-variable">CONFIG_SENSOR_INIT_PRIORITY</span>,</span><br /><span>    <span class="cm-operator">&amp;</span><span class="cm-variable">gas_sensor_api</span></span><br /><span>);</span></pre>
<div class="md-hr md-end-block"><hr /></div>
<h2><span class="md-plain">Practical Tips &amp; Gotchas</span></h2>
<h3><span class="md-plain">1. The boot-time chattering problem</span></h3>
<p class="md-end-block md-p"><span class="md-plain">When runtime PM is enabled, each device's init function may briefly </span><span class="md-pair-s"><code>get</code></span><span class="md-plain"> and </span><span class="md-pair-s"><code>put</code></span><span class="md-plain"> the power domain. With 10 devices on a domain, this can toggle the physical power rail 10+ times during boot. Solution: hold the domain active during init:</span></p>
<pre contenteditable="false"><span><span class="cm-meta">#include &lt;zephyr/init.h&gt;</span></span><br /><span><span class="cm-meta">#include &lt;zephyr/pm/device_runtime.h&gt;</span></span><br /><span>​</span><br /><span><span class="cm-keyword">static</span> <span class="cm-keyword">const</span> <span class="cm-keyword">struct</span> <span class="cm-def">device</span> <span class="cm-operator">*</span><span class="cm-variable">domain</span> <span class="cm-operator">=</span> <span class="cm-variable">DEVICE_DT_GET</span>(<span class="cm-variable">DT_NODELABEL</span>(<span class="cm-variable">sensor_rail</span>));</span><br /><span>​</span><br /><span><span class="cm-keyword">static</span> <span class="cm-variable-3">int</span> <span class="cm-def">hold_domain_during_boot</span>(<span class="cm-variable-3">void</span>)</span><br /><span>{</span><br /><span>    <span class="cm-comment">/* Keep the rail on through the entire POST_KERNEL init phase */</span></span><br /><span>    <span class="cm-keyword">return</span> <span class="cm-variable">pm_device_runtime_get</span>(<span class="cm-variable">domain</span>);</span><br /><span>}</span><br /><span>​</span><br /><span><span class="cm-keyword">static</span> <span class="cm-variable-3">int</span> <span class="cm-def">release_domain_after_boot</span>(<span class="cm-variable-3">void</span>)</span><br /><span>{</span><br /><span>    <span class="cm-comment">/* All drivers are initialized -- let the domain idle */</span></span><br /><span>    <span class="cm-keyword">return</span> <span class="cm-variable">pm_device_runtime_put</span>(<span class="cm-variable">domain</span>);</span><br /><span>}</span><br /><span>​</span><br /><span><span class="cm-variable">SYS_INIT</span>(<span class="cm-variable">hold_domain_during_boot</span>, <span class="cm-variable">POST_KERNEL</span>, <span class="cm-number">0</span>);</span><br /><span><span class="cm-variable">SYS_INIT</span>(<span class="cm-variable">release_domain_after_boot</span>, <span class="cm-variable">APPLICATION</span>, <span class="cm-number">99</span>);</span></pre>
<h3><span class="md-plain">2. Always guard </span><span class="md-pair-s"><code>TURN_OFF</code></span><span class="md-plain"> against backpowering</span></h3>
<p class="md-end-block md-p"><span class="md-plain">When a power rail goes down, any GPIO pin driving HIGH into a de-powered device will backpower it through its ESD diodes. Always disconnect or tri-state your outputs in </span><span class="md-pair-s"><code>TURN_OFF</code></span><span class="md-plain">:</span></p>
<pre contenteditable="false"><span><span class="cm-keyword">case</span> <span class="cm-variable">PM_DEVICE_ACTION_TURN_OFF</span>:</span><br /><span>    <span class="cm-comment">/* Prefer GPIO_DISCONNECTED; fall back to GPIO_INPUT */</span></span><br /><span>    <span class="cm-keyword">if</span> (<span class="cm-variable">gpio_pin_configure_dt</span>(<span class="cm-operator">&amp;</span><span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">cs_gpio</span>, <span class="cm-variable">GPIO_DISCONNECTED</span>) <span class="cm-operator">!=</span> <span class="cm-number">0</span>) {</span><br /><span>        <span class="cm-variable">gpio_pin_configure_dt</span>(<span class="cm-operator">&amp;</span><span class="cm-variable">cfg</span><span class="cm-operator">-&gt;</span><span class="cm-variable">cs_gpio</span>, <span class="cm-variable">GPIO_INPUT</span>);</span><br /><span>    }</span><br /><span>    <span class="cm-keyword">return</span> <span class="cm-number">0</span>;</span></pre>
<h3><span class="md-plain">3. Use </span><span class="md-pair-s"><code>k_can_yield()</code></span><span class="md-plain"> guard in PM callbacks</span></h3>
<p class="md-end-block md-p"><span class="md-plain">If system-managed device PM is also enabled, your PM callback might be invoked from the idle thread, which cannot block. Guard blocking operations:</span></p>
<pre contenteditable="false"><span><span class="cm-keyword">static</span> <span class="cm-variable-3">int</span> <span class="cm-def">my_pm_action</span>(<span class="cm-keyword">const</span> <span class="cm-keyword">struct</span> <span class="cm-def">device</span> <span class="cm-operator">*</span><span class="cm-variable">dev</span>, <span class="cm-keyword">enum</span> <span class="cm-variable">pm_device_action</span> <span class="cm-variable">action</span>)</span><br /><span>{</span><br /><span>    <span class="cm-keyword">if</span> (<span class="cm-operator">!</span><span class="cm-variable">k_can_yield</span>()) {</span><br /><span>        <span class="cm-keyword">return</span> <span class="cm-operator">-</span><span class="cm-variable">ENOTSUP</span>;</span><br /><span>    }</span><br /><span>​</span><br /><span>    <span class="cm-comment">/* Safe to do blocking I2C/SPI operations here */</span></span><br /><span>    <span class="cm-comment">/* ... */</span></span><br /><span>}</span></pre>
<h3><span class="md-plain">4. Async put for bursty workloads</span></h3>
<p class="md-end-block md-p"><span class="md-plain">If your device does many short transactions with brief gaps, synchronous </span><span class="md-pair-s"><code>put</code></span><span class="md-plain">/</span><span class="md-pair-s"><code>get</code></span><span class="md-plain"> on every transaction is wasteful. Use </span><span class="md-pair-s"><code>pm_device_runtime_put_async()</code></span><span class="md-plain"> with a delay to keep the device active through bursts:</span></p>
<pre contenteditable="false"><span><span class="cm-comment">/* Keep device active for 50ms after the last transaction, then suspend */</span></span><br /><span><span class="cm-variable">pm_device_runtime_put_async</span>(<span class="cm-variable">dev</span>, <span class="cm-variable">K_MSEC</span>(<span class="cm-number">50</span>));</span></pre>
<h3><span class="md-plain">5. Shell debugging is your best friend</span></h3>
<pre contenteditable="false"><span>uart:~$ device list</span><br /><span>devices:</span><br /><span>- sensor-power-rail (active, usage=2)</span><br /><span>- i2c@40003000 (active, usage=1)</span><br /><span>- accel@19 (active, usage=1)</span><br /><span>- temp_sensor@48 (suspended, usage=0)</span><br /><span>- radio@0 (suspended, usage=0)</span></pre>
<p class="md-end-block md-p"><span class="md-plain">The </span><span class="md-pair-s"><code>usage=N</code></span><span class="md-plain"> count tells you exactly how many references are held. If a device is stuck </span><span class="md-pair-s"><code>active</code></span><span class="md-plain"> with </span><span class="md-pair-s"><code>usage=1</code></span><span class="md-plain"> when you expect </span><span class="md-pair-s"><code>0</code></span><span class="md-plain">, you have a reference leak in a driver.</span></p>
<div class="md-hr md-end-block"><hr /></div>
<h2><span class="md-plain">Summary</span></h2>
<p class="md-end-block md-p"><span class="md-plain">The full setup checklist:</span></p>
<ol class="ol-list" start="">
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">Kconfig</span></strong></span><span class="md-plain">: Enable </span><span class="md-pair-s"><code>PM_DEVICE</code></span><span class="md-plain">, </span><span class="md-pair-s"><code>PM_DEVICE_RUNTIME</code></span><span class="md-plain">, and optionally </span><span class="md-pair-s"><code>PM_DEVICE_POWER_DOMAIN</code></span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">DTS</span></strong></span><span class="md-plain">: Define your power domain node, add </span><span class="md-pair-s"><code>power-domains = &lt;&amp;domain&gt;</code></span><span class="md-plain"> to child devices, use </span><span class="md-pair-s"><code>zephyr,pm-device-runtime-auto</code></span><span class="md-plain"> where appropriate</span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">Driver</span></strong></span><span class="md-plain">: Implement a PM action callback handling all four actions (</span><span class="md-pair-s"><code>RESUME</code></span><span class="md-plain">, </span><span class="md-pair-s"><code>SUSPEND</code></span><span class="md-plain">, </span><span class="md-pair-s"><code>TURN_ON</code></span><span class="md-plain">, </span><span class="md-pair-s"><code>TURN_OFF</code></span><span class="md-plain">)</span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">Driver init</span></strong></span><span class="md-plain">: End with </span><span class="md-pair-s"><code>pm_device_driver_init(dev, my_pm_action)</code></span><span class="md-plain"> and instantiate with </span><span class="md-pair-s"><code>PM_DEVICE_DT_INST_DEFINE</code></span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">Consumers</span></strong></span><span class="md-plain">: Bracket every hardware access with </span><span class="md-pair-s"><code>pm_device_runtime_get()</code></span><span class="md-plain">/</span><span class="md-pair-s"><code>pm_device_runtime_put()</code></span></p>
</li>
</ol>
<p class="md-end-block md-p md-focus"><span class="md-plain">The device runtime PM model with power domains is one of Zephyr's strongest features. It takes some effort to set up correctly, but once it's in place, your system only burns power on peripherals that are actually doing work - and that's the whole game in battery-powered products.</span></p>]]></content:encoded>
						                            <category domain="https://edgemtech.ch/community/"></category>                        <dc:creator>Gabriel CATEL TORRES</dc:creator>
                        <guid isPermaLink="true">https://edgemtech.ch/community/general-discussions/zephyr-device-power-management/</guid>
                    </item>
				                    <item>
                        <title>Yocto: The initramfs-framework</title>
                        <link>https://edgemtech.ch/community/general-discussions/yocto-the-initramfs-framework/</link>
                        <pubDate>Sun, 29 Mar 2026 08:48:27 +0000</pubDate>
                        <description><![CDATA[Introduction
When building embedded Linux systems with Yocto, there are situations where you need to run some code very early in the boot process, before the root filesystem is even mounted...]]></description>
                        <content:encoded><![CDATA[<h2>Introduction</h2>
<p class="font-claude-response-body break-words whitespace-normal leading-">When building embedded Linux systems with Yocto, there are situations where you need to run some code very early in the boot process, before the root filesystem is even mounted. This is exactly what the <strong>initramfs</strong> (initial RAM filesystem) is for: a temporary root filesystem loaded into memory by the bootloader, used to prepare the environment for the real root mount. Examples can be decrypting a partition, mounting a special device and so on.</p>
<p class="font-claude-response-body break-words whitespace-normal leading-">Yocto provides a useful framework to build and integrate an initramfs image directly into your distribution. In this post, we'll cover:</p>
<ul class=":mb-0 :mt-1 :gap-1 :pb-1 :pb-1 list-disc flex flex-col gap-1 pl-8 mb-3">
<li class="whitespace-normal break-words pl-2">How the initramfs framework works in Yocto</li>
<li class="whitespace-normal break-words pl-2">How to set it up in your layer</li>
<li class="whitespace-normal break-words pl-2">How to add a hook with a custom init script</li>
</ul>
<p class="font-claude-response-body break-words whitespace-normal leading-">Let's get into it.</p>
<h2> </h2>
<h2>Setup</h2>
<p class="font-claude-response-body break-words whitespace-normal leading-">The initramfs support in Yocto is built around a dedicated image recipe, typically based on <strong>core-image-minimal-initramfs</strong>, and a set of <strong>packagegroup</strong> recipes that bundle the necessary tools.</p>
<h3>Enabling the initramfs image</h3>
<p class="font-claude-response-body break-words whitespace-normal leading-">In your <strong>local.conf</strong> or distro configuration, add the following:</p>
<div class="relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100" role="group" aria-label="Code bash">
<div class="overflow-x-auto">
<pre contenteditable="false"><code class="language-bash"><span>INITRAMFS_IMAGE <span class="token token">=</span> <span class="token token">"core-image-minimal-initramfs"</span>
</span><span>INITRAMFS_IMAGE_BUNDLE <span class="token token">=</span> <span class="token token">"1"</span></span></code></pre>
</div>
</div>
<p class="font-claude-response-body break-words whitespace-normal leading-"><strong>INITRAMFS_IMAGE</strong> tells Yocto which image recipe to use as the initramfs. Setting <strong>INITRAMFS_IMAGE_BUNDLE</strong> to <strong>"1"</strong> instructs the kernel build to bundle the initramfs cpio archive directly into the kernel image, so the bootloader only needs to load a single binary.</p>
<h3>The initramfs image recipe</h3>
<p class="font-claude-response-body break-words whitespace-normal leading-">The default <strong>core-image-minimal-initramfs</strong> recipe lives in <strong>meta/recipes-core/images/</strong>. It inherits <strong>core-image</strong> and pulls in a minimal package group:</p>
<div class="relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100" role="group" aria-label="Code bash">
<div class="overflow-x-auto">
<pre contenteditable="false"><code class="language-bash"><span class="token token"><span>PACKAGE_INSTALL </span>=<span> </span>"\
</span><span class="token token">    initramfs-framework \
</span><span class="token token">    base-passwd \
</span><span class="token token">    ${VIRTUAL-RUNTIME_base-utils} \
</span><span class="token token">"</span></code></pre>
</div>
</div>
<p class="font-claude-response-body break-words whitespace-normal leading-">The key package here is <strong>initramfs-framework</strong>, which provides the modular init infrastructure we'll hook into.</p>
<h3>The initramfs-framework package</h3>
<p class="font-claude-response-body break-words whitespace-normal leading-"><strong>initramfs-framework</strong> is a shell-based, modular init system designed specifically for Yocto initramfs images. It lives in <strong>meta/recipes-core/initrdscripts/</strong> and provides:</p>
<ul class=":mb-0 :mt-1 :gap-1 :pb-1 :pb-1 list-disc flex flex-col gap-1 pl-8 mb-3">
<li class="whitespace-normal break-words pl-2">A main <strong>/init</strong> script that sources modules in order</li>
<li class="whitespace-normal break-words pl-2">A set of built-in modules (e.g. <strong>udev</strong>, <strong>rootfs</strong>, <strong>nfsrootfs</strong>, ...)</li>
<li class="whitespace-normal break-words pl-2">A clean hook mechanism to add your own modules</li>
</ul>
<p class="font-claude-response-body break-words whitespace-normal leading-">Modules are shell scripts placed in <strong>/init.d/</strong> inside the initramfs, named with a numeric prefix to control execution order (e.g. <strong>00-debug</strong>, <strong>10-overlayroot</strong>).</p>
<h2> </h2>
<h2>Adding a Hook with a Custom Init Script</h2>
<p class="font-claude-response-body break-words whitespace-normal leading-">Now the interesting part. Say you need to run a custom script early in boot — for instance, to configure a network interface, check hardware, or set up an overlay. Here is how to do it cleanly using the <strong>initramfs-framework</strong> hook system.</p>
<h3>1. Write the module script</h3>
<p class="font-claude-response-body break-words whitespace-normal leading-">Create a shell script following the <strong>initramfs-framework</strong> module convention. Each module must define two functions:</p>
<ul class=":mb-0 :mt-1 :gap-1 :pb-1 :pb-1 list-disc flex flex-col gap-1 pl-8 mb-3">
<li class="whitespace-normal break-words pl-2"><strong>&lt;module&gt;_enabled</strong>: Determine if your module should run. Returning <strong>0</strong> means your module should run, returning <strong>1</strong> means it is deactivated.</li>
<li class="whitespace-normal break-words pl-2"><strong>&lt;module&gt;_run</strong>: Your custom module logic</li>
</ul>
<div class="relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100" role="group" aria-label="Code bash">
<div class="sticky opacity-0 group-hover/copy:opacity-100 group-focus-within/copy:opacity-100 top-2 py-2 h-12 w-0 float-right">
<div class="absolute right-0 h-8 px-2 items-center inline-flex z-10">
<div class="relative"> </div>
</div>
</div>
<div class="overflow-x-auto">
<pre contenteditable="false"><code class="language-bash"><span><span class="token token shebang">#!/bin/sh</span>
</span><span><span class="token token"># modules/80-myhook</span>
</span><span>
</span><span><span class="token token">myhook_enabled</span><span class="token token">(</span><span class="token token">)</span> <span class="token token">{</span>
</span><span>    <span class="token token">return</span> <span class="token token">0</span>
</span><span><span class="token token">}</span>
</span><span>
</span><span><span class="token token">myhook_run</span><span class="token token">(</span><span class="token token">)</span> <span class="token token">{</span>
</span><span>    msg <span class="token token">"Running my custom initramfs hook..."</span>
</span><span>
</span><span>    <span class="token token"># Your logic here — e.g. load a kernel module, set up a mount, etc.</span>
</span><span>    modprobe mydriver
</span><span>
</span><span>    msg <span class="token token">"Custom hook done."</span>
</span><span><span class="token token">}</span></span></code></pre>
</div>
</div>
<p class="font-claude-response-body break-words whitespace-normal leading-">The <strong>msg</strong> helper is provided by <strong>initramfs-framework</strong> and writes to the console. It also offers some other logging wrappers such as <strong>fatal</strong> and <strong>warn</strong>.</p>
<h3>2. Create a bbappend or new recipe</h3>
<p class="font-claude-response-body break-words whitespace-normal leading-">The cleanest way to integrate this into Yocto is to create a recipe in your custom layer that installs the module into the initramfs image.</p>
<p class="font-claude-response-body break-words whitespace-normal leading-">Create a recipe file, e.g. <strong>recipes-core/initrdscripts/initramfs-myhook_1.0.bb</strong>:</p>
<div class="relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100" role="group" aria-label="Code bash">
<div class="overflow-x-auto">
<pre contenteditable="false"><code class="language-bash"><span>SUMMARY <span class="token token">=</span> <span class="token token">"Custom initramfs hook for my project"</span>
</span><span>LICENSE <span class="token token">=</span> <span class="token token">"MIT"</span>
</span><span>LIC_FILES_CHKSUM <span class="token token">=</span> <span class="token token">"file://</span><span class="token token">${COMMON_LICENSE_DIR}</span><span class="token token">/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"</span>
</span><span>
</span><span>SRC_URI <span class="token token">=</span> <span class="token token">"file://80-myhook"</span>
</span><span>
</span><span>S <span class="token token">=</span> <span class="token token">"</span><span class="token token">${WORKDIR}</span><span class="token token">"</span>
</span><span>
</span><span><span class="token token">do_install</span><span class="token token">(</span><span class="token token">)</span> <span class="token token">{</span>
</span><span>    <span class="token token">install</span> -d <span class="token token">${D}</span>/init.d
</span><span>    <span class="token token">install</span> -m 0755 <span class="token token">${WORKDIR}</span>/80-myhook <span class="token token">${D}</span>/init.d/80-myhook
</span><span><span class="token token">}</span>
</span><span>
</span><span>FILES:<span class="token token">${PN}</span> <span class="token token">=</span> <span class="token token">"/init.d/80-myhook"</span></span></code></pre>
</div>
</div>
<p class="font-claude-response-body break-words whitespace-normal leading-">Place your <strong>80-myhook</strong> script under <strong>recipes-core/initrdscripts/files/80-myhook</strong>.</p>
<h3>3. Add the package to your initramfs image</h3>
<p class="font-claude-response-body break-words whitespace-normal leading-">Either create a <strong>core-image-minimal-initramfs.bbappend</strong> in your layer, or create a custom initramfs image recipe. With the bbappend approach:</p>
<div class="relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100" role="group" aria-label="Code bash">
<div class="overflow-x-auto">
<pre contenteditable="false"><code class="language-bash"><span><span class="token token"># recipes-core/images/core-image-minimal-initramfs.bbappend</span>
</span><span>
</span><span>PACKAGE_INSTALL:append <span class="token token">=</span> <span class="token token">" initramfs-myhook"</span></span></code></pre>
</div>
</div>
<p class="font-claude-response-body break-words whitespace-normal leading-">And just like that, the <strong>minimal-initramfs</strong> is extended with your init script. A custom recipe would allow more customization on <strong>core-image-minimal-initramfs</strong>, but for this use case, extending the existing recipe is enough.</p>
<h3>4. Build</h3>
<p class="font-claude-response-body break-words whitespace-normal leading-">Trigger the build as usual:</p>
<div class="relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100" role="group" aria-label="Code bash">
<div class="overflow-x-auto">
<pre contenteditable="false"><code class="language-bash"><span>bitbake core-image-minimal-initramfs</span></code></pre>
</div>
</div>
<h2> </h2>
<h2>Conclusion</h2>
<p class="font-claude-response-body break-words whitespace-normal leading-">The <strong>initramfs-framework</strong> in Yocto provides a solid, modular foundation for early boot customization. Rather than hacking a monolithic <strong>init</strong> script, you can slot in well-isolated modules that are easy to maintain and test independently. The pattern is always the same: write a module script, package it in a recipe, add it to the initramfs image and Yocto takes care of the rest.</p>
<p class="font-claude-response-body break-words whitespace-normal leading-">This approach scales well as your project grows: need to add an <strong>overlayfs</strong> setup? A hardware self-test? An early network configuration? Each one becomes its own module with a clear numeric priority, living cleanly in your layer. It also bases itself on a well tested, actively maintained base.</p>]]></content:encoded>
						                            <category domain="https://edgemtech.ch/community/"></category>                        <dc:creator>David Truan</dc:creator>
                        <guid isPermaLink="true">https://edgemtech.ch/community/general-discussions/yocto-the-initramfs-framework/</guid>
                    </item>
				                    <item>
                        <title>EDGEMTech and HEIG-VD Build an Open-Source Linux Virtualization Stack for Embedded LVGL-Based HMIs.</title>
                        <link>https://edgemtech.ch/community/orchestration-and-services/edgemtech-and-heig-vd-build-an-open-source-linux-virtualization-stack-for-portable-lvgl-based-hmis/</link>
                        <pubDate>Sun, 22 Mar 2026 14:19:50 +0000</pubDate>
                        <description><![CDATA[A joint collaboration in Yverdon-les-Bains on Linux, AVZ virtualization, SO3 capsules, and safety-oriented LVGL user interfaces.




EDGEMTech and HEIG-VD are jointly developing an open...]]></description>
                        <content:encoded><![CDATA[<table>
<tbody>
<tr>
<td width="624">
<p><span> <em>A joint collaboration in Yverdon-les-Bains on Linux, AVZ virtualization, SO3 capsules, and safety-oriented LVGL user interfaces.</em></span></p>
</td>
</tr>
</tbody>
</table>
<p><strong>EDGEMTech</strong><span> and <strong>HEIG-VD</strong> are jointly developing an <strong>open-source embedded software environment</strong> that combines <strong>Linux</strong>, the <strong>AVZ virtualization layer</strong>, and <strong>SO3 capsules</strong> to host modular graphical applications. The objective is to provide a robust execution platform for embedded products where hardware access, application isolation, portability, and long-term maintainability must coexist without compromising user-interface richness.</span></p>
<table>
<tbody>
<tr>
<td width="627">
<p><strong>Why this architecture matters</strong></p>
<p><strong>• </strong>Linux retains the critical services and direct hardware access.</p>
<p><strong>• </strong>Portainer can remotely orchestrate multiple SO3 capsules through a SO3-enabled agent running in the Linux domain.</p>
<p><strong>• </strong>Each capsule can host a dedicated LVGL-based application, enabling clean separation between product modes or customer-specific HMIs.</p>
<p><strong>• </strong>A split graphics architecture isolates application logic from low-level rendering details, improving portability across platforms and display stacks.</p>
<p><strong>• </strong>LVGL Safe can be combined with capsule isolation for safety-oriented HMI designs.</p>
</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p><img style="float: left;margin-left: auto;margin-right: auto" src="https://edgemtech.ch/wp-content/uploads/wpforo/default_attachments/1774191600-20260322_MICOFE_LVGL.png" /></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><strong>Figure 1. </strong><em>High-level view of the software stack: Portainer-driven orchestration in Linux, multiple SO3 capsules hosting LVGL applications, and display front-end/back-end separation over the AVZ hypervisor.</em></p>
<ol>
<li><strong> Architecture overview</strong></li>
</ol>
<p><span>At the bottom of the stack, the AVZ hypervisor provides the virtualization layer used to partition the execution environment. Above it, the Linux domain remains the system anchor point: it hosts the services that need direct access to the physical hardware, keeps ownership of the low-level graphics drivers, and exposes the execution and orchestration services required to manage isolated application domains.</span></p>
<p><span>On top of this base, SO3 capsules can be instantiated as strongly isolated execution contexts dedicated to user-interface workloads. The diagram illustrates capsules hosting graphical applications written in C, C++, micro-Python and soon Rust, but the same model can be extended to other language bindings when relevant. Each capsule packages its own application logic together with the LVGL user-interface stack and the required runtime components.</span></p>
<ol start="2">
<li><strong> Portainer orchestration and capsule lifecycle</strong></li>
</ol>
<p><span>A key part of the approach is the integration of Portainer for orchestration. A Portainer manager communicates with a SO3-enabled agent running in the Linux domain. This agent acts as the control bridge between the orchestration layer and the embedded virtualization environment.</span></p>
<p><span>From an operational standpoint, this makes it possible to deploy, update, stop, and supervise several capsules on the same target. Instead of embedding all graphical functions inside a single monolithic application, different LVGL-based apps can be packaged into separate capsules and managed independently. This is especially attractive for products that need distinct operating modes, customer-specific HMIs, service interfaces, or staged feature rollouts.</span></p>
<p><span>The same mechanism also opens the door to switching between capsules according to the active application context. In practice, one capsule can implement one HMI profile while another capsule implements a different one, with the orchestrator selecting which capsule is currently active or visible. This preserves a clean separation between applications while simplifying update strategies and fault containment.</span></p>
<ol start="3">
<li><strong> Split graphics architecture and LVGL portability</strong></li>
</ol>
<p><span>One of the most interesting technical aspects of the architecture is the split between user-interface logic and graphics implementation. The LVGL application and its UI logic run inside the capsule, while the hardware-facing graphics components remain anchored in the Linux domain.</span></p>
<p><span>In the diagram, this split appears through the distinction between the display front-end driver on the capsule side and the display back-end driver on the Linux side, the latter itself relying on the native graphics stack and drivers such as DRM, framebuffer, or other platform-specific implementations. This separation keeps the application layer focused on UI behavior, widgets, layouts, and event handling, while the rendering and display integration can be adapted independently to the target hardware.</span></p>
<p><span>This design greatly improves portability. An LVGL application can be kept largely unchanged while the graphics back-end is retargeted to another SoC, another display controller, or another rendering path. The same UI logic can therefore be reused across product families with fewer modifications in the high-level application code. In other words, portability is obtained not by freezing the whole stack, but by isolating the platform-dependent layers where they belong.</span></p>
<ol start="4">
<li><strong> LVGL Pro workflow and LVGL Safe perspective</strong></li>
</ol>
<p><span>The workflow can also benefit from the LVGL Pro editor, which provides a practical way to design the user interface and generate the corresponding target code. In this architecture, the generated application can then be packaged into a capsule and deployed through the orchestration layer, preserving a consistent pipeline from UI design to embedded deployment.</span></p>
<p><span>The safety-oriented dimension is equally important. LVGL Safe is highly relevant in this context because it complements the architectural isolation already provided by capsules and virtualization. When a UI is placed inside a strongly partitioned execution environment, and when its graphics path is explicitly controlled through front-end/back-end separation, LVGL Safe becomes an especially compelling candidate for applications that have stronger safety, robustness, or certification-driven requirements.</span></p>
<p><span>The combination of LVGL Safe and SO3 capsules therefore offers a promising route for highly critical environments in which a graphical interface is still required, but where fault containment, software partitioning, controlled updates, and predictable system behavior are mandatory design constraints.</span></p>
<ol start="5">
<li><strong> Open-source engineering collaboration</strong></li>
</ol>
<p><span>Beyond the technical stack itself, this project also reflects the value of a close collaboration between industrial engineering and applied research. EDGEMTech brings product-oriented embedded software expertise, customer-facing constraints, and deployment pragmatism, while HEIG-VD contributes engineering depth, experimentation capacity, and a strong academic environment to structure and validate the approach.</span></p>
<p><span>Together, the goal is to consolidate a reusable open-source foundation for Linux-based virtualized embedded systems, capable of hosting advanced and portable HMIs with a clean separation of concerns between orchestration, application logic, and hardware-specific graphics integration.</span></p>
<p><span>We are currently preparing several showcases based on Raspberry Pi 4 platforms to illustrate this architecture in practice. More demonstrations and technical details will be shared soon.</span></p>
<p><em>This article is intended as a technical introduction to the ongoing collaboration. More implementation details, demonstrations, and feedback from field deployments will be shared as the project evolves.</em></p>
<p><em> </em></p>
<p><span> </span></p>
<div id="wpfa-2784" class="wpforo-attached-file"><a class="wpforo-default-attachment" title="20260322_MICOFE_LVGL.png" href="//edgemtech.ch/wp-content/uploads/wpforo/default_attachments/1774191600-20260322_MICOFE_LVGL.png" target="_blank" rel="noopener"><i class="fas fa-paperclip"></i> 20260322_MICOFE_LVGL.png</a></div>]]></content:encoded>
						                            <category domain="https://edgemtech.ch/community/"></category>                        <dc:creator>Daniel Rossier</dc:creator>
                        <guid isPermaLink="true">https://edgemtech.ch/community/orchestration-and-services/edgemtech-and-heig-vd-build-an-open-source-linux-virtualization-stack-for-portable-lvgl-based-hmis/</guid>
                    </item>
				                    <item>
                        <title>RTTarget-32</title>
                        <link>https://edgemtech.ch/community/realtime-systems/rttarget-32/</link>
                        <pubDate>Sun, 15 Mar 2026 12:03:21 +0000</pubDate>
                        <description><![CDATA[Recently, we&#039;ve been contracted to add support for the Intel Atom X6000E platform to an RTOS called RTTargetthe company that provided RTTarget, On-Time Informatik GmbH has ceased operations ...]]></description>
                        <content:encoded><![CDATA[<p>Recently, we've been contracted to add support for the Intel Atom X6000E platform to an RTOS called RTTarget <a href="http://www.on-time.com/rttarget-32.htm" target="_blank" rel="noopener">http://www.on-time.com/rttarget-32.htm</a><br /><br />Unfortunately, the company that provided RTTarget, On-Time Informatik GmbH has ceased operations back in 2022. But a lot of companies still use it. The RTOS implements the Win32 library allowing to run Windows applications on embedded x86 based platforms. After running them through a special tool that does fixups on .exe files.<br /><br />Our primary goal was add support for the network adapter provided by the X6000E which uses IP blocks from Synopsis. I've decided to use the dwqe driver from OpenBSD, because all other network drivers that RTTarget supports were taken from BSD variants.. It was not easy due to lack of information and only brief documentation online about this Ethernet controller. In the end, we were able to achieve the desired result, it was perhaps one of the hardest challenges that I've achieved in my career until now and I've learned a great lot about Ethernet controllers and PHYs in process.<br /><br />A lot of parts of RTTarget are written in pure x86 assembly using Borland Turbo Assembler (TASM), so I had to get back into the internals of the x86 architecture.<br /><br />I did enjoy doing instruction level debugging in QEMU to diagnose early boot problems when using the UEFI mode. Rttarget can boot on 64-bit boot systems because the EFI loader provided by On-Time switches the processor from long mode (x86-64) back into regular protected mode (x86) before calling the boot vector of RTTarget-32.<br /><br />I also enjoyed this project because it made me work with another compiler. Using something else instead of the usual GCC or LLVM clang is very rare nowadays... Borland C++ 5.5.1 provides very terse error messages that oblige the programmer to search for the error. GCC is way more helpful in this regard, pointing the exact cause and location of the error. C is also a language that supports many different styles, Rttarget-32 follows usual Windows conventions. That is heavy uses of typedefs and even lower case macros. But hey, it's still C, I can figure this out...<br /><br /></p>]]></content:encoded>
						                            <category domain="https://edgemtech.ch/community/"></category>                        <dc:creator>Erik Tagirov</dc:creator>
                        <guid isPermaLink="true">https://edgemtech.ch/community/realtime-systems/rttarget-32/</guid>
                    </item>
				                    <item>
                        <title>EDGEMTech at Embedded World 2026</title>
                        <link>https://edgemtech.ch/community/general-discussions/edgemtech-at-embedded-world-2026/</link>
                        <pubDate>Sun, 22 Feb 2026 14:53:31 +0000</pubDate>
                        <description><![CDATA[&#x1f680; EDGEMTech at Embedded World 2026
We’re excited to announce that EDGEMTech SA will be attending Embedded World 2026 in Nuremberg on March 10-11.
Embedded World is one of the most ...]]></description>
                        <content:encoded><![CDATA[<h2>&#x1f680; EDGEMTech at Embedded World 2026</h2>
<p data-start="212" data-end="330">We’re excited to announce that <strong data-start="243" data-end="259">EDGEMTech SA</strong> will be attending <strong data-start="278" data-end="301">Embedded World 2026</strong> in Nuremberg on March 10-11.</p>
<p data-start="332" data-end="732">Embedded World is one of the most important international gatherings for the embedded systems community. The event brings together experts in embedded hardware, firmware, software platforms, IoT, connectivity, HMI/GUI technologies, and system architecture. It’s always a great opportunity to explore the latest innovations, exchange ideas, and connect with industry leaders from across the ecosystem.</p>
<h3>&#x1f3af; Why we’re attending</h3>
<p data-start="762" data-end="790">This year, our goals are to:</p>
<ul data-start="792" data-end="1081">
<li data-start="792" data-end="843">
<p data-start="794" data-end="843">Strengthen relationships with existing partners</p>
</li>
<li data-start="844" data-end="908">
<p data-start="846" data-end="908">Meet new companies working in embedded and connected systems</p>
</li>
<li data-start="909" data-end="954">
<p data-start="911" data-end="954">Explore emerging technologies and tooling</p>
</li>
<li data-start="955" data-end="1016">
<p data-start="957" data-end="1016">Discuss upcoming projects and collaboration opportunities</p>
</li>
<li data-start="1017" data-end="1081">
<p data-start="1019" data-end="1081">Stay closely aligned with industry trends and best practices</p>
</li>
</ul>
<p data-start="1083" data-end="1200">Although we won’t have a booth this year, our team will be present at the event and visiting several of our partners.</p>
<p data-start="1202" data-end="1490">We’re also happy to share that our close partner <strong data-start="1251" data-end="1292"><span class="hover:entity-accent entity-underline inline cursor-pointer align-baseline"><span class="whitespace-normal">LVGL</span></span></strong> will be attending and exhibiting at Embedded World. As many of you know, LVGL plays a key role in modern embedded GUI development, and we look forward to connecting with their team on-site as well.</p>
<h3>&#x1f91d; Let’s connect in Nuremberg</h3>
<p data-start="1527" data-end="1655">If you’re attending Embedded World and would like to meet, we’d be delighted to connect in person. Whether you’re interested in:</p>
<ul data-start="1657" data-end="1829">
<li data-start="1657" data-end="1690">
<p data-start="1659" data-end="1690">Embedded firmware development</p>
</li>
<li data-start="1691" data-end="1716">
<p data-start="1693" data-end="1716">GUI and HMI solutions</p>
</li>
<li data-start="1717" data-end="1757">
<p data-start="1719" data-end="1757">System architecture and optimization</p>
</li>
<li data-start="1758" data-end="1789">
<p data-start="1760" data-end="1789">Product development support</p>
</li>
<li data-start="1790" data-end="1829">
<p data-start="1792" data-end="1829">Or exploring potential partnerships</p>
</li>
</ul>
<p data-start="1831" data-end="1899">Feel free to reach out and schedule a time to meet during the event.</p>
<p data-start="1901" data-end="1920">You can contact us:</p>
<ul data-start="1921" data-end="2003">
<li data-start="1921" data-end="1961">
<p data-start="1923" data-end="1961">Via direct message here on the forum</p>
</li>
<li data-start="1962" data-end="2003">
<p data-start="1964" data-end="2003">Or through our usual contact channels</p>
</li>
</ul>
<p data-start="2005" data-end="2121">We’re looking forward to a productive and inspiring few days in Nuremberg... and hopefully meeting some of you there!</p>]]></content:encoded>
						                            <category domain="https://edgemtech.ch/community/"></category>                        <dc:creator>Gabriel CATEL TORRES</dc:creator>
                        <guid isPermaLink="true">https://edgemtech.ch/community/general-discussions/edgemtech-at-embedded-world-2026/</guid>
                    </item>
				                    <item>
                        <title>Torizon VSCode extension: What are those Dockerfile?</title>
                        <link>https://edgemtech.ch/community/orchestration-and-services/torizon-vscode-extension-what-are-those-dockerfile/</link>
                        <pubDate>Sun, 15 Feb 2026 07:36:00 +0000</pubDate>
                        <description><![CDATA[Understanding the Dockerfiles in the Torizon VS Code Templates
Hello everyone! 
Following the article about Torizon VSCode Extension by my colleague Gabriel, I wanted to add a bit more exp...]]></description>
                        <content:encoded><![CDATA[<h1>Understanding the Dockerfiles in the Torizon VS Code Templates</h1>
<p>Hello everyone! </p>
<p>Following <a href="https://edgemtech.ch/community/orchestration-and-services/torizon-vs-code-ide-extension/#post-26" target="_blank" rel="noopener">the article about Torizon VSCode Extension</a> by my colleague Gabriel, I wanted to add a bit more explanation about how to effectively use it.</p>
<p>If you're getting started with the <a href="https://github.com/torizon/vscode-torizon-templates">Torizon IDE Extension</a> and wondering why your project has three Dockerfiles and a docker-compose file, this quick guide is for you. I'll use the <strong>cLvgl</strong> template as an example, but the same pattern applies to most C/C++ Torizon templates.</p>
<h2>Overview</h2>
<p>Every Torizon IDE C/C++ template includes these container-related files:</p>
<table>
<thead>
<tr>
<th>File</th>
<th>Purpose</th>
<th>"Runs" on</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Dockerfile</code></td>
<td>Production image (multi-stage: build + deploy)</td>
<td>Target device</td>
</tr>
<tr>
<td><code>Dockerfile.debug</code></td>
<td>Debug image with SSH &amp; GDB</td>
<td>Target device</td>
</tr>
<tr>
<td><code>Dockerfile.sdk</code></td>
<td>Cross-compilation toolchain</td>
<td>Host machine</td>
</tr>
<tr>
<td><code>docker-compose.yml</code></td>
<td>Orchestrates debug and release services</td>
<td>Target device</td>
</tr>
</tbody>
</table>
<p>Let's walk through each one.</p>
<h2> </h2>
<h2><code>Dockerfile:</code> Production Image</h2>
<p>This is a <strong>multi-stage</strong> Dockerfile that both compiles and packages your app for production.</p>
<p><strong>Stage 1 — <code>build</code>:</strong> Uses <code>torizon/cross-toolchain-${IMAGE_ARCH}</code> as a base. It installs <code>cmake</code>, copies your source code, and cross-compiles the application for your target architecture (arm64, armhf, etc.) in Release mode.</p>
<p><strong>Stage 2 — <code>deploy</code>:</strong> Starts from a minimal <code>torizon/debian</code> image for the target platform. It copies <em>only</em> the compiled binary from the build stage (<code>build-${IMAGE_ARCH}/bin</code>) into the container. The final image is clean — no compiler, no build tools, just your app.</p>
<p>The entrypoint is simply:</p>
<pre contenteditable="false"><code class="language-dockerfile">CMD 
</code></pre>
<p>This is the image you'll push to a registry and deploy to your devices in the field.</p>
<p>&nbsp;</p>
<h2><code>Dockerfile.debug:</code> Development/Debug Image</h2>
<p>This Dockerfile builds a container specifically for <strong>remote debugging</strong> during development. Unlike the production Dockerfile, it does <strong>not</strong> compile your code. Instead it:</p>
<ol>
<li>Starts from the same <code>torizon/debian</code> base image for the target platform.</li>
<li>Installs debug tooling: <code>openssh-server</code>, <code>gdb</code>, <code>rsync</code>, <code>curl</code>, and <code>file</code>.</li>
<li>Configures an SSH server on a custom port (<code>DEBUG_SSH_PORT</code>) so VS Code can connect via Pipe Transport and attach the debugger.</li>
</ol>
<p>The SSH setup is intentionally insecure (empty password, root login permitted) — this is fine for a development container, but obviously not something you'd ship.</p>
<p>Notice that there's no <code>COPY</code> of source or build artifacts in this file. During the debug workflow, the IDE Extension's tasks handle copying the compiled output (built by the SDK container) into this container at deploy time. The container's only job at runtime is to run the SSH daemon:</p>
<pre contenteditable="false"><code class="language-dockerfile">CMD 
</code></pre>
<p>VS Code then connects over SSH, uploads the binary, and attaches GDB.</p>
<h2> </h2>
<h2><code>Dockerfile.sdk: </code>Cross-Compilation Toolchain</h2>
<p>This Dockerfile creates the <strong>SDK container</strong> — the environment where your code gets cross-compiled during development.</p>
<p>It's based on <code>torizon/cross-toolchain-${IMAGE_ARCH}</code> and installs <code>cmake</code> plus any additional build dependencies you need. The key thing is how it's used: the Torizon IDE Extension runs this container on your <strong>host machine</strong> with your project workspace <strong>bind-mounted</strong> into it:</p>
<pre contenteditable="false"><code>-v ${workspaceFolder}:${APP_ROOT}
</code></pre>
<p>This means:</p>
<ul>
<li>The SDK container has access to your source code.</li>
<li>Build outputs land directly in your workspace (e.g., <code>build-arm64/</code>).</li>
<li>Incremental builds work — artifacts persist between sessions.</li>
<li>The container runs as user <code>torizon</code> (not root).</li>
</ul>
<p>You only need this file for compiled languages. Templates for interpreted languages (Python, Node.js, etc.) won't have a <code>Dockerfile.sdk</code>.</p>
<h2> </h2>
<h2><code>docker-compose.yml: Making it run!</code></h2>
<p>The compose file defines <strong>two services</strong>, separated by profiles:</p>
<h3><code>__container__-debug</code> (profile: <code>debug</code>)</h3>
<p>This service uses <code>Dockerfile.debug</code>. It:</p>
<ul>
<li>Forwards the <code>DEBUG_SSH_PORT</code> so VS Code can connect.</li>
<li>Grants access to device nodes (<code>/dev</code>) via a bind mount and cgroup rules for tty, input devices, DRI (GPU), and framebuffer — essential for an LVGL app that needs to render to a display.</li>
<li>Is only started when the <code>debug</code> profile is active.</li>
</ul>
<h3><code>__container__</code> (profile: <code>release</code>)</h3>
<p>This service uses the production <code>Dockerfile</code>. It has the same device access and volume configuration as the debug service, but no SSH port forwarding. The image tag uses <code>${DOCKER_LOGIN}</code> instead of <code>${LOCAL_REGISTRY}</code>, meaning it's intended to be pushed to a remote registry.</p>
<p>The <code>__container__</code> placeholder gets replaced with your actual project name when you create a project from the template.</p>
<h3>How the profiles work</h3>
<p>The Torizon IDE Extension activates the appropriate profile automatically:</p>
<ul>
<li>During development/debugging → <code>docker compose --profile debug up</code></li>
<li>For production/release testing → <code>docker compose --profile release up</code></li>
</ul>
<p>This means both services are defined in the same file but never run simultaneously.</p>
<h2> </h2>
<h2>Wrap up: How They Work Together</h2>
<p>Here's how the three Dockerfiles fit into the development workflow:</p>
<ol>
<li>You write code on your host machine.</li>
<li><code><strong>Dockerfile.sdk</strong></code> → The IDE Extension builds the SDK container and runs <code>cmake</code> inside it (with your workspace bind-mounted) to cross-compile your code.</li>
<li><strong><code>Dockerfile.debug</code> </strong>→ The IDE Extension builds the debug container, deploys it to the target device, copies the compiled binary into it, and VS Code attaches GDB over SSH.</li>
<li><strong><code>Dockerfile</code></strong> → When you're ready to release, this multi-stage Dockerfile compiles and packages everything into a minimal production image.</li>
</ol>
<h2> </h2>
<h2>Adding Custom Dependencies</h2>
<p>You'll notice special labels like <code>__torizon_packages_build_start__</code> and <code>__torizon_packages_prod_start__</code> in the Dockerfiles. The IDE Extension uses these markers to automatically inject packages you configure via <code>torizonPackages.json</code>. You can also add packages manually between those labels.</p>
<p>&nbsp;</p>
<h2>Conclusion</h2>
<p>Hopefully this clears up the role of each file in those templates! The Torizon IDE Extension handles most of the orchestration for you, but understanding what each Dockerfile does helps a lot when you need to customize your build or debug a deployment issue!</p>]]></content:encoded>
						                            <category domain="https://edgemtech.ch/community/"></category>                        <dc:creator>David Truan</dc:creator>
                        <guid isPermaLink="true">https://edgemtech.ch/community/orchestration-and-services/torizon-vscode-extension-what-are-those-dockerfile/</guid>
                    </item>
							        </channel>
        </rss>
		