<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged embedded</title>
<link>https://coderlegion.com/tag/embedded</link>
<description>Powered by Question2Answer</description>
<item>
<title>Building aegis-filter: Why Standard Kalman Filters Fail in Adversarial Environments</title>
<link>https://coderlegion.com/30401/building-aegis-filter-why-standard-kalman-filters-fail-in-adversarial-environments</link>
<description>&lt;p&gt;When building telemetry filters for autonomous drones or robotics, the textbook recommendation is always the same: &lt;em&gt;“Just slap a Kalman Filter on it.”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In theory, the linear Kalman filter is mathematically optimal under Gaussian white noise. In the real world, however, physical sensors do not live in textbook conditions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Adversarial GPS/Baro Spoofing:&lt;/strong&gt; A rogue transmitter or RF jammer injects sudden, massive delta jumps (e.g., +150 meters in a single millisecond).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Numerical Covariance Degeneracy:&lt;/strong&gt; Floating-point round-off errors over millions of iterations cause the error covariance matrix $P$ to lose its positive-definiteness ($P \not\succ 0$), producing negative variances and exploding state estimates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Severe Non-Linearities:&lt;/strong&gt; Radar range calculations ($r = \sqrt{x^2 + y^2}$) and orientation quaternions break linear assumptions; Extended Kalman Filters (EKF) require calculating brittle analytical Jacobian matrices that drift rapidly.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To solve these mission-critical vulnerabilities, we built and open-sourced &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://crates.io/crates/aegis-filter&quot;&gt;&lt;code&gt;aegis-filter&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; under &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Exploidus&quot;&gt;Exploidus&lt;/a&gt;&lt;/strong&gt;—a cyber-resilient, &lt;code&gt;#![no_std]&lt;/code&gt;, hard real-time estimation engine written from scratch in pure Rust.&lt;/p&gt;
&lt;p&gt;Here is the engineering breakdown of how it works and what the simulation numbers prove.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-the-core-architecture&quot;&gt;1. The Core Architecture&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;aegis-filter&lt;/code&gt; is engineered with three core defenses:&lt;/p&gt;
&lt;h4 id=&quot;a-joseph-stabilized-covariance-update&quot;&gt;A. Joseph-Stabilized Covariance Update&lt;/h4&gt;
&lt;p&gt;Standard Kalman filters update covariance using $P_k = (I - K_k H&lt;em&gt;k) P&lt;/em&gt;{k|k-1}$. In finite-precision IEEE 754 floating-point arithmetic, subtraction can yield asymmetric, non-positive-definite matrices. &lt;/p&gt;
&lt;p&gt;&lt;code&gt;aegis-filter&lt;/code&gt; enforces the &lt;strong&gt;Joseph Stabilized Form&lt;/strong&gt;:&lt;br&gt;
\[P_k = (I - K_k H_k) P_{k|k-1} (I - K_k H_k)^T + K_k R_k K_k^T\]&lt;/p&gt;
&lt;p&gt;Because both terms are quadratic forms ($A P A^T + B R B^T$), positive-definiteness is mathematically guaranteed across infinite operational lifetimes.&lt;/p&gt;
&lt;h4 id=&quot;b-chi-square-3-sigma-outlier-rejection-gating&quot;&gt;B. Chi-Square / 3-Sigma Outlier Rejection Gating&lt;/h4&gt;
&lt;p&gt;Before any raw measurement is allowed to alter the state vector, the innovation $\tilde{y}_k = z_k - H&lt;em&gt;k \hat{x}&lt;/em&gt;{k|k-1}$ is evaluated against the innovation covariance $S_k = H&lt;em&gt;k P&lt;/em&gt;{k|k-1} H_k^T + R_k$:&lt;br&gt;
\[\text{Reject if } |\tilde{y}_k| &amp;gt; \gamma \sqrt{S_k}\]&lt;/p&gt;
&lt;p&gt;If an adversary transmits a spoofed altitude or an IMU glitches, the reading is gated at line-rate. The filter falls back safely to its physical state propagation without corrupting the state.&lt;/p&gt;
&lt;h4 id=&quot;c-unscented-kalman-filtering-ukf-amp-h-infty-robust-minimax&quot;&gt;C. Unscented Kalman Filtering (UKF) &amp;amp; $H_\infty$ Robust Minimax&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;UKF (Merwe Scaled Sigma Points):&lt;/strong&gt; Replaces brittle Jacobian derivations by propagating $2n + 1$ deterministic sigma points through non-linear observation closures.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$H_\infty$ Minimax:&lt;/strong&gt; Optimizes state estimation against the &lt;em&gt;worst-case&lt;/em&gt; disturbance rather than assuming well-behaved Gaussian noise.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-flight-telemetry-amp-spoofing-resilience-simulation&quot;&gt;2. Flight Telemetry &amp;amp; Spoofing Resilience Simulation&lt;/h3&gt;
&lt;p&gt;To prove real-world resilience, we simulated a quadcopter ascending at $1.5\text{ m/s}$ subjected to atmospheric turbulence, normal sensor noise, and deliberate adversarial spoofing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Step 12:&lt;/strong&gt; Instantaneous $+150\text{m}$ GPS spoofing attack.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Step 22:&lt;/strong&gt; Severe $-80\text{m}$ sensor hardware dropout glitch.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is the telemetry log produced by running &lt;code&gt;cargo run --example flight_telemetry&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;===============================================================================
  AEGIS-FILTER :: FLIGHT TELEMETRY &amp;amp; ADVERSARIAL RESILIENCE SIMULATION
  Developed under Exploidus (https://github.com/Exploidus)
===============================================================================

Step   | True Alt (m) | Sensor Raw (m) | Aegis Est (m)  | Abs Error    | Status    
-------+--------------+----------------+----------------+--------------+-----------
1      | 1.50         | 1.79           | 1.75           | 0.254        | VALID     
2      | 3.00         | 3.22           | 3.24           | 0.238        | VALID     
3      | 4.50         | 3.46           | 4.31           | 0.187        | VALID     
4      | 6.00         | 6.74           | 6.06           | 0.057        | VALID     
5      | 7.50         | 7.67           | 7.58           | 0.081        | VALID     
6      | 9.00         | 8.90           | 9.04           | 0.045        | VALID     
7      | 10.50        | 9.68           | 10.39          | 0.113        | VALID     
8      | 12.00        | 12.98          | 12.08          | 0.075        | VALID     
9      | 13.50        | 13.41          | 13.55          | 0.047        | VALID     
10     | 15.00        | 14.69          | 14.99          | 0.009        | VALID     
11     | 16.50        | 16.04          | 16.42          | 0.080        | VALID     
12     | 18.00        | 168.00         | 17.92          | 0.080        | SPOOF REJECT
13     | 19.50        | 19.08          | 19.36          | 0.138        | VALID     
14     | 21.00        | 20.65          | 20.83          | 0.172        | VALID     
15     | 22.50        | 22.43          | 22.34          | 0.156        | VALID     
16     | 24.00        | 24.93          | 24.01          | 0.012        | VALID     
17     | 25.50        | 24.79          | 25.40          | 0.099        | VALID     
18     | 27.00        | 26.78          | 26.88          | 0.116        | VALID     
19     | 28.50        | 28.76          | 28.44          | 0.060        | VALID     
20     | 30.00        | 30.64          | 30.04          | 0.044        | VALID     
21     | 31.50        | 30.61          | 31.41          | 0.095        | VALID     
22     | 33.00        | -47.00         | 32.91          | 0.095        | SPOOF REJECT
23     | 34.50        | 34.96          | 34.50          | 0.003        | VALID     
24     | 36.00        | 36.25          | 36.04          | 0.036        | VALID     
25     | 37.50        | 36.60          | 37.39          | 0.110        | VALID     
26     | 39.00        | 39.34          | 38.96          | 0.041        | VALID     
27     | 40.50        | 40.99          | 40.54          | 0.039        | VALID     
28     | 42.00        | 41.84          | 42.01          | 0.009        | VALID     
29     | 43.50        | 42.78          | 43.40          | 0.100        | VALID     
30     | 45.00        | 45.60          | 45.00          | 0.003        | VALID     

-------------------------------------------------------------------------------
  SIMULATION SUMMARY:
  - Total Steps Processed      : 30
  - Valid Readings Fused       : 28
  - Adversarial Spikes Blocked : 2 (100% Gated)
  - Final Variance Confidence  : 0.1185
===============================================================================
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;key-takeaways-from-the-data-&quot;&gt;Key Takeaways from the Data:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Millimeter Tracking Precision:&lt;/strong&gt; Under normal noisy conditions (Step 10, Step 30), estimation error dropped to &lt;strong&gt;0.003–0.009 meters (3–9 mm)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flawless Spoof Rejection:&lt;/strong&gt; When the raw altitude leaped to $168\text{m}$ at Step 12, the filter isolated the anomaly, kept the aircraft altitude at $17.92\text{m}$ (error: $0.080\text{m}$), and prevented a fatal motor cut.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-quick-start-rust-&quot;&gt;3. Quick Start (Rust)&lt;/h3&gt;
&lt;p&gt;Add &lt;code&gt;aegis-filter&lt;/code&gt; to your &lt;code&gt;Cargo.toml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[dependencies]
aegis-filter = &quot;0.1.0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For bare-metal Cortex-M/RISC-V embedded targets:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[dependencies]
aegis-filter = { version = &quot;0.1.0&quot;, default-features = false }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;1d-outlier-gated-fusion-example-&quot;&gt;1D Outlier-Gated Fusion Example:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;use aegis_filter::{ScalarKalmanFilter, FilterError};

fn main() -&amp;gt; Result&amp;lt;(), FilterError&amp;gt; {
    // Initial state: 0.0m, Initial variance: 5.0, Process noise: 0.02, Measurement noise: 0.8
    let mut filter = ScalarKalmanFilter::new(0.0, 5.0, 0.02, 0.8)?;

    let sensor_stream = [1.5, 3.1, 4.4, 168.0 /* Spoofed spike */, 6.0];

    for &amp;amp;raw_reading in &amp;amp;sensor_stream {
        filter.predict(1.5)?; // Predict forward by 1.5m climb

        match filter.update(raw_reading, Some(3.0)) { // 3-sigma gate
            Ok(estimated_alt) =&amp;gt; println!(&quot;Estimated Altitude: {:.2}m&quot;, estimated_alt),
            Err(FilterError::MeasurementGated) =&amp;gt; {
                println!(&quot;Spoofed sensor reading ({:.1}m) rejected! Holding state.&quot;, raw_reading);
            }
            Err(e) =&amp;gt; return Err(e),
        }
    }

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-open-source-links&quot;&gt;4. Open Source Links&lt;/h3&gt;
&lt;p&gt;The engine is open-source under dual MIT/Apache-2.0 licenses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Crates.io:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://crates.io/crates/aegis-filter&quot;&gt;crates.io/crates/aegis-filter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Exploidus/aegis-filter&quot;&gt;Exploidus/aegis-filter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.rs/aegis-filter&quot;&gt;docs.rs/aegis-filter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Feedback, issues, and PRs from control engineers and systems developers are warmly welcome!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30401/building-aegis-filter-why-standard-kalman-filters-fail-in-adversarial-environments</guid>
<pubDate>Fri, 09 Oct 2026 05:45:50 +0000</pubDate>
</item>
<item>
<title>RS232 vs RS485 in a Vehicle: Which Serial Port for Which Peripheral</title>
<link>https://coderlegion.com/30356/rs232-vs-rs485-in-a-vehicle-which-serial-port-for-which-peripheral</link>
<description>&lt;p&gt;RS232 and RS485 both turn up on the same vehicle-terminal spec sheet, and integrators routinely wire the wrong one. The choice is not about speed or age. It comes down to two questions: how many devices are on the line, and how far does the cable run?&lt;/p&gt;
&lt;h3 id=&quot;the-actual-difference-single-ended-vs-differential&quot;&gt;The actual difference: single-ended vs differential&lt;/h3&gt;
&lt;p&gt;RS232 sends each signal as a voltage measured against a shared ground. That works over a short cable in a quiet electrical environment.&lt;/p&gt;
&lt;p&gt;RS485 sends over a &lt;strong&gt;twisted pair&lt;/strong&gt; and reads the &lt;em&gt;difference&lt;/em&gt; between the two wires. Interference that hits the cable hits both wires roughly equally, and cancels out when the receiver subtracts one from the other.&lt;/p&gt;
&lt;p&gt;Distance, device count and noise immunity all follow from that one design difference.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;RS232 &lt;/th&gt;&lt;th&gt;RS485&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Signalling &lt;/td&gt;&lt;td&gt;Single-ended (vs ground) &lt;/td&gt;&lt;td&gt;Differential (twisted pair)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Topology &lt;/td&gt;&lt;td&gt;Point-to-point &lt;/td&gt;&lt;td&gt;Multidrop bus&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Devices per port &lt;/td&gt;&lt;td&gt;1 &lt;/td&gt;&lt;td&gt;Up to 32 (more with repeaters)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Practical cable run &lt;/td&gt;&lt;td&gt;~15 m &lt;/td&gt;&lt;td&gt;~1200 m&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Noise immunity &lt;/td&gt;&lt;td&gt;Low &lt;/td&gt;&lt;td&gt;High&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Termination &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Yes, 120 ohm at both bus ends&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Addressing &lt;/td&gt;&lt;td&gt;Not needed &lt;/td&gt;&lt;td&gt;Required (each node needs an ID)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;which-one-for-which-peripheral&quot;&gt;Which one for which peripheral&lt;/h3&gt;
&lt;p&gt;Anything within arm's reach of the operator and connecting to exactly one port goes on RS232. Anything that leaves the cab, down a boom, along a trailer or across a yard, goes on RS485, even if it is the only device on that line, because the distance alone rules RS232 out.&lt;/p&gt;
&lt;h3 id=&quot;where-rs232-fails-quietly&quot;&gt;Where RS232 fails quietly&lt;/h3&gt;
&lt;p&gt;The failure that catches people out is not a dead link, it is an intermittent one. An RS232 run of 20 to 30 m will often work on a bench, and work on the machine while it is parked. Then it starts dropping characters once the hydraulics engage or a motor starts nearby.&lt;/p&gt;
&lt;p&gt;Because the fault only appears under load, it usually gets blamed on software or the peripheral for weeks before anyone re-examines the cable. If a serial link is flaky on a working machine but fine in the workshop, check cable length and signalling type first.&lt;/p&gt;
&lt;h3 id=&quot;rs485-wiring-that-people-skip&quot;&gt;RS485 wiring that people skip&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Termination:&lt;/strong&gt; a 120 ohm resistor at each end of the bus, not at every node. Missing termination causes reflections that look identical to random data corruption.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Daisy-chain, not star:&lt;/strong&gt; RS485 is a linear bus. Branching it into a star causes reflections even with correct termination.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unique node addresses:&lt;/strong&gt; two devices sharing an address will both answer and corrupt each other's replies.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Twisted pair, not any two conductors:&lt;/strong&gt; the noise cancellation depends on both wires following the same physical path.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;neither-replaces-can-bus&quot;&gt;Neither replaces CAN bus&lt;/h3&gt;
&lt;p&gt;CAN bus reads the vehicle's own network (engine data, fault codes, implement messages under J1939 or ISOBUS). Serial ports connect peripherals the vehicle maker never installed: scanners, printers, weigh scales, third-party sensors. A typical deployment uses all of them at once, which is why interface count, not just interface type, decides whether a terminal fits a job.&lt;/p&gt;
&lt;h3 id=&quot;a-real-example&quot;&gt;A real example&lt;/h3&gt;
&lt;p&gt;A contractor fitting terminals to self-propelled sprayers needed three things per machine: a cab-mounted barcode scanner for chemical batch logging, a legacy GNSS receiver, and four tank-level probes spread along a 28 m boom.&lt;/p&gt;
&lt;p&gt;The first install put everything on RS232 with extended cables. The scanner and receiver worked. The boom probes dropped readings whenever the pump ran. Rewiring the four probes as a single RS485 bus with 120 ohm termination at both ends fixed it. The scanner and receiver stayed on RS232, where their short runs were never the problem.&lt;/p&gt;
&lt;h3 id=&quot;faq&quot;&gt;FAQ&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Can I convert RS232 to RS485 with an adapter?&lt;/strong&gt; Yes, but each converter adds a powered component and a connection point that can fail in a vibrating vehicle. A native port avoids both.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is RS485 faster than RS232?&lt;/strong&gt; Not inherently at short distances. The real advantage is that RS485 holds its data rate over long runs where RS232 degrades.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do I need termination on a short RS485 bus?&lt;/strong&gt; Under a few metres it often works without, but termination costs nothing and removes an intermittent fault that is hard to diagnose later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How many RS485 devices can share one bus?&lt;/strong&gt; The standard allows 32 nodes. In practice, polling time becomes the limit before the electrical limit does, since the terminal queries each address in turn.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Which should I specify on a new build?&lt;/strong&gt; Default to RS485 unless you have a specific reason not to. One device under 15 m can use either. More than one device, or any run past 15 m, and RS485 is the one that works reliably in a vehicle.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;I work on rugged vehicle-mount terminals at CF-Device. The full write-up, including the spec-sheet checklist, is on our site: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.cf-device.com/blog/hardware/rs232-vs-rs485-vehicle-tablet-serial-guide/&quot;&gt;RS232 vs RS485 for Rugged Vehicle Tablets&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30356/rs232-vs-rs485-in-a-vehicle-which-serial-port-for-which-peripheral</guid>
<pubDate>Fri, 09 Oct 2026 02:19:45 +0000</pubDate>
</item>
<item>
<title>CAN Bus and J1939 Integration on Rugged Vehicle Tablets: A Practical Guide</title>
<link>https://coderlegion.com/30355/can-bus-and-j1939-integration-on-rugged-vehicle-tablets-a-practical-guide</link>
<description>&lt;p&gt;Rugged vehicle-mount tablets are only as useful as the data they can read from the machine they're bolted into. On heavy-duty vehicles — tractors, excavators, trucks — that data comes off the &lt;strong&gt;CAN bus&lt;/strong&gt;, and interpreting it correctly means supporting &lt;strong&gt;J1939&lt;/strong&gt; on top. Here's how the layers fit together and what to check when specifying hardware.&lt;/p&gt;
&lt;h3 id=&quot;can-bus-the-physical-layer&quot;&gt;CAN Bus: the physical layer&lt;/h3&gt;
&lt;p&gt;CAN (Controller Area Network) is the internal wiring protocol most commercial vehicles and heavy machinery use so their onboard components — ECUs, transmission modules, sensors — can talk to each other without a dedicated wire per signal. A vehicle terminal taps into this same network and reads the same data the dashboard uses.&lt;/p&gt;
&lt;h3 id=&quot;j1939-the-messaging-layer-on-top&quot;&gt;J1939: the messaging layer on top&lt;/h3&gt;
&lt;p&gt;CAN defines &lt;em&gt;how&lt;/em&gt; bytes move; &lt;strong&gt;J1939&lt;/strong&gt; (SAE) defines &lt;em&gt;what they mean&lt;/em&gt; — engine temperature, fuel rate, diagnostic trouble codes — in a standardized format across manufacturers. A terminal with native J1939 interprets that data correctly whether it's in a tractor, an excavator, or a delivery truck, with no per-brand integration work.&lt;/p&gt;
&lt;h3 id=&quot;iso-11783-isobus-and-iso-15765&quot;&gt;ISO 11783 (ISOBUS) and ISO 15765&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ISO 11783 / ISOBUS VT&lt;/strong&gt; extends J1939 for agricultural implement control — one terminal runs any compliant implement's interface.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ISO 15765&lt;/strong&gt; covers diagnostics.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Supporting all three on the same CAN channels means one hardware platform serves a mixed fleet without separate firmware per vehicle type.&lt;/p&gt;
&lt;h3 id=&quot;what-to-check-when-specifying-hardware&quot;&gt;What to check when specifying hardware&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Native CAN, not a USB/serial adapter&lt;/strong&gt; — an external dongle adds a powered component and a connection point that fails under vibration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Channel count&lt;/strong&gt; — a machine with multiple buses needs multiple channels, and this &lt;em&gt;cannot&lt;/em&gt; be added later.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CAN FD support&lt;/strong&gt; — larger payloads at higher speed, backward-compatible with classic CAN.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kernel-level access&lt;/strong&gt; — real-time, low-latency CAN needs SocketCAN at the kernel, not just an app-layer library.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;a-practical-note&quot;&gt;A practical note&lt;/h3&gt;
&lt;p&gt;Interface count, not processor speed, is usually the spec that decides whether a terminal fits a job. A unit two CAN channels short is unusable no matter how fast its CPU is.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Full technical write-up, with the specification sequence and model-by-model channel counts, is on our site: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.cf-device.com/blog/hardware/can-bus-j1939-integration-rugged-vehicle-tablet/&quot;&gt;CAN Bus and J1939 Integration for Rugged Vehicle Tablets&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;I'm with &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.cf-device.com/&quot;&gt;CF-Device&lt;/a&gt;, a manufacturer of rugged vehicle-mount tablets with native CAN bus, J1939 and ISOBUS support.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30355/can-bus-and-j1939-integration-on-rugged-vehicle-tablets-a-practical-guide</guid>
<pubDate>Fri, 09 Oct 2026 01:59:22 +0000</pubDate>
</item>
<item>
<title>Exploiting &quot;Trust-by-Default&quot;: How I transformed an ESP32-S3 into a BadUSB</title>
<link>https://coderlegion.com/26983/exploiting-trust-by-default-how-i-transformed-an-esp32-s3-into-a-badusb</link>
<description>&lt;p&gt;The first tool I decided to arm my Swiss army knife with was a BadUSB emulator. First and foremost, this extremely powerful tool (provided you have physical access to the target machine) relies on an inherent vulnerability of the USB protocol (born in 1996), originally designed to make connecting peripherals as simple and invisible as possible.&lt;/p&gt;
&lt;p&gt;Practically, when we plug a USB device into a computer, a rigorous process called &lt;em&gt;Enumeration&lt;/em&gt; begins. This is divided into several phases: the device is physically connected, its communication speed is identified, and immediately after, the device sends its Descriptors. These descriptors are a set of vital information that defines how the host machine will communicate with the device; subsequently, the device is reset, an address is assigned to it, the interfaces are loaded, and finally, the drivers.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(If you want to delve deeper into the enumeration phase, I recommend &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.totalphase.com/blog/2020/08/what-is-enumeration-why-usb-descriptors-important/&quot;&gt;this article by TotalPhase&lt;/a&gt;).&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Having defined this, it's easy to guess that if we alter the descriptors via software, we can disguise an ordinary flash drive (or in our case, an M5Stick) making it appear as a keyboard or a mouse.&lt;/p&gt;
&lt;p&gt;And this is where the &quot;Trust-by-Default&quot; vulnerability comes into play: all operating systems blindly trust an HID (Human Interface Device) like a keyboard, as it is an essential input tool. Generic drivers are already pre-installed in the kernel (zero incompatibilities or warning popups) and, moreover, antiviruses have no say in the matter, since they are not designed to scan hardware inputs sent via electrical impulses.&lt;/p&gt;
&lt;h4 id=&quot;the-architectural-choice-dynamic-parser-vs-hardcoding&quot;&gt;The architectural choice: Dynamic Parser vs Hardcoding&lt;/h4&gt;
&lt;p&gt;Initially, for &quot;simplicity&quot;, I thought about hardcoding the payloads directly in the C source code. But thinking about it carefully, the use cases in pentesting are infinite: the end-user must have the ability to modify scripts on the fly, without having to recompile or flash the ESP32 firmware every time.&lt;/p&gt;
&lt;p&gt;For this reason, I decided to implement a dynamic Parser for DuckyScript. This allows the user to upload their own plain text scripts (&lt;code&gt;.txt&lt;/code&gt;) onto the device's LittleFS memory and have the microcontroller interpret them on the fly.&lt;/p&gt;
&lt;p&gt;For those unfamiliar with it, DuckyScript is a macro language created by Hak5 that abstracts interaction with the victim machine. It's simple and easily readable.&lt;br&gt;
To better explain what I mean by &quot;abstract&quot;: if I were to write a payload directly in low-level C or Bash, I would have to carefully calculate every single delay between one command and the next to give the PC time to open windows, and I would have to manually map the hexadecimal codes (HID Keycodes) of every single key to &quot;press&quot;. DuckyScript, combined with my parser, automates all of this.&lt;/p&gt;
&lt;p&gt;Originally, to use Rubber Ducky scripts, they had to be transformed from simple text files into binary files; this conversion was performed by a PC tool called Ducky Encoder, which converted them into &lt;code&gt;inject.bin&lt;/code&gt;. This tool was used because many older devices lacked the computational power to analyze text during runtime, a problem that the ESP32-S3 has now overcome.&lt;/p&gt;
&lt;p&gt;So I decided to integrate a native parser into my code: this way, a user can simply upload a file from their phone (via the dedicated internal HTML page) or from a computer and execute it immediately, without having to use an external tool for the conversion. Practically, this is done using standard C functions to separate the actual command from the argument at runtime:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;char *cmd = strtok(line, &quot; &quot;);
char *arg = strtok(NULL, &quot;&quot;);

if (strcmp(cmd, &quot;DELAY&quot;) == 0 &amp;amp;&amp;amp; arg) {
    int delay_ms = atoi(arg); 
    if (delay_ms &amp;gt; 0) { 
        vTaskDelay(pdMS_TO_TICKS(delay_ms)); 
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;ram-security-o-1-space-complexity&quot;&gt;RAM Security: O(1) Space Complexity&lt;/h4&gt;
&lt;p&gt;But we all know that C doesn't joke around when it comes to memory allocation, so I tried to make the script reading phase as secure as possible. The underlying problem is that if a user decided to use a very long script (for example, uploading an entire binary file encoded in text), the M5Stick S3 would risk running out of the memory designated for the SRAM, resulting in a Kernel Panic followed by a crash and a device reboot.&lt;/p&gt;
&lt;p&gt;To bypass this hardware limit, I thought of allocating a buffer that reads the uploaded file in small, controlled blocks with the help of the &lt;code&gt;fgets()&lt;/code&gt; function, which is memory-safe by nature. This mathematically prevents a Buffer Overflow from occurring: even if the user uploads a malicious or malformed file with a single line 10,000 characters long, &lt;code&gt;fgets&lt;/code&gt; will still only read the first 255 characters per cycle, safeguarding the microcontroller's stack.&lt;/p&gt;
&lt;p&gt;If we look at the reading process from a space-complexity perspective, you can see that the way I decided to implement this mechanic has a space complexity of O(1); whereas the classic implementation would have had a complexity of O(N), as it would have loaded the entire file into RAM simultaneously.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// RAM consumption locked at only 256 bytes
char line[256];

// The file is strictly read and processed line by line
while (fgets(line, sizeof(line), f)) {
    trim_trailing(line); 
    if (strlen(line) == 0) continue;
    
    // ... start parsing for this specific line ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;beyond-abstraction-usb-protocol-and-polling-rate&quot;&gt;Beyond abstraction: USB Protocol and Polling Rate&lt;/h4&gt;
&lt;p&gt;Despite all the numerous advantages that come from using the ESP32-S3, one &quot;limit&quot; remains: not being able to use libraries that allow a high level of abstraction, such as Arduino's &lt;code&gt;Keyboard.h&lt;/code&gt;. This is a problem because, in the USB standard, a keyboard must regularly build and send 8-byte raw packets (divided into modifier mask, protocol byte, and key array) known as HID Reports.&lt;/p&gt;
&lt;p&gt;Sending these reports falls under the polling operation, which is a mechanism by which the Host (the PC) cyclically queries the USB device to check if there have been any state changes. Specifically, I handle these cyclic requests with &lt;code&gt;tud_hid_ready()&lt;/code&gt;, which checks if the USB Endpoint is free and if the PC has confirmed the reception of the previous packet. Through the &lt;code&gt;while (!tud_hid_ready()) vTaskDelay(...)&lt;/code&gt; block, I prevent the ESP32 from overwriting its own buffer before the packets have actually traveled across the cable. Without this blocking check, the keystrokes would be &quot;dropped&quot; (lost), and a DuckyScript command like &lt;code&gt;STRING powershell&lt;/code&gt; would be read by the victim PC as an incomprehensible &lt;code&gt;pwsel&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;state-based-logic-and-the-auto-repeat-trap&quot;&gt;State-Based Logic and the Auto-Repeat Trap&lt;/h4&gt;
&lt;p&gt;Added to this strict temporal &quot;dance&quot; of polling is one last huge engineering pitfall related to the nature of 8-byte reports. The most common cognitive error people run into is thinking that USB sends &quot;print commands&quot; (e.g., &quot;type the letter R&quot;). In reality, the USB architecture imposes a State-Based logic: the keyboard only communicates its electromechanical state to the PC at that precise moment.&lt;/p&gt;
&lt;p&gt;When we send a report with a specific key, we are not telling it to print it, but we are communicating: &quot;Right now, the user is physically pressing this key&quot;.&lt;/p&gt;
&lt;p&gt;The operating system's driver receives this state. If the microcontroller were to immediately move to the next command without doing anything else, the PC would think the user's finger was glued to the key, triggering the operating system's Auto-Repeat (which would endlessly open dozens of windows or print RRRRRRRR...). To solve this problem, my firmware is forced to close the cycle by always sending a subsequent &quot;zeroed&quot; HID Report (passing the &lt;code&gt;NULL&lt;/code&gt; array to TinyUSB). This tricks the PC, communicating that the key's physical contacts have finally been released (Key Release), allowing us to safely move on to the next letter.&lt;/p&gt;
&lt;h4 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h4&gt;
&lt;p&gt;Developing a low-level BadUSB emulator using &lt;code&gt;esp_tinyusb&lt;/code&gt; is an immensely more educational challenge compared to using &quot;pre-built&quot; libraries. It forces you to study the anatomy of USB packets, understand how operating systems handle polling, and deal with the physical limits of a microcontroller's memory.&lt;/p&gt;
&lt;p&gt;Now our digital &quot;Swiss Army Knife&quot; has its first real weapon, capable of uploading and launching payloads on the fly in total safety.&lt;/p&gt;
&lt;p&gt;But a multi-tool isn't just a keyboard. What if we used the ESP32's antenna to intercept the invisible data floating in the air? In the next article, I'll show you how I configured the network interface in promiscuous mode to turn the ESP32 into a Wi-Fi Sniffer, capable of capturing raw packets and saving them directly to the file system in &lt;code&gt;.pcap&lt;/code&gt; format, ready to be analyzed on Wireshark.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26983/exploiting-trust-by-default-how-i-transformed-an-esp32-s3-into-a-badusb</guid>
<pubDate>Sat, 12 Sep 2026 14:58:38 +0000</pubDate>
</item>
<item>
<title>AArch64 Register Conventions: What Survives a Function Call—and Why</title>
<link>https://coderlegion.com/26168/aarch64-register-conventions-what-survives-a-function-call-and-why</link>
<description>&lt;p&gt;What happens to a register when you call a function?&lt;/p&gt;
&lt;p&gt;On AArch64, the answer isn't &quot;the CPU saves it.&quot; When you call a function, the CPU doesn't look at a register table and decide what to preserve. &lt;code&gt;BL&lt;/code&gt; doesn't know about caller-saved or callee-saved registers. Those are ABI rules imposed on software, not behavior baked into the hardware.&lt;/p&gt;
&lt;p&gt;Once you understand that distinction, the rest of the calling convention becomes much easier to reason about: the ABI tells the caller and callee who is responsible for preserving what. That's why some registers are caller-saved, some are callee-saved, and why a register's ABI category says nothing about how long a particular value will live.&lt;/p&gt;
&lt;p&gt;When you write C++ code, the compiler decides whether a value lives in a register, on the stack, or nowhere at all after optimization. But under the hood, most scalar arithmetic and logical operations operate on register operands - and understanding the calling convention can make the difference between code that runs and code that &lt;em&gt;performs&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;If you've ever looked at compiler output for AArch64 and wondered why the compiler chose specific registers for specific values, this post is for you.&lt;/p&gt;
&lt;h3 id=&quot;aarch64-vs-arm64-what-s-in-a-name-&quot;&gt;AArch64 vs ARM64: what's in a name?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;AArch64&lt;/strong&gt; is Arm's official name for the 64-bit execution state and instruction set architecture introduced with Armv8-A. &lt;strong&gt;ARM64&lt;/strong&gt; is the colloquial term used in operating systems, toolchains, and developer communities (Linux refers to it as &lt;code&gt;aarch64&lt;/code&gt; in kernel sources, Apple calls it &lt;code&gt;arm64&lt;/code&gt;). They refer to the same thing. This post uses AArch64 to match the official architecture documentation, but you'll encounter both names in the wild.&lt;/p&gt;
&lt;h3 id=&quot;the-big-picture&quot;&gt;The big picture&lt;/h3&gt;
&lt;p&gt;AArch64 has 31 general-purpose registers (X0–X30), plus the special-purpose stack pointer SP and 32 128-bit SIMD/floating-point registers. That's a lot of registers - and beginners often see &quot;32&quot; in architecture documentation, so it's worth clarifying up front: there are 31 general-purpose registers, plus SP, which is a special-purpose register rather than an ordinary GPR (more on that later).&lt;/p&gt;
&lt;p&gt;But not all registers are created equal. The calling convention - the set of rules that govern how functions pass arguments and return values - assigns specific roles to specific registers. Understanding this convention requires a key distinction that underpins everything else.&lt;/p&gt;
&lt;h3 id=&quot;caller-saved-vs-callee-saved-the-conceptual-key&quot;&gt;Caller-saved vs callee-saved: the conceptual key&lt;/h3&gt;
&lt;p&gt;The ABI divides registers into two categories based on what happens across a function call:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Caller-saved&lt;/strong&gt; (also called &lt;em&gt;caller-clobbered&lt;/em&gt;): The caller cannot assume these registers survive a call. If the caller needs a value in one of these registers after a call, it must save the value before the call and restore it afterward. The called function is free to overwrite them.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Callee-saved&lt;/strong&gt;: If the callee modifies one of these registers, it must save the original value and restore it before returning. The caller can safely assume these registers are preserved across the call.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keep these names in mind as obligations, not characteristics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;Caller-saved&quot; means the caller must protect a value if it needs it after a call.&lt;/li&gt;
&lt;li&gt;&quot;Callee-saved&quot; means the callee must restore the incoming value before returning if it modifies the register.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Caller-saved and callee-saved are ABI properties, not hardware properties.&lt;/strong&gt; The processor does not enforce these conventions; they are rules that separately compiled functions agree to follow. The CPU is perfectly happy to let any function overwrite X19 - it's the calling convention that says &quot;if you modify it, you must restore it before returning.&quot;&lt;/p&gt;
&lt;h4 id=&quot;a-common-misconception&quot;&gt;A common misconception&lt;/h4&gt;
&lt;p&gt;A callee-saved register isn't inherently a &quot;long-lived register.&quot; It is simply a register whose incoming value must survive a call if the callee modifies it. The compiler can put a short-lived value in X19, and it can put a long-lived value in X9 if that value doesn't cross a call. Register allocation is driven by liveness, register pressure, call boundaries, and cost models - not by which category a register falls into.&lt;/p&gt;
&lt;p&gt;To make this precise, keep three separate concepts in mind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Register volatility&lt;/strong&gt;: what happens to the register across a call (caller-clobbered vs callee-saved).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Value liveness&lt;/strong&gt;: whether a particular value is needed again later in the code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Register allocation&lt;/strong&gt;: which physical register the compiler chooses for a value.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are three different concepts. A register can be caller-clobbered while holding a long-lived value, provided no call occurs while that value is live. A callee-saved register can hold a short-lived value. &quot;Callee-saved&quot; describes the register's contract, not the lifetime of the value stored in it.&lt;/p&gt;
&lt;h3 id=&quot;scope-what-this-article-covers&quot;&gt;Scope: what this article covers&lt;/h3&gt;
&lt;p&gt;This article focuses on the core AArch64 GPR and SIMD/FP register conventions used in ordinary AAPCS64 code. The current AAPCS64 also defines calling conventions for SVE and SME state - the Z0–Z31 scalable vector registers, P0–P15 predicate registers, the FFR, and the SME ZA/ZT0/FPMR state. These are outside the scope of this introduction.&lt;/p&gt;
&lt;h3 id=&quot;the-calling-convention-register-map&quot;&gt;The calling-convention register map&lt;/h3&gt;
&lt;p&gt;Here's the calling-convention register map as a quick overview. We'll walk through each group in detail below - remember, the full reference table lives in the &lt;a rel=&quot;nofollow&quot; href=&quot;#practical-cheat-sheet&quot;&gt;cheat sheet&lt;/a&gt; at the end:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;X0–X7&lt;/strong&gt; — Caller-clobbered: integer/pointer arguments and results&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;X8&lt;/strong&gt; — Caller-clobbered: indirect result location&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;X9–X15&lt;/strong&gt; — Caller-clobbered: temporary/scratch&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;X16–X17&lt;/strong&gt; — Caller-clobbered: IP0/IP1, linker/veneer scratch&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;X18&lt;/strong&gt; — Platform-dependent: platform register or caller-clobbered&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;X19–X28&lt;/strong&gt; — Callee-saved: general-purpose preserved state&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;X29&lt;/strong&gt; — Callee-saved: frame pointer / general-purpose register&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;X30&lt;/strong&gt; — Special: link register&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SP&lt;/strong&gt; — Callee-saved / special: stack pointer&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;V0–V7&lt;/strong&gt; — Caller-clobbered: FP/SIMD args and results&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;V8–V15&lt;/strong&gt; — Callee-saved, low 64 bits only: preserved FP/SIMD state&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;V16–V31&lt;/strong&gt; — Caller-clobbered: temporary FP/SIMD state&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NZCV&lt;/strong&gt; — Undefined: condition flags&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;x0-x7-arguments-and-results&quot;&gt;X0–X7: arguments and results&lt;/h3&gt;
&lt;p&gt;X0–X7 are the primary general-purpose argument and result registers. For integer, pointer, and other appropriately classified arguments, the first eight argument-register slots use X0–X7 - note that &quot;eight registers&quot; does not necessarily mean &quot;eight source-level arguments,&quot; because the AAPCS64 argument classification rules can cause arguments to be packed or passed differently. Floating-point and vector arguments normally go in V0–V7 instead, and aggregates can have more complex treatment depending on the ABI and argument types.&lt;/p&gt;
&lt;p&gt;These registers are &lt;em&gt;caller-saved&lt;/em&gt; (caller-clobbered). If a function wants to preserve their values across a call, it must save them itself. They aren't just arguments - they can also hold intermediate values between calls.&lt;/p&gt;
&lt;p&gt;The following example is deliberately simplified to illustrate the calling convention; real compiler output will look different, but the register assignment principle is the same:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;// Simplified illustration of argument passing
MOV     X0, #42        // first integer argument
MOV     X1, #100       // second integer argument
BL      my_function    // call - X0-X7 may be modified
// X0 contains the return value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This convention means that short, simple functions can be very efficient - no need to save and restore registers if you're just doing a quick computation.&lt;/p&gt;
&lt;h3 id=&quot;x8-indirect-results&quot;&gt;X8: indirect results&lt;/h3&gt;
&lt;p&gt;X8 is not an ordinary ninth argument register. It is specifically used by AAPCS64 to carry the address of caller-allocated storage when a function result is returned indirectly. For example, a result that cannot be returned in the normal result registers may use indirect result return — the caller allocates the result memory and passes its address in X8. There is no requirement for the callee to preserve X8, and X8 is not part of the normal argument sequence.&lt;/p&gt;
&lt;p&gt;The caller never passes X8 as an explicit argument in source code - the compiler handles it automatically when the return type requires it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;// Simplified illustration of indirect result
// Caller has allocated storage for the result.
// X8 points to that storage.
BL      get_large_struct
// The result has been written to the caller-provided storage.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;x9-x15-caller-clobbered-scratch-registers&quot;&gt;X9–X15: caller-clobbered scratch registers&lt;/h3&gt;
&lt;p&gt;These are caller-clobbered registers available for any intermediate use. The important nuance: &quot;caller-saved&quot; doesn't mean &quot;temporary&quot; in the sense that they can only hold short-lived values. It means the caller cannot &lt;em&gt;assume&lt;/em&gt; their values survive a call. A compiler may allocate a long-lived value to X9 if it can prove that no call occurs before the value is consumed - there's nothing in the ABI that prevents it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;// Simplified illustration of scratch register use
ADD     X9, X0, X1     // intermediate result
LSL     X10, X9, #3    // another intermediate
// X9 and X10 are caller-clobbered - a called function may overwrite them
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;x16-x17-ip0-ip1-and-linker-scratch&quot;&gt;X16–X17: IP0/IP1 and linker scratch&lt;/h3&gt;
&lt;p&gt;X16 and X17 are known as IP0 and IP1 - intra-procedure-call scratch registers. They have a special relationship with the linker and dynamic loader. The current AAPCS64 explicitly says X16/X17 can be used by call veneers and PLT code.&lt;/p&gt;
&lt;p&gt;The important consequence is that X16 and X17 are not safe places to keep values across calls. A linker-generated veneer or PLT sequence may use them even when the source-level callee doesn't. If you're writing hand-written assembly that calls external functions, treat these registers as clobbered.&lt;/p&gt;
&lt;h3 id=&quot;x18-platform-register&quot;&gt;X18: platform register&lt;/h3&gt;
&lt;p&gt;X18 is the platform register. Its preservation and usage rules are defined by the platform ABI rather than by the base AAPCS64.&lt;/p&gt;
&lt;p&gt;AAPCS64 says that a platform may reserve X18 for interprocedural state such as thread context. If the platform has no such requirement, X18 is an additional caller-saved register. Arm advises that platform-independent developers avoid X18 if possible.&lt;/p&gt;
&lt;p&gt;For example, Windows reserves X18 for the Thread Environment Block (TEB), and in user mode it points to the TEB. Android reserves X18 for the Shadow Call Stack (SCS) pointer in hardened binaries - a security mechanism that keeps return addresses on a separate, protected shadow stack. Apple platforms assign their own role to X18 as well. Other platforms have their own rules, so don't assume X18 is freely available without checking the target ABI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Portable assembly rule&lt;/strong&gt;: don't use X18 unless you know the target platform ABI.&lt;/p&gt;
&lt;h3 id=&quot;x19-x28-callee-saved-registers&quot;&gt;X19–X28: callee-saved registers&lt;/h3&gt;
&lt;p&gt;These registers are &lt;em&gt;callee-saved&lt;/em&gt; - if a function modifies any of them, it must save their original values and restore them before returning. This is the opposite of the caller-saved convention.&lt;/p&gt;
&lt;p&gt;The nuance: &quot;callee-saved&quot; doesn't mean the callee &lt;em&gt;must&lt;/em&gt; use these registers. It means if it &lt;em&gt;does&lt;/em&gt; use them, it must preserve them. The compiler decides whether to allocate a value to a callee-saved register based on liveness analysis, register pressure, and cost models - not because the ABI mandates it.&lt;/p&gt;
&lt;p&gt;If a function keeps a value live across calls, the compiler may choose a callee-saved register. The called function is then required to preserve that register, so the caller doesn't need to save the value around every individual call. The trade-off is that the callee may need to save and restore it in its own prologue and epilogue:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;// One possible prologue/epilogue pattern; real compiler-generated
// prologues vary with frame layout, optimization level, unwind
// requirements, and security features.
my_function:
    STP     X29, X30, [SP, #-16]!    // save frame pointer and link register
    STP     X19, X20, [SP, #-16]!    // save callee-saved registers we'll use

    // Because this function modifies X19/X20,
    // it must restore their incoming values before returning.
    MOV     X19, X0                   // preserve first argument across calls
    MOV     X20, X1                   // preserve second argument across calls

    BL      some_other_function       // X19 and X20 survive this call
    // ...

    LDP     X19, X20, [SP], #16      // restore callee-saved registers
    LDP     X29, X30, [SP], #16      // restore frame pointer and link register
    RET
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To see why this matters, compare the two paths for a value you need again after a call:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using caller-saved X9 means you must explicitly save the value before the call and restore it afterward.&lt;/li&gt;
&lt;li&gt;Using callee-saved X19 means the responsibility moves into the callee: if the current function itself needs to use X19, it saves it once in its prologue. But any function you call must also preserve X19, so your value survives.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That distinction is the heart of why the ABI has both classes. Values that remain live across calls are often good candidates for callee-saved registers, but the compiler may instead spill, recompute, or otherwise transform the value depending on its cost model.&lt;/p&gt;
&lt;h3 id=&quot;x29-and-x30-frame-pointer-and-link-register&quot;&gt;X29 and X30: frame pointer and link register&lt;/h3&gt;
&lt;h4 id=&quot;x29-frame-pointer&quot;&gt;X29: frame pointer&lt;/h4&gt;
&lt;p&gt;X29 is conventionally used as the frame pointer (FP) when a frame pointer is maintained. Optimized code may omit the frame pointer and use X29 as a general-purpose callee-saved register instead - this depends on compiler options (&lt;code&gt;-fno-omit-frame-pointer&lt;/code&gt;), debugging and unwinding requirements, and platform rules.&lt;/p&gt;
&lt;h4 id=&quot;x30-link-register&quot;&gt;X30: link register&lt;/h4&gt;
&lt;p&gt;X30 is the link register (LR), and it gets a special role rather than a simple caller/callee classification. When &lt;code&gt;BL&lt;/code&gt; (branch with link) executes, the CPU writes the return address into X30 before branching to the target. When the function returns with &lt;code&gt;RET&lt;/code&gt;, it jumps to the address in X30.&lt;/p&gt;
&lt;p&gt;The practical rule: a function that needs its incoming LR after making another call must preserve that LR. If a function makes another call, that call overwrites X30, so the function must save its incoming return address somewhere else - typically on the stack - if it still needs it to return.&lt;/p&gt;
&lt;p&gt;A leaf function that makes no calls can often leave X30 untouched and return directly with &lt;code&gt;RET&lt;/code&gt;, avoiding an LR save/restore.&lt;/p&gt;
&lt;p&gt;When a frame pointer is used, X29 and X30 are commonly saved together.&lt;/p&gt;
&lt;h4 id=&quot;pointer-authentication-and-x30-security&quot;&gt;Pointer Authentication and X30 security&lt;/h4&gt;
&lt;p&gt;In modern AArch64 environments (iOS, macOS, and hardened Linux builds), you will often see X30 handled with two additional instructions: &lt;code&gt;PACIASP&lt;/code&gt; and &lt;code&gt;AUTIASP&lt;/code&gt;. These are part of Armv8.3-A Pointer Authentication (PAC), a hardware security feature designed to prevent Return-Oriented Programming (ROP) and stack-smashing attacks.&lt;/p&gt;
&lt;p&gt;Because X30 holds the return address, an attacker who overwrites the saved X30 on the stack can hijack execution when the function returns. PAC solves this by signing the pointer in X30 using a secret key and the current stack pointer (SP) as context before pushing it to memory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;my_function:
    PACIASP                         // Sign X30 using A-key and SP
    STP     X29, X30, [SP, #-16]!   // Push frame pointer and signed X30

    // ... function body ...

    LDP     X29, X30, [SP], #16     // Restore signed X30 from stack
    AUTIASP                         // Authenticate X30 against SP
    RET                             // Return to verified address
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PACIASP&lt;/code&gt; (prologue)&lt;/strong&gt;: Computes a cryptographic signature from the value in X30 and SP, inserting it into the normally-unused upper bits of X30.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;AUTIASP&lt;/code&gt; (epilogue)&lt;/strong&gt;: Re-computes the signature using the current SP and validates X30. If the stack was corrupted and the saved X30 was modified, &lt;code&gt;AUTIASP&lt;/code&gt; replaces the signature with an invalid address pattern. When &lt;code&gt;RET&lt;/code&gt; then attempts to execute, the CPU triggers an immediate translation fault rather than jumping to malicious code.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Crucially for backward compatibility, &lt;code&gt;PACIASP&lt;/code&gt; and &lt;code&gt;AUTIASP&lt;/code&gt; execute as NOP instructions on legacy Armv8.0 hardware, so binaries built with PAC enabled can still run safely on older AArch64 processors.&lt;/p&gt;
&lt;h3 id=&quot;sp-xzr-and-w-registers&quot;&gt;SP, XZR, and W registers&lt;/h3&gt;
&lt;h4 id=&quot;sp-stack-pointer&quot;&gt;SP: stack pointer&lt;/h4&gt;
&lt;p&gt;SP is a special architectural register rather than an ordinary GPR, and it is not interchangeable with X0–X30. The AAPCS64 requires the stack pointer to be 16-byte aligned at public interfaces, and also requires alignment whenever memory is accessed via SP. AArch64 imposes additional restrictions on how SP can be used by instructions.&lt;/p&gt;
&lt;p&gt;Importantly, this 16-byte SP alignment isn't only an ABI convention. The CPU itself enforces an architectural SP Alignment Check whenever SP is used as the base register in a load or store: if SP isn't 16-byte aligned at such an access, the hardware raises an alignment fault regardless of what the ABI says. The ABI rule and the architectural rule work together - the calling convention keeps SP aligned so the hardware check never trips on ordinary code.&lt;/p&gt;
&lt;p&gt;AAPCS64 groups SP with the callee-saved registers, but that doesn't mean the callee must restore the exact incoming SP value instruction-for-instruction. The useful rule is: a function must restore SP to the required value before returning, and SP must remain properly aligned.&lt;/p&gt;
&lt;h4 id=&quot;xzr-the-zero-register&quot;&gt;XZR: the zero register&lt;/h4&gt;
&lt;p&gt;XZR is the architectural zero register. Reads return zero; writes are discarded. Many instructions use register encoding 31 to mean either SP or XZR, depending on the instruction class: data-processing instructions treat encoding 31 as XZR, while memory instructions and branches treat it as SP.&lt;/p&gt;
&lt;p&gt;There is therefore no X31 register you can use like X0–X30; encoding 31 is interpreted as SP or XZR depending on the instruction. This eliminates a common beginner question - there is no architectural X31 general-purpose register.&lt;/p&gt;
&lt;h4 id=&quot;w-registers-the-32-bit-view&quot;&gt;W registers: the 32-bit view&lt;/h4&gt;
&lt;p&gt;Each X register also has a 32-bit W-register view. For example, W0 refers to the low 32 bits of X0.&lt;/p&gt;
&lt;p&gt;A key AArch64 rule is that writing a W register zeroes the upper 32 bits of the corresponding X register:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;MOV     W0, #42
// X0 is now 0x000000000000002A
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is worth remembering when reading compiler output: W0 and X0 aren't separate physical registers. You'll encounter &lt;code&gt;w0&lt;/code&gt;, &lt;code&gt;w1&lt;/code&gt;, and so on constantly in disassembly.&lt;/p&gt;
&lt;h3 id=&quot;simd-and-floating-point-registers&quot;&gt;SIMD and floating-point registers&lt;/h3&gt;
&lt;p&gt;The V0-V31 registers are 128-bit SIMD/vector registers that also serve as floating-point registers. The calling convention divides them similarly to the general-purpose registers.&lt;/p&gt;
&lt;p&gt;Each V register has several aliases depending on the width you're accessing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;V0&lt;/strong&gt;: Full 128-bit register&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Q0&lt;/strong&gt;: Same 128 bits (quadword)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;D0&lt;/strong&gt;: Low 64 bits (doubleword)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;S0&lt;/strong&gt;: Low 32 bits (singleword)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;H0&lt;/strong&gt;: Low 16 bits (halfword)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B0&lt;/strong&gt;: Low 8 bits (byte)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;v0-v7-argument-and-return-registers&quot;&gt;V0–V7: argument and return registers&lt;/h4&gt;
&lt;p&gt;These are the primary SIMD/floating-point argument and return registers. Like their general-purpose counterparts, they're &lt;em&gt;caller-clobbered&lt;/em&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;// Simplified illustration: assume S0 and S1 already contain the arguments
BL      my_float_func
// S0 (low 32 bits of V0) contains the return value
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;v8-v15-partially-preserved&quot;&gt;V8–V15: partially preserved&lt;/h4&gt;
&lt;p&gt;Only the low 64 bits of V8–V15 (D8–D15) are callee-saved. That means: if a function modifies the bottom 64 bits of one of these registers, it must restore them before returning; larger values are the caller's responsibility. If you need a full 128-bit value to survive a call, you can't simply put it in V8–V15 and rely on the ABI - you must preserve the upper 64 bits yourself.&lt;/p&gt;
&lt;h4 id=&quot;v16-v31-temporary-registers&quot;&gt;V16–V31: temporary registers&lt;/h4&gt;
&lt;p&gt;These are fully caller-clobbered, like the general-purpose scratch registers. Use them for intermediate SIMD computations that don't need to survive function calls.&lt;/p&gt;
&lt;h3 id=&quot;condition-flags-nzcv&quot;&gt;Condition flags: NZCV&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;NZCV&lt;/strong&gt; is the condition flags register. It holds four single-bit flags:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;N&lt;/strong&gt; (Negative): Set to 1 if the result of the last operation was negative&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Z&lt;/strong&gt; (Zero): Set to 1 if the result was zero&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C&lt;/strong&gt; (Carry): Set to 1 if the last operation produced a carry out&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;V&lt;/strong&gt; (Overflow): Set to 1 if the last operation produced a signed overflow&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;NZCV is modified by flag-setting instructions such as &lt;code&gt;ADDS&lt;/code&gt;, &lt;code&gt;SUBS&lt;/code&gt;, &lt;code&gt;ANDS&lt;/code&gt;, and by comparison instructions such as &lt;code&gt;CMP&lt;/code&gt;, &lt;code&gt;CMN&lt;/code&gt;, and &lt;code&gt;TST&lt;/code&gt;. Ordinary arithmetic instructions such as &lt;code&gt;ADD&lt;/code&gt; and &lt;code&gt;SUB&lt;/code&gt; do not modify NZCV - you must explicitly use the flag-setting variant.&lt;/p&gt;
&lt;p&gt;NZCV is not preserved across a function call; its values are undefined at a public interface. It's also ordinary scratch state within a function: any flag-setting instruction can replace the previous condition flags.&lt;/p&gt;
&lt;p&gt;This matters for inline assembly: if your inline assembly modifies condition flags in a way the compiler must not assume is preserved, declare the appropriate &lt;code&gt;&quot;cc&quot;&lt;/code&gt; clobber (in GCC/Clang extended inline assembly). Conversely, if your assembly reads condition flags, you need to ensure they've been set appropriately before your block.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;// Simplified illustration of NZCV in inline asm
CMP     X0, #0          // flag-setting comparison: sets NZCV
B.NE    is_nonzero       // conditional branch based on Z flag
// ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;what-this-looks-like-in-compiler-output&quot;&gt;What this looks like in compiler output&lt;/h3&gt;
&lt;p&gt;When you examine compiler output, these conventions explain the register allocation patterns you see. Compilers often use caller-clobbered registers for values that don't need to survive a call, because doing so avoids unnecessary save/restore work. Values that are live across call boundaries may end up in callee-saved registers, but the compiler may instead spill to the stack, recompute the value, or transform the code entirely - register allocation is driven by a cost model, not a simple rule.&lt;/p&gt;
&lt;p&gt;Consider a concrete example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int bar(int);

int foo(int a, int b) {
    int x = a + b;
    return bar(x) + x;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;x&lt;/code&gt; is live across the call to &lt;code&gt;bar()&lt;/code&gt; - it's used again afterward. Therefore, the compiler needs some way to preserve its value across the call. It might use a callee-saved register, spill it to the stack, recompute it, or transform the expression. So conceptually:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt; → &lt;code&gt;x0&lt;/code&gt;, &lt;code&gt;x1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x&lt;/code&gt; → may move to &lt;code&gt;x19&lt;/code&gt;, because &lt;code&gt;x&lt;/code&gt; is live across the call to &lt;code&gt;bar()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A complete, valid implementation using a callee-saved register would also have to preserve the caller's incoming X19 in its prologue:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;foo:
    STP     X19, X30, [SP, #-16]!     // preserve caller's X19 and our LR

    ADD     W19, W0, W1               // x = a + b, kept in W19
    MOV     W0, W19                   // pass x as the argument
    BL      bar                       // call - x survives in W19
    ADD     W0, W0, W19               // result + x

    LDP     X19, X30, [SP], #16       // restore X19 and LR
    RET
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note what this makes explicit: using a callee-saved register doesn't make the register free. It transfers the preservation responsibility to the current function, which must save and restore X19 around its own body. That's the heart of the distinction between the two classes.&lt;/p&gt;
&lt;p&gt;Also notice how the stack is managed: &lt;code&gt;STP X19, X30, [SP, #-16]!&lt;/code&gt; pushes &lt;em&gt;two&lt;/em&gt; 64-bit registers at once (16 bytes total, hence the &lt;code&gt;#-16&lt;/code&gt; offset), and &lt;code&gt;LDP&lt;/code&gt; pops them back together. Storing registers in pairs of 64-bit registers is the standard AArch64 idiom - it's precisely how the code keeps SP moving in 16-byte units, maintaining the required alignment without extra padding. Because the pair takes exactly 16 bytes, the compiler never has to insert alignment holes.&lt;/p&gt;
&lt;p&gt;The compiler can also use caller-saved registers for long-lived values when it can prove no call intervenes. &quot;Caller-saved&quot; describes what happens across calls, not how long a value lives.&lt;/p&gt;
&lt;h3 id=&quot;writing-inline-assembly&quot;&gt;Writing inline assembly&lt;/h3&gt;
&lt;p&gt;With GCC/Clang-style extended inline assembly, ABI rules are only part of the contract. You must also tell the compiler which registers, memory, and condition flags your assembly modifies. An instruction sequence can obey the AArch64 ABI and still be invalid inline assembly if its clobbers and operands are incorrectly declared.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Important distinction&lt;/strong&gt;: in inline assembly the compiler - not your assembly code - is responsible for allocating registers around the asm block. This is fundamentally different from standalone assembly, where you own every register decision.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Specifically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Describe inputs, outputs, and clobbers using operands and constraints, allowing the compiler to allocate registers unless a specific register is genuinely required.&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;&quot;cc&quot;&lt;/code&gt; clobber if your assembly modifies condition flags.&lt;/li&gt;
&lt;li&gt;List any registers your assembly clobbers, or let the compiler allocate them via operand constraints.&lt;/li&gt;
&lt;li&gt;Declare memory effects if your assembly accesses memory the compiler doesn't know about.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Don't manually save a caller-saved register in inline asm just because it is caller-saved. Your job is to describe the inputs, outputs, and clobbers accurately. If you need a particular register preserved, model that requirement through operands/constraints rather than silently saving and restoring arbitrary registers.&lt;/p&gt;
&lt;p&gt;If your asm modifies a register that the compiler has allocated for something else and you don't tell the compiler, you can break the program even though you didn't violate the ABI. This is the key distinction between standalone assembly and inline assembly embedded in compiler-generated code.&lt;/p&gt;
&lt;h3 id=&quot;aapcs64-vs-platform-abi&quot;&gt;AAPCS64 vs platform ABI&lt;/h3&gt;
&lt;p&gt;The register conventions described above come from the &lt;strong&gt;Arm Procedure Call Standard for AArch64&lt;/strong&gt; (AAPCS64), which defines the core calling convention. But AAPCS64 is the base layer - not the whole story. The hierarchy looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AArch64 architecture
        ↓
    AAPCS64
        ↓
    platform ABI
        ↓
language/runtime/toolchain conventions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every platform builds on AAPCS64 with its own &lt;strong&gt;platform ABI&lt;/strong&gt; that adds rules specific to the operating system or runtime:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Linux / ELF&lt;/strong&gt;: AArch64 Linux user-space ABIs generally build on AAPCS64, with additional ELF and platform conventions. The exact ABI also depends on the toolchain and ABI variant being used.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Windows on AArch64&lt;/strong&gt;: Defines additional ABI rules, including reserving X18 for platform use and requiring specific unwind information.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Apple platforms&lt;/strong&gt;: Apple platforms build on AAPCS64 with additional platform and toolchain conventions. Apple platforms may also add security-related conventions such as pointer authentication, depending on the target and execution environment. If you're writing hand-written assembly, consult the ABI documentation for the specific Apple target rather than assuming that base AAPCS64 is the entire contract.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Beyond the platform ABI, C++ adds additional ABI requirements beyond plain C argument passing, and things like exception handling, TLS, unwind information, and toolchain-specific conventions can introduce further platform-specific rules.&lt;/p&gt;
&lt;p&gt;The practical consequence: when writing hand-written assembly or inline assembly, you need to know not just the AAPCS64 register rules, but also the platform-specific additions. Code that works on one platform may break on another if platform-specific register conventions are violated.&lt;/p&gt;
&lt;h3 id=&quot;practical-cheat-sheet&quot;&gt;Practical cheat sheet&lt;/h3&gt;
&lt;p&gt;Here's the compact cheat sheet - the part you'll want to bookmark:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Register &lt;/th&gt;&lt;th&gt;Across a call &lt;/th&gt;&lt;th&gt;Main purpose&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;X0–X7 &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;Arguments/results&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;X8 &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;Indirect result address&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;X9–X15 &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;Scratch&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;X16–X17 &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;Linker/veneer scratch&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;X18 &lt;/td&gt;&lt;td&gt;Platform-dependent &lt;/td&gt;&lt;td&gt;Platform register or caller-clobbered&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;X19–X28 &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;Callee-saved&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;X29 &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;Frame pointer / callee-saved GPR&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;X30 &lt;/td&gt;&lt;td&gt;Special &lt;/td&gt;&lt;td&gt;Link register&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;SP &lt;/td&gt;&lt;td&gt;✅ (special) &lt;/td&gt;&lt;td&gt;Stack pointer&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;V0–V7 &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;FP/SIMD args/results&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;V8–V15 &lt;/td&gt;&lt;td&gt;⚠️ &lt;/td&gt;&lt;td&gt;Low 64 bits callee-saved&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;V16–V31 &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;FP/SIMD scratch&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;NZCV &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;Condition flags; values are undefined across public interfaces&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;So what survives a function call?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;X19–X29 survive&lt;/strong&gt;, because the callee must preserve them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Only the low 64 bits of V8–V15 are guaranteed to survive.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;X0–X17 and V0–V7/V16–V31 are generally caller-clobbered.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;X30 must be preserved by a non-leaf function&lt;/strong&gt; if it needs its incoming return address.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NZCV is not something you can carry across a public call&lt;/strong&gt; - its values are undefined there.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But the deeper lesson is that the ABI describes register obligations, not value lifetimes. The compiler decides where a live value actually goes, choosing between callee-saved registers, spilling, or recomputation based on a cost model. And none of this is enforced by the hardware - it's a contract between separately compiled functions.&lt;/p&gt;
&lt;p&gt;Next time you look at compiler output for AArch64, you'll know why certain registers are chosen and what happens to them across function boundaries.&lt;/p&gt;
&lt;h3 id=&quot;official-documentation-and-further-reading&quot;&gt;Official documentation and further reading&lt;/h3&gt;
&lt;p&gt;The ABI specification is a moving target - the current AAPCS64 is the 2025Q4 release, issued in January 2026. Link to the current Arm ABI repository rather than an old developer.arm.com PDF:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ARM-software/abi-aa&quot;&gt;Arm ABI repository — AAPCS64&lt;/a&gt; - The authoritative home of the current AAPCS64 and other Arm ABIs&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.arm.com/documentation/ddi0602/latest&quot;&gt;Arm Architecture Reference Manual for AArch64&lt;/a&gt; - The authoritative source for the instruction set and register definitions&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ARM-software/abi-aa/blob/main/sysvabi64/sysvabi64.rst&quot;&gt;Arm System V ABI for AArch64&lt;/a&gt; - The ELF/generic System V conventions that build on AAPCS64&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/cpp/build/arm64-windows-abi-conventions&quot;&gt;Microsoft ARM64 ABI conventions&lt;/a&gt; - Microsoft's platform-specific conventions, including X18 and unwind requirements&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms&quot;&gt;Apple Platform ABI for AArch64&lt;/a&gt; - Apple's platform-specific conventions&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.kernel.org/arch/arm64/index.html&quot;&gt;Linux kernel AArch64 architecture&lt;/a&gt; - Platform-specific details for Linux on AArch64&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26168/aarch64-register-conventions-what-survives-a-function-call-and-why</guid>
<pubDate>Thu, 03 Sep 2026 20:13:24 +0000</pubDate>
</item>
<item>
<title>Why the Power-of-Two Trick Works for Binary Fractions (And Why It Breaks for Everything Else)</title>
<link>https://coderlegion.com/22297/why-the-power-of-two-trick-works-for-binary-fractions-and-why-it-breaks-for-everything-else</link>
<description>&lt;p&gt;As a 300-level Mechatronics Engineering student studying Digital Electronics, I came across something that changed the way I think about binary conversions — not just as steps to memorize, but as operations with real mathematical meaning underneath them.&lt;/p&gt;
&lt;h3 id=&quot;understanding-binary-coded-decimal-bcd-first&quot;&gt;Understanding Binary Coded Decimal (BCD) First&lt;/h3&gt;
&lt;p&gt;Before getting to the trick, it helps to understand &lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Binary-coded_decimal&quot;&gt;Binary Coded Decimal (BCD)&lt;/a&gt;, specifically the weight codes assigned to binary digits. In a 4-bit system, the weights are &lt;strong&gt;8, 4, 2, 1&lt;/strong&gt; (the 8421 code). In a 3-bit system, they are &lt;strong&gt;4, 2, 1&lt;/strong&gt; (the 421 code). These codes make binary-to-decimal conversion straightforward.&lt;/p&gt;
&lt;p&gt;For example, converting &lt;strong&gt;1111&lt;/strong&gt; in base 2 to decimal is simply:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;8 + 4 + 2 + 1 = 15
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same logic extends to binary numbers with a fractional part. To convert &lt;strong&gt;1111.0111&lt;/strong&gt; from base 2 to decimal, you group the digits in 4-bit sections and apply the weights:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Whole part:      1111 → 8 + 4 + 2 + 1 = 15
Fractional part: 0111 → 0 + 4 + 2 + 1 = 7
Result: 15.7 in decimal
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This direction — binary to decimal — is fairly intuitive once you understand BCD weights. The real challenge comes when you go the other way.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-with-decimal-to-binary-conversion&quot;&gt;The Problem with Decimal to Binary Conversion&lt;/h3&gt;
&lt;p&gt;Converting a decimal fraction to binary using the standard &lt;strong&gt;repeated multiplication method&lt;/strong&gt; works, but it is long and error-prone. The method requires you to multiply the fractional part by 2 repeatedly, recording the integer part each time and discarding it, until nothing remains. For some numbers this goes on for many steps, and one mistake anywhere gives you a wrong answer.&lt;/p&gt;
&lt;p&gt;That frustration is what sparked the idea I want to share here.&lt;/p&gt;
&lt;h3 id=&quot;the-fraction-shortcut&quot;&gt;The Fraction Shortcut&lt;/h3&gt;
&lt;p&gt;What if instead of using repeated multiplication, you convert the decimal fraction to a fraction first, then use &lt;strong&gt;BCD weight codes&lt;/strong&gt; to handle the conversion? This connects directly to the concept of &lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Fixed-point_arithmetic&quot;&gt;fixed-point arithmetic&lt;/a&gt; used in embedded systems.&lt;/p&gt;
&lt;h4 id=&quot;example-1-converting-0-25-to-binary&quot;&gt;Example 1: Converting 0.25 to Binary&lt;/h4&gt;
&lt;p&gt;0.25 as a fraction is &lt;strong&gt;1/4&lt;/strong&gt;. Assuming a 4-bit operation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Numerator: 1 in binary (using 8421 BCD) → &lt;strong&gt;0001&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Denominator: 4 in binary → &lt;strong&gt;0100&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Dividing &lt;strong&gt;0001&lt;/strong&gt; by &lt;strong&gt;0100&lt;/strong&gt; in binary gives &lt;strong&gt;0.01₂&lt;/strong&gt;.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Dividing by a power of 2 in binary is equivalent to shifting the binary point to the left — the same way dividing by 100 in decimal shifts the decimal point two places left.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Using the standard multiplication method on 0.25 gives the same answer: &lt;strong&gt;0.01₂&lt;/strong&gt;. But the fraction method shows &lt;em&gt;why&lt;/em&gt; — it is not magic, it is a &lt;strong&gt;point shift&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;example-2-converting-0-625-to-binary&quot;&gt;Example 2: Converting 0.625 to Binary&lt;/h4&gt;
&lt;p&gt;0.625 as a fraction is &lt;strong&gt;5/8&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Numerator: 5 in binary → &lt;strong&gt;101&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Denominator: 8 in binary → &lt;strong&gt;1000&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Dividing &lt;strong&gt;101&lt;/strong&gt; by &lt;strong&gt;1000&lt;/strong&gt; gives &lt;strong&gt;0.101₂&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Verified with the multiplication method: &lt;strong&gt;0.101₂&lt;/strong&gt; ✓&lt;/p&gt;
&lt;h4 id=&quot;example-3-converting-0-4375-to-binary&quot;&gt;Example 3: Converting 0.4375 to Binary&lt;/h4&gt;
&lt;p&gt;0.4375 as a fraction is &lt;strong&gt;7/16&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Numerator: 7 in binary → &lt;strong&gt;0111&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Denominator: 16 in binary → &lt;strong&gt;10000&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using BCD weights (16, 8, 4, 2, 1), dividing gives &lt;strong&gt;0.0111₂&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Verified with the multiplication method: &lt;strong&gt;0.0111₂&lt;/strong&gt; ✓&lt;/p&gt;
&lt;h3 id=&quot;where-this-breaks-down&quot;&gt;Where This Breaks Down&lt;/h3&gt;
&lt;p&gt;This shortcut works &lt;strong&gt;if and only if the denominator is a power of 2&lt;/strong&gt; — that is, 2, 4, 8, 16, 32, and so on.&lt;/p&gt;
&lt;p&gt;Try it with &lt;strong&gt;0.2&lt;/strong&gt;, which as a fraction is &lt;strong&gt;1/5&lt;/strong&gt;. The denominator is 5, and 5 in binary is &lt;strong&gt;101&lt;/strong&gt;. Since 101 is not a power of 2, there is no clean point shift. You would have to perform full binary long division, and the result never terminates — it repeats forever as &lt;strong&gt;0.00110011...&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This is the same reason &lt;strong&gt;0.1 + 0.2 does not exactly equal 0.3&lt;/strong&gt; in most programming languages. The decimal 0.1 is 1/10, and since 10 is not a power of 2, it cannot be represented exactly in binary. This is not a bug — it is a &lt;em&gt;structural limitation&lt;/em&gt; of representing base-10 fractions in a base-2 system. Learn more about this in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/IEEE_754&quot;&gt;IEEE 754 floating-point standard&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;conclusion-2&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;power-of-two fraction shortcut&lt;/strong&gt; offers a clearer, more intuitive path for converting certain decimal fractions to binary. Rather than blindly following the &lt;strong&gt;repeated multiplication algorithm&lt;/strong&gt;, converting the decimal to a fraction and applying &lt;strong&gt;BCD weight codes&lt;/strong&gt; reveals the actual mathematical operation taking place — a simple &lt;strong&gt;binary point shift&lt;/strong&gt;. This works cleanly for denominators that are &lt;strong&gt;powers of 2&lt;/strong&gt; (such as 2, 4, 8, 16), giving the same result as the standard method while making the &lt;em&gt;reason&lt;/em&gt; visible. However, it has a clear &lt;strong&gt;limitation&lt;/strong&gt;: denominators that are not powers of 2 produce non-terminating &lt;strong&gt;binary fractions&lt;/strong&gt;, requiring long division instead. Understanding this boundary does more than just solve conversion problems — it explains one of the most well-known quirks in &lt;strong&gt;floating-point arithmetic&lt;/strong&gt;. The goal of this article was never to introduce a new method, but to show that understanding the &lt;strong&gt;structure behind an algorithm&lt;/strong&gt; is always more powerful than memorizing its steps.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22297/why-the-power-of-two-trick-works-for-binary-fractions-and-why-it-breaks-for-everything-else</guid>
<pubDate>Sat, 11 Jul 2026 13:03:17 +0000</pubDate>
</item>
<item>
<title>8 Arduino coding habits that separate hobby sketches from real products</title>
<link>https://coderlegion.com/18954/8-arduino-coding-habits-that-separate-hobby-sketches-from-real-products</link>
<description>&lt;p&gt;Most Arduino tutorials teach you to blink an LED and stop there. The jump from &quot;it works on my desk&quot; to &quot;it ran for 6 months without crashing&quot; is where most hobby projects die. Here are eight habits that get you across that gap - most of them I learned the hard way.&lt;/p&gt;
&lt;h3 id=&quot;1-stop-using-delay-&quot;&gt;1. Stop using &lt;code&gt;delay()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;delay(1000)&lt;/code&gt; halts your entire program for a full second. Nothing else runs - not your buttons, not your sensor reads, not your serial output. Use &lt;code&gt;millis()&lt;/code&gt; instead:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;unsigned long lastBlink = 0;
const unsigned long BLINK_INTERVAL = 1000;

void loop() {
  if (millis() - lastBlink &amp;gt;= BLINK_INTERVAL) {
    lastBlink = millis();
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
  }
  // your other code keeps running here
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you internalize this pattern, every project gets better.&lt;/p&gt;
&lt;h3 id=&quot;2-avoid-the-string-class-on-avr-boards&quot;&gt;2. Avoid the &lt;code&gt;String&lt;/code&gt; class on AVR boards&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;String&lt;/code&gt; fragments the tiny heap on Uno/Nano/Mega. Your sketch runs fine for an hour, then crashes mysteriously at 3 AM. Use fixed-size &lt;code&gt;char&lt;/code&gt; buffers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;char buf[32];
snprintf(buf, sizeof(buf), &quot;Temp: %d C&quot;, temperature);
Serial.println(buf);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On ESP32 with megabytes of RAM, &lt;code&gt;String&lt;/code&gt; is fine. On a 2KB AVR, it's a timebomb.&lt;/p&gt;
&lt;h3 id=&quot;3-wrap-string-literals-in-f-&quot;&gt;3. Wrap string literals in &lt;code&gt;F()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Every &lt;code&gt;Serial.println(&quot;Hello world&quot;)&lt;/code&gt; copies that string into precious RAM at boot. The &lt;code&gt;F()&lt;/code&gt; macro keeps it in flash:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Serial.println(F(&quot;System ready&quot;));  // costs 0 bytes of RAM
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On a 2KB Uno, this single change can free hundreds of bytes.&lt;/p&gt;
&lt;h3 id=&quot;4-be-explicit-about-integer-sizes&quot;&gt;4. Be explicit about integer sizes&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;int&lt;/code&gt; is &lt;strong&gt;16 bits on AVR&lt;/strong&gt; and &lt;strong&gt;32 bits on ESP32/SAMD&lt;/strong&gt;. Code that works on an Uno silently breaks on an ESP32 (or vice versa) because of overflow. Use &lt;code&gt;&amp;lt;stdint.h&amp;gt;&lt;/code&gt; types:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;uint8_t  pin = 7;
uint16_t adcValue;
uint32_t timestamp;
int16_t  signedTemp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your future self porting between boards will thank you.&lt;/p&gt;
&lt;h3 id=&quot;5-use-input-pullup-instead-of-external-resistors&quot;&gt;5. Use &lt;code&gt;INPUT_PULLUP&lt;/code&gt; instead of external resistors&lt;/h3&gt;
&lt;p&gt;You almost never need a physical pull-up resistor for a button. The AVR/ESP32 has them built in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;pinMode(BUTTON_PIN, INPUT_PULLUP);
// button pressed = LOW, released = HIGH
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Less wiring, fewer parts, fewer things to debug.&lt;/p&gt;
&lt;h3 id=&quot;6-keep-isrs-tiny-and-mark-shared-variables-volatile&quot;&gt;6. Keep ISRs tiny and mark shared variables &lt;code&gt;volatile&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Interrupt service routines should set a flag and exit. Do the real work in &lt;code&gt;loop()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;volatile bool buttonPressed = false;

void IRAM_ATTR onButton() {   // IRAM_ATTR for ESP32; omit on AVR
  buttonPressed = true;
}

void loop() {
  if (buttonPressed) {
    buttonPressed = false;
    handleButton();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Calling &lt;code&gt;Serial.print()&lt;/code&gt; inside an ISR is a common rookie mistake - it can deadlock the whole board.&lt;/p&gt;
&lt;h3 id=&quot;7-model-state-machines-explicitly&quot;&gt;7. Model state machines explicitly&lt;/h3&gt;
&lt;p&gt;If your project has modes - idle, recording, transmitting, error - don't nest five &lt;code&gt;if&lt;/code&gt; statements. Use an &lt;code&gt;enum&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;enum class State { Idle, Recording, Transmitting, Error };
State state = State::Idle;

void loop() {
  switch (state) {
    case State::Idle:         handleIdle(); break;
    case State::Recording:    handleRecording(); break;
    case State::Transmitting: handleTransmitting(); break;
    case State::Error:        handleError(); break;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This one habit makes debugging an order of magnitude easier.&lt;/p&gt;
&lt;h3 id=&quot;8-enable-the-watchdog-for-anything-deployed&quot;&gt;8. Enable the watchdog for anything deployed&lt;/h3&gt;
&lt;p&gt;A device sitting in your garage with no debugger needs to recover from its own bugs. On AVR:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;lt;avr/wdt.h&amp;gt;
void setup() { wdt_enable(WDTO_8S); }
void loop()  { wdt_reset(); /* your code */ }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;loop()&lt;/code&gt; hangs for more than 8 seconds, the chip reboots itself. Beats driving across town to power-cycle.&lt;/p&gt;
&lt;h3 id=&quot;a-bonus-setup-and-loop-are-just-main-&quot;&gt;A bonus: &lt;code&gt;setup()&lt;/code&gt; and &lt;code&gt;loop()&lt;/code&gt; are just &lt;code&gt;main()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The Arduino IDE hides it, but underneath it's plain C++. You can - and should - define classes, split code across &lt;code&gt;.h&lt;/code&gt;/&lt;code&gt;.cpp&lt;/code&gt; files, and use namespaces. The &lt;code&gt;.ino&lt;/code&gt; file is just convention.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you want to go deeper on the jump from hobby projects to real embedded products, that's what we write about over at &lt;a rel=&quot;nofollow&quot; href=&quot;https://make-it.ai&quot;&gt;Make-it.ai&lt;/a&gt;. Tools for hardware design, project breakdowns, and lessons from makers actually shipping things in the wild.&lt;/p&gt;
&lt;p&gt;What's the Arduino habit you wish you'd picked up earlier? Drop it in the comments - I'm curious which of these resonate and which ones people disagree with.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18954/8-arduino-coding-habits-that-separate-hobby-sketches-from-real-products</guid>
<pubDate>Tue, 26 May 2026 05:54:48 +0000</pubDate>
</item>
<item>
<title>Ditch the Clunky GUIs: Meet ComChan, the Blazingly Fast Rust Serial Monitor</title>
<link>https://coderlegion.com/17983/ditch-the-clunky-guis-meet-comchan-the-blazingly-fast-rust-serial-monitor</link>
<description>&lt;p&gt;If you spend any significant amount of time doing embedded systems development, you know the drill. You write your firmware, flash the microcontroller, and then… you stare at a serial monitor. &lt;/p&gt;
&lt;p&gt;For years, the options have been a bit of a mixed bag. You have the classic Arduino IDE Serial Monitor (which is functional but basic), bloated GUI protocol analyzers that eat up RAM, or simple CLI tools like &lt;code&gt;minicom&lt;/code&gt; that get the job done but lack modern developer quality-of-life features.&lt;/p&gt;
&lt;p&gt;As someone who lives in the terminal—relying on tools like Neovim and terminal multiplexers—I wanted a serial monitor that actually fit into a keyboard-driven, modern workflow. I wanted something lightweight, visually clean, and capable of handling complex data without breaking a sweat.&lt;/p&gt;
&lt;p&gt;So, I built &lt;strong&gt;ComChan&lt;/strong&gt; (Communication Channel).&lt;/p&gt;
&lt;h4 id=&quot;what-is-comchan-&quot;&gt;What is ComChan?&lt;/h4&gt;
&lt;p&gt;ComChan is a minimal, blazingly fast serial monitor and plotter built entirely in Rust. It’s designed specifically for embedded systems engineers who want powerful data visualization and logging without leaving the command line. &lt;/p&gt;
&lt;p&gt;Because it's written in Rust, it’s memory-safe, incredibly fast, and runs natively across Linux, Windows, and macOS. &lt;/p&gt;
&lt;p&gt;Here is a look at the features that make ComChan stand out from the crowd.&lt;/p&gt;
&lt;h4 id=&quot;1-the-terminal-based-serial-plotter&quot;&gt;1. The Terminal-Based Serial Plotter&lt;/h4&gt;
&lt;p&gt;Sometimes, raw text isn't enough. When you are debugging an IMU, reading ADC values, or tracking temperature sensors, you need to see the data visually. &lt;/p&gt;
&lt;p&gt;Instead of exporting data to Excel or firing up a heavy GUI application, ComChan features a built-in TUI (Terminal User Interface) plotter. You simply pass the &lt;code&gt;--plot&lt;/code&gt; flag, and ComChan will automatically parse your incoming numeric data, assign legends, and graph multiple sensor channels in real-time—right inside your terminal.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=15727656772548640252&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Found a spike you want to investigate? You can hit &lt;code&gt;Ctrl+S&lt;/code&gt; to instantly export the current plot frame as a clean SVG file.&lt;/p&gt;
&lt;p&gt;Here is how it'll look like &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=2420146948092194581&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4 id=&quot;2-time-travel-with-session-replay&quot;&gt;2. Time Travel with Session Replay&lt;/h4&gt;
&lt;p&gt;This is perhaps my favorite feature. Debugging intermittent hardware anomalies is a nightmare because you can't always recreate the exact physical conditions that caused the bug.&lt;/p&gt;
&lt;p&gt;ComChan introduces &lt;strong&gt;Session Replay&lt;/strong&gt;. You can log your raw serial output to a &lt;code&gt;.log&lt;/code&gt; or &lt;code&gt;.csv&lt;/code&gt; file during a test run. Later, you can run &lt;code&gt;comchan --replay &amp;lt;file&amp;gt;&lt;/code&gt; and the tool will parse the timestamps and play the data back to you in &lt;em&gt;real-time&lt;/em&gt;. &lt;/p&gt;
&lt;p&gt;It perfectly recreates the exact timing of the original hardware run. You can even replay a log file directly into the visual Plotter to analyze the anomaly visually, without needing the physical microcontroller plugged in.&lt;/p&gt;
&lt;h4 id=&quot;3-continuous-csv-streaming&quot;&gt;3. Continuous CSV Streaming&lt;/h4&gt;
&lt;p&gt;If you are doing data collection for research or analysis, ComChan can automatically parse your incoming numeric telemetry and stream it directly into a clean, multi-column &lt;code&gt;.csv&lt;/code&gt; file on the fly. &lt;/p&gt;
&lt;p&gt;The best part? This works concurrently. You can stream to a CSV in the background while actively watching the data on the TUI Plotter.&lt;/p&gt;
&lt;h4 id=&quot;4-built-for-the-modern-embedded-workflow&quot;&gt;4. Built for the Modern Embedded Workflow&lt;/h4&gt;
&lt;p&gt;ComChan includes several smaller features designed specifically to remove friction from the embedded workflow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Auto-Detection:&lt;/strong&gt; Tired of typing &lt;code&gt;/dev/ttyUSB0&lt;/code&gt; or &lt;code&gt;COM3&lt;/code&gt;? Just use &lt;code&gt;comchan --auto&lt;/code&gt; and it will automatically find and connect to your active USB serial device.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zephyr RTOS Support:&lt;/strong&gt; If you work with Zephyr, ComChan has a dedicated &lt;code&gt;--zephyr&lt;/code&gt; shell mode that handles its specific echo and prompt behaviors cleanly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auto-Recovery:&lt;/strong&gt; If you accidentally unplug your device or reset the power, ComChan won't crash. It detects the broken pipe and gracefully waits to reconnect when the hardware comes back online.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configuration Files:&lt;/strong&gt; Save your favorite baud rates, data bits, and parity settings in a &lt;code&gt;.toml&lt;/code&gt; file so you don't have to pass a dozen flags every time you launch.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-brain-made-philosophy&quot;&gt;The &quot;Brain Made&quot; Philosophy&lt;/h4&gt;
&lt;p&gt;While AI tools are great for scaffolding tests or micro-optimizations, the core architecture, TUI design, and feature logic of ComChan are entirely human-crafted. It’s built to solve real pain points that I experience daily when working with memory-safe embedded development on ESP32s and RP2040s.&lt;/p&gt;
&lt;h4 id=&quot;try-it-out&quot;&gt;Try It Out&lt;/h4&gt;
&lt;p&gt;If you are ready to upgrade your embedded debugging workflow, ComChan is incredibly easy to install.&lt;/p&gt;
&lt;p&gt;If you have Cargo installed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cargo install comchan
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For Arch Linux users, it's available on the AUR:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;yay -S comchan
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can check out the full source code, read the documentation, and contribute on GitHub: &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Vaishnav-Sabari-Girish/ComChan&quot;&gt;Vaishnav-Sabari-Girish/ComChan&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Drop a star if you find it useful, and let me know what features you’d like to see next (a Hex Dump mode is already on the roadmap!). Happy debugging!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17983/ditch-the-clunky-guis-meet-comchan-the-blazingly-fast-rust-serial-monitor</guid>
<pubDate>Fri, 15 May 2026 15:58:16 +0000</pubDate>
</item>
<item>
<title>Interrupt Management in ARM Cortex-M</title>
<link>https://coderlegion.com/14090/interrupt-management-in-arm-cortex-m</link>
<description>&lt;p&gt;Interrupts is the must-known concept in embedded programming. So I wanna explain the interrupt management in ARM Cortex-M in this tutorial.&lt;/p&gt;
&lt;p&gt;ARM architecture distinguishes between the two types: &lt;em&gt;interrupt&lt;/em&gt; and &lt;em&gt;system exception&lt;/em&gt;. Interrupts are primarily sourced by external hardware like UART that warns about arrival of data. System exceptions are more software-related and caused by ARM Cortex internal.&lt;/p&gt;
&lt;p&gt;Interrupts and system exceptions are exchangeably used. But even if both of these do same thing (especially in programming), the differences are important for us, engineers!&lt;/p&gt;
&lt;p&gt;I'll use &quot;interrupt&quot; word for both terms further, if otherwise declared.&lt;/p&gt;
&lt;p&gt;Before diving into interrupts, you need to understand the general execution workflow of ARM Cortex. There are two types of &lt;em&gt;execution&lt;/em&gt; modes: &lt;em&gt;thread&lt;/em&gt; and &lt;em&gt;interrupt&lt;/em&gt;. After resetting the microcontroller, execution flow starts in thread mode so that if you want to use interrupts, you need to enter the interrupt mode. But how?&lt;/p&gt;
&lt;p&gt;When you flash your firmware into microcontroller (base address of 0x08000000, in case), ARM Cortex starts to execute the firmware's machine instructions in thread mode. If you want to use any interrupt, you have to explicitly say it to ARM Cortex by using &lt;em&gt;Interrupt Service Routines (ISRs)&lt;/em&gt;. ISRs are actually standard C functions, but have the special signatures. I'm gonna explain it later.&lt;/p&gt;
&lt;p&gt;The most important question about interrupts is that why do we need interrupts?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The answer: Interrupts are the source of multiprogramming!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Interrupts give us &lt;em&gt;second&lt;/em&gt; execution flow besides the main flow (executed in thread mode by default). It also gives the &lt;em&gt;context switch&lt;/em&gt; mechanism for RTOS. I will explain it lastly. &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5dvmqafsj7ilkcameuq1.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;ARM Cortex has a special hardware called &lt;em&gt;Nested Vectored Interrupt Controller (NVIC)&lt;/em&gt;. It handles and manages the all interrupt usage. Below is the its simple schematic:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ifm0nlj1u3zt5ujigevn.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;Another important dedicated hardware is the &lt;em&gt;EXTI Controller&lt;/em&gt;. The peripheral interrupts in the microcontroller directly are connected to NVIC. But If you want to use interrupts over microcontroller pins (like detecting rising or falling edge of a button), you have to use EXTI lines. Microcontroller pins are grouped and tied to specific EXTI lines. Please look at the reference manual to find it.&lt;/p&gt;
&lt;p&gt;We have many different types of interrupts. For now, let's look at these:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oxuqk3xcz9kjg3zxrjgh.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;ARM Cortex has defined 15 fixed system exceptions (coming from internal of Cortex). But microcontroller vendors specify also variable-length interrupts (coming from external of Cortex). For example, STM32 series microcontrollers allow up to 256 interrupts.&lt;/p&gt;
&lt;p&gt;But where are located these interrupts? The answer is the &lt;em&gt;at vector table&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The vector table is an array of word data inside the system&lt;br&gt;
memory, each representing the starting address of one exception type [1].&lt;/p&gt;
&lt;p&gt;If you've flashed any firmware into microcontroller over OpenOCD + GDB, you've seen probably the this vector table. If you've not, let's see it exactly. &lt;/p&gt;
&lt;p&gt;I have STM32F446RE microcontroller and ST-LINK connection. I've compiled a firmware (&lt;em&gt;firmware.elf&lt;/em&gt;) to flash it. Open a terminal and type:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And open the second terminal and type:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ arm-none-eabi-gdb firmware.elf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lastly, type these into GDB session:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) target remote localhost:3333
(gdb) monitor reset halt
(gdb) load
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After flashed the firmware into microcontroller, you will see:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Loading section .isr_vector, size 0x1c4 lma 0x8000000
Loading section .text, size 0x1080 lma 0x80001d0
Loading section .rodata, size 0x10 lma 0x8001250
Loading section .init_array, size 0x4 lma 0x8001260
Loading section .fini_array, size 0x4 lma 0x8001264
Loading section .data, size 0x10 lma 0x8001268
Start address 0x80011d0, load size 4716
Transfer rate: 9 KB/sec, 786 bytes/write.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yeah! You see the interrupt vector table (_.isr_vector_) section was flashed at the base address of flash memory and other sections after the vector table. If you use any type of interrupt, the ARM Cortex gets the corresponding ISR handler from there so that ARM Cortex exactly knows the ISR handler addresses. You just need to select the required ISR handler function!&lt;/p&gt;
&lt;p&gt;As I said before, ISR handlers are special type of C functions. But why? Because the handlers have predefined function prototypes. You have to use these function prototypes. Otherwise, ARM Cortex cannot find it and treated as normal C functions, not the ISR handlers. &lt;/p&gt;
&lt;p&gt;These predefined handler prototypes are stored in the &lt;em&gt;startup code&lt;/em&gt;. For STM32F446RE, it is _startup_stm32f446xx.S_ file. Below is a part of this startup code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(...)

  .word  Reset_Handler
  .word  NMI_Handler
  .word  HardFault_Handler
  .word  MemManage_Handler
  .word  BusFault_Handler

(...)

  .word     CAN1_TX_IRQHandler                /* CAN1 TX                      */                         
  .word     CAN1_RX0_IRQHandler               /* CAN1 RX0                     */                          
  .word     CAN1_RX1_IRQHandler               /* CAN1 RX1                     */                          
  .word     CAN1_SCE_IRQHandler               /* CAN1 SCE                     */                          
  .word     EXTI9_5_IRQHandler                /* External Line[9:5]s          */                          
  .word     TIM1_BRK_TIM9_IRQHandler          /* TIM1 Break and TIM9          */         
  .word     TIM1_UP_TIM10_IRQHandler          /* TIM1 Update and TIM10        */         
  .word     TIM1_TRG_COM_TIM11_IRQHandler     /* TIM1 Trigger and Commutation and TIM11 */
  .word     TIM1_CC_IRQHandler                /* TIM1 Capture Compare         */                          
  .word     TIM2_IRQHandler                   /* TIM2                         */                   
  .word     TIM3_IRQHandler                   /* TIM3                         */                   
  .word     TIM4_IRQHandler                   /* TIM4                         */                   
  .word     I2C1_EV_IRQHandler                /* I2C1 Event                   */                          
  .word     I2C1_ER_IRQHandler                /* I2C1 Error                   */                          
  .word     I2C2_EV_IRQHandler                /* I2C2 Event                   */                          
  .word     I2C2_ER_IRQHandler                /* I2C2 Error                   */          

(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At the beginning, I've stated that interrupts are the source of multiprogramming. This is core logic behind the RTOS!&lt;/p&gt;
&lt;p&gt;RTOSes use the interrupts to create many &lt;em&gt;tasks&lt;/em&gt;. In classic firmware written with RTOS, there are many tasks that the RTOS kernel have to handle and manage. You can think that task in the firmware corresponds to process in the computers. &lt;/p&gt;
&lt;p&gt;The RTOS kernel needs to execute these tasks in order and with same time-slice so that the kernel cuts the one task execution and passes to the next task execution using &lt;em&gt;context switches&lt;/em&gt;. This is done by using interrupts. For example, FreeRTOS kernel uses &lt;em&gt;SVC&lt;/em&gt; and &lt;em&gt;PendSV&lt;/em&gt; interrupts specificly to make the contex switch. Apart from that, the RTOS kernel uses also the &lt;em&gt;SysTick&lt;/em&gt; interrupt to give the equal execution time to each task. &lt;/p&gt;
&lt;p&gt;Got it. I've explain the interrupt management in ARM Cortex-M in general. I put the some references below. You can look at the interrupts and similar topics in there.&lt;/p&gt;
&lt;p&gt;References:&lt;/p&gt;
&lt;p&gt;[1]. Yiu J., The Definitive Guide to The ARM Cortex-M3, Second Edition, Newnes, page 36.&lt;br&gt;
Noviello C., Mastering STM32, 0.26 Release, Leanpub&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/14090/interrupt-management-in-arm-cortex-m</guid>
<pubDate>Mon, 06 Apr 2026 15:15:30 +0000</pubDate>
</item>
<item>
<title>How to build a firmware using Makefile?</title>
<link>https://coderlegion.com/14088/how-to-build-a-firmware-using-makefile</link>
<description>&lt;p&gt;In embedded programming, we trust IDEs like STM32CubeIDE, Keil or so on. These IDEs give us just some buttons and UIs to build, flash, and debug the source code that we've written for a MCU. But, I think that every engineer that works on this field have to know that how these steps are made by hand.&lt;/p&gt;
&lt;p&gt;In this tutorial, I will show building a firmware written for STM32F446RE MCU with just using a single Makefile.&lt;/p&gt;
&lt;p&gt;Below is the project directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ tree
.
├── bin/
├── drivers/
│   ├── CMSIS/
│   ├── STM32F446RETX_FLASH.ld
│   └── STM32F4xx_HAL_Driver/
├── lib/
├── Makefile
└── src/
    ├── it.c
    ├── main.c
    ├── main.h
    └── peripheral.c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Firstly, which libraries do we need to have?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;HAL library&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CMSIS library&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Linker script&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Startup/system code&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;arm-none-eabi-* tools&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I've put the all required libraries/scripts into &lt;strong&gt;/drivers&lt;/strong&gt; directory. Please download these using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ git clone --recurse-submodules https://github.com/STMicroelectronics/STM32CubeF4.git
$ sudo apt install gcc-arm-none-eabi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In here, HAL library is used to program the peripherals and CMSIS for ARM-Cortex itself. These are the most important libraries that we have to get. &lt;/p&gt;
&lt;p&gt;Apart from that, we also need some special files. First is the &lt;strong&gt;linker script&lt;/strong&gt;. It includes the memory layout of microcontroller. Sometimes, it comes from above command. But if you cannot find it there, please install it externally. Another file is the &lt;strong&gt;startup code&lt;/strong&gt;. It's written in Assembly language. As its name suggests, it's the first code that runs in microcontroller. Basically, it includes the &lt;strong&gt;Reset_Handler&lt;/strong&gt; and a branch that jumps into &lt;strong&gt;main()&lt;/strong&gt; function in your source code. Also &lt;strong&gt;system code&lt;/strong&gt; includes the some generic and general definitions.&lt;/p&gt;
&lt;p&gt;After downloaded and explained the required ones, Let's write the &lt;strong&gt;Makefile&lt;/strong&gt; step by step:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Toolchain definitions&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CC        := arm-none-eabi-gcc
OBJCOPY:= arm-none-eabi-objcopy
SIZE:= arm-none-eabi-size
RM        := rm -f
FILE:= file
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. MCU-specific definitions&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DEVICE_FAMILY     := STM32F4xx
DEVICE_MODEL     := STM32F446xx
DEVICE_VARIANT     := STM32F446RETx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. Compiler and linker flags&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CORTEX_FLAGS     := -mthumb -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard
COMMON_FLAGS     := -g3 -Os -ffunction-sections -fdata-sections -Wall
AS_FLAGS             := -x assembler-with-cpp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In here, we've defined some flags:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;-mthumb&lt;/strong&gt; means that the firmware uses thumb (16-bit) instruction set.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;-mcpu=cortex-m4&lt;/strong&gt; means the ARM-Cortex series itself.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;-mfpu=fpv4-sp-d16&lt;/strong&gt; and &lt;strong&gt;-mfloat-abi=hard&lt;/strong&gt; means that floating-point operations can be done in the firmware.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;4. Include paths&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MAIN_INC:= ./src
CMSIS_INC:= ./drivers/CMSIS/Include
CMSIS_DEV_INC:= ./drivers/CMSIS/Device/ST/STM32F4xx/Include
HAL_INC:= ./drivers/STM32F4xx_HAL_Driver/Inc
CMSIS_DSP_INC:= ./drivers/CMSIS/DSP/Include
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In here, we specified the header files that are used in the source code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Source and special files&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MAIN_SRC:= $(wildcard ./src/*.c)
HAL_SRC:= $(wildcard ./drivers/STM32F4xx_HAL_Driver/Src/*.c)
SYSTEM_SRC:= ./drivers/CMSIS/Device/ST/STM32F4xx/Source/Templates/system_stm32f4xx.c
STARTUP_CODE:= ./drivers/CMSIS/Device/ST/STM32F4xx/Source/Templates/gcc/startup_stm32f446xx.S
LINKER_SCRIPT:= ./drivers/STM32F446RETX_FLASH.ld
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;6. Sorthands and build definitions&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DEFINES:= -D$(DEVICE_FAMILY) -D$(DEVICE_MODEL) -D$(DEVICE_VARIANT) \
-DUSE_HAL_DRIVER

SOURCES:= $(MAIN_SRC) $(HAL_SRC) $(SYSTEM_SRC)
OBJECTS:= $(notdir $(patsubst %.c,%.o,$(SOURCES))) startup_stm32f446xx.o
INCLUDES:= -I$(MAIN_INC) -I$(CMSIS_DEV_INC) -I$(CMSIS_INC) -I$(HAL_INC) \
-I$(CMSIS_DSP_INC)

CFLAGS:= $(CORTEX_FLAGS) $(COMMON_FLAGS) $(DEFINES) $(INCLUDES)
AFLAGS:= $(CORTEX_FLAGS) $(AS_FLAGS) $(DEFINES) $(INCLUDES)
LDFLAGS:= $(CORTEX_FLAGS) -T $(LINKER_SCRIPT) \
-Wl,--gc-sections,--relax --specs=nano.specs --specs=nosys.specs \
   -Wl,--start-group -lc -lm -lnosys -Wl,--end-group
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In here, I mostly did the string operations and manipulations. But some points are interesting and need some explanations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Firstly, I've grouped the sources and libraries and converted the source file suffixes into object file formats.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Also, I've created three total flags that will be used for compiling, assembling, and linking phases.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In linker flags, I put the linker script. As you noticed, this step will be done after the compiling the source files. I also linked the some libraries. &lt;strong&gt;-lc&lt;/strong&gt; is the standard C library. In firmware, we use often string manipulation functions (&lt;strong&gt;strlen()&lt;/strong&gt;, &lt;strong&gt;strcat()&lt;/strong&gt;, &lt;strong&gt;strcpy()&lt;/strong&gt; or so on), &lt;strong&gt;snprint()&lt;/strong&gt;, &lt;strong&gt;memset()&lt;/strong&gt;, or similar ones. &lt;strong&gt;-lm&lt;/strong&gt; is the math library as you know. &lt;strong&gt;-lnosys&lt;/strong&gt; means that I'm just implementing the bare-metal firmware, there is no such as kernel or fully OS.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;7. Output files&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FIRMWARE_ELF:= firmware.elf
FIRMWARE_BIN:= firmware.bin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Generally, we use the ELF file when flashing and debugging. ELF executable includes the both machine code, metadata, debug information, symbol table or similar stuffs. In contrast, BIN executable just includes the machine code. If you look at the size of both, you will use the ELF executable is much bigger! &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;8. The recipe&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.PHONY: all

all:
@echo &quot;-------------------------------------&quot;
@echo &quot;----- Building the source files -----&quot;
@echo &quot;-------------------------------------&quot;
@$(CC) $(AFLAGS) -c $(STARTUP_CODE)
@$(CC) $(CFLAGS) -c $(SOURCES)

@echo &quot;\n------------------------------------&quot;
@echo &quot;----- Linking the object files -----&quot;
@echo &quot;------------------------------------&quot;
@$(CC) $(LDFLAGS) $(OBJECTS) -o $(FIRMWARE_ELF)
@$(OBJCOPY) -O binary $(FIRMWARE_ELF) $(FIRMWARE_BIN)
@$(RM) $(OBJECTS)

@echo &quot;\n-------------------------------------&quot;
@echo &quot;----- The firmware memory usage -----&quot;
@echo &quot;-------------------------------------&quot;
@$(SIZE) $(FIRMWARE_ELF)

@echo &quot;\n-------------------------------------&quot;
@echo &quot;----- The firmware binary format ----&quot;
@echo &quot;-------------------------------------&quot;
@$(FILE) $(FIRMWARE_ELF)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lastly, run the Makefile:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ make
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's it . You've built the both &lt;strong&gt;firmware.elf&lt;/strong&gt; and &lt;strong&gt;firmware.bin&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;You are ready to flash the built firmware into microcontroller with this or similar command (over ST-Link connection):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c &quot;program firmware.elf verify reset exit&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I've also published a repository for that. You can look at it from here: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/CanGulmez/Embedded-Firmware-with-Makefile&quot;&gt;https://github.com/CanGulmez/Embedded-Firmware-with-Makefile&lt;/a&gt;&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/14088/how-to-build-a-firmware-using-makefile</guid>
<pubDate>Mon, 06 Apr 2026 15:13:09 +0000</pubDate>
</item>
<item>
<title>Flashing and Debugging A Firmware in Detail</title>
<link>https://coderlegion.com/14087/flashing-and-debugging-a-firmware-in-detail</link>
<description>&lt;p&gt;In this tutorial, I will inspect, flash and debug a firmware written for STM32F446RE microcontroller. To accomplish this, we need some dedicated tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;openocd&lt;/li&gt;
&lt;li&gt;arm-none-eabi-*&lt;/li&gt;
&lt;li&gt;gdb-multiarch&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;openocd is the primarily tool that we have to get. It's used as bridge between your host machine and debugger hardware. (I'm using use ST-Link hardware in this time). I will use it to connect to the debugger hardware. arm-none-eabi-* tools are the ARM toolchain   used in development of the firmware. gdb-multiarch is the debugger too.&lt;/p&gt;
&lt;p&gt;You can get these tools:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ sudo apt install openocd gcc-arm-none-eabi gdb-multiarch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Note: I'm on Ubuntu 24.04, gcc-arm-none-eabi tools don't include the gdb debugger like arm-none-eabi-gdb. So that gdb-multiarch is being installed externally. On some distribution, the gdb debugger comes with gcc-arm-none-eabi.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Suppose that I have a firmware called &quot;firmware.elf&quot; and wanna inspect it as first step. &lt;/p&gt;
&lt;p&gt;The first thing that you should do probably is to determine the firmware itself:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;$ file firmware.elf
firmware.elf: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;em&gt;file&lt;/em&gt; command simply outputs the firmware properties like the endiannes, architecture, or so on. You can see easily that this firmware was written for 32-bit ARM Cortex.&lt;/p&gt;
&lt;p&gt;We can see also the section sizes (in bytes) of the firmware with &lt;em&gt;arm-none-eabi-size&lt;/em&gt; command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;$ arm-none-eabi-size firmware.elf
   text   data    bss    dec    hexfilename
   5252     20   1732   7004   1b5cfirmware.elf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you are not familiar with sections of the executables, I've wrote many tutorials that explains the ELF executable. You can look at there.&lt;/p&gt;
&lt;p&gt;If you exactly wanna see the contents of the firmware, &lt;em&gt;arm-none-eabi-objdump&lt;/em&gt; is the primarily tool for that:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;$ arm-none-eabi-objdump -j .text -d firmware.elf

firmware.elf:     file format elf32-littlearm


Disassembly of section .text:

(...)

0800079c &amp;lt;main&amp;gt;:
 800079c:b500      push{lr}
 800079e:b091      subsp, #68@ 0x44
 80007a0:f000 f8ac bl80008fc &amp;lt;HAL_Init&amp;gt;
 80007a4:f000 f82c bl8000800 &amp;lt;configOscClk&amp;gt;
 80007a8:f000 f844 bl8000834 &amp;lt;configDebugPort&amp;gt;
 80007ac:4911      ldrr1, [pc, #68]@ (80007f4 &amp;lt;main+0x58&amp;gt;)
 80007ae:4d12      ldrr5, [pc, #72]@ (80007f8 &amp;lt;main+0x5c&amp;gt;)
 80007b0:4c12      ldrr4, [pc, #72]@ (80007fc &amp;lt;main+0x60&amp;gt;)
 80007b2:4668      movr0, sp
 80007b4:f7ff ff08 bl80005c8 &amp;lt;strcpy&amp;gt;
 80007b8:4668      movr0, sp
 80007ba:f7ff ff81 bl80006c0 &amp;lt;strlen&amp;gt;
 80007be:f04f 33ff mov.wr3, #4294967295@ 0xffffffff
 80007c2:b282      uxthr2, r0
 80007c4:4669      movr1, sp
 80007c6:480d      ldrr0, [pc, #52]@ (80007fc &amp;lt;main+0x60&amp;gt;)
 80007c8:f000 fdc2 bl8001350 &amp;lt;HAL_UART_Transmit&amp;gt;
 80007cc:4629      movr1, r5
 80007ce:4668      movr0, sp
 80007d0:f7ff fefa bl80005c8 &amp;lt;strcpy&amp;gt;
 80007d4:4668      movr0, sp
 80007d6:f7ff ff73 bl80006c0 &amp;lt;strlen&amp;gt;
 80007da:f04f 33ff mov.wr3, #4294967295@ 0xffffffff
 80007de:b282      uxthr2, r0
 80007e0:4669      movr1, sp
 80007e2:4620      movr0, r4
 80007e4:f000 fdb4 bl8001350 &amp;lt;HAL_UART_Transmit&amp;gt;
 80007e8:f44f 707a mov.wr0, #1000@ 0x3e8
 80007ec:f000 f8b2 bl8000954 &amp;lt;HAL_Delay&amp;gt;
 80007f0:e7ec      b.n80007cc &amp;lt;main+0x30&amp;gt;
 80007f2:bf00      nop
 80007f4:0800146c .word0x0800146c
 80007f8:08001489 .word0x08001489
 80007fc:20000084 .word0x20000084

(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example, you see the &lt;em&gt;main()&lt;/em&gt; function in &lt;em&gt;.text&lt;/em&gt; section of the firmware. At left, the address of the machine code were being displayed. At center, you see the machine code corresponding to assembly instructions that is at right side.&lt;/p&gt;
&lt;p&gt;Inspecting the contents of ELF executable is huge topic. It has many variants and view-of-points in there.&lt;/p&gt;
&lt;p&gt;&quot;Flashing&quot; term means writing the dedicated sections of the firmware into the flash memory area of the microcontroller. It's done with many different ways. But I will show you openocd tool.&lt;/p&gt;
&lt;p&gt;In here, I have to define that how exactly does openocd work. It actually creates a server into the host machine with given configuration files. After starting the server session, you need to connect to that server over telnet or gdb. &lt;/p&gt;
&lt;p&gt;Let's do it. Firstly, connect the microcontroller to the host machine. Then open a terminal session and type this command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In here, I gave the two configuration files. First is the interface. As I said, I use the ST-Link connection. Second is the target file. It defines the target architecture. The openocd comes a bunch of configuration files. So search it and find the appropriate configuration files.&lt;/p&gt;
&lt;p&gt;You will see something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;Open On-Chip Debugger 0.12.0
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport &quot;hla_swd&quot;. To override use 'transport select &amp;lt;transport&amp;gt;'.
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : clock speed 2000 kHz
Info : STLINK V2J33M25 (API v2) VID:PID 0483:374B
Info : Target voltage: 3.224656
Info : [stm32f4x.cpu] Cortex-M4 r0p1 processor detected
Info : [stm32f4x.cpu] target has 6 breakpoints, 4 watchpoints
Info : starting gdb server for stm32f4x.cpu on 3333
Info : Listening on port 3333 for gdb connections
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The openocd attached the 4444 port to telnet and 3333 port to gdb. So that I will bind the 3333 port in the gdb session.&lt;/p&gt;
&lt;p&gt;Secondly, open the second terminal and type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ gdb-multiarch firmware.elf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At the moment, you are in the gdb session. In here, bind to the 3333 port:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;(gdb) target remote localhost:3333
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For now, you are in the control and responsible for the all flashing and debugging operations.&lt;/p&gt;
&lt;p&gt;To flash the firmware, use these commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;(gdb) monitor reset halt
(gdb) load
(gdb) monitor reset halt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are tons of commands you can type in the gdb session. We can split these commands in two categories. &lt;em&gt;monitor &amp;lt;command-name&amp;gt;&lt;/em&gt; commands directly are run by the openocd command. Other commands are run by the gdb itself.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;load&lt;/em&gt; is the main command to flash the firmware. Before and after that command, resetting and halting the CPU execution is good practice. Output will be like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Loading section .isr_vector, size 0x1c4 lma 0x8000000
Loading section .text, size 0x126c lma 0x8000200
Loading section .rodata, size 0x4c lma 0x800146c
Loading section .ARM, size 0x8 lma 0x80014b8
Loading section .init_array, size 0x4 lma 0x80014c0
Loading section .fini_array, size 0x4 lma 0x80014c4
Loading section .data, size 0xc lma 0x80014c8
Start address 0x08001400, load size 5272
Transfer rate: 9 KB/sec, 753 bytes/write.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That means that the firmware successfully was flashed .&lt;/p&gt;
&lt;p&gt;If you just wanna flash the firmware and not debugging, this following command is enough:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c &quot;program firmware.elf verify reset exit&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of dealing with two terminal sessions, above command is just the one-step operation.&lt;/p&gt;
&lt;p&gt;The main reason opening the second gdb session is to make the debugging. If you did the debugging previously, you will be familiar with these commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;(gdb) break main
(gdb) continue
(gdb) next
(gdb) finish
(gdb) print var
(gdb) x/16wx *ptr
(gdb) set var
(gdb) list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The debugging operations are huge and will not fit into a single tutorial as you guest. So that I don't wanna dive into the these and other operations. But I wanna give you a good reference: &lt;em&gt;The Art of Debugging with GDB, DDD, and Eclipse&lt;/em&gt; written by Norman Matloff and Peter Jay Salzman.&lt;/p&gt;
&lt;p&gt;Until here, I gave a overall explanation the end-to-end pipeline. I'll explain the other stuffs in further. &lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/14087/flashing-and-debugging-a-firmware-in-detail</guid>
<pubDate>Mon, 06 Apr 2026 14:53:30 +0000</pubDate>
</item>
<item>
<title>JsonX: Lightweight JSON to C struct mapping for Embedded Systems</title>
<link>https://coderlegion.com/6601/jsonx-lightweight-json-to-c-struct-mapping-for-embedded-systems</link>
<description>&lt;h3 id=&quot;jsonx-lightweight-json-c-struct-mapping-for-embedded-systems&quot;&gt;JsonX: Lightweight JSON → C struct mapping for Embedded Systems&lt;/h3&gt;
&lt;p&gt;JsonX is a lightweight C library for embedded developers who need to map JSON directly into structs — without dynamic memory or code generators.&lt;/p&gt;
&lt;p&gt;JSON is everywhere - from web APIs to IoT devices.&lt;/p&gt;
&lt;p&gt;But on microcontrollers, most JSON libraries feel like elephants in a china shop: they assume megabytes of RAM, malloc on every step, and a friendly OS.&lt;/p&gt;
&lt;p&gt;Typical JSON libraries for embedded systems fall into two extremes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full-featured giants (e.g. &lt;strong&gt;ArduinoJson, jansson&lt;/strong&gt;): powerful but memory-hungry.&lt;/li&gt;
&lt;li&gt;Bare parsers (e.g. &lt;strong&gt;JSMN, cJSON, jfes&lt;/strong&gt;): no mapping, just tokenization.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-the-problem-with-json-on-embedded-devices&quot;&gt; The Problem with JSON on Embedded Devices&lt;/h3&gt;
&lt;p&gt;When you work with RTOS or bare-metal firmware where every byte and cycle counts.&lt;br&gt;
That’s where &lt;strong&gt;JsonX&lt;/strong&gt; comes in - a lightweight layer over &lt;strong&gt;cJSON&lt;/strong&gt; designed specifically for MCU + RTOS environments, where predictable memory and simplicity matter most.&lt;/p&gt;
&lt;p&gt;So you either waste RAM or write tons of manual traversal code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// classical pain
value = cJSON_GetObjectItem(root, &quot;config&quot;);
param = cJSON_GetObjectItem(value, &quot;baudrate&quot;);
baudrate = param-&amp;gt;valueint;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;-jsonx-in-one-sentence&quot;&gt; JsonX in One Sentence&lt;/h3&gt;
&lt;p&gt;A thin mapping layer that turns JSON into C structs using a declarative table, not hand-written traversal.&lt;/p&gt;
&lt;p&gt;Example structure:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct
{
    char      device_name[32];
    double    baudrate;
    bool      debug;
} config_t;

config_t config;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mapping:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;JX_ELEMENT config_desc[] =
{
    JX_PROPERTY_STRING(&quot;device_name&quot;, config.device_name),
    JX_PROPERTY_NUMBER(&quot;baudrate&quot;, config.baudrate),
    JX_PROPERTY_BOOLEAN(&quot;debug&quot;, config.debug),
};

const size_t config_desc_size = sizeof(config_desc)/sizeof(config_desc[0]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that’s it - one call:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const char *config_json_str = &quot;{\&quot;device_name\&quot;:\&quot;sensor1\&quot;,\&quot;baudrate\&quot;:9600,\&quot;debug\&quot;:true}&quot;;

if(jx_json_to_struct(char *buffer, config_desc, config_desc_size, JX_MODE_STRICT) == JX_SUCCESS)
{
    jx_log(&quot;Configuration loaded: %s @ %lu\n&quot;, config.device_name, (uint32_t)config.baudrate);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No malloc() surprises, no deep tree traversal.&lt;/p&gt;
&lt;h3 id=&quot;-core-benefits&quot;&gt; Core Benefits&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Predictable Memory&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;All allocations go through RTOS pools or a static buffer.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No hidden malloc/free. Works under &lt;strong&gt;ThreadX, FreeRTOS, or bare-metal&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Automatic Struct Mapping&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Define once → parse many.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Shared parsing engine for all schemas.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Optional / Ignored Fields&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Unknown keys ignored automatically.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Optional fields can have defaults.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Makes firmware tolerant to JSON changes.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Readable and Debuggable&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You still get readable JSON in logs - easier to verify on target or&lt;br&gt;
via serial output.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-jsonx-vs-binary-formats&quot;&gt;⚖️ JsonX vs Binary Formats&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;When NOT to use JsonX:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You control both ends and need max bandwidth (LoRa, NB-IoT)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You rely on schema versioning (protobuf/flatbuffers)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;When JsonX shines:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You want human-readable logs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You interface with web APIs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You want deterministic memory use&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You prefer C-only solution&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-example-parsing-an-array-of-coordinates&quot;&gt; Example: Parsing an Array of Coordinates&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;    #define JX_USER_BUFFER_SIZE    1024    
    // Initialize JsonX (using standard malloc/free)
    jx_init();
    
    // Define point structure
    typedef struct {
        double x;
        double y;
    } Point_t;
        
    // Define square structure with array of 4 points
    typedef struct {
        Point_t points[4];
    } Square_t;
        
        
    // Create square data structure
    Square_t square = {
        .points = {
            {0, 0},
            {1, 0},
            {1, 1},
            {0, 1}
        }
    };
        
    // Define JSON mapping for a single point object
    JX_ELEMENT point_0[] = {
        JX_PROPERTY_NUMBER(&quot;x&quot;, square.points[0].x),
        JX_PROPERTY_NUMBER(&quot;y&quot;, square.points[0].y)
    };
    
    JX_ELEMENT point_1[] = {
        JX_PROPERTY_NUMBER(&quot;x&quot;, square.points[1].x),
        JX_PROPERTY_NUMBER(&quot;y&quot;, square.points[1].y)
    };
    
    JX_ELEMENT point_2[] = {
        JX_PROPERTY_NUMBER(&quot;x&quot;, square.points[2].x),
        JX_PROPERTY_NUMBER(&quot;y&quot;, square.points[2].y)
    };
    
    JX_ELEMENT point_3[] = {
        JX_PROPERTY_NUMBER(&quot;x&quot;, square.points[3].x),
        JX_PROPERTY_NUMBER(&quot;y&quot;, square.points[3].y)
    };
    
    // Define array of point objects
    JX_ELEMENT square_array[] = {
        JX_OBJECT_VAL(point_0),
        JX_OBJECT_VAL(point_1),
        JX_OBJECT_VAL(point_2),
        JX_OBJECT_VAL(point_3)
    };
    
    // Define root object with &quot;square&quot; property
    JX_ELEMENT root[] = {
        JX_PROPERTY_ARRAY(&quot;square&quot;, square_array)
    };
    
    // ===== SERIALIZATION: C struct -&amp;gt; JSON =====
    char *json_output = jx_alloc_memory(JX_USER_BUFFER_SIZE);
    if (json_output != NULL) {
        jx_struct_to_json(root, 1, json_output, JX_USER_BUFFER_SIZE, JX_FORMATTED);
        jx_log(&quot;Serialized JSON:\n%s\n\n&quot;, json_output);
        jx_free_memory(json_output);
    }
        
    // ===== DESERIALIZATION: JSON -&amp;gt; C struct =====
    const char *json_input =
    &quot;{&quot;
        &quot;  \&quot;square\&quot;: [&quot;
            &quot;    {\&quot;x\&quot;: 5, \&quot;y\&quot;: 6},&quot;
            &quot;    {\&quot;x\&quot;: 7, \&quot;y\&quot;: 8},&quot;
            &quot;    {\&quot;x\&quot;: 9, \&quot;y\&quot;: 10},&quot;
            &quot;    {\&quot;x\&quot;: 11, \&quot;y\&quot;: 12}&quot;
        &quot;  ]&quot;
    &quot;}&quot;;
    
    
    // Parse JSON
    jx_json_to_struct((char*)json_input, root, 1, JX_MODE_STRICT);
    
    // Print parsed data
    jx_log(&quot;Parsed square points:\n&quot;);
    for (int i = 0; i &amp;lt; 4; i++) {
        jx_log(&quot;  Point %d: x=%d, y=%d\n&quot;,
                    i,
                    (int)square.points[i].x,
                    (int)square.points[i].y);
        }
    // Cleanup
    jx_parser_deinit();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;-tested-configuration-stm32h7-threadx--os-gcc-12-&quot;&gt; Tested Configuration: STM32H7 + ThreadX (-Os, GCC 12)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;⚙️ Memory &amp;amp; Code Footprint&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Verified on STM32H7 (GCC 12, -Os, ThreadX)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;jx_parser.o&lt;/strong&gt; — 2228 B code, 4 B static RAM&lt;br&gt;
Core parser and mapping logic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;jx_static_allocator.o&lt;/strong&gt; — 248 B code, 4 B static RAM&lt;br&gt;
Implements static allocator for bare-metal mode.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;jx_version.o&lt;/strong&gt; — 68 B code, 0 B RAM&lt;br&gt;
Simple version string helper.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;example.o (demo)&lt;/strong&gt; — 765 B code, 1076 B RAM&lt;br&gt;
Demo test including a 1 KB byte pool for ThreadX.&lt;/p&gt;
&lt;p&gt;➡ &lt;strong&gt;Core only&lt;/strong&gt;: ≈ 2.5 KB code + 8 B static RAM&lt;/p&gt;
&lt;p&gt;➡ &lt;strong&gt;With example&lt;/strong&gt;: ≈ 3.2 KB code + 1.06 KB RAM&lt;/p&gt;
&lt;p&gt;The demo includes a 1 KB jx_byte_pool_buffer for ThreadX, but the actual pool size is user-defined.&lt;br&gt;
JsonX itself has &lt;strong&gt;no hidden heap allocations&lt;/strong&gt; - all memory management is deterministic.&lt;/p&gt;
&lt;p&gt;The static allocator consumes only ≈ &lt;strong&gt;12–16 bytes&lt;/strong&gt; of header overhead inside the provided buffer.&lt;/p&gt;
&lt;h3 id=&quot;-open-source-and-final-thoughts&quot;&gt; Open Source and Final Thoughts&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/embedmind/JsonX&quot;&gt;https://github.com/embedmind/JsonX&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JsonX&lt;/strong&gt; is lightweight, open-source (&lt;strong&gt;MIT&lt;/strong&gt;), and built purely in C for portability across RTOSes and MCU families.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;This library isn’t about inventing a new protocol - it’s about making life easier for embedded developers who already have to deal with JSON.&lt;/p&gt;
&lt;p&gt;Working with JSON on MCUs? I'd love to hear your use case - especially if you've battled with JSMN, Jansson, or ArduinoJson before.&lt;/p&gt;
&lt;h3 id=&quot;-join-the-discussion&quot;&gt; Join the Discussion&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;⭐ Star on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/embedmind/JsonX&quot;&gt;GitHub&lt;/a&gt; if this solves your problem&lt;/li&gt;
&lt;li&gt;Found a bug? Open an &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/embedmind/JsonX/issues&quot;&gt;issue&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Java</category>
<guid isPermaLink="true">https://coderlegion.com/6601/jsonx-lightweight-json-to-c-struct-mapping-for-embedded-systems</guid>
<pubDate>Fri, 17 Oct 2025 20:51:57 +0000</pubDate>
</item>
</channel>
</rss>