<?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[ sensors - 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[ sensors - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sat, 27 Jun 2026 14:18:54 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/sensors/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How Bluetooth Low Energy Devices Work: GATT Services and Characteristics Explained ]]>
                </title>
                <description>
                    <![CDATA[ Every time you check your smartwatch for heart rate, read the battery level of wireless earbuds, unlock a Bluetooth smart lock, or watch sensor data stream into an app, you are experiencing the result of GATT working quietly in the background. GATT i... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-bluetooth-low-energy-devices-work-gatt-services-and-characteristics-explained/</link>
                <guid isPermaLink="false">69307a1e2b79515d02383320</guid>
                
                    <category>
                        <![CDATA[ Bluetooth GATT ]]>
                    </category>
                
                    <category>
                        <![CDATA[ bluetooth ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Bluetooth Low Energy ]]>
                    </category>
                
                    <category>
                        <![CDATA[ sensors ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nikheel Vishwas Savant ]]>
                </dc:creator>
                <pubDate>Wed, 03 Dec 2025 17:57:50 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1764781967963/2ccd66f7-3a5f-490f-af66-e1091ef4e34d.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Every time you check your smartwatch for heart rate, read the battery level of wireless earbuds, unlock a Bluetooth smart lock, or watch sensor data stream into an app, you are experiencing the result of GATT working quietly in the background.</p>
<p>GATT is the Generic Attribute Profile, and it provides the structure that makes Bluetooth Low Energy (BLE) devices exchange meaningful information. Without GATT, Bluetooth radios would simply move bits back and forth with no agreed format or interpretation. With GATT, devices can communicate in a predictable and understandable language.</p>
<p>Think of Bluetooth radios as two people speaking to each other in a room. The radio waves allow them to talk, but without a common language, the exchange is useless. GATT provides that common language. It defines the vocabulary, grammar, and sentence structure. Instead of random binary, we get clear messages like Heart Rate equals 78 bpm, Battery equals 92 percent, or Light Switch equals ON.</p>
<p>Because of GATT, devices from different manufacturers are able to interoperate. A Polar heart rate strap can connect to a Peloton bike. A Samsung phone can read temperature from a medical sensor. An Apple Watch can control Philips Hue smart lights. These devices do not share hardware, companies, or operating systems, yet they can cooperate because GATT defines a universal structure for exposing and accessing data.</p>
<p>Once you understand GATT, Bluetooth becomes far less mysterious. Communication becomes a matter of reading or writing values in a small structured database. Debugging becomes logical. BLE app development becomes straightforward. And building your own IoT device becomes achievable, even for beginners.</p>
<p>In this article, we’ll walk through GATT in depth. You’ll learn how devices organize data into services and characteristics, how phones discover and read values, how notifications deliver real time updates, and how embedded and Android code interact with GATT. By the end, you’ll be able to design a GATT database, understand BLE logs, and confidently build BLE applications.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you continue, you should have a basic understanding of:</p>
<ul>
<li><p>What Bluetooth is at a high level (no deep protocol knowledge needed)</p>
</li>
<li><p>How mobile apps connect to external devices (Android, iOS, or embedded)</p>
</li>
<li><p>Very basic programming concepts (variables, functions, objects)</p>
</li>
</ul>
<p>You’ll also need:</p>
<ul>
<li><p>A smartphone or laptop with Bluetooth Low Energy support</p>
</li>
<li><p>A BLE-compatible development board or device (optional, but helpful if you want to try the code examples)</p>
</li>
<li><p>A BLE debugging/scanning app such as nRF Connect, LightBlue, or BLE Scanner</p>
</li>
</ul>
<p>If you’re completely new to BLE, don’t worry – this article walks through each concept step-by-step.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-gatt">What is GATT</a>?</p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-are-services-and-why-do-they-matter">What Are Services and Why Do They Matter?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-are-characteristics-and-how-they-work">What are Characteristics and How Do They Work</a>?</p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-design-a-gatt-profile-for-a-smart-plant-monitor">How to Design a GATT Profile for a Smart Plant Monitor</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-gatt">What is GATT?</h2>
<p>GATT stands for Generic Attribute Profile. It is the structured communication model used by Bluetooth Low Energy devices to exchange data in a clear and organized format.</p>
<p>GATT defines how data is stored, formatted, accessed, updated, and transmitted across BLE connections. Without GATT, Bluetooth devices would only exchange unstructured binary information that has no consistent meaning. With GATT, devices can share values such as battery percentage, heart rate, temperature readings, and status commands in a well-defined way.</p>
<h3 id="heading-gatt-client-and-server-roles">GATT Client and Server Roles</h3>
<p>All communication in BLE occurs between two roles. The GATT Server owns and exposes the data. The GATT Client requests, reads, writes, or subscribes to that data. The Server holds a database of values that the Client interacts with. A smartwatch usually acts as a GATT Server because it holds sensor values. A smartphone usually acts as a GATT Client because it retrieves that information.</p>
<p>These roles can switch depending on the task. For example, during a firmware update, the phone acts as the GATT Server providing firmware blocks and the wearable acts as the GATT Client requesting them.</p>
<h3 id="heading-services-characteristics-and-uuids">Services, Characteristics, and UUIDs</h3>
<p>The GATT Server stores its data in a structured database made up of Services and Characteristics. A Service is a container that groups related information. A Characteristic is a single data value inside a service.</p>
<p>For example, the Battery Service contains the Battery Level characteristic. The Heart Rate Service contains the Heart Rate Measurement characteristic.</p>
<p>All services and characteristics are identified using UUID values so that every device knows how to locate them. Standard Bluetooth SIG defined services such as Heart Rate and Battery use 16 bit UUIDs. Custom proprietary features use 128 bit UUIDs.</p>
<h3 id="heading-example-gatt-database-layout">Example GATT Database Layout</h3>
<p>Here is a conceptual breakdown of a simple GATT database layout:</p>
<pre><code class="lang-typescript">Service: Battery Service (UUID <span class="hljs-number">0x180F</span>)
    Characteristic: Battery Level (UUID <span class="hljs-number">0x2A19</span>)
    Example value: <span class="hljs-number">92</span> percent
</code></pre>
<h3 id="heading-example-reading-a-gatt-characteristic-from-android">Example: Reading a GATT Characteristic from Android</h3>
<p>When a phone connects to a BLE device and acts as the client, it performs a sequence of steps. It connects to the device, discovers services, finds the characteristic of interest, and reads or subscribes to its value.</p>
<p>The following complete Java example shows an Android app acting as a <strong>GATT Client</strong>, discovering services and reading the battery level.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> BleGattClientManager {

    <span class="hljs-keyword">private</span> BluetoothGatt bluetoothGatt;

    <span class="hljs-keyword">private</span> final BluetoothGattCallback gattCallback = <span class="hljs-keyword">new</span> BluetoothGattCallback() {

        <span class="hljs-meta">@Override</span>
        <span class="hljs-keyword">public</span> <span class="hljs-built_in">void</span> onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            <span class="hljs-keyword">if</span> (newState == BluetoothProfile.STATE_CONNECTED) {
                Log.d(TAG, <span class="hljs-string">"Connected to device. Discovering services."</span>);
                gatt.discoverServices();
            }
        }

        <span class="hljs-meta">@Override</span>
        <span class="hljs-keyword">public</span> <span class="hljs-built_in">void</span> onServicesDiscovered(BluetoothGatt gatt, int status) {
            UUID BATTERY_SERVICE_UUID =
                    UUID.fromString(<span class="hljs-string">"0000180F-0000-1000-8000-00805F9B34FB"</span>);
            UUID BATTERY_LEVEL_UUID =
                    UUID.fromString(<span class="hljs-string">"00002A19-0000-1000-8000-00805F9B34FB"</span>);

            BluetoothGattService service = gatt.getService(BATTERY_SERVICE_UUID);
            <span class="hljs-keyword">if</span> (service != <span class="hljs-literal">null</span>) {
                BluetoothGattCharacteristic characteristic =
                        service.getCharacteristic(BATTERY_LEVEL_UUID);
                <span class="hljs-keyword">if</span> (characteristic != <span class="hljs-literal">null</span>) {
                    gatt.readCharacteristic(characteristic);
                }
            }
        }

        <span class="hljs-meta">@Override</span>
        <span class="hljs-keyword">public</span> <span class="hljs-built_in">void</span> onCharacteristicRead(BluetoothGatt gatt,
                                         BluetoothGattCharacteristic characteristic,
                                         int status) {
            <span class="hljs-keyword">if</span> (status == BluetoothGatt.GATT_SUCCESS) {
                int batteryValue = characteristic.getIntValue(
                        BluetoothGattCharacteristic.FORMAT_UINT8, <span class="hljs-number">0</span>);
                Log.d(TAG, <span class="hljs-string">"Battery Level: "</span> + batteryValue + <span class="hljs-string">" percent"</span>);
            }
        }
    };

    <span class="hljs-keyword">public</span> <span class="hljs-built_in">void</span> connect(Context context, BluetoothDevice device) {
        bluetoothGatt = device.connectGatt(context, <span class="hljs-literal">false</span>, gattCallback);
    }
}
</code></pre>
<p>This Java class represents a Bluetooth Low Energy GATT client that connects to a BLE device and reads the battery level characteristic. The class holds a <code>BluetoothGatt</code> object that represents the active BLE connection. The <code>BluetoothGattCallback</code> handles events during the connection lifecycle.</p>
<p>When the device connection state changes and the new state indicates that the device is connected, the callback triggers service discovery by calling <code>gatt.discoverServices()</code>.</p>
<p>After the services are discovered, the callback receives <code>onServicesDiscovered</code>, where two standard UUIDs are defined: the Battery Service with UUID <code>0000180F-0000-1000-8000-00805F9B34FB</code> and the Battery Level characteristic with UUID <code>00002A19-0000-1000-8000-00805F9B34FB</code>. The client retrieves the Battery Service using <code>gatt.getService</code>, then retrieves the Battery Level characteristic using <code>getCharacteristic</code>.</p>
<p>If both objects are found, the client calls <code>gatt.readCharacteristic</code>, which sends a read request to the server. When the server responds, <code>onCharacteristicRead</code> is invoked. If the response is successful, the characteristic value is extracted using <code>getIntValue</code> as an unsigned 8 bit integer at offset zero, producing a percentage from zero to one hundred. This value is printed to the log.</p>
<p>The <code>connect</code> method initiates the connection by calling <code>device.connectGatt</code>, which begins the communication and links all callbacks.</p>
<p>In summary, the flow is simple: connect to the device, discover services, locate the Battery Service, read the Battery Level characteristic, and print the result. This code shows the core pattern of how a BLE client interacts with a GATT server to request information.</p>
<h3 id="heading-android-as-a-gatt-server">Android as a GATT Server</h3>
<p>The Android device can also act as a <strong>GATT Server</strong>. This is useful when the phone needs to expose its own characteristics that other BLE devices read or write, such as setup information, commands, or configuration data.</p>
<p>Below is a complete example of a custom GATT Server written in Java. It exposes a custom service and a custom characteristic that allows a BLE client to read and write values.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> BleGattServerManager {

    <span class="hljs-keyword">private</span> BluetoothGattServer gattServer;

    <span class="hljs-keyword">private</span> final UUID SERVICE_UUID =
            UUID.fromString(<span class="hljs-string">"12345678-1234-5678-1234-56789abcdef0"</span>);

    <span class="hljs-keyword">private</span> final UUID CHARACTERISTIC_UUID =
            UUID.fromString(<span class="hljs-string">"abcdef01-1234-5678-1234-56789abcdef0"</span>);

    <span class="hljs-keyword">private</span> final BluetoothGattServerCallback serverCallback =
            <span class="hljs-keyword">new</span> BluetoothGattServerCallback() {

        <span class="hljs-meta">@Override</span>
        <span class="hljs-keyword">public</span> <span class="hljs-built_in">void</span> onConnectionStateChange(BluetoothDevice device,
                                            int status,
                                            int newState) {
            Log.d(TAG, <span class="hljs-string">"Device connected: "</span> + device.getAddress());
        }

        <span class="hljs-meta">@Override</span>
        <span class="hljs-keyword">public</span> <span class="hljs-built_in">void</span> onCharacteristicReadRequest(BluetoothDevice device,
                                                int requestId,
                                                int offset,
                                                BluetoothGattCharacteristic characteristic) {

            byte[] value = <span class="hljs-string">"HELLO_ANDROID_SERVER"</span>.getBytes(StandardCharsets.UTF_8);
            gattServer.sendResponse(device, requestId,
                    BluetoothGatt.GATT_SUCCESS, offset, value);
        }

        <span class="hljs-meta">@Override</span>
        <span class="hljs-keyword">public</span> <span class="hljs-built_in">void</span> onCharacteristicWriteRequest(BluetoothDevice device,
                                                 int requestId,
                                                 BluetoothGattCharacteristic characteristic,
                                                 <span class="hljs-built_in">boolean</span> preparedWrite,
                                                 <span class="hljs-built_in">boolean</span> responseNeeded,
                                                 int offset,
                                                 byte[] value) {

            <span class="hljs-built_in">String</span> received = <span class="hljs-keyword">new</span> <span class="hljs-built_in">String</span>(value, StandardCharsets.UTF_8);
            Log.d(TAG, <span class="hljs-string">"Client wrote value: "</span> + received);

            gattServer.sendResponse(device, requestId,
                    BluetoothGatt.GATT_SUCCESS, offset, value);
        }
    };

    <span class="hljs-keyword">public</span> <span class="hljs-built_in">void</span> startServer(Context context) {
        BluetoothManager bluetoothManager =
                (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);

        gattServer = bluetoothManager.openGattServer(context, serverCallback);

        BluetoothGattService customService =
                <span class="hljs-keyword">new</span> BluetoothGattService(SERVICE_UUID,
                        BluetoothGattService.SERVICE_TYPE_PRIMARY);

        BluetoothGattCharacteristic customCharacteristic =
                <span class="hljs-keyword">new</span> BluetoothGattCharacteristic(
                        CHARACTERISTIC_UUID,
                        BluetoothGattCharacteristic.PROPERTY_READ |
                        BluetoothGattCharacteristic.PROPERTY_WRITE,
                        BluetoothGattCharacteristic.PERMISSION_READ |
                        BluetoothGattCharacteristic.PERMISSION_WRITE
                );

        customService.addCharacteristic(customCharacteristic);
        gattServer.addService(customService);
    }
}
</code></pre>
<p>This Java class implements a Bluetooth Low Energy GATT Server on Android, meaning it exposes a service and characteristic that another BLE device can read or write.</p>
<p>The class holds a <code>BluetoothGattServer</code> instance which is created when the server starts. Two UUIDs are defined, one for the custom service and one for the custom characteristic. The <code>BluetoothGattServerCallback</code> handles incoming events from any remote BLE client that connects to this server.</p>
<p>When a device connects, <code>onConnectionStateChange</code> logs the connection. When a client sends a read request on the characteristic, <code>onCharacteristicReadRequest</code> responds by sending back a static string value, in this case the bytes of the text <code>HELLO_ANDROID_SERVER</code>, using <code>sendResponse</code> with a success status.</p>
<p>When the client writes data to the characteristic, <code>onCharacteristicWriteRequest</code> converts the incoming byte array to a string, logs what was written, and returns a success response to acknowledge that the server accepted the new value.</p>
<p>The <code>startServer</code> method initializes the GATT Server by requesting the <code>BluetoothManager</code>, opening the server, creating a custom primary service, and adding a characteristic to that service with both read and write properties and permissions. The service is then registered on the server through <code>addService</code>, which makes it available to any BLE client that connects.</p>
<p>In summary, this code demonstrates how an Android device can behave like a BLE peripheral and expose a custom readable and writable characteristic that other devices can interact with. This forms the foundation for features like configuration setup, provisioning, remote control commands, or device to device communication.</p>
<p>This example shows that GATT is simply a structured database of readable and writable values that represent meaningful application behavior. Whether the task is battery level reporting, real time health monitoring, remote control of smart devices, or secure provisioning, the exchange always follows this same pattern.</p>
<p>Understanding GATT at this level is the foundation for all Bluetooth Low Energy engineering and problem solving.</p>
<h2 id="heading-what-are-services-and-why-do-they-matter">What Are Services and Why Do They Matter?</h2>
<h3 id="heading-services-as-capability-containers">Services as Capability Containers</h3>
<p>A Service in GATT is a logical container that groups related data. A single Bluetooth device can expose many services, and each service focuses on one capability or functional category.</p>
<p>For example, a smartwatch may expose a Heart Rate Service, a Battery Service, a Current Time Service, and a Device Information Service. A smart bulb may expose a Lighting Control Service with characteristics to change brightness and color. A medical thermometer may expose a Health Thermometer Service that continuously streams temperature values.</p>
<p>Services exist to separate different categories of information so that any client device can immediately understand what functions are available and how to interact with them.</p>
<p>A service does not hold raw values itself. Instead, it organizes characteristics, which contain the actual data elements. The service only defines the grouping and the type of behavior associated with that group. This design makes BLE communication extremely scalable. Applications only need to know which service to target, then they can discover and manipulate the characteristics inside it.</p>
<h3 id="heading-standard-vs-custom-services">Standard vs Custom Services</h3>
<p>Bluetooth SIG defines many standard services for interoperability. For example, the Battery Service exposes battery level in a characteristic called Battery Level. The Heart Rate Service exposes heart rate values so that any fitness application can subscribe to it. These standard services allow devices from different manufacturers to work together without custom integration.</p>
<p>Anyone building a custom device can also define their own original service using a 128 bit UUID. The structure is the same whether it is standard or custom.</p>
<h3 id="heading-example-a-device-with-multiple-services">Example: A Device with Multiple Services</h3>
<p>Below is an example representation of a device that exposes two services at the same time:</p>
<pre><code class="lang-java">Service: <span class="hljs-function">Battery <span class="hljs-title">Service</span> <span class="hljs-params">(UUID <span class="hljs-number">0x180F</span>)</span>
    Characteristic: Battery <span class="hljs-title">Level</span> <span class="hljs-params">(UUID <span class="hljs-number">0x2A19</span>)</span>
    Current value: 92 percent

Service: Heart Rate <span class="hljs-title">Service</span> <span class="hljs-params">(UUID <span class="hljs-number">0x180D</span>)</span>
    Characteristic: Heart Rate <span class="hljs-title">Measurement</span> <span class="hljs-params">(UUID <span class="hljs-number">0x2A37</span>)</span>
    Current value: 78 bpm</span>
</code></pre>
<p>An Android phone acting as a client can discover these services and then interact with characteristics inside them. Discovery is always the first step after establishing a BLE connection.</p>
<h3 id="heading-discovering-services-in-android">Discovering Services in Android</h3>
<p>The following example shows how to list all available services and log them in Java.</p>
<pre><code class="lang-java"><span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onServicesDiscovered</span><span class="hljs-params">(BluetoothGatt gatt, <span class="hljs-keyword">int</span> status)</span> </span>{
    <span class="hljs-keyword">for</span> (BluetoothGattService service : gatt.getServices()) {
        Log.d(TAG, <span class="hljs-string">"Service discovered: "</span> + service.getUuid().toString());
    }
}
</code></pre>
<p>This method is called automatically after the BLE client has finished discovering all services exposed by the remote GATT server. When a connection is established, the client initiates a service discovery procedure, and once the server responds with the complete list of available services, the Android stack triggers <code>onServicesDiscovered</code>.</p>
<p>Inside this callback, the code iterates through every <code>BluetoothGattService</code> returned by <code>gatt.getServices()</code>, which represents all services implemented by the connected device. For each service in that list, the code prints its UUID to the log. This output helps developers inspect what services exist on the device, confirm that expected services such as Heart Rate, Battery, or a custom service are present, and identify the correct UUIDs needed for reading or writing characteristics later.</p>
<p>This method is especially useful during development or debugging, because it allows you to verify that a device correctly exposes its GATT database and that the client can access the list of services before attempting to interact with any characteristics.</p>
<h3 id="heading-notifications-for-continuously-changing-data">Notifications for Continuously Changing Data</h3>
<p>Once the service is found, the next step is to read or subscribe to its characteristics. Some characteristics contain static or rarely changing values, which makes direct reads appropriate. Others, such as heart rate or temperature, change continuously, and should use notifications.</p>
<h4 id="heading-why-notifications-matter-in-services">Why Notifications Matter in Services</h4>
<p>Notifications allow a device to receive updates automatically whenever the value changes instead of repeatedly reading the characteristic. This reduces energy usage and latency, which is essential for wearables and sensors.</p>
<p>Below is a Java example showing how to enable notifications for the Heart Rate Measurement characteristic:</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">enableHeartRateNotifications</span><span class="hljs-params">(BluetoothGatt gatt)</span> </span>{

    UUID HEART_RATE_SERVICE_UUID =
            UUID.fromString(<span class="hljs-string">"0000180D-0000-1000-8000-00805F9B34FB"</span>);
    UUID HEART_RATE_MEASUREMENT_UUID =
            UUID.fromString(<span class="hljs-string">"00002A37-0000-1000-8000-00805F9B34FB"</span>);

    BluetoothGattService service = gatt.getService(HEART_RATE_SERVICE_UUID);
    <span class="hljs-keyword">if</span> (service != <span class="hljs-keyword">null</span>) {
        BluetoothGattCharacteristic characteristic =
                service.getCharacteristic(HEART_RATE_MEASUREMENT_UUID);

        <span class="hljs-keyword">if</span> (characteristic != <span class="hljs-keyword">null</span>) {
            gatt.setCharacteristicNotification(characteristic, <span class="hljs-keyword">true</span>);

            BluetoothGattDescriptor descriptor =
                    characteristic.getDescriptor(
                            UUID.fromString(<span class="hljs-string">"00002902-0000-1000-8000-00805F9B34FB"</span>)
                    );

            <span class="hljs-keyword">if</span> (descriptor != <span class="hljs-keyword">null</span>) {
                descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                gatt.writeDescriptor(descriptor);
            }
        }
    }
}
</code></pre>
<h4 id="heading-enabling-notifications-in-android">Enabling Notifications in Android</h4>
<p>This method enables notifications for the Heart Rate Measurement characteristic so that the device can push new values automatically whenever the heart rate changes.</p>
<p>It begins by defining the UUIDs for the Heart Rate Service and the Heart Rate Measurement characteristic. Using <code>gatt.getService</code>, it retrieves the Heart Rate Service from the connected device. If that service exists, it locates the Heart Rate Measurement characteristic within it. Once the characteristic is found, the method enables local notification handling on the Android side using <code>setCharacteristicNotification</code>, which prepares the client to receive asynchronous updates.</p>
<p>But enabling local notifications is not enough. The BLE specification requires writing to a special descriptor called the Client Characteristic Configuration Descriptor, identified by UUID <code>00002902-0000-1000-8000-00805F9B34FB</code>, so that the remote device also knows the client wants updates. The method retrieves this descriptor, sets its value to <code>ENABLE_NOTIFICATION_VALUE</code>, and writes it using <code>writeDescriptor</code>, which sends a request over the air to the server telling it to start sending notifications.</p>
<p>After this sequence completes, updates begin arriving automatically in the <code>onCharacteristicChanged</code> callback whenever the heart rate changes, without needing repeated read requests.</p>
<p>This is the preferred BLE pattern for continuous sensor data such as heart rate, temperature, step count, or motion values because it saves power and provides real time responsiveness.</p>
<p>When the device begins sending notifications, updates are received in the callback below. The values arrive whenever they change, making this very efficient for streaming.</p>
<pre><code class="lang-java"><span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCharacteristicChanged</span><span class="hljs-params">(BluetoothGatt gatt,
                                    BluetoothGattCharacteristic characteristic)</span> </span>{

    UUID HEART_RATE_MEASUREMENT_UUID =
            UUID.fromString(<span class="hljs-string">"00002A37-0000-1000-8000-00805F9B34FB"</span>);

    <span class="hljs-keyword">if</span> (characteristic.getUuid().equals(HEART_RATE_MEASUREMENT_UUID)) {
        <span class="hljs-keyword">int</span> flag = characteristic.getProperties();
        <span class="hljs-keyword">int</span> format;
        <span class="hljs-keyword">if</span> ((flag &amp; <span class="hljs-number">0x01</span>) != <span class="hljs-number">0</span>) {
            format = BluetoothGattCharacteristic.FORMAT_UINT16;
        } <span class="hljs-keyword">else</span> {
            format = BluetoothGattCharacteristic.FORMAT_UINT8;
        }
        <span class="hljs-keyword">int</span> heartRate = characteristic.getIntValue(format, <span class="hljs-number">1</span>);
        Log.d(TAG, <span class="hljs-string">"Heart Rate: "</span> + heartRate + <span class="hljs-string">" bpm"</span>);
    }
}
</code></pre>
<p>This method is called automatically whenever the Bluetooth device sends a notification indicating that the value of a characteristic has changed.</p>
<p>In this example, the method handles updates to the Heart Rate Measurement characteristic. The code first checks whether the characteristic that triggered the callback matches the Heart Rate Measurement UUID <code>00002A37-0000-1000-8000-00805F9B34FB</code>, ensuring that only relevant updates are processed.</p>
<p>The heart rate data can be encoded in either one or two bytes, depending on the flags defined in the characteristic properties. The method reads these flags and determines the correct format to use when decoding the heart rate value. If the least significant bit is set, the heart rate uses a 16 bit format. Otherwise, it uses a single 8 bit format.</p>
<p>After selecting the appropriate format, the method extracts the heart rate value using <code>getIntValue</code>, beginning at offset 1 because byte 0 contains the flags.</p>
<p>Finally, the value is printed to the log as beats per minute. This method is typically called repeatedly, such as once per second, so the log receives live heart rate updates in real time.</p>
<p>This approach demonstrates how notifications deliver continuous sensor data without repeatedly polling the device, which reduces latency and power usage for both the client and server.</p>
<p>This example demonstrates how the client retrieves real time sensor data using subscription instead of polling. The same mechanism is used for air quality sensors, smart home lighting brightness notifications, industrial temperature monitors, and more.</p>
<p>To summarize this section, a Service is a structured container that organizes related data and is essential for exposing abilities and functionality over BLE. Understanding how to discover and interact with services is the first major step toward building or debugging real Bluetooth applications.</p>
<h2 id="heading-what-are-characteristics-and-how-do-they-work">What Are Characteristics and How Do They Work?</h2>
<h3 id="heading-the-role-of-a-characteristic">The Role of a Characteristic</h3>
<p>If a Service is a folder, then a Characteristic is a file inside that folder that actually holds the content. In GATT, the Characteristic is where the real data lives. Almost everything your application cares about will eventually be read from, written to, or subscribed to on a characteristic.</p>
<h3 id="heading-the-four-parts-of-a-characteristic">The Four Parts of a Characteristic</h3>
<p>A characteristic is more than just a single number. It has four important parts. First, it has a UUID that identifies what it represents. It also has a value that stores the actual bytes. Then it has properties that describe what operations are allowed, such as read, write, or notify. And finally, it has permissions that control who can access it and under what security level. Understanding these pieces is the key to working confidently with BLE.</p>
<p>The UUID tells you what kind of data is inside the characteristic. For example, a standard Battery Level characteristic uses the UUID 0x2A19 and always contains a single byte that represents a percentage from zero to one hundred. A Heart Rate Measurement characteristic uses UUID 0x2A37 and packs heart rate and flags into a structured format. Custom characteristics use 128 bit UUIDs that developers define themselves.</p>
<p>The value is simply a sequence of bytes. On the wire, Bluetooth does not know about integers, floats, or strings. It only sees bytes. On the Android side, the <code>BluetoothGattCharacteristic</code> class helps interpret those bytes as different types. It provides helper methods such as <code>getIntValue</code>, <code>getFloatValue</code>, and <code>getStringValue</code> so that you can decode the data more easily.</p>
<p>The properties of a characteristic describe what kind of operations the client can perform. The most common properties are Read, Write, Notify, and Indicate.</p>
<p>Read means a client can ask the server to return the current value. Write means a client can send a new value to the server. Notify means the server can send updates to the client whenever the value changes. Indicate is similar to Notify, but with an extra confirmation. A characteristic may have one or many properties combined.</p>
<p>Permissions are related but slightly different. They focus on access control and security. For example, a characteristic may require encryption or authenticated pairing before it can be read or written. The Android <code>BluetoothGattCharacteristic</code> object contains these permission flags so that the stack enforces them correctly.</p>
<h3 id="heading-example-defining-a-custom-led-characteristic">Example: Defining a Custom LED Characteristic</h3>
<p>Let’s walk through a concrete example. Imagine a custom device that exposes a characteristic to control an LED state. The LED should be either ON or OFF. The characteristic needs to support both read and write, because the client may want to read the current state and also change it.</p>
<p>On the Android GATT Server side, you would define such a characteristic like this:</p>
<pre><code class="lang-java">UUID SERVICE_UUID =
        UUID.fromString(<span class="hljs-string">"12345678-1234-5678-1234-56789abcdef0"</span>);
UUID LED_CHAR_UUID =
        UUID.fromString(<span class="hljs-string">"abcdef01-1234-5678-1234-56789abcdef0"</span>);

BluetoothGattService ledService =
        <span class="hljs-keyword">new</span> BluetoothGattService(SERVICE_UUID,
                BluetoothGattService.SERVICE_TYPE_PRIMARY);

BluetoothGattCharacteristic ledCharacteristic =
        <span class="hljs-keyword">new</span> BluetoothGattCharacteristic(
                LED_CHAR_UUID,
                BluetoothGattCharacteristic.PROPERTY_READ |
                BluetoothGattCharacteristic.PROPERTY_WRITE,
                BluetoothGattCharacteristic.PERMISSION_READ |
                BluetoothGattCharacteristic.PERMISSION_WRITE
        );

<span class="hljs-comment">// Initial value</span>
ledCharacteristic.setValue(<span class="hljs-string">"OFF"</span>.getBytes(StandardCharsets.UTF_8));
ledService.addCharacteristic(ledCharacteristic);
gattServer.addService(ledService);
</code></pre>
<p>This code defines a custom GATT Service and a custom GATT Characteristic on an Android device acting as a Bluetooth Low Energy GATT Server.</p>
<p>Two UUIDs are created using <code>UUID.fromString</code>, one representing the custom service and the other representing the characteristic that belongs to that service. A new <code>BluetoothGattService</code> instance is then created, marked as a primary service to indicate that it is a main functional component rather than a secondary helper service.</p>
<p>Inside that service, a <code>BluetoothGattCharacteristic</code> object is created using the second UUID, and it’s configured to allow both reads and writes by a remote BLE client. The property flags indicate that a client can request the current value and can also send updates, and the permission flags define that both operations are permitted.</p>
<p>The characteristic is given an initial value of the string <code>"OFF"</code> encoded as bytes, which might represent the current state of a remote controlled LED, device mode, or some other configuration setting.</p>
<p>The characteristic is then added to the service, and finally the fully defined service is added to the GATT server using <code>gattServer.addService</code>, making it visible to any BLE client that connects.</p>
<p>At this point, another device can read the value <code>"OFF"</code> or write a new value such as <code>"ON"</code>, which the server could then use to trigger real behavior, such as toggling actual hardware.</p>
<h3 id="heading-handling-reads-and-writes-on-the-server">Handling Reads and Writes on the Server</h3>
<h4 id="heading-server-side-handlers">Server-Side Handlers</h4>
<p>On the GATT Server side, you must also respond to read and write requests. This happens inside <code>BluetoothGattServerCallback</code>.</p>
<pre><code class="lang-java"><span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> BluetoothGattServerCallback serverCallback =
        <span class="hljs-keyword">new</span> BluetoothGattServerCallback() {

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCharacteristicReadRequest</span><span class="hljs-params">(BluetoothDevice device,
                                            <span class="hljs-keyword">int</span> requestId,
                                            <span class="hljs-keyword">int</span> offset,
                                            BluetoothGattCharacteristic characteristic)</span> </span>{

        <span class="hljs-keyword">byte</span>[] currentValue = characteristic.getValue();
        gattServer.sendResponse(device,
                requestId,
                BluetoothGatt.GATT_SUCCESS,
                offset,
                currentValue);
    }

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCharacteristicWriteRequest</span><span class="hljs-params">(BluetoothDevice device,
                                             <span class="hljs-keyword">int</span> requestId,
                                             BluetoothGattCharacteristic characteristic,
                                             <span class="hljs-keyword">boolean</span> preparedWrite,
                                             <span class="hljs-keyword">boolean</span> responseNeeded,
                                             <span class="hljs-keyword">int</span> offset,
                                             <span class="hljs-keyword">byte</span>[] value)</span> </span>{

        String received = <span class="hljs-keyword">new</span> String(value, StandardCharsets.UTF_8);
        Log.d(TAG, <span class="hljs-string">"LED characteristic write: "</span> + received);

        characteristic.setValue(value);

        <span class="hljs-keyword">if</span> (<span class="hljs-string">"ON"</span>.equalsIgnoreCase(received)) {
            turnLedOn();
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (<span class="hljs-string">"OFF"</span>.equalsIgnoreCase(received)) {
            turnLedOff();
        } <span class="hljs-keyword">else</span> {
            Log.e(TAG, <span class="hljs-string">"Unhandled case!"</span>);
            <span class="hljs-keyword">return</span>;
        }

        <span class="hljs-keyword">if</span> (responseNeeded) {
            gattServer.sendResponse(device,
                    requestId,
                    BluetoothGatt.GATT_SUCCESS,
                    offset,
                    value);
        }
    }
};
</code></pre>
<p>This callback handles read and write requests coming from a remote BLE client interacting with the custom LED characteristic on the GATT Server.</p>
<p>When a client performs a read operation, <code>onCharacteristicReadRequest</code> is triggered. The method retrieves the current stored value from the characteristic using <code>getValue()</code> and returns it to the client by calling <code>sendResponse</code> with a success status. This means whatever value was last set, such as <code>"ON"</code> or <code>"OFF"</code>, is sent back to the requesting device.</p>
<p>When a client performs a write operation, <code>onCharacteristicWriteRequest</code> is called. The method converts the incoming byte array into a string so that the server can interpret the command. It logs the received text, sets the new value into the characteristic using <code>setValue</code>, and then checks whether the string equals <code>"ON"</code> or <code>"OFF"</code>. Depending on the value, it calls either <code>turnLedOn()</code> or <code>turnLedOff()</code>, which would typically control real hardware or trigger an action inside the application.</p>
<p>If the client requested a response, the server sends back a confirmation by calling <code>sendResponse</code> with <code>GATT_SUCCESS</code>, acknowledging that the write completed successfully. This callback demonstrates how interactive BLE control works: the server receives a command, updates internal state, performs a real action, and reports status back to the client.</p>
<p>Here, the server reads whatever value is stored in the characteristic upon a read request and sends it back to the client. When the client writes a new value, the server decodes the bytes as a string and updates internal state, including physical behavior like toggling the LED.</p>
<h3 id="heading-reading-and-writing-from-the-client">Reading and Writing from the Client</h3>
<h4 id="heading-client-side-handlers">Client-Side Handlers</h4>
<p>On the client side, a typical Android app needs to read and write to this same characteristic. The code for the client looks similar to what we saw in earlier sections, but now it uses the custom UUIDs.</p>
<h3 id="heading-reading-a-custom-characteristic-client">Reading a Custom Characteristic (Client)</h3>
<p>Reading the LED state from the client:</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">readLedState</span><span class="hljs-params">(BluetoothGatt gatt)</span> </span>{
    UUID SERVICE_UUID =
            UUID.fromString(<span class="hljs-string">"12345678-1234-5678-1234-56789abcdef0"</span>);
    UUID LED_CHAR_UUID =
            UUID.fromString(<span class="hljs-string">"abcdef01-1234-5678-1234-56789abcdef0"</span>);

    BluetoothGattService service = gatt.getService(SERVICE_UUID);
    <span class="hljs-keyword">if</span> (service != <span class="hljs-keyword">null</span>) {
        BluetoothGattCharacteristic ledChar = service.getCharacteristic(LED_CHAR_UUID);
        <span class="hljs-keyword">if</span> (ledChar != <span class="hljs-keyword">null</span>) {
            gatt.readCharacteristic(ledChar);
        }
    }
}

<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCharacteristicRead</span><span class="hljs-params">(BluetoothGatt gatt,
                                 BluetoothGattCharacteristic characteristic,
                                 <span class="hljs-keyword">int</span> status)</span> </span>{
    <span class="hljs-keyword">if</span> (status == BluetoothGatt.GATT_SUCCESS) {
        UUID LED_CHAR_UUID =
                UUID.fromString(<span class="hljs-string">"abcdef01-1234-5678-1234-56789abcdef0"</span>);
        <span class="hljs-keyword">if</span> (LED_CHAR_UUID.equals(characteristic.getUuid())) {
            String value = <span class="hljs-keyword">new</span> String(characteristic.getValue(), StandardCharsets.UTF_8);
            Log.d(TAG, <span class="hljs-string">"LED state is: "</span> + value);
        }
    }
}
</code></pre>
<p>This code shows how a Bluetooth Low Energy client reads the current state of a custom LED characteristic from a GATT server. The <code>readLedState</code> method begins by defining the UUIDs for the custom service and the LED characteristic so that the client knows exactly where to look inside the server’s GATT database.</p>
<p>It retrieves the service using <code>gatt.getService</code>, and if the service exists, it retrieves the LED characteristic using <code>getCharacteristic</code>. If that characteristic is found, the client calls <code>readCharacteristic</code>, which sends a read request to the remote device over BLE. Once the server responds, the callback method <code>onCharacteristicRead</code> is triggered.</p>
<p>This method first checks that the read was successful by confirming that the status equals <code>GATT_SUCCESS</code>. It then verifies that the characteristic being read is indeed the LED characteristic by comparing UUIDs. If it matches, the code converts the characteristic’s byte array into a string, which contains either <code>"ON"</code> or <code>"OFF"</code>, and logs the current state.</p>
<p>This flow demonstrates how a BLE client reads stored values from a peripheral device and responds when the server returns the data, forming the basis for real world interactions such as checking the status of a smart light, a switch, or any sensor value exposed through a custom characteristic.</p>
<h3 id="heading-writing-to-a-custom-characteristic-client">Writing to a Custom Characteristic (Client)</h3>
<p>Writing a new LED state from the client:</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">writeLedState</span><span class="hljs-params">(BluetoothGatt gatt, String newState)</span> </span>{
    UUID SERVICE_UUID =
            UUID.fromString(<span class="hljs-string">"12345678-1234-5678-1234-56789abcdef0"</span>);
    UUID LED_CHAR_UUID =
            UUID.fromString(<span class="hljs-string">"abcdef01-1234-5678-1234-56789abcdef0"</span>);

    BluetoothGattService service = gatt.getService(SERVICE_UUID);
    <span class="hljs-keyword">if</span> (service != <span class="hljs-keyword">null</span>) {
        BluetoothGattCharacteristic ledChar = service.getCharacteristic(LED_CHAR_UUID);
        <span class="hljs-keyword">if</span> (ledChar != <span class="hljs-keyword">null</span>) {
            ledChar.setValue(newState.getBytes(StandardCharsets.UTF_8));
            gatt.writeCharacteristic(ledChar);
        }
    }
}

<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCharacteristicWrite</span><span class="hljs-params">(BluetoothGatt gatt,
                                  BluetoothGattCharacteristic characteristic,
                                  <span class="hljs-keyword">int</span> status)</span> </span>{
    <span class="hljs-keyword">if</span> (status == BluetoothGatt.GATT_SUCCESS) {
        Log.d(TAG, <span class="hljs-string">"LED state write completed"</span>);
    }
}
</code></pre>
<p>This code demonstrates how a Bluetooth Low Energy client sends a command to update the LED state on a GATT server device.</p>
<p>The <code>writeLedState</code> method begins by defining the UUIDs for the custom service and LED characteristic, then retrieves the service from the connected GATT server. If the service is found, it accesses the LED characteristic inside it.</p>
<p>Once the characteristic is obtained, the new LED state, which will typically be the string <code>"ON"</code> or <code>"OFF"</code>, is converted into a byte array and placed into the characteristic with <code>setValue</code>. The method then calls <code>writeCharacteristic</code>, which sends a write request to the remote device to update the stored value.</p>
<p>When the server processes the write and returns a response, the callback method <code>onCharacteristicWrite</code> executes. If the status indicates success, the code logs that the write completed. At this point, the server code on the other side can take action based on the new state, such as turning a real LED on or off.</p>
<p>This flow illustrates how clients modify values on a BLE peripheral and how acknowledgment is handled once the operation finishes, forming a typical example of device control over GATT.</p>
<p>This combination of server definitions and client interactions shows how characteristics are the real workhorses of GATT. Every meaningful piece of data flows through them. Reads, writes, and notifications all operate at the characteristic level.</p>
<h3 id="heading-notifications-and-cccd">Notifications and CCCD</h3>
<p>Notifications are simply a special property on a characteristic. When enabled, the server can push new values to the client without the client asking every time. To support notifications, a characteristic needs the Notify property and usually a descriptor called the Client Characteristic Configuration Descriptor, often referred to as CCCD, with UUID 0x2902.</p>
<p>On the server side, you would update the value and call <code>notifyCharacteristicChanged</code>. On the client side, you set characteristic notification to true and write the descriptor with <code>ENABLE_NOTIFICATION_VALUE</code>. The code pattern is almost identical regardless of the type of data, which makes it easy to reuse once you understand it.</p>
<p>By this point, you can see that a characteristic is not just a static field. It is a complete unit of behavior. It defines what data exists, how it is represented, who can access it, and how it updates. Once you’re comfortable designing characteristics and manipulating them in Java, you’re very close to mastering practical BLE development.</p>
<h2 id="heading-how-to-design-a-gatt-profile-for-a-smart-plant-monitor">How to Design a GATT Profile for a Smart Plant Monitor</h2>
<p>To make GATT feel real, let’s design a complete profile for a simple but realistic device: a smart plant monitor.</p>
<p>Imagine a small BLE sensor that you stick into a flower pot. It measures soil moisture, reports its own battery level, and allows you to configure how often it sends updates. A phone app connects to it, reads the current moisture level, shows the battery percentage, and lets the user adjust the reporting interval.</p>
<p>We’ll design both sides in terms of GATT. First, we’ll decide which services and characteristics we need. Then, we’ll see how an Android device can act as a client. For teaching purposes, we’ll also show how Android could act as the server, although in a real product the plant monitor would normally be an embedded device.</p>
<h3 id="heading-designing-the-gatt-profile">Designing the GATT Profile</h3>
<p>We need three logical pieces of information:</p>
<ol>
<li><p>Soil moisture percentage – this is dynamic sensor data.</p>
</li>
<li><p>Battery level – this is standard, so we can reuse the Battery Service.</p>
</li>
<li><p>Reporting interval configuration – this is a setting that the client writes and the device uses.</p>
</li>
</ol>
<p>We can express this with one custom service plus the standard Battery Service.</p>
<p><strong>Profile plan:</strong></p>
<pre><code class="lang-java">Custom Service: Plant Monitor Service
    UUID: <span class="hljs-number">12345678</span>-<span class="hljs-number">1234</span>-<span class="hljs-number">5678</span>-<span class="hljs-number">1234</span>-<span class="hljs-number">56789</span>abc0001

    Characteristic: Soil Moisture
        UUID: <span class="hljs-number">12345678</span>-<span class="hljs-number">1234</span>-<span class="hljs-number">5678</span>-<span class="hljs-number">1234</span>-<span class="hljs-number">56789</span>abc0002
        Properties: Read, Notify
        Permissions: Read
        Format: uint8 (<span class="hljs-number">0</span> to <span class="hljs-number">100</span> percentage)

    Characteristic: Reporting Interval
        UUID: <span class="hljs-number">12345678</span>-<span class="hljs-number">1234</span>-<span class="hljs-number">5678</span>-<span class="hljs-number">1234</span>-<span class="hljs-number">56789</span>abc0003
        Properties: Read, Write
        Permissions: Read, Write
        Format: uint16 (seconds)

Standard Service: Battery Service
    UUID: <span class="hljs-number">00001</span>80F-<span class="hljs-number">0000</span>-<span class="hljs-number">1000</span>-<span class="hljs-number">8000</span>-<span class="hljs-number">00</span>805F9B34FB

    Characteristic: Battery Level
        UUID: <span class="hljs-number">00002</span>A19-<span class="hljs-number">0000</span>-<span class="hljs-number">1000</span>-<span class="hljs-number">8000</span>-<span class="hljs-number">00</span>805F9B34FB
        Properties: Read, Notify (optional)
        Permissions: Read
        Format: uint8 (<span class="hljs-number">0</span> to <span class="hljs-number">100</span> percentage)
</code></pre>
<p>Now we know exactly what exists inside the device. A client can connect, look for the Plant Monitor Service and Battery Service, and then interact with these three characteristics.</p>
<h3 id="heading-implementing-the-gatt-server">Implementing the GATT Server</h3>
<p>In a real hardware product, the plant monitor would be written in embedded C or C++. But for learning, we can simulate the server on Android itself. This’ll help you understand how the server side works.</p>
<p>First, we’ll create the services and characteristics.</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PlantMonitorGattServer</span> </span>{

    <span class="hljs-keyword">private</span> BluetoothGattServer gattServer;

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> UUID PLANT_SERVICE_UUID =
            UUID.fromString(<span class="hljs-string">"12345678-1234-5678-1234-56789abc0001"</span>);
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> UUID MOISTURE_CHAR_UUID =
            UUID.fromString(<span class="hljs-string">"12345678-1234-5678-1234-56789abc0002"</span>);
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> UUID INTERVAL_CHAR_UUID =
            UUID.fromString(<span class="hljs-string">"12345678-1234-5678-1234-56789abc0003"</span>);

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> UUID BATTERY_SERVICE_UUID =
            UUID.fromString(<span class="hljs-string">"0000180F-0000-1000-8000-00805F9B34FB"</span>);
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> UUID BATTERY_LEVEL_UUID =
            UUID.fromString(<span class="hljs-string">"00002A19-0000-1000-8000-00805F9B34FB"</span>);

    <span class="hljs-comment">// Simulated state</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> currentMoisture = <span class="hljs-number">55</span>;      <span class="hljs-comment">// percentage</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> reportingIntervalSec = <span class="hljs-number">60</span>; <span class="hljs-comment">// seconds</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> batteryLevel = <span class="hljs-number">87</span>;         <span class="hljs-comment">// percentage</span>

    <span class="hljs-keyword">private</span> BluetoothGattCharacteristic moistureCharacteristic;
    <span class="hljs-keyword">private</span> BluetoothGattCharacteristic intervalCharacteristic;
    <span class="hljs-keyword">private</span> BluetoothGattCharacteristic batteryLevelCharacteristic;
</code></pre>
<p>This class represents a Bluetooth Low Energy GATT Server that pretends to be a smart plant monitor device. It holds a <code>BluetoothGattServer</code> instance that will expose services and characteristics to any BLE client that connects.</p>
<p>Several UUIDs are defined to identify the custom Plant Monitor Service and its characteristics, as well as the standard Battery Service and Battery Level characteristic.</p>
<p>The custom Plant Monitor Service has two characteristics: one for soil moisture and one for the reporting interval. The Battery Service uses the standard UUIDs defined by the Bluetooth SIG so that any client can recognize and parse it.</p>
<p>The class also keeps some simulated internal state: <code>currentMoisture</code> starts at 55 percent, <code>reportingIntervalSec</code> is set to 60 seconds, and <code>batteryLevel</code> is set to 87 percent. These values act like sensor readings and configuration stored inside the device.</p>
<p>Finally, it declares three <code>BluetoothGattCharacteristic</code> fields that will later point to the actual moisture, interval, and battery level characteristics once they are created and added to their respective services.</p>
<p>These fields make it easy for the server to update values and send notifications later – for example, when moisture changes or when the battery level drops.</p>
<p>Next, we’ll set up the server and define the services and characteristics.</p>
<pre><code class="lang-java">    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">startServer</span><span class="hljs-params">(Context context)</span> </span>{
        BluetoothManager bluetoothManager =
                (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);

        gattServer = bluetoothManager.openGattServer(context, serverCallback);

        <span class="hljs-comment">// Plant Monitor Service</span>
        BluetoothGattService plantService =
                <span class="hljs-keyword">new</span> BluetoothGattService(
                        PLANT_SERVICE_UUID,
                        BluetoothGattService.SERVICE_TYPE_PRIMARY
                );

        moistureCharacteristic = <span class="hljs-keyword">new</span> BluetoothGattCharacteristic(
                MOISTURE_CHAR_UUID,
                BluetoothGattCharacteristic.PROPERTY_READ |
                BluetoothGattCharacteristic.PROPERTY_NOTIFY,
                BluetoothGattCharacteristic.PERMISSION_READ
        );

        intervalCharacteristic = <span class="hljs-keyword">new</span> BluetoothGattCharacteristic(
                INTERVAL_CHAR_UUID,
                BluetoothGattCharacteristic.PROPERTY_READ |
                BluetoothGattCharacteristic.PROPERTY_WRITE,
                BluetoothGattCharacteristic.PERMISSION_READ |
                BluetoothGattCharacteristic.PERMISSION_WRITE
        );

        <span class="hljs-comment">// Set initial values</span>
        moistureCharacteristic.setValue(<span class="hljs-keyword">new</span> <span class="hljs-keyword">byte</span>[]{(<span class="hljs-keyword">byte</span>) currentMoisture});
        intervalCharacteristic.setValue(intToTwoBytes(reportingIntervalSec));

        <span class="hljs-comment">// For notifications, add CCCD descriptor</span>
        BluetoothGattDescriptor moistureCccd = <span class="hljs-keyword">new</span> BluetoothGattDescriptor(
                UUID.fromString(<span class="hljs-string">"00002902-0000-1000-8000-00805F9B34FB"</span>),
                BluetoothGattDescriptor.PERMISSION_READ |
                BluetoothGattDescriptor.PERMISSION_WRITE
        );
        moistureCharacteristic.addDescriptor(moistureCccd);

        plantService.addCharacteristic(moistureCharacteristic);
        plantService.addCharacteristic(intervalCharacteristic);

        <span class="hljs-comment">// Battery Service</span>
        BluetoothGattService batteryService =
                <span class="hljs-keyword">new</span> BluetoothGattService(
                        BATTERY_SERVICE_UUID,
                        BluetoothGattService.SERVICE_TYPE_PRIMARY
                );

        batteryLevelCharacteristic = <span class="hljs-keyword">new</span> BluetoothGattCharacteristic(
                BATTERY_LEVEL_UUID,
                BluetoothGattCharacteristic.PROPERTY_READ |
                BluetoothGattCharacteristic.PROPERTY_NOTIFY,
                BluetoothGattCharacteristic.PERMISSION_READ
        );

        batteryLevelCharacteristic.setValue(<span class="hljs-keyword">new</span> <span class="hljs-keyword">byte</span>[]{(<span class="hljs-keyword">byte</span>) batteryLevel});

        BluetoothGattDescriptor batteryCccd = <span class="hljs-keyword">new</span> BluetoothGattDescriptor(
                UUID.fromString(<span class="hljs-string">"00002902-0000-1000-8000-00805F9B34FB"</span>),
                BluetoothGattDescriptor.PERMISSION_READ |
                BluetoothGattDescriptor.PERMISSION_WRITE
        );
        batteryLevelCharacteristic.addDescriptor(batteryCccd);

        batteryService.addCharacteristic(batteryLevelCharacteristic);

        gattServer.addService(plantService);
        gattServer.addService(batteryService);
    }

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">byte</span>[] intToTwoBytes(<span class="hljs-keyword">int</span> value) {
        <span class="hljs-keyword">byte</span>[] data = <span class="hljs-keyword">new</span> <span class="hljs-keyword">byte</span>[<span class="hljs-number">2</span>];
        data[<span class="hljs-number">0</span>] = (<span class="hljs-keyword">byte</span>) (value &amp; <span class="hljs-number">0xFF</span>);
        data[<span class="hljs-number">1</span>] = (<span class="hljs-keyword">byte</span>) ((value &gt;&gt; <span class="hljs-number">8</span>) &amp; <span class="hljs-number">0xFF</span>);
        <span class="hljs-keyword">return</span> data;
    }
</code></pre>
<p>This method starts the Bluetooth Low Energy GATT server and builds the full GATT database for the smart plant monitor.</p>
<p>It first obtains the <code>BluetoothManager</code> from the Android system and uses it to open a <code>BluetoothGattServer</code>, passing in a callback that will handle read, write, and notification events from connected clients.</p>
<p>Then it creates the custom Plant Monitor Service using the <code>PLANT_SERVICE_UUID</code> and marks it as a primary service.</p>
<p>Inside this service it defines two characteristics. The moisture characteristic is created with the <code>MOISTURE_CHAR_UUID</code> and given the properties Read and Notify, meaning a client can read the current soil moisture and also subscribe to notifications when it changes. It is read only, so it uses a read permission. The reporting interval characteristic is created with the <code>INTERVAL_CHAR_UUID</code> and uses both Read and Write properties so that a client can check the current interval and update it. It uses both read and write permissions.</p>
<p>The code sets the initial values for these characteristics: the moisture characteristic gets the current moisture percentage stored as a single byte, and the interval characteristic gets a two byte representation of the reporting interval using the helper method <code>intToTwoBytes</code>, which splits a 16 bit integer into low and high bytes.</p>
<p>To allow notifications for moisture, it adds a Client Characteristic Configuration Descriptor (CCCD) with a standard UUID <code>0x2902</code> and read or write permissions, then attaches this descriptor to the moisture characteristic. Both characteristics are added to the plant service.</p>
<p>Next, the method creates the standard Battery Service as another primary service using the well-known battery UUID. It defines the Battery Level characteristic with read and notify properties and read permission.</p>
<p>The initial battery level is stored as a single byte. Just like with moisture, it adds a CCCD descriptor to support notifications and attaches it to the battery characteristic. The battery characteristic is then added to the battery service.</p>
<p>Finally, the method registers both the plant service and the battery service with the GATT server using <code>addService</code>, which makes them visible to any BLE client that connects. As a small utility, the <code>intToTwoBytes</code> method at the end converts a 16 bit integer into a two element byte array with the least significant byte first, which is a common way to encode integers in BLE characteristics.</p>
<p>Now we’ll implement the callback to handle read, write, and notification logic.</p>
<pre><code class="lang-java">    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> BluetoothGattServerCallback serverCallback =
            <span class="hljs-keyword">new</span> BluetoothGattServerCallback() {

        <span class="hljs-meta">@Override</span>
        <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onConnectionStateChange</span><span class="hljs-params">(BluetoothDevice device,
                                            <span class="hljs-keyword">int</span> status,
                                            <span class="hljs-keyword">int</span> newState)</span> </span>{
            Log.d(TAG, <span class="hljs-string">"Device connection state: "</span> + newState);
        }

        <span class="hljs-meta">@Override</span>
        <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCharacteristicReadRequest</span><span class="hljs-params">(BluetoothDevice device,
                                                <span class="hljs-keyword">int</span> requestId,
                                                <span class="hljs-keyword">int</span> offset,
                                                BluetoothGattCharacteristic characteristic)</span> </span>{

            <span class="hljs-keyword">if</span> (characteristic.getUuid().equals(MOISTURE_CHAR_UUID)) {
                moistureCharacteristic.setValue(<span class="hljs-keyword">new</span> <span class="hljs-keyword">byte</span>[]{(<span class="hljs-keyword">byte</span>) currentMoisture});
                gattServer.sendResponse(device, requestId,
                        BluetoothGatt.GATT_SUCCESS, offset,
                        moistureCharacteristic.getValue());
            } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (characteristic.getUuid().equals(INTERVAL_CHAR_UUID)) {
                intervalCharacteristic.setValue(intToTwoBytes(reportingIntervalSec));
                gattServer.sendResponse(device, requestId,
                        BluetoothGatt.GATT_SUCCESS, offset,
                        intervalCharacteristic.getValue());
            } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (characteristic.getUuid().equals(BATTERY_LEVEL_UUID)) {
                batteryLevelCharacteristic.setValue(<span class="hljs-keyword">new</span> <span class="hljs-keyword">byte</span>[]{(<span class="hljs-keyword">byte</span>) batteryLevel});
                gattServer.sendResponse(device, requestId,
                        BluetoothGatt.GATT_SUCCESS, offset,
                        batteryLevelCharacteristic.getValue());
            } <span class="hljs-keyword">else</span> {
                gattServer.sendResponse(device, requestId,
                        BluetoothGatt.GATT_FAILURE, offset, <span class="hljs-keyword">null</span>);
            }
        }

        <span class="hljs-meta">@Override</span>
        <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCharacteristicWriteRequest</span><span class="hljs-params">(BluetoothDevice device,
                                                 <span class="hljs-keyword">int</span> requestId,
                                                 BluetoothGattCharacteristic characteristic,
                                                 <span class="hljs-keyword">boolean</span> preparedWrite,
                                                 <span class="hljs-keyword">boolean</span> responseNeeded,
                                                 <span class="hljs-keyword">int</span> offset,
                                                 <span class="hljs-keyword">byte</span>[] value)</span> </span>{

            <span class="hljs-keyword">if</span> (characteristic.getUuid().equals(INTERVAL_CHAR_UUID)) {
                <span class="hljs-keyword">int</span> newInterval = ((value[<span class="hljs-number">1</span>] &amp; <span class="hljs-number">0xFF</span>) &lt;&lt; <span class="hljs-number">8</span>) | (value[<span class="hljs-number">0</span>] &amp; <span class="hljs-number">0xFF</span>);
                reportingIntervalSec = newInterval;
                Log.d(TAG, <span class="hljs-string">"New reporting interval: "</span> + reportingIntervalSec + <span class="hljs-string">" sec"</span>);

                intervalCharacteristic.setValue(value);
            }

            <span class="hljs-keyword">if</span> (responseNeeded) {
                gattServer.sendResponse(device, requestId,
                        BluetoothGatt.GATT_SUCCESS, offset, value);
            }
        }

        <span class="hljs-meta">@Override</span>
        <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onDescriptorWriteRequest</span><span class="hljs-params">(BluetoothDevice device,
                                             <span class="hljs-keyword">int</span> requestId,
                                             BluetoothGattDescriptor descriptor,
                                             <span class="hljs-keyword">boolean</span> preparedWrite,
                                             <span class="hljs-keyword">boolean</span> responseNeeded,
                                             <span class="hljs-keyword">int</span> offset,
                                             <span class="hljs-keyword">byte</span>[] value)</span> </span>{

            <span class="hljs-keyword">if</span> (descriptor.getCharacteristic().getUuid().equals(MOISTURE_CHAR_UUID)) {
                Log.d(TAG, <span class="hljs-string">"Moisture notifications enabled"</span>);
            } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (descriptor.getCharacteristic().getUuid().equals(BATTERY_LEVEL_UUID)) {
                Log.d(TAG, <span class="hljs-string">"Battery notifications enabled"</span>);
            }

            <span class="hljs-keyword">if</span> (responseNeeded) {
                gattServer.sendResponse(device, requestId,
                        BluetoothGatt.GATT_SUCCESS, offset, value);
            }
        }
    };
}
</code></pre>
<p>This callback handles all the important server side events for the smart plant monitor GATT Server, including connection changes, characteristic reads and writes, and descriptor writes for notifications.</p>
<p>When a device connects or disconnects, <code>onConnectionStateChange</code> is called and simply logs the new connection state so you can see when a client appears or disappears. The core logic lives in <code>onCharacteristicReadRequest</code>, which is invoked whenever a BLE client performs a read on one of the server’s characteristics.</p>
<p>The method checks which characteristic is being read by comparing its UUID. If it’s the moisture characteristic, it refreshes the characteristic value with the current moisture percentage, then responds with <code>GATT_SUCCESS</code> and the encoded value. If it’s the interval characteristic, it encodes the current reporting interval into two bytes using <code>intToTwoBytes</code> and sends that back. If it’s the battery level characteristic, it encodes the current battery percentage into a single byte and returns it. If the UUID does not match any known characteristic, the server responds with <code>GATT_FAILURE</code>, which tells the client that the request could not be fulfilled.</p>
<p>The <code>onCharacteristicWriteRequest</code> method handles writes from the client. In this implementation, only the reporting interval characteristic is writable. When a write targets this characteristic, the code decodes the two byte value sent by the client into an integer by reconstructing it from the low and high bytes. It updates the internal <code>reportingIntervalSec</code> field, logs the new interval, and stores the received bytes in the characteristic so that future reads return the updated value. If the client requested a response, the server sends back a success status and echoes the written value.</p>
<p>Finally, <code>onDescriptorWriteRequest</code> is called when a client writes to a descriptor, typically the Client Characteristic Configuration Descriptor that controls notifications. The code checks whether the descriptor belongs to the moisture or battery characteristic and logs that notifications have been enabled for the corresponding data source. If a response is needed, it sends back <code>GATT_SUCCESS</code>.</p>
<p>Altogether, this callback turns the server into a live plant monitor that can answer real time read requests, accept configuration updates, and honor notification subscriptions for moisture and battery level.</p>
<p>We now have a fully functioning GATT Server that supports read and write operations, and can also send notifications for moisture and battery when needed.</p>
<p>To simulate notifications, the server can periodically update values and call <code>notifyCharacteristicChanged</code>:</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">simulateSensorUpdate</span><span class="hljs-params">(BluetoothDevice device)</span> </span>{
    <span class="hljs-comment">// Simulate moisture dropping slightly</span>
    currentMoisture = Math.max(<span class="hljs-number">0</span>, currentMoisture - <span class="hljs-number">1</span>);
    moistureCharacteristic.setValue(<span class="hljs-keyword">new</span> <span class="hljs-keyword">byte</span>[]{(<span class="hljs-keyword">byte</span>) currentMoisture});
    gattServer.notifyCharacteristicChanged(device, moistureCharacteristic, <span class="hljs-keyword">false</span>);
}
</code></pre>
<p>This method simulates a live update to the soil moisture sensor and demonstrates how a GATT Server sends notifications to a connected BLE client.</p>
<p>It decreases the current moisture reading by one percent, ensuring the value never falls below zero using <code>Math.max</code>. After adjusting the simulated value, the method stores the updated moisture value inside the moisture characteristic using <code>setValue</code>, which prepares the new data to be transmitted.</p>
<p>It then calls <code>notifyCharacteristicChanged</code>, which sends a BLE notification packet to the specified connected device, telling the client that the characteristic value has changed and delivering the new moisture reading immediately.</p>
<p>The final parameter <code>false</code> indicates that this is a notification rather than an indication, which means the server does not require an acknowledgment from the client. This method would typically be called on a timer or triggered by real sensor hardware, allowing the client application to receive continuous updates in real time without repeatedly polling the server.</p>
<h3 id="heading-implementing-the-gatt-client-in-java">Implementing the GATT Client in Java</h3>
<p>On the Android client side, we connect to the plant monitor, discover services, then interact with the three characteristics.</p>
<p>First, we’ll discover the services and store references.</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PlantMonitorClient</span> </span>{

    <span class="hljs-keyword">private</span> BluetoothGatt bluetoothGatt;

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> UUID PLANT_SERVICE_UUID =
            UUID.fromString(<span class="hljs-string">"12345678-1234-5678-1234-56789abc0001"</span>);
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> UUID MOISTURE_CHAR_UUID =
            UUID.fromString(<span class="hljs-string">"12345678-1234-5678-1234-56789abc0002"</span>);
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> UUID INTERVAL_CHAR_UUID =
            UUID.fromString(<span class="hljs-string">"12345678-1234-5678-1234-56789abc0003"</span>);

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> UUID BATTERY_SERVICE_UUID =
            UUID.fromString(<span class="hljs-string">"0000180F-0000-1000-8000-00805F9B34FB"</span>);
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> UUID BATTERY_LEVEL_UUID =
            UUID.fromString(<span class="hljs-string">"00002A19-0000-1000-8000-00805F9B34FB"</span>);

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">connect</span><span class="hljs-params">(Context context, BluetoothDevice device)</span> </span>{
        bluetoothGatt = device.connectGatt(context, <span class="hljs-keyword">false</span>, gattCallback);
    }

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> BluetoothGattCallback gattCallback = <span class="hljs-keyword">new</span> BluetoothGattCallback() {

        <span class="hljs-meta">@Override</span>
        <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onConnectionStateChange</span><span class="hljs-params">(BluetoothGatt gatt,
                                            <span class="hljs-keyword">int</span> status,
                                            <span class="hljs-keyword">int</span> newState)</span> </span>{
            <span class="hljs-keyword">if</span> (newState == BluetoothProfile.STATE_CONNECTED) {
                Log.d(TAG, <span class="hljs-string">"Connected. Discovering services."</span>);
                gatt.discoverServices();
            }
        }

        <span class="hljs-meta">@Override</span>
        <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onServicesDiscovered</span><span class="hljs-params">(BluetoothGatt gatt, <span class="hljs-keyword">int</span> status)</span> </span>{
            Log.d(TAG, <span class="hljs-string">"Services discovered."</span>);

            <span class="hljs-comment">// Read current moisture and battery once</span>
            readMoisture(gatt);
            readBatteryLevel(gatt);

            <span class="hljs-comment">// Enable notifications</span>
            enableMoistureNotifications(gatt);
        }
</code></pre>
<p>This class represents the Bluetooth Low Energy client side of the smart plant monitor example. It holds a <code>BluetoothGatt</code> reference that represents the active connection to the BLE server device.</p>
<p>Several UUID constants are defined so the client knows how to find the Plant Monitor Service and its characteristics for moisture and reporting interval, as well as the standard Battery Service and Battery Level characteristic.</p>
<p>The <code>connect</code> method starts the BLE connection by calling <code>device.connectGatt</code>, passing in the Android <code>Context</code>, a flag indicating no automatic reconnection, and a <code>BluetoothGattCallback</code> instance that will receive connection and data events.</p>
<p>Inside the callback, <code>onConnectionStateChange</code> is called whenever the connection state changes. When the new state indicates that the device is connected, the client logs this and calls <code>discoverServices</code> to request the full list of GATT services from the server.</p>
<p>Once the service discovery procedure completes, <code>onServicesDiscovered</code> is triggered. In this method, the client logs that services have been discovered, then immediately reads the current values of the moisture and battery level using helper methods <code>readMoisture</code> and <code>readBatteryLevel</code>, and finally enables notifications for moisture updates using <code>enableMoistureNotifications</code>.</p>
<p>Together, these steps mean that as soon as the client connects to a plant monitor device, it learns what services are available, fetches one time snapshots of important values, and subscribes to real time updates for the most important sensor – which in this case is soil moisture.</p>
<p>Now, we’ll define methods for reading moisture and battery.</p>
<pre><code class="lang-java">        <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">readMoisture</span><span class="hljs-params">(BluetoothGatt gatt)</span> </span>{
            BluetoothGattService service = gatt.getService(PLANT_SERVICE_UUID);
            <span class="hljs-keyword">if</span> (service != <span class="hljs-keyword">null</span>) {
                BluetoothGattCharacteristic ch =
                        service.getCharacteristic(MOISTURE_CHAR_UUID);
                <span class="hljs-keyword">if</span> (ch != <span class="hljs-keyword">null</span>) {
                    gatt.readCharacteristic(ch);
                }
            }
        }

        <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">readBatteryLevel</span><span class="hljs-params">(BluetoothGatt gatt)</span> </span>{
            BluetoothGattService service = gatt.getService(BATTERY_SERVICE_UUID);
            <span class="hljs-keyword">if</span> (service != <span class="hljs-keyword">null</span>) {
                BluetoothGattCharacteristic ch =
                        service.getCharacteristic(BATTERY_LEVEL_UUID);
                <span class="hljs-keyword">if</span> (ch != <span class="hljs-keyword">null</span>) {
                    gatt.readCharacteristic(ch);
                }
            }
        }

        <span class="hljs-meta">@Override</span>
        <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCharacteristicRead</span><span class="hljs-params">(BluetoothGatt gatt,
                                         BluetoothGattCharacteristic characteristic,
                                         <span class="hljs-keyword">int</span> status)</span> </span>{
            <span class="hljs-keyword">if</span> (status == BluetoothGatt.GATT_SUCCESS) {
                <span class="hljs-keyword">if</span> (MOISTURE_CHAR_UUID.equals(characteristic.getUuid())) {
                    <span class="hljs-keyword">int</span> moisture = characteristic.getIntValue(
                            BluetoothGattCharacteristic.FORMAT_UINT8, <span class="hljs-number">0</span>);
                    Log.d(TAG, <span class="hljs-string">"Soil moisture: "</span> + moisture + <span class="hljs-string">" percent"</span>);
                } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (BATTERY_LEVEL_UUID.equals(characteristic.getUuid())) {
                    <span class="hljs-keyword">int</span> battery = characteristic.getIntValue(
                            BluetoothGattCharacteristic.FORMAT_UINT8, <span class="hljs-number">0</span>);
                    Log.d(TAG, <span class="hljs-string">"Battery level: "</span> + battery + <span class="hljs-string">" percent"</span>);
                } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (INTERVAL_CHAR_UUID.equals(characteristic.getUuid())) {
                    <span class="hljs-keyword">int</span> interval = characteristic.getIntValue(
                            BluetoothGattCharacteristic.FORMAT_UINT16, <span class="hljs-number">0</span>);
                    Log.d(TAG, <span class="hljs-string">"Reporting interval: "</span> + interval + <span class="hljs-string">" sec"</span>);
                }
            }
        }
</code></pre>
<p>These methods handle reading values from the smart plant monitor GATT Server. The <code>readMoisture</code> method retrieves the Plant Monitor Service using its UUID, then looks up the soil moisture characteristic inside it. If the characteristic is found, it sends a read request using <code>gatt.readCharacteristic</code>, which asks the server to return the current moisture value.</p>
<p>The <code>readBatteryLevel</code> method behaves the same way but targets the standard Battery Service and Battery Level characteristic. When the server responds to either read request, the callback <code>onCharacteristicRead</code> is triggered. The method first checks whether the read was successful by confirming that the status equals <code>GATT_SUCCESS</code>. It then determines which characteristic was read by comparing UUIDs.</p>
<p>If the response is for the moisture characteristic, it decodes the value from a single byte into an integer percentage and logs it. If it is the battery characteristic, it similarly extracts the single byte battery percentage and logs that value. If the interval characteristic was read, it decodes two bytes into a 16 bit integer and logs the reporting interval in seconds.</p>
<p>This read flow provides the client with a snapshot of the current sensor and configuration values immediately after connecting, before monitoring changes through notifications.</p>
<p>Next, we’ll enable notifications for moisture so that the app receives updates when it changes.</p>
<pre><code class="lang-java">        <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">enableMoistureNotifications</span><span class="hljs-params">(BluetoothGatt gatt)</span> </span>{
            BluetoothGattService service = gatt.getService(PLANT_SERVICE_UUID);
            <span class="hljs-keyword">if</span> (service != <span class="hljs-keyword">null</span>) {
                BluetoothGattCharacteristic ch =
                        service.getCharacteristic(MOISTURE_CHAR_UUID);
                <span class="hljs-keyword">if</span> (ch != <span class="hljs-keyword">null</span>) {
                    gatt.setCharacteristicNotification(ch, <span class="hljs-keyword">true</span>);

                    BluetoothGattDescriptor descriptor =
                            ch.getDescriptor(UUID.fromString(
                                    <span class="hljs-string">"00002902-0000-1000-8000-00805F9B34FB"</span>));

                    <span class="hljs-keyword">if</span> (descriptor != <span class="hljs-keyword">null</span>) {
                        descriptor.setValue(
                                BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                        gatt.writeDescriptor(descriptor);
                    }
                }
            }
        }

        <span class="hljs-meta">@Override</span>
        <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCharacteristicChanged</span><span class="hljs-params">(BluetoothGatt gatt,
                                            BluetoothGattCharacteristic characteristic)</span> </span>{
            <span class="hljs-keyword">if</span> (MOISTURE_CHAR_UUID.equals(characteristic.getUuid())) {
                <span class="hljs-keyword">int</span> moisture = characteristic.getIntValue(
                        BluetoothGattCharacteristic.FORMAT_UINT8, <span class="hljs-number">0</span>);
                Log.d(TAG,
                        <span class="hljs-string">"Soil moisture update: "</span> + moisture + <span class="hljs-string">" percent"</span>);
            }
        }
    };
</code></pre>
<p>This code enables live moisture updates through notifications and handles them when they arrive.</p>
<p>The <code>enableMoistureNotifications</code> method first retrieves the Plant Monitor Service, then obtains the moisture characteristic using its UUID. If the characteristic is available, it calls <code>setCharacteristicNotification</code> with <code>true</code>, which tells the Android BLE stack to start listening for notifications on that characteristic.</p>
<p>But enabling notification support locally is not enough because the GATT specification requires that the client also write to the associated descriptor known as the Client Characteristic Configuration Descriptor, or CCCD, identified by the standard UUID <code>0x2902</code>. The method retrieves this descriptor, sets its value to <code>ENABLE_NOTIFICATION_VALUE</code>, and writes it using <code>writeDescriptor</code>, which sends a request over the air to the server to enable notifications on the device side. Once this configuration is complete, updates are delivered whenever the characteristic value changes.</p>
<p>The <code>onCharacteristicChanged</code> callback is triggered automatically each time the server pushes a new moisture reading. The method checks that the changed characteristic is the moisture characteristic by comparing UUIDs, extracts the soil moisture percentage from a single byte using <code>getIntValue</code>, and logs the updated value. This allows the client app to receive real time sensor readings without constantly polling the server, which saves energy and improves responsiveness for applications such as plant monitoring dashboards or notification alerts.</p>
<p>Finally, the client can write a new reporting interval, for example changing from 60 seconds to 30 seconds.</p>
<pre><code class="lang-java">    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">writeReportingInterval</span><span class="hljs-params">(<span class="hljs-keyword">int</span> newIntervalSec)</span> </span>{
        <span class="hljs-keyword">if</span> (bluetoothGatt == <span class="hljs-keyword">null</span>) <span class="hljs-keyword">return</span>;

        BluetoothGattService service =
                bluetoothGatt.getService(PLANT_SERVICE_UUID);
        <span class="hljs-keyword">if</span> (service != <span class="hljs-keyword">null</span>) {
            BluetoothGattCharacteristic ch =
                    service.getCharacteristic(INTERVAL_CHAR_UUID);
            <span class="hljs-keyword">if</span> (ch != <span class="hljs-keyword">null</span>) {
                <span class="hljs-keyword">byte</span>[] data = <span class="hljs-keyword">new</span> <span class="hljs-keyword">byte</span>[<span class="hljs-number">2</span>];
                data[<span class="hljs-number">0</span>] = (<span class="hljs-keyword">byte</span>) (newIntervalSec &amp; <span class="hljs-number">0xFF</span>);
                data[<span class="hljs-number">1</span>] = (<span class="hljs-keyword">byte</span>) ((newIntervalSec &gt;&gt; <span class="hljs-number">8</span>) &amp; <span class="hljs-number">0xFF</span>);
                ch.setValue(data);
                bluetoothGatt.writeCharacteristic(ch);
            }
        }
    }
}
</code></pre>
<p>This method allows the BLE client to update the reporting interval setting on the smart plant monitor by writing a new value to the interval characteristic on the GATT Server.</p>
<p>It first checks whether the <code>bluetoothGatt</code> object is valid, since no write can occur before a connection is established. It retrieves the Plant Monitor Service using its UUID and then looks up the reporting interval characteristic inside that service.</p>
<p>If the characteristic exists, the method converts the new interval value from an integer into a two byte array, placing the least significant byte first and the most significant byte second, which is the common little endian format used in Bluetooth characteristics. It sets this byte array as the characteristic’s new value and then calls <code>writeCharacteristic</code>, which sends a write request over the air to the server. When the server processes the command in its corresponding write request handler, it will update its internal interval value and acknowledge the change.</p>
<p>This method demonstrates how configuration settings are written from a BLE client to a BLE device, enabling interactive control of behavior instead of only reading sensor values.</p>
<p>With this design, our smart plant monitor system is complete. The GATT Server exposes well-defined services and characteristics. The Android client connects, discovers, reads, writes, and subscribes to notifications. The concept is always the same: services group features. Characteristics hold data and behavior. Clients manipulate characteristics. Servers store and protect them.</p>
<p>Once you can design and code such a profile end to end, you are effectively using GATT the way real products do. The same pattern scales to complex devices like glucose monitors, smart locks, smart glasses, and industrial sensors.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>GATT is the foundation that makes Bluetooth Low Energy communication understandable and reliable. It transforms raw radio signals into meaningful structured information through the use of services and characteristics. Once you understand that every BLE device exposes a database of values that a client can read, write, or subscribe to, the entire system becomes logical instead of mysterious.</p>
<p>Whether you are reading heart rate from a smartwatch, checking the battery level of wireless earbuds, controlling a smart bulb, or configuring an industrial sensor, the interaction always happens through GATT characteristics inside services.</p>
<p>By examining both sides of the communication, the GATT Server and the GATT Client, and by walking through real Java code examples for reading, writing, and receiving notifications, you now have the practical knowledge needed to build and debug real BLE applications. You saw how to define custom services and characteristics, how to interpret data formats, how to enable notifications for dynamic sensor updates, and how to organize a complete device profile using a realistic example in the plant monitor project.</p>
<p>Everything in Bluetooth Low Energy development begins with understanding GATT at this level. Once you are comfortable designing and interacting with services and characteristics, you can confidently move into more advanced topics such as secure pairing and bonding, throughput tuning using MTU and connection interval, power optimization, OTA firmware updates, and tools like nRF Connect and HCI log analysis.</p>
<p>The best way to strengthen what you learned is to build something hands on. Even a simple read and write test project will help the concepts become intuitive.</p>
<p>Mastering GATT is the first major step toward professional Bluetooth development. Every complex system built with BLE, from consumer wearables to medical devices and smart home automation, sits on top of this technology. Now that you understand the structure and communication model, you are ready to explore more sophisticated capabilities and create your own applications with confidence.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Learn Embedded Systems Firmware Basics – A Handbook for Developers ]]>
                </title>
                <description>
                    <![CDATA[ Have you ever wondered how your fridge knows when to cool, or how a coffee machine knows when to stop pouring? Behind the scenes, these devices are powered by embedded systems – small, dedicated computers designed to perform specific tasks reliably a... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/learn-embedded-systems-firmware-basics-handbook-for-devs/</link>
                <guid isPermaLink="false">6859c55cad0bcef0be044476</guid>
                
                    <category>
                        <![CDATA[ embedded systems ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Firmware Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ sensors ]]>
                    </category>
                
                    <category>
                        <![CDATA[ embeddedcourses ]]>
                    </category>
                
                    <category>
                        <![CDATA[ automation ]]>
                    </category>
                
                    <category>
                        <![CDATA[ debugging ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Soham Banerjee ]]>
                </dc:creator>
                <pubDate>Mon, 23 Jun 2025 21:21:32 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1750701027343/86918e8c-4348-4845-b048-6203ae0fcb38.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Have you ever wondered how your fridge knows when to cool, or how a coffee machine knows when to stop pouring? Behind the scenes, these devices are powered by embedded systems – small, dedicated computers designed to perform specific tasks reliably and efficiently.</p>
<p>An embedded system typically goes through a simple but powerful cycle:</p>
<ol>
<li><p>Sense – Gather information from the environment using sensors.</p>
</li>
<li><p>Process – Use software logic to decide what to do with the data.</p>
</li>
<li><p>Act – Trigger a response, like turning on a motor or lighting an LED.</p>
</li>
</ol>
<p>Each project begins with a use case – a specific goal like brewing coffee or controlling a car’s fuel injection. From that, engineers define system requirements, which are split into:</p>
<ul>
<li><p>Hardware (for example, microcontrollers, sensors, actuators)</p>
</li>
<li><p>Software (what we call embedded software)</p>
</li>
</ul>
<p>This handbook focuses on the software side of embedded systems: how we write code to make embedded systems intelligent. Embedded software runs on resource-constrained devices like microcontrollers, which may have just a few kilobytes of memory. The software might need to be highly efficient, reliable, and often capable of working in real-time.</p>
<p>But embedded software isn't just about writing code – it’s also about understanding:</p>
<ul>
<li><p>How hardware works</p>
</li>
<li><p>How to manage memory and power</p>
</li>
<li><p>How to handle timing and communication</p>
</li>
<li><p>How to build robust, fail-safe systems</p>
</li>
</ul>
<p>While embedded systems development isn’t typically research-focused in most industry roles, it demands a broad skill set, from low-level programming to system-level design. What makes this field especially exciting is how it brings together diverse domains like machine learning, digital signal processing (DSP), and control systems, all of which can be applied directly in real-world devices.</p>
<p>In this article, I’ll give you:</p>
<ul>
<li><p>A high-level overview of what embedded software involves</p>
</li>
<li><p>Key concepts every developer should know</p>
</li>
<li><p>A tour of commonly used tools and frameworks</p>
</li>
<li><p>Resources to help you learn and understand basics.</p>
</li>
</ul>
<p>Whether you're just curious or planning a career in embedded systems, this guide is your launchpad.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-hw-layer-microcontroller">HW Layer: Microcontroller</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-firmware-design-and-tools">Firmware Design and Tools</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-tools-and-concepts-for-embedded-development">Tools and Concepts for Embedded Development</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-bare-metal-rtos-and-embedded-operating-systems">Bare Metal, RTOS, and Embedded Operating Systems</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-designing-drivers-for-embedded-systems">Designing Drivers for Embedded Systems</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-security-in-embedded-systems">Security in Embedded Systems</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-debugging-and-forensics-in-embedded-systems">Debugging and Forensics in Embedded Systems</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-automation-and-testing-in-embedded-systems">Automation and Testing in Embedded Systems</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-where-to-go-from-here">Where to Go from Here</a></p>
</li>
</ul>
<p>This article offers a broad overview of embedded firmware development, but it doesn’t cover every aspect, particularly advanced software architecture frameworks or comprehensive lists of open source software and tools. Where appropriate, I have included external resources that were valuable in expanding my own understanding.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>You don’t need to be an expert to follow this guide, but some prior knowledge will help you get the most out of it:</p>
<ul>
<li><p>Basic C or C++ programming**:** Familiarity with functions, pointers, and memory concepts is helpful.</p>
</li>
<li><p>Computer architecture fundamentals**:** Understanding what a CPU does, how memory works, and basic instruction execution will make embedded concepts clearer.</p>
</li>
<li><p>Electronics basics (optional)<strong>:</strong> Knowing how sensors, resistors, or microcontrollers interact at a circuit level is useful but not mandatory.</p>
</li>
<li><p>Comfort with the command line**:** Especially for working with build systems, compilers, and flashing tools.</p>
</li>
</ul>
<p>This guide is ideal for students, engineers, or hobbyists looking to deepen their understanding of how software interacts with hardware in real-world systems.</p>
<p>With that, let’s start from the ground up, hardware. Throughout this guide, most examples will reference ARM Cortex-M microcontrollers, as they are among the most commonly used in the embedded world.</p>
<h2 id="heading-hw-layer-microcontroller">HW Layer: Microcontroller</h2>
<p>One of the most important knowledge blocks in embedded firmware development is understanding how a microcontroller (MCU) works and how it connects to sensors, actuators, and other microcontrollers.</p>
<p>If you’re familiar with basic computer architecture (like instruction sets and memory organization), that knowledge translates well to embedded systems. In fact, Computer System Organization, often taught in computer science and electrical engineering programs, is a great foundation for understanding microcontrollers.</p>
<h3 id="heading-what-is-a-microcontroller">What is a Microcontroller?</h3>
<p>A microcontroller is a compact computing unit that includes:</p>
<ul>
<li><p>A CPU (Central Processing Unit or Microprocessor)</p>
</li>
<li><p>Memory (Flash and RAM)</p>
</li>
<li><p>Peripherals (for I/O, timers, communication, and so on)</p>
</li>
</ul>
<p>In essence, it's a tiny computer-on-a-chip, optimized for specific control tasks like reading sensors or driving motors.</p>
<p>By contrast, a microprocessor is just the CPU. It requires external memory and peripherals to function. Microcontrollers are self-contained and better suited for embedded applications.</p>
<p>For example, this <a target="_blank" href="https://www.st.com/resource/en/reference_manual/dm00031020-stm32f405-415-stm32f407-417-stm32f427-437-and-stm32f429-439-advanced-arm-based-32-bit-mcus-stmicroelectronics.pdf">reference manual</a> for the STM32F4 series (from STMicroelectronics) provides detailed documentation on not just the CPU but each peripheral’s functionality and the register map.</p>
<h3 id="heading-instruction-set-architecture-isa">Instruction Set Architecture (ISA)</h3>
<p>A microprocessor executes a series of instructions defined by its Instruction Set Architecture (ISA). ISA as defined by <a target="_blank" href="https://www.arm.com/glossary/isa">ARM</a> is a part of the abstract model of a computer that defines how the CPU is controlled by the software. The ISA acts as an interface between the hardware and the software, specifying both what the processor is capable of doing as well as how it gets done.</p>
<p>For example:</p>
<ul>
<li><p>ARMv7 – used in ARM Cortex-M3.</p>
</li>
<li><p>ARMv7E – used in Cortex-M4 and M7.</p>
</li>
</ul>
<p>Many vendors (for example, STMicroelectronics, NXP, TI) manufacture MCUs that support ARM ISAs but include their own peripheral sets. Understanding the ISA is essential for low-level coding and interpreting assembly instructions.</p>
<p>This <a target="_blank" href="https://developer.arm.com/documentation/ddi0403/ee/?lang=en">ARMv7-M architecture reference manual</a> provides more details on v7 Architecture.</p>
<h3 id="heading-memory-in-microcontrollers">Memory in Microcontrollers</h3>
<p>Most microcontrollers typically feature two types of memory:</p>
<ul>
<li><p><strong>Flash</strong> – Stores your code and read-only data.</p>
</li>
<li><p><strong>RAM</strong> – Used during program execution to hold:</p>
<ul>
<li><p>The heap (for dynamic memory)</p>
</li>
<li><p>The stack</p>
</li>
<li><p>The .data and .bss sections (initialized/uninitialized global/static variables)</p>
</li>
</ul>
</li>
</ul>
<p>Later sections have resources that go deeper into memory mapping and how these regions interact during runtime.</p>
<h3 id="heading-clock-and-power-management">Clock and Power Management</h3>
<p>Microcontrollers are digital logic devices built from:</p>
<ul>
<li><p>Combinatorial logic – Logic gates that evaluate outputs instantly</p>
</li>
<li><p>Sequential logic – Relies on clocks to move through states</p>
</li>
</ul>
<p>The clock tree distributes timing signals across the CPU and peripherals. MCUs often support multiple clock sources (internal RC, external crystal, PLL), and use prescalers to drive components at different frequencies.</p>
<p>For power-sensitive applications, MCUs offer multiple low-power modes:</p>
<ul>
<li><p>Sleep – CPU off, timers and peripherals are mostly active, memory is retained</p>
</li>
<li><p>Deep Sleep – CPU off, most clocks off, memory is retained, wake-up is slower than sleep, power consumption is lower than Sleep</p>
</li>
<li><p>Standby – CPU off, few interrupts are active, everything else is powered down, memory is not retained. Lowest power mode.</p>
</li>
</ul>
<p>These modes reduce power consumption by turning off clocks and disabling unused peripherals. Designing the system to switch in and out of low-power states effectively is a core skill in embedded software development.</p>
<p>This article talks about <a target="_blank" href="https://www.playembedded.org/blog/arm-cortex-clock-tree-101/">Clock Trees and Oscillators</a> for the ARM Cortex microcontrollers.</p>
<h3 id="heading-interrupts">Interrupts</h3>
<p>Interrupts let MCUs react to asynchronous events, like button presses or sensor signals.</p>
<p>An interrupt temporarily pauses normal code execution to run a dedicated handler. After it’s serviced, the CPU resumes its previous task. They are vital for:</p>
<ul>
<li><p>Fast event response</p>
</li>
<li><p>Reduced polling</p>
</li>
<li><p>Efficient power use (for example, waking from sleep)</p>
</li>
</ul>
<h3 id="heading-timers">Timers</h3>
<p>Timers are built-in peripherals used to track time or generate events.</p>
<p>Common uses are:</p>
<ul>
<li><p>Implementing software delays</p>
</li>
<li><p>Creating precise software timers</p>
</li>
<li><p>Waking up from low-power modes</p>
</li>
</ul>
<p>Mastering timers helps with real-time behavior and precise event scheduling.</p>
<h3 id="heading-communication-protocols">Communication Protocols</h3>
<p>Microcontrollers often need to talk to other devices via built-in communication peripherals:</p>
<ul>
<li><p><strong>UART (Universal Asynchronous Receiver/Transmitter):</strong> Serial communication between two devices, great for logs and debugging.</p>
</li>
<li><p><strong>I²C (Inter-Integrated Circuit):</strong> Two wire protocol for talking to sensors and EEPROMs.</p>
</li>
<li><p><strong>SPI (Serial Peripheral Interface):</strong> High Speed, full-duplex protocol for devices like Flash or displays.</p>
</li>
<li><p><strong>USB (Universal Serial Bus):</strong> Complex but widely used for PCs, data acquisition and HID devices.</p>
</li>
</ul>
<p>Here’s a figure showing multiple peripherals connected to a MCU:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750017729550/799b8649-bb39-4d5d-a309-9c3b76898eb8.png" alt="A MCU that is connected to Flash over SPI, connected to another MCU2 over UART, connected to Temperature Sensor over I2C and connected to Host Computer over USB. This picture shows how multiple peripherals are connected to a Host Computer" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>DMA or Direct Memory Access is an important peripheral which can be used to transfer data to/from memory without CPU involvement. It improves performance and allows the CPU to perform other tasks or enter low power mode to reduce power consumption.</p>
<p>This <a target="_blank" href="https://www.parlezvoustech.com/en/comparaison-protocoles-communication-i2c-spi-uart/">article</a> provides a good overview of the communication protocols I2C, UART and SPI.</p>
<p>We’ve now covered the essential building blocks of microcontroller hardware – from memory and clocks to interrupts and communication buses.</p>
<p>Next, we’ll explore the software principles and tools that bring these microcontrollers to life, including compilers, debuggers, and embedded development frameworks.</p>
<h2 id="heading-firmware-design-and-tools">Firmware Design and Tools</h2>
<h3 id="heading-designing-embedded-software">Designing Embedded Software</h3>
<p>Even though embedded systems operate under unique hardware constraints, software design principles are still crucial. Applying them thoughtfully becomes even more important when memory, CPU cycles, and responsiveness are limited.</p>
<p>Most Embedded firmware projects begin with a structured design approach:</p>
<ol>
<li><p>Understand the problem statement</p>
</li>
<li><p>List assumptions</p>
</li>
<li><p>Define use cases</p>
</li>
<li><p>Define system and software requirements</p>
</li>
<li><p>Create high-level architecture</p>
</li>
<li><p>Drill down to detailed design and implementation</p>
</li>
</ol>
<p>If you’re new to software design, check out my <a target="_blank" href="https://www.freecodecamp.org/news/learn-software-design-basics/">article</a> on design principles.</p>
<p>Here’s a figure showing the five blocks of software design:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750557879213/eab45a1f-ec1a-4c3d-81ce-c67365a451d4.png" alt="Blocks of software design: Problem statement describes the problem, Use cases describe the use case for which the problem statement is valid, then comes collecting the requirements, creating the architecture and the final design  " class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-using-design-patterns">Using Design Patterns</h3>
<p>Once you're designing individual components, design patterns help you write scalable and maintainable code. Here are some common patterns in embedded systems:</p>
<ul>
<li><p>Publisher-Subscriber (Observer) – Useful for decoupling event producers and consumers (for example, sensor data being broadcast to multiple modules).</p>
</li>
<li><p>Singleton – Ensures only one instance of a module or resource manager exists (for example, for drivers or HAL layers).</p>
</li>
<li><p>Adapter – Translates between incompatible interfaces (for example, wrapping platform-specific code into a portable application layer).</p>
</li>
<li><p>State Machine – Represents system behavior as transitions between states (for example, Bluetooth states: <code>IDLE → SCANNING → CONNECTING → CONNECTED → DISCONNECTED</code>).</p>
</li>
</ul>
<p>Design patterns often need to be adapted for memory and timing constraints, but the core concepts remain highly relevant.</p>
<p>There are lot of great resources on design patterns – here are a few that helped me:</p>
<ol>
<li><p>Book: <a target="_blank" href="https://www.amazon.com/Head-First-Design-Patterns-Object-Oriented/dp/149207800X/">Head-first Design patterns</a> - A great book to get understand the concept of design patterns</p>
</li>
<li><p>Book: <a target="_blank" href="https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612/">Design Patterns: Elements of Reusable Object-Oriented Software</a></p>
</li>
<li><p>Course: <a target="_blank" href="https://www.freecodecamp.org/news/master-object-oriented-programming-and-design-patterns-in-c/">Object-Oriented Programming and Design Patterns in C#</a></p>
</li>
<li><p>Article on HSM: <a target="_blank" href="https://barrgroup.com/blog/introduction-hierarchical-state-machines">Hierarchical State Machine Overview (Barr Group)</a></p>
</li>
</ol>
<h3 id="heading-programming-languages-for-embedded-systems">Programming Languages for Embedded Systems</h3>
<p>While any language can theoretically be used if it compiles to machine code, in practice, three dominate the embedded world:</p>
<ul>
<li><p>C – The industry standard. Provides deterministic behavior and low-level access, making it ideal for memory and timing-sensitive code.</p>
</li>
<li><p>C++ – Adds object-oriented features while maintaining control. Once considered risky in embedded due to synthesized code and overhead, it’s now widely adopted where systems benefit from abstraction and modularity.</p>
</li>
<li><p>Rust – A memory-safe alternative gaining traction in safety-critical and open-source embedded development.</p>
</li>
</ul>
<p>Languages like Python (via MicroPython or CircuitPython) are used in educational or prototyping contexts but are not suitable for production due to performance and memory overhead.</p>
<p>Some resources on programming languages that might be helpful to understand concepts:</p>
<ol>
<li><p><a target="_blank" href="https://docs.rust-embedded.org/book/">The Embedded Rust Book</a></p>
</li>
<li><p><a target="_blank" href="https://www.freecodecamp.org/news/learn-c-programming-classic-book-dr-chuck/">C Programming Language by K&amp;R</a></p>
</li>
<li><p><a target="_blank" href="https://www.google.com/aclk?sa=L&amp;ai=DChcSEwi31JG8pvSNAxUpFa0GHX8lIoEYABAHGgJwdg&amp;co=1&amp;gclid=CjwKCAjw3rnCBhBxEiwArN0QE9cC5kuS7nAxauOzmDpkIoD63W3Ki8X0sTYfsUfrr8HYOdmqQQG5MBoCty4QAvD_BwE&amp;cce=1&amp;sig=AOD64_2a4D154E-aGKmSJlj_yP-RUq3HkQ&amp;ctype=5&amp;q=&amp;ved=2ahUKEwj_l428pvSNAxWaEzQIHb4eN3cQ9aACKAB6BAgLEA8&amp;adurl=">Inside the C++ Object model</a> – There are a lot of books and lectures on C++, but for embedded, understanding the object model benefits a lot.</p>
</li>
</ol>
<h3 id="heading-data-structures-matter">Data Structures Matter</h3>
<p>Embedded systems require careful data handling due to strict memory and timing constraints. Mastering core data structures is essential:</p>
<ul>
<li><p>Arrays – fixed-size data.</p>
</li>
<li><p>Linked Lists – Common in software timers, queues.</p>
</li>
<li><p>Stacks and Queues – Task scheduling, event management and data storage.</p>
</li>
<li><p>Bitfields/Flags – Memory efficient state representation.</p>
</li>
<li><p>Binary Trees – Used in routing tables or decision logic.</p>
</li>
</ul>
<p>You'll often build event queues, circular buffers, or timer lists, all of which rely on these foundational structures.</p>
<p>There are a lot of resources for understanding data structures, but I have found this one to be helpful for learning and practicing: <a target="_blank" href="https://www.geeksforgeeks.org/dsa/dsa-tutorial-learn-data-structures-and-algorithms/">GeeksForGeeks DSA Tutorial</a>. And <a target="_blank" href="https://www.freecodecamp.org/news/learn-data-structures-and-algorithms-2/">here’s a full course on DSA</a> if you want to dive deeper.</p>
<h3 id="heading-bit-manipulation-a-core-embedded-skill">Bit Manipulation: A Core Embedded Skill</h3>
<p>Unlike general-purpose software, embedded systems often require low-level access to registers and require precise bit control:</p>
<ul>
<li><p>Setting and clearing individual bits</p>
</li>
<li><p>Using bitwise operators like <code>AND (&amp;)</code>, <code>OR (|)</code>, <code>XOR (^)</code></p>
</li>
<li><p>Bit masking and shifting (<code>&lt;&lt;</code>, <code>&gt;&gt;</code>)</p>
</li>
</ul>
<p>Mastering bit hacks is essential for writing hardware drivers or manipulating control registers.</p>
<p>This resource provides a good number of examples for bit manipulation: <a target="_blank" href="https://graphics.stanford.edu/~seander/bithacks.html">Stanford Bit Hacks</a>.</p>
<h2 id="heading-tools-and-concepts-for-embedded-development">Tools and Concepts for Embedded Development</h2>
<h3 id="heading-cross-compilation">Cross Compilation</h3>
<p>Embedded code is compiled on a host (like your PC) for a target architecture using cross-compilers.</p>
<p>To do this, you need:</p>
<ul>
<li><p>A compiler (for example, <code>arm-none-eabi-gcc</code> for ARM Cortex-M) that compiles high level language code into Assembly language instructions.</p>
</li>
<li><p>A linker to layout and combine object files.</p>
</li>
<li><p>A Makefile or build system to organize and automate compilation, linking and binary creation.</p>
</li>
</ul>
<p>Here’s an example to compile a main.c to create a main.elf that can be flashed on the device:</p>
<pre><code class="lang-plaintext">arm-none-eabi-gcc main.c -o main.elf
</code></pre>
<p>A Makefile is a script used by the <code>make</code> build automation tool to compile and link programs to create a binary. It defines how to build your program from source files, manages compilation order based on dependencies and defines commands to complete the build.</p>
<p>For example, lets write a Makefile for building a project for an ARM Cortex-M4 target that has three source files: a main.c, utils.c, and sensor.c</p>
<pre><code class="lang-makefile">CC = arm-none-eabi-gcc
CFLAGS = -c -mcpu=cortex-m4 -mthumb -Wall -O2
LDFLAGS = -mcpu=cortex-m4 -mthumb
TARGET = main.elf
OBJS = main.o utils.o sensor.o
SRC = main.c utils.c sensor.c

<span class="hljs-variable">$(TARGET)</span>: <span class="hljs-variable">$(OBJS)</span>
    <span class="hljs-variable">$(CC)</span> <span class="hljs-variable">$(OBJS)</span> -o <span class="hljs-variable">$(TARGET)</span>

<span class="hljs-section">main.o: main.c</span>
    <span class="hljs-variable">$(CC)</span> <span class="hljs-variable">$(CFLAGS)</span> main.c

<span class="hljs-section">utils.o: utils.c</span>
    <span class="hljs-variable">$(CC)</span> <span class="hljs-variable">$(CFLAGS)</span> utils.c

<span class="hljs-section">sensor.o: sensor.c</span>
    <span class="hljs-variable">$(CC)</span> <span class="hljs-variable">$(CFLAGS)</span> sensor.c

<span class="hljs-section">clean:</span>
    rm -f *.o *.elf
</code></pre>
<p>In the above makefile, here’s a description of the flags:</p>
<ul>
<li><p><code>-mcpu=cortex-m4</code>: Targets the ARM Cortex-M4 processor.</p>
</li>
<li><p><code>-mthumb</code>: Enables Thumb instruction set, which is used by ARM Cortex-M series.</p>
</li>
<li><p><code>-Wall</code>: Enables all common warnings.</p>
</li>
<li><p><code>-O2</code>: Optimization level 2 for balance between performance and code size.</p>
</li>
</ul>
<p>Makefiles can seem intimidating, but they’re just scripts that define how to build your program from source. Once you understand the basics, they’re a huge productivity booster.</p>
<p>A linker script tells the linker (<code>ld</code>) how to organize the program in memory where to place code, data, stack, heap, and so on. It's crucial for embedded systems because you're working with limited memory and specific memory-mapped hardware.</p>
<p>Here’s an example of a simple linker script for a STM32F4 microcontroller:</p>
<pre><code class="lang-makefile">/* STM32F4 Cortex‑M4 Simple Linker Script */

ENTRY(Reset_Handler)

/* Define memory regions based on STM32F4 datasheet */
MEMORY
{
  FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
  RAM   (rwx): ORIGIN = 0x20000000, LENGTH = 128K
}

/* Section layout */
SECTIONS
{
  /* Interrupt vectors and code go into Flash */
  .isr_vector :
  {
    KEEP(*(.isr_vector))    /* Keep vector table (reset, etc.) */
  } &gt; FLASH

  .text :
  {
    *(.text*)               /* All code */
    *(.rodata*)             /* Read-only data */
    . = ALIGN(4)
    _etext = .             /* End of code (used for data init) */
  } &gt; FLASH

  /* Initialized data: load from Flash, run in RAM */
  .data : AT(_etext)
  {
    _sdata = .            /* Start of .data in RAM */
    *(.data*)
    . = ALIGN(4)
    _edata = .            /* End of .data */
  } &gt; RAM

  /* Uninitialized data (zero-filled) */
  .bss :
  {
    _sbss = .
    *(.bss*)
    *(COMMON)
    . = ALIGN(4)
    _ebss = .
  } &gt; RAM

  /* Define stack end (top of RAM) */
  _estack = ORIGIN(RAM) + LENGTH(RAM);
}
</code></pre>
<p>Descriptions of the above file:</p>
<ul>
<li><p>MEMORY: Defines your microcontroller’s memory layout – 1 MB Flash and 128 KB SRAM.</p>
</li>
<li><p>ENTRY(Reset_Handler): Sets the reset handler as the program entry point.</p>
</li>
<li><p>.isr_vector and **.**text: Code sections placed in Flash. <code>.isr_vector</code> must use <code>KEEP()</code> so it's not removed during linking.</p>
</li>
<li><p>.data : AT(_etext): Loads initialized variables from Flash but places them in RAM.</p>
</li>
<li><p>**.**bss: Zero-initialized data, allocated in RAM</p>
</li>
<li><p>_estack: Defines the initial stack pointer using the end of RAM.</p>
</li>
</ul>
<p>Here are some sources to understand Makefiles, cross-compilation, and Linkers. And just note that using Makefile in a project is the best way to learn and master Makefiles:</p>
<ol>
<li><p>Makefiles:</p>
<ul>
<li><p><a target="_blank" href="https://www.gnu.org/software/make/manual/make.pdf">GNU Make Manual</a></p>
</li>
<li><p><a target="_blank" href="https://makefiletutorial.com/">Makefile Tutorial</a></p>
</li>
<li><p><a target="_blank" href="https://www.gnu.org/software/make/manual/make.pdf">In Pyjama</a> <a target="_blank" href="https://inpyjama.com/post/makefile-2/">M</a><a target="_blank" href="https://makefiletutorial.com/">akef</a><a target="_blank" href="https://www.gnu.org/software/make/manual/make.pdf">ile Article</a></p>
</li>
</ul>
</li>
<li><p>Linker Scripts:</p>
<ul>
<li><p><a target="_blank" href="https://interrupt.memfault.com/blog/how-to-write-linker-scripts-for-firmware">Interrupt Blog on Linker Scripts</a></p>
</li>
<li><p><a target="_blank" href="https://medium.com/%40pc0is0me/an-introduction-to-linker-file-59ce2e9c5e73">Intro to Linker Files – Medium</a></p>
</li>
</ul>
</li>
</ol>
<h3 id="heading-flashing-the-binary">Flashing the Binary</h3>
<p>Once you’ve compiled your code into a binary file, the next step is to <strong>flash</strong> it into the target microcontroller’s non-volatile memory via <strong>SWD</strong> (Serial Wire Debug) or <strong>JTAG</strong>. Flashing tools like OpenOCD, ST-Link, J-Link, or vendor-specific utilities manage this process.</p>
<h4 id="heading-what-is-flashing">What Is Flashing?</h4>
<p>Flashing is the process of writing a compiled firmware image (typically a <code>.bin</code> or <code>.hex</code> file) into the microcontroller’s Flash memory. This enables the embedded system to retain and run your code even after power is removed.</p>
<p>The flashing tool communicates with the microcontroller over SWD or JTAG to:</p>
<ul>
<li><p>Halt the MCU (if needed)</p>
</li>
<li><p>Access the internal flash controller</p>
</li>
<li><p>Erase the relevant flash sectors</p>
</li>
<li><p>Write the binary data to specific memory addresses</p>
</li>
<li><p>Verify that the data was written correctly</p>
</li>
</ul>
<p>OpenOCD (Open On-Chip Debugger) is a powerful, open-source utility that facilitates debugging and flashing of ARM-based microcontrollers. It supports a wide variety of hardware interfaces and microcontroller families, including STM32.</p>
<p>OpenOCD provides:</p>
<ul>
<li><p>Flashing capabilities for <code>.elf</code>, <code>.bin</code>, and <code>.hex</code> files</p>
</li>
<li><p>Debugging via GDB (GNU’s open source debugger) integration</p>
</li>
<li><p>Support for multiple debug probes (J-Link, ST-Link, CMSIS-DAP)</p>
</li>
<li><p>Scripting via configuration files for board-specific and target-specific setups</p>
</li>
</ul>
<p>A simple command to flash a binary using OpenOCD might look like this:</p>
<pre><code class="lang-makefile">bashCopyEditopenocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c <span class="hljs-string">"program main.elf verify reset exit"</span>
</code></pre>
<p>This tells OpenOCD to:</p>
<ul>
<li><p>Use the ST-Link interface</p>
</li>
<li><p>Load the STM32F4 target configuration</p>
</li>
<li><p>Program <code>main.elf</code> into flash</p>
</li>
<li><p>Verify it was written correctly</p>
</li>
<li><p>Reset the MCU</p>
</li>
<li><p>Exit the session</p>
</li>
</ul>
<p>For a detailed walkthrough, check out: <a target="_blank" href="https://kickstartembedded.com/2024/03/26/openocd-one-software-to-rule-debug-them-all/">OpenOCD Deep Dive – Kickstart Embedded</a></p>
<h2 id="heading-bare-metal-rtos-and-embedded-operating-systems">Bare Metal, RTOS, and Embedded Operating Systems</h2>
<p>When writing embedded software, you can approach the problem in three main ways, each with its own trade-offs:</p>
<ol>
<li><p>Bare-Metal Programming</p>
</li>
<li><p>Real-Time Operating Systems (RTOS) (like FreeRTOS, Zephyr)</p>
</li>
<li><p>Embedded Operating Systems (like Embedded Linux)</p>
</li>
</ol>
<p>The best choice depends on your use case, application’s complexity, hardware constraints, and real-time needs.</p>
<p>Most Modern 32-bit microcontrollers (for example, STM32, NXP, Renesas) come with vendor-provided development tools that include:</p>
<ul>
<li><p>HAL (Hardware Abstraction Layer) libraries</p>
</li>
<li><p>Startup code and linker scripts</p>
</li>
<li><p>Peripheral drivers</p>
</li>
<li><p>Sometimes even middleware like USB, BLE, or file system stacks</p>
</li>
</ul>
<p>These tools (like <a target="_blank" href="https://www.st.com/en/ecosystems/stm32cube.html">STM32Cube</a> Config Tools) simplify setup and peripheral configuration, helping you get started quickly, without needing to write low-level code manually.</p>
<p><strong>Benefits of HALs</strong>:</p>
<ul>
<li><p>Rapid prototyping and development</p>
</li>
<li><p>Clean, reusable APIs for peripherals</p>
</li>
<li><p>Great for onboarding and small teams</p>
</li>
</ul>
<p><strong>Drawbacks</strong>:</p>
<ul>
<li><p>Code bloat – HALs support many edge cases and configurations, which can inflate your binary size</p>
</li>
<li><p>Extra latency – HAL often inserts unnecessary layers that reduce performance.</p>
</li>
</ul>
<p>For performance-critical systems, developers often replace HAL drivers with custom, low-level implementations.</p>
<h3 id="heading-bare-metal-programming">Bare-Metal Programming</h3>
<p>Bare-metal programming is the most direct and lightweight approach. There’s no OS, and your code runs directly on the hardware with full control.</p>
<p>Typical setup includes:</p>
<ul>
<li><p>Include the correct header files, especially MCU and peripheral-specific headers provided by the vendor’s HAL (Hardware Abstraction Layer).</p>
</li>
<li><p>Implement a <code>main()</code> function with an infinite loop (<code>while(1)</code>)</p>
</li>
<li><p>Perform all hardware initialization before entering the loop</p>
</li>
<li><p>Use Interrupts to handle asynchronous events.</p>
</li>
<li><p>Continuously check and control inputs/outputs inside the loop</p>
</li>
</ul>
<p>This assumes your toolchain provides startup code and memory setup from the vendor.</p>
<pre><code class="lang-c"><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">"MCU_Header.h"</span></span>

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">(<span class="hljs-keyword">void</span>)</span> </span>{
    <span class="hljs-comment">/* Initialize the MCU and the peripherals */</span>
    init_clock();
    init_peripherals();

    <span class="hljs-comment">/* runs in a loop forever */</span>
    <span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>) {
        <span class="hljs-comment">// Task 1 : Read sensor data</span>
        read_sensor(); 
        <span class="hljs-comment">// Task 2 : Update the actuator based on the sensor data</span>
        update_actuator(); 
    }
}
</code></pre>
<h4 id="heading-how-does-it-run">How does it run?</h4>
<p>When the device powers on or resets, the startup code provided by the vendor is executed first. This code:</p>
<ul>
<li><p>Initializes the reset vector</p>
</li>
<li><p>Copies initialized data from Flash to RAM</p>
</li>
<li><p>Zeros out the <code>.bss</code> section (for uninitialized global/static variables)</p>
</li>
<li><p>Calls your <code>main()</code> function</p>
</li>
</ul>
<p>After calling <code>main()</code>, the system enters an infinite loop where your logic runs. The only other context switch occurs when an interrupt is triggered, briefly diverting control to an Interrupt Service Routine (ISR), after which it returns to the main loop.</p>
<p><strong>When to use it</strong>:</p>
<ul>
<li><p>Simpler applications (for example, blinking LEDs, reading sensors)</p>
</li>
<li><p>Ultra-low-power or ultra-low-latency needs</p>
</li>
<li><p>When every byte of Flash and RAM matters</p>
</li>
</ul>
<p><strong>Pros</strong>:</p>
<ul>
<li><p>Minimal memory usage</p>
</li>
<li><p>Maximum control</p>
</li>
<li><p>Great for learning</p>
</li>
</ul>
<p><strong>Cons</strong>:</p>
<ul>
<li><p>No built-in task management or scheduling</p>
</li>
<li><p>Can become hard to maintain for complex systems</p>
</li>
</ul>
<p>This resource provides good details and example on <a target="_blank" href="https://github.com/cpq/bare-metal-programming-guide">Bare Metal Programming</a>. For more details, this book is great as well: <a target="_blank" href="https://umanovskis.se/files/arm-baremetal-ebook.pdf">ARM Baremetal Ebook</a>.</p>
<h3 id="heading-real-time-operating-systems-rtos">Real-Time Operating Systems (RTOS)</h3>
<p>A Real-Time Operating System (like <a target="_blank" href="https://www.freertos.org/Documentation/01-FreeRTOS-quick-start/01-Beginners-guide/00-Overview">FreeRTOS</a>, <a target="_blank" href="https://docs.zephyrproject.org/latest/">Zephyr</a>) adds lightweight multitasking capabilities to your embedded application. It allows you to split your software into independent tasks that run concurrently and communicate through queues, semaphores, or message passing.</p>
<p>RTOS kernels often support different scheduling strategies like:</p>
<ul>
<li><p>Rate Monotonic Scheduling (RMS) – Tasks with shorter periods get higher priority</p>
</li>
<li><p>Earliest Deadline First (EDF) – Tasks are prioritized based on impending deadlines</p>
</li>
</ul>
<p><strong>Example use cases</strong>:</p>
<ul>
<li><p>A drone where sensor data, motor control, and telemetry need to run in parallel</p>
</li>
<li><p>A medical device where timing is critical for safety</p>
</li>
<li><p>Rockets</p>
</li>
</ul>
<p><strong>Typical RTOS features</strong>:</p>
<ul>
<li><p>Task scheduling</p>
</li>
<li><p>Timers</p>
</li>
<li><p>Inter-task communication</p>
</li>
<li><p>Interrupt handling integration</p>
</li>
<li><p>Power management</p>
</li>
</ul>
<p><strong>Pros</strong>:</p>
<ul>
<li><p>Modular code structure with tasks</p>
</li>
<li><p>Easier to scale as complexity grows</p>
</li>
<li><p>Deterministic execution (when configured correctly)</p>
</li>
</ul>
<p><strong>Cons</strong>:</p>
<ul>
<li><p>Slightly higher memory footprint than bare-metal</p>
</li>
<li><p>Learning curve for scheduling and priority tuning</p>
</li>
</ul>
<p>RTOS Scheduling techniques are interesting – this part of the docs talks about <a target="_blank" href="https://docs.zephyrproject.org/latest/kernel/services/scheduling/index.html#scheduling-algorithm">Zephyr</a> scheduling.</p>
<h3 id="heading-embedded-operating-systems">Embedded Operating Systems</h3>
<p>Sometimes an embedded system is powerful enough to run a full-fledged OS like Embedded Linux, Android Things, or Windows IoT Core. This is common on devices with a display, networking stack, or file system.</p>
<p>It’s best used when the system requires multitasking, user interfaces, file systems, or network stacks, and when there’s plenty of processing power (for example, ARM Cortex-A).</p>
<p>Think of:</p>
<ul>
<li><p>Smart home hubs</p>
</li>
<li><p>Automotive infotainment</p>
</li>
<li><p>Industrial gateways</p>
</li>
</ul>
<p>This table provides a high level methodology for choosing the right type of OS based on your application:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Criteria</strong></td><td><strong>Bare Metal</strong></td><td><strong>RTOS</strong></td><td><strong>Embedded OS</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>System</strong> <strong>Complexity</strong></td><td>Low</td><td>Medium</td><td>High</td></tr>
<tr>
<td><strong>Memory</strong> <strong>Footprint</strong></td><td>Very Low</td><td>Moderate</td><td>High</td></tr>
<tr>
<td><strong>Real-Time Guarantees</strong></td><td>Limited</td><td>Yes</td><td>Depends on Kernel Design</td></tr>
<tr>
<td><strong>Learning Curve</strong></td><td>Steep for scaling</td><td>Moderate</td><td>Steeper (OS internals, tools)</td></tr>
<tr>
<td><strong>Use Case Examples</strong></td><td>Blinking LED, sensor polling</td><td>Drones, medical devices</td><td>Gateways, touchscreens</td></tr>
</tbody>
</table>
</div><p>To understand OS fundamentals, this is a great book: <a target="_blank" href="https://www.amazon.com/Operating-System-Concepts-Abraham-Silberschatz/dp/0470128720">Operating System Concepts</a> and this is a great course: <a target="_blank" href="https://www.youtube.com/playlist?list=PLF2K2xZjNEf97A_uBCwEl61sdxWVP7VWC">UC Berkeley: CS162</a>.</p>
<p>So far, we’ve looked at how embedded applications are structured, whether using bare-metal loops, RTOS multitasking, or full operating systems. But regardless of which execution model you choose, your software ultimately needs to interact with the hardware.</p>
<p>This is where driver development comes in. Drivers form the crucial link between your code and the peripherals it controls, whether it's reading temperature, blinking an LED, or transmitting data over SPI. Let’s take a closer look at how to design robust, portable drivers for embedded systems.</p>
<h2 id="heading-designing-drivers-for-embedded-systems">Designing Drivers for Embedded Systems</h2>
<p>When working with embedded software, one of the most practical and common tasks you’ll encounter is driver development.</p>
<p>A driver is a piece of software that enables the microcontroller (MCU) to interface with a hardware peripheral. This could be a temperature sensor, a motor controller, a display, or even a wireless module.</p>
<p>Drivers act as a bridge between your hardware and the application logic. They abstract away the raw register-level programming so that higher-level code can use clear function calls like <code>read_temperature()</code> or <code>start_motor()</code>.</p>
<h3 id="heading-what-goes-into-a-driver">What Goes Into a Driver?</h3>
<p>A typical embedded driver will include:</p>
<ul>
<li><p>Configuration – Setting up the peripheral with initial parameters (for example, baud rate for UART)</p>
</li>
<li><p>Initialization – Preparing the peripheral for use, including enabling clocks and interrupts</p>
</li>
<li><p>Calibration (if needed) – Adjusting the peripheral based on specific environment or use case</p>
</li>
<li><p>Register Access – Reading from and writing to hardware registers (if applicable)</p>
</li>
<li><p>Power Management – Enabling/disabling the peripheral to save power or putting the peripheral into a low power mode</p>
</li>
<li><p>Interrupt Management – Handling asynchronous events triggered by the peripheral</p>
</li>
</ul>
<p>Here’s a simplified view of a sensor driver API:</p>
<pre><code class="lang-c"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">sensor_init</span><span class="hljs-params">(<span class="hljs-keyword">void</span>)</span></span>;
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">sensor_calibrate</span><span class="hljs-params">(<span class="hljs-keyword">void</span>)</span></span>;
<span class="hljs-function"><span class="hljs-keyword">float</span> <span class="hljs-title">sensor_read_temperature</span><span class="hljs-params">(<span class="hljs-keyword">void</span>)</span></span>;
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">sensor_sleep</span><span class="hljs-params">(<span class="hljs-keyword">void</span>)</span></span>;
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">sensor_write</span><span class="hljs-params">(<span class="hljs-keyword">uint8_t</span> reg, <span class="hljs-keyword">uint8_t</span> value)</span></span>; <span class="hljs-comment">// Assumption : 8 bit register address and 8 bit data value</span>
</code></pre>
<p>The actual implementation might involve:</p>
<ul>
<li><p>Register definitions from the peripheral’s datasheet</p>
</li>
<li><p>Bit manipulations for control and status registers</p>
</li>
<li><p>Interrupt Service Routines (ISRs)</p>
</li>
<li><p>Timing and delay management</p>
</li>
</ul>
<h3 id="heading-platform-abstraction-why-it-matters">Platform Abstraction: Why It Matters</h3>
<p>One of the most important principles in driver design is decoupling the application from the platform. This makes your code easier to:</p>
<ul>
<li><p>Port to different MCUs</p>
</li>
<li><p>Adapt for similar hardware (for example, different sensor models)</p>
</li>
<li><p>Test across simulated or real environments</p>
</li>
</ul>
<h4 id="heading-platform-agnostic-design-example-in-c">Platform-Agnostic Design Example (in C++) :</h4>
<p>Let’s say you're writing a driver for a temperature sensor:</p>
<pre><code class="lang-cpp"><span class="hljs-comment">// Abstracts the HW platform on which the sensor driver is being written</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TemperatureSensorPlatform</span> {</span>
<span class="hljs-keyword">public</span>:
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">i2cInit</span><span class="hljs-params">(<span class="hljs-keyword">void</span>)</span></span>;
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">i2cWrite</span><span class="hljs-params">(<span class="hljs-keyword">uint8_t</span> reg, <span class="hljs-keyword">uint8_t</span> value)</span></span>;
    <span class="hljs-function"><span class="hljs-keyword">uint8_t</span> <span class="hljs-title">i2cRead</span><span class="hljs-params">(<span class="hljs-keyword">uint8_t</span> reg)</span></span>;
};

<span class="hljs-comment">// Creates a generic Temperature sensor driver interface</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TemperatureSensor</span> {</span>
<span class="hljs-keyword">public</span>:
    <span class="hljs-function"><span class="hljs-keyword">virtual</span> <span class="hljs-keyword">void</span> <span class="hljs-title">init</span><span class="hljs-params">()</span> </span>= <span class="hljs-number">0</span>;
    <span class="hljs-function"><span class="hljs-keyword">virtual</span> <span class="hljs-keyword">float</span> <span class="hljs-title">read</span><span class="hljs-params">()</span> </span>= <span class="hljs-number">0</span>;
    <span class="hljs-function"><span class="hljs-keyword">virtual</span> <span class="hljs-keyword">void</span> <span class="hljs-title">sleep</span><span class="hljs-params">()</span> </span>= <span class="hljs-number">0</span>;
};
</code></pre>
<p>You can implement this interface differently for a specific type of temperature sensor and also add the platform support for the HW platform you are writing the driver on for example STM32.</p>
<pre><code class="lang-cpp"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TempSensorTMP117</span> :</span> <span class="hljs-keyword">public</span> TemperatureSensor {
<span class="hljs-keyword">public</span>:

    TempSensorTMP117(TemperatureSensorPlatform platform) : 
    _platform(platform)
    TemperatureSensor()
    {}

    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">init</span><span class="hljs-params">()</span> <span class="hljs-keyword">override</span> </span>{
        <span class="hljs-comment">// TMP117-specific register configuration</span>
    }

    <span class="hljs-function"><span class="hljs-keyword">float</span> <span class="hljs-title">read</span><span class="hljs-params">()</span> <span class="hljs-keyword">override</span> </span>{
        <span class="hljs-comment">// Read ADC value and convert</span>
        <span class="hljs-keyword">return</span> <span class="hljs-number">25.4f</span>;
    }

    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">sleep</span><span class="hljs-params">()</span> <span class="hljs-keyword">override</span> </span>{
        <span class="hljs-comment">// Put sensor in low-power mode</span>
    }
<span class="hljs-keyword">private</span>:
    TemperatureSensorPlatform _platform; <span class="hljs-comment">// Implements the I2C driver for STM32</span>
};
</code></pre>
<p>Your application code now depends on the <code>TemperatureSensor</code> interface and Temperature Sensor Platform passed in the constructor making it portable and testable across temperature sensors and HW platforms.</p>
<p>One of my previous <a target="_blank" href="https://www.freecodecamp.org/news/connect-read-process-sensor-data-on-microcontrollers-for-beginners/">articles</a> provides details on how to interface a sensor and how to design a driver for it.</p>
<p>Designing robust and modular drivers helps your firmware interact seamlessly with hardware, but in today’s connected world, that’s only part of the challenge. As embedded devices increasingly communicate with other systems, security becomes just as critical as functionality.</p>
<p>Now that we’ve covered how to interface with hardware, let’s explore how to protect those systems from unauthorized access, tampering, and data breaches.</p>
<h2 id="heading-security-in-embedded-systems">Security in Embedded Systems</h2>
<p>Security is often overlooked in embedded development but it shouldn’t be. Embedded systems are increasingly connected to networks, cloud services, or other devices, which makes them vulnerable to attacks like unauthorized access, firmware tampering, or data leaks.</p>
<p>Even simple devices like smart plugs or fitness trackers can be exploited if their firmware is insecure.</p>
<h3 id="heading-key-security-practices">Key Security Practices</h3>
<ul>
<li><p><strong>Secure Boot:</strong> Ensure the firmware is cryptographically signed and verified before execution. This prevents unauthorized firmware from running.</p>
</li>
<li><p><strong>Firmware Update Integrity:</strong> Use encrypted or signed updates, especially for Over-the-Air (OTA) upgrades. Unprotected updates can be a major attack vector.</p>
</li>
<li><p><strong>Lock Debug Interfaces:</strong> After flashing the final firmware, disable or lock access to JTAG, SWD, or UART debug ports to prevent reverse engineering.</p>
</li>
<li><p><strong>Minimal Exposure:</strong> Disable unused peripherals (for example, Bluetooth, USB, network interfaces) and avoid exposing debug info (like UART prints) in production.</p>
</li>
<li><p><strong>Watchdog Timers:</strong> While not security features per se, watchdogs help ensure system recovery in the event of unexpected software behavior – which could result from attacks or bugs.</p>
</li>
</ul>
<p>Security should be layered, as no single mechanism is sufficient on its own. Build security into every stage of the development process, from boot to communication to update handling.</p>
<p>Whether you're designing a consumer product or an industrial controller, proactive security practices are essential for protecting user data, system reliability, and device reputation.</p>
<p>This resource provides a good understanding of Embedded Systems Security: <a target="_blank" href="https://blackberry.qnx.com/en/ultimate-guides/embedded-system-security">BlackBerry QNX: Embedded System Security Guide</a></p>
<h2 id="heading-debugging-and-forensics-in-embedded-systems">Debugging and Forensics in Embedded Systems</h2>
<p>Debugging embedded systems is one of the most challenging and fascinating aspects of development. Unlike in desktop or web applications, bugs in embedded systems often manifest as unexpected hardware behavior rather than error messages.</p>
<p>For example, suppose your code is supposed to blink an LED once per second:</p>
<ul>
<li><p>If the LED stays on, your delay code might be broken.</p>
</li>
<li><p>If it blinks erratically, you might have a timing bug.</p>
</li>
<li><p>If it doesn’t blink at all, you might never be reaching that part of your code or the hardware might not be configured correctly.</p>
</li>
</ul>
<h3 id="heading-why-debugging-is-critical">Why Debugging is Critical</h3>
<p>Embedded systems directly control real-world hardware, often in critical or safety-sensitive environments. A small bug can lead to large consequences.</p>
<p>Historical Note: During the Apollo 11 moon landing, the onboard computer started throwing alarms due to a task overflow. The system restarted and was able to recover itself and allowing the mission to continue safely.</p>
<p>Debugging and post-mortem analysis (forensics) are essential skills for embedded developers.</p>
<h3 id="heading-common-debugging-tools-and-techniques">Common Debugging Tools and Techniques</h3>
<h4 id="heading-1-print-statements-uart-logging">1. Print Statements (UART Logging)</h4>
<p>The simplest and most common method. They send debug messages over a serial connection (UART).</p>
<p>You can use <code>printf()</code> or similar to track variable values, function entries/exits, and system state</p>
<ul>
<li><p>Pros: Easy to implement</p>
</li>
<li><p>Cons: Can affect timing – not usable if UART is unavailable or disabled</p>
</li>
</ul>
<h4 id="heading-2-trace-variables">2. Trace Variables</h4>
<p>In systems without output peripherals (like UART), you can use trace flags, setting bits in a global variable to indicate code progress.</p>
<pre><code class="lang-c"><span class="hljs-keyword">uint32_t</span> trace_flags = <span class="hljs-number">0</span>;

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">init_sensor</span><span class="hljs-params">()</span> 
</span>{
    trace_flags |= (<span class="hljs-number">1</span> &lt;&lt; <span class="hljs-number">0</span>); <span class="hljs-comment">// Bit 0: sensor init started</span>
    <span class="hljs-comment">// ...</span>
    trace_flags |= (<span class="hljs-number">1</span> &lt;&lt; <span class="hljs-number">1</span>); <span class="hljs-comment">// Bit 1: sensor init complete</span>
}
</code></pre>
<p>You can then examine <code>trace_flags</code> in memory to track execution flow, even post-mortem. The trace flags can be printed out or dumped via lldb or gdb.</p>
<p><strong>3. Hardware Debugging: JTAG, SWD, and Debuggers</strong></p>
<p>Modern microcontrollers (like ARM Cortex-Ms) support hardware debugging interfaces such as:</p>
<ul>
<li><p>JTAG (Joint Test Action Group)</p>
</li>
<li><p>SWD (Serial Wire Debug)</p>
</li>
</ul>
<p>These allow a debugger to:</p>
<ul>
<li><p>Pause execution</p>
</li>
<li><p>Set breakpoints</p>
</li>
<li><p>Inspect and modify memory</p>
</li>
<li><p>Single-step through code</p>
</li>
</ul>
<p><a target="_blank" href="https://developer.arm.com/documentation/102520/0100">ARM CoreSight</a> is a debug and trace architecture developed by ARM for its processor cores (like Cortex-M, Cortex-A, Cortex-R). It provides a set of hardware modules built into ARM-based chips that allow developers to:</p>
<ul>
<li><p>Debug the system while it's running (non-intrusively)</p>
</li>
<li><p>Trace code execution, memory accesses, and peripheral activity</p>
</li>
<li><p>Analyze system performance and find hard-to-catch bugs</p>
</li>
</ul>
<p>In short: CoreSight lets you look inside your embedded system while it's alive and working, without halting it unnecessarily.</p>
<h3 id="heading-why-coresight-exists">Why CoreSight Exists</h3>
<p>Traditional debugging tools (like breakpoints or single-stepping with JTAG) are often intrusive (they pause the system), limited (can't capture what happened right before a crash), or not suitable for real-time systems.</p>
<p>CoreSight solves these by enabling real-time tracing and non-intrusive observation of what's happening inside the chip.</p>
<h4 id="heading-popular-debug-tools">Popular Debug Tools:</h4>
<ul>
<li><p>ST-Link – HW from STMicrocontrollers</p>
</li>
<li><p>J-Link – Universal debugger supporting a wide range of MCUs</p>
</li>
<li><p>OpenOCD – Open-source interface for hardware debugging</p>
</li>
<li><p>GDB / LLDB – Command-line debuggers used alongside the above</p>
</li>
</ul>
<p>Single-stepping is most effective when compiler optimizations are off. With optimization, code might be reordered, inlined, or even eliminated.</p>
<h3 id="heading-4-using-map-and-disassembly-files">4. Using Map and Disassembly Files</h3>
<p>When debugging complex issues, especially crashes or memory overflows, you'll need to go deeper.</p>
<p>Map Files show the layout of functions and variables in memory (Flash and RAM). They help you locate:</p>
<ul>
<li><p>Stack overflows</p>
</li>
<li><p>Unexpected memory usage</p>
</li>
<li><p>Function addresses</p>
</li>
</ul>
<p>Disassembly Files let you see the machine code generated from your source. This is critical when:</p>
<ul>
<li><p>Code is heavily optimized</p>
</li>
<li><p>You’re diagnosing instruction-level failures</p>
</li>
<li><p>You’re working without source code (e.g., binary-only drivers)</p>
</li>
</ul>
<p>This resource provides a good overview on Map files, linkers and ELF format: <a target="_blank" href="https://www.tenouk.com/ModuleW.html">Tenouk’s ELF/Map/Linker Guide</a></p>
<h3 id="heading-common-bug-buffer-overflows">Common Bug: Buffer Overflows</h3>
<p>Buffer overflows are one of the most frequent (and dangerous) issues in embedded systems. They happen when data is written past the end of an allocated array, overwriting nearby memory and causing unpredictable behavior.</p>
<p>Symptoms:</p>
<ul>
<li><p>Code crashes mysteriously</p>
</li>
<li><p>Data appears to “corrupt itself”</p>
</li>
<li><p>Variables change value without explanation</p>
</li>
</ul>
<p>You can learn more in my article on <a target="_blank" href="https://www.freecodecamp.org/news/how-to-debug-and-prevent-buffer-overflows-in-embedded-systems/">Debugging Buffer Overflows</a>, which walks through ways to debug a buffer overflow and build robust buffer code.</p>
<h3 id="heading-embedded-forensics">Embedded Forensics</h3>
<p>Sometimes, a device fails in the field, where you can’t attach a debugger. That’s where forensics comes in:</p>
<ul>
<li><p>Use watchdog timers to reset the system and log failure info</p>
</li>
<li><p>Save crash signatures to non-volatile memory (for example, EEPROM, Flash)</p>
</li>
<li><p>Implement assert handlers that log file names, line numbers, or fault types</p>
</li>
</ul>
<p>These techniques help you reconstruct what went wrong after the device has rebooted or been recovered.</p>
<p>You can learn more here: <a target="_blank" href="https://medium.com/@lanceharvieruntime/debugging-techniques-for-embedded-systems-94d00582074a">Debugging Techniques for Embedded Systems – Medium</a>.</p>
<p>Debugging and forensics are invaluable when something goes wrong – but a robust system should aim to catch issues before they reach deployment.</p>
<p>That’s where automated testing becomes essential. With embedded software increasingly powering critical applications, the ability to run consistent, repeatable tests across hardware configurations saves time, improves reliability, and enables faster development cycles.</p>
<p>Next, let’s explore how embedded testing works, the challenges unique to hardware, and how automation frameworks help streamline validation.</p>
<h2 id="heading-automation-and-testing-in-embedded-systems">Automation and Testing in Embedded Systems</h2>
<p>Like all other areas of software engineering, testing is essential in embedded systems. But testing embedded software comes with its own set of challenges, mainly because it interacts with hardware.</p>
<p>Manual testing can be time-consuming and resource-intensive, especially when tests need to be repeated for multiple firmware versions or configurations. That’s where automated testing becomes invaluable.</p>
<h3 id="heading-why-automated-testing">Why Automated Testing?</h3>
<p>Automated testing helps:</p>
<ul>
<li><p>Catch regressions early</p>
</li>
<li><p>Test edge cases consistently</p>
</li>
<li><p>Reduce human error</p>
</li>
<li><p>Scale testing across versions and hardware setups</p>
</li>
</ul>
<p>But automating tests for embedded systems isn’t just writing test cases – it’s about setting up an infrastructure that connects your code to the physical hardware under test.</p>
<h3 id="heading-test-architecture-host-dut">Test Architecture: Host + DUT</h3>
<p>Most embedded test setups involve two components:</p>
<ul>
<li><p>Host: Your development PC or CI test controller, which sends test commands and receives data.</p>
</li>
<li><p>DUT (Device Under Test): The microcontroller board or embedded system running the firmware.</p>
</li>
</ul>
<p>These two communicate over a physical link, commonly USB, UART, or FTDI, which carries commands and test data between them.</p>
<h4 id="heading-diagram-suggested-structure">Diagram (suggested structure)</h4>
<p>You could visualize this as:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749953253453/4a94ae37-dd17-4be1-aece-d1c2bee0248d.png" alt="Describes the flow of automation, Automation Manager on the host that takes CSV and Config Files and is the control center of Automation. Automation Manager on the DUT helps parse commands coming from host and provide replies to the host, the automation manager on the DUT will forward queries to different modules in the DUT for actions and queries. The communication protocol between Host and DUT is over USB or UART over FTDI" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-key-components-of-embedded-test-automation">Key Components of Embedded Test Automation</h3>
<h4 id="heading-1-file-management">1. <strong>File Management</strong></h4>
<p>Many automated tests rely on <strong>CSV or JSON files</strong> to define:</p>
<ul>
<li><p>Input configurations</p>
</li>
<li><p>Expected outputs</p>
</li>
<li><p>Test parameters</p>
</li>
</ul>
<p>Python makes it easy to:</p>
<ul>
<li><p>Read input vectors from CSVs</p>
</li>
<li><p>Write logs or pass/fail results</p>
</li>
<li><p>Parse structured data</p>
</li>
</ul>
<h4 id="heading-2-data-communication">2. <strong>Data Communication</strong></h4>
<p>Maintaining a stable and reliable link between the Host and DUT is critical. This includes:</p>
<ul>
<li><p>Opening and managing UART or USB connections (for example, with <code>pyserial</code>)</p>
</li>
<li><p>Framing test commands using opcodes or simple protocols</p>
</li>
<li><p>Handling timeouts, retries, and error recovery</p>
</li>
</ul>
<h5 id="heading-example-python-with-pyserial">Example (Python with PySerial):</h5>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> serial

ser = serial.Serial(<span class="hljs-string">'/dev/ttyUSB0'</span>, <span class="hljs-number">115200</span>) <span class="hljs-comment">#set Baud rate</span>
ser.write(<span class="hljs-string">b'\x01'</span>)  <span class="hljs-comment"># Send opcode for "start test"</span>
response = ser.read(<span class="hljs-number">64</span>)  <span class="hljs-comment"># Read 64 bytes of response</span>
</code></pre>
<h4 id="heading-3-automation-manager-dut-side">3. <strong>Automation Manager (DUT-side)</strong></h4>
<p>A lightweight software agent runs on the embedded device. Its responsibilities:</p>
<ul>
<li><p>Parse incoming commands</p>
</li>
<li><p>Trigger specific test routines</p>
</li>
<li><p>Send response data back to the host</p>
</li>
</ul>
<p>This is often implemented using a <code>switch-case</code> structure in <code>C</code> or <code>C++</code>:</p>
<pre><code class="lang-c"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">automation_manager</span><span class="hljs-params">(<span class="hljs-keyword">uint8_t</span> opcode)</span> </span>{
    <span class="hljs-keyword">switch</span>(opcode) {
        <span class="hljs-keyword">case</span> <span class="hljs-number">0x01</span>: run_sensor_test(); <span class="hljs-keyword">break</span>;
        <span class="hljs-keyword">case</span> <span class="hljs-number">0x02</span>: run_motor_test(); <span class="hljs-keyword">break</span>;
        <span class="hljs-keyword">default</span>: <span class="hljs-keyword">break</span>;
    }
}
</code></pre>
<h4 id="heading-4-automation-manager-host-side">4. <strong>Automation Manager (Host-side)</strong></h4>
<p>This is the control center of your test workflow:</p>
<ul>
<li><p>Sends test commands and parameters to the DUT</p>
</li>
<li><p>Waits for and logs results</p>
</li>
<li><p>Compares responses to expected output</p>
</li>
<li><p>Handles communication retries or failures</p>
</li>
</ul>
<p>Often written in Python using:</p>
<ul>
<li><p><code>pyserial</code> for communication</p>
</li>
<li><p><code>pandas</code> for file/data processing</p>
</li>
<li><p><code>unittest</code> or <code>pytest</code> for test structure</p>
</li>
</ul>
<h3 id="heading-tips-for-effective-automation">Tips for Effective Automation</h3>
<ul>
<li><p>Use unique opcodes for each test command to avoid ambiguity</p>
</li>
<li><p>Implement timeout handling to avoid hanging scripts</p>
</li>
<li><p>Log everything, responses, errors, test timestamps</p>
</li>
<li><p>Use versioned test input files to track changes over time</p>
</li>
<li><p>Include self-tests on the DUT to validate hardware state before running full tests</p>
</li>
</ul>
<p>Automated testing in embedded systems is not just about running scripts, it's about building a bridge between your host PC and your device, managing the flow of commands and data, and ensuring tests are consistent, repeatable, and reliable.</p>
<p>While this requires effort to set up, the payoff is huge: confidence in your firmware, faster development cycles, and reduced risk of bugs making it into production.</p>
<h2 id="heading-where-to-go-from-here">Where to Go from Here</h2>
<h3 id="heading-building-your-embedded-project">Building your Embedded Project</h3>
<p>After exploring the theory and tooling of embedded systems, it's time to apply what you've learned. This section walks you through the steps to create your own embedded system – from concept to code and deployment.</p>
<p>Use the checklist below to guide your first project, whether you're prototyping a sensor device or automating a simple process.</p>
<h4 id="heading-project-setup-checklist">Project Setup Checklist:</h4>
<ol>
<li><p><strong>Define the Goal</strong></p>
<ul>
<li><p>What task does the system perform?</p>
</li>
<li><p>Identify inputs (for example, temperature sensor) and outputs (for example, relay or LED).</p>
</li>
</ul>
</li>
<li><p><strong>Requirements Gathering</strong></p>
<ul>
<li><p>Functional: What features must it support?</p>
</li>
<li><p>Non-functional: Memory limits, real-time behavior, power constraints.</p>
</li>
<li><p>Any security or safety-critical elements?</p>
</li>
</ul>
</li>
<li><p><strong>Choose Your Hardware</strong></p>
<ul>
<li><p>Microcontroller (for example, STM32F4)</p>
</li>
<li><p>Sensors and actuators</p>
</li>
<li><p>Communication interfaces (UART, I2C, SPI, and so on)</p>
</li>
</ul>
</li>
<li><p><strong>Software Architecture</strong></p>
<ul>
<li><p>Bare-metal, RTOS, or embedded OS?</p>
</li>
<li><p>Driver abstraction: will you use HAL or custom low-level code?</p>
</li>
<li><p>Organize code into layers: application logic, drivers, hardware init.</p>
</li>
</ul>
</li>
<li><p><strong>Toolchain Setup</strong></p>
<ul>
<li><p>Install GCC toolchain (for example, <code>arm-none-eabi-gcc</code>)</p>
</li>
<li><p>Configure Makefile and linker script</p>
</li>
<li><p>Set up debugger and flashing tools (for example, OpenOCD, ST-Link)</p>
</li>
</ul>
</li>
<li><p><strong>Firmware Implementation</strong></p>
<ul>
<li><p>Initialize peripherals</p>
</li>
<li><p>Implement control logic inside <code>main()</code> or tasks</p>
</li>
<li><p>Use interrupts or timers for responsiveness</p>
</li>
</ul>
</li>
<li><p><strong>Flashing and Initial Tests</strong></p>
<ul>
<li><p>Use OpenOCD or ST-Link to flash the binary</p>
</li>
<li><p>Test peripheral behavior and debug with UART or GDB</p>
</li>
</ul>
</li>
<li><p><strong>Debug and Profile</strong></p>
<ul>
<li><p>Use JTAG/SWD, CoreSight, and trace logs</p>
</li>
<li><p>Check memory layout with map/disassembly files</p>
</li>
<li><p>Identify bottlenecks and edge cases</p>
</li>
</ul>
</li>
<li><p><strong>Security Hardening</strong></p>
<ul>
<li><p>Disable debug interfaces post-flash</p>
</li>
<li><p>Add firmware signing and secure boot</p>
</li>
<li><p>Minimize surface area: disable unused features</p>
</li>
</ul>
</li>
<li><p><strong>Testing and Automation</strong></p>
</li>
</ol>
<ul>
<li><p>Connect Host to DUT via UART/USB</p>
</li>
<li><p>Use Python + PySerial to send test vectors</p>
</li>
<li><p>Log, compare, and report test outcomes</p>
</li>
</ul>
<p>Embedded firmware development is a deep and rewarding field where software meets the hardware. Whether you're controlling an LED, reading from a sensor, or orchestrating multiple tasks in real time, the embedded stack teaches you how hardware, software, timing, and efficiency all come together.</p>
<h2 id="heading-summary">Summary:</h2>
<p>In this guide, we walked through the essential building blocks at a high level:</p>
<ul>
<li><p>What embedded systems are, and how they sense → process → act</p>
</li>
<li><p>How microcontrollers work, from memory layout to interrupts and protocols</p>
</li>
<li><p>How to design robust, scalable embedded software with clean architecture</p>
</li>
<li><p>When to choose bare-metal, RTOS, or full OS solutions</p>
</li>
<li><p>How to build drivers, write modular code, and interface with peripherals</p>
</li>
<li><p>Tools for debugging, tracing, and analyzing system behavior</p>
</li>
<li><p>Strategies for automating embedded testing using Python and host-device communication</p>
</li>
<li><p>And finally, why security matters, especially in a connected world</p>
</li>
</ul>
<p>Whether you're preparing for embedded job interviews, building your own IoT projects, or just exploring how software drives real-world systems, this article gives you a launchpad for deeper learning.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Connect, Read, and Process Sensor Data on Microcontrollers – A Beginner's Guide ]]>
                </title>
                <description>
                    <![CDATA[ In today’s world, computers are ubiquitous and generally serve two primary purposes. The first is general-purpose computing, where they handle a wide range of tasks, including running diverse applications and programs. Examples include laptops, deskt... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/connect-read-process-sensor-data-on-microcontrollers-for-beginners/</link>
                <guid isPermaLink="false">67d45997c9e7f2d42bb1c540</guid>
                
                    <category>
                        <![CDATA[ embedded systems ]]>
                    </category>
                
                    <category>
                        <![CDATA[ microcontroller ]]>
                    </category>
                
                    <category>
                        <![CDATA[ embedded software ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ADC ]]>
                    </category>
                
                    <category>
                        <![CDATA[ I2C ]]>
                    </category>
                
                    <category>
                        <![CDATA[ real-time data processing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Signal Processing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ sensors ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Electronics ]]>
                    </category>
                
                    <category>
                        <![CDATA[ hardware ]]>
                    </category>
                
                    <category>
                        <![CDATA[ electrical engineering ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software architecture ]]>
                    </category>
                
                    <category>
                        <![CDATA[ MathJax ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Soham Banerjee ]]>
                </dc:creator>
                <pubDate>Fri, 14 Mar 2025 16:30:15 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1741902732575/fd41a2d5-ed4f-445d-b186-936625837c8d.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In today’s world, computers are ubiquitous and generally serve two primary purposes.</p>
<p>The first is general-purpose computing, where they handle a wide range of tasks, including running diverse applications and programs. Examples include laptops, desktops, servers, and supercomputers.</p>
<p>The second is embedded systems, which are specialized computers designed for specific functions. Commonly found in devices such as thermostats, refrigerators, cars, and other smart appliances, they rely on sensors to collect environmental data and execute their tasks efficiently.</p>
<h3 id="heading-the-role-of-sensors"><strong>The Role of Sensors</strong></h3>
<p>Sensors play a critical role in both types of computing. In embedded systems, sensors gather environmental data to help devices like autonomous vehicles, home appliances, and industrial machines perform tasks. In general-purpose computers, sensors primarily monitor internal conditions such as temperature and voltage, ensuring safe operation and preventing issues like overheating or electrical faults.</p>
<p>As Artificial Intelligence (AI) and the Internet of Things (IoT) evolve, sensors have become indispensable for gathering real-world data to support intelligent decision-making. Embedded systems leverage sensors to perceive their environment, transforming raw data into actionable insights that power automation and improve efficiency across industries.</p>
<p>This means that understanding sensor interfacing and designing robust sensor-driven software has become a vital skill for engineers and hobbyists alike.</p>
<p>Whether you're a beginner or experienced engineer, this guide will help you build a solid understanding of sensor interfacing software.</p>
<h2 id="heading-what-youll-learn-and-article-scope"><strong>What You’ll Learn and Article Scope</strong></h2>
<p>In this article, you’ll learn how to connect sensors to microcontrollers (MCUs) and design sensor software pipelines that turn raw data into meaningful, usable information. You’ll also explore practical techniques for processing sensor data accurately and efficiently in embedded systems.</p>
<p>Here’s a breakdown of what we’ll cover:</p>
<ul>
<li><p>What sensors are and how they work – An introduction to sensors, common types, and how sensor pipelines help process sensor data.</p>
</li>
<li><p>Key sensor characteristics – Important parameters like sensitivity, accuracy, precision, range, drift, and response time to help you choose the right sensor for your project.</p>
</li>
<li><p>How to interface sensors with microcontrollers – Hardware connections and communication protocols like SPI, I²C, and GPIO that allow microcontrollers to read sensor data.</p>
</li>
<li><p>Software architecture for sensor data – A high-level overview of the software pipeline that processes sensor data, including drivers, ADC support, scaling, calibration, and post-processing.</p>
</li>
<li><p>Detailed design of pipeline components – A closer look at each step in the pipeline, focusing on scaling raw data, calibrating sensors, and applying filters to clean up noisy signals.</p>
</li>
<li><p>Practical tips for power management – Best practices for handling power efficiently using low-power modes, FIFO buffers, and DMA when working with sensor data in embedded systems.</p>
</li>
</ul>
<p>By the end of this article, you’ll know how to design and implement a complete sensor data pipeline for an embedded system, from reading raw sensor data to preparing it for real-world use in intelligent, connected devices.</p>
<p><strong>Note</strong>: Advanced data processing, high-resolution ADCs, and hardware circuit design for sensors are outside the scope of this article.</p>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>To get the most out of this article, you should have:</p>
<ol>
<li><p>Basic knowledge of microcontrollers: Understanding of common peripherals like ADCs (Analog-to-Digital Converters), SPI (Serial Peripheral Interface), I2C (Inter-Integrated Circuit) and GPIO (General Purpose Input/Output). If you’re new to these protocols, <a target="_blank" href="https://www.parlezvoustech.com/en/comparaison-protocoles-communication-i2c-spi-uart/">this article provides a great overview</a>.</p>
</li>
<li><p>Basic knowledge of electronics: Familiarity with circuits and signals, including analog and digital interfaces.</p>
</li>
<li><p>Programming in C: Familiarity in embedded software development, including driver development.</p>
</li>
<li><p>(Optional) Basic knowledge of sensors: Understanding different types of sensors (like temperature, pressure, motion) is helpful but not required.</p>
</li>
</ol>
<p>Also, this article assumes the following:</p>
<ul>
<li><p>You are working with a microcontroller equipped with the peripherals needed for sensor integration. The details of microcontroller peripherals can be found in a <a target="_blank" href="https://pdf.xab3.ro/manual/reference-manual-for-stm32f405415-stm32f407417-stm32f427437-and-stm32f429439-mcus-100">reference manual for example for an STM32F4</a> series microcontroller will have all the details :</p>
</li>
<li><p>You are familiar with compilers, debuggers, and IDEs used in embedded systems. Some common tools include:</p>
<ul>
<li><p>Compilers: <a target="_blank" href="https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads">GCC</a>, <a target="_blank" href="https://developer.arm.com/documentation/dui0773/l/Introducing-the-Toolchain/Toolchain-overview?lang=en">Clang</a>,</p>
</li>
<li><p>Debuggers: <a target="_blank" href="https://sourceware.org/gdb/">GDB</a>, <a target="_blank" href="https://lldb.llvm.org/use/tutorial.html">LLDB</a></p>
</li>
<li><p>IDEs: <a target="_blank" href="https://code.visualstudio.com">Visual Studio Code</a> (VSCode) is a popular choice, especially with extensions for embedded development and debugging.</p>
</li>
</ul>
</li>
<li><p>You aim to build reliable, sensor-driven embedded systems, capable of collecting and processing real-world data efficiently.</p>
</li>
</ul>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-a-sensor-and-sensor-pipeline">What is a Sensor and Sensor Pipeline?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-sensor-characteristics">Sensor Characteristics</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-interface-with-a-microcontroller">How to Interface with a Microcontroller</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-software-architecture">Software Architecture</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-high-level-overview-of-components">High-Level Overview of Components</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-accessing-data-from-the-sensor">Accessing Data from the Sensor</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-sensor-power-management">Sensor Power Management</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-detailed-design-of-components">Detailed Design of Components</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-sensor-driver">1. Sensor Driver</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-adc-support">2. ADC Support</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-scaling">3. Scaling</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-calibration">4. Calibration</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-5-data-post-processing">5. Data Post-Processing</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-a-sensor-and-sensor-pipeline"><strong>What is a Sensor and Sensor Pipeline?</strong></h2>
<p>A sensor detects changes in physical properties such as temperature, pressure, or light and converts them into electrical signals that can be measured or interpreted. For example, a thermistor is a type of resistor whose resistance changes with temperature. As the temperature varies, the resistance of the thermistor changes, altering the voltage across it. The system then interprets this voltage change to determine the temperature.</p>
<p>To better understand sensors, consider the natural sensors in the human body: the eyes, ears, skin, nose, and tongue. These natural sensors constantly send signals about the environment to the brain for processing. Different regions of the brain interpret these signals and use the information to drive actions and responses. Just like the brain processes signals from natural sensors, a microcontroller processes signals from electronic sensors using a sensor pipeline.</p>
<p>Sensors come in many types, each designed to detect specific physical properties. Some sensors have a sensing element that changes its properties in response to conditions like heat, light, or pressure. Examples include thermistors, infrared receivers, and photodiodes.</p>
<p>For detecting movement, such as acceleration and rotation, MEMS (Microelectromechanical Systems) sensors—like accelerometers and gyroscopes—are widely used.</p>
<p>To measure distance, sensors like sonars, ultrasonic sensors, and radars are common. These are just a few examples of the many types of sensors available.</p>
<p>Beyond the types of physical properties they detect, sensors also differ in their levels of integration. Some sensors are raw sensors, consisting only of a sensing element and a transducer with simple leads for direct connection to an external circuit.</p>
<p>Others, known as smart sensors, include additional components such as an ADC (analog-to-digital converter) and onboard processing capabilities, enabling them to handle more of the data processing independently.</p>
<p>The choice between a raw sensor and a smart sensor depends on your application requirements, including factors like cost, size, and the processing load on the interfacing microcontroller.</p>
<p>Returning to our human analogy, consider how vision works as a sensor pipeline. When light enters our eyes, photoreceptor cells (rods and cones) in the retina act as sensing elements, converting the light into electrical signals. These signals travel via the optic nerve to the brain’s visual cortex, where they undergo processing to form a recognizable image. The brain then interprets this information and initiates a response, like smiling when you see a beautiful scenery.</p>
<p>Similarly, a sensor pipeline for an embedded system can be defined as shown in the picture below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738828676916/75137176-c9ba-432d-bf44-bb3da093e18d.png" alt="Figure 1: A Sensor Pipeline showing analogue to digital conversion, calibration, filtering, and then processing." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Each of these steps may have different requirements based on the application. Creating a requirements document for the sensor is helpful when selecting the appropriate sensor and configuring the pipeline.</p>
<h2 id="heading-sensor-characteristics"><strong>Sensor Characteristics</strong></h2>
<p>Before you dive into the blocks of the sensor pipeline, let’s review some important characteristics of a sensor.</p>
<h3 id="heading-sensitivity"><strong>Sensitivity</strong></h3>
<p>Sensitivity is the ability of a sensor to detect small changes in the physical property it’s designed to measure.</p>
<p>Sensitivity can vary based on factors like manufacturing processes, cost, and the design of the sensing element.</p>
<p>Sensors designed for a specific property often come in different sensitivity levels, allowing users to select an appropriate sensitivity based on the application requirements.</p>
<h3 id="heading-accuracy"><strong>Accuracy</strong></h3>
<p>Accuracy is the degree to which a sensor’s measurement matches the true value of the physical property it’s measuring. Testing a sensor’s accuracy typically requires comparing its readings to those of a reference instrument.</p>
<p>A sensor may have gain and offset errors—issues that calibration can help correct. Calibration adjusts for these systematic errors, which are often due to manufacturing tolerances or design factors.</p>
<p>Once calibrated, the sensor’s output can be verified against a reference to confirm its accuracy. The required level of accuracy should be determined based on the application’s needs.</p>
<h3 id="heading-precision"><strong>Precision</strong></h3>
<p>Precision refers to the consistency or repeatability of a sensor's measurements, regardless of how close those measurements are to the true value. It indicates the sensor's ability to produce the same output under identical conditions and how finely it can resolve and report values.</p>
<p>For example, if the true temperature of an object is 12.53°C:</p>
<ul>
<li><p>A precise sensor will consistently measure values like 12.52°C, 12.53°C, or 12.54°C, even if those values are slightly offset from the true temperature.</p>
</li>
<li><p>A highly accurate sensor, on the other hand, will measure values close to 12.53°C but may lack precision if those readings vary widely (e.g., 12.50°C, 12.53°C, and 12.56°C).</p>
</li>
</ul>
<p>For applications requiring exact measurements, a sensor with both high accuracy (closeness to the true value) and high precision (low variability) is essential. This is especially important in distinguishing small differences, such as between 12.5°C and 12.53°C.</p>
<p>In contrast, applications with less stringent requirements might use sensors with broader tolerances, such as ±1°C, which are sufficient for general monitoring purposes.</p>
<h3 id="heading-range"><strong>Range</strong></h3>
<p>The range of a sensor refers to the span between the maximum and minimum values of the physical property it can measure while maintaining its specified precision and accuracy. A sensor's operating range may extend beyond its measurement range, but the measurement range defines the limits within which the sensor reliably adheres to its specified sensitivity, accuracy, and response time.</p>
<h3 id="heading-drift"><strong>Drift</strong></h3>
<p>Drift is when a sensor's output changes over time due to conditions like temperature or humidity. Components within the sensor, including the sensing element, may be sensitive to these conditions, leading to gradual shifts in measurements.</p>
<p>For example, many components are affected by temperature and humidity changes, which can alter sensor readings. Also, sensors with internal oscillators may experience time-based drift, impacting accuracy.</p>
<p>Regular calibration with an accurate external reference (such as a precise clock) can help correct for drift and maintain reliable measurements. For certain applications, selecting a sensor with acceptable drift characteristics is crucial.</p>
<h3 id="heading-response-time"><strong>Response Time</strong></h3>
<p>Response time is the duration a sensor takes to detect and reflect a change in the measured physical property. For example, if the temperature rises by 5°C, the response time indicates how long the temperature sensor takes to reflect this change in its output.</p>
<p>Response time depends on the sensor’s design, manufacturing quality, and internal components, such as the ADC (Analog-to-Digital Converter), averaging circuits, and filters within the sensor pipeline.</p>
<p>All the parameters mentioned above are thoroughly documented in the sensor’s data-sheet. In practice, it’s a good idea to create a sensor requirements document for each specific application, detailing these key parameters as a baseline for sensor selection.</p>
<p>Now that you’ve examined the key characteristics of sensors, let’s explore how you can connect them to a microcontroller for real-world applications.</p>
<h2 id="heading-how-to-interface-with-a-microcontroller"><strong>How to Interface with a Microcontroller</strong></h2>
<h3 id="heading-choosing-a-communication-protocol">Choosing a Communication Protocol</h3>
<p>Another essential aspect of sensor requirements is specifying the communication interface between the sensor and the MCU or processor in the system. It’s important to understand how the sensor will be interfaced based on its output signal type and the available pins on the microcontroller.</p>
<p>For instance, certain sensors may connect directly to an analog or digital input pin on a microcontroller. A raw sensor, such as a temperature sensor, typically connects to an analog input pin, which is then read by the microcontroller’s internal ADC (Analog-to-Digital Converter).</p>
<p>In contrast, a digital-output sensor connects to a digital GPIO (General Purpose Input/Output) pin. For instance, speed sensors generate square waves with variable pulse widths to indicate speed. These signals are usually connected to a GPIO pin configured as an external interrupt or timer capture input, allowing the microcontroller to measure pulse width accurately.</p>
<p>A smart sensor, on the other hand, often supports communication protocols like SPI (Serial Peripheral Interface) or I2C (Inter-Integrated Circuit). These interfaces enable the microcontroller to configure the sensor, check its status, and retrieve data through register reads and writes.</p>
<p>Choosing the appropriate communication protocol for interfacing a sensor depends on the available pins in the system and the specific requirements of the application.</p>
<p><strong>Tip</strong>: When working with protocols like I²C or SPI, using tools such as <a target="_blank" href="https://www.saleae.com">Saleae</a> logic analyzers can greatly simplify debugging and validation. Logic analyzers capture and visualize communication signals, and tools like Saleae offer built-in protocol interpreters to help you decode sensor communication in real time. This can be especially helpful when troubleshooting configuration issues, timing problems, or communication errors during sensor interfacing.</p>
<p>Figure 2 below shows an example of a microcontroller connected to 4 sensors having different interfaces.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738828730915/25e62db6-a583-427a-bd77-c61c33990cdf.png" alt="Figure 2: A microcontroller interfacing with different sensors using different communication interfaces." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-determining-power-requirements">Determining Power Requirements</h3>
<p>Power requirements are another key consideration when interfacing a sensor. Sensors may operate at different voltages (for example, 3.3V or 5V), so ensuring the microcontroller can accommodate these levels is essential. Level converters can bridge voltage mismatches, ensuring compatibility between the sensor and microcontroller voltage levels.</p>
<p>Timing and sampling requirements must also be evaluated, especially for sensors generating high-frequency data. Configuring external interrupts on GPIO pins can ensure timely data capture, while techniques like using DMA can streamline data transfer for sensors sampling at high frequencies without CPU involvement.</p>
<p>Now that you’ve learned about communication protocols and hardware connections, let’s focus on designing the software architecture that acquires, processes, and prepares sensor data for use. Designing effective software is crucial for obtaining clean, reliable data from the sensor.</p>
<h2 id="heading-software-architecture"><strong>Software Architecture</strong></h2>
<p>Now that we’ve chosen the sensor and communication protocol, let’s design the software architecture for the sensor pipeline. This software runs on the microcontroller connected to the sensor and processes raw data to make it clean and usable.</p>
<p>While application-level data processing is beyond the scope of this article, let’s focus on interfacing with the sensor and preparing the data for application use.</p>
<p>The sensor processing pipeline can be broken into the following components:</p>
<ol>
<li><p>Sensor Driver</p>
</li>
<li><p>Analog-to-Digital Conversion (ADC) Support</p>
</li>
<li><p>Scaling</p>
</li>
<li><p>Calibration</p>
</li>
<li><p>Data Post-Processing</p>
</li>
</ol>
<p>Let’s examine a high-level overview of these components for both smart and raw sensors.</p>
<h3 id="heading-high-level-overview-of-components"><strong>High-Level Overview of Components</strong></h3>
<ol>
<li><p><strong>Sensor Driver</strong></p>
<ol>
<li><p>Smart sensors: The driver configures the sensor, manages power, and handles read and write operations to the sensor registers over a communication protocol like SPI, I2C.</p>
</li>
<li><p>Raw sensors: The driver may only control GPIOs for power management, as raw sensors typically lack registers.</p>
</li>
</ol>
</li>
<li><p><strong>Analog-to-Digital Conversion (ADC) Support</strong></p>
<ol>
<li><p>Smart sensors: Include an onboard ADC, which is configured through the sensor driver.</p>
</li>
<li><p>Raw sensors: Requires an external ADC, an ADC driver implemented in software to configure the ADC, initiate conversions, and retrieve data.</p>
</li>
</ol>
</li>
<li><p><strong>Scaling</strong>: Scaling is necessary for both smart and raw sensors. It converts digital counts after the analog to digital conversion into meaningful physical quantities using formulas provided in the sensor data sheet. For example, a temperature sensor will use a formula to convert digital counts to degree Celsius.</p>
</li>
<li><p><strong>Calibration</strong>: Once the measured physical quantity is obtained, calibration adjusts the value by applying offsets, gains, or both to correct errors. This process ensures the sensor output aligns with reference values across its entire measurement range. A detailed discussion of the calibration process will follow in the next section.</p>
</li>
<li><p><strong>Data Post-Processing</strong>: Post-processing techniques, such as filtering are applied to improve data quality and reduce noise. Common filters such as low-pass or high-pass filters can remove unwanted frequency components.</p>
</li>
</ol>
<h3 id="heading-accessing-data-from-the-sensor"><strong>Accessing Data from the Sensor</strong></h3>
<p>The method of accessing data depends on the whether it’s a raw sensor or a smart sensor. Smart sensors will have onboard ADCs and FIFOs. Before delving into how data is accessed, it’s important to first understand sampling frequency.</p>
<h4 id="heading-sampling-frequency">Sampling Frequency:</h4>
<p>The frequency of taking a measurement from the sensor must follow the <a target="_blank" href="https://www.allaboutcircuits.com/technical-articles/nyquist-shannon-theorem-understanding-sampled-systems/">Nyquist-Shannon sampling theorem</a>. It states that the sampling rate must be twice the highest frequency component of the signal to be measured to accurately reconstruct the measured data.</p>
<p>The sampling frequency defines how often the sensor captures data, which affects how the data is accessed. Depending on whether the sensor is a raw sensor or a smart sensor, the approach to handling this sampled data varies.</p>
<p><strong>Smart Sensors:</strong></p>
<ol>
<li><p>Data register: The sensor writes sampled data directly into a register based on the set sample frequency updated during setup. The microcontroller reads this data register based on a data conversion completion interrupt.</p>
</li>
<li><p>FIFObBuffer: Some sensors include FIFO (First-In, First-Out) buffers to store multiple data points. When enabled, the FIFO updates at the configured sampling frequency and trigger interrupts when it becomes full or reaches a predefined level.<br> The benefits of FIFO include:</p>
<ol>
<li><p>Power efficiency: The MCU can process data in batches, reducing CPU overhead and allowing it to enter low-power mode during data collection.</p>
</li>
<li><p>Sampling and processing rate matching: FIFO buffers help reconcile differences between the sensor’s sampling rate and the MCU’s data processing rate.</p>
</li>
<li><p>For MCUs with Direct Memory Access (DMA), data transfer from the sensor to MCU memory can occur without CPU intervention, further reducing power consumption.</p>
</li>
</ol>
</li>
</ol>
<p><strong>Raw Sensors:</strong></p>
<p>For raw sensors, the MCU triggers ADC conversions at the sampling frequency, often using a timer interrupt. Data is read upon the ADC conversion complete interrupt, allowing the MCU to sleep during conversions and between samples to save power.</p>
<h3 id="heading-sensor-power-management"><strong>Sensor Power Management</strong></h3>
<p>Power management is critical for energy-sensitive applications. Strategies include:</p>
<ol>
<li><p>Low-power modes: Many sensors support low-power modes configurable through sensor registers.</p>
</li>
<li><p>GPIO-controlled power cycling (Duty-Cycling): For sensors without built-in low-power modes, the microcontroller can toggle the sensor’s power line using a GPIO pin, reducing power consumption further. Figure 3 below shows the diagram of a raw temperature sensor whose power is controlled using a GPIO from the MCU. For example, a temperature sensor in sleep mode can be activated only when temperature readings are required.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739042040654/1f2d4bbd-f15a-417a-9c79-3b93384e95bd.png" alt="Figure 3: Raw Temperature Sensor Interfacing a MCU" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The above techniques ensure efficient use of power while maintaining the required data sampling rate and sensor responsiveness.</p>
<p>With the high-level architecture in mind, we’ll now dive into the detailed design of each pipeline component.</p>
<h2 id="heading-detailed-design-of-components"><strong>Detailed Design of Components</strong></h2>
<p>In this section, you’ll delve into the key components of the sensor pipeline outlined in the Software Architecture section.</p>
<h3 id="heading-1-sensor-driver"><strong>1. Sensor Driver</strong></h3>
<p>The sensor driver is responsible for managing communication, configuration, power, and data acquisition for both smart and raw sensors.</p>
<h4 id="heading-smart-sensor-driver">Smart Sensor Driver:</h4>
<ol>
<li><p>Communication driver: Generic I2C or SPI drivers on the MCU can be adapted using wrapper functions to handle sensor-specific requirements, such as 1-byte, 2-byte, or 4-byte transfers.</p>
</li>
<li><p>Configuration: Typical tasks include setting the sampling rate, configuring interrupts, managing FIFO buffers, and, if needed, clock settings.</p>
</li>
<li><p>Power management: APIs should allow higher software layers to transition sensors between power modes by writing to specific registers or controlling GPIO lines for sensors without built-in power modes.</p>
</li>
</ol>
<h4 id="heading-raw-sensor-driver">Raw Sensor Driver:</h4>
<p>For raw sensors, the driver primarily manages power, often through GPIO-controlled toggling.</p>
<h3 id="heading-2-adc-support"><strong>2. ADC Support</strong></h3>
<p>ADC support is required only for raw sensors. In this article, we’re focusing on SAR ADCs, which are commonly embedded in microcontrollers.</p>
<h4 id="heading-how-sar-adcs-work">How SAR ADCs Work?</h4>
<p>A SAR ADC converts an analog signal to a digital value over multiple clock cycles, with the number of cycles equal to its bit resolution (for example, 10 cycles for a 10-bit ADC).</p>
<h4 id="heading-key-terms-related-to-adcs">Key terms related to ADCs:</h4>
<ol>
<li><p>Reference Voltage (VRef): Represents the maximum voltage the ADC can measure. Analog signals exceeding this limit must be scaled down.</p>
</li>
<li><p>Resolution: Determines the smallest detectable voltage change. For example, a 10-bit ADC with a 3.3V VRef has a resolution of 3.22 mV</p>
</li>
</ol>
<p>$$V_{\text{Res}} = V_{\text{Ref}} /2^{10}$$</p><p>The ADC result is stored in a data register, which can then be scaled to meaningful physical units.</p>
<h3 id="heading-3-scaling"><strong>3. Scaling</strong></h3>
<p>Scaling converts ADC counts into meaningful physical values, such as temperature (°C) or acceleration (g) depending on the sensor type. Sensor datasheets typically provide the necessary formulas or lookup tables.</p>
<p>For example, the method to convert a voltage measured by a raw temperature sensor to temperature value is shown below:</p>
<p>$$V_{\text{Measured}} = Counts_{\text{ADC}} / 2^{10} * V_{\text{Ref}} \quad \text{(Get V_Measured from ADC Counts)}$$</p><p>$$Temperature_{\text{Measured}} = V_{\text{Measured}} * T_{\text{C/mV}} \quad \text{(Get Temperature physical value)}$$</p><p>Similarly, a 3-axis accelerometer maps counts on the X, Y, and Z axes to acceleration values in g or milli-g.</p>
<h3 id="heading-4-calibration"><strong>4. Calibration</strong></h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738829686302/bfa643dc-5e01-4b24-b885-b682acdb11cb.png" alt="Figure 4a: Calibration with gain &amp; offset | Figure 4b: Calibration with fixed offset" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The figure above on the left (4a) is showing Calibration with gain and offset, while the figure above on the right (4b) is showing calibration with fixed offset.</p>
<p>$$x_{\text{calibrated}} = Gain * x_{\text{raw}} + Offset \quad \text{(Figure 4a - Linear Calibration)}$$</p><p>$$x_{\text{calibrated}} = x_{\text{raw}} + Offset \quad \text{(Figure 4b - Fixed offset Calibration)}$$</p><p>Calibration ensures the sensor’s output aligns with reference measurements, correcting for errors introduced by design, materials, or manufacturing.</p>
<h4 id="heading-types-of-errors">Types of Errors:</h4>
<ol>
<li><p>Offset error: A constant deviation of the sensor’s output from the true reference value, regardless of input magnitude.</p>
</li>
<li><p>Gain error: A proportional error where the sensor’s output scale deviates from the expected value, causing the output to increase or decrease incorrectly relative to the input.</p>
</li>
</ol>
<h4 id="heading-calibration-methods">Calibration Methods:</h4>
<ol>
<li><p>2/3-Point calibration: This type of calibration may involve either applying a fixed offset to the raw value or applying both gain and offset. Figure 4a illustrates an example of a gain/offset calibration, while Figure 4b depicts offset calibration. In both figures, the y-axis represents the reference value measured by an accurate instrument, while the x-axis represents the raw value measured by the sensor after ADC.</p>
</li>
<li><p>N-Point calibration: Involves multiple points for more complex, non-linear error correction.</p>
</li>
</ol>
<h4 id="heading-implementation">Implementation:</h4>
<ol>
<li><p>Calibration points shall cover the sensor’s entire measurement range for accuracy.</p>
</li>
<li><p>Parameters like gain and offset once estimated shall be stored in a non-volatile memory in the system for persistence to be used across power cycles.</p>
</li>
</ol>
<h3 id="heading-5-data-post-processing"><strong>5. Data Post-Processing</strong></h3>
<p>Post-processing covered in this section talks about removing noise and unwanted signal components, which improves data reliability.</p>
<h4 id="heading-filtering">Filtering</h4>
<p>Filtering is the process of removing unwanted frequency components from a signal to improve data quality. There are several different types of filters:</p>
<ul>
<li><p>Low-Pass Filters: Allows low-frequency signals to pass while attenuating high-frequency noise.</p>
</li>
<li><p>High-Pass Filters: Allows high-frequency signals to pass while attenuating low-frequency noise. (for example, gravitational acceleration in accelerometer data).</p>
</li>
<li><p>Band-Pass Filters: Retains only signals within a specific frequency range, removing both lower and higher frequencies outside the desired band.</p>
</li>
</ul>
<p>These filters are often implemented as FIR (Finite Impulse Response) or IIR (Infinite Impulse Response) filters. IIR filters are easy to implement and computationally efficient while FIR filters are computationally intensive but have better control over the frequency response.</p>
<p>Here, we will explore a simple low-pass filter known as the Exponential Moving Average (EMA), a type of IIR filter. A moving average filter is a mathematical technique that smooths short-term fluctuations while highlighting longer-term trends.</p>
<p>Unlike other moving average filters, EMA does not require maintaining a buffer, making it more memory-efficient. It is also more responsive to data changes while still providing smoothing, making it well-suited for real-time filtering. EMA assigns greater weight to recent data samples than older ones, allowing it to adapt quickly to changes in sensor readings.</p>
<p>EMA can be calculated like this:</p>
<p>$$EMA_{\text{t}} = \alpha * x_{\text{t}} + (1 - \alpha) * EMA_{\text{t - 1}}$$</p><p>$$\alpha = 2 / (N + 1) \quad \text{(Smoothening Factor, N - filter window size)}$$</p><p>$$EMA_{\text{t}} \quad \text{(Exponential Moving Average in current iteration)}$$</p><p>$$x_{\text{t}} \quad \text{(New Data Sample in Current Iteration)}$$</p><p>$$EMA_{\text{t - 1}} \quad \text{(Exponential Moving Average in the last iteration)}$$</p><p>Now that we understand the Exponential Moving Average (EMA) filter, here are two key factors to consider when tuning it for an application:</p>
<ul>
<li><p>Smoothing vs. Responsiveness: A higher smoothing factor (closer to 1, smaller filter window size) gives more weight to recent data, making the filter more responsive to changes but less effective at noise reduction. A lower smoothing factor (closer to 0, larger filter window size) provides better noise reduction but reacts more slowly to data changes.</p>
</li>
<li><p>Application-Specific Tuning: The smoothing factor should be chosen based on the sampling rate, sensor sensitivity, and application requirements. Real-time systems often require a balance between quick responsiveness and stable output.</p>
</li>
</ul>
<p>Here’s a code sample for EMA:</p>
<pre><code class="lang-c"><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;stdint.h&gt;</span></span>

<span class="hljs-comment">// Exponential Moving Average (EMA) filter implementation</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> FILTER_WINDOW 5</span>

<span class="hljs-comment">// Function to calculate EMA</span>
<span class="hljs-function"><span class="hljs-keyword">float</span> <span class="hljs-title">calculateEMA</span><span class="hljs-params">(<span class="hljs-keyword">float</span> ema, <span class="hljs-keyword">float</span> new_value, <span class="hljs-keyword">float</span> alpha)</span> </span>{
    <span class="hljs-keyword">return</span> (alpha * new_value) + (<span class="hljs-number">1</span> - alpha) * ema;
}

<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-keyword">float</span> sensorReadings[] = {<span class="hljs-number">26.0</span>, <span class="hljs-number">27.5</span>, <span class="hljs-number">28.2</span>, <span class="hljs-number">27.0</span>, <span class="hljs-number">26.8</span>, <span class="hljs-number">26.5</span>, <span class="hljs-number">27.2</span>};
    <span class="hljs-keyword">int</span> numReadings = <span class="hljs-keyword">sizeof</span>(sensorReadings) / <span class="hljs-keyword">sizeof</span>(sensorReadings[<span class="hljs-number">0</span>]);

    <span class="hljs-keyword">float</span> alpha = <span class="hljs-number">2.0f</span> / (FILTER_WINDOW + <span class="hljs-number">1</span>); <span class="hljs-comment">// Standard EMA formula</span>
    <span class="hljs-keyword">float</span> ema = sensorReadings[<span class="hljs-number">0</span>];  <span class="hljs-comment">// Initialize EMA with the first reading</span>

    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"EMA Filtered Sensor Data:\n"</span>);

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; numReadings; i++) {
        ema = calculateEMA(ema, sensorReadings[i], alpha);
        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Reading %d: Raw = %.2f, EMA = %.2f\n"</span>, i + <span class="hljs-number">1</span>, sensorReadings[i], ema);
    }

    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}
</code></pre>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>In summary, sensors are the backbone of modern smart devices, bridging the gap between the physical world and digital systems. From consumer electronics to industrial automation and medical devices, they enable devices to perceive and interact with their environments.</p>
<p>Understanding how sensors work, the components of their data pipeline, and their integration with microcontrollers is essential for engineers and hobbyists alike. By designing effective pipelines, developers can ensure accurate, clean, and reliable data, enabling systems to meet performance and power efficiency goals.</p>
<p>If you have questions or want to talk more about this topic, feel free to reach out on <a target="_blank" href="https://x.com/sohamstars">Twitter</a> or <a target="_blank" href="https://x.com/sohamstars">Lin</a><a target="_blank" href="https://www.linkedin.com/in/sohambanerjee2/">kedIn</a>. Always happy to connect.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
