<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ QuRT - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ QuRT - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 26 May 2026 10:36:31 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/qurt/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Use SCons to Build Software Projects [Full Handbook] ]]>
                </title>
                <description>
                    <![CDATA[ If you've ever wrestled with Makefile syntax, fought tab-versus-spaces bugs, or tried to make a build system work across Linux, macOS, and Windows, SCons is worth your attention. It replaces Make, aut ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-scons-to-build-software-projects-full-handbook/</link>
                <guid isPermaLink="false">69fd02969f93a850a41cccc2</guid>
                
                    <category>
                        <![CDATA[ SCON ]]>
                    </category>
                
                    <category>
                        <![CDATA[ build ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Makefile ]]>
                    </category>
                
                    <category>
                        <![CDATA[ compilation ]]>
                    </category>
                
                    <category>
                        <![CDATA[ QuRT ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nikheel Vishwas Savant ]]>
                </dc:creator>
                <pubDate>Thu, 07 May 2026 21:22:30 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/05c9c2af-e245-4740-b50e-1144e4db1484.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you've ever wrestled with Makefile syntax, fought tab-versus-spaces bugs, or tried to make a build system work across Linux, macOS, and Windows, SCons is worth your attention. It replaces Make, autoconf, and automake with a single tool where every build file is a real Python script.</p>
<p>This handbook walks through SCons from first principles. You'll install it, build a multi-file C++ project with a static library, set up cross-compilation for an embedded target (Qualcomm's QuRT real-time operating system), and learn the internals that make SCons different from Make and CMake.</p>
<p>By the end, you'll have a working build system you can adapt to your own projects.</p>
<p>The full example code is self-contained. You can type it out, run it, and see real output at every step.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-is-scons-and-why-does-it-exist">What is SCons and Why Does it Exist</a></p>
</li>
<li><p><a href="#heading-how-scons-compares-to-make-cmake-and-meson">How SCons Compares to Make, CMake, and Meson</a></p>
</li>
<li><p><a href="#heading-a-side-by-side-look-at-make-versus-scons">A Side-by-Side Look at Make Versus SCons</a></p>
</li>
<li><p><a href="#heading-installing-scons">Installing SCons</a></p>
</li>
<li><p><a href="#heading-core-concepts-you-need-before-writing-a-build-file">Core Concepts You Need Before Writing a Build File</a></p>
</li>
<li><p><a href="#heading-the-three-environments-in-scons">The Three Environments in SCons</a></p>
</li>
<li><p><a href="#heading-construction-variables-reference">Construction Variables Reference</a></p>
</li>
<li><p><a href="#heading-your-first-sconstruct-file">Your First SConstruct File</a></p>
</li>
<li><p><a href="#heading-building-a-multi-file-c-project-step-by-step">Building a Multi-File C++ Project Step by Step</a></p>
</li>
<li><p><a href="#heading-detailed-walkthrough-of-every-file-in-the-project">Detailed Walkthrough of Every File in the Project</a></p>
</li>
<li><p><a href="#heading-running-the-build-and-understanding-the-output">Running the Build and Understanding the Output</a></p>
</li>
<li><p><a href="#heading-what-happens-during-an-incremental-build">What Happens During an Incremental Build</a></p>
</li>
<li><p><a href="#heading-cross-compiling-for-qurt-qualcomm-real-time-os">Cross-Compiling for QuRT (Qualcomm Real-Time OS)</a></p>
</li>
<li><p><a href="#heading-writing-qurt-specific-application-code">Writing QuRT-Specific Application Code</a></p>
</li>
<li><p><a href="#heading-building-both-native-and-qurt-from-one-sconstruct">Building Both Native and QuRT From One SConstruct</a></p>
</li>
<li><p><a href="#heading-how-scons-detects-dependencies-and-decides-what-to-rebuild">How SCons Detects Dependencies and Decides What to Rebuild</a></p>
</li>
<li><p><a href="#heading-writing-a-custom-scanner">Writing a Custom Scanner</a></p>
</li>
<li><p><a href="#heading-the-shared-build-cache">The Shared Build Cache</a></p>
</li>
<li><p><a href="#heading-working-with-shared-libraries">Working with Shared Libraries</a></p>
</li>
<li><p><a href="#heading-adding-command-line-options-with-addoption">Adding Command-Line Options with AddOption</a></p>
</li>
<li><p><a href="#heading-configure-checks-for-portability">Configure Checks for Portability</a></p>
</li>
<li><p><a href="#heading-custom-builders-for-non-standard-file-types">Custom Builders for Non-Standard File Types</a></p>
</li>
<li><p><a href="#heading-aliases-default-targets-and-install-rules">Aliases, Default Targets, and Install Rules</a></p>
</li>
<li><p><a href="#heading-platform-specific-configuration">Platform-Specific Configuration</a></p>
</li>
<li><p><a href="#heading-customizing-build-output">Customizing Build Output</a></p>
</li>
<li><p><a href="#heading-how-to-debug-scons-build-files">How to Debug SCons Build Files</a></p>
</li>
<li><p><a href="#heading-the-scons-command-line-reference">The SCons Command-Line Reference</a></p>
</li>
<li><p><a href="#heading-common-mistakes-and-how-to-avoid-them">Common Mistakes and How to Avoid Them</a></p>
</li>
<li><p><a href="#heading-summary">Summary</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>You need Python 3.7 or newer installed on your system. You also need a C++ compiler (GCC, Clang, or MSVC). Familiarity with basic C/C++ compilation (what a compiler and linker do) is assumed. Prior experience with Make or any build system is helpful but not required.</p>
<p>For the QuRT cross-compilation sections, you need the Qualcomm Hexagon SDK installed on your machine. Those sections are self-contained, so you can skip them if you're only interested in native builds.</p>
<h2 id="heading-what-is-scons-and-why-does-it-exist">What is SCons and Why Does it Exist?</h2>
<p>SCons is an open-source, cross-platform software construction tool written entirely in Python. Steven Knight created it in 2001 after his design won the Software Carpentry SC Build competition in August 2000.</p>
<p>The competition asked participants to design a better build tool, and Knight's "ScCons" entry beat out the alternatives. The name was later shortened to "SCons" after the project separated from Software Carpentry.</p>
<p>Knight's design drew heavily from Cons, a Perl-based build tool created by Bob Sidebotham in the late 1990s. Cons introduced several ideas that were radical at the time: content-based change detection (using MD5 hashes instead of timestamps), automatic dependency scanning for C/C++ headers, and a single global dependency graph that eliminated the problems with recursive Make.</p>
<p>SCons took all of these ideas and reimplemented them in Python, adding a proper configuration API, cross-platform support, and extensibility through Python's object model.</p>
<p>The project is currently maintained by William Deegan and Gary Oberbrunner, and it's released under the MIT license. The current stable version is 4.10.x. Development happens on GitHub, and the community communicates through a Discord server, IRC (#scons on Libera.Chat), and mailing lists.</p>
<h3 id="heading-how-scons-works">How SCons Works</h3>
<p>The central idea behind SCons is straightforward: build files should be written in a real programming language, not a domain-specific language with quirky syntax rules.</p>
<p>An SConstruct file is a Python script. You have access to loops, conditionals, functions, classes, and every Python library on your system. There are no special syntax rules to memorize, no tab-sensitivity bugs, and no distinction between spaces and tabs that silently breaks your build. If you can write Python, you can write SCons build files.</p>
<p>SCons also differs from Make in how it determines what needs to be rebuilt. Make compares file timestamps. If you run <code>touch main.c</code>, Make will recompile it even though nothing actually changed.</p>
<p>SCons computes a content hash (MD5 by default) of every source file. If the content hasn't changed, SCons skips the rebuild. This eliminates an entire class of unnecessary recompilations. It also means you never need to run <code>make clean</code> because you are unsure whether the build state is consistent. SCons' build state is always correct, because it tracks content, not time.</p>
<p>Several large projects have used SCons in production. The Godot game engine uses SCons as its build system. MongoDB used SCons for years. PlatformIO, the embedded development ecosystem, uses SCons as its core build engine. National Instruments has used it for projects with over 5,000 source files. NSIS (the Nullsoft Scriptable Install System) and several aerospace projects (including the Aerosonde UAV) have also relied on SCons.</p>
<h2 id="heading-how-scons-compares-to-make-cmake-and-meson">How SCons Compares to Make, CMake, and Meson</h2>
<p>Understanding where SCons fits relative to other build tools helps you decide when to reach for it.</p>
<h3 id="heading-scons-versus-make">SCons versus Make</h3>
<p>Make uses a custom DSL that is notoriously finicky. Tabs matter (a space where a tab should be silently does nothing). Variable expansion rules are complex and have multiple flavors (<code>=</code>, <code>:=</code>, <code>?=</code>, <code>+=</code>). Dependency detection for C/C++ headers requires manual setup or external tools like <code>makedepend</code> or compiler-generated <code>.d</code> files.</p>
<p>Recursive Make (the standard pattern for multi-directory projects) can miss cross-directory dependencies entirely, a problem documented in Peter Miller's famous 1997 paper "Recursive Make Considered Harmful."</p>
<p>SCons solves all of these problems. It scans C/C++ source files automatically, builds a single global dependency graph across all directories in a single pass, and uses content hashing instead of timestamps.</p>
<p>The tradeoff is startup speed. SCons must read every build file and construct the full dependency graph before building anything, which adds overhead that Make doesn't have. On small to medium projects (up to a few thousand source files), this overhead is negligible. On very large projects (tens of thousands of files), it can add several seconds to every invocation.</p>
<h3 id="heading-scons-versus-cmake">SCons versus CMake</h3>
<p>CMake is not a build tool. It's a meta-build system that generates Makefiles, Ninja files, or Visual Studio project files. You write CMakeLists.txt, run <code>cmake</code> to generate the native build files, then run <code>make</code> or <code>ninja</code> to actually build.</p>
<p>SCons builds directly. There is no generation step. CMake has a much larger ecosystem, better IDE integration (it can generate Xcode projects, Visual Studio solutions, and CLion configurations), and a huge library of <code>find_package</code> modules for locating third-party libraries like Boost, OpenSSL, and Qt. SCons has nothing comparable.</p>
<p>Where SCons wins is in simplicity and debuggability. Your build files are Python. You can <code>print()</code> variables, set breakpoints with <code>pdb</code>, use list comprehensions, and call any Python function. CMake's custom language is harder to debug, has surprising scoping rules, and requires learning a distinct syntax that's not used anywhere else.</p>
<h3 id="heading-scons-versus-meson">SCons versus Meson</h3>
<p>Meson is a newer build tool that generates Ninja files for fast parallel builds. It uses a custom DSL that is intentionally not Turing-complete. You can't write loops over source files or call arbitrary external programs during the configuration phase. This sounds limiting, but it prevents an entire class of build file bugs (like accidentally depending on host state that doesn't exist on other developers' machines).</p>
<p>Meson is faster than SCons on large projects because Ninja, its backend, is extremely optimized for incremental builds. Meson also has better built-in support for cross-compilation through a dedicated "cross file" format.</p>
<p>SCons gives you more flexibility through Python, but Meson's opinionated approach catches more mistakes at configuration time and produces faster builds.</p>
<p>The short version: use SCons when you want the full power of Python in your build files, when you need content-based rebuild detection, when you're working on a project that already uses it, or when you're doing embedded work where the build system needs to handle unusual toolchains and file types.</p>
<p>Use CMake when IDE integration and ecosystem size matter most. Use Meson when build speed on large projects is the primary concern.</p>
<h2 id="heading-a-side-by-side-look-at-make-versus-scons">A Side-by-Side Look at Make Versus SCons</h2>
<p>Seeing the same build expressed in both Make and SCons makes the differences concrete. Consider a simple project with two C files and a header.</p>
<p>The Makefile looks like this:</p>
<pre><code class="language-shell">CC = gcc
CFLAGS = -Wall -O2
OBJECTS = main.o utils.o

myapp: $(OBJECTS)
	\((CC) \)(CFLAGS) -o \(@ \)^

main.o: main.c utils.h
	\((CC) \)(CFLAGS) -c $&lt;

utils.o: utils.c utils.h
	\((CC) \)(CFLAGS) -c $&lt;

clean:
	rm -f myapp $(OBJECTS)
</code></pre>
<p>This Makefile has 13 lines and requires you to manually list every header dependency. If you add a new header file and forget to update the Makefile, your build will succeed but produce incorrect output. The indented lines must use literal tab characters, not spaces. The <code>\(@</code>, <code>\)^</code>, and <code>$&lt;</code> automatic variables are cryptic until you memorize them.</p>
<p>The equivalent SConstruct file looks like this:</p>
<pre><code class="language-python">env = Environment(CCFLAGS=['-Wall', '-O2'])
env.Program('myapp', ['main.c', 'utils.c'])
</code></pre>
<p>Two lines. SCons detects the header dependency on <code>utils.h</code> automatically by scanning the <code>#include</code> directives in the source files. There's no <code>clean</code> target because <code>scons -c</code> handles cleanup. There are no tab sensitivity issues because this is Python.</p>
<p>The Makefile approach has one advantage: it starts faster on large projects because it doesn't need to scan every source file for includes.</p>
<p>On a two-file project, this difference is unmeasurable. On a 10,000-file project, the SCons overhead might add 2 to 5 seconds. Whether that tradeoff matters depends on your project size and your tolerance for manual dependency management.</p>
<h2 id="heading-installing-scons">Installing SCons</h2>
<p>The simplest installation method is pip, since SCons is a pure Python package with no compiled dependencies.</p>
<pre><code class="language-shell">pip install scons
</code></pre>
<p>This installs the <code>scons</code> command globally (or in your active virtual environment). The package name on PyPI is <code>SCons</code>. On some systems, you may need to use <code>pip3</code> instead of <code>pip</code> to target Python 3.</p>
<p>You can also install through system package managers:</p>
<pre><code class="language-shell"># Debian / Ubuntu
sudo apt install scons

# Fedora
sudo dnf install scons

# macOS with Homebrew
brew install scons

# Arch Linux
sudo pacman -S scons

# Conda
conda install -c conda-forge scons
</code></pre>
<p>The <code>pip install</code> line pulls the SCons package from PyPI and places the <code>scons</code> executable on your PATH. System package managers do the same thing but integrate with your OS's package database. Either approach works. The pip method tends to give you the latest version, while system packages may lag behind by one or two releases.</p>
<p>Verify the installation by checking the version.</p>
<pre><code class="language-shell">scons --version
</code></pre>
<p>You should see output showing the SCons version number and the Python version it's running under. If the command isn't found, make sure your Python scripts directory is on your PATH. On Linux, this is typically <code>~/.local/bin</code> for user installs. On macOS with Homebrew Python, it's usually <code>/usr/local/bin</code> or <code>/opt/homebrew/bin</code>.</p>
<h2 id="heading-core-concepts-you-need-before-writing-a-build-file">Core Concepts You Need Before Writing a Build File</h2>
<p>SCons organizes builds around five core concepts. Understanding them before you write any code saves confusion later.</p>
<h3 id="heading-the-sconstruct-build-file">The SConstruct Build File</h3>
<p>This is the top-level build file. When you run <code>scons</code> in a directory, it looks for a file named <code>SConstruct</code> (capital S, capital C, no file extension). SCons also accepts the alternative names <code>Sconstruct</code> and <code>sconstruct</code>, but the capitalized version is the convention.</p>
<p>This file is a Python script. It defines what to build and how. There is exactly one SConstruct per project, and it lives in the project root.</p>
<h3 id="heading-sconscript-build-files">SConscript Build Files</h3>
<p>These are subsidiary build files for subdirectories. The top-level SConstruct calls <code>SConscript('src/SConscript')</code> to pull in build definitions from the <code>src</code> directory.</p>
<p>All file paths inside an SConscript are relative to that SConscript's location, not the project root. The <code>#</code> character at the start of a path means "relative to the SConstruct directory," which is useful for referencing shared include directories from any SConscript at any depth.</p>
<p>For example, <code>#include</code> always refers to the <code>include</code> directory at the project root, regardless of which subdirectory's SConscript uses it.</p>
<h3 id="heading-construction-environment">Construction Environment</h3>
<p>This is a Python object (created with <code>Environment()</code>) that holds all the configuration for a build: which compiler to use, what flags to pass, where to find headers, what libraries to link. You can create multiple environments for different build configurations (debug vs. release, or native vs. cross-compiled).</p>
<p>Every environment has a set of construction variables (like <code>CC</code>, <code>CCFLAGS</code>, <code>CPPPATH</code>, <code>LIBS</code>) and a set of builders (like <code>Program</code>, <code>Library</code>, <code>Object</code>). When you modify an environment with <code>env.Append()</code> or <code>env.Replace()</code>, you change the configuration for all subsequent builder calls on that environment. To isolate changes, clone the environment first with <code>env.Clone()</code>.</p>
<h3 id="heading-builder-methods">Builder Methods</h3>
<p>These are methods on the Environment object that know how to produce specific types of output.</p>
<ul>
<li><p><code>env.Program()</code> compiles and links an executable.</p>
</li>
<li><p><code>env.StaticLibrary()</code> creates a static library (<code>.a</code> on Linux, <code>.lib</code> on Windows).</p>
</li>
<li><p><code>env.SharedLibrary()</code> creates a shared library (<code>.so</code> on Linux, <code>.dylib</code> on macOS, <code>.dll</code> on Windows).</p>
</li>
<li><p><code>env.Object()</code> compiles a single source file to an object file.</p>
</li>
<li><p><code>env.Command()</code> runs an arbitrary shell command.</p>
</li>
</ul>
<p>Every builder returns a list of Node objects representing the files it will produce. You can define your own builders for file types that SCons doesn't know about, such as protocol buffer definitions, shader files, or firmware images.</p>
<h3 id="heading-nodes">Nodes</h3>
<p>These are SCons' internal representation of files and directories. When you call <code>env.Object('main.cpp')</code>, you get back a Node object, not a string. You can pass Node objects to other builders, concatenate them with the <code>+</code> operator, and use them anywhere SCons expects a file reference.</p>
<p>Working with Nodes instead of raw strings makes your build files portable across platforms because SCons handles platform-specific file extensions and path separators internally.</p>
<p>You can also create Nodes explicitly: <code>File('foo.c')</code> creates a file Node, <code>Dir('src')</code> creates a directory Node, and <code>Entry('ambiguous')</code> creates a Node whose type (file or directory) SCons determines later.</p>
<h2 id="heading-the-three-environments-in-scons">The Three Environments in SCons</h2>
<p>SCons distinguishes three types of environments, and confusing them is a common source of bugs. Understanding the distinction upfront prevents a category of hard-to-diagnose build failures.</p>
<p>The <strong>External Environment</strong> is your shell's environment, accessible through <code>os.environ</code> in Python. It contains variables like <code>PATH</code>, <code>HOME</code>, <code>PKG_CONFIG_PATH</code>, and anything else you have set in your <code>.bashrc</code> or <code>.zshrc</code>.</p>
<p>SCons doesn't automatically import this environment. This is deliberate. If SCons inherited your shell environment, your build would depend on whatever happened to be set in each developer's shell, making builds non-reproducible. A build that works on your machine but fails on a colleague's machine because they have a different <code>PATH</code> is exactly the kind of problem SCons tries to prevent.</p>
<p>The <strong>Construction Environment</strong> is the <code>Environment()</code> object you create in your SConstruct file. It holds construction variables that control how SCons invokes tools.</p>
<ul>
<li><p><code>CC</code> specifies the C compiler.</p>
</li>
<li><p><code>CXX</code> specifies the C++ compiler.</p>
</li>
<li><p><code>CCFLAGS</code> holds flags for both C and C++ compilation.</p>
</li>
<li><p><code>CPPPATH</code> lists header search directories.</p>
</li>
<li><p><code>LIBS</code> lists libraries to link.</p>
</li>
<li><p><code>LIBPATH</code> lists library search directories.</p>
</li>
</ul>
<p>These variables don't come from your shell. SCons populates them with platform-appropriate defaults (for example, <code>CC</code> defaults to <code>gcc</code> on Linux and <code>cl</code> on Windows with MSVC).</p>
<p>The <strong>Execution Environment</strong> is a dictionary stored at <code>env['ENV']</code> inside the construction environment. This is the environment that gets passed to child processes (compilers, linkers, archivers) when SCons runs them.</p>
<p>By default, it contains a minimal <code>PATH</code> sufficient to find the compiler. If your build tools need additional environment variables (for example, a cross-compiler that reads <code>HEXAGON_SDK_ROOT</code>), you must add them to <code>env['ENV']</code> explicitly.</p>
<p>When a build fails because a tool is "not found," the problem is almost always that the tool is on your shell's <code>PATH</code> (external environment) but not on the execution environment's <code>PATH</code> (<code>env['ENV']['PATH']</code>). The fix is to pass it through:</p>
<pre><code class="language-python">import os
env = Environment()
env['ENV']['PATH'] = os.environ['PATH']
</code></pre>
<p>This line copies your shell's <code>PATH</code> into the execution environment so child processes can find the same tools you can find in your terminal.</p>
<p>A broader approach is <code>env = Environment(ENV=os.environ.copy())</code>, which copies everything, but this reduces reproducibility because your build now depends on every variable in your shell.</p>
<h2 id="heading-construction-variables-reference">Construction Variables Reference</h2>
<p>SCons has dozens of construction variables. The ones you'll use most frequently for C/C++ projects are worth knowing by name.</p>
<p><code>CC</code> is the C compiler command. Defaults to the platform's default C compiler (<code>gcc</code> on Linux, <code>clang</code> on macOS, <code>cl</code> on Windows with MSVC). Override it to use a different compiler or a cross-compiler.</p>
<p><code>CXX</code> is the C++ compiler command. Same defaults as <code>CC</code> but for C++.</p>
<p><code>CCFLAGS</code> holds flags passed to both the C and C++ compilers during compilation. Use this for warnings (<code>-Wall</code>), optimization (<code>-O2</code>), and other flags that apply regardless of language.</p>
<p><code>CFLAGS</code> holds flags passed only to the C compiler. Use this for C-specific flags like <code>-std=c11</code>.</p>
<p><code>CXXFLAGS</code> holds flags passed only to the C++ compiler. Use this for C++-specific flags like <code>-std=c++17</code>.</p>
<p><code>CPPPATH</code> is a list of directories to search for header files. SCons translates each entry into a <code>-I</code> flag. The <code>#</code> prefix means relative to the SConstruct directory.</p>
<p><code>CPPDEFINES</code> is a list of preprocessor definitions. <code>env.Append(CPPDEFINES=['DEBUG', ('VERSION', '2')])</code> translates to <code>-DDEBUG -DVERSION=2</code>. Using <code>CPPDEFINES</code> instead of adding <code>-D</code> flags to <code>CCFLAGS</code> is preferred because SCons tracks them as structured data and can compare them correctly for rebuild decisions.</p>
<p><code>LIBS</code> is a list of libraries to link against. <code>LIBS=['pthread', 'm']</code> translates to <code>-lpthread -lm</code>. You can also pass Node objects returned by <code>StaticLibrary</code> or <code>SharedLibrary</code> builders.</p>
<p><code>LIBPATH</code> is a list of directories to search for libraries. Translates to <code>-L</code> flags.</p>
<p><code>LINKFLAGS</code> holds flags passed to the linker. Use this for linker-specific options like <code>-nostdlib</code>, <code>-Wl,--gc-sections</code>, or <code>-static</code>.</p>
<p><code>AR</code> is the static library archiver command. Defaults to <code>ar</code> on POSIX systems.</p>
<p><code>LINK</code> is the linker command. Defaults to the C or C++ compiler (which invokes the linker internally).</p>
<p><code>PROGSUFFIX</code> is the suffix for executable files. Empty on POSIX, <code>.exe</code> on Windows. You rarely need to set this, as SCons detects it from the platform.</p>
<p>All of these variables can be set in the <code>Environment()</code> constructor, modified with <code>env.Append()</code>, <code>env.Prepend()</code>, or <code>env.Replace()</code>, or overridden per-builder-call by passing them as keyword arguments.</p>
<h2 id="heading-your-first-sconstruct-file">Your First SConstruct File</h2>
<p>Create a directory for experimentation and put a single C file in it.</p>
<pre><code class="language-c">// hello.c
#include &lt;stdio.h&gt;

int main() {
    printf("Hello from SCons!\n");
    return 0;
}
</code></pre>
<p>This is a minimal C program that prints a message and exits. Nothing complicated. It exists solely to give SCons something to build.</p>
<p>Now create an SConstruct file in the same directory.</p>
<pre><code class="language-python">Program('hello.c')
</code></pre>
<p>This single line is a complete SConstruct file. <code>Program</code> is a default builder that's available without creating an explicit Environment. Behind the scenes, SCons creates a default environment with platform-appropriate compiler settings and uses it for this <code>Program</code> call. It tells SCons to compile <code>hello.c</code> and link it into an executable.</p>
<p>Run the build.</p>
<pre><code class="language-shell">scons
</code></pre>
<p>SCons prints output showing the compilation and linking commands it executes. On Linux with GCC, you'll see something like <code>gcc -o hello.o -c hello.c</code> followed by <code>gcc -o hello hello.o</code>. The resulting executable is named <code>hello</code> (on Linux/macOS) or <code>hello.exe</code> (on Windows). SCons derives the output name from the source file name by stripping the extension.</p>
<p>Run <code>scons</code> again without changing anything. SCons prints <code>scons: 'hello' is up to date.</code> and does nothing. It read the content hash of <code>hello.c</code>, compared it to the stored hash from the previous build, and determined that no rebuild was necessary. This is the content-based rebuild detection in action.</p>
<p>Now run <code>touch hello.c</code> and then <code>scons</code> again. SCons still does nothing. The content of <code>hello.c</code> didn't change, so the hash is identical. Make would have recompiled here. SCons does not.</p>
<p>For a slightly more realistic example, create an explicit environment with custom flags.</p>
<pre><code class="language-python">env = Environment(
    CC='gcc',
    CCFLAGS=['-Wall', '-Wextra', '-O2'],
)
env.Program('hello', 'hello.c')
</code></pre>
<p>This version creates a construction environment, sets the compiler to <code>gcc</code> explicitly, enables extra warnings with <code>-Wextra</code>, and optimizes with <code>-O2</code>. The <code>Program</code> call now takes two arguments: the target name <code>'hello'</code> and the source file <code>'hello.c'</code>. When you provide both, you control the output name directly.</p>
<p>You can add multiple programs in the same SConstruct:</p>
<pre><code class="language-python">env = Environment(CCFLAGS=['-Wall', '-O2'])
env.Program('hello', 'hello.c')
env.Program('goodbye', 'goodbye.c')
</code></pre>
<p>Running <code>scons</code> builds both executables. Running <code>scons hello</code> builds only the first one. SCons accepts target names on the command line to build selectively.</p>
<h2 id="heading-building-a-multi-file-c-project-step-by-step">Building a Multi-File C++ Project Step by Step</h2>
<p>A single-file example is useful for verifying your installation, but real projects have multiple source files, libraries, and header directories. This section builds a complete project with all of those elements.</p>
<p>The project structure looks like this:</p>
<pre><code class="language-shell">myproject/
    SConstruct
    include/
        config.h
    lib/
        SConscript
        mathutils.h
        mathutils.cpp
        stringutils.h
        stringutils.cpp
    src/
        SConscript
        main.cpp
        app.h
        app.cpp
</code></pre>
<p>This diagram shows a project with three directories beneath the root. The <code>include</code> directory holds a shared configuration header that defines version constants. The <code>lib</code> directory contains two utility modules (math and string operations) that get compiled into a static library called <code>libmyutils.a</code>. The <code>src</code> directory holds the main application code that depends on the library.</p>
<p>Each directory with compilable source files has its own <code>SConscript</code> file. The top-level <code>SConstruct</code> orchestrates everything.</p>
<p>The build system compiles the library first, then the application, and places all build artifacts in a separate <code>build</code> directory to keep the source tree clean. This separation means you can delete the entire <code>build</code> directory and rebuild from scratch without touching any source files.</p>
<p>Create the project directory and all subdirectories first.</p>
<pre><code class="language-shell">mkdir -p myproject/include myproject/lib myproject/src
cd myproject
</code></pre>
<p>These commands create the full directory tree. The <code>-p</code> flag on <code>mkdir</code> creates parent directories as needed and does not error if they already exist.</p>
<p>Now create each file. Start with the shared configuration header.</p>
<pre><code class="language-c">// include/config.h
#ifndef CONFIG_H
#define CONFIG_H
#define APP_VERSION "1.0.0"
#define APP_NAME "SCons Demo"
#endif
</code></pre>
<p>This header defines version and name constants that the application code will reference. The include guards (<code>#ifndef</code> / <code>#define</code> / <code>#endif</code>) prevent double-inclusion, which is standard practice in C/C++ headers. Because this header is in the <code>include</code> directory, any source file that wants to use it must have <code>include</code> on its header search path. The SConstruct file handles this through the <code>CPPPATH</code> variable.</p>
<p>Next, the math utility library:</p>
<pre><code class="language-cpp">// lib/mathutils.h
#ifndef MATHUTILS_H
#define MATHUTILS_H

int factorial(int n);
double circle_area(double radius);

#endif
</code></pre>
<pre><code class="language-cpp">// lib/mathutils.cpp
#include "mathutils.h"
#include &lt;cmath&gt;

int factorial(int n) {
    if (n &lt;= 1) return 1;
    return n * factorial(n - 1);
}

double circle_area(double radius) {
    return M_PI * radius * radius;
}
</code></pre>
<p>The <code>mathutils</code> module provides two functions: a recursive factorial calculation and a circle area computation. The header declares the function signatures so that other translation units can call them. The implementation file defines the function bodies. The <code>cmath</code> include brings in <code>M_PI</code>, the mathematical constant for pi.</p>
<p>When SCons processes <code>mathutils.cpp</code>, it scans the <code>#include</code> directives and discovers that <code>mathutils.cpp</code> depends on both <code>mathutils.h</code> and the system header <code>cmath</code>. If you later modify <code>mathutils.h</code>, SCons knows to recompile <code>mathutils.cpp</code> without any manual dependency declaration.</p>
<p>Now the string utility:</p>
<pre><code class="language-cpp">// lib/stringutils.h
#ifndef STRINGUTILS_H
#define STRINGUTILS_H
#include &lt;string&gt;

std::string to_upper(const std::string&amp; s);

#endif
</code></pre>
<pre><code class="language-cpp">// lib/stringutils.cpp
#include "stringutils.h"
#include &lt;algorithm&gt;
#include &lt;cctype&gt;

std::string to_upper(const std::string&amp; s) {
    std::string result = s;
    std::transform(result.begin(), result.end(),
                   result.begin(), ::toupper);
    return result;
}
</code></pre>
<p>The <code>stringutils</code> module has a single function that converts a string to uppercase using the standard library's <code>transform</code> algorithm. The <code>::toupper</code> passed as the transformation function is the C locale version from <code>&lt;cctype&gt;</code>. Together with <code>mathutils</code>, these two modules form a small utility library that the application will link against.</p>
<p>Now the application layer:</p>
<pre><code class="language-cpp">// src/app.h
#ifndef APP_H
#define APP_H

void run_app();

#endif
</code></pre>
<pre><code class="language-cpp">// src/app.cpp
#include "app.h"
#include "config.h"
#include "mathutils.h"
#include "stringutils.h"
#include &lt;iostream&gt;

void run_app() {
    std::cout &lt;&lt; "Application: " &lt;&lt; APP_NAME &lt;&lt; std::endl;
    std::cout &lt;&lt; "Version: " &lt;&lt; APP_VERSION &lt;&lt; std::endl;
    std::cout &lt;&lt; "5! = " &lt;&lt; factorial(5) &lt;&lt; std::endl;
    std::cout &lt;&lt; "Circle area (r=3): " &lt;&lt; circle_area(3.0) &lt;&lt; std::endl;
    std::cout &lt;&lt; to_upper("hello scons") &lt;&lt; std::endl;
}
</code></pre>
<pre><code class="language-cpp">// src/main.cpp
#include "app.h"

int main() {
    run_app();
    return 0;
}
</code></pre>
<p>The <code>app.cpp</code> file includes headers from all three directories: <code>config.h</code> from <code>include</code>, <code>mathutils.h</code> and <code>stringutils.h</code> from <code>lib</code>, and its own <code>app.h</code>.</p>
<p>This cross-directory dependency pattern is common in real projects and is precisely the scenario where Make's manual dependency tracking becomes error-prone. SCons handles it automatically. The <code>main.cpp</code> file is deliberately thin, delegating all work to <code>run_app()</code>. This pattern (a thin <code>main</code> that calls into application logic) makes the code easier to test because you can link <code>app.cpp</code> against a test harness without pulling in <code>main</code>.</p>
<p>Now the build files. Start with the top-level SConstruct:</p>
<pre><code class="language-python"># SConstruct
import os

env = Environment(
    CPPPATH=['#include', '#lib'],
    CCFLAGS=['-Wall', '-std=c++17'],
)

debug = ARGUMENTS.get('debug', '0')
if debug == '1':
    env.Append(CCFLAGS=['-g', '-O0', '-DDEBUG'])
    variant = 'build/debug'
else:
    env.Append(CCFLAGS=['-O2', '-DNDEBUG'])
    variant = 'build/release'

Export('env')

lib = SConscript('lib/SConscript',
                 variant_dir=variant + '/lib',
                 duplicate=0)

SConscript('src/SConscript',
           variant_dir=variant + '/src',
           duplicate=0,
           exports={'mylib': lib})
</code></pre>
<p>This SConstruct file is the control center of the build. The next section walks through every line in detail.</p>
<p>The library's SConscript file:</p>
<pre><code class="language-python"># lib/SConscript
Import('env')

lib = env.StaticLibrary('myutils', [
    'mathutils.cpp',
    'stringutils.cpp',
])

Return('lib')
</code></pre>
<p>This file imports the shared environment, compiles both library source files into a static library named <code>libmyutils.a</code> (on Linux) or <code>myutils.lib</code> (on Windows), and returns the resulting Node to the caller.</p>
<p>The source file paths <code>mathutils.cpp</code> and <code>stringutils.cpp</code> are relative to this SConscript file's directory, which is <code>lib/</code>. You don't need to write <code>lib/mathutils.cpp</code> because SCons already knows the context.</p>
<p>The application's SConscript file:</p>
<pre><code class="language-python"># src/SConscript
Import('env')
Import('mylib')

app = env.Program(
    target='myapp',
    source=['main.cpp', 'app.cpp'],
    LIBS=[mylib, 'm'],
    LIBPATH=['#build/release/lib', '#build/debug/lib'],
)

Return('app')
</code></pre>
<p>This file imports both the shared environment and the library Node. It compiles the application sources and links them against the <code>myutils</code> library and the math library (<code>-lm</code>). The <code>LIBPATH</code> tells the linker where to find <code>libmyutils.a</code>.</p>
<p>Both the debug and release library paths are listed so the linker finds the library regardless of which build variant is active.</p>
<h2 id="heading-detailed-walkthrough-of-every-file-in-the-project">Detailed Walkthrough of Every File in the Project</h2>
<p>This section explains the SConstruct and SConscript files line by line. Understanding each line is the difference between cargo-culting a build system and being able to modify it confidently.</p>
<h3 id="heading-the-sconstruct-file">The SConstruct File</h3>
<pre><code class="language-python">import os
</code></pre>
<p>Standard Python import. You might need <code>os.environ</code> later to pass shell environment variables into the build, <code>os.path.join</code> to construct portable file paths, or <code>os.path.exists</code> to check for optional toolchains. Even if you don't use it immediately, having it available is common practice in SConstruct files.</p>
<pre><code class="language-python">env = Environment(
    CPPPATH=['#include', '#lib'],
    CCFLAGS=['-Wall', '-std=c++17'],
)
</code></pre>
<p><code>Environment()</code> creates a construction environment. This is the central configuration object that holds everything SCons needs to compile and link your code. <code>CPPPATH</code> sets the header search path. The <code>#</code> prefix means "relative to the directory containing SConstruct." So <code>#include</code> resolves to <code>myproject/include</code> and <code>#lib</code> resolves to <code>myproject/lib</code>, regardless of which SConscript file uses this environment.</p>
<p>When SCons invokes the compiler, it translates <code>CPPPATH</code> entries into <code>-I</code> flags automatically: <code>-Iinclude -Ilib</code>. <code>CCFLAGS</code> holds compiler flags passed to both the C and C++ compilers. <code>-Wall</code> enables all standard warnings. <code>-std=c++17</code> selects the C++17 standard. Note that <code>-std=c++17</code> is a language standard flag, so it could also go in <code>CXXFLAGS</code> (C++ only), but placing it in <code>CCFLAGS</code> is harmless here because this project has no C files.</p>
<pre><code class="language-python">debug = ARGUMENTS.get('debug', '0')
if debug == '1':
    env.Append(CCFLAGS=['-g', '-O0', '-DDEBUG'])
    variant = 'build/debug'
else:
    env.Append(CCFLAGS=['-O2', '-DNDEBUG'])
    variant = 'build/release'
</code></pre>
<p><code>ARGUMENTS</code> is a global dictionary that SCons populates from command-line key=value pairs. Running <code>scons debug=1</code> sets <code>ARGUMENTS['debug']</code> to the string <code>'1'</code>. The <code>get</code> method provides a default of <code>'0'</code> when the key is absent, so running <code>scons</code> without arguments builds in release mode.</p>
<p>Depending on the value, the code appends debug flags (<code>-g</code> for debug symbols so GDB can show source lines, <code>-O0</code> for no optimization so variable values are not optimized away, and <code>-DDEBUG</code> to define a preprocessor macro your code can check with <code>#ifdef DEBUG</code>) or release flags (<code>-O2</code> for optimization and <code>-DNDEBUG</code> to disable <code>assert()</code> statements).</p>
<p>The <code>variant</code> variable determines the output directory for build artifacts. <code>env.Append()</code> adds to an existing variable without overwriting what is already there. If <code>CCFLAGS</code> already contains <code>['-Wall', '-std=c++17']</code>, appending <code>['-g', '-O0', '-DDEBUG']</code> produces <code>['-Wall', '-std=c++17', '-g', '-O0', '-DDEBUG']</code>.</p>
<pre><code class="language-python">Export('env')
</code></pre>
<p><code>Export</code> makes the <code>env</code> variable available to SConscript files that call <code>Import('env')</code>. This is SCons' mechanism for sharing data between build files. It works through a global namespace managed by SCons, not through Python's module import system. You can export any Python object: environments, strings, lists, dictionaries, or Node objects. Multiple variables can be exported at once: <code>Export('env', 'version', 'platform')</code>.</p>
<pre><code class="language-python">lib = SConscript('lib/SConscript',
                 variant_dir=variant + '/lib',
                 duplicate=0)
</code></pre>
<p><code>SConscript()</code> reads and executes a subsidiary build file. The first argument is the path to the SConscript file relative to the SConstruct. The <code>variant_dir</code> parameter redirects all build output from <code>lib/</code> into the variant directory (for example, <code>build/release/lib</code>). This keeps compiled object files and libraries out of your source tree. <code>duplicate=0</code> tells SCons not to copy (or symlink) source files into the variant directory.</p>
<p>Without this flag, SCons creates copies of your source files inside <code>build/release/lib</code> so that the build tool sees sources and outputs in the same directory. This duplication is rarely necessary and can be confusing because you end up with two copies of every source file. Setting <code>duplicate=0</code> tells SCons to reference the original source files in place. The return value of <code>SConscript()</code> is whatever the subsidiary file passes to <code>Return()</code>. In this case, it's the Node object representing the built static library.</p>
<pre><code class="language-python">SConscript('src/SConscript',
           variant_dir=variant + '/src',
           duplicate=0,
           exports={'mylib': lib})
</code></pre>
<p>This second <code>SConscript</code> call reads the application's build file. The <code>exports</code> parameter is different from the global <code>Export()</code> function. It passes the library Node (returned from the library SConscript) into the application SConscript under the name <code>mylib</code>.</p>
<p>This is a scoped export: only this specific SConscript call receives <code>mylib</code>. The application SConscript retrieves it with <code>Import('mylib')</code>. This is how the application build file knows about the library without hardcoding paths to <code>.a</code> files.</p>
<h3 id="heading-the-library-sconscript">The Library SConscript</h3>
<pre><code class="language-python">Import('env')
</code></pre>
<p><code>Import</code> retrieves a variable from SCons' global export namespace. This pulls in the environment that the SConstruct file exported with <code>Export('env')</code>. After this line, <code>env</code> refers to the same Environment object created in SConstruct. Any modifications you make to <code>env</code> here will affect it everywhere. If you need local modifications, use <code>env.Clone()</code> first.</p>
<pre><code class="language-python">lib = env.StaticLibrary('myutils', [
    'mathutils.cpp',
    'stringutils.cpp',
])
</code></pre>
<p><code>env.StaticLibrary()</code> is a builder that compiles the listed source files into object files and then archives them into a static library using <code>ar</code>.</p>
<p>The first argument is the library name. SCons automatically adds the platform-appropriate prefix and suffix: <code>libmyutils.a</code> on Linux/macOS, <code>myutils.lib</code> on Windows. You never need to hard-code these. The source file paths are relative to this SConscript file's directory (which is <code>lib/</code>).</p>
<p>SCons also automatically scans these <code>.cpp</code> files for <code>#include</code> directives to establish implicit dependencies on header files. If <code>mathutils.cpp</code> includes <code>mathutils.h</code>, that dependency is tracked without any action from you.</p>
<pre><code class="language-python">Return('lib')
</code></pre>
<p><code>Return</code> sends the library Node back to the calling <code>SConscript()</code> function in SConstruct. The string <code>'lib'</code> is the name of the local variable to return, not a file path. This is similar to a Python <code>return</code> statement, but it works across SCons' build file execution model. You can return multiple values: <code>Return('lib', 'headers')</code>.</p>
<h3 id="heading-the-application-sconscript">The Application SConscript</h3>
<pre><code class="language-python">Import('env')
Import('mylib')
</code></pre>
<p>Two imports: the shared construction environment (from the global <code>Export</code>) and the library Node (from the scoped <code>exports</code> parameter of the <code>SConscript()</code> call in the SConstruct file). These are separate <code>Import</code> calls, but you can also write <code>Import('env', 'mylib')</code> on a single line.</p>
<pre><code class="language-python">app = env.Program(
    target='myapp',
    source=['main.cpp', 'app.cpp'],
    LIBS=[mylib, 'm'],
    LIBPATH=['#build/release/lib', '#build/debug/lib'],
)
</code></pre>
<p><code>env.Program()</code> compiles source files and links them into an executable. <code>target</code> is the output executable name (SCons adds <code>.exe</code> on Windows automatically). <code>source</code> lists the C++ files to compile. The order of source files doesn't matter for the final result, but convention is to list <code>main.cpp</code> first.</p>
<p><code>LIBS</code> specifies libraries to link against. Passing the <code>mylib</code> Node directly (instead of a string like <code>'myutils'</code>) is the correct approach because SCons then knows the exact file dependency and will rebuild the executable if the library changes.</p>
<p>The <code>'m'</code> string links the system math library (<code>-lm</code> on the command line), needed because <code>mathutils.cpp</code> uses functions from <code>&lt;cmath&gt;</code>. <code>LIBPATH</code> tells the linker where to search for libraries, translated to <code>-L</code> flags. Both debug and release paths are listed so the correct one is found regardless of build type.</p>
<p>These keyword arguments (<code>LIBS</code>, <code>LIBPATH</code>) override the environment's values for this specific builder call only. They don't modify the shared <code>env</code>.</p>
<pre><code class="language-python">Return('app')
</code></pre>
<p>Returns the application Node to the caller. The SConstruct doesn't use this return value in the current example, but returning it is good practice because it allows future extensions. You might later add <code>env.Install('/usr/local/bin', app)</code> in the SConstruct, or create an <code>env.Alias('run', app, './build/release/src/myapp')</code> to define a <code>scons run</code> command.</p>
<h2 id="heading-running-the-build-and-understanding-the-output">Running the Build and Understanding the Output</h2>
<p>With all files in place, run the build from the project root.</p>
<pre><code class="language-bash">scons
</code></pre>
<p>SCons produces output like this (on Linux with GCC):</p>
<pre><code class="language-plaintext">scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
g++ -o build/release/lib/mathutils.o -c -Wall -std=c++17 -O2 -DNDEBUG -Iinclude -Ilib lib/mathutils.cpp
g++ -o build/release/lib/stringutils.o -c -Wall -std=c++17 -O2 -DNDEBUG -Iinclude -Ilib lib/stringutils.cpp
ar rc build/release/lib/libmyutils.a build/release/lib/mathutils.o build/release/lib/stringutils.o
ranlib build/release/lib/libmyutils.a
g++ -o build/release/src/main.o -c -Wall -std=c++17 -O2 -DNDEBUG -Iinclude -Ilib src/main.cpp
g++ -o build/release/src/app.o -c -Wall -std=c++17 -O2 -DNDEBUG -Iinclude -Ilib src/app.cpp
g++ -o build/release/src/myapp build/release/src/main.o build/release/src/app.o -Lbuild/release/lib -Lbuild/debug/lib build/release/lib/libmyutils.a -lm
scons: done building targets.
</code></pre>
<p>The first two lines show SCons reading all SConstruct and SConscript files. During this phase, it constructs the complete dependency graph in memory. No compilation happens yet.</p>
<p>The "Building targets" section shows the actual commands executed. Each <code>g++</code> call includes the <code>-I</code> flags derived from <code>CPPPATH</code> (note <code>-Iinclude -Ilib</code>), the flags from <code>CCFLAGS</code> (<code>-Wall -std=c++17 -O2 -DNDEBUG</code>), and the <code>-c</code> flag for compilation (producing an object file, not linking).</p>
<p>The <code>ar rc</code> command creates the static library archive, and <code>ranlib</code> generates the archive index so the linker can find symbols efficiently.</p>
<p>The final <code>g++</code> line links everything together, with <code>-L</code> flags from <code>LIBPATH</code> pointing the linker to the library directories, the explicit library file path, and <code>-lm</code> for the system math library.</p>
<p>Run the resulting executable:</p>
<pre><code class="language-bash">./build/release/src/myapp
</code></pre>
<p>The output is:</p>
<pre><code class="language-plaintext">Application: SCons Demo
Version: 1.0.0
5! = 120
Circle area (r=3): 28.2743
HELLO SCONS
</code></pre>
<p>Each line corresponds to a function call in <code>run_app()</code>. The version and name come from <code>config.h</code>. The factorial and circle area come from <code>mathutils</code>. The uppercase string comes from <code>stringutils</code>. All libraries linked correctly and all header paths resolved.</p>
<p>Now build the debug version:</p>
<pre><code class="language-bash">scons debug=1
</code></pre>
<p>This creates a parallel set of build artifacts under <code>build/debug/</code>. The release build artifacts under <code>build/release/</code> remain untouched.</p>
<p>You can switch between debug and release builds without triggering a full recompile of the other variant. Each variant has its own <code>.o</code> files, <code>.a</code> library, and executable. The directory structure under <code>build/debug/</code> mirrors <code>build/release/</code>.</p>
<h2 id="heading-what-happens-during-an-incremental-build">What Happens During an Incremental Build</h2>
<p>Understanding what SCons does on the second and subsequent builds helps you trust the system and diagnose unexpected rebuilds.</p>
<p>Run <code>scons</code> again after a successful build. The output is:</p>
<pre><code class="language-plaintext">scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
scons: `.' is up to date.
scons: done building targets.
</code></pre>
<p>SCons still reads every SConscript file and constructs the full dependency graph. It then walks the graph and checks every node.</p>
<p>For each source file, it computes the content hash and compares it to the hash stored in <code>.sconsign.dblite</code>. For each target file, it checks whether the source hashes, compiler command, and flags match the values from the previous build. Everything matches, so nothing is rebuilt.</p>
<p>Now modify <code>lib/mathutils.h</code> by adding a new function declaration:</p>
<pre><code class="language-cpp">// Add this line to mathutils.h
int fibonacci(int n);
</code></pre>
<p>Run <code>scons</code> again. SCons recompiles <code>mathutils.cpp</code> (because it includes <code>mathutils.h</code>, which changed), recompiles <code>app.cpp</code> (because it also includes <code>mathutils.h</code>), re-archives the static library (because <code>mathutils.o</code> changed), and re-links the executable (because both the library and <code>app.o</code> changed).</p>
<p>It doesn't recompile <code>stringutils.cpp</code> (it doesn't include <code>mathutils.h</code>) or <code>main.cpp</code> (it only includes <code>app.h</code>, which didn't change).</p>
<p>This is the dependency graph at work. SCons knows the complete chain: <code>mathutils.h</code> changed, so every file that directly or transitively depends on it gets rebuilt. Files that don't depend on it are untouched. You didn't need to specify any of these dependencies manually.</p>
<p>Now add a comment to <code>stringutils.cpp</code> without changing any actual code:</p>
<pre><code class="language-cpp">// This is just a comment
#include "stringutils.h"
</code></pre>
<p>Run <code>scons</code>. SCons recompiles <code>stringutils.cpp</code> because its content hash changed (comments are part of the content).</p>
<p>But here's where SCons gets clever: after recompiling, it computes the hash of the new <code>stringutils.o</code>. If the compiler produced an identical object file (which it often does for comment-only changes because comments don't affect the compiled output), SCons doesn't re-archive the library or re-link the executable.</p>
<p>This "short-circuiting" behavior prevents unnecessary downstream rebuilds. Make can't do this because it only looks at timestamps, not content.</p>
<h2 id="heading-cross-compiling-for-qurt-qualcomm-real-time-os">Cross-Compiling for QuRT (Qualcomm Real-Time OS)</h2>
<p>One of SCons' strengths is that setting up cross-compilation does not require a separate toolchain file format (like CMake's toolchain files). You configure everything in Python, using the same Environment API you already know.</p>
<h3 id="heading-what-is-qurt">What is QuRT</h3>
<p><a href="https://www.freecodecamp.org/news/qurt-the-real-time-os-inside-your-phone-s-processor-full-handbook/">QuRT is Qualcomm's proprietary real-time operating system</a> that runs on the Hexagon DSP (Digital Signal Processor) found in Snapdragon processors. The Hexagon DSP is a separate processor core on the Snapdragon SoC (System on Chip), distinct from the ARM application cores that run Android or Linux.</p>
<p>While the ARM cores handle the user interface and general application logic, the Hexagon DSP handles computationally intensive, latency-sensitive tasks: audio processing, sensor fusion, camera image processing, and machine learning inference.</p>
<p>QuRT provides the threading, memory management, and interrupt handling layer on the Hexagon DSP. It's a microkernel RTOS with hard real-time guarantees: interrupt latencies are bounded and predictable, which is essential for applications like audio where a missed deadline produces an audible glitch. QuRT supports POSIX-like threading (with <code>qurt_thread_create</code> instead of <code>pthread_create</code>), mutexes, semaphores, signals, and memory-mapped I/O.</p>
<p>Building code for QuRT requires the Hexagon SDK, which includes the Hexagon compiler (<code>hexagon-clang</code> and <code>hexagon-clang++</code>), linker, assembler, archiver, and QuRT-specific system headers and libraries. The SDK also includes a simulator (<code>hexagon-sim</code>) that can run Hexagon binaries on your development machine for testing without physical hardware.</p>
<h3 id="heading-the-hexagon-sdk-directory-structure">The Hexagon SDK Directory Structure</h3>
<p>The Hexagon SDK follows a specific layout that you need to know to configure your build system. A typical installation looks like this:</p>
<pre><code class="language-plaintext">$HEXAGON_SDK_ROOT/
    tools/
        HEXAGON_Tools/
            8.8.06/
                Tools/
                    bin/
                        hexagon-clang
                        hexagon-clang++
                        hexagon-ar
                        hexagon-ranlib
                        hexagon-as
                        hexagon-sim
                    include/
                    lib/
    rtos/
        qurt/
            computev66/
                include/
                    qurt.h
                    qurt_thread.h
                    qurt_mutex.h
                    posix/
                lib/
                    libqurt.a
            computev73/
                include/
                lib/
    libs/
        common/
</code></pre>
<p>The <code>tools/HEXAGON_Tools</code> directory contains the compiler toolchain. The version number (like <code>8.8.06</code>) corresponds to the Hexagon Tools release. The <code>rtos/qurt</code> directory contains the QuRT kernel headers and prebuilt libraries, organized by architecture variant. <code>computev66</code> targets the Hexagon V66 architecture (found in older Snapdragon chips), while <code>computev73</code> targets the V73 (found in newer ones like Snapdragon 8 Gen 2). Each variant has its own <code>include</code> and <code>lib</code> directories because the kernel is compiled differently for each architecture version.</p>
<h3 id="heading-the-cross-compilation-sconstruct">The Cross-Compilation SConstruct</h3>
<p>The following SConstruct file configures a cross-compilation environment for QuRT. It assumes the Hexagon SDK is installed and the <code>HEXAGON_SDK_ROOT</code> environment variable points to it.</p>
<pre><code class="language-python"># SConstruct for QuRT / Hexagon cross-compilation
import os
import sys

hexagon_sdk = os.environ.get('HEXAGON_SDK_ROOT',
                              '/opt/hexagon/sdk')
if not os.path.isdir(hexagon_sdk):
    print('Error: HEXAGON_SDK_ROOT not set or directory does not exist')
    print('Set it with: export HEXAGON_SDK_ROOT=/path/to/hexagon/sdk')
    Exit(1)

hexagon_tools = os.path.join(hexagon_sdk, 'tools', 'HEXAGON_Tools')
hexagon_ver = os.environ.get('HEXAGON_TOOLS_VER', '8.8.06')
tool_base = os.path.join(hexagon_tools, hexagon_ver, 'Tools')
tool_bin = os.path.join(tool_base, 'bin')

hexagon_arch = ARGUMENTS.get('arch', 'v73')
qurt_root = os.path.join(hexagon_sdk, 'rtos', 'qurt')
qurt_variant = 'compute' + hexagon_arch
qurt_inc = os.path.join(qurt_root, qurt_variant, 'include')
qurt_lib = os.path.join(qurt_root, qurt_variant, 'lib')

env = Environment(
    CC=os.path.join(tool_bin, 'hexagon-clang'),
    CXX=os.path.join(tool_bin, 'hexagon-clang++'),
    AR=os.path.join(tool_bin, 'hexagon-ar'),
    RANLIB=os.path.join(tool_bin, 'hexagon-ranlib'),
    AS=os.path.join(tool_bin, 'hexagon-as'),
    LINK=os.path.join(tool_bin, 'hexagon-clang++'),
    CPPPATH=[
        '#include',
        '#lib',
        qurt_inc,
        os.path.join(qurt_inc, 'posix'),
    ],
    CCFLAGS=[
        '-m' + hexagon_arch,
        '-G0',
        '-Wall',
        '-O2',
        '-fPIC',
        '-DQURT',
        '-D__QURT',
    ],
    LINKFLAGS=[
        '-m' + hexagon_arch,
        '-G0',
        '-nostdlib',
    ],
    LIBPATH=[
        '#build/qurt/lib',
        qurt_lib,
    ],
    LIBS=[
        'qurt',
        'qcc',
        'timer',
    ],
    ENV={
        'PATH': tool_bin + ':' + os.environ.get('PATH', ''),
        'HEXAGON_SDK_ROOT': hexagon_sdk,
    },
)

env['CCCOMSTR'] = '  HEX-CC   $TARGET'
env['CXXCOMSTR'] = '  HEX-CXX  $TARGET'
env['LINKCOMSTR'] = '  HEX-LINK $TARGET'
env['ARCOMSTR'] = '  HEX-AR   $TARGET'

Export('env')

lib = SConscript('lib/SConscript',
                 variant_dir='build/qurt/lib',
                 duplicate=0)

SConscript('src/SConscript',
           variant_dir='build/qurt/src',
           duplicate=0,
           exports={'mylib': lib})
</code></pre>
<p>This file does a lot, so it's worth going through the key parts in detail.</p>
<p>The first block validates and constructs file paths to the Hexagon toolchain. <code>HEXAGON_SDK_ROOT</code> is the standard environment variable set when you install the Hexagon SDK. If it's not set, the build exits with a clear error message instead of failing later with a cryptic "compiler not found" error. The <code>tool_bin</code> variable points to the directory containing <code>hexagon-clang</code>, <code>hexagon-clang++</code>, <code>hexagon-ar</code>, and other cross-compilation tools.</p>
<p>The architecture is configurable through the command line with <code>scons arch=v66</code> or <code>scons arch=v73</code>. The <code>hexagon_arch</code> variable defaults to <code>v73</code> and feeds into both the compiler flags (<code>-mv73</code>) and the QuRT directory path (<code>computev73</code>). This makes it easy to target different Hexagon versions from the same build file.</p>
<p>The <code>qurt_root</code>, <code>qurt_inc</code>, and <code>qurt_lib</code> variables locate the QuRT headers and prebuilt libraries. The <code>posix</code> subdirectory inside the include path contains POSIX-compatible wrappers that let you use familiar function signatures (like <code>pthread_mutex_init</code>) that map to QuRT's native API underneath.</p>
<p>The <code>Environment()</code> call overrides every tool. <code>CC</code>, <code>CXX</code>, <code>AR</code>, <code>RANLIB</code>, <code>AS</code>, and <code>LINK</code> all point to the Hexagon cross-compiler tools instead of the host system's native compiler.</p>
<p>This is the fundamental mechanism for cross-compilation in SCons: you swap out the tools in the construction environment. The same SConscript files that work for native builds work for cross-builds because they only interact with the environment through the <code>env</code> variable, never by calling <code>gcc</code> directly.</p>
<p>The <code>CCFLAGS</code> array contains Hexagon-specific flags. <code>-mv73</code> (assembled from <code>-m</code> + the architecture variable) targets the V73 architecture and tells the compiler to generate Hexagon V73 instructions.</p>
<p><code>-G0</code> disables the small data section. On the Hexagon DSP, the small data section uses a special register (GP) for faster access to small global variables, but disabling it with <code>-G0</code> is standard practice for shared libraries and position-independent code where the GP register cannot be relied upon.</p>
<p><code>-fPIC</code> generates position-independent code, required for shared objects on the DSP. The <code>-DQURT</code> and <code>-D__QURT</code> defines are preprocessor macros that QuRT headers and application code check with <code>#ifdef</code> to detect a QuRT build and enable RTOS-specific code paths.</p>
<p>The <code>LINKFLAGS</code> include <code>-nostdlib</code> because QuRT provides its own C runtime. The standard GNU C library (glibc) is built for Linux and would pull in Linux system calls that don't exist on the Hexagon DSP. QuRT provides its own versions of functions like <code>malloc</code>, <code>printf</code>, and <code>memcpy</code> that are implemented on top of the QuRT kernel.</p>
<p>The <code>LIBS</code> list specifies QuRT-specific libraries: <code>qurt</code> (the RTOS kernel interface, providing threading, mutexes, and memory management), <code>qcc</code> (Qualcomm C compiler runtime, providing low-level arithmetic helpers and compiler intrinsics), and <code>timer</code> (hardware timer access for profiling and delay functions).</p>
<p>The <code>ENV</code> dictionary controls what environment the child processes (compilers, linkers) see when SCons invokes them. The Hexagon tool binary directory is prepended to <code>PATH</code> so that tools can find each other (for example, <code>hexagon-clang</code> may internally invoke <code>hexagon-as</code> for assembly steps). <code>HEXAGON_SDK_ROOT</code> is passed through because some Hexagon tools reference it internally to locate standard headers and runtime libraries.</p>
<p>The <code>CCCOMSTR</code>, <code>CXXCOMSTR</code>, <code>LINKCOMSTR</code>, and <code>ARCOMSTR</code> variables customize the build output. Instead of printing the full compiler command line (which can be hundreds of characters long with all the flags and paths), SCons prints a short summary like <code>HEX-CXX build/qurt/lib/mathutils.o</code>. This makes it easy to see at a glance that you're using the cross-compiler, not the host compiler.</p>
<p>To see the full commands (useful for debugging), remove these four lines or run <code>scons</code> with <code>verbose=1</code> and add the corresponding check in the SConstruct.</p>
<p>Everything after the environment setup is identical to the native build: <code>Export</code>, <code>SConscript</code> calls with variant directories, and the same library and application SConscript files.</p>
<p>The SConscript files don't know or care whether they're building for the host or for QuRT. They just use whatever environment they receive through <code>Import('env')</code>. This separation is a key design advantage. Your build logic (what files to compile, what libraries to create) stays in the SConscript files. Your toolchain configuration stays in the SConstruct.</p>
<p>To build for QuRT, set the SDK path and run SCons.</p>
<pre><code class="language-bash">export HEXAGON_SDK_ROOT=/path/to/hexagon/sdk
scons
</code></pre>
<p>The output shows the Hexagon compiler being invoked instead of GCC.</p>
<pre><code class="language-plaintext">  HEX-CXX  build/qurt/lib/mathutils.o
  HEX-CXX  build/qurt/lib/stringutils.o
  HEX-AR   build/qurt/lib/libmyutils.a
  HEX-CXX  build/qurt/src/main.o
  HEX-CXX  build/qurt/src/app.o
  HEX-LINK build/qurt/src/myapp
</code></pre>
<p>Each line confirms that the Hexagon tools are running, not the host tools. The resulting <code>myapp</code> binary is a Hexagon executable. You can't run it directly on your development machine (it contains Hexagon instructions, not x86 or ARM). To test it, use the Hexagon simulator: <code>hexagon-sim build/qurt/src/myapp</code>.</p>
<p>To target a different Hexagon architecture, pass the <code>arch</code> argument.</p>
<pre><code class="language-bash">scons arch=v66
</code></pre>
<p>This changes the compiler flag to <code>-mv66</code> and selects the <code>computev66</code> QuRT headers and libraries. Everything else remains the same.</p>
<h2 id="heading-writing-qurt-specific-application-code">Writing QuRT-Specific Application Code</h2>
<p>Real QuRT applications use the RTOS API for threading, synchronization, and hardware interaction. The following example replaces the generic <code>main.cpp</code> with a QuRT-specific version that creates threads and uses a mutex.</p>
<pre><code class="language-cpp">// src/main_qurt.cpp
#include "app.h"
#include &lt;qurt.h&gt;
#include &lt;qurt_thread.h&gt;
#include &lt;qurt_mutex.h&gt;
#include &lt;stdio.h&gt;

#define STACK_SIZE 4096

static qurt_mutex_t print_mutex;
static char worker_stack[STACK_SIZE];

void worker_thread(void *arg) {
    int id = (int)(long)arg;
    qurt_mutex_lock(&amp;print_mutex);
    printf("Worker thread %d running on QuRT\n", id);
    run_app();
    qurt_mutex_unlock(&amp;print_mutex);
    qurt_thread_exit(0);
}

int main() {
    qurt_thread_t thread_id;
    qurt_thread_attr_t attr;

    qurt_mutex_init(&amp;print_mutex);

    qurt_thread_attr_init(&amp;attr);
    qurt_thread_attr_set_name(&amp;attr, "worker");
    qurt_thread_attr_set_stack_addr(&amp;attr, worker_stack);
    qurt_thread_attr_set_stack_size(&amp;attr, STACK_SIZE);
    qurt_thread_attr_set_priority(&amp;attr, 100);

    qurt_thread_create(&amp;thread_id, &amp;attr,
                       worker_thread, (void *)1);

    int status;
    qurt_thread_join(thread_id, &amp;status);

    qurt_mutex_destroy(&amp;print_mutex);
    return 0;
}
</code></pre>
<p>This code demonstrates the core QuRT threading API.</p>
<ul>
<li><p><code>qurt_mutex_init</code> initializes a mutex for synchronizing access to <code>printf</code> (which isn't thread-safe on QuRT without protection).</p>
</li>
<li><p><code>qurt_thread_attr_init</code> creates a thread attribute structure, and the subsequent calls configure the thread's name (visible in the debugger), stack memory (you provide the buffer, QuRT doesn't allocate it for you), stack size (4096 bytes is typical for lightweight threads), and priority (QuRT uses priority-based preemptive scheduling where lower numbers mean higher priority).</p>
</li>
<li><p><code>qurt_thread_create</code> spawns the thread, passing a function pointer and an argument.</p>
</li>
<li><p><code>qurt_thread_join</code> blocks until the thread completes, similar to <code>pthread_join</code>.</p>
</li>
<li><p><code>qurt_mutex_destroy</code> cleans up the mutex.</p>
</li>
</ul>
<p>Several differences from POSIX threading matter for correctness. On QuRT, you must provide the stack memory yourself as a statically allocated buffer (or dynamically allocated via <code>qurt_malloc</code>). The RTOS doesn't have a general-purpose <code>malloc</code>-like stack allocator the way Linux does. Thread priorities are explicit and mandatory – there's no default priority. And <code>qurt_thread_exit</code> must be called at the end of every thread function: falling off the end of the function without calling it is undefined behavior on QuRT.</p>
<p>To build with this QuRT-specific main instead of the generic one, modify the <code>src/SConscript</code> to select the right file:</p>
<pre><code class="language-python"># src/SConscript (QuRT-aware version)
Import('env')
Import('mylib')

import os
is_qurt = 'DQURT' in ' '.join(env.get('CCFLAGS', []))

main_src = 'main_qurt.cpp' if is_qurt else 'main.cpp'

app = env.Program(
    target='myapp',
    source=[main_src, 'app.cpp'],
    LIBS=[mylib, 'm'],
    LIBPATH=['#build/qurt/lib', '#build/release/lib', '#build/debug/lib'],
)

Return('app')
</code></pre>
<p>This SConscript inspects the environment's <code>CCFLAGS</code> to determine whether the QuRT preprocessor define is present. If it is, the build uses <code>main_qurt.cpp</code>. If not, it uses the standard <code>main.cpp</code>.</p>
<p>This is a simple example of using Python logic in a build file to adapt to different targets, something that requires convoluted syntax in Make and a separate toolchain file in CMake.</p>
<h2 id="heading-building-both-native-and-qurt-from-one-sconstruct">Building Both Native and QuRT From One SConstruct</h2>
<p>If you need both a native build (for running unit tests on your development machine) and a QuRT build (for deployment to the DSP), you can configure both in a single SConstruct.</p>
<pre><code class="language-python"># SConstruct (dual-target: native + QuRT)
import os
import sys

native_env = Environment(
    CPPPATH=['#include', '#lib'],
    CCFLAGS=['-Wall', '-std=c++17', '-O2'],
)

hexagon_sdk = os.environ.get('HEXAGON_SDK_ROOT', '')
build_qurt = os.path.isdir(hexagon_sdk)

if build_qurt:
    hexagon_tools = os.path.join(hexagon_sdk, 'tools', 'HEXAGON_Tools')
    hexagon_ver = os.environ.get('HEXAGON_TOOLS_VER', '8.8.06')
    tool_bin = os.path.join(hexagon_tools, hexagon_ver, 'Tools', 'bin')
    hexagon_arch = ARGUMENTS.get('arch', 'v73')
    qurt_root = os.path.join(hexagon_sdk, 'rtos', 'qurt')
    qurt_variant = 'compute' + hexagon_arch
    qurt_inc = os.path.join(qurt_root, qurt_variant, 'include')
    qurt_lib = os.path.join(qurt_root, qurt_variant, 'lib')

    qurt_env = Environment(
        CC=os.path.join(tool_bin, 'hexagon-clang'),
        CXX=os.path.join(tool_bin, 'hexagon-clang++'),
        AR=os.path.join(tool_bin, 'hexagon-ar'),
        RANLIB=os.path.join(tool_bin, 'hexagon-ranlib'),
        LINK=os.path.join(tool_bin, 'hexagon-clang++'),
        CPPPATH=['#include', '#lib', qurt_inc,
                 os.path.join(qurt_inc, 'posix')],
        CCFLAGS=['-m' + hexagon_arch, '-G0', '-Wall',
                 '-O2', '-fPIC', '-DQURT', '-D__QURT'],
        LINKFLAGS=['-m' + hexagon_arch, '-G0', '-nostdlib'],
        LIBPATH=[qurt_lib],
        LIBS=['qurt', 'qcc', 'timer'],
        ENV={'PATH': tool_bin + ':' + os.environ.get('PATH', ''),
             'HEXAGON_SDK_ROOT': hexagon_sdk},
    )
    qurt_env['CXXCOMSTR'] = '  HEX-CXX  $TARGET'
    qurt_env['LINKCOMSTR'] = '  HEX-LINK $TARGET'
    qurt_env['ARCOMSTR'] = '  HEX-AR   $TARGET'

native_lib = SConscript('lib/SConscript',
                        variant_dir='build/native/lib',
                        duplicate=0,
                        exports={'env': native_env})
SConscript('src/SConscript',
           variant_dir='build/native/src',
           duplicate=0,
           exports={'env': native_env, 'mylib': native_lib})

if build_qurt:
    qurt_lib_node = SConscript('lib/SConscript',
                               variant_dir='build/qurt/lib',
                               duplicate=0,
                               exports={'env': qurt_env})
    SConscript('src/SConscript',
               variant_dir='build/qurt/src',
               duplicate=0,
               exports={'env': qurt_env, 'mylib': qurt_lib_node})
</code></pre>
<p>Each <code>SConscript</code> call passes a different environment through the <code>exports</code> parameter. The SConscript files themselves remain completely unchanged from the single-target version. SCons executes both variants in a single invocation and correctly handles dependencies between them. The native build always runs. The QuRT build runs only when <code>HEXAGON_SDK_ROOT</code> points to a valid directory. This means developers who don't have the Hexagon SDK installed can still build and test the native version without errors.</p>
<p>This pattern shows why Python build files are powerful. Conditional logic, environment detection, path validation, and multi-target builds all use standard Python constructs. There's no special cross-compilation syntax to learn, no separate toolchain file format, and no need to run the build tool twice with different arguments.</p>
<h2 id="heading-how-scons-detects-dependencies-and-decides-what-to-rebuild">How SCons Detects Dependencies and Decides What to Rebuild</h2>
<p>SCons ships with built-in scanners for C/C++ (<code>#include</code> directives), Fortran (<code>INCLUDE</code> and <code>USE</code> statements), Java (<code>import</code> statements), D (<code>import</code> statements), and LaTeX (<code>\include</code> and <code>\input</code> commands).</p>
<p>When SCons compiles <code>app.cpp</code>, it reads the file, finds <code>#include "config.h"</code>, <code>#include "mathutils.h"</code>, and the other includes, resolves them against the <code>CPPPATH</code> search path, and automatically adds those headers to the dependency graph.</p>
<p>If you change <code>mathutils.h</code>, SCons knows to recompile <code>app.cpp</code> even though you didn't list that dependency anywhere. Make requires you to set this up manually or use a tool like <code>gcc -MM</code> to generate dependency files, and if you forget, your build produces incorrect results silently.</p>
<p>The default rebuild strategy uses content hashing. SCons computes an MD5 hash of every source file and stores it in a database file called <code>.sconsign.dblite</code> in the project root. On the next build, it recomputes hashes and compares. If the hash hasn't changed, the file isn't rebuilt.</p>
<p>This extends to the build outputs themselves: if recompiling a <code>.cpp</code> file produces an identical <code>.o</code> file (for example, because you only changed a comment), SCons won't re-link the final executable.</p>
<p>This "short-circuiting" behavior can save significant time on large projects where a header change triggers recompilation of many files but only a few actually produce different object code.</p>
<p>The <code>.sconsign.dblite</code> file stores more than just content hashes. It records the full build signature for each target: the content hashes of all source files, the compiler command line (including all flags), and the implicit dependencies discovered by scanners. If you change a compiler flag (for example, switching from <code>-O2</code> to <code>-O3</code>), SCons detects that the build signature has changed and recompiles everything, even though no source files changed. Make can't do this because it only tracks file timestamps.</p>
<p>You can change the rebuild strategy with the <code>Decider</code> function:</p>
<pre><code class="language-python">Decider('content')            # Default: MD5 hash comparison
Decider('timestamp-newer')    # Make-like: rebuild if source is newer
Decider('timestamp-match')    # Rebuild if timestamp changed at all
Decider('content-timestamp')  # Hybrid: only hash if timestamp changed
</code></pre>
<p><code>'content'</code> is the default and the most correct. It reads every source file on every build to compute hashes, which is thorough but adds I/O overhead.</p>
<p><code>'timestamp-newer'</code> mimics Make's behavior: rebuild if the source file's modification time is newer than the target's. This is fast but misses cases where a file is restored from backup (older timestamp, different content).</p>
<p><code>'timestamp-match'</code> rebuilds if the timestamp has changed in either direction, which handles the restore case.</p>
<p><code>'content-timestamp'</code> is the best hybrid: it only reads file contents (to compute hashes) when the timestamp has changed, skipping the I/O for files that haven't been touched. On projects with thousands of source files, this can cut SCons' startup overhead noticeably.</p>
<p>You can also change the hash algorithm:</p>
<pre><code class="language-python">SetOption('hash_format', 'sha256')
</code></pre>
<p>This switches from MD5 to SHA-256. MD5 is not collision-resistant for adversarial inputs, but for build system purposes (detecting accidental changes to source files), it's perfectly adequate. SHA-256 is an option for environments with strict compliance requirements.</p>
<p>You can write a custom decider function for specialized rebuild logic:</p>
<pre><code class="language-python">def my_decider(dependency, target, prev_ni, repo_node=None):
    return dependency.get_timestamp() != prev_ni.timestamp

env.Decider(my_decider)
</code></pre>
<p>The custom decider receives the dependency node, the target node, and the "node info" from the previous build. It returns <code>True</code> to trigger a rebuild or <code>False</code> to skip. This is useful for exotic scenarios like triggering rebuilds based on external state (database versions, API schemas) that aren't captured by file content.</p>
<h2 id="heading-writing-a-custom-scanner">Writing a Custom Scanner</h2>
<p>If your project uses a file format that includes other files (similar to C's <code>#include</code>), you can write a custom scanner so SCons tracks those dependencies automatically.</p>
<p>Consider a custom configuration file format where <code>@import filename.cfg</code> includes another file:</p>
<pre><code class="language-python">import re

import_re = re.compile(r'^@import\s+(\S+)', re.MULTILINE)

def cfg_scan(node, env, path):
    contents = node.get_text_contents()
    includes = import_re.findall(contents)
    return [env.File(f) for f in includes]

cfg_scanner = Scanner(
    function=cfg_scan,
    skeys=['.cfg'],
    recursive=True,
)

env.Append(SCANNERS=cfg_scanner)
</code></pre>
<p>The <code>cfg_scan</code> function reads the file contents, finds all <code>@import</code> directives using a regular expression, and returns a list of File nodes representing the imported files.</p>
<p>The <code>skeys</code> parameter tells SCons to apply this scanner to files with the <code>.cfg</code> extension.</p>
<p>The <code>recursive=True</code> parameter tells SCons to scan the imported files as well, so transitive dependencies are tracked. After appending the scanner to the environment, any builder that processes <code>.cfg</code> files will automatically detect and track <code>@import</code> dependencies.</p>
<h2 id="heading-the-shared-build-cache">The Shared Build Cache</h2>
<p>SCons supports <code>CacheDir</code>, a shared build cache that stores compiled artifacts indexed by their build signature (a hash incorporating the source content, compiler command, and flags). If another developer on your team has already built an identical configuration, you get the cached result instead of recompiling.</p>
<pre><code class="language-python">CacheDir('/shared/network/build_cache')
</code></pre>
<p>This line is all you need to enable caching. When SCons builds a file, it stores a copy in the cache directory, named by the build signature hash. On subsequent builds (by you or anyone else pointing to the same cache), if the build signature matches, the cached file is copied into the build directory instead of running the compiler. This works like ccache but applies to any build artifact, not just compiled objects. Libraries, executables, generated code, and any other builder output can be cached.</p>
<p>The build signature is comprehensive. It incorporates the content hashes of all source files, the full compiler command line (including flags), and the tool version. Different compiler flags produce different cache entries, so debug and release builds don't interfere with each other. If two developers use the same compiler version and the same flags on the same source code, they share cache hits.</p>
<p>Several command-line flags control cache behavior:</p>
<pre><code class="language-bash">scons --cache-show       # Show what command would have run for cached targets
scons --cache-disable    # Ignore cache for this run
scons --cache-readonly   # Read from cache but do not write new entries
scons --cache-force      # Update cache even if target is up to date
</code></pre>
<p><code>--cache-show</code> is useful for debugging. When a target is retrieved from cache, SCons normally prints nothing (or a short message). With <code>--cache-show</code>, it prints the command that would have been executed, so you can verify the cached entry matches your expectations.</p>
<p><code>--cache-readonly</code> is useful for CI systems that should consume cache entries built by developers but not pollute the cache with CI-specific configurations.</p>
<h2 id="heading-working-with-shared-libraries">Working with Shared Libraries</h2>
<p>Building shared libraries (<code>.so</code> on Linux, <code>.dylib</code> on macOS, <code>.dll</code> on Windows) requires different compiler and linker flags than static libraries. SCons handles most of this automatically through the <code>SharedLibrary</code> builder.</p>
<pre><code class="language-python">env = Environment()
shared_lib = env.SharedLibrary('myutils', [
    'mathutils.cpp',
    'stringutils.cpp',
])
</code></pre>
<p>On Linux, this produces <code>libmyutils.so</code>. SCons automatically adds <code>-fPIC</code> to the compilation flags for source files that go into a shared library (it uses <code>SharedObject</code> internally instead of <code>StaticObject</code>). On Windows, it produces <code>myutils.dll</code> plus <code>myutils.lib</code> (the import library).</p>
<p>For versioned shared libraries on POSIX systems, use the <code>SHLIBVERSION</code> parameter:</p>
<pre><code class="language-python">shared_lib = env.SharedLibrary('myutils', sources,
                                SHLIBVERSION='1.2.3')
</code></pre>
<p>This produces three files: <code>libmyutils.so.1.2.3</code> (the actual library), <code>libmyutils.so.1</code> (the soname symlink used at runtime), and <code>libmyutils.so</code> (the development symlink used at link time). SCons creates all three and manages the symlinks.</p>
<p>You can't mix <code>StaticObject</code> and <code>SharedObject</code> files. If you compile a file with <code>env.Object()</code> (which creates a static object without <code>-fPIC</code>), you can't put it into a <code>SharedLibrary</code>. SCons enforces this and produces an error if you try. If you need the same source file compiled both ways, call each builder separately.</p>
<pre><code class="language-python">static_objs = [env.StaticObject(f) for f in sources]
shared_objs = [env.SharedObject(f) for f in sources]

static_lib = env.StaticLibrary('myutils', static_objs)
shared_lib = env.SharedLibrary('myutils', shared_objs)
</code></pre>
<p>Each source file gets compiled twice: once without <code>-fPIC</code> for the static library, once with <code>-fPIC</code> for the shared library. The resulting object files have different names (SCons appends different suffixes) so they don't collide.</p>
<h2 id="heading-adding-command-line-options-with-addoption">Adding Command-Line Options with AddOption</h2>
<p>The <code>ARGUMENTS</code> dictionary works for simple key=value pairs, but for more complex command-line interfaces (flags like <code>--prefix</code>, <code>--enable-feature</code>, or <code>--with-library</code>), use <code>AddOption</code>.</p>
<pre><code class="language-python">AddOption('--prefix',
    dest='prefix',
    type='string',
    nargs=1,
    action='store',
    metavar='DIR',
    default='/usr/local',
    help='Installation prefix (default: /usr/local)')

AddOption('--enable-tests',
    dest='enable_tests',
    action='store_true',
    default=False,
    help='Build and run unit tests')

prefix = GetOption('prefix')
build_tests = GetOption('enable_tests')

env = Environment(PREFIX=prefix)

app = env.Program('myapp', sources)
env.Install(os.path.join(prefix, 'bin'), app)

if build_tests:
    test_env = env.Clone()
    test_env.Program('test_runner', test_sources)
</code></pre>
<p><code>AddOption</code> uses Python's <code>optparse</code> module under the hood, so the parameter names (<code>dest</code>, <code>type</code>, <code>action</code>, <code>metavar</code>, <code>default</code>, <code>help</code>) follow the same conventions. <code>GetOption</code> retrieves the parsed value. These options appear in <code>scons --help</code> output alongside SCons' built-in options, giving users a clean command-line interface.</p>
<p>Running <code>scons --prefix=/opt/myapp --enable-tests</code> installs to <code>/opt/myapp/bin</code> and builds the test suite. Running <code>scons --help</code> shows all available options with their descriptions.</p>
<p>The advantage over <code>ARGUMENTS</code> is discoverability. <code>ARGUMENTS</code> requires the user to know which key=value pairs your build file accepts. <code>AddOption</code> makes them visible in <code>--help</code> output and provides type checking and default values.</p>
<h2 id="heading-configure-checks-for-portability">Configure Checks for Portability</h2>
<p>SCons includes an autoconf-like system for probing the build environment. You can check for headers, libraries, functions, and type sizes before building.</p>
<pre><code class="language-python">env = Environment()
conf = Configure(env)

if not conf.CheckCHeader('math.h'):
    print('Error: math.h not found')
    Exit(1)

if not conf.CheckCXXHeader('iostream'):
    print('Error: C++ standard library headers not found')
    Exit(1)

if not conf.CheckLib('pthread', language='C'):
    print('Error: pthread library not found')
    Exit(1)

if conf.CheckFunc('posix_memalign'):
    conf.env.Append(CPPDEFINES=['HAVE_POSIX_MEMALIGN'])

if conf.CheckFunc('aligned_alloc'):
    conf.env.Append(CPPDEFINES=['HAVE_ALIGNED_ALLOC'])

if conf.CheckTypeSize('long') == 8:
    conf.env.Append(CPPDEFINES=['HAVE_64BIT_LONG'])

env = conf.Finish()
</code></pre>
<p><code>Configure()</code> creates a configuration context that compiles and links small test programs behind the scenes to determine whether headers exist, libraries can be linked, and functions are available. Each <code>Check</code> method writes a tiny C or C++ program, compiles it with the current environment settings, and returns <code>True</code> or <code>False</code> based on whether compilation and linking succeeded. <code>conf.Finish()</code> returns the (possibly modified) environment and cleans up.</p>
<p><code>CheckCHeader</code> verifies that a C header can be included. <code>CheckCXXHeader</code> does the same for C++ headers. <code>CheckLib</code> verifies that a library can be linked; the <code>language</code> parameter determines whether to use the C or C++ compiler for the test. <code>CheckFunc</code> checks whether a function is available (it creates a test program that references the function and attempts to link it). <code>CheckTypeSize</code> compiles a program that uses <code>sizeof()</code> and returns the size as an integer.</p>
<p>The <code>CPPDEFINES</code> added by the checks (like <code>HAVE_POSIX_MEMALIGN</code>) follow the standard autoconf convention. Your source code can then use these defines:</p>
<pre><code class="language-cpp">#ifdef HAVE_POSIX_MEMALIGN
    posix_memalign(&amp;ptr, alignment, size);
#elif defined(HAVE_ALIGNED_ALLOC)
    ptr = aligned_alloc(alignment, size);
#else
    ptr = malloc(size);
#endif
</code></pre>
<p>This pattern makes your code portable across systems that may or may not have specific functions, without hardcoding platform assumptions.</p>
<p>Configure checks are cached in <code>.sconf_temp/</code> and <code>.sconsign.dblite</code>. On subsequent builds, if the environment hasn't changed, SCons skips the checks and uses the cached results. You can force rechecking with <code>scons --config=force</code>.</p>
<h2 id="heading-custom-builders-for-non-standard-file-types">Custom Builders for Non-Standard File Types</h2>
<p>You can define builders for file types that SCons doesn't know about. A builder wraps a shell command (or a Python function) with source/target suffix handling.</p>
<h3 id="heading-builder-with-an-external-command">Builder with an External Command</h3>
<pre><code class="language-python">protobuf = Builder(
    action='protoc --cpp_out=\(TARGET.dir \)SOURCE',
    suffix='.pb.cc',
    src_suffix='.proto',
)
env.Append(BUILDERS={'Protobuf': protobuf})
env.Protobuf('messages.proto')
</code></pre>
<p>This creates a <code>Protobuf</code> builder that runs <code>protoc</code> on <code>.proto</code> files and produces <code>.pb.cc</code> files. The <code>action</code> string uses SCons variable substitution: <code>\(SOURCE</code> expands to the input file path and <code>\)TARGET.dir</code> expands to the directory of the output file. The <code>suffix</code> and <code>src_suffix</code> parameters let SCons infer target and source file names automatically. After appending the builder to the environment, you call <code>env.Protobuf('messages.proto')</code> and SCons produces <code>messages.pb.cc</code>.</p>
<p>The critical detail: use <code>env.Append(BUILDERS={...})</code> to add your builder. If you set <code>BUILDERS</code> directly in the <code>Environment()</code> constructor, like <code>Environment(BUILDERS={'Protobuf': protobuf})</code>, you overwrite the entire builder dictionary and lose all the default builders (Program, Library, Object, and so on).</p>
<h3 id="heading-builder-with-a-python-function">Builder with a Python Function</h3>
<pre><code class="language-python">def generate_version_header(target, source, env):
    version = env.get('APP_VERSION', '0.0.0')
    with open(str(target[0]), 'w') as f:
        f.write('#ifndef VERSION_H\n')
        f.write('#define VERSION_H\n')
        f.write('#define VERSION "%s"\n' % version)
        f.write('#endif\n')
    return 0

version_builder = Builder(action=generate_version_header,
                           suffix='.h',
                           src_suffix='.ver')
env.Append(BUILDERS={'VersionHeader': version_builder})
env.VersionHeader('version.h', 'version.ver',
                  APP_VERSION='2.1.0')
</code></pre>
<p>The Python function receives three arguments: <code>target</code> (a list of target Node objects), <code>source</code> (a list of source Node objects), and <code>env</code> (the construction environment). Node objects must be converted to strings with <code>str()</code> to get the file path. The function must return 0 for success or a non-zero value for failure.</p>
<p>Using a Python function instead of a shell command is useful when the build step involves logic that is awkward to express in shell (like reading a file, parsing JSON, or generating code with complex structure).</p>
<h3 id="heading-the-command-builder-for-one-off-rules">The Command Builder for One-Off Rules</h3>
<p>For build rules that are used only once, the <code>Command</code> builder avoids the overhead of defining a named builder.</p>
<pre><code class="language-python">env.Command('config.h', 'config.h.in',
            "sed 's/@VERSION@/1.0.0/g' &lt; \(SOURCE &gt; \)TARGET")
</code></pre>
<p>This runs <code>sed</code> to substitute a version placeholder in <code>config.h.in</code> and writes the result to <code>config.h</code>. The <code>Command</code> builder is the SCons equivalent of a Make rule with a custom recipe. It takes the target, source, and action as arguments. The action can be a shell command string, a Python function, or a list of either.</p>
<h2 id="heading-aliases-default-targets-and-install-rules">Aliases, Default Targets, and Install Rules</h2>
<p><code>env.Alias()</code> creates named targets you can invoke from the command line. <code>Default()</code> specifies what gets built when you run <code>scons</code> with no arguments.</p>
<pre><code class="language-python">app = env.Program('myapp', sources)
tests = env.Program('test_runner', test_sources)

Default(app)
env.Alias('test', tests)
env.Alias('all', [app, tests])
</code></pre>
<p>Running <code>scons</code> builds only <code>myapp</code> because it's the default target. Running <code>scons test</code> builds the test executable. Running <code>scons all</code> builds everything. Without the <code>Default</code> call, SCons builds everything in the current directory and below, which includes both the application and the tests.</p>
<p>Install targets copy built files to a destination directory.</p>
<pre><code class="language-python">env.Install('/usr/local/bin', app)
env.Install('/usr/local/lib', shared_lib)
env.InstallAs('/usr/local/bin/my-application', app)

env.Alias('install', '/usr/local/bin')
env.Alias('install', '/usr/local/lib')
</code></pre>
<p><code>env.Install()</code> copies the specified file to the destination directory. <code>env.InstallAs()</code> copies it with a different name. Install targets aren't built by default because they write outside the project tree. You must invoke them explicitly with <code>scons install</code> (which works because the Alias connects the name "install" to the install directories).</p>
<p>You can combine Alias with a command action to create a "run" target.</p>
<pre><code class="language-python">env.Alias('run', app, './build/release/src/myapp')
</code></pre>
<p>Running <code>scons run</code> builds the application (if needed) and then executes it. The third argument to <code>Alias</code> is an action that runs after the target is built.</p>
<h2 id="heading-platform-specific-configuration">Platform-Specific Configuration</h2>
<p>Because SConstruct files are Python, platform-specific configuration uses standard Python constructs.</p>
<pre><code class="language-python">import sys
import os

env = Environment(
    CPPPATH=['#include'],
    CCFLAGS=['-Wall'],
)

if sys.platform == 'win32':
    env.Append(LIBS=['ws2_32', 'advapi32'])
    env.Append(CPPDEFINES=['_WIN32', 'NOMINMAX'])
elif sys.platform == 'darwin':
    env.Append(FRAMEWORKS=['CoreFoundation', 'Security'])
    env.Append(CCFLAGS=['-mmacosx-version-min=10.15'])
elif sys.platform.startswith('linux'):
    env.Append(LIBS=['pthread', 'dl', 'rt'])
    env.Append(CPPDEFINES=['_GNU_SOURCE'])
</code></pre>
<p><code>sys.platform</code> returns <code>'win32'</code> on Windows, <code>'darwin'</code> on macOS, and <code>'linux'</code> on Linux. The <code>FRAMEWORKS</code> variable is macOS-specific and translates to <code>-framework CoreFoundation -framework Security</code> on the linker command line. On Linux, <code>-lrt</code> links the POSIX realtime library (for <code>clock_gettime</code> on older glibc versions), and <code>-ldl</code> links the dynamic loading library (for <code>dlopen</code>).</p>
<p>For more granular detection, use <code>platform.machine()</code> to check the CPU architecture.</p>
<pre><code class="language-python">import platform

if platform.machine() == 'aarch64':
    env.Append(CCFLAGS=['-march=armv8-a'])
elif platform.machine() == 'x86_64':
    env.Append(CCFLAGS=['-march=x86-64-v2'])
</code></pre>
<p>You can also use <code>env['PLATFORM']</code> which SCons sets to <code>'posix'</code>, <code>'win32'</code>, or <code>'darwin'</code>.</p>
<p>For integrating with system libraries that provide <code>pkg-config</code> metadata, use <code>ParseConfig</code>.</p>
<pre><code class="language-python">env.ParseConfig('pkg-config --cflags --libs libpng')
env.ParseConfig('pkg-config --cflags --libs zlib')
</code></pre>
<p><code>ParseConfig</code> runs the specified command, captures its output, and parses the flags into the appropriate construction variables. <code>-I</code> flags go into <code>CPPPATH</code>, <code>-L</code> flags go into <code>LIBPATH</code>, <code>-l</code> flags go into <code>LIBS</code>, and remaining flags go into <code>CCFLAGS</code>. This is the SCons equivalent of <code>$(pkg-config --cflags --libs libpng)</code> in a Makefile.</p>
<h2 id="heading-customizing-build-output">Customizing Build Output</h2>
<p>By default, SCons prints the full compiler command line for every file it processes. On projects with long include paths and many flags, this produces walls of text that obscure the build progress. You can customize the output with <code>COMSTR</code> variables:</p>
<pre><code class="language-python">env = Environment()

env['CCCOMSTR'] = '  CC    $TARGET'
env['CXXCOMSTR'] = '  CXX   $TARGET'
env['LINKCOMSTR'] = '  LINK  $TARGET'
env['ARCOMSTR'] = '  AR    $TARGET'
env['SHCCCOMSTR'] = '  CC    $TARGET (shared)'
env['SHCXXCOMSTR'] = '  CXX   $TARGET (shared)'
env['SHLINKCOMSTR'] = '  LINK  $TARGET (shared)'
env['RANLIBCOMSTR'] = '  INDEX $TARGET'
env['INSTALLSTR'] = '  INST  $TARGET'
</code></pre>
<p>With these settings, the build output looks clean and scannable. Each line shows the action type and the target file. The <code>$TARGET</code> variable in the string is expanded by SCons at runtime.</p>
<p>To support both quiet and verbose modes, check a command-line argument.</p>
<pre><code class="language-python">if ARGUMENTS.get('verbose', '0') != '1':
    env['CCCOMSTR'] = '  CC    $TARGET'
    env['CXXCOMSTR'] = '  CXX   $TARGET'
    env['LINKCOMSTR'] = '  LINK  $TARGET'
    env['ARCOMSTR'] = '  AR    $TARGET'
</code></pre>
<p>Running <code>scons</code> shows the short output. Running <code>scons verbose=1</code> shows the full command lines. This pattern is common in SCons projects and mimics the <code>V=1</code> convention used by the Linux kernel's build system.</p>
<h2 id="heading-how-to-debug-scons-build-files">How to Debug SCons Build Files</h2>
<p>When a build doesn't do what you expect, SCons provides several debugging tools.</p>
<h3 id="heading-print-variables">Print Variables</h3>
<p>Because SConstruct files are Python, you can print anything.</p>
<pre><code class="language-python">env = Environment(CCFLAGS=['-Wall', '-O2'])
print('CCFLAGS:', env['CCFLAGS'])
print('CC:', env['CC'])
print('CPPPATH:', env.get('CPPPATH', []))
</code></pre>
<p>This prints the current values of construction variables. Use this to verify that your flags are set correctly, especially after <code>Append</code>, <code>Prepend</code>, or <code>Clone</code> calls.</p>
<h3 id="heading-the-debug-flag">The <code>--debug</code> flag</h3>
<p>SCons has a <code>--debug</code> option with several modes.</p>
<pre><code class="language-bash">scons --debug=explain
</code></pre>
<p>This tells SCons to print the reason for every rebuild. Instead of silently recompiling a file, it prints something like <code>scons: rebuilding 'build/release/lib/mathutils.o' because 'lib/mathutils.h' changed</code>. This is invaluable for understanding unexpected rebuilds.</p>
<pre><code class="language-bash">scons --debug=tree
</code></pre>
<p>This prints the full dependency tree for every target, showing which files depend on which other files. The output can be large, so combine it with a specific target: <code>scons --debug=tree build/release/src/myapp</code>.</p>
<pre><code class="language-bash">scons --debug=includes
</code></pre>
<p>This prints the include files found by the C/C++ scanner for each source file. Useful for diagnosing "header not found" errors or unexpected include paths.</p>
<pre><code class="language-bash">scons --debug=presub
</code></pre>
<p>This prints the un-substituted command line (with <code>\(CC</code>, <code>\)CCFLAGS</code>, and so on still as variable names) before SCons expands them. Helps you understand which variables contribute to the final command.</p>
<h3 id="heading-the-dry-run-flag">The <code>--dry-run</code> flag</h3>
<p><code>scons -n</code> shows what SCons would do without actually doing it. Every command that would be executed is printed, but no files are created or modified. This is a safe way to verify your build logic before running it.</p>
<h3 id="heading-the-dump-method">The <code>Dump</code> method</h3>
<p><code>env.Dump()</code> returns a formatted string of every construction variable and its value. It produces a lot of output, so pipe it to a file or search for specific variables.</p>
<pre><code class="language-python">print(env.Dump())
</code></pre>
<p>This is the nuclear option for debugging: it shows everything SCons knows about the environment.</p>
<h2 id="heading-the-scons-command-line-reference">The SCons Command-Line Reference</h2>
<p>SCons accepts many command-line options. The ones you will use most frequently are listed here.</p>
<ul>
<li><p><code>scons</code> builds the default targets (or everything if no <code>Default()</code> is set).</p>
</li>
<li><p><code>scons -j N</code> runs up to N build commands in parallel. Set N to the number of CPU cores on your machine for fastest builds. You can also set this in the SConstruct with <code>SetOption('num_jobs', 4)</code>.</p>
</li>
<li><p><code>scons -c</code> cleans (removes) all built targets. This is the equivalent of <code>make clean</code> but doesn't require you to write a clean rule. SCons knows exactly which files it created and removes only those.</p>
</li>
<li><p><code>scons -n</code> is a dry run. Shows what would be built without building anything.</p>
</li>
<li><p><code>scons -Q</code> suppresses SCons' status messages ("Reading SConscript files", "Building targets", etc.) and shows only the build commands. Useful for piping build output to other tools.</p>
</li>
<li><p><code>scons -s</code> is silent mode. Suppresses both status messages and build commands. Only errors are printed.</p>
</li>
<li><p><code>scons --debug=explain</code> explains why each target is being rebuilt.</p>
</li>
<li><p><code>scons --debug=tree</code> prints the dependency tree.</p>
</li>
<li><p><code>scons --config=force</code> forces re-running of all Configure checks, ignoring cached results.</p>
</li>
<li><p><code>scons target_name</code> builds only the specified target and its dependencies. You can specify multiple targets: <code>scons myapp test_runner</code>.</p>
</li>
<li><p><code>scons key=value</code> passes a key-value pair accessible through <code>ARGUMENTS.get('key')</code> in the SConstruct.</p>
</li>
<li><p><code>scons --help</code> shows SCons' built-in options plus any options added with <code>AddOption</code> in the SConstruct.</p>
</li>
</ul>
<h2 id="heading-common-mistakes-and-how-to-avoid-them">Common Mistakes and How to Avoid Them</h2>
<p><strong>Overwriting default builders:</strong> Passing <code>BUILDERS</code> as a keyword argument to <code>Environment()</code> replaces the entire builder dictionary. You lose <code>Program</code>, <code>Library</code>, <code>Object</code>, and everything else. Always add custom builders with <code>env.Append(BUILDERS={'Name': builder})</code>.</p>
<p><strong>Assuming shell environment variables are available:</strong> SCons deliberately doesn't import your shell environment. If your build fails because a tool isn't found, you probably need to pass <code>PATH</code> through explicitly.</p>
<p>The safest approach for finding the compiler is <code>env['ENV']['PATH'] = os.environ['PATH']</code>. Importing the entire environment with <code>ENV=os.environ.copy()</code> works but reduces build reproducibility because your build now depends on every variable in your shell.</p>
<p><strong>Modifying a shared environment in a SConscript file:</strong> If the SConstruct exports one environment and multiple SConscript files import it, any <code>Append</code> or modification in one SConscript affects all of them because they all hold a reference to the same Python object. Clone the environment first with <code>local_env = env.Clone()</code> and modify the clone. The clone is a deep copy that can be modified independently.</p>
<p><strong>Forgetting Return() in SConscript:</strong> If your SConstruct calls <code>lib = SConscript('lib/SConscript')</code> and the SConscript file has no <code>Return()</code> statement, <code>lib</code> is <code>None</code>. You'll get a confusing error later when you try to link against it, typically something like <code>TypeError: expected a string or list of strings</code> when <code>None</code> is passed as a library.</p>
<p><strong>Confusing variant_dir with source paths:</strong> When you use <code>variant_dir</code>, the source file paths in your SConscript are still relative to the SConscript's original location, not the variant directory.</p>
<p>SCons handles the mapping internally. Don't use paths into the build directory in your SConscript files. Writing <code>Object('build/release/lib/mathutils.cpp')</code> is wrong, while writing <code>Object('mathutils.cpp')</code> inside <code>lib/SConscript</code> is correct.</p>
<p><strong>Forgetting to add .sconsign.dblite to .gitignore:</strong> SCons stores its dependency database in this file. It should never be committed to version control because it contains absolute paths and machine-specific data.</p>
<p>Add <code>.sconsign.dblite</code>, the <code>build/</code> directory, and the <code>.sconf_temp/</code> directory (created by Configure checks) to your <code>.gitignore</code>.</p>
<pre><code class="language-plaintext"># .gitignore
.sconsign.dblite
.sconf_temp/
build/
</code></pre>
<p>This <code>.gitignore</code> file has three entries.</p>
<ul>
<li><p><code>.sconsign.dblite</code> is the dependency database.</p>
</li>
<li><p><code>.sconf_temp/</code> is the directory where Configure check test programs are compiled.</p>
</li>
<li><p><code>build/</code> is the variant directory containing all compiled artifacts.</p>
</li>
</ul>
<p><strong>Expecting</strong> <code>touch</code> <strong>to trigger a rebuild:</strong> SCons uses content hashing by default. Running <code>touch</code> on a source file changes its modification time but not its content, so the hash is identical and SCons doesn't rebuild. If you need Make-like timestamp behavior, call <code>Decider('timestamp-newer')</code> in your SConstruct.</p>
<p><strong>Using string file names instead of Nodes:</strong> Passing raw strings with platform-specific extensions makes your build files non-portable.</p>
<pre><code class="language-python"># Fragile: hardcodes the .o extension
Program('myapp', ['main.o', 'utils.o'])
</code></pre>
<pre><code class="language-python"># Portable: let SCons handle extensions
main_obj = env.Object('main.cpp')
utils_obj = env.Object('utils.cpp')
env.Program('myapp', [main_obj, utils_obj])
</code></pre>
<p>The first version breaks on Windows where object files use the <code>.obj</code> extension. The second version works everywhere because the Node objects carry platform-specific metadata.</p>
<p><strong>Getting the target/source argument order wrong:</strong> Builder methods take the target first, then the source. <code>Program('output_name', 'source.c')</code> is correct. <code>Program('source.c', 'output_name')</code> compiles <code>output_name</code> (which doesn't exist) and tries to create <code>source.c</code> as the executable. The convention mimics assignment: target = source.</p>
<p><strong>Expecting Install targets to build by default:</strong> <code>env.Install('/usr/local/bin', app)</code> creates an install target, but SCons does not build it unless you explicitly request it. Targets outside the project directory tree are never default targets. Use <code>env.Alias('install', '/usr/local/bin')</code> and run <code>scons install</code> to trigger the installation.</p>
<p><strong>Using Glob without understanding it returns Nodes:</strong> <code>Glob('*.cpp')</code> returns a list of Node objects, not strings. You can concatenate them with other Node lists using <code>+</code>, pass them to builders, and use them in most places that accept source lists. You can't call string methods on them directly. Use <code>[str(n) for n in Glob('*.cpp')]</code> if you need strings, but prefer working with Nodes whenever possible.</p>
<h2 id="heading-summary">Summary</h2>
<p>SCons replaces Make with a build system where every configuration file is a Python script.</p>
<p>The <code>Environment</code> object holds your compiler, flags, and paths. Builders like <code>Program</code>, <code>StaticLibrary</code>, and <code>SharedLibrary</code> know how to produce specific output types. <code>SConscript</code> files organize multi-directory projects, and <code>variant_dir</code> keeps build artifacts separate from source code. Content hashing eliminates unnecessary rebuilds, and automatic header scanning removes the need to manually specify implicit dependencies.</p>
<p>Cross-compilation to targets like QuRT requires nothing more than pointing the environment's tool variables (<code>CC</code>, <code>CXX</code>, <code>LINK</code>) at the cross-compiler and adding the target's include paths and libraries. The same SConscript files work for both native and cross-compiled builds because they operate on whatever environment they receive through <code>Import</code>.</p>
<p>QuRT-specific features (threading, mutexes, hardware timers) are accessed through standard C function calls, and the build system's only responsibility is making sure the right compiler, headers, and libraries are in place.</p>
<p>The Configure subsystem replaces autoconf for probing the build environment. Custom builders extend SCons to handle file types it does not know about (protocol buffers, shaders, firmware images).</p>
<p>Aliases and install rules give users a clean command-line interface (<code>scons</code>, <code>scons test</code>, <code>scons install</code>). And the <code>--debug=explain</code> flag tells you exactly why any file is being rebuilt, eliminating the guesswork that plagues Make-based builds.</p>
<p>SCons isn't the fastest build tool for very large codebases, and its ecosystem is smaller than CMake's. But for projects where build file clarity, correctness, cross-compilation flexibility, and the ability to express complex logic in a real programming language matter more than raw speed, it's a strong choice.</p>
<p>The Python foundation means you already know the language, and the content-based rebuild strategy means you can trust that what gets built actually needs to be built.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ QuRT: The Real-Time OS Inside Your Phone's Processor [Full Handbook] ]]>
                </title>
                <description>
                    <![CDATA[ The Hexagon DSP in every Qualcomm-powered phone handles wake word detection, sensor processing, noise cancellation, and Bluetooth audio streaming – all while the main ARM CPU runs Android. The operati ]]>
                </description>
                <link>https://www.freecodecamp.org/news/qurt-the-real-time-os-inside-your-phone-s-processor-full-handbook/</link>
                <guid isPermaLink="false">69fbcaed50ecad4533880efa</guid>
                
                    <category>
                        <![CDATA[ freeRTOS  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ QuRT ]]>
                    </category>
                
                    <category>
                        <![CDATA[ qualcomm ]]>
                    </category>
                
                    <category>
                        <![CDATA[ os ]]>
                    </category>
                
                    <category>
                        <![CDATA[ embedded ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nikheel Vishwas Savant ]]>
                </dc:creator>
                <pubDate>Wed, 06 May 2026 23:12:45 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/e20376ee-713a-473e-946c-5c837eef0b12.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>The Hexagon DSP in every Qualcomm-powered phone handles wake word detection, sensor processing, noise cancellation, and Bluetooth audio streaming – all while the main ARM CPU runs Android.</p>
<p>The operating system orchestrating that work on the DSP is QuRT (Qualcomm Real-Time Operating System), a POSIX-like, priority-based, preemptive RTOS purpose-built for Qualcomm's Hexagon Digital Signal Processor.</p>
<p>This article is a practical guide to Qualcomm's Real-Time Operating System. It covers QuRT from the ground up: architecture, thread creation, synchronization primitives, memory management, interrupt handling, timers, inter-processor communication through FastRPC, and a complete sensor fusion pipeline. Every concept includes working code and an explanation of what's happening under the hood.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-why-qurt-matters">Why QuRT Matters</a></p>
</li>
<li><p><a href="#heading-setting-up-your-development-environment">Setting Up Your Development Environment</a></p>
</li>
<li><p><a href="#heading-the-qurt-programming-model">The QuRT Programming Model</a></p>
</li>
<li><p><a href="#heading-creating-your-first-qurt-thread">Creating Your First QuRT Thread</a></p>
</li>
<li><p><a href="#heading-how-thread-creation-works-internally">How Thread Creation Works Internally</a></p>
</li>
<li><p><a href="#heading-working-with-multiple-threads">Working with Multiple Threads</a></p>
</li>
<li><p><a href="#heading-synchronization-primitives">Synchronization Primitives</a></p>
</li>
<li><p><a href="#heading-memory-management">Memory Management</a></p>
</li>
<li><p><a href="#heading-timers-and-timing">Timers and Timing</a></p>
</li>
<li><p><a href="#heading-interrupt-handling">Interrupt Handling</a></p>
</li>
<li><p><a href="#heading-pipes-and-message-queues">Pipes and Message Queues</a></p>
</li>
<li><p><a href="#heading-qurt-and-fastrpc">QuRT and FastRPC</a></p>
</li>
<li><p><a href="#heading-building-a-sensor-fusion-pipeline">Building a Sensor Fusion Pipeline</a></p>
</li>
<li><p><a href="#heading-debugging-qurt-applications">Debugging QuRT Applications</a></p>
</li>
<li><p><a href="#heading-common-pitfalls">Common Pitfalls</a></p>
</li>
<li><p><a href="#heading-performance-optimization">Performance Optimization</a></p>
</li>
<li><p><a href="#heading-api-quick-reference">API Quick Reference</a></p>
</li>
<li><p><a href="#heading-next-steps">Next Steps</a></p>
</li>
</ul>
<h2 id="heading-why-qurt-matters">Why QuRT Matters</h2>
<p>Consider what happens during a phone call. The device is simultaneously running noise cancellation on the microphone audio, executing a neural network for wake word detection, reading accelerometer data 400 times per second, and managing Bluetooth audio streaming.</p>
<p>None of this runs on the main ARM CPU. It all happens on Qualcomm's <strong>Hexagon DSP</strong>, and the operating system coordinating it is <strong>QuRT</strong>.</p>
<p>QuRT (Qualcomm Real-Time Operating System) is a POSIX-like, priority-based, preemptive RTOS that runs on Qualcomm's Hexagon Digital Signal Processor. Where Linux is a general-purpose operating system designed for flexibility, QuRT is a precision instrument designed for deterministic, microsecond-level scheduling.</p>
<h3 id="heading-where-qurt-fits-in-the-system">Where QuRT Fits in the System</h3>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/23b64c27-4715-4923-bf97-b55742a71032.png" alt="The two-processor architecture inside a Qualcomm SoC" style="display:block;margin:0 auto" width="2916" height="1332" loading="lazy">

<p>This diagram shows the two-processor architecture inside a Qualcomm SoC. The ARM CPU on the left runs Android or Linux and handles general application logic. The Hexagon DSP on the right runs QuRT and handles latency-sensitive workloads: audio processing, sensor fusion, ML inference, and compute offload.</p>
<p>The two processors communicate through a framework called <strong>FastRPC</strong>. You write code for the DSP side using the Hexagon SDK, and QuRT is the OS that executes your code on the Hexagon processor.</p>
<h2 id="heading-setting-up-your-development-environment">Setting Up Your Development Environment</h2>
<p>Before writing any QuRT code, you need the toolchain and either a simulator or physical hardware.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>You will need the Hexagon SDK (version 3.5+ or 4.x), which is Qualcomm's official SDK and includes the Hexagon Tools compiler toolchain.</p>
<p>For running your code, you can use either a Qualcomm development board (such as the Robotics RB5 or an SM8250 HDK) or the SDK's built-in simulator. A Linux host machine running Ubuntu 18.04 or 20.04 works best for development.</p>
<h3 id="heading-installing-the-hexagon-sdk">Installing the Hexagon SDK</h3>
<pre><code class="language-shell"># Download the Hexagon SDK from Qualcomm's developer portal
# https://developer.qualcomm.com/software/hexagon-dsp-sdk

# Extract and run the installer
chmod +x qualcomm_hexagon_sdk_4_x_x_x.bin
./qualcomm_hexagon_sdk_4_x_x_x.bin

# Set up environment variables
export HEXAGON_SDK_ROOT=~/Qualcomm/Hexagon_SDK/4.x.x.x
export HEXAGON_TOOLS_ROOT=~/Qualcomm/Hexagon_SDK/4.x.x.x/tools
source $HEXAGON_SDK_ROOT/setup_sdk_env.source
</code></pre>
<p>This installs the SDK to your home directory and sets up the environment variables that the build system and simulator need. The <code>setup_sdk_env.source</code> script configures your shell with paths to the compiler, simulator, and libraries.</p>
<h3 id="heading-verifying-your-setup">Verifying Your Setup</h3>
<pre><code class="language-shell"># Check the Hexagon compiler
hexagon-clang --version

# You should see something like:
# Qualcomm Hexagon Clang version 8.x.xx

# Run the QuRT simulator to make sure it works
$HEXAGON_SDK_ROOT/tools/HEXAGON_Tools/8.x.xx/Tools/bin/hexagon-sim \
    --simulated_returnval --cosim_file \
    $HEXAGON_SDK_ROOT/libs/common/qurt/computev66/sdksim_bin/osam.cfg \
    -- $HEXAGON_SDK_ROOT/libs/common/qurt/computev66/sdksim_bin/bootimg.pbn
</code></pre>
<p>The first command confirms that the Hexagon Clang compiler is installed and accessible. The second command launches the QuRT simulator, which is analogous to an Android emulator: it lets you test QuRT programs without physical hardware. Timing won't match real hardware, but the simulator is valuable for validating correctness during development.</p>
<h3 id="heading-project-structure">Project Structure</h3>
<p>The Hexagon SDK uses <strong>SCons</strong> as its underlying build system. Projects live inside the SDK tree and are configured through <code>.min</code> files, which are declarative build descriptors that the SDK's SCons infrastructure parses.</p>
<p>A minimal project looks like this:</p>
<pre><code class="language-shell">$HEXAGON_SDK_ROOT/examples/my_qurt_project/
├── src/
│   └── main.c              # Your QuRT application code
├── inc/
│   └── my_module.h         # Header files
├── hexagon.min              # SCons build config for Hexagon DSP side
└── android.min              # SCons build config for ARM side (if using FastRPC)
</code></pre>
<p>The <code>hexagon.min</code> file configures the DSP-side build, while <code>android.min</code> handles the ARM side when using FastRPC for cross-processor communication. Both are read by the SDK's top-level <code>SConstruct</code> file, which lives at <code>$HEXAGON_SDK_ROOT/SConstruct</code>. You don't need a separate <code>Makefile</code> or <code>SConscript</code> for projects inside the SDK tree.</p>
<h3 id="heading-build-configuration-with-scons">Build Configuration with SCons</h3>
<p>A minimal <code>hexagon.min</code> build file looks like this:</p>
<pre><code class="language-shell"># hexagon.min - SCons build descriptor for the DSP side

BUILD_LIBS = libmy_qurt_app

# Source files
libmy_qurt_app_C_SRCS = src/main.c

# QuRT OS library
libmy_qurt_app_LIBS = atomic rpcmem

# Compiler flags
libmy_qurt_app_HEXAGON_CFLAGS = -O2 -Wall

# Link against QuRT
libmy_qurt_app_DLLS = libmy_qurt_app_skel
</code></pre>
<p>The <code>.min</code> file format is specific to the Hexagon SDK's SCons build system. <code>BUILD_LIBS</code> names the library target. <code>C_SRCS</code> lists source files. <code>LIBS</code> specifies libraries to link against. <code>HEXAGON_CFLAGS</code> sets compiler flags. <code>DLLS</code> defines the shared library output name, where the <code>_skel</code> suffix is a FastRPC convention for DSP-side implementations.</p>
<p>Under the hood, the SDK's <code>SConstruct</code> walks the project tree, reads each <code>.min</code> file, and translates its declarations into SCons build targets. The <code>V</code> (variant) parameter you pass at build time selects the target architecture, build type, and toolchain version. For example, <code>V=hexagon_Release_dynamic_toolv84_v66</code> means: build for Hexagon, release mode, dynamic linking, using the v84 toolchain targeting the v66 DSP architecture.</p>
<p>For projects that need more control than the <code>.min</code> format provides, you can write a standalone <code>SConscript</code> file:</p>
<pre><code class="language-python"># SConscript - Standalone SCons build for a QuRT project

Import('env')

env = env.Clone()

# Add include paths
env.Append(CPPPATH = ['inc'])

# Compiler flags
env.Append(CCFLAGS = ['-O2', '-Wall'])

# Build the shared library
sources = ['src/main.c']
libs = ['atomic', 'rpcmem']

env.SharedLibrary(
    target = 'libmy_qurt_app_skel',
    source = sources,
    LIBS = libs
)
</code></pre>
<p>The <code>SConscript</code> approach gives you full access to SCons features: conditional compilation, custom build steps, dependency scanning, and variant builds. The <code>Import('env')</code> call pulls in the build environment configured by the SDK's top-level <code>SConstruct</code>, which already knows about Hexagon compiler paths, QuRT headers, and system libraries. <code>env.Clone()</code> creates a copy so your modifications do not affect other projects in the tree.</p>
<h2 id="heading-the-qurt-programming-model">The QuRT Programming Model</h2>
<p>The core mental model for QuRT programming is straightforward:</p>
<p><strong>QuRT is a priority-based preemptive RTOS.</strong> That means everything runs in a thread (there is no bare-metal main loop). Higher priority threads always preempt lower priority ones, immediately and without negotiation. Threads at the same priority level are round-robin scheduled.</p>
<p>The scheduler is tick-less, meaning it doesn't wake up periodically. It only runs when something changes, such as a thread blocking, a signal being set, or a higher-priority thread becoming ready.</p>
<pre><code class="language-plaintext">Priority Levels (0-255, lower number = higher priority)

 000  ┃ ████ Interrupt handlers (do not touch this)
 001  ┃ ████ Critical system tasks
 ...  ┃
 064  ┃ ████ Your high-priority audio processing
 ...  ┃
 128  ┃ ████ Your medium-priority sensor fusion
 ...  ┃
 192  ┃ ████ Your low-priority logging/reporting
 ...  ┃
 255  ┃ ████ Idle thread (QuRT's built-in background)
</code></pre>
<p>This priority map shows how QuRT's 256 priority levels are typically allocated. Priority 0 is the <strong>highest</strong> priority and 255 is the <strong>lowest</strong>. This is the opposite of FreeRTOS, where higher numbers mean higher priority.</p>
<p>Interrupt handlers occupy the top priority levels, system tasks sit just below, and user threads occupy the middle range. The idle thread at priority 255 runs only when nothing else is ready.</p>
<h2 id="heading-creating-your-first-qurt-thread">Creating Your First QuRT Thread</h2>
<p>The simplest QuRT program creates a single thread that prints a message and exits.</p>
<pre><code class="language-c">/* main.c - First QuRT program */

#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;qurt.h&gt;

#define STACK_SIZE 4096

/* Thread stack must be 8-byte aligned */
static char thread_stack[STACK_SIZE] __attribute__((aligned(8)));

void my_thread_func(void *arg)
{
    int thread_id = (int)(uintptr_t)arg;

    printf("Hello from QuRT thread %d!\n", thread_id);
    printf("My thread ID: %lu\n", qurt_thread_get_id());

    /* Thread must explicitly exit */
    qurt_thread_exit(QURT_EOK);
}

int main(void)
{
    qurt_thread_t      thread_id;
    qurt_thread_attr_t attr;

    printf("Main thread starting on QuRT!\n");

    /* Initialize thread attributes */
    qurt_thread_attr_init(&amp;attr);

    /* Configure the thread */
    qurt_thread_attr_set_name(&amp;attr, "my_first_thread");
    qurt_thread_attr_set_stack_addr(&amp;attr, thread_stack);
    qurt_thread_attr_set_stack_size(&amp;attr, STACK_SIZE);
    qurt_thread_attr_set_priority(&amp;attr, 128);  /* Medium priority */

    /* Create and start the thread */
    int result = qurt_thread_create(&amp;thread_id, &amp;attr,
                                     my_thread_func,
                                     (void *)42);

    if (result != QURT_EOK) {
        printf("Thread creation failed with error: %d\n", result);
        return -1;
    }

    printf("Thread created successfully! ID: %lu\n", thread_id);

    /* Wait for the thread to finish */
    int status;
    qurt_thread_join(thread_id, &amp;status);

    printf("Thread finished with status: %d\n", status);
    return 0;
}
</code></pre>
<p>This program demonstrates the four-step thread creation process in QuRT. First, <code>qurt_thread_attr_init()</code> initializes a thread attribute's structure. Second, the program configures the thread with a debug name (which shows up in crash dumps), a stack address, a stack size, and a priority. Third, <code>qurt_thread_create()</code> creates and immediately starts the thread, passing a function pointer and an argument. Fourth, <code>qurt_thread_join()</code> blocks the calling thread until the new thread calls <code>qurt_thread_exit()</code>.</p>
<p>Two details are critical. QuRT doesn't allocate stack memory for you: you must provide a statically allocated, 8-byte-aligned buffer. And every thread must call <code>qurt_thread_exit()</code> before returning. If a thread function simply returns without calling exit, the behavior is undefined.</p>
<h3 id="heading-thread-creation-flow">Thread Creation Flow</h3>
<pre><code class="language-plaintext">     qurt_thread_attr_init()
              │
              ▼
    ┌─────────────────────┐
    │  Set name           │
    │  Set stack address  │
    │  Set stack size     │
    │  Set priority       │
    └─────────────────────┘
              │
              ▼
     qurt_thread_create()
              │
              ▼
    Thread starts running ──► my_thread_func()
              │                      │
              ▼                      ▼
     qurt_thread_join()       qurt_thread_exit()
     (waits for exit)         (signals "I'm done")
</code></pre>
<p>This flow shows the lifecycle of a single thread. The attributes structure acts as a configuration object: you set all the thread parameters, then pass it to <code>qurt_thread_create()</code>. Once created, the thread runs its entry function. When the entry function calls <code>qurt_thread_exit()</code>, the thread terminates and any thread blocked in <code>qurt_thread_join()</code> is unblocked and receives the exit status code.</p>
<h2 id="heading-how-thread-creation-works-internally">How Thread Creation Works Internally</h2>
<p>Most tutorials skip what happens inside <code>qurt_thread_create()</code>. Understanding the internals makes debugging and priority design decisions much clearer.</p>
<h3 id="heading-what-the-kernel-does-during-thread-creation">What the Kernel Does During Thread Creation</h3>
<p>When you call <code>qurt_thread_create()</code>, you're making a <strong>system call</strong> into the QuRT kernel. The kernel performs five steps in sequence:</p>
<pre><code class="language-plaintext">  Your code calls qurt_thread_create()
         │
         ▼
  ┌──────────────────────────────────────────────────────────┐
  │  1. VALIDATE                                             │
  │     • Is the stack pointer non-NULL and aligned?         │
  │     • Is the stack size &gt;= minimum (typ. 2KB)?           │
  │     • Is the priority in range 0-255?                    │
  │     • Is the entry function pointer non-NULL?            │
  │     (If any check fails → return QURT_EINVALID)          │
  ├──────────────────────────────────────────────────────────┤
  │  2. ALLOCATE THREAD CONTROL BLOCK (TCB)                  │
  │     • QuRT allocates a kernel-side data structure        │
  │     • This holds: thread ID, priority, state, saved      │
  │       registers, signal masks, mutex wait list, etc.     │
  ├──────────────────────────────────────────────────────────┤
  │  3. INITIALIZE THE STACK FRAME                           │
  │     • The kernel sets up a synthetic stack frame at the  │
  │       top of YOUR stack memory                           │
  │     • It writes the initial register values:             │
  │       ┌──────────────────────────────────────┐           │
  │       │  Stack Top (high address)            │           │
  │       │  ┌──────────────────────────────────┐│           │
  │       │  │ PC  = my_thread_func (entry)     ││           │
  │       │  │ SP  = stack_addr + stack_size    ││           │
  │       │  │ R0  = arg (your void* argument)  ││           │
  │       │  │ LR  = qurt_thread_exit           ││           │
  │       │  │ SR  = default status register    ││           │
  │       │  │ R1-R31 = 0                       ││           │
  │       │ └──────────────────────────────────┘│            │
  │       │  ... (rest of stack is untouched) ...│           │
  │       │  Stack Bottom (low address)          │           │
  │       └──────────────────────────────────────┘           │
  ├──────────────────────────────────────────────────────────┤
  │  4. INSERT INTO READY QUEUE                              │
  │     • The TCB is added to the scheduler's ready queue    │
  │       at the appropriate priority level                  │
  │     • The thread's state is set to READY                 │
  ├──────────────────────────────────────────────────────────┤
  │  5. TRIGGER A RESCHEDULE                                 │
  │     • The scheduler checks: "Is this new thread's        │
  │       priority higher than the currently running         │
  │       thread?"                                           │
  │     • If YES: context switch happens RIGHT NOW           │
  │       (the calling thread is preempted)                  │
  │     • If NO: the new thread waits in the ready queue     │
  │       until it's the highest priority runnable thread    │
  └──────────────────────────────────────────────────────────┘
         │
         ▼
  qurt_thread_create() returns to the caller
  (but the new thread may already be running!)
</code></pre>
<p>The most surprising aspect of this flow is step 5. If the new thread has higher priority than the thread that created it, <strong>the new thread starts running before</strong> <code>qurt_thread_create()</code> <strong>returns to the caller</strong>. The creating thread is preempted mid-call. This is what "preemptive" means in practice: the scheduler doesn't wait for a convenient moment. It enforces priority ordering immediately.</p>
<h3 id="heading-how-the-stack-frame-launches-your-function">How the Stack Frame Launches Your Function</h3>
<p>When the scheduler context-switches to a brand-new thread for the first time, it does exactly what it does for any context switch: it restores the saved registers from the TCB and jumps to the saved Program Counter.</p>
<p>For a new thread, those registers were set up synthetically by the kernel during step 3. The <strong>PC (Program Counter)</strong> was set to <code>my_thread_func</code>, so the processor jumps to your function. <strong>R0</strong> was set to your <code>arg</code> parameter, so your function receives it as the first argument (following the Hexagon calling convention). The <strong>SP (Stack Pointer)</strong> was set to the top of your stack, so your function has a working stack. And the <strong>LR (Link Register)</strong> was set to <code>qurt_thread_exit</code>, so if your function returns normally (which you should not rely on), it falls through to <code>qurt_thread_exit</code>.</p>
<pre><code class="language-plaintext">The illusion:
──────────────
To your thread function, it looks like someone
"called" it normally with the argument you passed.

The reality:
──────────────
The scheduler restored a set of synthetic registers
that make the processor THINK it is returning from
a function call into your entry point.

It's like waking up in a room you have never been in,
but someone arranged everything so perfectly that
you do not realize you did not walk in through the door.
</code></pre>
<p>This diagram contrasts the programmer's mental model (a normal function call) with what actually happens at the hardware level (a register restore that simulates a function call). The thread function has no way to distinguish between these two scenarios, which is exactly the point. The kernel creates a seamless illusion.</p>
<h3 id="heading-context-switch-walkthrough">Context Switch Walkthrough</h3>
<p>Consider a concrete example: thread A (priority 128) creates thread B (priority 64, which is higher priority). The following timeline shows what happens at each step:</p>
<pre><code class="language-plaintext">Time ──────────────────────────────────────────────►

Thread A (pri 128)          Kernel/Scheduler         Thread B (pri 64)
────────────────           ────────────────           ────────────────
Calls                      
qurt_thread_create()       
   │                       
   ├─► System call ──────►  Validates params
                            Allocates TCB
                            Sets up stack frame
                            Inserts B into ready queue
                            
                            "B (64) &gt; A (128)?  YES."
                            
                            SAVE A's registers   ──┐
                            to A's TCB             │
                                                   │
                            LOAD B's registers   ◄─┘
                            from B's TCB (the
                            synthetic ones)
                            
                            Jump to PC ─────────► my_thread_func(arg)
                                                   │
                                                   │ does work...
                                                   │ calls qurt_thread_exit()
                                                   │
                            B is removed ◄─────── Exit system call
                            from ready queue
                            
                            "Who's next? A."
                            
                            LOAD A's registers
   │                        Jump to A's PC
   │◄──────────────────────
   │
   ├─► qurt_thread_create()
   │   returns QURT_EOK
   │
   ▼ continues...
</code></pre>
<p>From thread A's perspective, <code>qurt_thread_create()</code> is just a function call that takes a while to return. Thread A has no idea it was suspended. It doesn't know thread B already ran to completion during that pause.</p>
<p>The scheduler makes preemption invisible to the preempted thread. This is a fundamental property of preemptive scheduling: threads don't need to cooperate or even be aware of each other's existence.</p>
<h3 id="heading-thread-control-block-contents">Thread Control Block Contents</h3>
<p>The TCB is the kernel's internal data structure for tracking each thread. You never access it directly, but understanding its contents explains a lot of QuRT behavior:</p>
<pre><code class="language-c">/* Conceptual TCB layout (simplified, not actual QuRT source) */
struct qurt_tcb {
    /* Identity */
    qurt_thread_t   thread_id;
    char            name[16];
    
    /* Scheduling */
    uint8_t         base_priority;
    uint8_t         effective_priority; /* May differ due to priority inheritance */
    uint8_t         state;             /* READY, RUNNING, BLOCKED, SUSPENDED */
    
    /* Saved CPU context (filled during context switch) */
    uint32_t        saved_regs[32];
    uint32_t        saved_pc;
    uint32_t        saved_sp;
    uint32_t        saved_sr;
    
    /* Stack info (for debugging and overflow detection) */
    void           *stack_base;
    size_t          stack_size;
    
    /* Blocking info */
    void           *wait_object;  /* Mutex/signal/pipe being waited on */
    uint32_t        wait_mask;    /* Signal bits being waited for */
    
    /* Linked list pointers */
    struct qurt_tcb *next_ready;
    struct qurt_tcb *next_waiting;
    
    /* Join support */
    int             exit_status;  /* Value passed to qurt_thread_exit() */
    qurt_thread_t   joiner;      /* Thread waiting in qurt_thread_join() */
};
</code></pre>
<p>The TCB stores everything the scheduler needs: identity information (thread ID and debug name), scheduling state (base and effective priority, current state), saved CPU context (all 32 general-purpose registers plus PC, SP, and status register), stack bounds, blocking information (what the thread is waiting on), linked list pointers for the ready and wait queues, and join support fields.</p>
<p>The <code>effective_priority</code> field may differ from <code>base_priority</code> when priority inheritance is active, which is covered in the synchronization section.</p>
<h3 id="heading-thread-state-machine">Thread State Machine</h3>
<p>A QuRT thread is always in one of four states:</p>
<pre><code class="language-plaintext">                    qurt_thread_create()
                           │
                           ▼
                    ┌──────────┐
          ┌─────────│  READY   │◄──────────────────────────┐
          │         └──────────┘                           │
          │              │ ▲                               │
          │  Scheduler   │ │ Preempted by                  │
          │  picks this  │ │ higher-priority               │
          │  thread      │ │ thread                        │
          │              ▼ │                               │
          │         ┌──────────┐     Signal/mutex/         │
          │         │ RUNNING  │     timer event           │
          │         └──────────┘     unblocks thread       │
          │              │                                 │
          │  Thread calls│                                 │
          │  blocking    │                                 │
          │  API:        │                                 │
          │  - mutex_lock│                                 │
          │  - signal_   │                                 │
          │    wait      │                                 │
          │  - pipe_     │                                 │
          │    receive   ▼                                 │
          │         ┌──────────┐                           │
          │         │ BLOCKED  │───────────────────────────┘
          │         └──────────┘
          │
          │  qurt_thread_exit()
          │         │
          │         ▼
          │    ┌──────────┐
          └───►│  DEAD    │
               └──────────┘
</code></pre>
<ul>
<li><p><strong>READY</strong> means the thread can run and is waiting for a hardware thread slot.</p>
</li>
<li><p><strong>RUNNING</strong> means the thread is currently executing on a hardware thread (only one thread per hardware thread slot is in this state at a time).</p>
</li>
<li><p><strong>BLOCKED</strong> means the thread is waiting for an external event: a mutex to be released, a signal to be set, or a timer to expire.</p>
</li>
<li><p><strong>DEAD</strong> means the thread called <code>qurt_thread_exit()</code>. If another thread called <code>qurt_thread_join()</code> on it, that thread receives the exit status.</p>
</li>
</ul>
<h3 id="heading-hardware-thread-slots">Hardware Thread Slots</h3>
<p>The Hexagon DSP is a <strong>hardware-multithreaded processor</strong> with multiple hardware thread slots per core (typically 2 to 4). This means QuRT can run multiple threads truly simultaneously on a single core, not just time-sliced.</p>
<pre><code class="language-plaintext">┌─────────────────────────────────────────┐
│          Hexagon DSP Core               │
│                                         │
│  ┌───────────┐  ┌───────────┐           │
│  │ HW Thread │  │ HW Thread │           │
│  │ Slot 0    │  │ Slot 1    │  ...      │
│  │           │  │           │           │
│  │ Thread A  │  │ Thread B  │           │
│  │ (running) │  │ (running) │           │
│  └───────────┘  └───────────┘           │
│                                         │
│  Ready Queue: [C, D, E, F, ...]         │
│  The scheduler fills HW slots with      │
│  the highest-priority READY threads     │
└─────────────────────────────────────────┘
</code></pre>
<p>This diagram shows a single Hexagon core with two hardware thread slots. Each slot can execute a thread independently and simultaneously. The scheduler fills the hardware slots with the highest-priority ready threads. When there are more software threads than hardware slots, the scheduler time-slices the lower-priority threads. But the highest-priority threads get dedicated hardware slots and run without context switching at all.</p>
<p>On a typical Hexagon v66 with 4 hardware threads, the top 4 priority threads each have their own execution pipeline. Context switches only happen when a thread blocks or a higher-priority thread wakes up and displaces one from a hardware slot. This is why QuRT achieves such low scheduling latency.</p>
<h3 id="heading-full-thread-lifecycle">Full Thread Lifecycle</h3>
<p>The following code shows a complete thread lifecycle with annotations for what QuRT does at each step:</p>
<pre><code class="language-c">static char stack[8192] __attribute__((aligned(8)));

void my_func(void *arg)
{
    /* State: RUNNING. Stack is fresh, R0 contains arg. */
    int val = *(int *)arg;

    qurt_mutex_lock(&amp;some_mutex);
    /* If mutex is held: state becomes BLOCKED until holder unlocks */

    shared_data = val;
    qurt_mutex_unlock(&amp;some_mutex);

    qurt_thread_exit(QURT_EOK);
    /* State becomes DEAD. Joiner (if any) is unblocked. */
}

int main(void)
{
    qurt_thread_t tid;
    qurt_thread_attr_t attr;
    int my_arg = 42;

    qurt_thread_attr_init(&amp;attr);
    qurt_thread_attr_set_stack_addr(&amp;attr, stack);
    qurt_thread_attr_set_stack_size(&amp;attr, sizeof(stack));
    qurt_thread_attr_set_priority(&amp;attr, 100);

    qurt_thread_create(&amp;tid, &amp;attr, my_func, &amp;my_arg);
    /* If my_func's priority (100) &gt; main's: main is preempted here */

    int status;
    qurt_thread_join(tid, &amp;status);
    /* Blocks until my_func exits; returns immediately if already exited */

    return 0;
}
</code></pre>
<p>When <code>my_func</code> starts running, the kernel has already set up its registers so that <code>arg</code> contains the pointer to <code>my_arg</code>. The thread's state is RUNNING.</p>
<p>When it calls <code>qurt_mutex_lock()</code>, one of two things happens: if the mutex is available, the thread acquires it and continues. If the mutex is held by another thread, the calling thread's state changes to BLOCKED, its registers are saved to its TCB, and the scheduler picks the next highest-priority ready thread.</p>
<p>When the mutex holder calls <code>qurt_mutex_unlock()</code>, the blocked thread moves back to READY and the scheduler re-evaluates priorities.</p>
<p>On the <code>main</code> side, <code>qurt_thread_create()</code> may or may not return before <code>my_func</code> finishes. If <code>my_func</code> has higher priority than <code>main</code>, the scheduler preempts <code>main</code> immediately, and <code>qurt_thread_create()</code> doesn't return until <code>my_func</code> completes (or blocks). <code>qurt_thread_join()</code> either blocks <code>main</code> until <code>my_func</code> exits, or returns immediately if <code>my_func</code> has already exited.</p>
<p>One important note about stack sizing: if you set <code>STACK_SIZE</code> to something too small (say, 256 bytes) and your thread calls <code>printf</code>, the result is a <strong>stack overflow</strong>. QuRT doesn't detect stack overflows for you. The crash will be silent and difficult to diagnose. Always give your threads at least 8192 bytes of stack and optimize later after profiling.</p>
<h3 id="heading-building-and-running-on-the-simulator">Building and Running on the Simulator</h3>
<p>The Hexagon SDK provides a <code>make</code> wrapper that invokes SCons underneath. Both of the following commands produce the same result:</p>
<pre><code class="language-bash"># Option 1: Use the make wrapper (invokes SCons internally)
cd $HEXAGON_SDK_ROOT
make V=hexagon_Release_dynamic_toolv84_v66 \
     tree=my_qurt_project

# Option 2: Invoke SCons directly
cd $HEXAGON_SDK_ROOT
python tools/build/scons/scons.py \
    V=hexagon_Release_dynamic_toolv84_v66 \
    my_qurt_project
</code></pre>
<p>Both commands build the project for the Hexagon v66 architecture using the v84 toolchain in release mode. The <code>make</code> wrapper is a convenience layer: it parses the <code>V=</code> and <code>tree=</code> arguments and forwards them to SCons. Using SCons directly gives you access to additional flags such as <code>--jobs=N</code> for parallel builds and <code>--verbose</code> for full compiler command output.</p>
<pre><code class="language-bash"># Run on the simulator
hexagon-sim --simulated_returnval \
    --cosim_file osam.cfg \
    -- bootimg.pbn \
    -- my_qurt_app.so
</code></pre>
<p>The <code>hexagon-sim</code> command launches the QuRT simulator with your compiled application. The <code>--simulated_returnval</code> flag captures the return value from your <code>main</code> function, and <code>--cosim_file</code> points to the QuRT OS configuration.</p>
<h2 id="heading-working-with-multiple-threads">Working with Multiple Threads</h2>
<p>Real QuRT applications have multiple threads running simultaneously. The producer-consumer pattern is one of the most common in DSP programming: one thread reads from hardware, another processes the data.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;qurt.h&gt;

#define STACK_SIZE    8192
#define BUFFER_SIZE   16
#define NUM_ITEMS     100

/* Thread stacks */
static char producer_stack[STACK_SIZE] __attribute__((aligned(8)));
static char consumer_stack[STACK_SIZE] __attribute__((aligned(8)));

/* Shared buffer */
static int buffer[BUFFER_SIZE];
static int head = 0;
static int tail = 0;
static int count = 0;

/* Synchronization primitives */
qurt_mutex_t buffer_mutex;
qurt_cond_t  not_full;
qurt_cond_t  not_empty;

void producer_thread(void *arg)
{
    for (int i = 0; i &lt; NUM_ITEMS; i++) {
        qurt_mutex_lock(&amp;buffer_mutex);

        /* Wait until there is space in the buffer */
        while (count == BUFFER_SIZE) {
            qurt_cond_wait(&amp;not_full, &amp;buffer_mutex);
        }

        /* Produce an item */
        buffer[head] = i;
        head = (head + 1) % BUFFER_SIZE;
        count++;

        printf("[Producer] Put item %d (buffer count: %d)\n", i, count);

        /* Signal the consumer that data is available */
        qurt_cond_signal(&amp;not_empty);
        qurt_mutex_unlock(&amp;buffer_mutex);
    }

    qurt_thread_exit(QURT_EOK);
}

void consumer_thread(void *arg)
{
    for (int i = 0; i &lt; NUM_ITEMS; i++) {
        qurt_mutex_lock(&amp;buffer_mutex);

        /* Wait until there is data in the buffer */
        while (count == 0) {
            qurt_cond_wait(&amp;not_empty, &amp;buffer_mutex);
        }

        /* Consume an item */
        int item = buffer[tail];
        tail = (tail + 1) % BUFFER_SIZE;
        count--;

        printf("[Consumer] Got item %d (buffer count: %d)\n", item, count);

        /* Signal the producer that space is available */
        qurt_cond_signal(&amp;not_full);
        qurt_mutex_unlock(&amp;buffer_mutex);
    }

    qurt_thread_exit(QURT_EOK);
}

int main(void)
{
    qurt_thread_t producer, consumer;
    qurt_thread_attr_t attr;

    /* Initialize sync primitives BEFORE creating threads */
    qurt_mutex_init(&amp;buffer_mutex);
    qurt_cond_init(&amp;not_full);
    qurt_cond_init(&amp;not_empty);

    /* Create producer (higher priority) */
    qurt_thread_attr_init(&amp;attr);
    qurt_thread_attr_set_name(&amp;attr, "producer");
    qurt_thread_attr_set_stack_addr(&amp;attr, producer_stack);
    qurt_thread_attr_set_stack_size(&amp;attr, STACK_SIZE);
    qurt_thread_attr_set_priority(&amp;attr, 100);
    qurt_thread_create(&amp;producer, &amp;attr, producer_thread, NULL);

    /* Create consumer (lower priority) */
    qurt_thread_attr_init(&amp;attr);
    qurt_thread_attr_set_name(&amp;attr, "consumer");
    qurt_thread_attr_set_stack_addr(&amp;attr, consumer_stack);
    qurt_thread_attr_set_stack_size(&amp;attr, STACK_SIZE);
    qurt_thread_attr_set_priority(&amp;attr, 110);
    qurt_thread_create(&amp;consumer, &amp;attr, consumer_thread, NULL);

    /* Wait for both threads to finish */
    int status;
    qurt_thread_join(producer, &amp;status);
    qurt_thread_join(consumer, &amp;status);

    /* Clean up */
    qurt_mutex_destroy(&amp;buffer_mutex);
    qurt_cond_destroy(&amp;not_full);
    qurt_cond_destroy(&amp;not_empty);

    printf("All done! Produced and consumed %d items.\n", NUM_ITEMS);
    return 0;
}
</code></pre>
<p>This code implements a classic bounded-buffer producer-consumer pattern. The shared buffer is a circular array of 16 integers protected by a mutex. The producer writes items into the buffer and the consumer reads them out.</p>
<p>When the buffer is full, the producer blocks on the <code>not_full</code> condition variable. When the buffer is empty, the consumer blocks on <code>not_empty</code>. Each side signals the other after modifying the buffer.</p>
<p>The producer has higher priority (100) than the consumer (110) for a deliberate reason. In a real DSP scenario, the producer is typically reading from hardware (a microphone, a sensor). If the producer misses a hardware sample, that data is lost forever. The consumer can always process data later. This is a general RTOS design principle: <strong>never starve your hardware-facing threads.</strong></p>
<h2 id="heading-synchronization-primitives">Synchronization Primitives</h2>
<p>QuRT provides five main synchronization mechanisms: mutexes, condition variables, signals, barriers, and semaphores.</p>
<pre><code class="language-plaintext">┌──────────────┬────────────────────────────────────────────────────┐
│ Primitive    │ When to Use                                        │
├──────────────┼────────────────────────────────────────────────────┤
│ Mutex        │ Protecting shared data from concurrent access      │
│ Condition Var│ "Wait until X is true" (always paired with mutex)  │
│ Signal       │ One thread notifying another (like poking someone) │
│ Barrier      │ "Everyone wait here until all threads arrive"      │
├──────────────┼────────────────────────────────────────────────────┤
│ Semaphore    │ Controlling access to a limited resource pool      │
│              │ (for example, 4 DMA channels shared by 10 threads)        │
└──────────────┴────────────────────────────────────────────────────┘
</code></pre>
<p>This table summarizes each primitive and its primary use case. Mutexes enforce exclusive access to shared data. Condition variables let a thread sleep until a specific data condition becomes true, and are always used in combination with a mutex. Signals provide lightweight one-to-one notifications between threads. Barriers synchronize a group of threads at a common point. Semaphores control access to a pool of N identical resources.</p>
<h3 id="heading-mutexes">Mutexes</h3>
<p>A mutex ensures that only one thread accesses a critical section at a time. QuRT mutexes also support non-blocking acquisition through <code>qurt_mutex_try_lock()</code>.</p>
<pre><code class="language-c">qurt_mutex_t my_mutex;

void init_example(void)
{
    /* Always initialize before use */
    qurt_mutex_init(&amp;my_mutex);
}

void critical_section_example(void)
{
    qurt_mutex_lock(&amp;my_mutex);

    /* Only one thread can be here at a time */
    shared_counter++;
    shared_buffer[index] = new_value;

    qurt_mutex_unlock(&amp;my_mutex);
}

/* Non-blocking version */
void try_lock_example(void)
{
    int result = qurt_mutex_try_lock(&amp;my_mutex);

    if (result == QURT_EOK) {
        shared_counter++;
        qurt_mutex_unlock(&amp;my_mutex);
    } else {
        printf("Busy, will try later\n");
    }
}

void cleanup_example(void)
{
    qurt_mutex_destroy(&amp;my_mutex);
}
</code></pre>
<p>The <code>qurt_mutex_lock()</code> call blocks the calling thread until the mutex is available, then acquires it. <code>qurt_mutex_try_lock()</code> attempts to acquire the mutex and returns immediately with <code>QURT_EOK</code> on success or an error code if the mutex is held. Always call <code>qurt_mutex_destroy()</code> when you're done with a mutex.</p>
<p>QuRT mutexes implement <strong>priority inheritance</strong>. If a high-priority thread is waiting for a mutex held by a low-priority thread, the low-priority thread temporarily gets boosted to the high-priority level. This prevents <strong>priority inversion</strong>, the classic bug that caused the Mars Pathfinder spacecraft to repeatedly reset during its mission.</p>
<p>QuRT handles priority inheritance automatically, but you should be aware it's happening so you don't get confused by unexpected priority behavior during debugging.</p>
<h3 id="heading-signals">Signals</h3>
<p>Signals in QuRT are a lightweight notification mechanism. A thread waits for specific signal bits, and another thread (or an ISR) sets those bits to wake it up.</p>
<pre><code class="language-c">#include &lt;qurt.h&gt;

#define SIGNAL_DATA_READY   0x01
#define SIGNAL_STOP         0x02
#define SIGNAL_ERROR        0x04

qurt_signal_t my_signal;

void signal_init(void)
{
    qurt_signal_init(&amp;my_signal);
}

/* Waiting thread */
void waiter_thread(void *arg)
{
    unsigned int received_signals;

    while (1) {
        /* Wait for ANY of these signals */
        received_signals = qurt_signal_wait(
            &amp;my_signal,
            SIGNAL_DATA_READY | SIGNAL_STOP | SIGNAL_ERROR,
            QURT_SIGNAL_ATTR_WAIT_ANY
        );

        if (received_signals &amp; SIGNAL_STOP) {
            printf("Received stop signal. Exiting.\n");
            break;
        }

        if (received_signals &amp; SIGNAL_DATA_READY) {
            printf("Data is ready! Processing...\n");
            process_data();
            /* Clear the signal after handling it */
            qurt_signal_clear(&amp;my_signal, SIGNAL_DATA_READY);
        }

        if (received_signals &amp; SIGNAL_ERROR) {
            printf("Error occurred! Handling...\n");
            handle_error();
            qurt_signal_clear(&amp;my_signal, SIGNAL_ERROR);
        }
    }

    qurt_signal_destroy(&amp;my_signal);
    qurt_thread_exit(QURT_EOK);
}

/* Signaling thread (or ISR) */
void sender_thread(void *arg)
{
    prepare_data();
    qurt_signal_set(&amp;my_signal, SIGNAL_DATA_READY);

    /* Later, tell it to stop */
    qurt_signal_set(&amp;my_signal, SIGNAL_STOP);

    qurt_thread_exit(QURT_EOK);
}
</code></pre>
<p>The waiting thread calls <code>qurt_signal_wait()</code> with a bitmask of the signals it cares about. <code>QURT_SIGNAL_ATTR_WAIT_ANY</code> means the thread wakes up when any of the specified bits are set. The sender thread calls <code>qurt_signal_set()</code> to set one or more bits. After handling a signal, the waiter must call <code>qurt_signal_clear()</code> to reset the bit. If you forget to clear a signal, the next call to <code>qurt_signal_wait()</code> returns immediately, and your thread processes the same event again.</p>
<p>The choice between signals and condition variables depends on the use case. Signals are best for notifications between unrelated threads, or from an ISR, because they're simpler and lighter weight. Condition variables are better when the notification is tied to a specific data condition (buffer full, queue empty) and you need mutex protection for the data check.</p>
<h3 id="heading-barriers">Barriers</h3>
<p>A barrier blocks all participating threads until every one of them has reached the barrier point. This is useful when a computation is split into phases and each phase depends on the results of the previous one.</p>
<pre><code class="language-c">#define NUM_WORKER_THREADS  4

qurt_barrier_t sync_barrier;

void worker_thread(void *arg)
{
    int thread_num = (int)(uintptr_t)arg;

    /* Phase 1: Each thread computes its portion */
    printf("Thread %d: Computing phase 1...\n", thread_num);
    compute_partial_result(thread_num);

    /* All threads wait here until everyone finishes phase 1 */
    qurt_barrier_wait(&amp;sync_barrier);

    /* Phase 2: All partial results are ready, combine them */
    printf("Thread %d: Computing phase 2...\n", thread_num);
    combine_results(thread_num);

    qurt_thread_exit(QURT_EOK);
}

int main(void)
{
    qurt_barrier_init(&amp;sync_barrier, NUM_WORKER_THREADS);

    /* Create worker threads */
    for (int i = 0; i &lt; NUM_WORKER_THREADS; i++) {
        create_worker(i);
    }

    join_all_workers();

    qurt_barrier_destroy(&amp;sync_barrier);
    return 0;
}
</code></pre>
<p>The barrier is initialized with the number of participating threads. Each thread calls <code>qurt_barrier_wait()</code> when it reaches the synchronization point. The call blocks until all threads have arrived. Once the last thread calls <code>qurt_barrier_wait()</code>, all threads are released simultaneously and continue to phase 2.</p>
<h3 id="heading-semaphores">Semaphores</h3>
<p>A semaphore controls access to a pool of N identical resources. Unlike a mutex (which is a semaphore with N=1), a semaphore allows up to N threads to hold it simultaneously.</p>
<pre><code class="language-c">#define MAX_DMA_CHANNELS 4

qurt_sem_t dma_semaphore;

void init_dma_pool(void)
{
    /* 4 DMA channels available */
    qurt_sem_init_val(&amp;dma_semaphore, MAX_DMA_CHANNELS);
}

void thread_needing_dma(void *arg)
{
    /* Acquire a DMA channel (blocks if all 4 are in use) */
    qurt_sem_down(&amp;dma_semaphore);

    int channel = allocate_dma_channel();
    perform_dma_transfer(channel);
    release_dma_channel(channel);

    /* Release the semaphore slot */
    qurt_sem_up(&amp;dma_semaphore);

    qurt_thread_exit(QURT_EOK);
}
</code></pre>
<p>The semaphore starts with a count of 4, matching the number of DMA channels. Each <code>qurt_sem_down()</code> decrements the count and blocks if the count reaches zero. Each <code>qurt_sem_up()</code> increments the count and unblocks one waiting thread if any are queued. This guarantees that no more than 4 threads use DMA channels simultaneously.</p>
<h2 id="heading-memory-management">Memory Management</h2>
<p>Memory on a DSP is limited. A typical Hexagon DSP has between 256 KB and 2 MB of tightly-coupled memory (TCM) plus access to DDR. QuRT provides tools to manage both effectively.</p>
<h3 id="heading-the-memory-map">The Memory Map</h3>
<pre><code class="language-plaintext">┌───────────────────────────────────┐  High Address
│         DDR (Shared with ARM)     │
│   - Large buffers                 │
│   - Neural network weights        │
│   - Audio/video frames            │
├───────────────────────────────────┤
│         QuRT Virtual Memory       │
│   - User heap                     │
│   - Thread stacks                 │
├───────────────────────────────────┤
│         L2 Cache (TCM Mode)       │
│   - Frequently accessed buffers   │
│   - Lookup tables                 │
├───────────────────────────────────┤
│         QuRT Kernel               │
│   - Scheduler, ISR handlers       │
│   - System data structures        │
└───────────────────────────────────┘  Low Address
</code></pre>
<p>This diagram shows the Hexagon DSP memory layout from low to high addresses. The QuRT kernel occupies the lowest addresses and is off-limits to user code. Above that, L2 cache configured in TCM mode provides fast storage for hot data. The virtual memory region holds the user heap and thread stacks. At the top, DDR is shared with the ARM CPU and is used for large data buffers, ML model weights, and media frames. DDR has higher latency than TCM but much more capacity.</p>
<h3 id="heading-dynamic-memory-allocation">Dynamic Memory Allocation</h3>
<pre><code class="language-c">#include &lt;qurt.h&gt;
#include &lt;stdlib.h&gt;

void memory_examples(void)
{
    /* Standard malloc/free works (QuRT provides a heap) */
    int *data = (int *)malloc(1024 * sizeof(int));
    if (!data) {
        printf("malloc failed! Out of heap memory.\n");
        return;
    }

    for (int i = 0; i &lt; 1024; i++) {
        data[i] = i * 2;
    }

    free(data);
}
</code></pre>
<p>QuRT provides a standard C heap, so <code>malloc</code> and <code>free</code> work as expected. But <code>malloc</code> has unpredictable execution time because it may need to search the free list, split blocks, or coalesce adjacent free regions. This makes it unsuitable for real-time hot paths, where execution time must be deterministic. Use <code>malloc</code> for setup and teardown, not for per-frame or per-sample allocation.</p>
<h3 id="heading-cache-management">Cache Management</h3>
<p>On the Hexagon DSP, explicit cache management is essential when sharing memory with the ARM CPU.</p>
<pre><code class="language-c">#include &lt;qurt.h&gt;

void cache_management_example(void)
{
    void *buffer;
    size_t buffer_size = 4096;

    /* Allocate physically contiguous, cache-aligned memory */
    int result = qurt_mem_region_create(
        &amp;buffer,
        buffer_size,
        qurt_mem_default_pool,
        QURT_MEM_REGION_SHARED
    );

    if (result != QURT_EOK) {
        printf("Memory region creation failed\n");
        return;
    }

    /* BEFORE reading data written by another processor (e.g., ARM): */
    qurt_mem_cache_clean(buffer, buffer_size,
                          QURT_MEM_CACHE_INVALIDATE);

    /* Read data from the buffer... */

    /* AFTER writing data that another processor will read: */
    fill_buffer_with_results(buffer, buffer_size);
    qurt_mem_cache_clean(buffer, buffer_size,
                          QURT_MEM_CACHE_FLUSH);
}
</code></pre>
<p>The <code>qurt_mem_region_create()</code> call allocates a physically contiguous memory region suitable for sharing with other processors. The <code>QURT_MEM_REGION_SHARED</code> flag marks it for cross-processor use.</p>
<p>The cache rules for shared memory are simple but critical:</p>
<ol>
<li><p><strong>Invalidate</strong> before you <strong>read</strong>, so you see the latest data written by the ARM CPU rather than stale cache entries.</p>
</li>
<li><p><strong>Flush</strong> after you <strong>write</strong>, so the ARM CPU sees your changes rather than the old contents of main memory.</p>
</li>
</ol>
<p>Forgetting these operations causes bugs where your code is logically correct but operates on stale data.</p>
<h3 id="heading-memory-pools-for-predictable-allocation">Memory Pools for Predictable Allocation</h3>
<p>Memory pools provide O(1) allocation time, making them suitable for real-time hot paths.</p>
<pre><code class="language-c">#include &lt;qurt.h&gt;

#define BLOCK_SIZE    256
#define NUM_BLOCKS    32

/* Pool memory is statically allocated for determinism */
static char pool_memory[BLOCK_SIZE * NUM_BLOCKS] __attribute__((aligned(8)));
static qurt_mem_pool_t my_pool;

void pool_init(void)
{
    qurt_mem_pool_create(&amp;my_pool, pool_memory,
                          BLOCK_SIZE * NUM_BLOCKS,
                          BLOCK_SIZE);
}

void *pool_alloc(void)
{
    void *block = qurt_mem_pool_alloc(&amp;my_pool);
    if (!block) {
        printf("Pool exhausted!\n");
    }
    return block;
}

void pool_free(void *block)
{
    qurt_mem_pool_free(&amp;my_pool, block);
}
</code></pre>
<p>This code creates a pool of 32 blocks, each 256 bytes. The pool memory is statically allocated to avoid any dependency on <code>malloc</code> at runtime.</p>
<p><code>qurt_mem_pool_alloc()</code> returns a block in constant time, and <code>qurt_mem_pool_free()</code> returns it in constant time. If the pool is exhausted, the allocation returns NULL rather than blocking or searching for memory elsewhere.</p>
<p>This determinism makes memory pools the right choice for audio processing loops, sensor data handlers, and any other code that runs on a strict deadline.</p>
<h2 id="heading-timers-and-timing">Timers and Timing</h2>
<p>QuRT provides hardware-backed timers for precise timing. This is critical for DSP work: if you're processing audio at 48 kHz, you need a new buffer every 10.67 milliseconds, with no exceptions.</p>
<h3 id="heading-one-shot-timer">One-Shot Timer</h3>
<pre><code class="language-c">#include &lt;qurt.h&gt;
#include &lt;qurt_timer.h&gt;

qurt_timer_t my_timer;
qurt_signal_t timer_signal;

#define TIMER_EXPIRED_SIGNAL  0x01

void timer_example(void)
{
    qurt_signal_init(&amp;timer_signal);

    qurt_timer_attr_t attr;
    qurt_timer_attr_init(&amp;attr);

    /* Set timer duration: 10 milliseconds */
    qurt_timer_attr_set_duration(&amp;attr,
        qurt_timer_convert_time_to_ticks(10000,  /* microseconds */
                                          QURT_TIME_USEC));

    /* Set the signal to fire when timer expires */
    qurt_timer_attr_set_signal(&amp;attr, &amp;timer_signal);
    qurt_timer_attr_set_signal_mask(&amp;attr, TIMER_EXPIRED_SIGNAL);

    /* One-shot: fires once */
    qurt_timer_attr_set_type(&amp;attr, QURT_TIMER_ONESHOT);

    /* Create and start the timer */
    qurt_timer_create(&amp;my_timer, &amp;attr);

    /* Wait for the timer to expire */
    qurt_signal_wait(&amp;timer_signal,
                      TIMER_EXPIRED_SIGNAL,
                      QURT_SIGNAL_ATTR_WAIT_ANY);

    printf("Timer expired! 10ms have passed.\n");
    qurt_signal_clear(&amp;timer_signal, TIMER_EXPIRED_SIGNAL);

    /* Clean up */
    qurt_timer_delete(my_timer);
    qurt_signal_destroy(&amp;timer_signal);
}
</code></pre>
<p>This creates a one-shot timer that fires after 10 milliseconds. The timer is configured with an attributes structure that specifies the duration, the signal object to notify, the signal bitmask to set, and the timer type (<code>QURT_TIMER_ONESHOT</code>). When the timer expires, it sets the specified signal bit, which wakes up the thread blocked in <code>qurt_signal_wait()</code>. After handling the event, the thread clears the signal and cleans up the timer.</p>
<h3 id="heading-periodic-timer">Periodic Timer</h3>
<pre><code class="language-c">void periodic_timer_thread(void *arg)
{
    qurt_timer_t periodic_timer;
    qurt_signal_t periodic_signal;
    qurt_timer_attr_t attr;

    qurt_signal_init(&amp;periodic_signal);
    qurt_timer_attr_init(&amp;attr);

    /* Fire every 1 millisecond */
    qurt_timer_attr_set_duration(&amp;attr,
        qurt_timer_convert_time_to_ticks(1000, QURT_TIME_USEC));
    qurt_timer_attr_set_signal(&amp;attr, &amp;periodic_signal);
    qurt_timer_attr_set_signal_mask(&amp;attr, 0x01);
    qurt_timer_attr_set_type(&amp;attr, QURT_TIMER_PERIODIC);

    qurt_timer_create(&amp;periodic_timer, &amp;attr);

    int iteration = 0;
    while (iteration &lt; 1000) {
        qurt_signal_wait(&amp;periodic_signal, 0x01,
                          QURT_SIGNAL_ATTR_WAIT_ANY);
        qurt_signal_clear(&amp;periodic_signal, 0x01);

        /* This runs every 1ms */
        process_audio_frame(iteration);
        iteration++;
    }

    qurt_timer_delete(periodic_timer);
    qurt_signal_destroy(&amp;periodic_signal);
    qurt_thread_exit(QURT_EOK);
}
</code></pre>
<p>The periodic timer uses <code>QURT_TIMER_PERIODIC</code> instead of <code>QURT_TIMER_ONESHOT</code>. It fires repeatedly at the specified interval. This example runs 1000 iterations at 1 ms intervals, processing one audio frame per tick. The signal must be cleared after each iteration, or the next <code>qurt_signal_wait()</code> will return immediately.</p>
<h3 id="heading-reading-the-current-time">Reading the Current Time</h3>
<pre><code class="language-c">void timing_example(void)
{
    unsigned long long start_ticks = qurt_sysclock_get_hw_ticks();

    heavy_computation();

    unsigned long long end_ticks = qurt_sysclock_get_hw_ticks();
    unsigned long long elapsed_ticks = end_ticks - start_ticks;

    unsigned long long elapsed_us =
        qurt_timer_convert_ticks_to_time(elapsed_ticks, QURT_TIME_USEC);

    printf("Computation took %llu microseconds\n", elapsed_us);
}
</code></pre>
<p><code>qurt_sysclock_get_hw_ticks()</code> reads the hardware cycle counter, which provides the highest-resolution timing available on the DSP. <code>qurt_timer_convert_ticks_to_time()</code> converts raw ticks to human-readable units (microseconds in this case). Use this pattern to profile individual functions and identify performance bottlenecks.</p>
<h2 id="heading-interrupt-handling">Interrupt Handling</h2>
<p>On a DSP, interrupts are how hardware signals that it needs attention. QuRT provides a thread-based interrupt model that's more structured than bare-metal ISR handlers.</p>
<pre><code class="language-c">#include &lt;qurt.h&gt;
#include &lt;qurt_interrupt.h&gt;

#define MY_SENSOR_IRQ      42
#define IRQ_SIGNAL         0x01

static qurt_signal_t irq_signal;

void sensor_isr_thread(void *arg)
{
    int irq = MY_SENSOR_IRQ;

    /* Register this thread as the handler for IRQ 42 */
    qurt_interrupt_register(irq, &amp;irq_signal, IRQ_SIGNAL);

    printf("Sensor ISR thread ready, waiting for interrupts...\n");

    while (1) {
        /* Block until the hardware interrupt fires */
        unsigned int sigs = qurt_signal_wait(
            &amp;irq_signal, IRQ_SIGNAL, QURT_SIGNAL_ATTR_WAIT_ANY);

        if (sigs &amp; IRQ_SIGNAL) {
            qurt_signal_clear(&amp;irq_signal, IRQ_SIGNAL);

            /* Read sensor data quickly */
            int sensor_value = read_sensor_register();

            /* Put data in a queue for the processing thread */
            enqueue_sensor_data(sensor_value);

            /* Signal the processing thread */
            qurt_signal_set(&amp;processing_signal, DATA_READY);

            /* Re-enable the interrupt */
            qurt_interrupt_acknowledge(irq);
        }
    }
}
</code></pre>
<p>QuRT ISRs are different from bare-metal ISRs. They run in a dedicated thread context, which means you can use mutexes and signals inside them. But the ISR thread should still do minimal work: read the hardware register, enqueue the data, signal a processing thread, and acknowledge the interrupt. All expensive computation should happen in a separate, lower-priority processing thread.</p>
<pre><code class="language-plaintext">Hardware IRQ
     │
     ▼
ISR Thread (high priority)     Processing Thread (medium priority)
┌──────────────────┐          ┌──────────────────────────┐
│ Read HW register │          │ Wait for DATA_READY      │
│ Enqueue data     │ ──────►  │ Dequeue data             │
│ Signal "ready"   │          │ Run FFT / filter / etc.  │
│ ACK interrupt    │          │ Write results            │
└──────────────────┘          └──────────────────────────┘
</code></pre>
<p>This diagram shows the ISR offloading pattern. The ISR thread on the left handles the hardware interrupt with minimal latency: it reads the sensor register, enqueues the raw data, signals the processing thread, and acknowledges the interrupt so it can fire again. The processing thread on the right does the expensive work (FFT, filtering, ML inference) at a lower priority.</p>
<p>This design ensures that the ISR thread is always available to service the next hardware interrupt, even if the processing thread is still working on the previous sample.</p>
<h2 id="heading-pipes-and-message-queues">Pipes and Message Queues</h2>
<p>QuRT provides built-in pipe support for safe, structured inter-thread communication. Pipes are fixed-size message queues with blocking send and receive operations.</p>
<pre><code class="language-c">#include &lt;qurt.h&gt;
#include &lt;qurt_pipe.h&gt;

#define PIPE_ELEMENTS   16
#define ELEMENT_SIZE    sizeof(sensor_msg_t)

typedef struct {
    int sensor_id;
    int value;
    unsigned long long timestamp;
} sensor_msg_t;

/* Pipe buffer must be allocated by you */
static char pipe_buffer[PIPE_ELEMENTS * ELEMENT_SIZE]
    __attribute__((aligned(8)));

qurt_pipe_t sensor_pipe;

void pipe_init(void)
{
    qurt_pipe_attr_t attr;
    qurt_pipe_attr_init(&amp;attr);
    qurt_pipe_attr_set_buffer(&amp;attr, pipe_buffer);
    qurt_pipe_attr_set_buffer_partition(&amp;attr, PIPE_ELEMENTS);
    qurt_pipe_attr_set_elements(&amp;attr, PIPE_ELEMENTS);
    qurt_pipe_attr_set_element_size(&amp;attr, ELEMENT_SIZE);

    qurt_pipe_create(&amp;sensor_pipe, &amp;attr);
}

/* Producer: send sensor data into the pipe */
void sensor_reader_thread(void *arg)
{
    while (1) {
        sensor_msg_t msg;
        msg.sensor_id = 1;
        msg.value = read_accelerometer();
        msg.timestamp = qurt_sysclock_get_hw_ticks();

        /* Blocking send: waits if pipe is full */
        qurt_pipe_send(&amp;sensor_pipe, (char *)&amp;msg, ELEMENT_SIZE);
    }
}

/* Consumer: receive sensor data from the pipe */
void data_processor_thread(void *arg)
{
    sensor_msg_t msg;

    while (1) {
        /* Blocking receive: waits if pipe is empty */
        qurt_pipe_receive(&amp;sensor_pipe, (char *)&amp;msg, ELEMENT_SIZE);

        printf("Sensor %d: value=%d at tick=%llu\n",
               msg.sensor_id, msg.value, msg.timestamp);

        process_sensor_reading(&amp;msg);
    }
}
</code></pre>
<p>A QuRT pipe is configured with a statically allocated buffer, a number of elements, and an element size. Like stacks, the buffer memory is your responsibility. <code>qurt_pipe_send()</code> copies a message into the pipe and blocks if the pipe is full. <code>qurt_pipe_receive()</code> copies a message out and blocks if the pipe is empty. The pipe handles all internal synchronization, so you don't need a separate mutex.</p>
<p>Pipes are a natural fit for the sensor data pattern shown here: the reader thread samples hardware at a fixed rate and pushes messages into the pipe, while the processor thread pulls messages out and handles them. The pipe provides buffering and backpressure automatically.</p>
<h2 id="heading-qurt-and-fastrpc">QuRT and FastRPC</h2>
<p>In real Qualcomm devices, you rarely use QuRT alone. Your Android or Linux application on the ARM CPU offloads compute-intensive work to the DSP using <strong>FastRPC</strong> (Fast Remote Procedure Call). The following diagram shows the full pipeline:</p>
<pre><code class="language-plaintext">┌───────────────────────────────────────────────────────────────┐
│                         ARM CPU Side                          │
│                                                               │
│   your_app.c                                                  │
│   ┌───────────────────────────────────────────────────┐       │
│   │  #include "my_dsp_module.h"  // auto-generated    │       │
│   │                                                   │       │
│   │  // This looks like a normal function call,       │       │
│   │  // but it actually executes on the DSP!          │       │
│   │  result = my_dsp_module_process_audio(            │       │
│   │      input_buffer, output_buffer, num_samples);   │       │
│   └───────────────────┬───────────────────────────────┘       │
│                       │ FastRPC                               │
└───────────────────────┼───────────────────────────────────────┘
            (crosses processor boundary)          
┌───────────────────────┼───────────────────────────────────────┐
│                       ▼                                       │
│                  DSP Side (QuRT)                              │
│   my_dsp_module_skel.c  // auto-generated skeleton            │
│   ┌───────────────────────────────────────────────────┐       │
│   │  int my_dsp_module_process_audio(                 │       │
│   │      const int16_t *input,                        │       │
│   │      int16_t *output,                             │       │
│   │      int num_samples)                             │       │
│   │  {                                                │       │
│   │      // This runs on the Hexagon DSP under QuRT   │       │
│   │      apply_noise_reduction(input, output,         │       │
│   │                             num_samples);         │       │
│   │      return 0;                                    │       │
│   │  }                                                │       │
│   └───────────────────────────────────────────────────┘       │
└───────────────────────────────────────────────────────────────┘
</code></pre>
<p>This diagram shows the FastRPC architecture. On the ARM CPU side, your application calls a function that appears to be a normal C function. Under the hood, FastRPC serializes the arguments, sends them across the processor boundary to the Hexagon DSP, executes the function under QuRT, and returns the result. The programmer experience is a transparent remote procedure call.</p>
<h3 id="heading-step-1-define-the-interface-idl-file">Step 1: Define the Interface (IDL File)</h3>
<p>Create a <code>.idl</code> file that describes the functions the ARM can call on the DSP:</p>
<pre><code class="language-idl">/* my_dsp_module.idl */
#include "remote.idl"
#include "AEEStdDef.idl"

interface my_dsp_module {

    /* Simple computation */
    long process_audio(
        in sequence&lt;short&gt; input,
        rout sequence&lt;short&gt; output,
        in long num_samples
    );

    /* Matrix multiply offload */
    long matrix_multiply(
        in sequence&lt;float&gt; mat_a,
        in sequence&lt;float&gt; mat_b,
        rout sequence&lt;float&gt; result,
        in long rows_a,
        in long cols_a,
        in long cols_b
    );
};
</code></pre>
<p>The IDL (Interface Definition Language) file defines the cross-processor API. Each function specifies its parameters with direction qualifiers: <code>in</code> for data flowing from ARM to DSP, <code>rout</code> for data flowing from DSP back to ARM. The <code>sequence&lt;type&gt;</code> syntax specifies a variable-length array. The Hexagon SDK's IDL compiler generates stub code for the ARM side and skeleton code for the DSP side from this definition.</p>
<h3 id="heading-step-2-implement-the-dsp-side">Step 2: Implement the DSP Side</h3>
<pre><code class="language-c">/* my_dsp_module_imp.c - DSP implementation */

#include "my_dsp_module.h"
#include &lt;qurt.h&gt;
#include &lt;stdio.h&gt;

int my_dsp_module_process_audio(
    const int16_t *input, int input_len,
    int16_t *output, int output_len,
    int num_samples)
{
    if (!input || !output || num_samples &lt;= 0) {
        return -1;
    }

    /* Invalidate cache: ARM wrote this data */
    qurt_mem_cache_clean((void *)input,
                          num_samples * sizeof(int16_t),
                          QURT_MEM_CACHE_INVALIDATE);

    /* Process on the DSP */
    for (int i = 0; i &lt; num_samples; i++) {
        /* Simple noise gate */
        if (abs(input[i]) &lt; 100) {
            output[i] = 0;
        } else {
            output[i] = input[i];
        }
    }

    /* Flush cache: ARM will read this data */
    qurt_mem_cache_clean(output,
                          num_samples * sizeof(int16_t),
                          QURT_MEM_CACHE_FLUSH);

    return 0;
}
</code></pre>
<p>The DSP implementation receives the input buffer that the ARM CPU wrote. Before reading it, the code invalidates the cache so the DSP sees the latest data from main memory rather than stale cache entries. After writing the output, the code flushes the cache so the ARM CPU sees the DSP's results. The actual processing (a simple noise gate in this example) runs between the cache operations.</p>
<h3 id="heading-step-3-implement-the-arm-side">Step 3: Implement the ARM Side</h3>
<pre><code class="language-c">/* main_arm.c - ARM/Android application */

#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;rpcmem.h&gt;
#include "my_dsp_module.h"

int main(void)
{
    int num_samples = 1024;

    /* Use ION memory for zero-copy sharing with DSP */
    rpcmem_init();

    int16_t *input = (int16_t *)rpcmem_alloc(
        RPCMEM_HEAP_ID_SYSTEM,
        RPCMEM_DEFAULT_FLAGS,
        num_samples * sizeof(int16_t));

    int16_t *output = (int16_t *)rpcmem_alloc(
        RPCMEM_HEAP_ID_SYSTEM,
        RPCMEM_DEFAULT_FLAGS,
        num_samples * sizeof(int16_t));

    if (!input || !output) {
        printf("rpcmem_alloc failed!\n");
        return -1;
    }

    /* Fill input with audio data */
    for (int i = 0; i &lt; num_samples; i++) {
        input[i] = (int16_t)(i % 256);
    }

    /* This call goes to the DSP via FastRPC */
    int result = my_dsp_module_process_audio(
        input, num_samples,
        output, num_samples,
        num_samples);

    if (result != 0) {
        printf("DSP processing failed: %d\n", result);
    } else {
        printf("DSP processing succeeded!\n");
        printf("First 10 output samples: ");
        for (int i = 0; i &lt; 10; i++) {
            printf("%d ", output[i]);
        }
        printf("\n");
    }

    rpcmem_free(input);
    rpcmem_free(output);
    rpcmem_deinit();

    return 0;
}
</code></pre>
<p>The ARM side uses <code>rpcmem_alloc()</code> to allocate ION memory, which is a shared memory region accessible by both the ARM CPU and the Hexagon DSP without copying. The call to <code>my_dsp_module_process_audio()</code> looks like a normal function call, but FastRPC transparently routes it to the DSP. When the call returns, the output buffer contains the DSP's results.</p>
<h3 id="heading-building-the-complete-project">Building the Complete Project</h3>
<p>A FastRPC project requires two SCons builds: one for the ARM CPU side and one for the Hexagon DSP side. Each side has its own <code>.min</code> file (<code>android.min</code> and <code>hexagon.min</code>), and both are processed by the SDK's <code>SConstruct</code>.</p>
<pre><code class="language-bash">cd $HEXAGON_SDK_ROOT

# Build for ARM target (Android) via make wrapper
make V=android_Release tree=my_dsp_module

# Build for Hexagon DSP via make wrapper
make V=hexagon_Release_dynamic_toolv84_v66 tree=my_dsp_module

# Or invoke SCons directly for both variants
python tools/build/scons/scons.py \
    V=android_Release \
    V=hexagon_Release_dynamic_toolv84_v66 \
    my_dsp_module

# Push to device
adb push android_Release/ship/my_dsp_module /data/local/tmp/
adb push hexagon_Release_dynamic_toolv84_v66/ship/libmy_dsp_module_skel.so \
    /data/local/tmp/

# Run it
adb shell "cd /data/local/tmp &amp;&amp; ./my_dsp_module"
</code></pre>
<p>The build produces two outputs: an ARM executable (compiled from the stub and your <code>main_arm.c</code>) and a Hexagon shared library (the <code>_skel.so</code> file, compiled from your DSP implementation). SCons handles the IDL compilation step automatically: it detects the <code>.idl</code> file, generates the stub and skeleton C source files, and includes them in the appropriate variant build. Both outputs are pushed to the device.</p>
<p>When the ARM executable runs and calls a FastRPC function, the system loads the skeleton library onto the DSP and routes the call through.</p>
<h2 id="heading-building-a-sensor-fusion-pipeline">Building a Sensor Fusion Pipeline</h2>
<p>This section brings together threads, synchronization, timers, and signals into a complete, realistic QuRT application. The pipeline reads from three simulated sensors (accelerometer, gyroscope, magnetometer), fuses the data using a complementary filter, and reports orientation at 100 Hz.</p>
<pre><code class="language-c">/*
 * sensor_fusion.c - Multi-sensor fusion pipeline on QuRT
 *
 * Architecture:
 *   [Accel ISR] ──► [Fusion Thread] ──► [Report Thread]
 *   [Gyro ISR]  ──►       ▲
 *   [Mag ISR]   ──►       │
 *                    [Timer Thread]
 *                    (triggers fusion every 10ms)
 */

#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;qurt.h&gt;
#include &lt;qurt_timer.h&gt;

/* Configuration */
#define STACK_SIZE          8192
#define FUSION_PERIOD_US    10000   /* 10ms = 100Hz fusion rate */
#define QUEUE_DEPTH         32

/* Data types */
typedef struct {
    float x, y, z;
    unsigned long long timestamp;
} vec3_sample_t;

typedef struct {
    vec3_sample_t accel;
    vec3_sample_t gyro;
    vec3_sample_t mag;
    float roll, pitch, yaw;
} fused_state_t;

/* Thread stacks */
static char accel_stack[STACK_SIZE]  __attribute__((aligned(8)));
static char gyro_stack[STACK_SIZE]   __attribute__((aligned(8)));
static char mag_stack[STACK_SIZE]    __attribute__((aligned(8)));
static char fusion_stack[STACK_SIZE] __attribute__((aligned(8)));
static char report_stack[STACK_SIZE] __attribute__((aligned(8)));

/* Shared state */
static vec3_sample_t latest_accel;
static vec3_sample_t latest_gyro;
static vec3_sample_t latest_mag;
static fused_state_t latest_fused;

static qurt_mutex_t sensor_mutex;
static qurt_mutex_t fused_mutex;
static qurt_signal_t fusion_signal;
static qurt_signal_t report_signal;

#define SIG_FUSION_TICK    0x01
#define SIG_NEW_FUSED_DATA 0x01
#define SIG_SHUTDOWN       0x80

static volatile int running = 1;

/* Simulated sensor reads */
static void read_accelerometer(vec3_sample_t *sample)
{
    sample-&gt;x = 0.01f;
    sample-&gt;y = 0.02f;
    sample-&gt;z = 9.81f;
    sample-&gt;timestamp = qurt_sysclock_get_hw_ticks();
}

static void read_gyroscope(vec3_sample_t *sample)
{
    sample-&gt;x = 0.001f;
    sample-&gt;y = -0.002f;
    sample-&gt;z = 0.0005f;
    sample-&gt;timestamp = qurt_sysclock_get_hw_ticks();
}

static void read_magnetometer(vec3_sample_t *sample)
{
    sample-&gt;x = 25.0f;
    sample-&gt;y = -5.0f;
    sample-&gt;z = 40.0f;
    sample-&gt;timestamp = qurt_sysclock_get_hw_ticks();
}

/* Accelerometer thread */
void accel_thread(void *arg)
{
    printf("[Accel] Thread started\n");

    while (running) {
        vec3_sample_t sample;
        read_accelerometer(&amp;sample);

        qurt_mutex_lock(&amp;sensor_mutex);
        latest_accel = sample;
        qurt_mutex_unlock(&amp;sensor_mutex);

        /* ~400Hz sample rate */
        qurt_timer_sleep(2500);
    }

    printf("[Accel] Thread exiting\n");
    qurt_thread_exit(QURT_EOK);
}

/* Gyroscope thread */
void gyro_thread(void *arg)
{
    printf("[Gyro] Thread started\n");

    while (running) {
        vec3_sample_t sample;
        read_gyroscope(&amp;sample);

        qurt_mutex_lock(&amp;sensor_mutex);
        latest_gyro = sample;
        qurt_mutex_unlock(&amp;sensor_mutex);

        /* 1kHz sample rate */
        qurt_timer_sleep(1000);
    }

    printf("[Gyro] Thread exiting\n");
    qurt_thread_exit(QURT_EOK);
}

/* Magnetometer thread */
void mag_thread(void *arg)
{
    printf("[Mag] Thread started\n");

    while (running) {
        vec3_sample_t sample;
        read_magnetometer(&amp;sample);

        qurt_mutex_lock(&amp;sensor_mutex);
        latest_mag = sample;
        qurt_mutex_unlock(&amp;sensor_mutex);

        /* 100Hz sample rate */
        qurt_timer_sleep(10000);
    }

    printf("[Mag] Thread exiting\n");
    qurt_thread_exit(QURT_EOK);
}

/* Simplified complementary filter */
static void compute_orientation(
    const vec3_sample_t *accel,
    const vec3_sample_t *gyro,
    const vec3_sample_t *mag,
    fused_state_t *state)
{
    float dt = 0.01f;

    float accel_roll = atan2f(accel-&gt;y, accel-&gt;z) * 57.2958f;
    float accel_pitch = atan2f(-accel-&gt;x,
        sqrtf(accel-&gt;y * accel-&gt;y + accel-&gt;z * accel-&gt;z)) * 57.2958f;

    /* Trust gyro short-term, accel long-term */
    state-&gt;roll = 0.98f * (state-&gt;roll + gyro-&gt;x * dt * 57.2958f)
                + 0.02f * accel_roll;
    state-&gt;pitch = 0.98f * (state-&gt;pitch + gyro-&gt;y * dt * 57.2958f)
                 + 0.02f * accel_pitch;

    state-&gt;yaw = atan2f(mag-&gt;y, mag-&gt;x) * 57.2958f;

    state-&gt;accel = *accel;
    state-&gt;gyro = *gyro;
    state-&gt;mag = *mag;
}

/* Fusion thread (runs every 10ms) */
void fusion_thread(void *arg)
{
    qurt_timer_t fusion_timer;
    qurt_timer_attr_t timer_attr;

    printf("[Fusion] Thread started\n");

    qurt_timer_attr_init(&amp;timer_attr);
    qurt_timer_attr_set_duration(&amp;timer_attr,
        qurt_timer_convert_time_to_ticks(FUSION_PERIOD_US,
                                          QURT_TIME_USEC));
    qurt_timer_attr_set_signal(&amp;timer_attr, &amp;fusion_signal);
    qurt_timer_attr_set_signal_mask(&amp;timer_attr, SIG_FUSION_TICK);
    qurt_timer_attr_set_type(&amp;timer_attr, QURT_TIMER_PERIODIC);

    qurt_timer_create(&amp;fusion_timer, &amp;timer_attr);

    while (running) {
        unsigned int sigs = qurt_signal_wait(
            &amp;fusion_signal,
            SIG_FUSION_TICK | SIG_SHUTDOWN,
            QURT_SIGNAL_ATTR_WAIT_ANY);

        if (sigs &amp; SIG_SHUTDOWN) break;

        qurt_signal_clear(&amp;fusion_signal, SIG_FUSION_TICK);

        /* Snapshot sensor data under lock */
        vec3_sample_t a, g, m;
        qurt_mutex_lock(&amp;sensor_mutex);
        a = latest_accel;
        g = latest_gyro;
        m = latest_mag;
        qurt_mutex_unlock(&amp;sensor_mutex);

        /* Run the fusion algorithm (no lock needed, local data) */
        fused_state_t state;
        qurt_mutex_lock(&amp;fused_mutex);
        state = latest_fused;
        qurt_mutex_unlock(&amp;fused_mutex);

        compute_orientation(&amp;a, &amp;g, &amp;m, &amp;state);

        /* Publish fused result */
        qurt_mutex_lock(&amp;fused_mutex);
        latest_fused = state;
        qurt_mutex_unlock(&amp;fused_mutex);

        /* Notify reporter */
        qurt_signal_set(&amp;report_signal, SIG_NEW_FUSED_DATA);
    }

    qurt_timer_delete(fusion_timer);
    printf("[Fusion] Thread exiting\n");
    qurt_thread_exit(QURT_EOK);
}

/* Reporting thread */
void report_thread(void *arg)
{
    int report_count = 0;

    printf("[Report] Thread started\n");

    while (running) {
        unsigned int sigs = qurt_signal_wait(
            &amp;report_signal,
            SIG_NEW_FUSED_DATA | SIG_SHUTDOWN,
            QURT_SIGNAL_ATTR_WAIT_ANY);

        if (sigs &amp; SIG_SHUTDOWN) break;

        qurt_signal_clear(&amp;report_signal, SIG_NEW_FUSED_DATA);

        fused_state_t state;
        qurt_mutex_lock(&amp;fused_mutex);
        state = latest_fused;
        qurt_mutex_unlock(&amp;fused_mutex);

        /* Report every 100th update (once per second at 100Hz) */
        if (++report_count % 100 == 0) {
            printf("[Report] Orientation - Roll: %.2f  Pitch: %.2f  "
                   "Yaw: %.2f  (update #%d)\n",
                   state.roll, state.pitch, state.yaw, report_count);
        }
    }

    printf("[Report] Thread exiting\n");
    qurt_thread_exit(QURT_EOK);
}

/* Main */
int main(void)
{
    qurt_thread_t threads[5];
    qurt_thread_attr_t attr;
    int status;

    printf("=== Sensor Fusion Pipeline Starting ===\n");

    /* Initialize synchronization primitives */
    qurt_mutex_init(&amp;sensor_mutex);
    qurt_mutex_init(&amp;fused_mutex);
    qurt_signal_init(&amp;fusion_signal);
    qurt_signal_init(&amp;report_signal);
    memset(&amp;latest_fused, 0, sizeof(latest_fused));

    struct {
        const char *name;
        char *stack;
        int priority;
        void (*func)(void *);
    } thread_configs[] = {
        {"accel_reader", accel_stack,  60, accel_thread},
        {"gyro_reader",  gyro_stack,   60, gyro_thread},
        {"mag_reader",   mag_stack,    70, mag_thread},
        {"fusion",       fusion_stack, 80, fusion_thread},
        {"reporter",     report_stack, 120, report_thread},
    };

    /* Create all threads */
    for (int i = 0; i &lt; 5; i++) {
        qurt_thread_attr_init(&amp;attr);
        qurt_thread_attr_set_name(&amp;attr, thread_configs[i].name);
        qurt_thread_attr_set_stack_addr(&amp;attr, thread_configs[i].stack);
        qurt_thread_attr_set_stack_size(&amp;attr, STACK_SIZE);
        qurt_thread_attr_set_priority(&amp;attr, thread_configs[i].priority);

        int result = qurt_thread_create(&amp;threads[i], &amp;attr,
                                         thread_configs[i].func, NULL);
        if (result != QURT_EOK) {
            printf("Failed to create thread '%s': %d\n",
                   thread_configs[i].name, result);
            return -1;
        }
        printf("Created thread '%s' (priority %d)\n",
               thread_configs[i].name, thread_configs[i].priority);
    }

    /* Let it run for 10 seconds */
    printf("Pipeline running for 10 seconds...\n");
    qurt_timer_sleep(10000000);

    /* Shutdown */
    printf("Shutting down...\n");
    running = 0;
    qurt_signal_set(&amp;fusion_signal, SIG_SHUTDOWN);
    qurt_signal_set(&amp;report_signal, SIG_SHUTDOWN);

    /* Wait for all threads to finish */
    for (int i = 0; i &lt; 5; i++) {
        qurt_thread_join(threads[i], &amp;status);
    }

    /* Clean up */
    qurt_mutex_destroy(&amp;sensor_mutex);
    qurt_mutex_destroy(&amp;fused_mutex);
    qurt_signal_destroy(&amp;fusion_signal);
    qurt_signal_destroy(&amp;report_signal);

    printf("=== Sensor Fusion Pipeline Complete ===\n");
    return 0;
}
</code></pre>
<p>This pipeline demonstrates several QuRT patterns working together.</p>
<p>Three sensor reader threads run at the highest priority (60 for accel and gyro, 70 for the slower magnetometer) and continuously write the latest samples into shared state under a mutex.</p>
<p>A fusion thread, triggered by a periodic timer every 10 ms, snapshots all three sensor readings, runs a complementary filter to compute roll, pitch, and yaw, and publishes the fused result.</p>
<p>A reporting thread at the lowest priority (120) receives a signal each time new fused data is available and logs orientation once per second.</p>
<h3 id="heading-priority-assignment">Priority Assignment</h3>
<pre><code class="language-plaintext">Priority 60:  Sensor readers (highest priority, never miss hardware data)
Priority 80:  Fusion engine (runs every 10ms, must finish quickly)
Priority 120: Reporter (lowest priority, only logging)
</code></pre>
<p>The priority assignments follow a strict rule: threads closer to hardware get higher priority. If the fusion thread takes too long, the reporter waits. That's acceptable because a delayed log message has no real-time consequence. If a sensor read gets delayed, the fusion algorithm operates on stale data.</p>
<p>In a real application controlling a drone or robot, stale IMU data means incorrect orientation estimates, which can lead to physical failures.</p>
<h2 id="heading-debugging-qurt-applications">Debugging QuRT Applications</h2>
<p>QuRT debugging is more limited than Linux debugging. There's no <code>gdb</code> with a TUI, and error messages from crashes are often unhelpful. The following techniques form a practical debugging toolkit.</p>
<h3 id="heading-printf-debugging">Printf Debugging</h3>
<pre><code class="language-c">#include &lt;stdio.h&gt;

void debug_example(void)
{
    printf("[%s:%d] value = %d\n", __func__, __LINE__, some_var);
}
</code></pre>
<p>QuRT supports <code>printf</code> through a semi-hosting mechanism. On the simulator, output goes to stdout. On hardware, it goes to a DIAG buffer (similar to Android's logcat). This is the most common debugging technique in QuRT development.</p>
<h3 id="heading-qurt-error-codes">QuRT Error Codes</h3>
<pre><code class="language-c">switch (result) {
    case QURT_EOK:
        break;
    case QURT_EINVALID:
        printf("Invalid argument\n");
        break;
    case QURT_EFAILED:
        printf("General failure\n");
        break;
    case QURT_EMEM:
        printf("Out of memory\n");
        break;
    case QURT_ENOTALLOWED:
        printf("Operation not allowed (check permissions)\n");
        break;
    case QURT_ETIMEOUT:
        printf("Operation timed out\n");
        break;
    default:
        printf("Unknown error: %d\n", result);
}
</code></pre>
<p>Always check return values from QuRT API calls. These are the error codes you'll encounter most frequently.</p>
<p><code>QURT_EINVALID</code> usually means a bad parameter (unaligned stack, null pointer, out-of-range priority). <code>QURT_EMEM</code> means the kernel ran out of memory for internal structures. <code>QURT_ENOTALLOWED</code> often indicates a permissions issue on hardware.</p>
<h3 id="heading-thread-state-inspection">Thread State Inspection</h3>
<pre><code class="language-c">void dump_thread_info(void)
{
    qurt_thread_t tid = qurt_thread_get_id();
    char name[QURT_THREAD_ATTR_NAME_MAXLEN];

    qurt_thread_get_name(name, sizeof(name));

    printf("Thread: %s (ID: %lu)\n", name, tid);
}
</code></pre>
<p>This function prints the current thread's name and ID, which is useful when you have multiple threads writing to the same log output and need to distinguish which thread produced each message.</p>
<h3 id="heading-stack-overflow-detection">Stack Overflow Detection</h3>
<pre><code class="language-c">#define STACK_CANARY 0xDEADBEEF

static char my_stack[STACK_SIZE] __attribute__((aligned(8)));

void init_stack_canary(void)
{
    /* Write canary at the bottom of the stack */
    ((unsigned int *)my_stack)[0] = STACK_CANARY;
    ((unsigned int *)my_stack)[1] = STACK_CANARY;
}

void check_stack_canary(void)
{
    if (((unsigned int *)my_stack)[0] != STACK_CANARY ||
        ((unsigned int *)my_stack)[1] != STACK_CANARY) {
        printf("STACK OVERFLOW DETECTED!\n");
    }
}
</code></pre>
<p>QuRT doesn't detect stack overflows. This canary pattern writes a known value at the bottom of the stack before the thread starts. If the stack grows downward past its bounds, it overwrites the canary value. Periodically checking the canary (or checking it on thread exit) catches overflows that would otherwise manifest as mysterious, unrelated crashes.</p>
<h3 id="heading-using-the-hexagon-simulator">Using the Hexagon Simulator</h3>
<pre><code class="language-bash"># Run with instruction tracing
hexagon-sim --timing --pmu_statsfile stats.txt \
    --cosim_file osam.cfg \
    -- bootimg.pbn -- my_app.so

# The stats file gives you:
# - Total cycles
# - Cache hit/miss rates
# - Stall cycles
# - Instructions per cycle (IPC)
</code></pre>
<p>The <code>--timing</code> flag enables cycle-accurate simulation, and <code>--pmu_statsfile</code> writes performance counter data to a file. The stats file reports total cycles, cache hit and miss rates, stall cycles, and instructions per cycle (IPC). This data is essential for identifying whether your bottleneck is compute-bound, memory-bound, or stall-bound.</p>
<h2 id="heading-common-pitfalls">Common Pitfalls</h2>
<h3 id="heading-pitfall-1-forgetting-to-exit-threads">Pitfall 1: Forgetting to Exit Threads</h3>
<pre><code class="language-c">/* BAD: thread function returns without exit */
void bad_thread(void *arg) {
    do_work();
    return;  /* CRASH or undefined behavior */
}

/* GOOD */
void good_thread(void *arg) {
    do_work();
    qurt_thread_exit(QURT_EOK);
}
</code></pre>
<p>A QuRT thread that returns from its entry function without calling <code>qurt_thread_exit()</code> causes undefined behavior. The kernel set the link register to <code>qurt_thread_exit</code> as a safety net during thread creation, but you shouldn't rely on this. Always call <code>qurt_thread_exit()</code> explicitly.</p>
<h3 id="heading-pitfall-2-stack-allocated-in-wrong-scope">Pitfall 2: Stack Allocated in Wrong Scope</h3>
<pre><code class="language-c">/* BAD: stack is on the calling thread's stack */
void create_thread_bad(void) {
    char stack[4096];
    qurt_thread_attr_set_stack_addr(&amp;attr, stack);
    qurt_thread_create(&amp;tid, &amp;attr, func, NULL);
}   /* stack disappears here, new thread crashes */

/* GOOD: use static or heap allocation */
static char stack[4096] __attribute__((aligned(8)));
void create_thread_good(void) {
    qurt_thread_attr_set_stack_addr(&amp;attr, stack);
    qurt_thread_create(&amp;tid, &amp;attr, func, NULL);
}
</code></pre>
<p>The stack memory must outlive the thread that uses it. If you allocate the stack as a local variable in a function, it's freed when that function returns, but the thread may still be running. Use static allocation (as shown) or heap allocation with careful lifetime management.</p>
<h3 id="heading-pitfall-3-priority-inversion-without-awareness">Pitfall 3: Priority Inversion Without Awareness</h3>
<pre><code class="language-c">/* BAD: manual spinlock, no priority inheritance */
volatile int lock = 0;
while (__sync_lock_test_and_set(&amp;lock, 1)) { /* spin */ }

/* GOOD: QuRT mutex with priority inheritance */
qurt_mutex_lock(&amp;my_mutex);
</code></pre>
<p>If a high-priority thread spins on a manual spinlock held by a low-priority thread, and a medium-priority thread preempts the lock holder, the high-priority thread is effectively blocked by the medium-priority thread.</p>
<p>QuRT mutexes solve this with automatic priority inheritance: the lock holder is temporarily boosted to the priority of the highest-priority waiter. Manual spinlocks don't get this treatment.</p>
<h3 id="heading-pitfall-4-unaligned-memory">Pitfall 4: Unaligned Memory</h3>
<pre><code class="language-c">/* BAD */
char stack[4096];

/* GOOD */
char stack[4096] __attribute__((aligned(8)));

/* For DMA buffers, you often need 256-byte alignment */
char dma_buffer[1024] __attribute__((aligned(256)));
</code></pre>
<p>Thread stacks must be 8-byte aligned. DMA buffers typically require 256-byte alignment. Unaligned memory causes hard faults on the Hexagon architecture that produce minimal diagnostic output.</p>
<h3 id="heading-pitfall-5-blocking-in-isr-context">Pitfall 5: Blocking in ISR Context</h3>
<pre><code class="language-c">/* BAD: mutex_lock may block indefinitely */
void isr_handler(void *arg) {
    qurt_mutex_lock(&amp;some_mutex);
    qurt_mutex_unlock(&amp;some_mutex);
}

/* GOOD: non-blocking try_lock with fallback */
void isr_handler(void *arg) {
    if (qurt_mutex_try_lock(&amp;some_mutex) == QURT_EOK) {
        /* Quick update */
        qurt_mutex_unlock(&amp;some_mutex);
    } else {
        /* Defer to processing thread */
        qurt_signal_set(&amp;deferred_signal, DEFERRED_WORK);
    }
}
</code></pre>
<p>Although QuRT ISR threads can technically call blocking APIs, doing so in a high-priority interrupt handler freezes interrupt processing until the blocking condition is resolved. Use <code>qurt_mutex_try_lock()</code> for non-blocking attempts, and defer work to a lower-priority thread using signals if the lock is unavailable.</p>
<h2 id="heading-performance-optimization">Performance Optimization</h2>
<h3 id="heading-using-hvx-hexagon-vector-extensions">Using HVX (Hexagon Vector Extensions)</h3>
<pre><code class="language-c">#include &lt;hexagon_types.h&gt;
#include &lt;hvx_hexagon_protos.h&gt;

/* Process 128 bytes at once with HVX */
void vectorized_gain(int16_t *audio, int num_samples, int16_t gain)
{
    HVX_Vector *vptr = (HVX_Vector *)audio;
    HVX_Vector vgain = Q6_Vh_vsplat_R(gain);
    int num_vectors = num_samples * sizeof(int16_t) / sizeof(HVX_Vector);

    for (int i = 0; i &lt; num_vectors; i++) {
        vptr[i] = Q6_Vh_vmpy_VhVh_sat(vptr[i], vgain);
    }
}
</code></pre>
<p>HVX provides 128-byte SIMD operations on the Hexagon DSP. The <code>Q6_Vh_vsplat_R</code> intrinsic broadcasts a scalar value across all lanes of a vector register. <code>Q6_Vh_vmpy_VhVh_sat</code> performs a saturating multiply of two half-word vectors. A single HVX instruction processes 64 16-bit samples, which can yield an order-of-magnitude speedup over scalar code for audio and signal processing workloads.</p>
<h3 id="heading-locking-l2-cache-for-hot-data">Locking L2 Cache for Hot Data</h3>
<pre><code class="language-c">void lock_cache_example(void)
{
    extern float fft_twiddle_factors[];
    size_t twiddle_size = 1024 * sizeof(float);

    /* Pin data in L2 to prevent eviction */
    qurt_mem_l2cache_lock((unsigned int)fft_twiddle_factors,
                           twiddle_size);

    /* When done: */
    qurt_mem_l2cache_unlock((unsigned int)fft_twiddle_factors,
                             twiddle_size);
}
</code></pre>
<p><code>qurt_mem_l2cache_lock()</code> pins a memory region in the L2 cache, preventing it from being evicted by other cache traffic. This is useful for lookup tables and constant data that are accessed frequently in hot loops (such as FFT twiddle factors).</p>
<p>Locking too much data in L2 reduces the cache available for other threads, so use this technique selectively.</p>
<h3 id="heading-avoiding-dynamic-memory-in-hot-paths">Avoiding Dynamic Memory in Hot Paths</h3>
<pre><code class="language-c">/* BAD: malloc in the audio processing loop */
void process_audio_bad(void) {
    while (1) {
        float *temp = malloc(1024 * sizeof(float));
        process(temp);
        free(temp);
    }
}

/* GOOD: pre-allocate everything */
static float temp_buffer[1024];
void process_audio_good(void) {
    while (1) {
        process(temp_buffer);
    }
}
</code></pre>
<p><code>malloc</code> and <code>free</code> have non-deterministic execution time because they may traverse free lists, split or coalesce blocks, and in the worst case, request additional memory from the kernel.</p>
<p>In a real-time audio processing loop running at 48 kHz, a single slow allocation can cause an audible glitch. Pre-allocate all buffers during initialization and reuse them.</p>
<h2 id="heading-api-quick-reference">API Quick Reference</h2>
<pre><code class="language-plaintext">┌─────────────────────────────────────────────────────────────────┐
│                    QuRT API Quick Reference                     │
├─────────────────┬───────────────────────────────────────────────┤
│ THREADS         │                                               │
│  create         │ qurt_thread_create(&amp;id, &amp;attr, func, arg)     │
│  exit           │ qurt_thread_exit(status)                      │
│  join           │ qurt_thread_join(id, &amp;status)                 │
│  get id         │ qurt_thread_get_id()                          │
│  sleep          │ qurt_timer_sleep(usec)                        │
├─────────────────┼───────────────────────────────────────────────┤
│ MUTEX           │                                               │
│  init           │ qurt_mutex_init(&amp;mutex)                       │
│  lock           │ qurt_mutex_lock(&amp;mutex)                       │
│  try lock       │ qurt_mutex_try_lock(&amp;mutex)                   │
│  unlock         │ qurt_mutex_unlock(&amp;mutex)                     │
│  destroy        │ qurt_mutex_destroy(&amp;mutex)                    │
├─────────────────┼───────────────────────────────────────────────┤
│ SIGNALS         │                                               │
│  init           │ qurt_signal_init(&amp;signal)                     │
│  wait           │ qurt_signal_wait(&amp;sig, mask, attr)            │
│  set            │ qurt_signal_set(&amp;signal, mask)                │
│  clear          │ qurt_signal_clear(&amp;signal, mask)              │
│  destroy        │ qurt_signal_destroy(&amp;signal)                  │
├─────────────────┼───────────────────────────────────────────────┤
│ TIMERS          │                                               │
│  create         │ qurt_timer_create(&amp;timer, &amp;attr)              │
│  delete         │ qurt_timer_delete(timer)                      │
│  sleep          │ qurt_timer_sleep(usec)                        │
│  ticks          │ qurt_sysclock_get_hw_ticks()                  │
├─────────────────┼───────────────────────────────────────────────┤
│ MEMORY          │                                               │
│  cache flush    │ qurt_mem_cache_clean(addr, sz, FLUSH)         │
│  cache inval    │ qurt_mem_cache_clean(addr, sz, INVALIDATE)    │
│  l2 lock        │ qurt_mem_l2cache_lock(addr, size)             │
│  l2 unlock      │ qurt_mem_l2cache_unlock(addr, size)           │
├─────────────────┼───────────────────────────────────────────────┤
│ SEMAPHORE       │                                               │
│  init           │ qurt_sem_init_val(&amp;sem, count)                │
│  down (wait)    │ qurt_sem_down(&amp;sem)                           │
│  up (post)      │ qurt_sem_up(&amp;sem)                             │
│  destroy        │ qurt_sem_destroy(&amp;sem)                        │
├─────────────────┼───────────────────────────────────────────────┤
│ BARRIER         │                                               │
│  init           │ qurt_barrier_init(&amp;barrier, count)            │
│  wait           │ qurt_barrier_wait(&amp;barrier)                   │
│  destroy        │ qurt_barrier_destroy(&amp;barrier)                │
└─────────────────┴───────────────────────────────────────────────┘
</code></pre>
<p>This table lists the most commonly used QuRT API functions organized by category. The left column names the operation and the right column shows the function signature.</p>
<ul>
<li><p>Thread operations cover creation, termination, joining, and sleeping.</p>
</li>
<li><p>Mutex operations provide lock, try-lock, and unlock.</p>
</li>
<li><p>Signal operations support wait, set, and clear with bitmask-based notifications. Timer operations handle creation, deletion, and sleeping, plus reading the hardware tick counter.</p>
</li>
<li><p>Memory operations cover cache flush and invalidate (essential for cross-processor buffers) and L2 cache locking for performance-critical data.</p>
</li>
<li><p>Semaphore and barrier operations round out the synchronization primitives.</p>
</li>
</ul>
<h2 id="heading-next-steps">Next Steps</h2>
<p>This handbook covered the fundamentals of QuRT programming: thread management, synchronization, memory, timers, interrupts, pipes, FastRPC, and a multi-sensor fusion pipeline. The next steps for deeper learning follow a natural progression.</p>
<p>Start by downloading the Hexagon SDK and running the included example projects on the simulator. The examples in <code>$HEXAGON_SDK_ROOT/examples/</code> demonstrate real ARM-DSP communication patterns through FastRPC and are the best way to see complete, working projects.</p>
<p>Read the QuRT User Guide in <code>$HEXAGON_SDK_ROOT/docs/</code>. It covers every API discussed in this article in full detail, plus many that weren't covered (such as QuRT's TLB management and power management interfaces).</p>
<p>Experiment with HVX, the Hexagon Vector Extensions. HVX is where the real performance of the Hexagon DSP lives, and learning to write vectorized DSP code is the single largest performance lever available to you.</p>
<p>Finally, get a development board (such as the Qualcomm RB5) and run your code on real hardware. The simulator validates correctness, but only real hardware reveals timing behavior, cache effects, and the interaction between your code and other software running on the DSP.</p>
<h3 id="heading-recommended-reading">Recommended Reading</h3>
<p>The Hexagon SDK Documentation is located at <code>\(HEXAGON_SDK_ROOT/docs/</code>. The QuRT API Reference is at <code>\)HEXAGON_SDK_ROOT/docs/qurt/</code>. The Qualcomm Developer Network at developer.qualcomm.com provides additional resources, forums, and application notes. The Hexagon DSP Architecture Reference is the definitive guide to the hardware itself.</p>
<p>QuRT is a precision instrument. It won't hold your hand, but it gives you microsecond-level control over real-time processing on one of the most powerful DSP architectures in the world. The learning curve is steep, but once you are past it, you will understand why billions of devices trust this tiny OS with their most time-critical tasks.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
