<?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[ embedded - 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[ embedded - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 25 Aug 2026 22:03:57 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/embedded/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build Bluetooth Applications with Zephyr OS: A Handbook for Devs ]]>
                </title>
                <description>
                    <![CDATA[ Your phone just connected to wireless earbuds, your smartwatch synced health data to an app, and a sensor somewhere in your building reported its temperature to a gateway. All of those interactions ha ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-bluetooth-applications-with-zephyr-os-a-handbook-for-devs/</link>
                <guid isPermaLink="false">6a1de78e328352c4a380157a</guid>
                
                    <category>
                        <![CDATA[ bluetooth ]]>
                    </category>
                
                    <category>
                        <![CDATA[ zephyr ]]>
                    </category>
                
                    <category>
                        <![CDATA[ embedded ]]>
                    </category>
                
                    <category>
                        <![CDATA[ RTOS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nikheel Vishwas Savant ]]>
                </dc:creator>
                <pubDate>Mon, 01 Jun 2026 20:11:58 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/2915a74f-5ad2-4e92-b1e6-6516ce9f0ca0.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Your phone just connected to wireless earbuds, your smartwatch synced health data to an app, and a sensor somewhere in your building reported its temperature to a gateway. All of those interactions happened over Bluetooth Low Energy (BLE). And increasingly, the firmware behind those devices is built on Zephyr OS.</p>
<p>This handbook teaches you how to build Bluetooth applications on Zephyr from the ground up. You'll start with the fundamentals of BLE (what GAP, GATT, services, and characteristics actually mean), then move into writing real Zephyr firmware: advertising a device, creating custom services, handling connections, reading sensor data over BLE, and building a complete BLE peripheral that a phone can talk to.</p>
<p>Every concept comes with working code, and every code block comes with an explanation of what it does and why it matters.</p>
<p>This is a long, detailed guide. Bluetooth has a lot of moving parts, and most tutorials gloss over the pieces that trip people up in real projects.</p>
<p>This one does not. Work through it sequentially, build the code as you go, and by the end you'll have the knowledge to build production BLE devices on Zephyr.</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-zephyr-os-and-why-use-it-for-bluetooth">What Is Zephyr OS (And Why Use It for Bluetooth)</a>?</p>
</li>
<li><p><a href="#heading-bluetooth-low-energy-fundamentals">Bluetooth Low Energy Fundamentals</a></p>
</li>
<li><p><a href="#heading-the-gap-layer-advertising-and-connections">The GAP Layer: Advertising and Connections</a></p>
</li>
<li><p><a href="#heading-the-gatt-layer-services-and-characteristics">The GATT Layer: Services and Characteristics</a></p>
</li>
<li><p><a href="#heading-setting-up-your-zephyr-development-environment">Setting Up Your Zephyr Development Environment</a></p>
</li>
<li><p><a href="#heading-your-first-ble-application-a-simple-beacon">Your First BLE Application: A Simple Beacon</a></p>
</li>
<li><p><a href="#heading-building-a-ble-peripheral-with-a-custom-service">Building a BLE Peripheral with a Custom Service</a></p>
</li>
<li><p><a href="#heading-handling-connections-and-connection-callbacks">Handling Connections and Connection Callbacks</a></p>
</li>
<li><p><a href="#heading-adding-write-support-receiving-data-from-a-phone">Adding Write Support: Receiving Data from a Phone</a></p>
</li>
<li><p><a href="#heading-notifications-pushing-data-to-a-connected-device">Notifications: Pushing Data to a Connected Device</a></p>
</li>
<li><p><a href="#heading-building-a-complete-ble-sensor-node">Building a Complete BLE Sensor Node</a></p>
</li>
<li><p><a href="#heading-pairing-and-security">Pairing and Security</a></p>
</li>
<li><p><a href="#heading-implementing-a-standard-ble-profile-heart-rate">Implementing a Standard BLE Profile (Heart Rate)</a></p>
</li>
<li><p><a href="#heading-building-a-ble-central">Building a BLE Central</a></p>
</li>
<li><p><a href="#heading-mtu-negotiation-and-data-throughput">MTU Negotiation and Data Throughput</a></p>
</li>
<li><p><a href="#heading-phy-selection-for-range-and-speed">PHY Selection for Range and Speed</a></p>
</li>
<li><p><a href="#heading-firmware-updates-over-ble">Firmware Updates Over BLE</a></p>
</li>
<li><p><a href="#heading-bluetooth-mesh-on-zephyr">Bluetooth Mesh on Zephyr</a></p>
</li>
<li><p><a href="#heading-le-audio-the-next-generation-of-bluetooth-audio">LE Audio: The Next Generation of Bluetooth Audio</a></p>
</li>
<li><p><a href="#heading-debugging-bluetooth-applications">Debugging Bluetooth Applications</a></p>
</li>
<li><p><a href="#heading-power-optimization-for-ble-devices">Power Optimization for BLE Devices</a></p>
</li>
<li><p><a href="#heading-zephyr-bluetooth-vs-other-stacks">Zephyr Bluetooth vs Other Stacks</a></p>
</li>
<li><p><a href="#heading-where-to-go-from-here">Where to Go from Here</a></p>
</li>
<li><p><a href="#heading-summary">Summary</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along with this tutorial, you should be comfortable reading and writing C code. Pointers, structs, function pointers, and callbacks should not be foreign concepts. You need a command-line terminal and basic familiarity with building C projects. Prior Bluetooth experience isn't required. This article explains BLE concepts from scratch.</p>
<p>For hardware, a Nordic Semiconductor nRF52840 DK is the ideal board for following along. Nordic's chips have the best-supported Bluetooth stack in Zephyr, and the nRF52840 DK is affordable (around $40), widely available, and includes an onboard debugger.</p>
<p>If you have a different Zephyr-supported board with Bluetooth (nRF52832 DK, nRF5340 DK, or any board with a BLE-capable radio), that works too.</p>
<p>For testing the BLE side, you'll need a phone with a BLE scanner app (nRF Connect for Mobile is excellent and free on both iOS and Android).</p>
<p>You'll also need a computer running Linux (Ubuntu 22.04 or newer), macOS, or Windows with WSL2.</p>
<h2 id="heading-what-is-zephyr-os-and-why-use-it-for-bluetooth">What Is Zephyr OS (And Why Use It for Bluetooth)?</h2>
<p>Zephyr OS is a small, open-source, real-time operating system built for resource-constrained embedded devices. It's hosted by the Linux Foundation, licensed under Apache 2.0, and runs on microcontrollers with as little as 8 KB of RAM. It supports over 600 boards across ARM, RISC-V, x86, Xtensa, and other architectures.</p>
<p>But this article is about Bluetooth, so here's why Zephyr matters specifically for BLE development.</p>
<p>Zephyr includes a full, Bluetooth SIG-qualified BLE stack. That means the stack has been through the official Bluetooth qualification process and meets the specification requirements. You aren't building on a hobby implementation. You're building on a stack that has passed conformance testing.</p>
<p>The stack covers both the host (GAP, GATT, SMP, L2CAP, ATT) and the controller (Link Layer, HCI). For supported radios (primarily Nordic nRF series), Zephyr provides its own open-source controller implementation. This means the entire Bluetooth stack, from your application code down to the radio registers, is open source. No binary blobs. No closed-source libraries. You can read, debug, and modify every line.</p>
<p>Nordic Semiconductor, the company whose chips dominate the BLE market, built their nRF Connect SDK on top of Zephyr. When Nordic's own engineers write BLE firmware, they use Zephyr. That's a strong endorsement.</p>
<p>The Zephyr Bluetooth stack supports Bluetooth 5.x features (2M PHY, Coded PHY for long range, Extended Advertising), Bluetooth Mesh (relay, proxy, friend, and low-power nodes), LE Audio (the newest Bluetooth audio standard with LC3 codec, broadcast, and hearing aid support), Direction Finding (Angle of Arrival and Angle of Departure for indoor positioning), and all the standard BLE profiles and services you need for product development.</p>
<p>In short: if you're building a BLE product these days, Zephyr is one of the best platforms available. Google uses it in Chromebooks, Nordic uses it for their entire SDK, and hundreds of companies ship products built on it.</p>
<h2 id="heading-bluetooth-low-energy-fundamentals">Bluetooth Low Energy Fundamentals</h2>
<p>Before you write any code, you need a mental model of how BLE works. Skip this section if you already know BLE well. Read it carefully if you don't, because everything that follows builds on these concepts.</p>
<p>BLE is not the same as "classic" Bluetooth (the kind used for audio streaming to speakers before LE Audio). Classic Bluetooth (BR/EDR) is designed for continuous, high-throughput data streaming. BLE is designed for intermittent, low-power communication.</p>
<p>A BLE sensor might wake up once per minute, send 20 bytes of data, and go back to sleep. That interaction consumes microwatts of energy. This fundamental design difference is why BLE devices can run on a coin cell battery for years.</p>
<p>BLE communication is organized into two main layers that you interact with as a developer: GAP and GATT.</p>
<p><strong>GAP (Generic Access Profile)</strong> controls how devices discover each other and establish connections. Think of GAP as the "meeting people at a party" layer.</p>
<p>A device can be in one of several GAP roles. A <strong>peripheral</strong> (also called an advertiser) broadcasts small packets of data at regular intervals, announcing its presence and basic information. A <strong>central</strong> (also called a scanner) listens for these broadcasts and can initiate a connection. Your phone is typically a central. A sensor, earbud, or smartwatch is typically a peripheral.</p>
<p><strong>GATT (Generic Attribute Profile)</strong> controls how data is exchanged after two devices are connected. Think of GATT as the "having a conversation" layer.</p>
<p>GATT defines a hierarchical data structure. At the top level, a device exposes one or more <strong>services</strong>. A service groups related data together.</p>
<p>For example, a heart rate monitor might have a Heart Rate Service. Inside each service are one or more <strong>characteristics</strong>. A characteristic is a single data point with a value and metadata. The Heart Rate Service might have a Heart Rate Measurement characteristic (the actual BPM value) and a Body Sensor Location characteristic (where the sensor is worn).</p>
<p>Each characteristic has <strong>properties</strong> that define what you can do with it. A characteristic can be readable (a central can request its value), writable (a central can set its value), notifiable (the peripheral can push updates to the central without being asked), or indicatable (like notify but with acknowledgment). A characteristic can have multiple properties.</p>
<p>Every service and characteristic is identified by a <strong>UUID</strong>. The Bluetooth SIG defines standard 16-bit UUIDs for common services and characteristics (Heart Rate is 0x180D, Battery Service is 0x180F, and so on). For custom functionality, you define your own 128-bit UUIDs.</p>
<p>Here's a concrete mental model. Imagine a temperature sensor device:</p>
<pre><code class="language-plaintext">Device: "My Temp Sensor"
  |
  +-- Environmental Sensing Service (UUID: 0x181A)
  |     |
  |     +-- Temperature characteristic (UUID: 0x2A6E)
  |     |     Properties: Read, Notify
  |     |     Value: 23.5 (degrees C)
  |     |
  |     +-- Humidity characteristic (UUID: 0x2A6F)
  |           Properties: Read, Notify
  |           Value: 65.2 (percent)
  |
  +-- Battery Service (UUID: 0x180F)
        |
        +-- Battery Level characteristic (UUID: 0x2A19)
              Properties: Read, Notify
              Value: 87 (percent)
</code></pre>
<p>This diagram shows a device with two services and three characteristics. A connected phone could read the temperature, subscribe to humidity notifications, and check the battery level. The services and characteristics are the API of your BLE device.</p>
<p>Designing them well is like designing a good REST API: think about what data your device exposes and how clients will interact with it.</p>
<p>One more concept: <strong>advertising data</strong>. When a peripheral advertises, it broadcasts small packets (up to 31 bytes in legacy advertising, larger with Extended Advertising). These packets contain structured data like the device name, supported services, manufacturer-specific data, and flags. The advertising data is what a scanner sees before making a connection. It is your device's "business card."</p>
<h2 id="heading-the-gap-layer-advertising-and-connections">The GAP Layer: Advertising and Connections</h2>
<p>GAP is the first layer you interact with when building a BLE device. Your peripheral needs to advertise its presence before anything else can happen.</p>
<p>Advertising works like this: the peripheral's radio wakes up at regular intervals (the "advertising interval"), transmits a short packet on one of three advertising channels (channels 37, 38, and 39), and goes back to sleep. A central scanning those channels picks up the packet and learns about the device.</p>
<p>The advertising interval is a tradeoff: shorter intervals (like 20 ms) make the device easier to discover but consume more power. Longer intervals (like 1000 ms) save power but make discovery slower. For most applications, 100 to 500 ms is a reasonable range.</p>
<p>An advertising packet contains structured fields called AD (Advertising Data) structures. Each AD structure has a length byte, a type byte, and data bytes. Common types include flags (indicating discoverability and BR/EDR support), complete or shortened local name, list of service UUIDs, TX power level, and manufacturer-specific data.</p>
<p>The total payload is limited to 31 bytes for legacy advertising (Bluetooth 4.x), so you can't fit much. You often have to choose between including the device name or the service UUID, because both might not fit.</p>
<p>Bluetooth 5.0 introduced Extended Advertising, which allows advertising payloads up to 254 bytes per fragment (chained into even larger payloads), advertising on all 40 BLE channels (not just the three advertising channels), and multiple simultaneous advertising sets. Zephyr supports Extended Advertising when the hardware and controller support it.</p>
<p>When a central decides to connect, it sends a connection request to the peripheral. The two devices negotiate connection parameters: the <strong>connection interval</strong> (how often they communicate, typically 7.5 ms to 4 seconds), the <strong>peripheral latency</strong> (how many connection events the peripheral can skip to save power), and the <strong>supervision timeout</strong> (how long to wait before considering the connection lost).</p>
<p>These parameters affect both data throughput and power consumption. A short connection interval gives you fast data transfer but costs more power. A high peripheral latency saves power but adds latency to data exchange.</p>
<h2 id="heading-the-gatt-layer-services-and-characteristics">The GATT Layer: Services and Characteristics</h2>
<p>Once a connection is established, GATT takes over. The connected devices exchange data through the service/characteristic hierarchy described earlier.</p>
<p>On the peripheral side, you define the GATT database: the list of services and characteristics your device exposes. On the central side, you perform service discovery: querying the peripheral for its available services and characteristics.</p>
<p>Each characteristic in the GATT database has several components. The <strong>value</strong> is the actual data (a byte array). The <strong>properties</strong> define permitted operations: Read (0x02), Write Without Response (0x04), Write (0x08), Notify (0x10), and Indicate (0x20) are the most common. The <strong>permissions</strong> are security requirements: whether reading/writing requires encryption, authentication, or authorization.</p>
<p>For notifications and indications, the characteristic has a <strong>Client Characteristic Configuration Descriptor (CCCD)</strong>. This is a special 2-byte value that the central writes to enable or disable notifications/indications. When the central writes 0x0001 to the CCCD, notifications are enabled. When it writes 0x0000, they're disabled. Zephyr handles the CCCD automatically when you define a characteristic with the notify property.</p>
<p>The GATT operations flow like this.</p>
<ul>
<li><p>For a read: the central sends a read request, the peripheral responds with the characteristic value.</p>
</li>
<li><p>For a write: the central sends a write request with the new value, the peripheral updates the value and sends a response.</p>
</li>
<li><p>For a notification: the peripheral sends the value to the central without the central asking. The central must have previously enabled notifications on that characteristic.</p>
</li>
</ul>
<p>This request/response model means that BLE isn't a streaming protocol. It's a message-passing protocol. If you need to send continuous sensor data, you use notifications at a fixed interval. If you need to configure the device, you write to a characteristic. This shapes how you design your BLE application.</p>
<h2 id="heading-setting-up-your-zephyr-development-environment">Setting Up Your Zephyr Development Environment</h2>
<p>This section walks through a complete environment setup. If you already have a Zephyr environment, skip to the next section.</p>
<p>Install system dependencies (Ubuntu):</p>
<pre><code class="language-shell">sudo apt update
sudo apt install --no-install-recommends git cmake ninja-build gperf \
  ccache dfu-util device-tree-compiler wget python3-dev python3-pip \
  python3-setuptools python3-tk python3-wheel xz-utils file \
  make gcc gcc-multilib g++-multilib libsdl2-dev libmagic1
</code></pre>
<p>These packages provide the compiler toolchain, build systems, and utilities that Zephyr's build process requires.</p>
<p>The devicetree compiler (<code>device-tree-compiler</code>) is particularly important for Zephyr because it processes the hardware description files that tell the build system about your board's peripherals and pin assignments.</p>
<p>Install west, the Zephyr command-line tool:</p>
<pre><code class="language-shell">pip3 install west
</code></pre>
<p>West manages the multi-repository workspace that Zephyr uses, and provides commands for building, flashing, and debugging firmware. It's the single tool you interact with most.</p>
<p>Initialize and update the workspace:</p>
<pre><code class="language-shell">west init ~/zephyrproject
cd ~/zephyrproject
west update
</code></pre>
<p>The <code>west init</code> command creates a workspace and clones the main Zephyr repository. The <code>west update</code> command then fetches all module dependencies: vendor HALs (the low-level chip support packages), cryptography libraries, the Bluetooth controller code, and other components. This downloads several gigabytes, so it takes a while.</p>
<p>Install Python requirements:</p>
<pre><code class="language-shell">pip3 install -r ~/zephyrproject/zephyr/scripts/requirements.txt
</code></pre>
<p>This installs the Python packages that Zephyr's build scripts and west extensions depend on, including the devicetree processing tools, the Kconfig frontend, and various code generation utilities. The requirements file pins specific versions to ensure build reproducibility.</p>
<p>Install the Zephyr SDK (provides cross-compilation toolchains for all supported architectures):</p>
<pre><code class="language-shell">cd ~
wget https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.16.8/zephyr-sdk-0.16.8_linux-x86_64.tar.xz
tar xvf zephyr-sdk-0.16.8_linux-x86_64.tar.xz
cd zephyr-sdk-0.16.8
./setup.sh
</code></pre>
<p>The SDK includes GCC-based toolchains for ARM, RISC-V, x86, Xtensa, and more. The <code>setup.sh</code> script registers them with CMake. Check the Zephyr releases page for the latest SDK version number, as it may have been updated since this handbook was written.</p>
<p>Set environment variables (add these to your <code>~/.bashrc</code> or <code>~/.zshrc</code>):</p>
<pre><code class="language-shell">export ZEPHYR_BASE=~/zephyrproject/zephyr
source ~/zephyrproject/zephyr/zephyr-env.sh
</code></pre>
<p>The <code>ZEPHYR_BASE</code> variable tells the build system where the Zephyr source tree lives. The <code>zephyr-env.sh</code> script sets up additional paths. With both configured, you can build for any supported board from any directory.</p>
<h2 id="heading-your-first-ble-application-a-simple-beacon">Your First BLE Application: A Simple Beacon</h2>
<p>We'll start with the simplest possible BLE application: a device that advertises but does nothing else. No connections, no services, no data exchange. Just a beacon broadcasting its existence.</p>
<p>Create the project structure:</p>
<pre><code class="language-shell">mkdir -p ~/my_ble_apps/beacon/src
</code></pre>
<p>This creates the standard Zephyr application directory layout. Every Zephyr application lives in its own directory with a <code>src/</code> subdirectory for C source files. The project root holds the build configuration files.</p>
<p>Create <code>~/my_ble_apps/beacon/CMakeLists.txt</code>:</p>
<pre><code class="language-plaintext">cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(ble_beacon)
target_sources(app PRIVATE src/main.c)
</code></pre>
<p>The <code>find_package(Zephyr)</code> line loads the entire Zephyr build system. The <code>target_sources</code> line adds your source file to the build target named <code>app</code>, which is the standard target name for Zephyr applications.</p>
<p>Create <code>~/my_ble_apps/beacon/prj.conf</code>:</p>
<pre><code class="language-plaintext">CONFIG_BT=y
CONFIG_BT_BROADCASTER=y
</code></pre>
<p>Two configuration lines: <code>CONFIG_BT=y</code> enables the Bluetooth subsystem, pulling in the host stack, HCI layer, and (on supported boards) the controller. <code>CONFIG_BT_BROADCASTER=y</code> enables the broadcaster role, which is the minimal role for a device that only advertises. You don't need the peripheral role yet because this beacon doesn't accept connections.</p>
<p>Create <code>~/my_ble_apps/beacon/src/main.c</code>:</p>
<pre><code class="language-c">#include &lt;zephyr/kernel.h&gt;
#include &lt;zephyr/bluetooth/bluetooth.h&gt;

static const struct bt_data ad[] = {
    BT_DATA_BYTES(BT_DATA_FLAGS, BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR),
    BT_DATA_BYTES(BT_DATA_NAME_COMPLETE,
                  'M', 'y', 'B', 'e', 'a', 'c', 'o', 'n'),
};

int main(void)
{
    int err;

    printk("Starting BLE Beacon\n");

    err = bt_enable(NULL);
    if (err) {
        printk("Bluetooth init failed (err %d)\n", err);
        return 0;
    }

    printk("Bluetooth initialized\n");

    err = bt_le_adv_start(BT_LE_ADV_NCONN, ad, ARRAY_SIZE(ad), NULL, 0);
    if (err) {
        printk("Advertising failed to start (err %d)\n", err);
        return 0;
    }

    printk("Beacon is advertising\n");

    return 0;
}
</code></pre>
<p>Let's walk through this code piece by piece.</p>
<p>The <code>ad</code> array defines the advertising data. It contains two AD structures.</p>
<p>The first is the Flags field, which is mandatory in BLE advertising. <code>BT_LE_AD_GENERAL</code> means the device is in General Discoverable mode (visible to all scanners). <code>BT_LE_AD_NO_BREDR</code> indicates that the device doesn't support classic Bluetooth (BR/EDR), only BLE.</p>
<p>The second AD structure is the complete local name, spelled out character by character. The <code>BT_DATA_BYTES</code> macro packs these into the correct AD structure format.</p>
<p>The <code>bt_enable(NULL)</code> call initializes the entire Bluetooth subsystem. This sets up the HCI transport, initializes the controller (if using an onboard radio), and prepares the host stack. The <code>NULL</code> argument means this is a synchronous call: it blocks until initialization is complete. You could pass a callback function to make it asynchronous.</p>
<p>The <code>bt_le_adv_start</code> call begins advertising. The first argument, <code>BT_LE_ADV_NCONN</code>, specifies non-connectable advertising. This means scanners can see the beacon but cannot connect to it. The <code>ad</code> array and its size (<code>ARRAY_SIZE(ad)</code>) provide the advertising data. The last two arguments (<code>NULL, 0</code>) are for scan response data, which is additional data sent when a scanner actively scans (sends a scan request). You aren't using scan response data in this simple example.</p>
<p>After <code>main()</code> returns, the Zephyr main thread terminates, but the system keeps running. The Bluetooth subsystem continues advertising in the background, driven by the controller and the Bluetooth host thread.</p>
<p>Build and flash:</p>
<pre><code class="language-shell">cd ~/zephyrproject
west build -b nrf52840dk/nrf52840 ~/my_ble_apps/beacon
west flash
</code></pre>
<p>The <code>west build</code> command compiles the application for the nRF52840 DK, producing an ELF binary and a HEX file in the <code>build/</code> directory. The <code>west flash</code> command programs that binary onto the board via the onboard J-Link debugger, automatically detecting the connected board and using the correct programming protocol.</p>
<p>Open the nRF Connect app on your phone, start scanning, and you should see "MyBeacon" appear in the list of discovered devices. That's your firmware, running on your board, advertising over BLE.</p>
<h2 id="heading-building-a-ble-peripheral-with-a-custom-service">Building a BLE Peripheral with a Custom Service</h2>
<p>A beacon that broadcasts is useful for some applications (iBeacon, Eddystone, asset tracking). But most BLE devices need to be connectable and expose data through GATT services. Now you'll build a peripheral with a custom service.</p>
<p>You'll create a "LED Service" that lets a phone control an LED on your board and read a button state. This is a classic BLE demo that teaches you the patterns you'll use in every BLE project.</p>
<p>Create the project:</p>
<pre><code class="language-shell">mkdir -p ~/my_ble_apps/led_service/src
</code></pre>
<p>Same project layout as the beacon: a root directory for build configuration and a <code>src/</code> directory for source code. This separation keeps build artifacts isolated from your source files.</p>
<p>Create <code>~/my_ble_apps/led_service/prj.conf</code>:</p>
<pre><code class="language-plaintext">CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="Zephyr LED"
CONFIG_BT_DEVICE_APPEARANCE=0
CONFIG_GPIO=y
CONFIG_BT_GATT_DYNAMIC_DB=y
</code></pre>
<p><code>CONFIG_BT_PERIPHERAL=y</code> enables the peripheral role, which includes the broadcaster role plus the ability to accept connections. <code>CONFIG_BT_DEVICE_NAME</code> sets the default device name used in advertising and the GAP Device Name characteristic. <code>CONFIG_GPIO=y</code> enables the GPIO driver so you can control the LED and read the button.</p>
<p>Create <code>~/my_ble_apps/led_service/CMakeLists.txt</code>:</p>
<pre><code class="language-cmake">cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(ble_led_service)
target_sources(app PRIVATE src/main.c)
</code></pre>
<p>This is the same CMake boilerplate from the beacon project. The only change is the project name (<code>ble_led_service</code>). The <code>find_package(Zephyr)</code> call loads the full Zephyr build system, and <code>target_sources</code> registers your application source file.</p>
<p>Create <code>~/my_ble_apps/led_service/src/main.c</code>:</p>
<pre><code class="language-c">#include &lt;zephyr/kernel.h&gt;
#include &lt;zephyr/bluetooth/bluetooth.h&gt;
#include &lt;zephyr/bluetooth/gatt.h&gt;
#include &lt;zephyr/bluetooth/uuid.h&gt;
#include &lt;zephyr/drivers/gpio.h&gt;

/* Custom service UUID: 00001234-0000-1000-8000-00805f9b34fb */
#define BT_UUID_LED_SERVICE_VAL \
    BT_UUID_128_ENCODE(0x00001234, 0x0000, 0x1000, 0x8000, 0x00805f9b34fb)
#define BT_UUID_LED_SERVICE BT_UUID_DECLARE_128(BT_UUID_LED_SERVICE_VAL)

/* LED characteristic UUID */
#define BT_UUID_LED_CHAR_VAL \
    BT_UUID_128_ENCODE(0x00001235, 0x0000, 0x1000, 0x8000, 0x00805f9b34fb)
#define BT_UUID_LED_CHAR BT_UUID_DECLARE_128(BT_UUID_LED_CHAR_VAL)

/* Button characteristic UUID */
#define BT_UUID_BUTTON_CHAR_VAL \
    BT_UUID_128_ENCODE(0x00001236, 0x0000, 0x1000, 0x8000, 0x00805f9b34fb)
#define BT_UUID_BUTTON_CHAR BT_UUID_DECLARE_128(BT_UUID_BUTTON_CHAR_VAL)

#define LED0_NODE DT_ALIAS(led0)
#define SW0_NODE  DT_ALIAS(sw0)

static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
static const struct gpio_dt_spec button = GPIO_DT_SPEC_GET(SW0_NODE, gpios);

static uint8_t led_state;
static uint8_t button_state;

static ssize_t read_led(struct bt_conn *conn,
                        const struct bt_gatt_attr *attr,
                        void *buf, uint16_t len, uint16_t offset)
{
    return bt_gatt_attr_read(conn, attr, buf, len, offset,
                             &amp;led_state, sizeof(led_state));
}

static ssize_t write_led(struct bt_conn *conn,
                         const struct bt_gatt_attr *attr,
                         const void *buf, uint16_t len,
                         uint16_t offset, uint8_t flags)
{
    if (len != 1) {
        return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
    }

    led_state = *((const uint8_t *)buf);
    gpio_pin_set_dt(&amp;led, led_state ? 1 : 0);

    printk("LED %s\n", led_state ? "ON" : "OFF");

    return len;
}

static ssize_t read_button(struct bt_conn *conn,
                           const struct bt_gatt_attr *attr,
                           void *buf, uint16_t len, uint16_t offset)
{
    button_state = gpio_pin_get_dt(&amp;button);
    return bt_gatt_attr_read(conn, attr, buf, len, offset,
                             &amp;button_state, sizeof(button_state));
}

BT_GATT_SERVICE_DEFINE(led_service,
    BT_GATT_PRIMARY_SERVICE(BT_UUID_LED_SERVICE),

    BT_GATT_CHARACTERISTIC(BT_UUID_LED_CHAR,
        BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
        BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
        read_led, write_led, NULL),

    BT_GATT_CHARACTERISTIC(BT_UUID_BUTTON_CHAR,
        BT_GATT_CHRC_READ,
        BT_GATT_PERM_READ,
        read_button, NULL, NULL),
);

static const struct bt_data ad[] = {
    BT_DATA_BYTES(BT_DATA_FLAGS, BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR),
    BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME,
            sizeof(CONFIG_BT_DEVICE_NAME) - 1),
};

static const struct bt_data sd[] = {
    BT_DATA_BYTES(BT_DATA_UUID128_ALL, BT_UUID_LED_SERVICE_VAL),
};

int main(void)
{
    int err;

    if (!gpio_is_ready_dt(&amp;led) || !gpio_is_ready_dt(&amp;button)) {
        printk("GPIO devices not ready\n");
        return 0;
    }

    gpio_pin_configure_dt(&amp;led, GPIO_OUTPUT_INACTIVE);
    gpio_pin_configure_dt(&amp;button, GPIO_INPUT);

    err = bt_enable(NULL);
    if (err) {
        printk("Bluetooth init failed (err %d)\n", err);
        return 0;
    }

    printk("Bluetooth initialized\n");

    err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad),
                          sd, ARRAY_SIZE(sd));
    if (err) {
        printk("Advertising failed to start (err %d)\n", err);
        return 0;
    }

    printk("Advertising as '%s'\n", CONFIG_BT_DEVICE_NAME);

    return 0;
}
</code></pre>
<p>This is a substantial piece of code, so let's go through it section by section.</p>
<p>The UUID definitions at the top create custom 128-bit UUIDs for the service and its two characteristics. When you build a custom BLE device, you generate your own UUIDs rather than using the standard Bluetooth SIG UUIDs (unless you're implementing a standard profile like Heart Rate).</p>
<p>In production, you would generate random 128-bit UUIDs using a tool like <code>uuidgen</code>. The UUIDs here are simple for readability. The <code>BT_UUID_128_ENCODE</code> macro formats the UUID in the byte order that the Bluetooth stack expects. <code>BT_UUID_DECLARE_128</code> creates a <code>struct bt_uuid_128</code> from the encoded value.</p>
<p>The <code>led</code> and <code>button</code> GPIO specs use the devicetree aliases <code>led0</code> and <code>sw0</code>, which most Zephyr boards define. The <code>GPIO_DT_SPEC_GET</code> macro pulls the pin number, GPIO controller, and flags directly from the devicetree, keeping the code portable across boards.</p>
<p>The <code>read_led</code> callback is called when a connected central reads the LED characteristic. It uses <code>bt_gatt_attr_read</code>, a helper function that handles offset and length correctly (GATT reads can be partial if the value is larger than the MTU). The function returns the current LED state as a single byte.</p>
<p>The <code>write_led</code> callback is called when a central writes to the LED characteristic. It validates that exactly one byte was written, updates the <code>led_state</code> variable, and calls <code>gpio_pin_set_dt</code> to physically toggle the LED. It returns the number of bytes consumed (<code>len</code>), or a GATT error if the length was wrong. The <code>BT_GATT_ERR</code> macro wraps an ATT error code into the return value that the stack expects.</p>
<p>The <code>read_button</code> callback reads the current physical button state at the moment of the read request. It calls <code>gpio_pin_get_dt</code> to sample the pin, stores the result, and returns it.</p>
<p>The <code>BT_GATT_SERVICE_DEFINE</code> macro is where the GATT database is constructed at compile time. The first argument names the service variable. <code>BT_GATT_PRIMARY_SERVICE</code> declares a primary service with the custom UUID. Each <code>BT_GATT_CHARACTERISTIC</code> declaration takes the characteristic UUID, the properties (Read + Write for the LED, Read-only for the button), the permissions (who can read/write), the read callback, the write callback, and a pointer to user data (NULL in both cases here).</p>
<p>The <code>ad</code> array contains the advertising data: flags and the device name. The <code>sd</code> array contains the scan response data: the 128-bit service UUID.</p>
<p>Splitting data between advertising and scan response is common because the 31-byte advertising packet limit is tight. The service UUID alone is 16 bytes, which would leave little room for other data in the main advertising packet. By putting the UUID in the scan response, you keep the advertising packet small and include the UUID only when a scanner explicitly requests it.</p>
<p>The <code>bt_le_adv_start</code> call uses <code>BT_LE_ADV_CONN</code> instead of <code>BT_LE_ADV_NCONN</code>. This makes the advertising connectable: scanners can now establish a connection to your device. The <code>sd</code> and <code>ARRAY_SIZE(sd)</code> arguments provide the scan response data that was NULL in the beacon example.</p>
<p>Build, flash, and test:</p>
<pre><code class="language-bash">cd ~/zephyrproject
west build -b nrf52840dk/nrf52840 ~/my_ble_apps/led_service
west flash
</code></pre>
<p>The build command compiles the application with the full Bluetooth stack, GPIO drivers, and GATT database linked into a single binary. The flash command programs it onto the board. Because <code>CONFIG_BT_PERIPHERAL</code> is enabled, the binary is significantly larger than the beacon (the connectable advertising, GATT server, and ATT protocol layers are all included).</p>
<p>Open nRF Connect on your phone, scan, and find "Zephyr LED." Tap Connect. After connecting, you'll see the GATT services. Find the custom service (UUID starting with 00001234). You'll see two characteristics. Read the button characteristic to see the button state. Write 0x01 to the LED characteristic to turn on the LED, and 0x00 to turn it off. You just controlled hardware over Bluetooth from your phone.</p>
<h2 id="heading-handling-connections-and-connection-callbacks">Handling Connections and Connection Callbacks</h2>
<p>In a real application, you need to know when devices connect and disconnect. Maybe you want to stop advertising when a device connects (to save power), restart advertising when it disconnects (to allow reconnection), or update a status LED to indicate connection state.</p>
<p>Zephyr provides connection callbacks through a registration mechanism:</p>
<pre><code class="language-c">#include &lt;zephyr/bluetooth/conn.h&gt;

static void connected(struct bt_conn *conn, uint8_t err)
{
    if (err) {
        printk("Connection failed (err %u)\n", err);
        return;
    }

    char addr[BT_ADDR_LE_STR_LEN];
    bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
    printk("Connected: %s\n", addr);
}

static void disconnected(struct bt_conn *conn, uint8_t reason)
{
    char addr[BT_ADDR_LE_STR_LEN];
    bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
    printk("Disconnected: %s (reason %u)\n", addr, reason);

    /* Restart advertising after disconnect */
    bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad),
                    sd, ARRAY_SIZE(sd));
}

BT_CONN_CB_DEFINE(conn_callbacks) = {
    .connected = connected,
    .disconnected = disconnected,
};
</code></pre>
<p>The <code>BT_CONN_CB_DEFINE</code> macro statically registers a set of connection callbacks. The <code>.connected</code> callback fires when a connection is established. The <code>err</code> parameter indicates whether the connection was successful (0 means success). The <code>.disconnected</code> callback fires when a connection ends. The <code>reason</code> parameter is an HCI disconnect reason code (0x13 is "Remote User Terminated Connection," which is the normal disconnect).</p>
<p>In the <code>connected</code> callback, the code retrieves the remote device's Bluetooth address using <code>bt_conn_get_dst</code> and converts it to a printable string. This is useful for logging and debugging.</p>
<p>In the <code>disconnected</code> callback, the code restarts advertising. By default, Zephyr stops advertising when a connection is established (because the radio is now used for the connection). When the connection drops, you typically want to start advertising again so the device can be rediscovered.</p>
<p>The <code>bt_conn</code> pointer represents the connection. You can use it to query connection parameters, request parameter updates, initiate pairing, or disconnect programmatically. Hold a reference to it (using <code>bt_conn_ref</code>) if you need to use it outside the callback. Release the reference (using <code>bt_conn_unref</code>) when you're done.</p>
<h2 id="heading-adding-write-support-receiving-data-from-a-phone">Adding Write Support: Receiving Data from a Phone</h2>
<p>The LED service already has a write characteristic, but let's look more closely at the write callback pattern and how to handle more complex data.</p>
<p>Consider a scenario where the phone sends a configuration struct to the device:</p>
<pre><code class="language-c">struct device_config {
    uint8_t mode;
    uint16_t interval_ms;
    uint8_t threshold;
} __packed;

static struct device_config current_config = {
    .mode = 0,
    .interval_ms = 1000,
    .threshold = 50,
};

static ssize_t write_config(struct bt_conn *conn,
                            const struct bt_gatt_attr *attr,
                            const void *buf, uint16_t len,
                            uint16_t offset, uint8_t flags)
{
    if (offset != 0) {
        return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
    }

    if (len != sizeof(struct device_config)) {
        return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
    }

    memcpy(&amp;current_config, buf, len);

    printk("Config updated: mode=%u, interval=%u ms, threshold=%u\n",
           current_config.mode,
           current_config.interval_ms,
           current_config.threshold);

    return len;
}

static ssize_t read_config(struct bt_conn *conn,
                           const struct bt_gatt_attr *attr,
                           void *buf, uint16_t len, uint16_t offset)
{
    return bt_gatt_attr_read(conn, attr, buf, len, offset,
                             &amp;current_config, sizeof(current_config));
}
</code></pre>
<p>The <code>__packed</code> attribute on the struct ensures that there is no padding between fields, so the byte layout matches what the phone sends. Without <code>__packed</code>, the compiler might insert padding bytes between <code>mode</code> and <code>interval_ms</code> for alignment, and the data wouldn't match.</p>
<p>The write callback validates two things. First, it checks that the offset is zero (no partial writes for this simple case). Second, it checks that the length matches the expected struct size. If either check fails, it returns a GATT error code that the central receives as a write response error.</p>
<p>Validating input is critical in BLE applications because the central is sending raw bytes over the air. Malformed data should be rejected, not blindly accepted.</p>
<p>The <code>memcpy</code> copies the validated data into the configuration struct. In a real application, you would likely apply the new configuration to your device's behavior (change a sensor polling interval, switch operating modes, and so on).</p>
<p>The <code>flags</code> parameter in the write callback indicates whether this is a Write With Response (the central expects an acknowledgment) or Write Without Response (fire-and-forget). You can check <code>flags &amp; BT_GATT_WRITE_FLAG_CMD</code> to distinguish the two. For configuration data, you typically want Write With Response so the central knows the write succeeded.</p>
<h2 id="heading-notifications-pushing-data-to-a-connected-device">Notifications: Pushing Data to a Connected Device</h2>
<p>Reading and writing work for on-demand data. But many BLE applications need the peripheral to push data to the central proactively. A heart rate monitor doesn't wait for the phone to ask for the heart rate every second. It pushes the value via notifications.</p>
<p>Here's how to add notification support to a characteristic:</p>
<pre><code class="language-c">static uint8_t sensor_value;
static bool notifications_enabled;

static void sensor_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value)
{
    notifications_enabled = (value == BT_GATT_CCC_NOTIFY);
    printk("Notifications %s\n", notifications_enabled ? "enabled" : "disabled");
}

static ssize_t read_sensor(struct bt_conn *conn,
                           const struct bt_gatt_attr *attr,
                           void *buf, uint16_t len, uint16_t offset)
{
    return bt_gatt_attr_read(conn, attr, buf, len, offset,
                             &amp;sensor_value, sizeof(sensor_value));
}

BT_GATT_SERVICE_DEFINE(sensor_service,
    BT_GATT_PRIMARY_SERVICE(BT_UUID_LED_SERVICE),

    BT_GATT_CHARACTERISTIC(BT_UUID_LED_CHAR,
        BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
        BT_GATT_PERM_READ,
        read_sensor, NULL, &amp;sensor_value),

    BT_GATT_CCC(sensor_ccc_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
);
</code></pre>
<p>The characteristic now has <code>BT_GATT_CHRC_NOTIFY</code> in its properties, which tells connected centrals that this characteristic supports notifications.</p>
<p>The <code>BT_GATT_CCC</code> macro adds the Client Characteristic Configuration Descriptor (CCCD). The <code>sensor_ccc_changed</code> callback is called when a central enables or disables notifications by writing to the CCCD. The <code>value</code> parameter will be <code>BT_GATT_CCC_NOTIFY</code> (0x0001) when notifications are enabled and 0 when disabled.</p>
<p>To actually send a notification:</p>
<pre><code class="language-c">void send_sensor_notification(void)
{
    if (!notifications_enabled) {
        return;
    }

    sensor_value = read_actual_sensor();

    int err = bt_gatt_notify(NULL, &amp;sensor_service.attrs[1],
                             &amp;sensor_value, sizeof(sensor_value));
    if (err) {
        printk("Notify failed (err %d)\n", err);
    }
}
</code></pre>
<p>The <code>bt_gatt_notify</code> function sends a notification to all connected centrals that have enabled notifications on this characteristic.</p>
<p>The first argument is <code>NULL</code> to notify all connections (you can pass a specific <code>bt_conn</code> pointer to notify only one). The second argument is a pointer to the characteristic attribute in the GATT table. The <code>&amp;sensor_service.attrs[1]</code> points to the first characteristic value attribute (index 0 is the service declaration, index 1 is the characteristic declaration, and the value follows). The third and fourth arguments are the data and its length.</p>
<p>A common pattern is to call this function from a timer or a work queue at a regular interval:</p>
<pre><code class="language-c">void sensor_work_handler(struct k_work *work)
{
    send_sensor_notification();
}

K_WORK_DELAYABLE_DEFINE(sensor_work, sensor_work_handler);

/* In main(), after Bluetooth is initialized and advertising: */
k_work_schedule(&amp;sensor_work, K_SECONDS(1));

/* In the work handler, reschedule for periodic execution: */
void sensor_work_handler(struct k_work *work)
{
    send_sensor_notification();
    k_work_schedule(&amp;sensor_work, K_SECONDS(1));
}
</code></pre>
<p>This approach uses a delayable work item that reschedules itself every second. Each time it fires, it reads the sensor value and sends a notification (if notifications are enabled). The work item runs on the system work queue thread, not in interrupt context, so it's safe to call <code>bt_gatt_notify</code> and other Bluetooth APIs.</p>
<h2 id="heading-building-a-complete-ble-sensor-node">Building a Complete BLE Sensor Node</h2>
<p>Now we'll tie everything together into a complete application. This is a BLE environmental sensor that reads temperature (simulated), exposes it through a custom GATT service with read and notify support, handles connections and disconnections, and manages advertising.</p>
<p>Create <code>~/my_ble_apps/sensor_node/src/main.c</code>:</p>
<pre><code class="language-c">#include &lt;zephyr/kernel.h&gt;
#include &lt;zephyr/bluetooth/bluetooth.h&gt;
#include &lt;zephyr/bluetooth/gatt.h&gt;
#include &lt;zephyr/bluetooth/uuid.h&gt;
#include &lt;zephyr/bluetooth/conn.h&gt;
#include &lt;zephyr/drivers/gpio.h&gt;

/* UUIDs */
#define BT_UUID_ENV_SERVICE_VAL \
    BT_UUID_128_ENCODE(0xaabbccdd, 0x0000, 0x1000, 0x8000, 0x00805f9b34fb)
#define BT_UUID_ENV_SERVICE BT_UUID_DECLARE_128(BT_UUID_ENV_SERVICE_VAL)

#define BT_UUID_TEMP_CHAR_VAL \
    BT_UUID_128_ENCODE(0xaabbccdd, 0x0001, 0x1000, 0x8000, 0x00805f9b34fb)
#define BT_UUID_TEMP_CHAR BT_UUID_DECLARE_128(BT_UUID_TEMP_CHAR_VAL)

#define BT_UUID_INTERVAL_CHAR_VAL \
    BT_UUID_128_ENCODE(0xaabbccdd, 0x0002, 0x1000, 0x8000, 0x00805f9b34fb)
#define BT_UUID_INTERVAL_CHAR BT_UUID_DECLARE_128(BT_UUID_INTERVAL_CHAR_VAL)

/* LED for connection status */
#define LED0_NODE DT_ALIAS(led0)
static const struct gpio_dt_spec status_led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);

/* Sensor state */
static int16_t temperature_value = 2250;
static uint16_t notify_interval_ms = 1000;
static bool temp_notifications_enabled;
static struct bt_conn *current_conn;

/* Forward declaration */
static void sensor_work_handler(struct k_work *work);
K_WORK_DELAYABLE_DEFINE(sensor_work, sensor_work_handler);

static int16_t simulate_temperature(void)
{
    static int16_t base = 2250;
    base += (k_uptime_get_32() % 11) - 5;
    if (base &gt; 3500) base = 3500;
    if (base &lt; 1000) base = 1000;
    return base;
}

/* GATT callbacks */
static ssize_t read_temperature(struct bt_conn *conn,
                                const struct bt_gatt_attr *attr,
                                void *buf, uint16_t len, uint16_t offset)
{
    temperature_value = simulate_temperature();
    return bt_gatt_attr_read(conn, attr, buf, len, offset,
                             &amp;temperature_value, sizeof(temperature_value));
}

static void temp_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value)
{
    temp_notifications_enabled = (value == BT_GATT_CCC_NOTIFY);
    printk("Temperature notifications %s\n",
           temp_notifications_enabled ? "enabled" : "disabled");

    if (temp_notifications_enabled) {
        k_work_schedule(&amp;sensor_work, K_MSEC(notify_interval_ms));
    } else {
        k_work_cancel_delayable(&amp;sensor_work);
    }
}

static ssize_t read_interval(struct bt_conn *conn,
                             const struct bt_gatt_attr *attr,
                             void *buf, uint16_t len, uint16_t offset)
{
    return bt_gatt_attr_read(conn, attr, buf, len, offset,
                             &amp;notify_interval_ms, sizeof(notify_interval_ms));
}

static ssize_t write_interval(struct bt_conn *conn,
                              const struct bt_gatt_attr *attr,
                              const void *buf, uint16_t len,
                              uint16_t offset, uint8_t flags)
{
    if (len != sizeof(uint16_t)) {
        return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
    }

    uint16_t new_interval = *((const uint16_t *)buf);

    if (new_interval &lt; 100 || new_interval &gt; 60000) {
        return BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED);
    }

    notify_interval_ms = new_interval;
    printk("Notification interval changed to %u ms\n", notify_interval_ms);

    if (temp_notifications_enabled) {
        k_work_cancel_delayable(&amp;sensor_work);
        k_work_schedule(&amp;sensor_work, K_MSEC(notify_interval_ms));
    }

    return len;
}

/* GATT service definition */
BT_GATT_SERVICE_DEFINE(env_service,
    BT_GATT_PRIMARY_SERVICE(BT_UUID_ENV_SERVICE),

    BT_GATT_CHARACTERISTIC(BT_UUID_TEMP_CHAR,
        BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
        BT_GATT_PERM_READ,
        read_temperature, NULL, NULL),
    BT_GATT_CCC(temp_ccc_changed,
        BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),

    BT_GATT_CHARACTERISTIC(BT_UUID_INTERVAL_CHAR,
        BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
        BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
        read_interval, write_interval, NULL),
);

/* Notification sender */
static void sensor_work_handler(struct k_work *work)
{
    temperature_value = simulate_temperature();

    if (temp_notifications_enabled) {
        int err = bt_gatt_notify(NULL, &amp;env_service.attrs[2],
                                 &amp;temperature_value,
                                 sizeof(temperature_value));
        if (err &amp;&amp; err != -ENOTCONN) {
            printk("Notify error: %d\n", err);
        }

        k_work_schedule(&amp;sensor_work, K_MSEC(notify_interval_ms));
    }
}

/* Advertising data */
static const struct bt_data ad[] = {
    BT_DATA_BYTES(BT_DATA_FLAGS, BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR),
    BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME,
            sizeof(CONFIG_BT_DEVICE_NAME) - 1),
};

static const struct bt_data sd[] = {
    BT_DATA_BYTES(BT_DATA_UUID128_ALL, BT_UUID_ENV_SERVICE_VAL),
};

/* Connection callbacks */
static void connected(struct bt_conn *conn, uint8_t err)
{
    if (err) {
        printk("Connection failed (err %u)\n", err);
        return;
    }

    current_conn = bt_conn_ref(conn);
    gpio_pin_set_dt(&amp;status_led, 1);

    char addr[BT_ADDR_LE_STR_LEN];
    bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
    printk("Connected: %s\n", addr);
}

static void disconnected(struct bt_conn *conn, uint8_t reason)
{
    char addr[BT_ADDR_LE_STR_LEN];
    bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
    printk("Disconnected: %s (reason %u)\n", addr, reason);

    if (current_conn) {
        bt_conn_unref(current_conn);
        current_conn = NULL;
    }

    temp_notifications_enabled = false;
    k_work_cancel_delayable(&amp;sensor_work);
    gpio_pin_set_dt(&amp;status_led, 0);

    bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
}

BT_CONN_CB_DEFINE(conn_callbacks) = {
    .connected = connected,
    .disconnected = disconnected,
};

int main(void)
{
    int err;

    if (!gpio_is_ready_dt(&amp;status_led)) {
        printk("LED not ready\n");
        return 0;
    }
    gpio_pin_configure_dt(&amp;status_led, GPIO_OUTPUT_INACTIVE);

    err = bt_enable(NULL);
    if (err) {
        printk("Bluetooth init failed (err %d)\n", err);
        return 0;
    }

    printk("Bluetooth initialized\n");

    err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad),
                          sd, ARRAY_SIZE(sd));
    if (err) {
        printk("Advertising failed (err %d)\n", err);
        return 0;
    }

    printk("Environmental sensor ready. Advertising as '%s'\n",
           CONFIG_BT_DEVICE_NAME);

    return 0;
}
</code></pre>
<p>The <code>prj.conf</code> for this application:</p>
<pre><code class="language-plaintext">CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="Zephyr Sensor"
CONFIG_BT_GATT_DYNAMIC_DB=y
CONFIG_GPIO=y
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
</code></pre>
<p>This application demonstrates the full lifecycle of a BLE sensor device, so study the design carefully.</p>
<p>The service exposes two characteristics. The temperature characteristic supports read and notify. When a central reads it, it gets the latest simulated temperature. When the central enables notifications, the device starts pushing temperature updates at a configurable interval.</p>
<p>The interval characteristic lets the central read and write the notification interval, bounded between 100 ms and 60000 ms. The validation in <code>write_interval</code> rejects values outside this range with <code>BT_ATT_ERR_VALUE_NOT_ALLOWED</code>, which the central receives as an error response.</p>
<p>Temperature values are stored as <code>int16_t</code> in hundredths of a degree Celsius (2250 = 22.50 degrees C). This fixed-point representation avoids floating-point math, which is expensive on microcontrollers without an FPU, and gives you 0.01 degree resolution in a 2-byte value.</p>
<p>The connection callbacks manage the full connection lifecycle. On connect, the code takes a reference to the connection object (<code>bt_conn_ref</code>) and turns on the status LED. On disconnect, it releases the reference (<code>bt_conn_unref</code>), cancels any pending notification work, turns off the LED, and restarts advertising.</p>
<p>The reference counting is important because the <code>bt_conn</code> pointer is only valid while you hold a reference. Using it after the reference is released leads to undefined behavior.</p>
<p>The notification work item reschedules itself at the configured interval, creating a periodic loop. When notifications are disabled (either explicitly by the central or implicitly by disconnection), the work is cancelled. This prevents wasted CPU cycles (and battery) when nobody is listening.</p>
<h2 id="heading-pairing-and-security">Pairing and Security</h2>
<p>Production BLE devices almost always need security. Without pairing, any device within radio range can connect and interact with your GATT services. Pairing establishes an encrypted link and optionally authenticates the devices to each other.</p>
<p>BLE supports several pairing methods. "Just Works" provides encryption but no authentication. It protects against passive eavesdropping but not against active man-in-the-middle attacks. "Passkey Entry" requires the user to enter a 6-digit code, providing authentication. "Numeric Comparison" displays a number on both devices and the user confirms they match. "Out of Band (OOB)" uses an external channel (like NFC) to exchange pairing information.</p>
<p>Enable security in your <code>prj.conf</code>:</p>
<pre><code class="language-plaintext">CONFIG_BT_SMP=y
CONFIG_BT_SETTINGS=y
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y
CONFIG_SETTINGS=y
</code></pre>
<p><code>CONFIG_BT_SMP=y</code> enables the Security Manager Protocol, which handles pairing. The Settings, Flash, and NVS options enable persistent storage so that bonding information (the keys exchanged during pairing) survives reboots. Without persistent storage, the device would need to re-pair after every power cycle, which is a terrible user experience.</p>
<p>To require encryption on a characteristic, change its permissions:</p>
<pre><code class="language-c">BT_GATT_CHARACTERISTIC(BT_UUID_TEMP_CHAR,
    BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
    BT_GATT_PERM_READ_ENCRYPT,
    read_temperature, NULL, NULL),
</code></pre>
<p><code>BT_GATT_PERM_READ_ENCRYPT</code> means the characteristic can only be read over an encrypted connection. If a central tries to read it without pairing first, the stack automatically triggers pairing. You can also use <code>BT_GATT_PERM_READ_AUTHEN</code> to require authenticated pairing (Passkey or Numeric Comparison, not Just Works).</p>
<p>Register authentication callbacks to handle passkey display or input:</p>
<pre><code class="language-c">static void auth_passkey_display(struct bt_conn *conn, unsigned int passkey)
{
    char addr[BT_ADDR_LE_STR_LEN];
    bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
    printk("Passkey for %s: %06u\n", addr, passkey);
}

static void auth_cancel(struct bt_conn *conn)
{
    char addr[BT_ADDR_LE_STR_LEN];
    bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
    printk("Pairing cancelled: %s\n", addr);
}

static struct bt_conn_auth_cb auth_callbacks = {
    .passkey_display = auth_passkey_display,
    .cancel = auth_cancel,
};

/* In main(), after bt_enable(): */
bt_conn_auth_cb_register(&amp;auth_callbacks);
</code></pre>
<p>The <code>passkey_display</code> callback fires when the stack generates a passkey that the user needs to enter on the central (phone). On a device with a display, you would show the passkey on screen. On a device without a display (like a sensor), you might print it to a serial console during development or use Just Works pairing in production.</p>
<p>The <code>bt_conn_auth_cb_register</code> function registers these callbacks with the stack. Only one set of callbacks can be active at a time.</p>
<h2 id="heading-implementing-a-standard-ble-profile-heart-rate">Implementing a Standard BLE Profile (Heart Rate)</h2>
<p>The examples so far have used custom 128-bit UUIDs. In production, many BLE devices implement standard profiles defined by the Bluetooth SIG. Standard profiles use 16-bit UUIDs, which consume less advertising space and allow generic apps (like nRF Connect) to automatically parse and display the data in a human-readable format. The Heart Rate Profile is one of the most common and illustrates how standard profiles work in Zephyr.</p>
<p>The Heart Rate Service (UUID 0x180D) contains a Heart Rate Measurement characteristic (UUID 0x2A37) that uses notifications to push heart rate data. The measurement characteristic has a specific byte format defined by the Bluetooth SIG: the first byte is a flags field, and the remaining bytes contain the heart rate value and optional fields like energy expended and RR-interval.</p>
<pre><code class="language-c">#include &lt;zephyr/kernel.h&gt;
#include &lt;zephyr/bluetooth/bluetooth.h&gt;
#include &lt;zephyr/bluetooth/gatt.h&gt;
#include &lt;zephyr/bluetooth/uuid.h&gt;
#include &lt;zephyr/bluetooth/conn.h&gt;

static uint8_t heart_rate_bpm = 72;
static bool hr_notifications_enabled;

static void hr_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value)
{
    hr_notifications_enabled = (value == BT_GATT_CCC_NOTIFY);
}

static ssize_t read_body_sensor_location(struct bt_conn *conn,
                                         const struct bt_gatt_attr *attr,
                                         void *buf, uint16_t len,
                                         uint16_t offset)
{
    uint8_t location = 0x01; /* Chest */
    return bt_gatt_attr_read(conn, attr, buf, len, offset,
                             &amp;location, sizeof(location));
}

BT_GATT_SERVICE_DEFINE(hr_service,
    BT_GATT_PRIMARY_SERVICE(BT_UUID_HRS),

    BT_GATT_CHARACTERISTIC(BT_UUID_HRS_MEASUREMENT,
        BT_GATT_CHRC_NOTIFY,
        BT_GATT_PERM_NONE,
        NULL, NULL, NULL),
    BT_GATT_CCC(hr_ccc_changed,
        BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),

    BT_GATT_CHARACTERISTIC(BT_UUID_HRS_BODY_SENSOR,
        BT_GATT_CHRC_READ,
        BT_GATT_PERM_READ,
        read_body_sensor_location, NULL, NULL),
);

static void send_heart_rate(void)
{
    if (!hr_notifications_enabled) {
        return;
    }

    uint8_t hr_data[2];
    hr_data[0] = 0x00; /* Flags: uint8 format, no extra fields */
    hr_data[1] = heart_rate_bpm;

    bt_gatt_notify(NULL, &amp;hr_service.attrs[1], hr_data, sizeof(hr_data));
}
</code></pre>
<p>This code uses Zephyr's predefined UUID macros (<code>BT_UUID_HRS</code>, <code>BT_UUID_HRS_MEASUREMENT</code>, <code>BT_UUID_HRS_BODY_SENSOR</code>) instead of custom 128-bit UUIDs. Zephyr defines macros for all standard Bluetooth SIG services and characteristics in <code>zephyr/bluetooth/uuid.h</code>. Using these standard UUIDs means that any BLE heart rate app on a phone can automatically discover, connect, and display data from your device without custom app development.</p>
<p>The Heart Rate Measurement characteristic has <code>BT_GATT_PERM_NONE</code> for permissions because it's notify-only. No read or write access is permitted: the data flows exclusively through notifications.</p>
<p>The first byte of the notification payload (<code>hr_data[0]</code>) is a flags field defined by the SIG specification. A value of 0x00 means the heart rate is in uint8 format (values 0 to 255 BPM) with no optional fields present. Setting bit 0 would switch to uint16 format for heart rates above 255. Setting other bits would indicate the presence of energy expended or RR-interval data.</p>
<p>The Body Sensor Location characteristic is a simple read-only value. The value 0x01 means "Chest." Other defined values include 0x00 (Other), 0x02 (Wrist), 0x03 (Finger), 0x04 (Hand), 0x05 (Ear Lobe), and 0x06 (Foot).</p>
<p>The <code>prj.conf</code> for a standard profile application doesn't need any special configuration beyond the basic Bluetooth peripheral setup:</p>
<pre><code class="language-plaintext">CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="Zephyr HR"
</code></pre>
<p>The standard UUIDs are always available when <code>CONFIG_BT=y</code> is set. No additional Kconfig options are needed to use SIG-defined service and characteristic UUIDs.</p>
<p>The advertising data for a standard profile device typically includes the 16-bit service UUID in the advertising packet, which allows phones to filter scans by service type:</p>
<pre><code class="language-c">static const struct bt_data ad[] = {
    BT_DATA_BYTES(BT_DATA_FLAGS, BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR),
    BT_DATA_BYTES(BT_DATA_UUID16_ALL, BT_UUID_16_ENCODE(0x180D)),
    BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME,
            sizeof(CONFIG_BT_DEVICE_NAME) - 1),
};
</code></pre>
<p>The <code>BT_DATA_UUID16_ALL</code> type advertises the complete list of 16-bit service UUIDs. The <code>BT_UUID_16_ENCODE(0x180D)</code> macro encodes the Heart Rate Service UUID in little-endian byte order. A 16-bit UUID takes only 2 bytes in the advertising packet (compared to 16 bytes for a 128-bit UUID), leaving much more room for other advertising data. This is a significant advantage of using standard profiles.</p>
<h2 id="heading-building-a-ble-central">Building a BLE Central</h2>
<p>Every example so far has built a peripheral (a device that advertises and accepts connections). The other side of a BLE connection is the central: a device that scans for peripherals, initiates connections, and reads/writes characteristics. Gateways, hubs, and data collectors are typically centrals.</p>
<p>Building a central on Zephyr requires a second board, but understanding the central role is essential for building complete BLE systems.</p>
<p>The <code>prj.conf</code> for a central application:</p>
<pre><code class="language-plaintext">CONFIG_BT=y
CONFIG_BT_CENTRAL=y
CONFIG_BT_GATT_CLIENT=y
CONFIG_BT_SCAN=y
CONFIG_BT_DEVICE_NAME="Zephyr Central"
</code></pre>
<p><code>CONFIG_BT_CENTRAL=y</code> enables the central role (scanning and connection initiation). <code>CONFIG_BT_GATT_CLIENT=y</code> enables the GATT client APIs for service discovery, reading, writing, and subscribing. <code>CONFIG_BT_SCAN=y</code> enables the scan module, which provides a higher-level scanning API with filtering.</p>
<p>A BLE central starts by scanning for peripherals:</p>
<pre><code class="language-c">#include &lt;zephyr/kernel.h&gt;
#include &lt;zephyr/bluetooth/bluetooth.h&gt;
#include &lt;zephyr/bluetooth/conn.h&gt;
#include &lt;zephyr/bluetooth/gatt.h&gt;
#include &lt;zephyr/bluetooth/uuid.h&gt;

static struct bt_conn *default_conn;

static void device_found(const bt_addr_le_t *addr, int8_t rssi,
                          uint8_t type, struct net_buf_simple *ad)
{
    char addr_str[BT_ADDR_LE_STR_LEN];
    bt_addr_le_to_str(addr, addr_str, sizeof(addr_str));

    if (rssi &lt; -70) {
        return; /* Skip distant devices */
    }

    printk("Found device: %s (RSSI %d)\n", addr_str, rssi);

    /* Stop scanning before connecting */
    bt_le_scan_stop();

    int err = bt_conn_le_create(addr, BT_CONN_LE_CREATE_CONN,
                                BT_LE_CONN_PARAM_DEFAULT,
                                &amp;default_conn);
    if (err) {
        printk("Connection failed (err %d)\n", err);
        bt_le_scan_start(BT_LE_SCAN_ACTIVE, device_found);
    }
}

int main(void)
{
    int err;

    err = bt_enable(NULL);
    if (err) {
        printk("Bluetooth init failed (err %d)\n", err);
        return 0;
    }

    printk("Starting BLE scan...\n");

    err = bt_le_scan_start(BT_LE_SCAN_ACTIVE, device_found);
    if (err) {
        printk("Scan failed (err %d)\n", err);
        return 0;
    }

    return 0;
}
</code></pre>
<p>The <code>bt_le_scan_start</code> function begins scanning for BLE advertisements. The first argument, <code>BT_LE_SCAN_ACTIVE</code>, enables active scanning, which means the scanner sends scan request packets to advertisers to get their scan response data (passive scanning only listens). The second argument is a callback function that's called for every advertising packet received.</p>
<p>The <code>device_found</code> callback receives the advertiser's address, RSSI (signal strength in dBm), advertising type, and the raw advertising data.</p>
<p>In this example, the code filters by RSSI to ignore distant devices, then attempts to connect to the first device it finds. The <code>bt_le_scan_stop</code> call is necessary before initiating a connection because the radio can't scan and connect simultaneously.</p>
<p>The <code>bt_conn_le_create</code> function initiates a connection. It takes the target address, creation parameters (which control the scan window used during connection establishment), connection parameters (interval, latency, timeout), and a pointer to store the connection reference. <code>BT_LE_CONN_PARAM_DEFAULT</code> uses the default connection parameters, which are suitable for most applications.</p>
<p>In a real application, you would filter the scan results more carefully, typically by checking the advertised service UUIDs or device name. After connecting, you would perform GATT service discovery and then read/write/subscribe to characteristics:</p>
<pre><code class="language-c">static uint8_t discover_func(struct bt_conn *conn,
                              const struct bt_gatt_attr *attr,
                              struct bt_gatt_discover_params *params)
{
    if (!attr) {
        printk("Discovery complete\n");
        return BT_GATT_ITER_STOP;
    }

    char uuid_str[BT_UUID_STR_LEN];
    bt_uuid_to_str(params-&gt;uuid, uuid_str, sizeof(uuid_str));
    printk("Discovered attribute: handle %u, UUID %s\n",
           attr-&gt;handle, uuid_str);

    return BT_GATT_ITER_CONTINUE;
}

static struct bt_gatt_discover_params discover_params;

static void start_discovery(struct bt_conn *conn)
{
    discover_params.uuid = NULL; /* Discover all services */
    discover_params.func = discover_func;
    discover_params.start_handle = BT_ATT_FIRST_ATTRIBUTE_HANDLE;
    discover_params.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE;
    discover_params.type = BT_GATT_DISCOVER_PRIMARY;

    int err = bt_gatt_discover(conn, &amp;discover_params);
    if (err) {
        printk("Discovery failed (err %d)\n", err);
    }
}
</code></pre>
<p>The <code>bt_gatt_discover</code> function initiates GATT service discovery on the connected peripheral. The <code>discover_params</code> structure controls what to discover: setting <code>uuid</code> to NULL discovers all primary services. Setting <code>type</code> to <code>BT_GATT_DISCOVER_PRIMARY</code> discovers primary services. You can also set it to <code>BT_GATT_DISCOVER_CHARACTERISTIC</code> to discover characteristics within a service, or <code>BT_GATT_DISCOVER_DESCRIPTOR</code> to discover descriptors within a characteristic.</p>
<p>The discovery callback (<code>discover_func</code>) is called once for each discovered attribute and once more with <code>attr</code> set to NULL when discovery is complete. Returning <code>BT_GATT_ITER_CONTINUE</code> tells the stack to continue discovering. Returning <code>BT_GATT_ITER_STOP</code> stops discovery early.</p>
<p>After discovering the services and characteristics, you read a characteristic value using <code>bt_gatt_read</code> and subscribe to notifications using <code>bt_gatt_subscribe</code>. The subscribe function takes a <code>bt_gatt_subscribe_params</code> structure that specifies the characteristic handle, a notification callback function, and the CCC value to write (BT_GATT_CCC_NOTIFY).</p>
<h2 id="heading-mtu-negotiation-and-data-throughput">MTU Negotiation and Data Throughput</h2>
<p>The default BLE ATT MTU (Maximum Transmission Unit) is 23 bytes. Subtract 3 bytes of ATT protocol overhead, and you're left with 20 bytes of actual payload per GATT operation. For a single temperature reading, 20 bytes is plenty. For transferring a firmware image or a large sensor data buffer, 20 bytes per operation is painfully slow.</p>
<p>MTU negotiation allows two connected devices to agree on a larger MTU, up to 517 bytes (the BLE maximum). A larger MTU means more data per packet, fewer round-trips, and higher throughput. On an nRF52840 with 2M PHY, the difference between a 23-byte MTU and a 247-byte MTU can be 5x to 10x throughput improvement.</p>
<p>Enable a larger MTU in your <code>prj.conf</code>:</p>
<pre><code class="language-plaintext">CONFIG_BT_L2CAP_TX_MTU=247
CONFIG_BT_BUF_ACL_RX_SIZE=251
CONFIG_BT_BUF_ACL_TX_SIZE=251
CONFIG_BT_CTLR_DATA_LENGTH_MAX=251
</code></pre>
<p><code>CONFIG_BT_L2CAP_TX_MTU=247</code> sets the maximum ATT MTU that the device will request during negotiation. The value 247 is commonly used because it aligns with the maximum single-packet data length (251 bytes at the Link Layer minus 4 bytes of L2CAP header).</p>
<p><code>CONFIG_BT_BUF_ACL_RX_SIZE</code> and <code>CONFIG_BT_BUF_ACL_TX_SIZE</code> set the ACL buffer sizes to accommodate the larger packets. <code>CONFIG_BT_CTLR_DATA_LENGTH_MAX</code> enables Data Length Extension (DLE) at the controller level, allowing the Link Layer to send longer packets instead of fragmenting them into 27-byte chunks.</p>
<p>MTU negotiation happens automatically after a connection is established. The Zephyr stack initiates an MTU exchange on connection if the configured MTU is larger than the default. You can also trigger it explicitly:</p>
<pre><code class="language-c">static void exchange_func(struct bt_conn *conn, uint8_t att_err,
                           struct bt_gatt_exchange_params *params)
{
    if (att_err) {
        printk("MTU exchange failed (err %u)\n", att_err);
        return;
    }

    uint16_t mtu = bt_gatt_get_mtu(conn);
    printk("MTU exchanged: %u bytes\n", mtu);
}

static struct bt_gatt_exchange_params exchange_params = {
    .func = exchange_func,
};

static void connected(struct bt_conn *conn, uint8_t err)
{
    if (err) {
        return;
    }

    bt_gatt_exchange_mtu(conn, &amp;exchange_params);
}
</code></pre>
<p>The <code>bt_gatt_exchange_mtu</code> function sends an MTU exchange request to the remote device. The callback receives the result. After a successful exchange, <code>bt_gatt_get_mtu</code> returns the negotiated MTU, which is the minimum of both devices' supported MTU values. The effective payload per notification or write operation is the negotiated MTU minus 3 bytes of ATT header.</p>
<p>When sending large amounts of data over notifications, the effective throughput depends on three factors: the MTU (larger means fewer packets), the connection interval (shorter means more transmission opportunities), and the PHY (2M PHY doubles the raw data rate compared to 1M PHY).</p>
<p>With a 247-byte MTU, a 7.5 ms connection interval, and 2M PHY, you can achieve throughput in the range of 800 kbps to 1400 kbps, depending on the specific controller and radio conditions.</p>
<p>One practical consideration: iOS and Android handle MTU negotiation differently. Android allows you to request a specific MTU via the app layer, while iOS automatically negotiates the maximum supported MTU (typically 185 or 251 bytes depending on the iOS version) without app intervention. Your firmware should always handle whatever MTU is negotiated, rather than assuming a specific value.</p>
<h2 id="heading-phy-selection-for-range-and-speed">PHY Selection for Range and Speed</h2>
<p>Bluetooth 5.0 introduced two new PHY (Physical Layer) options beyond the original 1M PHY. Selecting the right PHY trades off range, throughput, and power consumption.</p>
<p><strong>1M PHY</strong> is the default and the only PHY available in Bluetooth 4.x. It transmits at 1 megabit per second with standard range. Every BLE device supports 1M PHY.</p>
<p><strong>2M PHY</strong> doubles the data rate to 2 megabits per second. Each packet takes half as long to transmit, which means the radio is active for less time. This improves both throughput (more data per unit time) and power consumption (shorter radio on-time). The tradeoff is slightly reduced range compared to 1M PHY because the receiver has less time to integrate each bit. Use 2M PHY when the central and peripheral are close together (within a few meters) and throughput matters.</p>
<p><strong>Coded PHY</strong> uses forward error correction to extend range significantly, approximately 2x to 4x compared to 1M PHY. It achieves this by transmitting each bit with redundant coding (S=2 for 2x range, S=8 for 4x range). The cost is reduced throughput: Coded PHY S=8 has an effective data rate of 125 kbps, which is 8x slower than 1M PHY. Use Coded PHY for applications that need long range (outdoor asset tracking, building-wide sensor networks) and can tolerate low data rates.</p>
<p>Enable PHY support in <code>prj.conf</code>:</p>
<pre><code class="language-plaintext">CONFIG_BT_USER_PHY_UPDATE=y
CONFIG_BT_CTLR_PHY_2M=y
CONFIG_BT_CTLR_PHY_CODED=y
</code></pre>
<p><code>CONFIG_BT_USER_PHY_UPDATE=y</code> enables the application to request PHY updates after a connection is established. <code>CONFIG_BT_CTLR_PHY_2M</code> and <code>CONFIG_BT_CTLR_PHY_CODED</code> enable support for the respective PHYs in the controller.</p>
<p>Request a PHY update after connection:</p>
<pre><code class="language-c">static void connected(struct bt_conn *conn, uint8_t err)
{
    if (err) {
        return;
    }

    /* Request 2M PHY for higher throughput */
    struct bt_conn_le_phy_param phy_param = {
        .options = BT_CONN_LE_PHY_OPT_NONE,
        .pref_tx_phy = BT_GAP_LE_PHY_2M,
        .pref_rx_phy = BT_GAP_LE_PHY_2M,
    };

    int phy_err = bt_conn_le_phy_update(conn, &amp;phy_param);
    if (phy_err) {
        printk("PHY update request failed (err %d)\n", phy_err);
    }
}
</code></pre>
<p>The <code>bt_conn_le_phy_update</code> function sends a PHY update request to the remote device. The <code>pref_tx_phy</code> and <code>pref_rx_phy</code> fields indicate the preferred PHY for transmitting and receiving data, respectively. The remote device may accept or suggest an alternative. Both devices must support the requested PHY for the update to succeed.</p>
<p>To monitor PHY changes, register a callback:</p>
<pre><code class="language-c">static void phy_updated(struct bt_conn *conn,
                         struct bt_conn_le_phy_info *param)
{
    printk("PHY updated: TX PHY %u, RX PHY %u\n",
           param-&gt;tx_phy, param-&gt;rx_phy);
}

BT_CONN_CB_DEFINE(conn_callbacks) = {
    .connected = connected,
    .disconnected = disconnected,
    .le_phy_updated = phy_updated,
};
</code></pre>
<p>The <code>le_phy_updated</code> callback fires whenever the PHY changes on a connection. The <code>tx_phy</code> and <code>rx_phy</code> fields report the active PHY for each direction. A value of 1 means 1M PHY, 2 means 2M PHY, and 4 means Coded PHY. The TX and RX PHYs can be different (asymmetric PHY), though most applications use the same PHY in both directions.</p>
<p>For Coded PHY with S=8 encoding (maximum range), set the options field:</p>
<pre><code class="language-c">struct bt_conn_le_phy_param phy_param = {
    .options = BT_CONN_LE_PHY_OPT_CODED_S8,
    .pref_tx_phy = BT_GAP_LE_PHY_CODED,
    .pref_rx_phy = BT_GAP_LE_PHY_CODED,
};
</code></pre>
<p>The <code>BT_CONN_LE_PHY_OPT_CODED_S8</code> option selects S=8 coding, which provides the maximum range extension at the cost of the lowest throughput. Omitting this option (or using <code>BT_CONN_LE_PHY_OPT_CODED_S2</code>) selects S=2 coding, which provides moderate range extension with better throughput.</p>
<h2 id="heading-firmware-updates-over-ble">Firmware Updates Over BLE</h2>
<p>Deploying firmware updates to devices in the field is a critical capability for any production BLE product. Users shouldn't need to connect a USB cable or visit a service center to get bug fixes and new features.</p>
<p>Zephyr supports Device Firmware Update (DFU) over BLE through integration with MCUboot, a secure open-source bootloader.</p>
<p>The DFU architecture has two components. MCUboot is a bootloader that runs before your application. It manages two firmware slots: the active slot (running firmware) and the upgrade slot (new firmware waiting to be applied). When new firmware is written to the upgrade slot, MCUboot verifies its cryptographic signature, swaps the slots, and boots the new firmware. If the new firmware fails to confirm itself (mark itself as valid), MCUboot automatically rolls back to the previous version on the next reboot.</p>
<p>The BLE transport for DFU uses the SMP (Simple Management Protocol) over a GATT service. The mcumgr library implements SMP, and Zephyr includes a BLE SMP transport that exposes an SMP GATT service. A phone app (like nRF Connect or mcumgr CLI) connects to this service and uploads the new firmware image in chunks.</p>
<p>Enable DFU in your <code>prj.conf</code>:</p>
<pre><code class="language-plaintext">CONFIG_BOOTLOADER_MCUBOOT=y
CONFIG_MCUMGR=y
CONFIG_MCUMGR_TRANSPORT_BT=y
CONFIG_MCUMGR_GRP_IMG=y
CONFIG_MCUMGR_GRP_OS=y
CONFIG_IMG_MANAGER=y
CONFIG_STREAM_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_FLASH=y
</code></pre>
<p><code>CONFIG_BOOTLOADER_MCUBOOT=y</code> tells the build system that this application runs under MCUboot, which changes the linker script and image format. <code>CONFIG_MCUMGR=y</code> enables the mcumgr management library. <code>CONFIG_MCUMGR_TRANSPORT_BT=y</code> enables the BLE SMP transport, which creates a GATT service that the phone connects to for uploading firmware. <code>CONFIG_MCUMGR_GRP_IMG=y</code> enables the image management command group (upload, confirm, erase). <code>CONFIG_MCUMGR_GRP_OS=y</code> enables the OS management group (reset, echo).</p>
<p>Register the BLE SMP transport in your application:</p>
<pre><code class="language-c">#include &lt;zephyr/mgmt/mcumgr/transport/smp_bt.h&gt;

int main(void)
{
    int err;

    err = bt_enable(NULL);
    if (err) {
        printk("Bluetooth init failed (err %d)\n", err);
        return 0;
    }

    /* Start SMP BLE transport for DFU */
    smp_bt_register();

    /* Start advertising (include SMP service UUID) */
    bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), NULL, 0);

    printk("DFU-capable device ready\n");
    return 0;
}
</code></pre>
<p>The <code>smp_bt_register()</code> call registers the SMP GATT service with the Bluetooth stack. After this, any BLE central that connects can discover the SMP service and upload firmware using the mcumgr protocol.</p>
<p>Building with MCUboot requires flashing the bootloader first. MCUboot is built separately and programmed into the boot partition of flash:</p>
<pre><code class="language-bash">west build -b nrf52840dk/nrf52840 bootloader/mcuboot/boot/zephyr \
    -d build_mcuboot
west flash -d build_mcuboot

west build -b nrf52840dk/nrf52840 my_dfu_app
west flash
</code></pre>
<p>The first two commands build and flash MCUboot. The second two commands build and flash your application. MCUboot occupies the first portion of flash and boots your application from the primary slot.</p>
<p>To perform a DFU, you build a new version of your application, which produces a <code>zephyr.signed.bin</code> file in the build directory. Upload that file to the device using the nRF Connect mobile app (which has built-in DFU support) or the mcumgr command-line tool. The upload happens over the BLE SMP connection. After the upload completes, the device reboots, MCUboot verifies the new image, and swaps it into the active slot.</p>
<p>MCUboot supports several security features that are important for production. Image signing ensures that only firmware signed with your private key can be installed. Image encryption prevents reverse-engineering of firmware images during transfer. Rollback protection reverts to the previous firmware if the new version does not boot successfully. These features require additional configuration but are essential for any product that accepts over-the-air updates.</p>
<h2 id="heading-bluetooth-mesh-on-zephyr">Bluetooth Mesh on Zephyr</h2>
<p>BLE point-to-point connections work well for devices that communicate directly with a phone or gateway. But they break down when you need to control hundreds of light bulbs in a building. You can't connect to each one individually. Bluetooth Mesh solves this.</p>
<p>Bluetooth Mesh is a many-to-many networking standard that runs on top of BLE. Devices in a mesh network relay messages to each other, extending the range far beyond a single BLE connection. A command to turn on the lights can originate from one device, bounce through multiple relay nodes, and reach every light bulb in the building.</p>
<p>Zephyr includes a complete Bluetooth Mesh implementation. Here's the conceptual architecture.</p>
<p>Mesh devices have roles. A <strong>relay node</strong> forwards messages from other devices, extending the network range. A <strong>proxy node</strong> bridges between GATT-connected devices (phones) and the mesh network, so a phone without mesh support can control mesh devices through a proxy. A <strong>friend node</strong> stores messages for nearby low-power nodes that spend most of their time sleeping. A <strong>low-power node</strong> periodically wakes up and asks its friend for any stored messages.</p>
<p>Mesh communication uses a publish/subscribe model. Devices publish messages to addresses, and devices subscribed to those addresses receive them. A light switch publishes "turn on" to a group address. All light bulbs subscribed to that group address turn on.</p>
<p>Data in mesh is organized using <strong>models</strong>. A model defines a set of messages that a device can send and receive. The Bluetooth SIG defines standard models for common use cases: Generic OnOff (on/off switches and lights), Generic Level (dimmers), Sensor (sensor data), and Lighting (color temperature, lightness, hue). You can also define custom models.</p>
<p>Here's a minimal mesh node configuration in <code>prj.conf</code>:</p>
<pre><code class="language-plaintext">CONFIG_BT=y
CONFIG_BT_MESH=y
CONFIG_BT_MESH_RELAY=y
CONFIG_BT_MESH_PB_ADV=y
CONFIG_BT_MESH_PB_GATT=y
CONFIG_BT_MESH_GATT_PROXY=y
CONFIG_BT_MESH_CFG_CLI=y
CONFIG_BT_MESH_HEALTH_SRV=y
</code></pre>
<p><code>CONFIG_BT_MESH=y</code> enables the mesh stack. <code>CONFIG_BT_MESH_RELAY=y</code> makes this node relay messages for other nodes. <code>CONFIG_BT_MESH_PB_ADV=y</code> and <code>CONFIG_BT_MESH_PB_GATT=y</code> enable provisioning (the process of adding a device to the mesh network) over both advertising and GATT connections. <code>CONFIG_BT_MESH_GATT_PROXY=y</code> enables the proxy role for phone connectivity.</p>
<p>A full mesh application involves defining your composition data (what models your device supports), implementing model handlers, and setting up provisioning. The Zephyr samples directory (<code>samples/bluetooth/mesh/</code>) contains several complete examples including a light bulb, a light switch, and a sensor server that demonstrate the full pattern.</p>
<h2 id="heading-le-audio-the-next-generation-of-bluetooth-audio">LE Audio: The Next Generation of Bluetooth Audio</h2>
<p><a href="https://www.freecodecamp.org/news/the-bluetooth-le-audio-handbook/">LE Audio</a> is the most significant addition to the Bluetooth specification in years. It replaces the Classic Bluetooth audio profile (A2DP) with a new system built entirely on BLE. Zephyr's implementation of LE Audio is one of the most complete available in any open-source stack.</p>
<p>The core of LE Audio is the LC3 (Low Complexity Communication Codec). LC3 provides better audio quality than the SBC codec used in classic Bluetooth audio, at half the bitrate. This means better sound quality and lower power consumption.</p>
<p>LE Audio introduces two communication modes. <strong>Connected Isochronous Streams (CIS)</strong> are point-to-point audio connections, similar to classic Bluetooth audio but more efficient. CIS is used for phone-to-earbud connections, hearing aids, and other paired audio scenarios.</p>
<p><strong>Broadcast Isochronous Streams (BIS)</strong> are one-to-many broadcasts. A single source can broadcast audio to an unlimited number of receivers. This is the technology behind Auracast, which envisions public venues broadcasting audio that any compatible device can tune into (think: silent TVs in airports, hearing assistance in theaters, multi-language broadcasts in conference halls).</p>
<p>Zephyr implements the full LE Audio profile stack: BAP (Basic Audio Profile), PACS (Published Audio Capabilities), ASCS (Audio Stream Control), VCP (Volume Control), MCP (Media Control), CCP (Call Control), TMAP (Telephony and Media Audio Profile), and CAP (Common Audio Profile).</p>
<p>The <code>prj.conf</code> for an LE Audio project includes:</p>
<pre><code class="language-plaintext">CONFIG_BT=y
CONFIG_BT_AUDIO=y
CONFIG_BT_BAP_UNICAST_SERVER=y
CONFIG_BT_PACS=y
CONFIG_BT_ASCS=y
CONFIG_BT_ISO=y
CONFIG_BT_PAC_SNK=y
CONFIG_BT_PAC_SRC=y
</code></pre>
<p>LE Audio development is considerably more complex than basic BLE peripheral development. It involves managing isochronous channels, configuring codec parameters, handling audio data streams, and implementing the various profile layers. The Zephyr samples in <code>samples/bluetooth/bap_unicast_server</code> and <code>samples/bluetooth/bap_broadcast_source</code> are the best starting points.</p>
<p>If you're building hearing aids, earbuds, or any audio device, LE Audio on Zephyr is worth serious investment. The open-source stack gives you full visibility into the implementation, and Nordic's nRF5340 and nRF54 series chips provide the hardware support needed.</p>
<h2 id="heading-debugging-bluetooth-applications">Debugging Bluetooth Applications</h2>
<p>BLE applications are harder to debug than regular embedded applications because the radio communication is invisible. You can't set a breakpoint in the air. Here are the tools and techniques that make BLE debugging manageable.</p>
<p><strong>Zephyr's logging subsystem</strong> is your first line of defense. Enable detailed Bluetooth logging in <code>prj.conf</code>:</p>
<pre><code class="language-plaintext">CONFIG_LOG=y
CONFIG_BT_DEBUG_LOG=y
CONFIG_BT_LOG_LEVEL_DBG=4
</code></pre>
<p><code>CONFIG_LOG=y</code> enables the logging framework. <code>CONFIG_BT_DEBUG_LOG=y</code> and <code>CONFIG_BT_LOG_LEVEL_DBG=4</code> enable debug-level logging for the Bluetooth stack.</p>
<p>This produces verbose output showing every HCI command, advertising event, connection event, GATT operation, and error. The output goes to the console (typically UART). It's extremely verbose, so only enable it during active debugging.</p>
<p><strong>The Zephyr shell</strong> provides an interactive command line for Bluetooth operations:</p>
<pre><code class="language-plaintext">CONFIG_SHELL=y
CONFIG_BT_SHELL=y
</code></pre>
<p>With these enabled, you get shell commands like <code>bt init</code>, <code>bt advertise on</code>, <code>bt connect</code>, <code>bt gatt discover</code>, and <code>bt gatt read</code> that let you control and inspect the Bluetooth stack interactively over the serial console. This is invaluable for testing because you can manually trigger operations and see the results without modifying firmware.</p>
<p><strong>nRF Connect for Mobile</strong> (iOS/Android) is an essential companion tool. Beyond scanning and connecting, it displays all GATT services and characteristics, lets you read/write/subscribe to characteristics, shows raw advertising data, and logs BLE events with timestamps. Use it to verify that your device is advertising correctly, that your GATT database looks right, and that reads/writes/notifications work as expected.</p>
<p><strong>Bluetooth sniffers</strong> capture the actual radio packets. The nRF52840 DK can be used as a sniffer with Nordic's nRF Sniffer for Bluetooth LE firmware. Combined with Wireshark (which has BLE protocol dissectors), you can inspect every packet on the wire: advertising PDUs, connection events, GATT requests/responses, and pairing exchanges. This is the ultimate debugging tool when something at the protocol level isn't working.</p>
<p><strong>Common debugging patterns.</strong> If advertising isn't visible, check that the advertising data doesn't exceed 31 bytes (for legacy advertising) and that the flags field is present.</p>
<p>If connections drop immediately, check that <code>CONFIG_BT_PERIPHERAL</code> is enabled (not just <code>CONFIG_BT_BROADCASTER</code>). If GATT reads return zero-length data, verify that your read callback returns the correct value from <code>bt_gatt_attr_read</code>. If notifications don't arrive, confirm that the central has written 0x0001 to the CCCD. If pairing fails, ensure <code>CONFIG_BT_SMP=y</code> is set and the authentication callbacks are registered.</p>
<h2 id="heading-power-optimization-for-ble-devices">Power Optimization for BLE Devices</h2>
<p>A BLE device that drains its battery in a week is a failed product. Power optimization isn't an afterthought – it's a core design constraint. Zephyr provides the tools, but you have to use them correctly.</p>
<p>The biggest power consumer in a BLE device is the radio. Every advertising event, connection event, and scan window activates the radio transmitter or receiver for milliseconds at a time, consuming milliamps of current. Reducing radio usage is the primary lever for extending battery life.</p>
<p>For advertising, increase the advertising interval. An interval of 1000 ms uses roughly 5x less power than an interval of 200 ms. If fast discovery isn't critical, use an even longer interval.</p>
<p>You can also use a two-phase approach: advertise at a fast interval (100 ms) for the first 30 seconds after power-on, then switch to a slow interval (1000 ms) for the steady state. Zephyr's advertising API supports changing parameters while advertising.</p>
<p>For connected devices, use the peripheral latency parameter. A peripheral latency of 4 means the peripheral can skip 4 connection events before it must respond. If the connection interval is 50 ms and the peripheral latency is 4, the peripheral only wakes up every 250 ms instead of every 50 ms. Request a longer connection interval from the central when high-throughput isn't needed:</p>
<pre><code class="language-c">static struct bt_le_conn_param conn_params = {
    .interval_min = 80,   /* 100 ms (units of 1.25 ms) */
    .interval_max = 160,  /* 200 ms */
    .latency = 4,
    .timeout = 400,       /* 4 seconds (units of 10 ms) */
};

/* After connection is established: */
bt_conn_le_param_update(conn, &amp;conn_params);
</code></pre>
<p>The <code>bt_conn_le_param_update</code> function sends a connection parameter update request to the central. The central may accept or reject the request. Most centrals (phones) accept reasonable parameter ranges.</p>
<p>Enable Zephyr's system power management:</p>
<pre><code class="language-plaintext">CONFIG_PM=y
CONFIG_PM_DEVICE=y
</code></pre>
<p>With power management enabled, the kernel puts the processor into a low-power state whenever no threads are ready to run.</p>
<p>On an nRF52840, the idle current drops from roughly 3 mA (active) to about 1.5 microamps (System OFF with RAM retention). The difference is enormous. The Zephyr power management policy automatically selects the deepest sleep state that the system can enter given the next scheduled wake-up event.</p>
<p>Disable unused peripherals in your devicetree overlay. Every enabled peripheral (UART, SPI, I2C) consumes power even when idle. If you don't need UART in production (you used it only for development logging), disable it:</p>
<pre><code class="language-dts">&amp;uart0 {
    status = "disabled";
};
</code></pre>
<p>Measure your actual current consumption. Tools like the Nordic Power Profiler Kit II show real-time current draw at microamp resolution. Zephyr's <code>CONFIG_THREAD_ANALYZER</code> helps you right-size thread stacks (over-allocated stacks waste RAM, which means more RAM needs to be powered).</p>
<p>The goal for a well-optimized BLE sensor is single-digit microamp average current, which translates to years of operation on a coin cell battery. Zephyr makes this achievable, but it requires attention to every layer: radio scheduling, peripheral management, clock configuration, and application design.</p>
<h2 id="heading-zephyr-bluetooth-vs-other-stacks">Zephyr Bluetooth vs Other Stacks</h2>
<p>When choosing a Bluetooth stack, you have options. Here is how Zephyr compares.</p>
<h3 id="heading-1-zephyr-vs-nordic-softdevice">1. Zephyr vs. Nordic SoftDevice</h3>
<p>Nordic's SoftDevice was their proprietary BLE stack before they moved to Zephyr. SoftDevice was a pre-compiled binary blob: you couldn't read or modify the stack code. Nordic's nRF Connect SDK (built on Zephyr) replaces SoftDevice with Zephyr's open-source stack. If you're starting a new Nordic project, use Zephyr. SoftDevice is legacy.</p>
<h3 id="heading-zephyr-vs-nimble-apache-mynewt">Zephyr vs. NimBLE (Apache Mynewt)</h3>
<p>NimBLE is a lightweight, open-source BLE host stack originally from the Apache Mynewt project (also usable standalone and ported to ESP-IDF). NimBLE is smaller than Zephyr's stack and may be a better fit for extremely RAM-constrained devices. Zephyr's stack is more feature-complete (LE Audio, Mesh, Direction Finding) and has broader industry adoption. For new products, Zephyr is the stronger choice unless you are severely RAM-constrained.</p>
<h3 id="heading-zephyr-vs-esp-idf-bluetooth">Zephyr vs. ESP-IDF Bluetooth</h3>
<p>Espressif's ESP-IDF includes a Bluetooth stack (Bluedroid or NimBLE) for ESP32 chips. If you're exclusively using ESP32 hardware, ESP-IDF is a valid choice. Zephyr supports ESP32 and gives you portability to other hardware, a unified build system, and a more comprehensive BLE feature set. If you might ever need to switch chips, Zephyr's portability is a significant advantage.</p>
<h3 id="heading-zephyr-vs-vendor-sdks-with-proprietary-stacks">Zephyr vs. vendor SDKs with proprietary stacks</h3>
<p>Many chip vendors provide their own BLE SDKs with closed-source stacks. These work well but lock you into that vendor's ecosystem. Zephyr gives you portability, open source, and the collective effort of a large community. The trade-off is a steeper learning curve.</p>
<p>For a product that needs to ship on a timeline with one known chip, a vendor SDK might be faster to start with. For a product line that spans multiple chips or that values long-term maintainability, Zephyr wins.</p>
<h2 id="heading-where-to-go-from-here">Where to Go from Here</h2>
<p>We've covered a lot of ground in this handbook. You should now understand BLE fundamentals, GAP advertising, GATT services, connections, notifications, pairing, Mesh, LE Audio, debugging, and power optimization. You've written code for every major concept. Here's how to continue.</p>
<p>Explore the Zephyr Bluetooth samples directory (<code>zephyr/samples/bluetooth/</code>). It contains over 30 Bluetooth-specific examples. The <code>peripheral_hr</code> sample implements a complete Heart Rate profile. The <code>central</code> sample shows how to build a scanning/connecting central role. The <code>mesh/</code> subdirectory has light switch, light bulb, and sensor examples for Mesh development. The <code>bap_*</code> samples demonstrate LE Audio.</p>
<p>Read the Zephyr Bluetooth documentation at <code>docs.zephyrproject.org/latest/connectivity/bluetooth/</code>. The API reference covers every function, macro, and configuration option. The Bluetooth architecture documentation explains how the host, controller, and HCI layers interact.</p>
<p>Get an nRF Connect SDK setup if you are using Nordic hardware. The nRF Connect SDK adds Nordic-specific features on top of Zephyr, including their Bluetooth libraries, proprietary wireless protocols (ESB, Gazell), and cellular modem support. It uses the same Zephyr kernel and build system.</p>
<p>Build something real. A BLE remote control for your desk lamp. A sensor that reports room temperature and humidity to your phone. A custom keyboard with BLE connectivity. A pet tracker.</p>
<p>The best way to solidify this knowledge is to encounter and solve real problems on real hardware. Every BLE product has its quirks (connection parameter negotiation with iOS vs Android, handling reconnection after bond loss, managing MTU size for efficient data transfer), and you learn those quirks by building.</p>
<p>The BLE ecosystem is enormous and growing. Zephyr gives you a production-quality, open-source foundation to build on, with a community and corporate backing that ensures it will be actively developed for years to come. You now have the knowledge to start building on it.</p>
<h2 id="heading-summary">Summary</h2>
<p>This handbook covered the full spectrum of Bluetooth development on Zephyr OS, from foundational concepts to production-ready features.</p>
<p>The BLE fundamentals section established the mental model that everything else builds on: GAP controls discovery and connections, GATT defines how data is structured and exchanged, services and characteristics form the API of your device, and UUIDs identify every component. These concepts are not Zephyr-specific&nbsp;– they apply to any BLE stack.</p>
<p>On the Zephyr side, you built progressively more complex applications. The beacon demonstrated the minimal BLE setup: initialize the stack and start advertising. The LED service introduced GATT with read and write characteristics, showing how to control hardware from a phone. The notification examples added real-time data push, and the complete sensor node tied together advertising, GATT, connection management, and periodic data delivery into a single cohesive application.</p>
<p>Each of these patterns (advertising data construction, GATT callback signatures, CCC handling, connection reference management) will appear in every BLE project you build.</p>
<p>The standard Heart Rate profile showed how SIG-defined 16-bit UUIDs integrate with Zephyr's predefined UUID macros, enabling interoperability with generic BLE apps.</p>
<p>The central role example demonstrated the other side of BLE: scanning, connecting, and discovering services on remote devices. MTU negotiation and PHY selection are the two primary levers for optimizing data throughput and range after a connection is established, and both require coordinated configuration in Kconfig and runtime API calls.</p>
<p>The DFU section addressed the production requirement of field-updatable firmware, integrating MCUboot with SMP over BLE.</p>
<p>Beyond point-to-point connections, Bluetooth Mesh extends BLE into many-to-many networks for building automation and large-scale IoT deployments, while LE Audio represents the next generation of wireless audio with the LC3 codec and broadcast capabilities. Pairing and security protect production devices from unauthorized access. Power optimization, debugging tooling, and stack comparison round out the practical knowledge needed to ship a real product.</p>
<p>The code patterns and Kconfig options presented here form the toolkit for building any BLE device on Zephyr, from a simple beacon to a complex multi-protocol gateway.</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>
        
            <item>
                <title>
                    <![CDATA[ How to Debug and Prevent Buffer Overflows in Embedded Systems ]]>
                </title>
                <description>
                    <![CDATA[ Buffer overflows are one of the most serious software bugs, especially in embedded systems, where hardware limitations and real-time execution make them hard to detect and fix. A buffer overflow happens when a program writes more data into a buffer t... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-debug-and-prevent-buffer-overflows-in-embedded-systems/</link>
                <guid isPermaLink="false">67d84f228d156200bc7d3d8c</guid>
                
                    <category>
                        <![CDATA[ embedded systems ]]>
                    </category>
                
                    <category>
                        <![CDATA[ embedded ]]>
                    </category>
                
                    <category>
                        <![CDATA[ memory-management ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Buffer Overfow ]]>
                    </category>
                
                    <category>
                        <![CDATA[ debugging ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Firmware Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Code Quality ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Software Engineering ]]>
                    </category>
                
                    <category>
                        <![CDATA[ learn to code ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Programming basics ]]>
                    </category>
                
                    <category>
                        <![CDATA[ C ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Coding Best Practices ]]>
                    </category>
                
                    <category>
                        <![CDATA[ clean code ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Soham Banerjee ]]>
                </dc:creator>
                <pubDate>Mon, 17 Mar 2025 16:34:42 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742229245130/858b21cc-443e-43ee-82ce-091438f6c5c0.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Buffer overflows are one of the most serious software bugs, especially in embedded systems, where hardware limitations and real-time execution make them hard to detect and fix.</p>
<p>A buffer overflow happens when a program writes more data into a buffer than it was allocated, leading to memory corruption, crashes, or even security vulnerabilities. A buffer corruption occurs when unintended modifications overwrite unread data or modify memory in unexpected ways.</p>
<p>In safety-critical systems like cars, medical devices, and spacecraft, buffer overflows can cause life-threatening failures. Unlike simple software bugs, buffer overflows are unpredictable and depend on the state of the system, making them difficult to diagnose and debug.</p>
<p>To prevent these issues, it's important to understand how buffer overflows and corruptions occur, and how to detect and fix them.</p>
<h2 id="heading-article-scope">Article Scope</h2>
<p>In this article, you will learn:</p>
<ol>
<li><p>What buffers, buffer overflows, and corruptions are. I’ll give you a beginner-friendly explanation with real-world examples.</p>
</li>
<li><p>How to debug buffer overflows. You’ll learn how to use tools like GDB, LLDB, and memory maps to find memory corruption.</p>
</li>
<li><p>How to prevent buffer overflows. We’ll cover some best practices like input validation, safe memory handling, and defensive programming.</p>
</li>
</ol>
<p>I’ll also show you some hands-on code examples – simple C programs that demonstrate buffer overflow issues and how to fix them.</p>
<p>What this article doesn’t cover:</p>
<ol>
<li><p>Security exploits and hacking techniques. We’ll focus on preventing accidental overflows, not hacking-related buffer overflows.</p>
</li>
<li><p>Operating system-specific issues. This guide is for embedded systems, not general-purpose computers or servers.</p>
</li>
<li><p>Advanced RTOS memory management. While we discuss interrupt-driven overflows, we won’t dive deep into real-time operating system (RTOS) concepts.</p>
</li>
</ol>
<p>Now that you know what this article covers (and what it doesn’t), let’s go over the skills that will help you get the most out of it.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>This article is designed for developers who have some experience with C programming and want to understand how to debug and prevent buffer overflows in embedded systems. Still, beginners can follow along, as I’ll explain key concepts in a clear and structured way.</p>
<p>Before reading, it helps if you know:</p>
<ol>
<li><p>Basic C programming.</p>
</li>
<li><p>How memory works – the difference between stack, heap, and global variables.</p>
</li>
<li><p>Basic debugging concepts – if you’ve used a debugger like GDB or LLDB, that’s a plus, but not required.</p>
</li>
<li><p>What embedded systems are – a basic idea of how microcontrollers store and manage memory.</p>
</li>
</ol>
<p>Even if you’re not familiar with these topics, this guide will walk you through them in an easy-to-understand way.</p>
<p>Before you dive into buffer overflows, debugging, and prevention, let’s take a step back and understand what a buffer is and why it’s important in embedded systems. Buffers play a crucial role in managing data flow between hardware and software but when handled incorrectly, they can lead to serious software failures.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-a-buffer-and-how-does-it-work">What is a Buffer, and How Does it Work?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-a-buffer-overflow">What is a Buffer Overflow?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-common-causes-of-buffer-overflows-and-corruption">Common Causes of Buffer Overflows and Corruption</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-consequences-of-buffer-overflows">Consequences of Buffer Overflows</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-debug-buffer-overflows">How to Debug Buffer Overflows</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-prevent-buffer-overflows">How to Prevent Buffer Overflows</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-a-buffer-and-how-does-it-work">What is a Buffer, and How Does it Work?</h2>
<p>A buffer is a contiguous block of memory used to temporarily store data before it is processed. Buffers are commonly used in two scenarios:</p>
<ol>
<li><p>Data accumulation: When the system needs to collect a certain amount of data before processing.</p>
</li>
<li><p>Rate matching: When the data producer generates data faster than the data consumer can process it.</p>
</li>
</ol>
<p>Buffers are typically implemented as arrays in C, where elements are indexed from 0 to N-1 (where N is the buffer size).</p>
<p>Let’s look at an example of a buffer in a sensor system.</p>
<p>Consider a system with a sensor task that generates data at 400 Hz (400 samples per second or 1 sample every 2.5 ms). But the data processor (consumer) operates at only 100 Hz (100 samples per second or 1 sample every 10 ms). Since the consumer task is slower than the producer, we need a buffer to store incoming data until it is processed.</p>
<p>To determine the buffer size, we calculate:</p>
<p>Buffer Size = Time to consume 1 sample / Time to generate 1 sample = 10 ms/ 2.5 ms = 4</p>
<p>This means the buffer must hold at least 4 samples at a time to avoid data loss.</p>
<p>Once the buffer reaches capacity, there are several strategies to decide which data gets passed to the consumer task:</p>
<ol>
<li><p>Max/min sampling: Use the maximum or minimum value in the buffer.</p>
</li>
<li><p>Averaging: Compute the average of all values in the buffer.</p>
</li>
<li><p>Random access: Pick a sample from a specific location (for example, the most recent or the first).</p>
</li>
</ol>
<p>In real-world applications, it’s beneficial to use circular buffers or double buffering to prevent data corruption.</p>
<ul>
<li><p>Circular buffer approach: A circular buffer (also called a ring buffer) continuously wraps around when it reaches the end, ensuring old data is overwritten safely without exceeding memory boundaries. The buffer size should be multiplied by 2 (4 × 2 = 8) to hold 8 samples. This allows the consumer task to process 4 samples while the next 4 samples are being filled, preventing data overwrites.</p>
</li>
<li><p>Double buffer approach: Double buffering is useful when data loss is unacceptable. It allows continuous data capture while the processor is busy handling previous data. A second buffer of the same size is added. When the first buffer is full, the write pointer switches to the second buffer, allowing the consumer task to process data from the first buffer while the second buffer is being filled. This prevents data overwrites and ensures a continuous data flow.</p>
</li>
</ul>
<p>Buffers help manage data efficiently, but what happens when they are mismanaged? This is where buffer overflows and corruptions come into play.</p>
<h2 id="heading-what-is-a-buffer-overflow">What is a Buffer Overflow?</h2>
<p>A buffer overflow occurs when a program writes more data into a buffer than it was allocated, causing unintended memory corruption. This can lead to unpredictable behavior, ranging from minor bugs to critical system failures.</p>
<p>To understand buffer overflow, let's use a simple analogy. Imagine a jug with a tap near the bottom. The jug represents a buffer, while the tap controls how much liquid (data) is consumed.</p>
<p>The jug is designed to hold a fixed amount of liquid. As long as water flows into the jug at the same rate or slower than it flows out, everything works fine. But if water flows in faster than it flows out, the jug will eventually overflow.</p>
<p>Similarly, in software, if data enters a buffer faster than it is processed, it exceeds the allocated memory space, causing a buffer overflow. In the case of a circular buffer, this can cause the write pointer to wrap around and overwrite unread data, leading to buffer corruption.</p>
<h3 id="heading-buffer-overflows-in-software">Buffer Overflows in Software</h3>
<p>Unlike the jug, where water simply spills over, a buffer overflow in software overwrites adjacent memory locations. This can cause a variety of hard-to-diagnose issues, including:</p>
<ol>
<li><p>Corrupting other data stored nearby.</p>
</li>
<li><p>Altering program execution, leading to crashes.</p>
</li>
<li><p>Security vulnerabilities, where attackers exploit overflows to inject malicious code.</p>
</li>
</ol>
<p>When a buffer overflow occurs, data can overwrite variables, function pointers, or even return addresses, depending on where the buffer is allocated.</p>
<p>Buffer overflows can occur in different memory regions:</p>
<ol>
<li><p>Buffer overflows in global/static memory (.bss / .data sections)</p>
<ul>
<li><p>These occur when global or static variables exceed their allocated size.</p>
</li>
<li><p>The overflow can corrupt adjacent variables, leading to unexpected behavior in other modules.</p>
</li>
<li><p>Debugging is easier because memory addresses are fixed at compile time unless the compiler optimizes them. Map files provide a memory layout of variables during the compilation and linking.</p>
</li>
</ul>
</li>
<li><p>Stack-based buffer overflow (more predictable, easier to debug):</p>
<ul>
<li><p>Happens when a buffer is allocated in the stack (for example, local variables inside functions).</p>
</li>
<li><p>Overflowing the stack can affect adjacent local variables or return addresses, potentially crashing the program.</p>
</li>
<li><p>In embedded systems with small stack sizes, this often leads to a crash or execution of unintended code.</p>
</li>
</ul>
</li>
<li><p>Heap-based buffer overflow (harder to debug):</p>
<ul>
<li><p>Happens when a buffer is dynamically allocated in the heap (for example, using malloc() in C).</p>
</li>
<li><p>Overflowing a heap buffer can corrupt adjacent dynamically allocated objects or heap management structures.</p>
</li>
<li><p>Debugging is harder because heap memory is allocated dynamically at runtime, causing memory locations to vary.</p>
</li>
</ul>
</li>
</ol>
<h4 id="heading-buffer-overflow-vs-buffer-corruption">Buffer Overflow vs Buffer Corruption</h4>
<p>Buffer overflow and buffer corruption are of course related, but refer to different situations.</p>
<p>A buffer overflow happens when data is written beyond the allocated buffer size, leading to memory corruption, unpredictable behavior, or system crashes.</p>
<p>A buffer corruption happens when unintended data modifications result in unexpected software failures, even if the write remains within buffer boundaries.</p>
<p>Both issues typically result from poor write pointer management, lack of boundary checks, and unexpected system behavior.</p>
<p>Now that we've covered what a buffer overflow is and how it can overwrite memory, let’s take a closer look at how these issues affect embedded systems.</p>
<p>In the next section, we’ll explore how buffer overflows and corruption happen in real-world embedded systems and break down common causes, including pointer mismanagement and boundary violations.</p>
<h2 id="heading-common-causes-of-buffer-overflows-and-corruption">Common Causes of Buffer Overflows and Corruption</h2>
<p>Embedded systems use buffers to store data from sensors, communication interfaces (like UART (Universal Asynchronous Receiver-Transmitter), SPI (Serial Peripheral Interface), I2C (Inter-integrated Circuit), and real-time tasks. These buffers are often statically allocated to avoid memory fragmentation, and many implementations use circular (ring) buffers to efficiently handle continuous data streams.</p>
<p>Here are three common scenarios where buffer overflows or corruptions occur in embedded systems:</p>
<h3 id="heading-writing-data-larger-than-the-available-space">Writing Data Larger Than the Available Space</h3>
<p><strong>Issue</strong>: The software writes incoming data to the buffer without checking if there is enough space.</p>
<p><strong>Example</strong>: Imagine a 100-byte buffer to store sensor data. The buffer receives variable-sized packets. If an incoming packet is larger than the remaining space, it will overwrite adjacent memory, leading to corruption.</p>
<p>So why does this happen?</p>
<ul>
<li><p>Some embedded designs increment the write pointer after copying data, making it too late to prevent overflow.</p>
</li>
<li><p>Many low-level memory functions (memcpy, strcpy, etc.) do not check buffer boundaries, leading to unintended writes.</p>
</li>
<li><p>Without proper bound checking, a large write can exceed the buffer size and corrupt nearby memory.</p>
</li>
</ul>
<p>Here’s a code sample to demonstrate buffer overflow in a .bss / .data section:</p>
<pre><code class="lang-c">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdint.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdio.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;string.h&gt;</span></span>

  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> BUFFER_SIZE 300</span>

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">uint16_t</span> sample_count = <span class="hljs-number">0</span>;
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">uint8_t</span> buffer[BUFFER_SIZE] = {<span class="hljs-number">0</span>};

  <span class="hljs-comment">// Function to simulate a buffer overflow scenario</span>
  <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">updateBufferWithData</span><span class="hljs-params">(<span class="hljs-keyword">uint8_t</span> *data, <span class="hljs-keyword">uint16_t</span> size)</span>
  </span>{
      <span class="hljs-comment">// Simulating a buffer overflow: No boundary check!</span>
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Attempting to write %d bytes at position %d...\n"</span>, size, sample_count);

      <span class="hljs-comment">// Deliberate buffer overflow for demonstration</span>
      <span class="hljs-keyword">if</span> (sample_count + size &gt; BUFFER_SIZE)
      {
          <span class="hljs-built_in">printf</span>(<span class="hljs-string">"WARNING: Buffer Overflow Occurred! Writing beyond allocated memory!\n"</span>);
      }

      <span class="hljs-comment">// Copy data (unsafe, can cause overflow)</span>
      <span class="hljs-built_in">memcpy</span>(&amp;buffer[sample_count], data, size);

      <span class="hljs-comment">// Increment sample count (incorrectly, leading to wraparound issues)</span>
      sample_count += size;
  }

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span>
  </span>{   
      <span class="hljs-comment">// Save 1 byte to buffer</span>
      <span class="hljs-keyword">uint8_t</span> data_to_buffer = <span class="hljs-number">10</span>;
      updateBufferWithData(&amp;data_to_buffer, <span class="hljs-number">1</span>);

      <span class="hljs-comment">// Save an array of 20 bytes to buffer</span>
      <span class="hljs-keyword">uint8_t</span> data_to_buffer_1[<span class="hljs-number">20</span>] = {<span class="hljs-number">5</span>};
      updateBufferWithData(data_to_buffer_1, <span class="hljs-keyword">sizeof</span>(data_to_buffer_1));

      <span class="hljs-comment">// Intentional buffer overflow: Save an array of 50 x 8 bytes (400 bytes)</span>
      <span class="hljs-keyword">uint64_t</span> data_to_buffer_2[<span class="hljs-number">50</span>] = {<span class="hljs-number">7</span>};
      updateBufferWithData((<span class="hljs-keyword">uint8_t</span>*)data_to_buffer_2, <span class="hljs-keyword">sizeof</span>(data_to_buffer_2));

      <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
  }
</code></pre>
<h3 id="heading-interrupt-driven-overflows-real-time-systems">Interrupt-Driven Overflows (Real-time Systems)</h3>
<p><strong>Issue</strong>: The interrupt service routine (ISR) may write data faster than the main task can process, leading to buffer corruption or buffer overflow if the write pointer is not properly managed.</p>
<p><strong>Example</strong>: Imagine a sensor ISR that writes incoming data into a buffer every time a new reading arrives. Meanwhile, a low-priority processing task reads and processes the data.</p>
<p>What can go wrong?</p>
<ul>
<li><p>If the ISR triggers too frequently (due to a misbehaving sensor or high interrupt priority), the buffer may fill up faster than the processing task can keep up.</p>
</li>
<li><p>This can result in one of two failures:</p>
<ol>
<li><p>Buffer Corruption: The ISR overwrites unread data, leading to loss of information.</p>
</li>
<li><p>Buffer Overflow: The ISR exceeds buffer boundaries, causing memory corruption or system crashes.</p>
</li>
</ol>
</li>
</ul>
<p>So why does this happen?</p>
<ul>
<li><p>In real-time embedded systems, ISR execution preempts lower-priority tasks.</p>
</li>
<li><p>If the processing task doesn't not get enough CPU time, the buffer may become overwritten or overflow beyond its allocated scope.</p>
</li>
</ul>
<h3 id="heading-system-state-changes-amp-buffer-corruption">System State Changes &amp; Buffer Corruption</h3>
<p><strong>Issue</strong>: The system may unexpectedly reset, enter low-power mode, or changes operating state, leaving the buffer write pointers in an inconsistent state. This can result in buffer corruption (stale or incorrect data) or buffer overflow (writing past the buffer’s limits.</p>
<p><strong>Example Scenarios</strong>:</p>
<ol>
<li><p>Low-power wake-up issue (Buffer Overflow risk): Some embedded systems enter deep sleep to conserve energy. Upon waking up, if the buffer write pointer is not correctly reinitialized, it may point outside buffer boundaries, leading to buffer overflow and unintended memory corruption.</p>
</li>
<li><p>Unexpected mode transitions: If a sensor task is writing data and the system suddenly switches modes, the buffer states and pointers may not be cleaned up. The next time the sensor task runs, it may continue writing without clearing previous data. This can cause undefined behavior due to presence of stale data.</p>
</li>
</ol>
<p>Now that you understand how buffer overflows and corruptions happen, let’s examine their consequences in embedded systems ranging from incorrect sensor readings to complete system failures, making debugging and prevention critical.</p>
<h2 id="heading-consequences-of-buffer-overflows">Consequences of Buffer Overflows</h2>
<p>Buffer overflows can be catastrophic in embedded systems, leading to system crashes, data corruption, and unpredictable behavior. Unlike general-purpose computers, many embedded devices lack memory protection, making them particularly vulnerable to buffer overflows.</p>
<p>A buffer overflow can corrupt two critical types of memory:</p>
<h3 id="heading-1-data-variables-corruption">1. Data Variables Corruption</h3>
<p>A buffer overflow can overwrite data variables, corrupting the inputs for other software modules. This can cause unexpected behavior or even system crashes if critical parameters are modified.</p>
<p>For example, a buffer overflow could accidentally overwrite a sensor calibration value stored in memory. As a result, the system would start using incorrect sensor readings, leading to faulty operation and potentially unsafe conditions.</p>
<h3 id="heading-2-function-pointer-corruption">2. Function Pointer Corruption</h3>
<p>In embedded systems, function pointers are often used for interrupt handlers, callback functions, and RTOS task scheduling. If a buffer overflow corrupts a function pointer, the system may execute unintended instructions, leading to a crash or unexpected behavior.</p>
<p>As an example, a function pointer controlling motor speed regulation could be overwritten. Instead of executing the correct function, the system would jump to a random memory address, causing a system fault or erratic motor behavior.</p>
<p>Buffer overflows are among the hardest bugs to identify and fix because their effects depend on which data is corrupted and the values it contains. A buffer overflow can affect memory in different ways:</p>
<ul>
<li><p>If a buffer overflow corrupts unused memory, the system may seem fine during testing, making the issue harder to detect.</p>
</li>
<li><p>if a buffer overflow alters critical data variables, it can cause hidden logic errors that cause unpredictable behavior.</p>
</li>
<li><p>If a buffer overflow corrupts function pointers, it may crash immediately, making the problem easier to identify.</p>
</li>
</ul>
<p>During development, if tests focus only on detecting crashes, they may overlook silent memory corruption caused by a buffer overflow. In real-world deployments, new use cases not covered in testing can trigger previously undetected buffer overflow issues, leading to unpredictable failures.</p>
<p>Buffer overflows can cause a chain reaction, where one overflow leads to another overflow or buffer corruption, resulting in widespread system failures. So how does this happen?</p>
<ol>
<li><p>A buffer overflow corrupts a critical variable (for example, a timer interval).</p>
</li>
<li><p>The corrupted variable disrupts another module (for example, triggers the timer interrupt too frequently, causing it to push more data into a buffer than intended.).</p>
</li>
<li><p>This increased interrupt frequency forces a sensor task to write data faster than intended, eventually causing another buffer overflow or corruption by overwriting unread data.</p>
</li>
</ol>
<p>This chain reaction can spread across multiple software modules, making debugging nearly impossible. In real-word applications, buffer overflows in embedded systems can be life-threatening:</p>
<ul>
<li><p>In cars: A buffer overflow in an ECU (Electronic Control Unit) could cause brake failure or unintended acceleration.</p>
</li>
<li><p>In a spacecraft: A memory corruption issue could disable navigation systems, leading to mission failure.</p>
</li>
</ul>
<p>Now that we’ve seen how buffer overflows can corrupt memory, disrupt system behavior, and even cause critical failures, the next step is understanding how to detect and fix them before they lead to serious issues.</p>
<h2 id="heading-how-to-debug-buffer-overflows">How to Debug Buffer Overflows</h2>
<p>Debugging buffer overflows in embedded systems can be complex, as their effects range from immediate crashes to silent data corruption, making them difficult to trace. A buffer overflow can cause either:</p>
<ol>
<li><p>A system crash, which is easier to detect since it halts execution or forces a system reboot.</p>
</li>
<li><p>Unexpected behavior, which is much harder to debug as it requires tracing how corrupted data affects different modules.</p>
</li>
</ol>
<p>This section focuses on embedded system debugging techniques using memory map files, debuggers (GDB/LLDB), and a structured debugging approach. Let’s look into the debuggers and memory map files.</p>
<h3 id="heading-memory-map-file-map-file">Memory Map File (.map file)</h3>
<p>A memory map file is generated during the linking process. It provides a memory layout of global/static variables, function addresses, and heap/stack locations. It provides a memory layout of Flash and RAM, including:</p>
<ul>
<li><p>Text section (.text): Stores executable code.</p>
</li>
<li><p>Read-only section (.rodata): Stores constants and string literals.</p>
</li>
<li><p>BSS section (.bss): Stores uninitialized global and static variables.</p>
</li>
<li><p>Data section (.data): Stores initialized global and static variables.</p>
</li>
<li><p>Heap and stack locations, depending on the linker script.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739064875727/1e01992d-4d9d-42fb-b971-6f4e92452c22.png" alt="Figure 1: A visual of the memory layout" class="image--center mx-auto" width="1256" height="425" loading="lazy"></p>
<p>If a buffer overflow corrupts a global variable, the .map file can identify nearby variables that may also be affected, provided the compiler has not optimized the memory allocation. Similarly, if a function pointer is corrupted, the .map file can reveal where it was stored in memory.</p>
<h3 id="heading-debuggers-gdb-amp-lldb">Debuggers (GDB &amp; LLDB)</h3>
<p>Debugging tools like GDB (GNU Debugger) and LLDB (LLVM Debugger) allow:</p>
<ul>
<li><p>Controlling execution (breakpoints, stepping through code).</p>
</li>
<li><p>Inspecting variable values and memory addresses.</p>
</li>
<li><p>Getting backtraces (viewing function calls before a crash).</p>
</li>
<li><p>Extracting core dumps from microcontrollers for post-mortem analysis.</p>
</li>
</ul>
<p>If the system halts on a crash, a backtrace (bt command in GDB) can reveal which function was executing before failure. If the overflow affects a heap-allocated variable, GDB can inspect heap memory usage to detect corruption.</p>
<h3 id="heading-the-debugging-process">The Debugging Process</h3>
<p>Now, let’s go through a step-by-step debugging process to identify and fix buffer overflows. Once a crash or unexpected behavior occurs, follow these techniques to trace the root cause:</p>
<h4 id="heading-step-1-identify-the-misbehaving-module">Step 1: Identify the misbehaving module</h4>
<p>If the system crashes, use GDB or LLDB backtrace (bt command) to locate the last executed function. If the system behaves unexpectedly, determine which software module controls the affected functionality.</p>
<h4 id="heading-step-2-analyze-inputs-and-outputs-of-the-module">Step 2: Analyze inputs and outputs of the module</h4>
<p>Every function or module has inputs and outputs. Create a truth table listing expected outputs for all possible inputs. Check if the unexpected behavior matches any undefined input combination, which may indicate corruption.</p>
<h4 id="heading-step-3-locate-memory-corruption-using-address-analysis">Step 3: Locate memory corruption using address analysis</h4>
<p>If a variable shows incorrect values, determine its physical memory location. Depending on where the variable is stored:</p>
<ol>
<li><p>Global/static variables (.bss / .data): Look up the memory map file for nearby buffers.</p>
</li>
<li><p>Heap variables: Snapshot heap allocations using GDB.  </p>
<p> Here’s an example of using GDB to find corrupted variables:</p>
<pre><code class="lang-c"> (gdb) print &amp;my_variable  # Get memory address of the variable
 $<span class="hljs-number">1</span> = (<span class="hljs-keyword">int</span> *) <span class="hljs-number">0x20001000</span>
 (gdb) x/<span class="hljs-number">10</span>x <span class="hljs-number">0x20001000</span>   # Examine memory near <span class="hljs-keyword">this</span> address, Display <span class="hljs-number">10</span> memory words in hexadecimal format starting from <span class="hljs-number">0x20001000</span>
</code></pre>
</li>
</ol>
<h4 id="heading-step-4-identify-the-overflowing-buffer">Step 4: Identify the overflowing buffer</h4>
<p>If a buffer is located just before the corrupted variable, inspect its usage in the code. Review all possible code paths that write to the buffer. Check if any design limitations could cause an overflow under a specific use cases.</p>
<h4 id="heading-step-5-fix-the-root-cause">Step 5: Fix the root cause</h4>
<p>If the buffer overflow happened due to missing bounds checks, add proper input validation to prevent it. Buffer design should enforce strict memory limits. The module should implement strict boundary checks for all inputs and maintain a consistent state.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739065828677/74322607-5997-4275-87d0-b3d0acf54373.png" alt="Figure 2: Steps to debug a buffer overflow" class="image--center mx-auto" width="1105" height="325" loading="lazy"></p>
<p>In addition to GDB/LLDB, you can also use techniques like hardware tracing and fault injection to simulate buffer overflows and observe system behavior in real-time.</p>
<p>While debugging helps identify and fix buffer overflows, prevention is always the best approach. Let’s explore techniques that can help avoid buffer overflows altogether.</p>
<h2 id="heading-how-to-prevent-buffer-overflows">How to Prevent Buffer Overflows</h2>
<p>You can often prevent buffer overflows through good software design, defensive programming, hardware protections, and rigorous testing. Embedded systems, unlike general-purpose computers, often lack memory protection mechanisms, which means that buffer overflow prevention critical for system reliability and security.</p>
<p>Here are some key techniques to help prevent buffer overflows:</p>
<h3 id="heading-defensive-programming">Defensive Programming</h3>
<p>Defensive programming helps minimize buffer overflow risks by ensuring all inputs are validated and unexpected conditions are handled safely.</p>
<p>First, it’s crucial to validate input size before writing to a buffer. Always check the write index by adding the size of data to be written prior to writing data to make sure more data is not written than the available buffer space.</p>
<p>Then you’ll want to make sure you have proper error handling and fail-safe mechanisms in place. If an input is invalid, halt execution, log the error, or switch to a safe state. Also, functions should indicate success/failure with helpful error codes to prevent misuse.</p>
<p>Sample Code:</p>
<pre><code class="lang-c">   <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdint.h&gt;</span></span>
   <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;string.h&gt;</span></span>
   <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdbool.h&gt;</span></span>
   <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdio.h&gt;</span></span>

   <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> BUFFER_SIZE 300</span>

   <span class="hljs-keyword">static</span> <span class="hljs-keyword">uint16_t</span> sample_count = <span class="hljs-number">0</span>;
   <span class="hljs-keyword">static</span> <span class="hljs-keyword">uint8_t</span> buffer[BUFFER_SIZE] = {<span class="hljs-number">0</span>};

   <span class="hljs-keyword">typedef</span> <span class="hljs-keyword">enum</span>
   {
       SUCCESS = <span class="hljs-number">0</span>,
       NOT_ENOUGH_SPACE = <span class="hljs-number">1</span>,
       DATA_IS_INVALID = <span class="hljs-number">2</span>,
   } buffer_err_code_e;


   <span class="hljs-function">buffer_err_code_e <span class="hljs-title">updateBufferWithData</span><span class="hljs-params">(<span class="hljs-keyword">uint8_t</span> *data, <span class="hljs-keyword">uint16_t</span> size)</span>
   </span>{
       <span class="hljs-keyword">if</span> (data == <span class="hljs-literal">NULL</span> || size == <span class="hljs-number">0</span> || size &gt; BUFFER_SIZE)  
       {
           <span class="hljs-keyword">return</span> DATA_IS_INVALID; <span class="hljs-comment">// Invalid input size</span>
       }

       <span class="hljs-keyword">uint16_t</span> available_space = BUFFER_SIZE - sample_count;
       <span class="hljs-keyword">bool</span> can_write = (available_space &gt;= size) ? <span class="hljs-literal">true</span> : <span class="hljs-literal">false</span>;

       <span class="hljs-keyword">if</span> (!can_write)  
       {
           <span class="hljs-keyword">return</span> NOT_ENOUGH_SPACE;
       }

       <span class="hljs-comment">// Copy data safely</span>
       <span class="hljs-built_in">memcpy</span>(&amp;buffer[sample_count], data, size);
       sample_count += size;

       <span class="hljs-keyword">return</span> SUCCESS;
   }

   <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span>
   </span>{   
       buffer_err_code_e ret;

       <span class="hljs-comment">// Save 1 byte to buffer</span>
       <span class="hljs-keyword">uint8_t</span> data_to_buffer = <span class="hljs-number">10</span>;
       ret = updateBufferWithData(&amp;data_to_buffer, <span class="hljs-keyword">sizeof</span>(data_to_buffer));
       <span class="hljs-keyword">if</span> (ret)  
       {
           <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Buffer update didn't succeed, Err:%d\n"</span>, ret);
       }

       <span class="hljs-comment">// Save an array of 20 bytes to buffer</span>
       <span class="hljs-keyword">uint8_t</span> data_to_buffer_1[<span class="hljs-number">20</span>] = {<span class="hljs-number">5</span>};
       ret = updateBufferWithData(data_to_buffer_1, <span class="hljs-keyword">sizeof</span>(data_to_buffer_1));
       <span class="hljs-keyword">if</span> (ret)  
       {
           <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Buffer update didn't succeed, Err:%d\n"</span>, ret);
       }

       <span class="hljs-comment">// Save an array of 50 x 8 bytes, Intentional buffer overflow</span>
       <span class="hljs-keyword">uint64_t</span> data_to_buffer_2[<span class="hljs-number">50</span>] = {<span class="hljs-number">7</span>};
       ret = updateBufferWithData((<span class="hljs-keyword">uint8_t</span>*)data_to_buffer_2, <span class="hljs-keyword">sizeof</span>(data_to_buffer_2));  
       <span class="hljs-keyword">if</span> (ret)  
       {
           <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Buffer update didn't succeed, Err:%d\n"</span>, ret);
       }

       <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
   }
</code></pre>
<h3 id="heading-choosing-the-right-buffer-design-and-size">Choosing the Right Buffer Design And Size</h3>
<p>Some buffer designs handle overflow better than others. Choosing the correct buffer type and size for the application reduces the risk of corruption.</p>
<ul>
<li><p>Circular Buffers (Ring Buffers) prevent out-of-bounds writes by wrapping around. They overwrite the oldest data instead of corrupting memory. These are useful for real-time streaming data (for example, UART, sensor readings). This approach is ideal for applications where data loss is unacceptable.</p>
</li>
<li><p>Ping-Pong Buffers (Double Buffers) use two buffers. One buffer fills up with data. Then, once it’s full, it switches to the second buffer while the first one is processed. This approach is beneficial for application that have strict requirements on no data loss. The buffer design should be based on the speed of write and read tasks.</p>
</li>
</ul>
<h3 id="heading-hardware-protection">Hardware Protection</h3>
<h4 id="heading-memory-protection-unit-mpu">Memory Protection Unit (MPU)</h4>
<p>An MPU (Memory Protection Unit) helps detect unauthorized memory accesses, including buffer overflows, by restricting which regions of memory can be written to. It prevents buffer overflows from modifying critical memory regions and triggers a MemManage Fault if a process attemps to write outside an allowed region.</p>
<p>But keep in mind that, an MPU does not prevent buffer overflows – it only detects and stops execution when they occur. Not all microcontrollers have an MPU, and some low-end MCUs lack hardware protection, making software-based safeguards even more critical.</p>
<p>Modern C compilers provide several flags to identify memory errors at compile-time:</p>
<ol>
<li><p>-Wall -Wextra: Enables useful warnings</p>
</li>
<li><p>-Warray-bounds: Detects out-of-bounds array access when the array size is known at compile-time</p>
</li>
<li><p>-Wstringop-overflow: Warns about possible overflows in string functions like memcpy and strcpy.</p>
</li>
</ol>
<h3 id="heading-testing-and-validation">Testing and Validation</h3>
<p>Testing helps detect buffer overflows before deployment, reducing the risk of field failures. Unit testing each function independently with valid inputs, boundary cases, and invalid inputs helps detect buffer-related issues early. Automated testing involves feeding random and invalid inputs into the system to uncover crashes and unexpected behavior. Static Analysis Tools like Coverity, Clang Static Analyzer help detect buffer overflows before runtime. Run real-world inputs on embedded hardware to detect issues.</p>
<p>Now that we've explored how to identify, debug, and prevent buffer overflows, it’s clear that these vulnerabilities pose a significant threat to embedded systems. From silent data corruption to catastrophic system failures, the consequences can be severe.</p>
<p>But with the right debugging tools, systematic analysis, and preventive techniques, you can effectively either prevent or mitigate buffer overflows in your systems.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Buffer overflows and corruption are major challenges in embedded systems, leading to crashes, unpredictable behavior, and security risks. Debugging these issues is difficult because their symptoms vary based on system state, requiring systematic analysis using memory map files, GDB/LLDB, and structured debugging approaches.</p>
<p>In this article, we explored:</p>
<ul>
<li><p>The causes and consequences of buffer overflows and corruptions</p>
</li>
<li><p>How to debug buffer overflows using memory analysis and debugging tools</p>
</li>
<li><p>Best practices for prevention</p>
</li>
</ul>
<p>Buffer overflow prevention requires a multi-layered approach:</p>
<ol>
<li><p>Follow a structured software design process to identify risks early.</p>
</li>
<li><p>Apply defensive programming principles to validate inputs and handle errors gracefully.</p>
</li>
<li><p>Use hardware-based protections like MPUs where available.</p>
</li>
<li><p>Enable compiler flags that help identify memory errors.</p>
</li>
<li><p>Test extensively, unit testing, automated testing, and code reviews help catch vulnerabilities early.</p>
</li>
</ol>
<p>By implementing these best practices, you can minimize the risk of buffer overflows in embedded systems, improving reliability and security.</p>
<p>In embedded systems, where reliability and safety are critical, preventing buffer overflows is not just a best practice, it is a necessity. A single buffer overflow can compromise an entire system. Defensive programming, rigorous testing, and hardware protections are essential for building secure and robust embedded applications.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
