<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pranav Jain</title>
    <description>The latest articles on DEV Community by Pranav Jain (@pranavhj1998).</description>
    <link>https://dev.to/pranavhj1998</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4086376%2Fdbd45688-d88c-410f-b4f4-c88030895b32.png</url>
      <title>DEV Community: Pranav Jain</title>
      <link>https://dev.to/pranavhj1998</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pranavhj1998"/>
    <language>en</language>
    <item>
      <title>AI Code Assistants for Embedded Engineers: What Works, What Doesn</title>
      <dc:creator>Pranav Jain</dc:creator>
      <pubDate>Thu, 20 Aug 2026 10:06:26 +0000</pubDate>
      <link>https://dev.to/pranavhj1998/ai-code-assistants-for-embedded-engineers-what-works-what-doesn-4efg</link>
      <guid>https://dev.to/pranavhj1998/ai-code-assistants-for-embedded-engineers-what-works-what-doesn-4efg</guid>
      <description>&lt;p&gt;I write C for microcontrollers. My code talks to SPI peripherals, configures DMA channels, and runs in environments where a buffer overflow doesn't crash a browser — it crashes a piece of industrial equipment. AI code assistants were not built for this.&lt;/p&gt;

&lt;p&gt;But I use them every day. Here's what actually works, what's dangerous, and where I think this is heading.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Tested
&lt;/h2&gt;

&lt;p&gt;I've been using three AI assistants in my embedded workflow for the past year:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Copilot&lt;/strong&gt; — inline completions in VS Code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude&lt;/strong&gt; — long-form code generation, architecture discussions, code review&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ChatGPT&lt;/strong&gt; — quick questions, datasheet interpretation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used them on real firmware projects: STM32F4, nRF52840, ESP32-S3. Bare-metal and Zephyr RTOS. 20-60K LOC codebases. Here's what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI Assistants Actually Help
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Boilerplate Peripheral Initialization
&lt;/h3&gt;

&lt;p&gt;This is the clearest win. Peripheral init code is formulaic — read the reference manual, fill in the struct fields, enable the clocks, configure the pins. AI assistants are very good at this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; "Write STM32 HAL SPI1 init for PA5/PA6/PA7, Mode 0, 1 MHz, 8-bit"&lt;/p&gt;

&lt;p&gt;Claude gives me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SPI1_Init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;__HAL_RCC_SPI1_CLK_ENABLE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;__HAL_RCC_GPIOA_CLK_ENABLE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;GPIO_InitTypeDef&lt;/span&gt; &lt;span class="n"&gt;gpio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;gpio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GPIO_PIN_5&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;GPIO_PIN_6&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;GPIO_PIN_7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;gpio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GPIO_MODE_AF_PP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;gpio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pull&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GPIO_NOPULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;gpio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Speed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GPIO_SPEED_FREQ_LOW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;gpio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Alternate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GPIO_AF5_SPI1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;HAL_GPIO_Init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GPIOA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gpio&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;hspi1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SPI1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;hspi1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SPI_MODE_MASTER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;hspi1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SPI_DIRECTION_2LINES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;hspi1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SPI_DATASIZE_8BIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;hspi1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLKPolarity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SPI_POLARITY_LOW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;hspi1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLKPhase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SPI_PHASE_1EDGE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;hspi1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaudRatePrescaler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SPI_BAUDRATEPRESCALER_64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;hspi1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FirstBit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SPI_FIRSTBIT_MSB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;HAL_SPI_Init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;hspi1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is correct, took 5 seconds, and would have taken me 10 minutes of cross-referencing the reference manual. The prescaler calculation (64 MHz / 64 = 1 MHz) is right. The AF mapping (AF5 for SPI1 on PA5-7) is right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time saved: 10 minutes per peripheral init. Maybe 30-40 minutes per project.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Protocol Decoders and Data Parsers
&lt;/h3&gt;

&lt;p&gt;Parsing sensor data, decoding protocol frames, bit manipulation — AI assistants handle this well because the logic is well-defined and the edge cases are enumerable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; "Parse this BME280 temperature reading from raw registers"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="nf"&gt;bme280_compensate_temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="n"&gt;adc_T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                       &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bme280_calib&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="n"&gt;var1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((((&lt;/span&gt;&lt;span class="n"&gt;adc_T&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;cal&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;dig_T1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
                    &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;cal&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;dig_T2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="n"&gt;var2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(((((&lt;/span&gt;&lt;span class="n"&gt;adc_T&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;cal&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;dig_T1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
                      &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;adc_T&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;cal&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;dig_T1&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
                    &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;cal&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;dig_T3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;var2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is directly from the BME280 datasheet compensation formula. AI gets it right because it's seen this exact code in thousands of projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Test Scaffolding
&lt;/h3&gt;

&lt;p&gt;AI is good at generating test cases for embedded code — especially when you give it the function signature and describe the edge cases.&lt;/p&gt;

&lt;p&gt;"Write Unity test cases for this CRC-16 function, including: empty input, single byte, known test vector, maximum length buffer."&lt;/p&gt;

&lt;p&gt;This saves 15-20 minutes per function and produces tests I might not have thought of.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Build System Configuration
&lt;/h3&gt;

&lt;p&gt;CMake for cross-compilation is arcane. AI assistants know the incantations:&lt;/p&gt;

&lt;p&gt;"Generate a CMake toolchain file for ARM GCC targeting Cortex-M4 with FPU"&lt;/p&gt;

&lt;p&gt;This consistently produces working output. CMake is well-documented online and AI has seen thousands of examples.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Documentation and Comments
&lt;/h3&gt;

&lt;p&gt;"Add doxygen comments to this driver interface header" — AI does this well. It reads the parameter names, infers the purpose, and produces reasonable documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI Assistants Are Dangerous
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Register-Level Code for Uncommon Peripherals
&lt;/h3&gt;

&lt;p&gt;Ask for LTDC (LCD controller) configuration on STM32F4 and you'll get plausible-looking code that doesn't work. The AI has seen fewer examples of LTDC than SPI, so it generates something that looks right but has wrong timing parameters or missing register fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule:&lt;/strong&gt; If the peripheral has fewer than 1000 open-source code examples on GitHub, don't trust AI-generated register-level code without verifying against the reference manual.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. DMA Configuration
&lt;/h3&gt;

&lt;p&gt;This is where I've seen the most AI-generated bugs. DMA involves channel assignment, priority, FIFO thresholds, memory alignment, and peripheral-specific constraints. AI gets the structure right but misses constraints like "DMA2 Stream 0 Channel 3 is the only valid assignment for SPI1 RX on this specific STM32 variant."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AI-generated DMA config — looks correct but...&lt;/span&gt;
&lt;span class="n"&gt;hdma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DMA_CHANNEL_3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Wrong channel for this peripheral&lt;/span&gt;
&lt;span class="n"&gt;hdma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DMA_PERIPH_TO_MEMORY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;hdma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PeriphInc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DMA_PINC_DISABLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;hdma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MemInc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DMA_MINC_ENABLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;hdma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FIFOThreshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DMA_FIFO_THRESHOLD_FULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Bad choice for small transfers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule:&lt;/strong&gt; Always verify DMA channel assignments against the DMA request mapping table in the reference manual. AI can't reliably do this.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Interrupt Priority and RTOS Integration
&lt;/h3&gt;

&lt;p&gt;AI assistants don't understand the runtime implications of interrupt priorities. They'll generate code that assigns ISR priorities without considering what other interrupts are active, whether the RTOS uses BASEPRI masking, or what &lt;code&gt;configMAX_SYSCALL_INTERRUPT_PRIORITY&lt;/code&gt; is set to in FreeRTOS.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Timing-Critical Code
&lt;/h3&gt;

&lt;p&gt;Anything that depends on cycle-accurate timing — bit-banged protocols, pulse measurement, ISR latency — is a bad fit for AI assistance. The AI doesn't know your clock speed, pipeline behavior, or compiler optimization settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Security-Sensitive Code
&lt;/h3&gt;

&lt;p&gt;Cryptographic operations, secure boot, key storage. Don't use AI for this. The surface area for subtle bugs is too large, and the consequences of getting it wrong are too severe.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Actually Use AI in My Workflow
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Morning:&lt;/strong&gt; Open the project, use Copilot for autocomplete on routine code. It fills in struct initializations, for-loop bodies, and switch-case arms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture questions:&lt;/strong&gt; "I need SPI communication between two MCUs with flow control. What patterns work?" — I ask Claude, get 3 approaches with tradeoffs, then implement the one that fits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debugging:&lt;/strong&gt; "This SPI transfer returns HAL_TIMEOUT. The clock is configured for 1 MHz, CPOL=0, CPHA=0. What should I check?" — AI gives a reasonable debugging checklist. Not always right, but a good starting point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code review:&lt;/strong&gt; Paste a function into Claude and ask "What bugs or edge cases do you see?" — catches things like integer overflow in ADC scaling, missing null checks on buffer pointers, off-by-one in circular buffer indices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I never do:&lt;/strong&gt; Copy-paste AI-generated code into production without reading every line. Especially register-level code. Especially DMA.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Embedded-Specific Gap
&lt;/h2&gt;

&lt;p&gt;AI assistants are trained on web and application code. The embedded domain has unique challenges they handle poorly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hardware datasheets&lt;/strong&gt; are not in their training data (or poorly represented)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time constraints&lt;/strong&gt; aren't something they can reason about&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory-constrained environments&lt;/strong&gt; mean patterns that work in application code (dynamic allocation, string formatting) are wrong in embedded&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vendor-specific errata&lt;/strong&gt; — every MCU has hardware bugs documented in errata sheets. AI doesn't know about them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ideal embedded AI assistant would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Have the MCU's reference manual in context&lt;/li&gt;
&lt;li&gt;Know the specific chip variant and its errata&lt;/li&gt;
&lt;li&gt;Understand RTOS-specific constraints (stack sizes, priority inversions)&lt;/li&gt;
&lt;li&gt;Verify DMA channel assignments against the mapping table&lt;/li&gt;
&lt;li&gt;Flag timing assumptions that depend on clock configuration&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We're not there yet. But we're closer than we were a year ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;AI code assistants save me 30-60 minutes per day on embedded projects. Mostly from boilerplate generation, test scaffolding, and build system configuration.&lt;/p&gt;

&lt;p&gt;They produce dangerous output for DMA, interrupt priorities, and uncommon peripherals. The cost of a subtle register-level bug in production embedded code is much higher than in a web app — hours of debugging with an oscilloscope and logic analyzer, or worse, a field failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use them as a first draft generator, not a finished code source.&lt;/strong&gt; Every line gets reviewed against the reference manual. That discipline turns AI from a liability into a genuine productivity boost.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Pranav Jain writes middleware and abstraction layers for embedded systems. Find him on &lt;a href="https://github.com/pranavhj" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>embedded</category>
      <category>ai</category>
      <category>productivity</category>
      <category>firmware</category>
    </item>
    <item>
      <title>I Reviewed 5 Open-Source HALs — What They Got Right and Wrong</title>
      <dc:creator>Pranav Jain</dc:creator>
      <pubDate>Thu, 20 Aug 2026 10:01:27 +0000</pubDate>
      <link>https://dev.to/pranavhj1998/i-reviewed-5-open-source-hals-what-they-got-right-and-wrong-4kn0</link>
      <guid>https://dev.to/pranavhj1998/i-reviewed-5-open-source-hals-what-they-got-right-and-wrong-4kn0</guid>
      <description>&lt;p&gt;I've spent the last few years writing abstraction layers between hardware and application software. Part of my job is knowing what's out there, what works, and what breaks when you need it most. So I went through five open-source hardware abstraction layers and evaluated them on the criteria that matter for production firmware.&lt;/p&gt;

&lt;p&gt;Here's what I found.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Evaluation Criteria
&lt;/h2&gt;

&lt;p&gt;I scored each HAL on five dimensions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Portability&lt;/strong&gt; — How many MCU families does it support? How hard is it to add a new one?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt; — What's the overhead vs direct register access?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Design&lt;/strong&gt; — Is the API intuitive? Consistent? Does it leak hardware details?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation&lt;/strong&gt; — Can a new developer figure it out without reading the source?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production Readiness&lt;/strong&gt; — Is it used in real products? Are there gotchas?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Scale: 1 (poor) to 5 (excellent).&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Zephyr RTOS Device Driver Model
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; Zephyr isn't just an RTOS — it's a full operating system with a driver model based on Linux's device tree concept. Hardware is described in &lt;code&gt;.dts&lt;/code&gt; files, and drivers bind to device tree nodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example — GPIO:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;zephyr/drivers/gpio.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="cp"&gt;#define LED_NODE DT_ALIAS(led0)
&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;gpio_dt_spec&lt;/span&gt; &lt;span class="n"&gt;led&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GPIO_DT_SPEC_GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LED_NODE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gpios&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;gpio_pin_configure_dt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;led&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GPIO_OUTPUT_ACTIVE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;gpio_pin_toggle_dt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;led&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;k_msleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What they got right:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Device tree separates hardware description from driver code. Your app code literally doesn't know which MCU it's running on.&lt;/li&gt;
&lt;li&gt;Consistent API across all peripherals. GPIO, SPI, I2C, UART all follow the same pattern.&lt;/li&gt;
&lt;li&gt;Massive MCU support — STM32, nRF, ESP32, NXP, TI, Renesas, Microchip, RISC-V families.&lt;/li&gt;
&lt;li&gt;Active community, regular releases, Linux Foundation backing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What they got wrong:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Learning curve is steep. Device tree overlay syntax is intimidating for embedded engineers coming from bare-metal.&lt;/li&gt;
&lt;li&gt;Build system (west + CMake + Kconfig) is powerful but complex. First build can take 30 minutes of setup.&lt;/li&gt;
&lt;li&gt;Overhead isn't zero. The driver model uses function pointer dispatch, which adds a few cycles per call. Fine for most apps, not ideal for bit-banging at MHz speeds.&lt;/li&gt;
&lt;li&gt;Flash footprint starts around 40-60KB minimum. Not viable for small Cortex-M0 parts with 32KB flash.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scores:&lt;/strong&gt;&lt;br&gt;
| Portability | Performance | API Design | Documentation | Production Ready |&lt;br&gt;
|:-:|:-:|:-:|:-:|:-:|&lt;br&gt;
| 5 | 3 | 4 | 4 | 5 |&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; Best choice if your project can afford the footprint and learning curve. The portability is unmatched.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. CMSIS (Cortex Microcontroller Software Interface Standard)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; ARM's official standard for Cortex-M software interfaces. Defines core access functions, DSP intrinsics, RTOS API, and driver APIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example — GPIO (CMSIS-Driver):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"Driver_GPIO.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="n"&gt;ARM_DRIVER_GPIO&lt;/span&gt; &lt;span class="n"&gt;Driver_GPIO0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;led_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Driver_GPIO0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Pin 5&lt;/span&gt;
    &lt;span class="n"&gt;Driver_GPIO0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetDirection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ARM_GPIO_OUTPUT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;led_toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;Driver_GPIO0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What they got right:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's a &lt;em&gt;standard&lt;/em&gt;. If every vendor implemented it, portability would be solved.&lt;/li&gt;
&lt;li&gt;CMSIS-Core (register access, NVIC, SysTick) is universally used and excellent.&lt;/li&gt;
&lt;li&gt;CMSIS-DSP is genuinely useful and well-optimized.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What they got wrong:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Almost nobody implements CMSIS-Driver in practice. Vendors ship their own HALs (STM32 HAL, nRF drivers) instead.&lt;/li&gt;
&lt;li&gt;The driver API is over-abstracted. It tries to be generic enough for every peripheral on every chip, resulting in an API that's awkward for all of them.&lt;/li&gt;
&lt;li&gt;Documentation exists but is dense and spec-like, not tutorial-like.&lt;/li&gt;
&lt;li&gt;CMSIS-RTOS is a wrapper API, not a real RTOS. It adds overhead without adding capability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scores:&lt;/strong&gt;&lt;br&gt;
| Portability | Performance | API Design | Documentation | Production Ready |&lt;br&gt;
|:-:|:-:|:-:|:-:|:-:|&lt;br&gt;
| 2 | 4 | 2 | 3 | 3 |&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; CMSIS-Core is essential. CMSIS-Driver is a good idea that the industry ignored. Don't build on it unless you want to maintain the vendor implementations yourself.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Arduino HAL
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; The most successful embedded abstraction layer in terms of adoption. &lt;code&gt;digitalWrite()&lt;/code&gt;, &lt;code&gt;analogRead()&lt;/code&gt;, &lt;code&gt;Serial.begin()&lt;/code&gt; — you know it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;pinMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LED_BUILTIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OUTPUT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Serial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;115200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;digitalWrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LED_BUILTIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HIGH&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;digitalWrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LED_BUILTIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LOW&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Serial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"blink"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What they got right:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simplicity. A beginner can blink an LED in 5 minutes. That's an achievement.&lt;/li&gt;
&lt;li&gt;Massive ecosystem. Libraries for every sensor, display, and communication module.&lt;/li&gt;
&lt;li&gt;Runs on Arduino AVR, ESP32 (arduino-esp32), STM32 (STM32duino), nRF (Adafruit), RP2040. Genuine portability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What they got wrong:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No DMA, no interrupts (without platform-specific extensions), no low-power modes. The API hides too much.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;digitalWrite()&lt;/code&gt; is &lt;em&gt;slow&lt;/em&gt;. On AVR, it's ~50 clock cycles vs 2 for direct port manipulation. On ARM it's better, but still has overhead from pin lookup.&lt;/li&gt;
&lt;li&gt;Global state everywhere. One &lt;code&gt;Serial&lt;/code&gt; object, one &lt;code&gt;SPI&lt;/code&gt; object. Multi-instance is hacked in.&lt;/li&gt;
&lt;li&gt;C++ requirement. Many embedded teams work in C. Arduino forces C++ with its class-based API.&lt;/li&gt;
&lt;li&gt;Error handling is nonexistent. Functions either work or silently fail. No error codes, no status flags.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scores:&lt;/strong&gt;&lt;br&gt;
| Portability | Performance | API Design | Documentation | Production Ready |&lt;br&gt;
|:-:|:-:|:-:|:-:|:-:|&lt;br&gt;
| 4 | 2 | 3 | 5 | 2 |&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; Great for prototypes and education. Not for production firmware that needs DMA, interrupts, or low power. The API design philosophy (hide everything) is the opposite of what embedded needs (control everything).&lt;/p&gt;


&lt;h2&gt;
  
  
  4. libopencm3
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; An open-source, community-maintained firmware library for ARM Cortex-M microcontrollers. Lower level than STM32 HAL, higher level than direct register access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example — GPIO:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;libopencm3/stm32/rcc.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;libopencm3/stm32/gpio.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;led_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;rcc_periph_clock_enable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RCC_GPIOA&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;gpio_mode_setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GPIOA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GPIO_MODE_OUTPUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GPIO_PUPD_NONE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GPIO5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;led_toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;gpio_toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GPIOA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GPIO5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What they got right:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clean, C-only API. No opaque handles, no callback registration, no HAL_StatusTypeDef. Just function calls that map closely to hardware operations.&lt;/li&gt;
&lt;li&gt;Thin. The overhead is minimal — most functions compile to a handful of register writes.&lt;/li&gt;
&lt;li&gt;Supports STM32, EFM32, LPC, SAM, and some others. Not as wide as Zephyr but respectable.&lt;/li&gt;
&lt;li&gt;Good linker scripts and startup code included. Build system is straightforward.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What they got wrong:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Community-maintained means inconsistent coverage. Some MCU families (STM32F1/F4) are well-supported. Others have gaps.&lt;/li&gt;
&lt;li&gt;Not enough abstraction for easy porting. The API uses vendor-specific register names (&lt;code&gt;GPIOA&lt;/code&gt;, &lt;code&gt;RCC_GPIOA&lt;/code&gt;). Porting from STM32 to SAM requires changing every call.&lt;/li&gt;
&lt;li&gt;No RTOS integration. It's purely a peripheral library.&lt;/li&gt;
&lt;li&gt;Documentation is sparse. You'll read source code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scores:&lt;/strong&gt;&lt;br&gt;
| Portability | Performance | API Design | Documentation | Production Ready |&lt;br&gt;
|:-:|:-:|:-:|:-:|:-:|&lt;br&gt;
| 2 | 5 | 4 | 2 | 3 |&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; Excellent for single-MCU projects where you want clean register access without the bloat of vendor HALs. Not a portability solution.&lt;/p&gt;


&lt;h2&gt;
  
  
  5. Tock OS
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; A secure embedded operating system written in Rust. Uses Rust's type system and ownership model to enforce isolation between the kernel, drivers, and applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example — GPIO (Tock capsule):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Kernel-side driver (capsule)&lt;/span&gt;
&lt;span class="k"&gt;impl&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;hil&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;gpio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Pin&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;hil&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;gpio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;GpioDriver&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;fired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.callback&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="nf"&gt;.schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What they got right:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Memory safety guaranteed by the compiler. Buffer overflows, use-after-free, data races — caught at compile time.&lt;/li&gt;
&lt;li&gt;Strong isolation model. Untrusted applications can't crash the kernel or other apps.&lt;/li&gt;
&lt;li&gt;The hardware interface layer (HIL) traits are well-designed. Clean separation between hardware-specific and hardware-independent code.&lt;/li&gt;
&lt;li&gt;Academic rigor — published in SOSP, backed by serious research.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What they got wrong:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rust on embedded is still maturing. Toolchain support, debugging tools, and community knowledge are improving but behind C.&lt;/li&gt;
&lt;li&gt;Very limited MCU support compared to C-based alternatives. Primarily Nordic nRF52, some STM32, some RISC-V.&lt;/li&gt;
&lt;li&gt;Overhead from the capsule/process model. More suitable for application processors (Cortex-M4+) than constrained MCUs.&lt;/li&gt;
&lt;li&gt;Small community. If you hit a problem, there are fewer people who can help.&lt;/li&gt;
&lt;li&gt;Hard to integrate with existing C codebases. If you have 50K lines of C firmware, Tock isn't a migration target.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scores:&lt;/strong&gt;&lt;br&gt;
| Portability | Performance | API Design | Documentation | Production Ready |&lt;br&gt;
|:-:|:-:|:-:|:-:|:-:|&lt;br&gt;
| 2 | 3 | 5 | 3 | 2 |&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; The future of secure embedded systems, but not the present for most teams. If you're starting a greenfield project on a supported MCU and your team knows Rust, it's worth evaluating.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary Scorecard
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;HAL&lt;/th&gt;
&lt;th&gt;Portability&lt;/th&gt;
&lt;th&gt;Performance&lt;/th&gt;
&lt;th&gt;API Design&lt;/th&gt;
&lt;th&gt;Docs&lt;/th&gt;
&lt;th&gt;Production&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Zephyr&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;21&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CMSIS&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arduino&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;libopencm3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tock&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I'd Actually Recommend
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For a new production project that needs portability:&lt;/strong&gt; Zephyr. The learning curve is real, but the portability payoff is worth it. The ecosystem is growing fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For a bare-metal project on a single MCU:&lt;/strong&gt; libopencm3 (if your MCU is supported) or the vendor HAL with a thin abstraction layer on top.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For a prototype or proof-of-concept:&lt;/strong&gt; Arduino. Get it working, then rewrite for production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For security-critical applications on supported hardware:&lt;/strong&gt; Look at Tock. It's early but the safety guarantees are compelling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For everyone:&lt;/strong&gt; Don't use CMSIS-Driver. Use CMSIS-Core (it's great), but build your own driver abstraction or use Zephyr's.&lt;/p&gt;

&lt;p&gt;And regardless of which HAL you choose — keep vendor types out of your application code. That single rule does more for portability than any framework.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Pranav Jain builds the middleware between hardware and application software. Find him on &lt;a href="https://github.com/pranavhj" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>embedded</category>
      <category>opensource</category>
      <category>firmware</category>
      <category>c</category>
    </item>
    <item>
      <title>How to Test Firmware Without Physical Hardware</title>
      <dc:creator>Pranav Jain</dc:creator>
      <pubDate>Thu, 20 Aug 2026 10:01:16 +0000</pubDate>
      <link>https://dev.to/pranavhj1998/how-to-test-firmware-without-physical-hardware-1gj4</link>
      <guid>https://dev.to/pranavhj1998/how-to-test-firmware-without-physical-hardware-1gj4</guid>
      <description>&lt;p&gt;Most firmware teams test by flashing the board and watching an LED blink. That works until your test matrix is 200 cases, your CI pipeline needs to run on every commit, and the dev kit is on someone else's desk.&lt;/p&gt;

&lt;p&gt;Here's how I test firmware without hardware for roughly 80% of the test surface. The remaining 20% — timing-critical code, analog peripherals, RF — still needs real silicon. But 80% is enough to catch the bugs that matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Testing Pyramid for Firmware
&lt;/h2&gt;

&lt;p&gt;Borrow the concept from web development but adapt it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        /\
       /  \       Hardware-in-the-loop (HIL)
      /    \      Real board, real peripherals
     /------\
    /        \    Integration tests on target
   /          \   QEMU or native_sim
  /------------\
 /              \  Unit tests on host (x86)
/________________\ Mocked HAL, no hardware
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most of your tests should be at the bottom. Fast, cheap, run on any machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 1: Unit Tests on Host (x86)
&lt;/h2&gt;

&lt;p&gt;This is the highest-ROI testing strategy for firmware. Compile your application code for your development machine, mock the hardware interfaces, test with any C test framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Setup
&lt;/h3&gt;

&lt;p&gt;Your code needs to be structured so that application logic doesn't directly call vendor HAL functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// application/sensor_reader.c&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"hal/spi.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"hal/gpio.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="cp"&gt;#define SENSOR_CS_PIN ((gpio_pin_t){.port = 0, .pin = 4})
&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;sensor_read_temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int16_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;temp_out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Read temperature register&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="n"&gt;gpio_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SENSOR_CS_PIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spi_transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SPI_BUS_0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;gpio_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SENSOR_CS_PIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;temp_out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int16_t&lt;/span&gt;&lt;span class="p"&gt;)((&lt;/span&gt;&lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now mock the HAL for host testing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// test/mocks/mock_spi.c&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"hal/spi.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;string.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;spi_rx_buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;spi_rx_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;spi_fail_next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;mock_spi_set_rx_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spi_rx_buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;spi_rx_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;mock_spi_set_fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;spi_fail_next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;spi_transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spi_bus_t&lt;/span&gt; &lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spi_fail_next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;spi_fail_next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rx&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;spi_rx_len&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spi_rx_buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// test/test_sensor_reader.c&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"unity.h"&lt;/span&gt;&lt;span class="c1"&gt;  // or any C test framework&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"application/sensor_reader.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"test/mocks/mock_spi.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;test_sensor_read_temperature_normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 25.0°C = 400 raw = 0x0190&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;fake_data&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mh"&gt;0x01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x90&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;mock_spi_set_rx_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fake_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;int16_t&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sensor_read_temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;TEST_ASSERT_EQUAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;TEST_ASSERT_EQUAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;test_sensor_read_temperature_spi_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mock_spi_set_fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;int16_t&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sensor_read_temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;TEST_ASSERT_NOT_EQUAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;test_sensor_read_negative_temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// -10.0°C = -160 raw = 0xFF60&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;fake_data&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mh"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x60&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;mock_spi_set_rx_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fake_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;int16_t&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sensor_read_temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;TEST_ASSERT_EQUAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;TEST_ASSERT_EQUAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compile and run on your laptop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcc &lt;span class="nt"&gt;-o&lt;/span&gt; test_sensor &lt;span class="nb"&gt;test&lt;/span&gt;/test_sensor_reader.c &lt;span class="se"&gt;\&lt;/span&gt;
    application/sensor_reader.c &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nb"&gt;test&lt;/span&gt;/mocks/mock_spi.c &lt;span class="nb"&gt;test&lt;/span&gt;/mocks/mock_gpio.c &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-Iinclude&lt;/span&gt; &lt;span class="nt"&gt;-Itest&lt;/span&gt;/frameworks/unity/src &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nb"&gt;test&lt;/span&gt;/frameworks/unity/src/unity.c
./test_sensor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Runs in milliseconds. No hardware. Catches logic bugs, edge cases, error handling.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Can Test This Way
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Data parsing and protocol decoding&lt;/li&gt;
&lt;li&gt;State machines&lt;/li&gt;
&lt;li&gt;Command handlers&lt;/li&gt;
&lt;li&gt;Configuration validation&lt;/li&gt;
&lt;li&gt;Error handling paths&lt;/li&gt;
&lt;li&gt;Math and algorithms&lt;/li&gt;
&lt;li&gt;Buffer management&lt;/li&gt;
&lt;li&gt;Anything that doesn't depend on timing or real peripherals&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What You CAN'T Test This Way
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Real-time behavior (ISR latency, DMA timing)&lt;/li&gt;
&lt;li&gt;Peripheral initialization sequences&lt;/li&gt;
&lt;li&gt;Power management&lt;/li&gt;
&lt;li&gt;Analog signal paths&lt;/li&gt;
&lt;li&gt;RF communication&lt;/li&gt;
&lt;li&gt;Boot sequences&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Level 2: QEMU for ARM Targets
&lt;/h2&gt;

&lt;p&gt;QEMU emulates ARM Cortex-M processors well enough to run firmware images. It won't emulate your specific board's peripherals, but it handles the CPU, memory map, NVIC, and SysTick.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zephyr + QEMU
&lt;/h3&gt;

&lt;p&gt;Zephyr has first-class QEMU support:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build for QEMU Cortex-M3&lt;/span&gt;
west build &lt;span class="nt"&gt;-b&lt;/span&gt; qemu_cortex_m3 samples/hello_world
west build &lt;span class="nt"&gt;-t&lt;/span&gt; run

&lt;span class="c"&gt;# Output:&lt;/span&gt;
&lt;span class="c"&gt;# *** Booting Zephyr OS build v3.x.0 ***&lt;/span&gt;
&lt;span class="c"&gt;# Hello World! qemu_cortex_m3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs your Zephyr application in QEMU — including the kernel, scheduler, and any drivers that have QEMU backends.&lt;/p&gt;

&lt;h3&gt;
  
  
  What QEMU Gives You
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;RTOS task scheduling and synchronization testing&lt;/li&gt;
&lt;li&gt;Memory allocation and stack overflow detection&lt;/li&gt;
&lt;li&gt;Kernel API correctness&lt;/li&gt;
&lt;li&gt;Multi-threaded logic bugs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What QEMU Doesn't Give You
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Real peripheral behavior (SPI, I2C, GPIO are stubs or absent)&lt;/li&gt;
&lt;li&gt;Real timing (QEMU runs faster or slower than real hardware)&lt;/li&gt;
&lt;li&gt;Board-specific initialization&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Level 3: Zephyr native_sim (Best of Both Worlds)
&lt;/h2&gt;

&lt;p&gt;This is my favorite approach. Zephyr's &lt;code&gt;native_sim&lt;/code&gt; target compiles your firmware as a native Linux/macOS executable. It uses POSIX threads to simulate Zephyr's threading model, and you can link against host-side libraries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;west build &lt;span class="nt"&gt;-b&lt;/span&gt; native_sim samples/hello_world
./build/zephyr/zephyr.exe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this is powerful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Your Zephyr application&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;zephyr/kernel.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;zephyr/drivers/gpio.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gpio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DEVICE_DT_GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DT_NODELABEL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpio0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;gpio_pin_configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GPIO_OUTPUT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;gpio_pin_toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;k_msleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On &lt;code&gt;native_sim&lt;/code&gt;, this compiles to a normal executable. The GPIO driver is a stub that logs calls. You can add assertions, inject faults, and run under Valgrind or AddressSanitizer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;west build &lt;span class="nt"&gt;-b&lt;/span&gt; native_sim &lt;span class="nt"&gt;-DCONFIG_ASAN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;y my_app
./build/zephyr/zephyr.exe
&lt;span class="c"&gt;# AddressSanitizer catches buffer overflows, use-after-free, etc.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Level 4: Hardware-in-the-Loop (HIL)
&lt;/h2&gt;

&lt;p&gt;For the 20% that needs real hardware, automate it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────┐     USB/SWD      ┌──────────┐
│  CI Host │ ──────────────── │  Dev Kit  │
│  (RPi)   │     Serial       │  (DUT)    │
│          │ ──────────────── │           │
└──────────┘                  └──────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Raspberry Pi (or any Linux machine) connected to your dev kit via SWD (for flashing) and serial (for output). The CI pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Flashes the firmware via OpenOCD / pyOCD / nrfjprog&lt;/li&gt;
&lt;li&gt;Resets the board&lt;/li&gt;
&lt;li&gt;Reads serial output&lt;/li&gt;
&lt;li&gt;Asserts on expected output&lt;/li&gt;
&lt;li&gt;Reports pass/fail
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# hil_test.sh&lt;/span&gt;
pyocd flash build/firmware.hex
pyocd reset
&lt;span class="nb"&gt;timeout &lt;/span&gt;10 &lt;span class="nb"&gt;cat&lt;/span&gt; /dev/ttyACM0 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"SELF_TEST: PASS"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"HIL test PASSED"&lt;/span&gt;
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"HIL test FAILED"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CI Pipeline Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/firmware-test.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Firmware Tests&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;unit-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and run unit tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;mkdir build &amp;amp;&amp;amp; cd build&lt;/span&gt;
          &lt;span class="s"&gt;cmake .. -DTARGET=host_test&lt;/span&gt;
          &lt;span class="s"&gt;make&lt;/span&gt;
          &lt;span class="s"&gt;ctest --output-on-failure&lt;/span&gt;

  &lt;span class="na"&gt;zephyr-native&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/zephyrproject-rtos/ci:latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build for native_sim&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;west build -b native_sim app&lt;/span&gt;
          &lt;span class="s"&gt;timeout 30 ./build/zephyr/zephyr.exe || true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run with ASAN&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;west build -b native_sim app -- -DCONFIG_ASAN=y&lt;/span&gt;
          &lt;span class="s"&gt;timeout 30 ./build/zephyr/zephyr.exe&lt;/span&gt;

  &lt;span class="c1"&gt;# HIL tests run on self-hosted runner with physical board&lt;/span&gt;
  &lt;span class="na"&gt;hil-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;self-hosted&lt;/span&gt;  &lt;span class="c1"&gt;# RPi with connected dev kit&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;unit-tests&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;zephyr-native&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Flash and test&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./scripts/hil_test.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unit tests and native_sim run on every commit (free, fast). HIL tests run on merge to main (needs hardware, slower).&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Advice
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with unit tests on host.&lt;/strong&gt; If your code can't compile for x86 because of vendor HAL dependencies, that's the first problem to fix. Introduce a HAL interface, mock it, get your application code compiling on the host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Unity or CMock for C testing.&lt;/strong&gt; They're lightweight, embedded-friendly, and widely used. Unity is just a single .c and .h file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't mock too much.&lt;/strong&gt; If you're mocking 15 interfaces to test one function, your function is too coupled. Refactor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep tests fast.&lt;/strong&gt; All host-side tests should complete in under 10 seconds. If they don't, something is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test error paths.&lt;/strong&gt; The happy path usually works. The bugs are in: SPI timeout handling, buffer overflow on unexpected response length, negative temperature values, config validation edge cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure coverage, but don't worship it.&lt;/strong&gt; 80% coverage with meaningful tests beats 100% coverage with trivial assertions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Payoff
&lt;/h2&gt;

&lt;p&gt;On a recent project, we had 340 unit tests running on x86, 20 integration tests on native_sim, and 12 HIL tests on a self-hosted runner. The unit tests caught 90% of bugs before they ever touched hardware. The average debug cycle went from "flash, observe, wonder, flash again" (15 minutes) to "run test, see failure, fix, run test" (30 seconds).&lt;/p&gt;

&lt;p&gt;It's more work upfront. But firmware debugging is expensive — an hour saved per bug, across hundreds of bugs, across the life of a project, is measured in weeks.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Pranav Jain is an embedded systems engineer focused on middleware, abstraction layers, and developer tooling for firmware teams. Find him on &lt;a href="https://github.com/pranavhj" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>embedded</category>
      <category>testing</category>
      <category>firmware</category>
      <category>ci</category>
    </item>
    <item>
      <title>The Chip Shortage Taught Us One Thing: Don't Vendor-Lock Your Firmware</title>
      <dc:creator>Pranav Jain</dc:creator>
      <pubDate>Thu, 20 Aug 2026 09:53:39 +0000</pubDate>
      <link>https://dev.to/pranavhj1998/the-chip-shortage-taught-us-one-thing-dont-vendor-lock-your-firmware-3ajg</link>
      <guid>https://dev.to/pranavhj1998/the-chip-shortage-taught-us-one-thing-dont-vendor-lock-your-firmware-3ajg</guid>
      <description>&lt;p&gt;Between 2021 and 2024, I watched firmware teams go through the five stages of grief. Their chip was on a 52-week lead time. The drop-in replacement didn't exist. And their entire codebase was welded to one vendor's HAL.&lt;/p&gt;

&lt;p&gt;Now that lead times have mostly normalized, it's tempting to forget. Don't. The shortage exposed a structural weakness in how most teams write firmware, and the fix isn't "keep more inventory." It's in how you architect your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern I Saw Repeatedly
&lt;/h2&gt;

&lt;p&gt;Company ships a product on STM32F4. Works great. Firmware is 30-50K lines of C, tightly coupled to STM32 HAL. Then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;STM32F4 goes to 40-week lead time&lt;/li&gt;
&lt;li&gt;Purchasing finds an nRF52840 that's available NOW&lt;/li&gt;
&lt;li&gt;Engineering estimates the port at "2-3 weeks"&lt;/li&gt;
&lt;li&gt;Actual port takes 8-12 weeks&lt;/li&gt;
&lt;li&gt;Product launch slips a quarter&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I saw this pattern at three different companies between 2022 and 2023. The details varied, but the story was always the same: the firmware was married to the silicon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "Just Use Zephyr" Isn't the Full Answer
&lt;/h2&gt;

&lt;p&gt;The reflexive response is "use Zephyr RTOS" or "use an RTOS with a HAL." And yes, Zephyr's device tree model gives you portability. But:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Most firmware doesn't run an RTOS.&lt;/strong&gt; Bare-metal is still the majority of embedded projects, especially at the lower end. If you're on a Cortex-M0 with 32KB flash, Zephyr isn't an option.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RTOS HALs have their own lock-in.&lt;/strong&gt; You're not locked to STM32 HAL anymore — you're locked to Zephyr's API. If Zephyr's SPI driver doesn't support your use case (say, a specific DMA mode), you're back to writing vendor-specific code anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The abstraction has to be yours.&lt;/strong&gt; The only HAL you fully control is one you wrote. It doesn't need to be complex. It needs to be intentional.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Portable Firmware Actually Looks Like
&lt;/h2&gt;

&lt;p&gt;Here's what the teams that survived the shortage had in common:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. A Thin Peripheral Interface
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hal/gpio.h — YOUR abstraction, not the vendor's&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;gpio_pin_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GPIO_MODE_INPUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;GPIO_MODE_OUTPUT_PP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;GPIO_MODE_OUTPUT_OD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;GPIO_MODE_AF&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;gpio_mode_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;gpio_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gpio_mode_t&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;gpio_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;gpio_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;gpio_toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is maybe 50 lines of header. The implementation file is different per target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hal/stm32/gpio.c&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"hal/gpio.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"stm32f4xx_hal.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;gpio_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gpio_mode_t&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GPIO_InitTypeDef&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;GPIO_MODE_OUTPUT_PP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;GPIO_MODE_OUTPUT_PP&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;GPIO_MODE_INPUT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pull&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GPIO_NOPULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Speed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GPIO_SPEED_FREQ_LOW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;GPIO_TypeDef&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="cm"&gt;/* port lookup */&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;HAL_GPIO_Init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hal/nrf52/gpio.c&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"hal/gpio.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"nrf_gpio.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;gpio_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gpio_mode_t&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;nrf_pin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NRF_GPIO_PIN_MAP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;GPIO_MODE_OUTPUT_PP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;nrf_gpio_cfg_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nrf_pin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;nrf_gpio_cfg_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nrf_pin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NRF_GPIO_PIN_NOPULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the shortage hit, the team with this structure swapped backends in 3 days. The team without it spent 6 weeks.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Build System That Supports Multiple Targets
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cmake"&gt;&lt;code&gt;&lt;span class="c1"&gt;# CMakeLists.txt&lt;/span&gt;
&lt;span class="nb"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;TARGET_MCU &lt;span class="s2"&gt;"Target MCU family"&lt;/span&gt; &lt;span class="s2"&gt;"stm32f4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;TARGET_MCU STREQUAL &lt;span class="s2"&gt;"stm32f4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;add_subdirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;hal/stm32&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;target_compile_definitions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;app PRIVATE TARGET_STM32F4&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;elseif&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;TARGET_MCU STREQUAL &lt;span class="s2"&gt;"nrf52"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;add_subdirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;hal/nrf52&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;target_compile_definitions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;app PRIVATE TARGET_NRF52&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;endif&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your build system can't switch targets with a single flag, your abstraction isn't real.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. No Vendor Types in Application Code
&lt;/h3&gt;

&lt;p&gt;This is the rule that matters most. If your application code contains &lt;code&gt;GPIO_TypeDef&lt;/code&gt;, &lt;code&gt;nrf_gpio_pin_map&lt;/code&gt;, or &lt;code&gt;esp_err_t&lt;/code&gt;, you have a portability problem. Vendor types belong in the backend implementation files — nowhere else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BAD — vendor type leaked into application&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sensor_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GPIO_InitTypeDef&lt;/span&gt; &lt;span class="n"&gt;gpio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;gpio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GPIO_PIN_5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;gpio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GPIO_MODE_OUTPUT_PP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;HAL_GPIO_Init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GPIOA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gpio&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// GOOD — application uses your types only&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sensor_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;cs_pin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;gpio_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cs_pin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GPIO_MODE_OUTPUT_PP&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Test Without Hardware
&lt;/h3&gt;

&lt;p&gt;The teams that ported fastest had tests that ran on their host machine (x86), not on the target. They mocked the HAL interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// test/mock_gpio.c&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;gpio_states&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;gpio_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;gpio_states&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;gpio_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;gpio_states&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When they switched MCUs, the application tests still passed. They only needed new tests for the backend implementation — which is a much smaller surface area.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cost of NOT Doing This
&lt;/h2&gt;

&lt;p&gt;Let me put numbers on it. From three migrations I was involved with:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;LOC&lt;/th&gt;
&lt;th&gt;Abstraction?&lt;/th&gt;
&lt;th&gt;Port Time&lt;/th&gt;
&lt;th&gt;Bugs Found Post-Port&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;45K&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;11 weeks&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;38K&lt;/td&gt;
&lt;td&gt;Partial (GPIO/UART only)&lt;/td&gt;
&lt;td&gt;5 weeks&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;52K&lt;/td&gt;
&lt;td&gt;Full (all peripherals)&lt;/td&gt;
&lt;td&gt;2 weeks&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Project C had more code but ported 5x faster. The abstraction cost maybe 2 weeks to build originally. It paid for itself on the first port.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "But We'll Never Switch" Fallacy
&lt;/h2&gt;

&lt;p&gt;I've heard this from every team that eventually had to switch. "We're committed to STM32." "Nordic is our long-term partner." "We'll never need to port."&lt;/p&gt;

&lt;p&gt;Until:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your chip goes EOL&lt;/li&gt;
&lt;li&gt;A new product variant needs a different feature set&lt;/li&gt;
&lt;li&gt;Your customer requires a specific silicon vendor&lt;/li&gt;
&lt;li&gt;A cheaper chip cuts your BOM by $2 (which, at volume, is millions)&lt;/li&gt;
&lt;li&gt;Another shortage hits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The question isn't whether you'll port. It's when.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do on a New Project Today
&lt;/h2&gt;

&lt;p&gt;If I were starting a bare-metal project tomorrow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Day 1:&lt;/strong&gt; Define the peripheral interface (GPIO, SPI, I2C, UART, Timer — maybe 200 lines of headers total)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 2-3:&lt;/strong&gt; Implement backend for the primary target&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 3:&lt;/strong&gt; Set up CMake with target selection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 3:&lt;/strong&gt; Write mock backends for host testing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ongoing:&lt;/strong&gt; Never let vendor types leak past the HAL boundary&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total overhead: 2-3 days. Insurance against a multi-week port later.&lt;/p&gt;

&lt;p&gt;If you're using an RTOS, the RTOS likely provides the abstraction. But verify: can you actually switch targets with just a config change? If not, you have hidden vendor dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Broader Lesson
&lt;/h2&gt;

&lt;p&gt;The chip shortage wasn't a freak event. It was a stress test. And it revealed that most firmware architectures are optimized for the happy path (one chip, forever) rather than the realistic path (you'll switch eventually).&lt;/p&gt;

&lt;p&gt;The fix is simple. Not easy — it requires discipline to keep vendor code out of your application layer. But simple. A thin abstraction, a switchable build system, and the willingness to spend 2-3 days on infrastructure at the start of a project.&lt;/p&gt;

&lt;p&gt;The teams that did this barely noticed the shortage. Everyone else had a very expensive year.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Pranav Jain is an embedded systems engineer specializing in middleware and abstraction layers between hardware and application software. He's building &lt;a href="https://pranavhj.github.io/portpilot-landing/" rel="noopener noreferrer"&gt;PortPilot&lt;/a&gt;, a tool that automates MCU migration analysis. Find him on &lt;a href="https://github.com/pranavhj" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>embedded</category>
      <category>firmware</category>
      <category>architecture</category>
      <category>hardware</category>
    </item>
    <item>
      <title>Designing a HAL That Doesn't Lock You In</title>
      <dc:creator>Pranav Jain</dc:creator>
      <pubDate>Thu, 20 Aug 2026 09:53:28 +0000</pubDate>
      <link>https://dev.to/pranavhj1998/designing-a-hal-that-doesnt-lock-you-in-50ho</link>
      <guid>https://dev.to/pranavhj1998/designing-a-hal-that-doesnt-lock-you-in-50ho</guid>
      <description>&lt;p&gt;Every embedded engineer has lived through this moment: your company's chip supplier emails you that the MCU you designed around is end-of-life, or lead times just jumped to 52 weeks. You stare at 40,000 lines of firmware that call &lt;code&gt;HAL_SPI_Transmit()&lt;/code&gt; in 200 places, and you realize your "abstraction" was just ST's abstraction. You're locked in.&lt;/p&gt;

&lt;p&gt;I've spent years writing the mid-layer software that sits between customer applications and firmware — the abstraction that's supposed to make hardware swappable. I've seen what works, what doesn't, and what looks elegant in a conference talk but falls apart when you actually need to port to a different silicon vendor. This post is the guide I wish I'd had five years ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem Statement
&lt;/h2&gt;

&lt;p&gt;A hardware abstraction layer has one job: let the code above it not care which chip is below it. That sounds simple. It isn't. The tension is between three competing goals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Portability&lt;/strong&gt; — the whole point. Write once, run on STM32, nRF, ESP32, RP2040.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt; — embedded systems have hard timing constraints. Every layer of indirection costs cycles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expressiveness&lt;/strong&gt; — different MCUs have different capabilities. Your abstraction either exposes them (and leaks hardware details) or hides them (and loses functionality).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pick two. The art is in knowing which two matter for your project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Landscape: How Everyone Else Does It
&lt;/h2&gt;

&lt;p&gt;Before building something custom, let's look at what exists and where each approach breaks down.&lt;/p&gt;

&lt;h3&gt;
  
  
  CMSIS: The Lowest Common Denominator
&lt;/h3&gt;

&lt;p&gt;ARM's Cortex Microcontroller Software Interface Standard gives you register-level access with consistent naming. It's not really an abstraction — it's a naming convention for registers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CMSIS-style GPIO toggle on STM32&lt;/span&gt;
&lt;span class="n"&gt;GPIOA&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ODR&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="n"&gt;GPIO_PIN_5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Same idea on an LPC&lt;/span&gt;
&lt;span class="n"&gt;LPC_GPIO0&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;FIOPIN&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CMSIS standardizes the Cortex-M core peripherals (NVIC, SysTick, MPU) beautifully. But GPIO? SPI? UART? Those are vendor peripherals, and CMSIS doesn't touch them. Every vendor's register map is different, and CMSIS doesn't help you bridge that gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; CMSIS is a foundation, not a HAL. You still need something on top.&lt;/p&gt;

&lt;h3&gt;
  
  
  STM32 HAL: The Golden Handcuffs
&lt;/h3&gt;

&lt;p&gt;ST's HAL is the most widely used abstraction in the embedded world, mostly because STM32 is the most widely used MCU family. It's comprehensive, well-documented, and it will absolutely destroy your portability.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// STM32 HAL SPI transmit&lt;/span&gt;
&lt;span class="n"&gt;SPI_HandleTypeDef&lt;/span&gt; &lt;span class="n"&gt;hspi1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;tx_buf&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mh"&gt;0xAA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0xBB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0xCC&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="n"&gt;HAL_SPI_Transmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;hspi1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx_buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx_buf&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;HAL_MAX_DELAY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem isn't that &lt;code&gt;HAL_SPI_Transmit&lt;/code&gt; is a bad API. It's actually pretty good. The problem is that &lt;code&gt;SPI_HandleTypeDef&lt;/code&gt; contains 15 fields that are deeply STM32-specific — the prescaler values map to ST's clock tree, the alternate function pin mappings are ST-specific, and the DMA channel configuration assumes ST's DMA controller topology.&lt;/p&gt;

&lt;p&gt;When you call &lt;code&gt;HAL_SPI_Init()&lt;/code&gt;, you're committing to ST's entire initialization model. Every file that touches that handle is now ST-locked, even if it never reads a vendor-specific field.&lt;/p&gt;

&lt;p&gt;I've seen codebases where the "platform-independent" business logic imports &lt;code&gt;stm32f4xx_hal.h&lt;/code&gt; because someone passed an &lt;code&gt;SPI_HandleTypeDef*&lt;/code&gt; through three layers of function calls. That's the lock-in. It's not the function call — it's the type that leaks upward.&lt;/p&gt;

&lt;h3&gt;
  
  
  Arduino: Simplicity at a Cost
&lt;/h3&gt;

&lt;p&gt;Arduino's approach is the opposite extreme: hide everything behind the simplest possible API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Arduino SPI&lt;/span&gt;
&lt;span class="n"&gt;SPI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;SPI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xAA&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Arduino GPIO&lt;/span&gt;
&lt;span class="n"&gt;digitalWrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HIGH&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is genuinely portable — Arduino runs on AVR, SAMD, ESP32, RP2040, STM32, and more. But the cost is severe. &lt;code&gt;digitalWrite()&lt;/code&gt; is famously slow (50-80 cycles on AVR vs. 1-2 cycles for direct port manipulation) because it does a pin lookup table traversal at runtime. You can't configure DMA-driven SPI transfers. You can't set up half-duplex mode. You can't do pin-level interrupts with configurable edge detection on some platforms.&lt;/p&gt;

&lt;p&gt;Arduino proves that you &lt;em&gt;can&lt;/em&gt; build a universal HAL, but it also proves that "universal" often means "universally limited."&lt;/p&gt;

&lt;h3&gt;
  
  
  Zephyr: Device Tree and the Nuclear Option
&lt;/h3&gt;

&lt;p&gt;Zephyr RTOS takes the most sophisticated approach: device tree bindings (borrowed from Linux) combined with a driver model that separates API, driver implementation, and hardware description.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Zephyr GPIO — device tree driven&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gpio_dev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DEVICE_DT_GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DT_NODELABEL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpio0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;gpio_pin_configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpio_dev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GPIO_OUTPUT_ACTIVE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;gpio_pin_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpio_dev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Zephyr SPI — same pattern&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;spi_dev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DEVICE_DT_GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DT_NODELABEL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spi1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;spi_buf&lt;/span&gt; &lt;span class="n"&gt;tx_buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;spi_buf_set&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buffers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tx_buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;spi_config&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frequency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;operation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SPI_WORD_SET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;SPI_TRANSFER_MSB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="n"&gt;spi_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spi_dev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hardware description lives in &lt;code&gt;.dts&lt;/code&gt; files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;amp;spi1 {
    status = "okay";
    cs-gpios = &amp;lt;&amp;amp;gpio0 4 GPIO_ACTIVE_LOW&amp;gt;;
    my_sensor: sensor@0 {
        compatible = "bosch,bme280";
        reg = &amp;lt;0&amp;gt;;
        spi-max-frequency = &amp;lt;1000000&amp;gt;;
    };
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the gold standard for portability. The same application code runs on any board with a Zephyr BSP. Swapping MCUs means changing the device tree overlay and the board config — zero application code changes.&lt;/p&gt;

&lt;p&gt;But Zephyr is an entire operating system. You're adopting a build system (west + CMake + Kconfig), a threading model, a memory management policy, a logging framework, and a driver model. For a blinking LED project, this is absurd. For a product with 3+ year lifecycle and potential MCU swaps, it might be exactly right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; If you can afford the complexity, Zephyr's model is the best-designed HAL in the embedded ecosystem. But "can you afford the complexity" is doing a lot of heavy lifting in that sentence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hand-Rolled: The Default Choice
&lt;/h3&gt;

&lt;p&gt;Most production firmware I've worked on uses a hand-rolled HAL. Not because engineers sat down and designed one, but because someone created &lt;code&gt;hal_gpio.h&lt;/code&gt; one afternoon and it grew organically. These range from elegant to horrifying.&lt;/p&gt;

&lt;p&gt;The horrifying ones look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The "abstraction" that isn't&lt;/span&gt;
&lt;span class="cp"&gt;#ifdef STM32F4
&lt;/span&gt;    &lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"stm32f4xx_hal.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;    &lt;span class="cp"&gt;#define MY_SPI_HANDLE hspi1
&lt;/span&gt;    &lt;span class="cp"&gt;#define MY_SPI_TRANSMIT(buf, len) HAL_SPI_Transmit(&amp;amp;MY_SPI_HANDLE, buf, len, 1000)
#elif defined(NRF52)
&lt;/span&gt;    &lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"nrfx_spi.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;    &lt;span class="cp"&gt;#define MY_SPI_HANDLE m_spi
&lt;/span&gt;    &lt;span class="cp"&gt;#define MY_SPI_TRANSMIT(buf, len) nrfx_spi_xfer(&amp;amp;MY_SPI_HANDLE, \
        &amp;amp;(nrfx_spi_xfer_desc_t){.p_tx_buffer = buf, .tx_length = len}, 0)
#endif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a thin preprocessor skin over vendor APIs. It "works" until you need error handling (each vendor returns errors differently), or async transfers (each vendor's callback model is different), or you add a third platform. The &lt;code&gt;#ifdef&lt;/code&gt; jungle grows until no one can reason about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Works: Building a Portable GPIO + SPI Abstraction
&lt;/h2&gt;

&lt;p&gt;Here's how I'd design a HAL for a team that needs to support 2-3 MCU families without adopting Zephyr. This is the pattern I've used in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Principle 1: Your Types, Not Theirs
&lt;/h3&gt;

&lt;p&gt;The single most important rule: &lt;strong&gt;never let a vendor type appear in your public API.&lt;/strong&gt; The moment &lt;code&gt;SPI_HandleTypeDef&lt;/code&gt; or &lt;code&gt;nrfx_spi_t&lt;/code&gt; shows up in a header that application code includes, you're locked in.&lt;/p&gt;

&lt;p&gt;Define your own types. They can be thin wrappers — that's fine. But they're &lt;em&gt;yours&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hal/hal_gpio.h — YOUR public API&lt;/span&gt;
&lt;span class="cp"&gt;#pragma once
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdint.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdbool.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;HAL_GPIO_MODE_INPUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;HAL_GPIO_MODE_OUTPUT_PP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// push-pull&lt;/span&gt;
    &lt;span class="n"&gt;HAL_GPIO_MODE_OUTPUT_OD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// open-drain&lt;/span&gt;
    &lt;span class="n"&gt;HAL_GPIO_MODE_AF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// alternate function (SPI, I2C, etc.)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;hal_gpio_mode_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;HAL_GPIO_PULL_NONE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;HAL_GPIO_PULL_UP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;HAL_GPIO_PULL_DOWN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;hal_gpio_pull_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// 0 = GPIOA / P0, 1 = GPIOB / P1, etc.&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// 0-15 typically&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;hal_gpio_pin_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;hal_gpio_mode_t&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;hal_gpio_pull_t&lt;/span&gt; &lt;span class="n"&gt;pull&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;af_num&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// alternate function number (ignored if mode != AF)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;hal_gpio_config_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// API&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;hal_gpio_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;hal_gpio_config_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;hal_gpio_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;hal_gpio_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;hal_gpio_toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's missing: no &lt;code&gt;GPIO_TypeDef*&lt;/code&gt;, no &lt;code&gt;nrf_gpio_pin_dir_t&lt;/code&gt;, no vendor anything. Application code includes this header and only this header.&lt;/p&gt;

&lt;h3&gt;
  
  
  Principle 2: One Implementation File Per Platform
&lt;/h3&gt;

&lt;p&gt;Each platform gets its own &lt;code&gt;.c&lt;/code&gt; file. The build system picks which one to compile.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hal/stm32/hal_gpio_stm32.c&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"hal/hal_gpio.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"stm32f4xx_hal.h"&lt;/span&gt;&lt;span class="c1"&gt;   // vendor header — confined to this file&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;GPIO_TypeDef&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;port_map&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;GPIOA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GPIOB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GPIOC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GPIOD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GPIOE&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;hal_gpio_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;hal_gpio_config_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port_map&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Enable clock — this is the kind of vendor-specific detail&lt;/span&gt;
    &lt;span class="c1"&gt;// that MUST live in the platform file&lt;/span&gt;
    &lt;span class="n"&gt;__HAL_RCC_GPIOA_CLK_ENABLE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// simplified; real code uses port index&lt;/span&gt;

    &lt;span class="n"&gt;GPIO_InitTypeDef&lt;/span&gt; &lt;span class="n"&gt;gpio_init&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pin&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;HAL_GPIO_MODE_OUTPUT_PP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;GPIO_MODE_OUTPUT_PP&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;HAL_GPIO_MODE_OUTPUT_OD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;GPIO_MODE_OUTPUT_OD&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;HAL_GPIO_MODE_AF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;GPIO_MODE_AF_PP&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;
                                                         &lt;span class="n"&gt;GPIO_MODE_INPUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pull&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;pull&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;HAL_GPIO_PULL_UP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;GPIO_PULLUP&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;pull&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;HAL_GPIO_PULL_DOWN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;GPIO_PULLDOWN&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;
                                                    &lt;span class="n"&gt;GPIO_NOPULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Speed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GPIO_SPEED_FREQ_HIGH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Alternate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;af_num&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;HAL_GPIO_Init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gpio_init&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;hal_gpio_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;HAL_GPIO_WritePin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                      &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;GPIO_PIN_SET&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;GPIO_PIN_RESET&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;hal_gpio_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;HAL_GPIO_ReadPin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;GPIO_PIN_SET&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;hal_gpio_toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;HAL_GPIO_TogglePin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hal/nrf52/hal_gpio_nrf52.c&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"hal/hal_gpio.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"nrf_gpio.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="c1"&gt;// nRF uses a flat pin numbering: port * 32 + pin&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kr"&gt;inline&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="nf"&gt;to_nrf_pin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;hal_gpio_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;hal_gpio_config_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;npin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to_nrf_pin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;nrf_gpio_pin_pull_t&lt;/span&gt; &lt;span class="n"&gt;pull&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;pull&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;HAL_GPIO_PULL_UP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;NRF_GPIO_PIN_PULLUP&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;pull&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;HAL_GPIO_PULL_DOWN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;NRF_GPIO_PIN_PULLDOWN&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;
                                            &lt;span class="n"&gt;NRF_GPIO_PIN_NOPULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;HAL_GPIO_MODE_INPUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;nrf_gpio_cfg_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;npin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pull&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;nrf_gpio_cfg_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;npin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;hal_gpio_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;nrf_gpio_pin_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_nrf_pin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
          &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;nrf_gpio_pin_clear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_nrf_pin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;hal_gpio_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;nrf_gpio_pin_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_nrf_pin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;hal_gpio_toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_gpio_pin_t&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;nrf_gpio_pin_toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_nrf_pin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application code is identical regardless of which platform file is compiled. That's the whole point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Principle 3: SPI — Where It Gets Interesting
&lt;/h3&gt;

&lt;p&gt;SPI is harder than GPIO because it has more modes, needs error handling, and often involves DMA for performance. Here's a practical abstraction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hal/hal_spi.h&lt;/span&gt;
&lt;span class="cp"&gt;#pragma once
#include&lt;/span&gt; &lt;span class="cpf"&gt;"hal/hal_gpio.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdint.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stddef.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;hal_spi&lt;/span&gt; &lt;span class="n"&gt;hal_spi_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// opaque — defined per platform&lt;/span&gt;

&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;HAL_SPI_MODE_0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// CPOL=0, CPHA=0&lt;/span&gt;
    &lt;span class="n"&gt;HAL_SPI_MODE_1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// CPOL=0, CPHA=1&lt;/span&gt;
    &lt;span class="n"&gt;HAL_SPI_MODE_2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// CPOL=1, CPHA=0&lt;/span&gt;
    &lt;span class="n"&gt;HAL_SPI_MODE_3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// CPOL=1, CPHA=1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;hal_spi_mode_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;HAL_SPI_BIT_ORDER_MSB_FIRST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;HAL_SPI_BIT_ORDER_LSB_FIRST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;hal_spi_bit_order_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint32_t&lt;/span&gt;            &lt;span class="n"&gt;max_freq_hz&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;hal_spi_mode_t&lt;/span&gt;      &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;hal_spi_bit_order_t&lt;/span&gt; &lt;span class="n"&gt;bit_order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;hal_gpio_pin_t&lt;/span&gt;      &lt;span class="n"&gt;cs_pin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// managed by HAL, not by caller&lt;/span&gt;
    &lt;span class="n"&gt;bool&lt;/span&gt;                &lt;span class="n"&gt;cs_active_low&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;hal_spi_config_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="nf"&gt;void&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;hal_spi_callback_t&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Lifecycle&lt;/span&gt;
&lt;span class="n"&gt;hal_spi_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;hal_spi_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;hal_spi_config_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt;       &lt;span class="nf"&gt;hal_spi_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_spi_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Blocking&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;hal_spi_transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_spi_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;hal_spi_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_spi_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;hal_spi_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_spi_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Async (optional — returns -ENOTSUP if platform doesn't support it)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;hal_spi_transfer_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_spi_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                           &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hal_spi_callback_t&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key design decisions here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Opaque handle.&lt;/strong&gt; &lt;code&gt;hal_spi_t&lt;/code&gt; is forward-declared in the header and defined in each platform's &lt;code&gt;.c&lt;/code&gt; file. This is the critical firewall — application code can't reach into the handle and touch vendor-specific fields because it doesn't know what they are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CS pin management.&lt;/strong&gt; The HAL asserts/deasserts chip select. This sounds minor but it's one of the most common sources of porting bugs. Different vendors handle CS differently (hardware vs. software, active high vs. low), and if your application code manages CS directly, every call site needs platform ifdefs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Async as optional.&lt;/strong&gt; Not every platform supports DMA-driven SPI with the same callback model. Rather than forcing a lowest-common-denominator async API, we let it return &lt;code&gt;-ENOTSUP&lt;/code&gt; and let the caller fall back to blocking. Pragmatic beats pure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dispatch Question: Compile-Time vs. Runtime
&lt;/h2&gt;

&lt;p&gt;This is where HAL design gets philosophical. How does &lt;code&gt;hal_spi_transfer()&lt;/code&gt; know which implementation to call?&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: Compile-Time Dispatch (Link-Time Selection)
&lt;/h3&gt;

&lt;p&gt;The simplest approach: only one platform &lt;code&gt;.c&lt;/code&gt; file is compiled into the binary. The linker resolves &lt;code&gt;hal_spi_transfer&lt;/code&gt; to whichever implementation was compiled.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="c"&gt;# Makefile
&lt;/span&gt;&lt;span class="k"&gt;ifeq&lt;/span&gt; &lt;span class="nv"&gt;($(PLATFORM),stm32)&lt;/span&gt;
    &lt;span class="nv"&gt;HAL_SRC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; hal/stm32/hal_spi_stm32.c hal/stm32/hal_gpio_stm32.c
&lt;span class="err"&gt;else&lt;/span&gt; &lt;span class="k"&gt;ifeq&lt;/span&gt; &lt;span class="nv"&gt;($(PLATFORM),nrf52)&lt;/span&gt;
    &lt;span class="nv"&gt;HAL_SRC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; hal/nrf52/hal_spi_nrf52.c hal/nrf52/hal_gpio_nrf52.c
&lt;span class="k"&gt;endif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Zero overhead. No function pointers, no vtables, no indirection. The compiler can inline everything. This is what you want for hard real-time systems where every cycle matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; You can only target one platform per binary. You can't have a test binary that mocks the hardware — unless you add a &lt;code&gt;hal/mock/&lt;/code&gt; platform and compile against that.&lt;/p&gt;

&lt;p&gt;This is the approach I recommend for 90% of projects. The "one platform per binary" limitation sounds restrictive until you realize that's what you're doing anyway — you don't ship the same &lt;code&gt;.elf&lt;/code&gt; to an STM32 and an nRF52.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: Function Pointer Tables
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hal/hal_spi.h (function pointer variant)&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;hal_spi_t&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;hal_spi_config_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt;       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;hal_spi_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt;        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;hal_spi_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt;        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;hal_spi_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt;        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;hal_spi_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;hal_spi_ops_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Each platform registers its ops&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;hal_spi_ops_t&lt;/span&gt; &lt;span class="n"&gt;hal_spi_ops&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Application code calls through the table&lt;/span&gt;
&lt;span class="cp"&gt;#define hal_spi_transfer(spi, tx, rx, len) hal_spi_ops.transfer(spi, tx, rx, len)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; You can swap implementations at link time &lt;em&gt;or&lt;/em&gt; at runtime. Makes mocking trivial for tests — just plug in a mock ops table. This is essentially what Zephyr does under the hood.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; One level of pointer indirection per call. On a Cortex-M4 at 168 MHz, this is ~2-5 extra cycles — irrelevant for SPI (the bus itself is the bottleneck) but potentially meaningful for GPIO bit-banging in a tight loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option C: C++ Virtual Dispatch (vtable)
&lt;/h3&gt;

&lt;p&gt;If you're in C++ land:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ISpiDriver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nl"&gt;public:&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;ISpiDriver&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Stm32SpiDriver&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ISpiDriver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Nrf52SpiDriver&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ISpiDriver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is functionally identical to Option B but with language support for the dispatch. Same overhead, slightly cleaner syntax, and you get type safety on the ops table for free. The downside is that many embedded teams avoid C++ virtual dispatch because the vtable pointer adds 4 bytes per object instance, and the compiler-generated vtable code is harder to audit in safety-critical contexts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option D: Preprocessor Switching
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hal_spi.h&lt;/span&gt;
&lt;span class="cp"&gt;#if defined(PLATFORM_STM32)
&lt;/span&gt;    &lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"hal/stm32/hal_spi_stm32_inline.h"&lt;/span&gt;&lt;span class="cp"&gt;
#elif defined(PLATFORM_NRF52)
&lt;/span&gt;    &lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"hal/nrf52/hal_spi_nrf52_inline.h"&lt;/span&gt;&lt;span class="cp"&gt;
#endif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is compile-time dispatch with the implementations inlined into the header. Maximum performance (everything can be inlined and optimized), but it means your public headers pull in vendor headers transitively. The whole point of the abstraction was to avoid this. Use this only for truly hot-path operations where the function call overhead is measured and proven to matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Actually Recommend
&lt;/h2&gt;

&lt;p&gt;After shipping firmware on STM32, nRF, TI, and Renesas parts, here's my practical decision tree:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're starting a new product with potential for MCU changes (most products):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use compile-time dispatch (Option A) with the opaque handle pattern. Define your own types. One &lt;code&gt;.c&lt;/code&gt; per platform, selected by the build system. This gives you zero overhead, clean separation, and easy porting. When you need to test, add a &lt;code&gt;hal/mock/&lt;/code&gt; platform with stub implementations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're building a framework or SDK that ships to other developers:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use function pointer tables (Option B). Your users might need to mock, extend, or substitute drivers. The pointer indirection cost is negligible for anything above bit-bang-speed peripherals. This is the right tradeoff for flexibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're already using Zephyr or considering an RTOS:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Just use Zephyr's driver model. Seriously. Don't build a second HAL on top of Zephyr's HAL — that's two layers of abstraction for the same job. Zephyr's device tree + driver API is the most well-designed HAL in the ecosystem. The cost is adopting Zephyr, but if you're already there, you've already paid it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If your product will only ever run on one MCU family:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Don't build a HAL at all. Use the vendor HAL (STM32 HAL, nrfx, ESP-IDF drivers) directly. A HAL is insurance against hardware changes. If that risk is genuinely zero, the insurance premium (development time, code complexity, debugging difficulty) isn't worth paying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;A few patterns I've seen fail repeatedly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leaking vendor types through "convenience" macros.&lt;/strong&gt; Someone adds &lt;code&gt;#define MY_SPI hspi1&lt;/code&gt; and now every file that uses &lt;code&gt;MY_SPI&lt;/code&gt; transitively depends on ST's headers. The macro looked harmless. It wasn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Over-abstracting initialization.&lt;/strong&gt; Init is where platforms differ the most (clock trees, pin muxing, DMA channel allocation). Trying to make init generic leads to massive config structs that are just as vendor-specific as the original API but harder to understand. Let init be platform-specific. Abstract the &lt;em&gt;operations&lt;/em&gt;, not the &lt;em&gt;setup&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Abstracting too early.&lt;/strong&gt; Don't build a HAL on day one of a project. Write directly against the vendor HAL, get the product working, then extract the abstraction boundary once you can see which operations are actually used. Premature abstraction creates APIs that don't match real usage patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring error semantics.&lt;/strong&gt; STM32 HAL returns &lt;code&gt;HAL_StatusTypeDef&lt;/code&gt; (OK, ERROR, BUSY, TIMEOUT). nrfx returns &lt;code&gt;nrfx_err_t&lt;/code&gt;. ESP-IDF returns &lt;code&gt;esp_err_t&lt;/code&gt;. If your HAL just returns &lt;code&gt;int&lt;/code&gt; with 0 for success and -1 for failure, you've lost the ability to distinguish between "bus busy, retry later" and "hardware fault, pin not configured." Define your own error codes that capture the categories your application actually needs to handle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Test Story
&lt;/h2&gt;

&lt;p&gt;The strongest argument for a clean HAL isn't portability — it's testability. With the opaque handle pattern and compile-time dispatch, you can create a &lt;code&gt;hal/mock/&lt;/code&gt; implementation that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Records every call (what was written, to which pin/bus)&lt;/li&gt;
&lt;li&gt;Returns scripted responses (inject error conditions)&lt;/li&gt;
&lt;li&gt;Validates sequencing (CS asserted before transfer, released after)&lt;/li&gt;
&lt;li&gt;Runs on your development machine, not on target hardware&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means your application logic — the part that talks to sensors, runs protocols, makes decisions — can be tested on x86 with a standard test framework. No JTAG required. No waiting for hardware. No "it works on my board" debugging.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hal/mock/hal_spi_mock.c&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;tx_log&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kt"&gt;size_t&lt;/span&gt;  &lt;span class="n"&gt;tx_log_pos&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;rx_script&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kt"&gt;size_t&lt;/span&gt;  &lt;span class="n"&gt;rx_script_pos&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt;     &lt;span class="n"&gt;next_error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;mock_state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;hal_spi_transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_spi_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mock_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next_error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mock_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next_error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;mock_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;mock_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tx_log&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mock_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tx_log_pos&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;mock_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tx_log_pos&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;mock_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rx_script&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mock_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rx_script_pos&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;mock_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rx_script_pos&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Test helper: inject an error on the next call&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;mock_spi_set_next_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mock_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This mock compiles and runs on any platform. Your CI pipeline catches protocol bugs before hardware is even available. That's the real payoff of a well-designed HAL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;The best HAL is the one your team can actually maintain. I've seen beautiful, theoretically perfect abstractions that nobody understood and everyone worked around. I've also seen ugly &lt;code&gt;#ifdef&lt;/code&gt; forests that somehow shipped reliable products for a decade.&lt;/p&gt;

&lt;p&gt;The principles that matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Your types at the boundary.&lt;/strong&gt; Vendor types stay in platform files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opaque handles.&lt;/strong&gt; Application code can't reach into hardware-specific fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Abstract operations, not init.&lt;/strong&gt; Let setup be messy and platform-specific.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compile-time dispatch by default.&lt;/strong&gt; Add indirection only when you have a concrete reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mock-friendly from day one.&lt;/strong&gt; If you can't test it on x86, your abstraction has holes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal isn't a perfect universal API. The goal is that when — not if — you need to swap silicon, the damage is contained to a set of well-defined platform files, and your application logic doesn't change.&lt;/p&gt;

&lt;p&gt;That's a HAL that doesn't lock you in.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Pranav Jain is a semiconductor software engineer specializing in the abstraction layer between customer applications and firmware. He works with C++, Python, and embedded systems across multiple MCU platforms. Find his open-source work on &lt;a href="https://github.com/pranavhj" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>embedded</category>
      <category>firmware</category>
      <category>c</category>
      <category>architecture</category>
    </item>
    <item>
      <title>10 Mistakes Companies Make When Porting Firmware Between MCU Families</title>
      <dc:creator>Pranav Jain</dc:creator>
      <pubDate>Thu, 20 Aug 2026 09:44:33 +0000</pubDate>
      <link>https://dev.to/pranavhj1998/10-mistakes-companies-make-when-porting-firmware-between-mcu-families-4gc6</link>
      <guid>https://dev.to/pranavhj1998/10-mistakes-companies-make-when-porting-firmware-between-mcu-families-4gc6</guid>
      <description>&lt;p&gt;I've spent years working in the layer between customer software and firmware — the middleware that has to survive MCU swaps, silicon shortages, and last-minute BOM changes. I've watched teams burn weeks on firmware ports that should have taken days, and I've done enough post-mortems to see the same mistakes repeat across companies.&lt;/p&gt;

&lt;p&gt;These aren't theoretical. Every mistake below comes from real porting efforts I've seen or been called in to fix. Most involve moving between STM32, ESP32, and nRF52 — the three families that cover probably 80% of new embedded designs in 2026.&lt;/p&gt;

&lt;p&gt;If you're planning a firmware port (or trying to build firmware that won't need a painful port later), this is the list I wish someone had given me five years ago.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake 1: Copy-Pasting Vendor HAL Calls Into Application Logic
&lt;/h2&gt;

&lt;p&gt;This is the original sin of firmware porting. Engineers write application code that directly calls &lt;code&gt;HAL_SPI_Transmit()&lt;/code&gt; or &lt;code&gt;nrf_drv_spi_transfer()&lt;/code&gt; in business logic functions. When the MCU changes, every file that touches a peripheral has to be rewritten.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mistake:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sensor_driver.c — STM32 version&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"stm32f4xx_hal.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="n"&gt;SPI_HandleTypeDef&lt;/span&gt; &lt;span class="n"&gt;hspi1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;sensor_read_temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint16_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;temp_raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0xD0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="n"&gt;HAL_GPIO_WritePin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GPIOA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GPIO_PIN_4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GPIO_PIN_RESET&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// CS low&lt;/span&gt;
    &lt;span class="n"&gt;HAL_SPI_Transmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;hspi1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;HAL_SPI_Receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;hspi1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;HAL_GPIO_WritePin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GPIOA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GPIO_PIN_4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GPIO_PIN_SET&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// CS high&lt;/span&gt;

    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;temp_raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you need to port to nRF52. Every line of this function changes. The SPI API is different, the GPIO API is different, and the pin numbering is completely different. Multiply this by 30 sensor/actuator functions and you're looking at a week of tedious, error-prone work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Wrap peripheral access behind a thin abstraction. The application code calls your API, not the vendor's.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hal_spi.h — your abstraction&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;hal_spi&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;hal_spi_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;hal_spi_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_spi_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;hal_spi_config_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;hal_spi_transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_spi_t&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;hal_spi_cs_assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_spi_t&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;hal_spi_cs_deassert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_spi_t&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// sensor_driver.c — portable version&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;sensor_read_temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_spi_t&lt;/span&gt; &lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint16_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;temp_raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0xD0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="n"&gt;hal_spi_cs_assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;hal_spi_transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;hal_spi_transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;hal_spi_cs_deassert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;temp_raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now porting means writing one new &lt;code&gt;hal_spi_nrf52.c&lt;/code&gt; backend. The sensor driver doesn't change at all. This is the single highest-ROI investment in any firmware architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake 2: Hardcoding Interrupt Priorities
&lt;/h2&gt;

&lt;p&gt;STM32 uses a 4-bit priority field (0-15, where 0 is highest). nRF52 uses 3 bits (0-7). ESP32's interrupt system is completely different — it uses levels 1-6 with dedicated high-priority interrupts that can only run from IRAM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mistake:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// stm32_setup.c&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setup_interrupts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;HAL_NVIC_SetPriority&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;USART1_IRQn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// UART at priority 5&lt;/span&gt;
    &lt;span class="n"&gt;HAL_NVIC_SetPriority&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SPI1_IRQn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// SPI at priority 3&lt;/span&gt;
    &lt;span class="n"&gt;HAL_NVIC_SetPriority&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TIM2_IRQn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// Timer at priority 1 (high)&lt;/span&gt;
    &lt;span class="n"&gt;HAL_NVIC_SetPriority&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXTI0_IRQn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// External interrupt at 2&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Port this to nRF52 and priorities 5 and above don't exist — the maximum is 7, but the SoftDevice (BLE stack) reserves priorities 0, 1, and 4. Your "high priority" timer at 1 now collides with the SoftDevice and causes random BLE disconnections that take a week to debug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Define priority levels semantically and map them per platform.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// irq_priorities.h&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;IRQ_PRIO_CRITICAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// timing-critical, cannot be preempted&lt;/span&gt;
    &lt;span class="n"&gt;IRQ_PRIO_HIGH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// fast peripherals (SPI, timer callbacks)&lt;/span&gt;
    &lt;span class="n"&gt;IRQ_PRIO_MEDIUM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// standard peripherals (UART, I2C)&lt;/span&gt;
    &lt;span class="n"&gt;IRQ_PRIO_LOW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// background tasks (ADC, low-rate sensors)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;irq_priority_level_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// irq_priorities_stm32.h&lt;/span&gt;
&lt;span class="cp"&gt;#define IRQ_PRIO_MAP_CRITICAL  1
#define IRQ_PRIO_MAP_HIGH      3
#define IRQ_PRIO_MAP_MEDIUM    5
#define IRQ_PRIO_MAP_LOW       8
&lt;/span&gt;
&lt;span class="c1"&gt;// irq_priorities_nrf52.h (SoftDevice reserves 0, 1, 4)&lt;/span&gt;
&lt;span class="cp"&gt;#define IRQ_PRIO_MAP_CRITICAL  2
#define IRQ_PRIO_MAP_HIGH      3
#define IRQ_PRIO_MAP_MEDIUM    5
#define IRQ_PRIO_MAP_LOW       6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Document which priority levels the BLE/Wi-Fi stack reserves. This avoids the most common source of "it works on STM32 but randomly crashes on nRF52" bugs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake 3: Assuming Memory Layout Is Portable
&lt;/h2&gt;

&lt;p&gt;STM32F4 has a flat memory map — flash, SRAM, and peripherals all in one address space. ESP32 has instruction RAM (IRAM), data RAM (DRAM), SPI flash with caching, and RTC slow memory. nRF52 has flash, RAM, and the SoftDevice sitting in the first chunk of both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mistake:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Works on STM32 — a function pointer stored in flash and called normally&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="nf"&gt;void&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;callback_t&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;callback_t&lt;/span&gt; &lt;span class="n"&gt;isr_table&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;__attribute__&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".rodata"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;handler_timer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;handler_spi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;handler_uart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// On ESP32, this crashes. Functions called from interrupts MUST be in IRAM,&lt;/span&gt;
&lt;span class="c1"&gt;// not flash. Flash access is disabled during SPI operations and cache misses&lt;/span&gt;
&lt;span class="c1"&gt;// cause exceptions in ISR context.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The fix for ESP32:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ESP32 — ISR handlers must be in IRAM&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;IRAM_ATTR&lt;/span&gt; &lt;span class="nf"&gt;handler_timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// This function lives in IRAM, safe to call from interrupts&lt;/span&gt;
    &lt;span class="c1"&gt;// IMPORTANT: anything this function calls must also be in IRAM&lt;/span&gt;
    &lt;span class="c1"&gt;// or be inlined. No calls to flash-resident code.&lt;/span&gt;
    &lt;span class="n"&gt;gpio_set_level&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LED_PIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// gpio_set_level is IRAM-safe&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Do NOT call printf, logging functions, or anything that&lt;/span&gt;
&lt;span class="c1"&gt;// accesses flash from an IRAM_ATTR function&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On nRF52 with SoftDevice, the first ~116KB of flash and ~8KB of RAM are owned by the SoftDevice. Your linker script must start application code after the SoftDevice region, and the size changes between SoftDevice versions (S132 v7.0 vs v7.2 have different sizes). I've seen boards that worked perfectly until a SoftDevice update shifted the memory map and the application overwrote SoftDevice data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build a memory map document for each target.&lt;/strong&gt; Not in someone's head — in a file checked into the repo. Include reserved regions, stack sizes, and heap configuration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake 4: Ignoring Clock Tree Differences
&lt;/h2&gt;

&lt;p&gt;Every MCU family has a different clock tree, and peripherals derive their clocks from different sources. A SPI peripheral running at 8 MHz on STM32 might end up at 6.67 MHz or 10 MHz on nRF52 because the available dividers are different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mistake:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// STM32: APB2 clock is 84 MHz, SPI prescaler = 16 → 5.25 MHz SPI clock&lt;/span&gt;
&lt;span class="n"&gt;spi_handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaudRatePrescaler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SPI_BAUDRATEPRESCALER_16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// nRF52: SPI frequency options are discrete:&lt;/span&gt;
&lt;span class="c1"&gt;// 125K, 250K, 500K, 1M, 2M, 4M, 8M&lt;/span&gt;
&lt;span class="c1"&gt;// There is no 5.25 MHz option. Engineer picks 8M and the&lt;/span&gt;
&lt;span class="c1"&gt;// sensor can't handle it. Or picks 4M and the data rate&lt;/span&gt;
&lt;span class="c1"&gt;// is too slow for the application.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is worse for UART. STM32 can generate almost any baud rate from its flexible prescalers. nRF52 generates baud rates from a 16 MHz clock with limited dividers — 115200 baud actually runs at 115942 baud (0.64% error). For most UART devices this is fine, but I've seen it cause framing errors with picky GPS modules that barely tolerate 0.5% error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Specify peripheral speeds as &lt;em&gt;requirements&lt;/em&gt; (minimum and maximum), not as exact register values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// peripheral_config.h — specify intent, not register values&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;freq_min_hz&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// minimum acceptable clock&lt;/span&gt;
    &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;freq_max_hz&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// maximum acceptable clock&lt;/span&gt;
    &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;freq_target_hz&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ideal clock&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;spi_clock_requirement_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// The platform-specific init code finds the best available&lt;/span&gt;
&lt;span class="c1"&gt;// divider and logs a warning if it falls outside the range.&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;hal_spi_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_spi_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;hal_spi_config_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;actual_freq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;find_closest_spi_freq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;freq_target_hz&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual_freq&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;freq_min_hz&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
        &lt;span class="n"&gt;actual_freq&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;freq_max_hz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;LOG_WARN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SPI%d: requested %u Hz, got %u Hz (out of range)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;freq_target_hz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actual_freq&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;EINVAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;LOG_INFO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SPI%d: configured at %u Hz (target: %u Hz)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actual_freq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;freq_target_hz&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ... configure the peripheral&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Mistake 5: Porting the RTOS Configuration Verbatim
&lt;/h2&gt;

&lt;p&gt;Teams running FreeRTOS on STM32 copy their &lt;code&gt;FreeRTOSConfig.h&lt;/code&gt; to the ESP32 build and wonder why things break. The problem: ESP32's FreeRTOS is a &lt;em&gt;fork&lt;/em&gt; by Espressif (ESP-IDF FreeRTOS) that adds symmetric multiprocessing, has different defaults for tick rate, and uses a different idle task hook mechanism.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mistake:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// FreeRTOSConfig.h — copied from STM32 project&lt;/span&gt;
&lt;span class="cp"&gt;#define configUSE_PREEMPTION         1
#define configTICK_RATE_HZ           1000
#define configMINIMAL_STACK_SIZE     128  // in words (512 bytes on ARM)
#define configTOTAL_HEAP_SIZE        (32 * 1024)
#define configUSE_TICKLESS_IDLE      1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Problems when this lands on ESP32:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;configMINIMAL_STACK_SIZE&lt;/code&gt; of 128 words (512 bytes) is dangerously small on ESP32 — ESP-IDF tasks typically need 2048-4096 bytes minimum because of deeper call stacks in the Wi-Fi/BLE stack.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;configTOTAL_HEAP_SIZE&lt;/code&gt; is ignored — ESP32 uses its own multi-region heap allocator.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;configUSE_TICKLESS_IDLE&lt;/code&gt; interacts badly with Wi-Fi power management on ESP32.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On nRF52 with SoftDevice, you can't use FreeRTOS's standard &lt;code&gt;vPortSVCHandler&lt;/code&gt; and &lt;code&gt;xPortPendSVHandler&lt;/code&gt; — the SoftDevice owns those interrupt vectors. You need the nRF52-specific FreeRTOS port that routes through the SoftDevice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Treat RTOS configuration as platform-specific. Keep a &lt;em&gt;base&lt;/em&gt; config with shared application-level settings (task priorities, queue sizes) and a &lt;em&gt;platform&lt;/em&gt; config with hardware-dependent values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// rtos_config_common.h — shared across all platforms&lt;/span&gt;
&lt;span class="cp"&gt;#define APP_TASK_PRIORITY_SENSOR     3
#define APP_TASK_PRIORITY_COMMS      4
#define APP_TASK_PRIORITY_CONTROL    5
#define APP_QUEUE_SIZE_SENSOR        16
#define APP_QUEUE_SIZE_CMD           8
&lt;/span&gt;
&lt;span class="c1"&gt;// rtos_config_stm32.h&lt;/span&gt;
&lt;span class="cp"&gt;#define PLATFORM_MIN_STACK_SIZE      512   // bytes
#define PLATFORM_DEFAULT_STACK_SIZE  1024
#define PLATFORM_TICK_RATE_HZ        1000
#define PLATFORM_USE_TICKLESS_IDLE   1
&lt;/span&gt;
&lt;span class="c1"&gt;// rtos_config_esp32.h&lt;/span&gt;
&lt;span class="cp"&gt;#define PLATFORM_MIN_STACK_SIZE      2048  // ESP-IDF needs more
#define PLATFORM_DEFAULT_STACK_SIZE  4096
#define PLATFORM_TICK_RATE_HZ        100   // ESP-IDF default
#define PLATFORM_USE_TICKLESS_IDLE   0     // conflicts with Wi-Fi PM
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Mistake 6: Not Auditing DMA Channel Allocation
&lt;/h2&gt;

&lt;p&gt;DMA is one of the least portable subsystems across MCU families. STM32F4 has 2 DMA controllers with 8 streams each, and each stream can connect to specific peripherals via a request mapping table. nRF52 uses EasyDMA, which is peripheral-specific — each peripheral (SPI, I2C, UART) has its own DMA tied to it. ESP32 uses a GDMA controller where channels are dynamically assignable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mistake:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// STM32: manually assign DMA stream to SPI&lt;/span&gt;
&lt;span class="c1"&gt;// DMA1 Stream 3, Channel 0 → SPI2_RX (from reference manual)&lt;/span&gt;
&lt;span class="n"&gt;hdma_spi2_rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DMA1_Stream3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;hdma_spi2_rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DMA_CHANNEL_0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;hdma_spi2_rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DMA_PERIPH_TO_MEMORY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="c1"&gt;// Engineer ports to nRF52 and tries to find equivalent DMA channels.&lt;/span&gt;
&lt;span class="c1"&gt;// There are none. nRF52's EasyDMA is built into each peripheral.&lt;/span&gt;
&lt;span class="c1"&gt;// You configure it by setting the TXD.PTR, TXD.MAXCNT, RXD.PTR, RXD.MAXCNT&lt;/span&gt;
&lt;span class="c1"&gt;// registers on the SPIM peripheral itself.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Abstract DMA as a property of the peripheral, not a separate subsystem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Your SPI config struct should express whether DMA is desired,&lt;/span&gt;
&lt;span class="c1"&gt;// not which DMA channel to use&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// SPI0, SPI1, etc.&lt;/span&gt;
    &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;freq_hz&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;use_dma&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;              &lt;span class="c1"&gt;// platform code handles the details&lt;/span&gt;
    &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;dma_threshold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// only use DMA for transfers &amp;gt; N bytes&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;hal_spi_config_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// hal_spi_stm32.c — DMA setup is internal&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;setup_dma_for_spi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;spi_instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dma_handles_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;dma&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Look up DMA stream/channel from a mapping table&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;dma_mapping_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_spi_dma_mapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spi_instance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;LOG_WARN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No DMA available for SPI%d, falling back to polling"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spi_instance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;ENOTSUP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ... configure DMA&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// hal_spi_nrf52.c — EasyDMA is automatic, just set the buffer pointers&lt;/span&gt;
&lt;span class="c1"&gt;// No separate DMA configuration needed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: on some platforms DMA is a separate resource you must manage; on others it's transparent. Your abstraction should hide this difference.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake 7: Forgetting That GPIO Numbering Means Different Things
&lt;/h2&gt;

&lt;p&gt;STM32 uses port+pin (GPIOA pin 5). nRF52 uses a flat numbering scheme (P0.05, P0.13, P1.09). ESP32 uses GPIO numbers (GPIO_NUM_18) that may or may not correspond to the physical pin on the package. On top of this, pin muxing rules differ — STM32 has alternate function registers, nRF52 lets you route most peripherals to any pin, and ESP32 uses a GPIO matrix with some restrictions on certain functions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mistake:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Scattered across the codebase, different files:&lt;/span&gt;
&lt;span class="cp"&gt;#define LED_PIN    GPIO_PIN_5       // Which port? GPIOA? GPIOB?
#define BUTTON_PIN 13               // Is this a port pin or a flat GPIO number?
#define SPI_CS     4                // 4 on which port?
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is ambiguous even on a single platform. During a port, it's a nightmare.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; One file, one table, fully qualified.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// board_pinmap.h — ONE file defines ALL pin assignments for a board&lt;/span&gt;
&lt;span class="c1"&gt;// This file is the ONLY thing that changes when the PCB changes&lt;/span&gt;

&lt;span class="cp"&gt;#if defined(BOARD_CUSTOM_STM32F4)
&lt;/span&gt;    &lt;span class="cp"&gt;#define PIN_LED_STATUS     { .port = GPIOA, .pin = 5 }
&lt;/span&gt;    &lt;span class="cp"&gt;#define PIN_BUTTON_USER    { .port = GPIOC, .pin = 13 }
&lt;/span&gt;    &lt;span class="cp"&gt;#define PIN_SPI_SENSOR_CS  { .port = GPIOA, .pin = 4 }
&lt;/span&gt;    &lt;span class="cp"&gt;#define PIN_UART_DEBUG_TX  { .port = GPIOA, .pin = 2 }
&lt;/span&gt;    &lt;span class="cp"&gt;#define PIN_UART_DEBUG_RX  { .port = GPIOA, .pin = 3 }
&lt;/span&gt;
&lt;span class="cp"&gt;#elif defined(BOARD_CUSTOM_NRF52840)
&lt;/span&gt;    &lt;span class="c1"&gt;// nRF52 uses flat pin numbers: port * 32 + pin&lt;/span&gt;
    &lt;span class="cp"&gt;#define PIN_LED_STATUS     NRF_GPIO_PIN_MAP(0, 13)
&lt;/span&gt;    &lt;span class="cp"&gt;#define PIN_BUTTON_USER    NRF_GPIO_PIN_MAP(0, 11)
&lt;/span&gt;    &lt;span class="cp"&gt;#define PIN_SPI_SENSOR_CS  NRF_GPIO_PIN_MAP(1, 8)
&lt;/span&gt;    &lt;span class="cp"&gt;#define PIN_UART_DEBUG_TX  NRF_GPIO_PIN_MAP(0, 6)
&lt;/span&gt;    &lt;span class="cp"&gt;#define PIN_UART_DEBUG_RX  NRF_GPIO_PIN_MAP(0, 8)
&lt;/span&gt;
&lt;span class="cp"&gt;#elif defined(BOARD_CUSTOM_ESP32S3)
&lt;/span&gt;    &lt;span class="cp"&gt;#define PIN_LED_STATUS     GPIO_NUM_2
&lt;/span&gt;    &lt;span class="cp"&gt;#define PIN_BUTTON_USER    GPIO_NUM_0
&lt;/span&gt;    &lt;span class="cp"&gt;#define PIN_SPI_SENSOR_CS  GPIO_NUM_10
&lt;/span&gt;    &lt;span class="cp"&gt;#define PIN_UART_DEBUG_TX  GPIO_NUM_43
&lt;/span&gt;    &lt;span class="cp"&gt;#define PIN_UART_DEBUG_RX  GPIO_NUM_44
&lt;/span&gt;
&lt;span class="cp"&gt;#else
&lt;/span&gt;    &lt;span class="cp"&gt;#error "No board defined — add your pin map"
#endif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you port to a new board, you add one &lt;code&gt;#elif&lt;/code&gt; block. Nothing else in the codebase mentions pin numbers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake 8: Testing Only the Happy Path After Porting
&lt;/h2&gt;

&lt;p&gt;The firmware boots, the LED blinks, SPI reads return data, UART prints work. Ship it, right? No. The failure modes are where ports break.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What gets missed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Peripheral error recovery.&lt;/strong&gt; STM32's HAL sets error flags on &lt;code&gt;hspi.ErrorCode&lt;/code&gt; — your error handler clears them and retries. nRF52's SPIM peripheral uses event registers (&lt;code&gt;EVENTS_STOPPED&lt;/code&gt;) that work differently. Your error recovery code from STM32 does nothing on nRF52, so the first bus error hangs the SPI peripheral forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timing edge cases.&lt;/strong&gt; A watchdog timer that worked with STM32's 32 kHz LSI oscillator (which has +/- 10% accuracy) may trip on nRF52's 32.768 kHz crystal (much more accurate), or vice versa, depending on how you calculated the timeout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Power state transitions.&lt;/strong&gt; Sleep/wake behavior is wildly different. STM32 has STOP, STANDBY, and SHUTDOWN modes. nRF52 has System ON (idle with RAM retention) and System OFF. ESP32 has light sleep, deep sleep, and hibernation. Your "wake from sleep" code is not portable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack overflow under load.&lt;/strong&gt; A task that used 400 bytes of stack on STM32 might use 800 on ESP32 due to deeper call chains in ESP-IDF library functions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Build a porting test checklist and run it on every target.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// port_validation_tests.c — run on each new target&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;test_spi_error_recovery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Intentionally cause a bus error (disconnect MISO)&lt;/span&gt;
    &lt;span class="c1"&gt;// Verify the driver detects and recovers&lt;/span&gt;
    &lt;span class="n"&gt;hal_spi_transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_spi_get_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;HAL_ERR_NONE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;hal_spi_reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Verify SPI works again after recovery&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;ret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hal_spi_transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ret&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;test_watchdog_timing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Start watchdog with 2-second timeout&lt;/span&gt;
    &lt;span class="n"&gt;hal_wdt_start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Sleep for 1.9 seconds — should NOT trigger&lt;/span&gt;
    &lt;span class="n"&gt;hal_delay_ms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1900&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;hal_wdt_feed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// If we get here, the watchdog timing is correct on this platform&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;test_sleep_wake_integrity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;volatile&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;canary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0xDEADBEEF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;hal_enter_sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SLEEP_MODE_LIGHT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ... external interrupt wakes us&lt;/span&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canary&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0xDEADBEEF&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// RAM retained?&lt;/span&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hal_spi_transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// peripherals re-inited?&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Mistake 9: Trying to Port Everything at Once
&lt;/h2&gt;

&lt;p&gt;I've seen teams attempt to port an entire 50-file firmware project in one shot. They create a new target in the build system, switch all the HAL calls, and then spend three weeks debugging because nothing works and they have no idea which change broke what.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Port in layers, bottom-up, validating at each step.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Week 1: Board bring-up.&lt;/strong&gt; Get the chip running — clock config, a blinking LED, and &lt;code&gt;printf&lt;/code&gt; over UART. Nothing else. If this doesn't work, you can't debug anything above it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Week 2: Peripheral drivers.&lt;/strong&gt; Port one peripheral at a time. SPI first (because sensors usually need it), then I2C, then timers, then DMA. Test each one in isolation with a simple loopback or sensor read before moving on.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Week 3: RTOS + middleware.&lt;/strong&gt; Bring up FreeRTOS (or Zephyr, or your RTOS of choice) with a single task. Verify scheduling, then add tasks one at a time. Verify inter-task communication (queues, semaphores) before adding application logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Week 4: Application logic.&lt;/strong&gt; If your abstraction layer is done right, this step should require zero changes to application code. If it requires changes, your abstraction leaked — fix the abstraction, don't patch the application.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Commit at each step.&lt;/strong&gt; If step 3 breaks something, you can diff against step 2 and see exactly what changed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake 10: No Automated Build for Multiple Targets
&lt;/h2&gt;

&lt;p&gt;After the port, you have two (or more) targets. Developers work on one target and forget to compile the other. Six months later someone tries to build the second target and it's broken — header files moved, function signatures changed, a new module was added without a platform implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mistake:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# "Just build the one you're working on"&lt;/span&gt;
make &lt;span class="nv"&gt;TARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;stm32f4
&lt;span class="c"&gt;# Nobody runs this for months:&lt;/span&gt;
make &lt;span class="nv"&gt;TARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nrf52840
&lt;span class="c"&gt;# It's been broken since March&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; CI that builds every target on every commit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/firmware-build.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Multi-target firmware build&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;stm32f4&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;nrf52840&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;esp32s3&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/your-org/firmware-toolchain:latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build ${{ matrix.target }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;make TARGET=${{ matrix.target }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run unit tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;make test TARGET=${{ matrix.target }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don't have CI, at minimum add a &lt;code&gt;build_all.sh&lt;/code&gt; script and run it before every merge. The 30 seconds it takes to compile both targets saves the hours it takes to fix a broken build that drifted for months.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Common Thread
&lt;/h2&gt;

&lt;p&gt;Every one of these mistakes comes from the same root cause: treating firmware porting as a search-and-replace exercise instead of an architecture problem.&lt;/p&gt;

&lt;p&gt;The time to make firmware portable is &lt;em&gt;before&lt;/em&gt; you need to port it. The second best time is when you're planning the port — before you start changing code. An afternoon spent mapping out peripheral differences, memory layouts, and interrupt schemes saves weeks of debugging.&lt;/p&gt;

&lt;p&gt;If you're facing a port right now and the codebase has no abstraction layer, resist the temptation to "just get it working" on the new target by copy-pasting and patching. You'll end up maintaining two divergent codebases. Take the time to extract an abstraction layer during the port — it's the last time you'll need to do this work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.zephyrproject.org/latest/kernel/drivers/index.html" rel="noopener noreferrer"&gt;Zephyr's device driver model&lt;/a&gt; — a good reference for how a mature project handles multi-platform peripheral abstraction&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/freertos_idf.html" rel="noopener noreferrer"&gt;ESP-IDF FreeRTOS SMP changes&lt;/a&gt; — critical reading before porting FreeRTOS config to ESP32&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/app_dev/nrf5_sdk_migration.html" rel="noopener noreferrer"&gt;nRF5 SDK to nRF Connect SDK migration guide&lt;/a&gt; — Nordic's own porting guide, useful patterns even for non-Nordic ports&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Pranav Jain is an embedded systems and middleware engineer specializing in the abstraction layer between application software and firmware. He builds tools and writes about making firmware portable, testable, and maintainable. Find his work on &lt;a href="https://github.com/pranavhj" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>firmware</category>
      <category>embedded</category>
      <category>mcu</category>
      <category>porting</category>
    </item>
    <item>
      <title>Cross-MCU Migration: A Practical Guide</title>
      <dc:creator>Pranav Jain</dc:creator>
      <pubDate>Thu, 20 Aug 2026 09:44:09 +0000</pubDate>
      <link>https://dev.to/pranavhj1998/cross-mcu-migration-a-practical-guide-1kbh</link>
      <guid>https://dev.to/pranavhj1998/cross-mcu-migration-a-practical-guide-1kbh</guid>
      <description>&lt;p&gt;You've been told to port the firmware from one MCU to another. Maybe the chip went EOL. Maybe the shortage made it unavailable. Maybe the new product variant needs Bluetooth and your current MCU doesn't have it.&lt;/p&gt;

&lt;p&gt;Whatever the reason, you're staring at tens of thousands of lines of C that were written for one specific chip, and you need them running on a different one. This guide is the process I follow. It won't make the port painless, but it'll keep you from wasting time on the wrong things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before You Touch Any Code
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 0: Understand the Target
&lt;/h3&gt;

&lt;p&gt;Before you change a single line, answer these questions about the target MCU:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Why It Matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What's the clock tree look like?&lt;/td&gt;
&lt;td&gt;Peripheral speeds, PLL config, clock domains are never the same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What DMA model does it use?&lt;/td&gt;
&lt;td&gt;Linked-list DMA vs channel-based vs no DMA — big architectural impact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What's the interrupt priority scheme?&lt;/td&gt;
&lt;td&gt;ARM NVIC is standard, but the number of priority levels and grouping differs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What SDK/HAL does the vendor provide?&lt;/td&gt;
&lt;td&gt;STM32 HAL vs nRF Connect SDK vs ESP-IDF — completely different philosophies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What's the flash/RAM budget?&lt;/td&gt;
&lt;td&gt;Tight MCUs may need code restructuring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What RTOS does the vendor SDK expect?&lt;/td&gt;
&lt;td&gt;nRF Connect SDK assumes Zephyr. ESP-IDF has FreeRTOS built in. STM32 HAL is RTOS-agnostic.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Spend a day on this. Read the reference manual's clock tree and peripheral overview sections. It saves weeks later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Dependency Audit
&lt;/h3&gt;

&lt;p&gt;This is the most important step. Catalog every vendor-specific dependency in your codebase.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Quick audit for STM32 HAL dependencies&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"HAL_&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;LL_&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;__HAL_&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;stm32"&lt;/span&gt; src/ &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.c"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.h"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"// "&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt;: &lt;span class="nt"&gt;-k1&lt;/span&gt;,1 | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; hal_dependencies.txt

&lt;span class="c"&gt;# Count by peripheral type&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s2"&gt;"HAL_(GPIO|SPI|I2C|UART|TIM|DMA|ADC|DAC|RCC|PWR|FLASH|RTC|IWDG|WWDG|CAN|USB|ETH)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  hal_dependencies.txt | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typical output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    187 HAL_GPIO
    143 HAL_SPI
     98 HAL_I2C
     87 HAL_TIM
     76 HAL_UART
     54 HAL_DMA
     34 HAL_ADC
     29 HAL_RCC
     18 HAL_PWR
     12 HAL_FLASH
      8 HAL_RTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells you where the work is. GPIO and SPI will take the most effort not because they're complex, but because there are the most call sites.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Classify Each Dependency
&lt;/h3&gt;

&lt;p&gt;Not all HAL calls are equal. Classify them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direct equivalents (green):&lt;/strong&gt; The target MCU has a function that does exactly the same thing with different syntax. &lt;code&gt;HAL_GPIO_WritePin()&lt;/code&gt; → &lt;code&gt;nrf_gpio_pin_write()&lt;/code&gt;. These are mechanical translations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Behavioral differences (yellow):&lt;/strong&gt; The target MCU can do the same thing, but the API works differently. STM32's SPI uses handles and callbacks; nRF Connect SDK uses Zephyr's SPI API with transaction descriptors. You need to understand both models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No equivalent (red):&lt;/strong&gt; The target MCU doesn't have the feature, or implements it fundamentally differently. STM32's flexible DMA linked-list mode vs nRF52's EasyDMA which has a different set of constraints. These need redesign.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Migration Effort Matrix:

┌─────────────────────────────────────────────────┐
│  Peripheral   │  Calls  │ Class  │ Est. Effort  │
├───────────────┼─────────┼────────┼──────────────┤
│  GPIO         │   187   │ Green  │   1 day      │
│  SPI          │   143   │ Yellow │   3 days     │
│  I2C          │    98   │ Yellow │   2 days     │
│  Timer        │    87   │ Yellow │   3 days     │
│  UART         │    76   │ Green  │   1 day      │
│  DMA          │    54   │ Red    │   5 days     │
│  ADC          │    34   │ Yellow │   2 days     │
│  Clock config │    29   │ Red    │   2 days     │
│  Power mgmt   │    18   │ Yellow │   1 day      │
│  Flash/NVM    │    12   │ Red    │   2 days     │
│  RTC          │     8   │ Green  │   0.5 days   │
├───────────────┼─────────┼────────┼──────────────┤
│  TOTAL        │   746   │        │  ~22 days    │
└─────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Migration Process
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Phase 1: Build System (Days 1-2)
&lt;/h3&gt;

&lt;p&gt;Get the project compiling for the new target — even if nothing works yet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cmake"&gt;&lt;code&gt;&lt;span class="c1"&gt;# CMakeLists.txt — add target selection&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;TARGET_MCU &lt;span class="s2"&gt;"stm32f4"&lt;/span&gt; CACHE STRING &lt;span class="s2"&gt;"Target MCU family"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;set_property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;CACHE TARGET_MCU PROPERTY STRINGS stm32f4 nrf52840 esp32s3&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;TARGET_MCU STREQUAL &lt;span class="s2"&gt;"nrf52840"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;CMAKE_TOOLCHAIN_FILE &lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NRF_SDK_PATH&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/toolchain.cmake&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;add_subdirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;hal/nrf52&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;elseif&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;TARGET_MCU STREQUAL &lt;span class="s2"&gt;"stm32f4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;add_subdirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;hal/stm32&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;endif&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Application code is the SAME regardless of target&lt;/span&gt;
&lt;span class="nb"&gt;add_subdirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;application&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;target_link_libraries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;application PRIVATE hal&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're migrating to a Zephyr-based SDK (nRF Connect), you'll need to restructure into a Zephyr application. This is a bigger lift:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my_project/
├── CMakeLists.txt      # Zephyr-style CMake
├── prj.conf            # Kconfig
├── boards/             # Board overlays
│   ├── nrf52840dk_nrf52840.overlay
│   └── nucleo_f429zi.overlay
├── src/
│   └── main.c
└── hal/                # Your abstraction (if not using Zephyr's drivers directly)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Phase 2: Clock Tree and Startup (Days 2-3)
&lt;/h3&gt;

&lt;p&gt;This is where most estimates go wrong. Every MCU has a different clock tree, and getting it wrong produces bizarre failures later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STM32:&lt;/strong&gt; CubeMX generates &lt;code&gt;SystemClock_Config()&lt;/code&gt;. Clock tree has HSE/HSI → PLL → SYSCLK → AHB/APB prescalers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;nRF52:&lt;/strong&gt; Simpler clock model. HFCLK (64 MHz, from crystal or RC) and LFCLK (32.768 kHz). Less configurable but less error-prone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ESP32:&lt;/strong&gt; Dual-core, much more complex. PLL, CPU frequency, APB frequency, RTC clocks.&lt;/p&gt;

&lt;p&gt;Don't try to match clock-for-clock. Understand what frequencies your peripherals need and configure the target's clock tree to deliver them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// STM32: complex clock config generated by CubeMX&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SystemClock_Config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;RCC_OscInitTypeDef&lt;/span&gt; &lt;span class="n"&gt;RCC_OscInitStruct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;RCC_ClkInitTypeDef&lt;/span&gt; &lt;span class="n"&gt;RCC_ClkInitStruct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="c1"&gt;// ... 40 lines of clock configuration&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// nRF52: much simpler&lt;/span&gt;
&lt;span class="c1"&gt;// Most clock setup happens automatically via Zephyr's devicetree&lt;/span&gt;
&lt;span class="c1"&gt;// or a few register writes&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;clock_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;NRF_CLOCK&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;TASKS_HFCLKSTART&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;NRF_CLOCK&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;EVENTS_HFCLKSTARTED&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Phase 3: Green Peripherals First (Days 3-5)
&lt;/h3&gt;

&lt;p&gt;Start with the easy wins. GPIO, UART, basic timers.&lt;/p&gt;

&lt;p&gt;GPIO migration between any two Cortex-M MCUs is mostly mechanical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// STM32 → nRF52 GPIO mapping&lt;/span&gt;
&lt;span class="c1"&gt;// HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET)&lt;/span&gt;
&lt;span class="c1"&gt;// becomes:&lt;/span&gt;
&lt;span class="c1"&gt;// nrf_gpio_pin_set(NRF_GPIO_PIN_MAP(0, 5))&lt;/span&gt;

&lt;span class="c1"&gt;// Or if you have an abstraction layer:&lt;/span&gt;
&lt;span class="c1"&gt;// gpio_write((gpio_pin_t){0, 5}, 1)  // same call, different backend&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get LEDs blinking and UART printing. This validates your build system, startup code, and basic peripheral access. Everything else builds on this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 4: Yellow Peripherals (Days 5-15)
&lt;/h3&gt;

&lt;p&gt;SPI, I2C, and timers usually have behavioral differences that require understanding both MCU's models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SPI example: STM32 → nRF52 (Zephyr)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// STM32 HAL SPI&lt;/span&gt;
&lt;span class="n"&gt;HAL_SPI_TransmitReceive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;hspi1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx_buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rx_buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HAL_MAX_DELAY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Zephyr SPI (used by nRF Connect SDK)&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;spi_buf&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{.&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx_buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;spi_buf&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{.&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rx_buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;spi_buf_set&lt;/span&gt; &lt;span class="n"&gt;tx_set&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{.&lt;/span&gt;&lt;span class="n"&gt;buffers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;spi_buf_set&lt;/span&gt; &lt;span class="n"&gt;rx_set&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{.&lt;/span&gt;&lt;span class="n"&gt;buffers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="n"&gt;spi_transceive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spi_dev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;spi_cfg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tx_set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;rx_set&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API model is completely different (handle+callback vs device+descriptor), but the functionality is the same. Don't try to write a compatibility wrapper that makes Zephyr's API look like STM32's. Learn the target's API and use it idiomatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 5: Red Peripherals (Days 15-22)
&lt;/h3&gt;

&lt;p&gt;DMA, complex timers, and power management. These are where the real work is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DMA is the biggest headache.&lt;/strong&gt; Every MCU family implements DMA differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;STM32:&lt;/strong&gt; DMA streams/channels, each assignable to specific peripherals. Flexible but complex.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nRF52:&lt;/strong&gt; EasyDMA, tightly integrated with each peripheral. Less flexible but simpler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ESP32:&lt;/strong&gt; GDMA with channel allocation. Different yet again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no mechanical translation. You need to understand what the DMA was doing (circular buffer? ping-pong? linked list?) and redesign it for the target's DMA model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// STM32: DMA circular buffer for ADC&lt;/span&gt;
&lt;span class="n"&gt;hdma_adc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DMA_CIRCULAR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;hdma_adc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MemInc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DMA_MINC_ENABLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;HAL_DMA_Start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;hdma_adc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ADC1&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;DR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;adc_buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ADC_BUF_LEN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// nRF52: SAADC with EasyDMA — completely different model&lt;/span&gt;
&lt;span class="n"&gt;nrfx_saadc_buffer_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adc_buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ADC_BUF_LEN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// EasyDMA handles the transfer internally — no separate DMA config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Phase 6: Integration Testing (Days 22-25)
&lt;/h3&gt;

&lt;p&gt;Once all peripherals are ported, test the system as a whole:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Peripheral smoke test:&lt;/strong&gt; Each peripheral works in isolation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Communication test:&lt;/strong&gt; SPI/I2C devices respond correctly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timing test:&lt;/strong&gt; Real-time operations meet deadlines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Power test:&lt;/strong&gt; Sleep modes work, current consumption is acceptable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stress test:&lt;/strong&gt; Run for 48 hours, check for memory leaks, watchdog resets&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Phase 7: Edge Cases (Days 25-28)
&lt;/h3&gt;

&lt;p&gt;These are what catches teams 3 weeks into testing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interrupt priority differences:&lt;/strong&gt; STM32 has 16 priority levels, nRF52 has 4 (in some configs). If your original code relied on fine-grained priorities, you need to restructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Byte ordering in peripheral registers:&lt;/strong&gt; Usually the same (little-endian ARM), but DMA scatter-gather can expose ordering issues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Startup timing:&lt;/strong&gt; Some peripherals need time to stabilize after power-on. Your original code might have had implicit delays from slow clock startup that the new MCU doesn't have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brownout behavior:&lt;/strong&gt; Different MCUs handle power dips differently. Test power-off-power-on sequences.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Estimation Formula
&lt;/h2&gt;

&lt;p&gt;From my experience, here's a rough formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Estimated weeks = (LOC / 10000) × peripheral_complexity × abstraction_factor

where:
  peripheral_complexity = 1.0 (GPIO/UART only)
                        = 1.5 (+ SPI/I2C)
                        = 2.5 (+ DMA/complex timers)
                        = 4.0 (+ USB/Ethernet/RF)

  abstraction_factor = 0.3 (full HAL abstraction in place)
                     = 1.0 (no abstraction)
                     = 1.5 (spaghetti code, vendor types everywhere)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example: 40K LOC, SPI+DMA, no abstraction = (40/10) × 2.5 × 1.0 = 10 weeks.&lt;/p&gt;

&lt;p&gt;Add 30% for testing and edge cases. So ~13 weeks. If your manager says "4 weeks," show them this formula.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools That Help
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;grep / ripgrep:&lt;/strong&gt; Fast dependency auditing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ctags / cscope:&lt;/strong&gt; Navigate call chains to find hidden vendor dependencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compiler warnings:&lt;/strong&gt; Build for the new target with &lt;code&gt;-Wall -Werror&lt;/code&gt; early — the compiler will find most API mismatches&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git branches:&lt;/strong&gt; Keep the original working on &lt;code&gt;main&lt;/code&gt;, do the port on a branch. You'll need to compare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI with multiple targets:&lt;/strong&gt; Build for both old and new target on every commit during the migration&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'm Building
&lt;/h2&gt;

&lt;p&gt;I'm working on a tool called &lt;a href="https://pranavhj.github.io/portpilot-landing/" rel="noopener noreferrer"&gt;PortPilot&lt;/a&gt; that automates the dependency audit and mapping phases (Steps 1-2 above). It scans your firmware, classifies every vendor HAL call, and generates a migration report showing what maps directly, what needs review, and what needs redesign.&lt;/p&gt;

&lt;p&gt;It won't do the port for you — the red peripherals still need engineering judgment. But it cuts the audit from a week to an hour and makes sure you don't miss anything.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Pranav Jain is an embedded systems engineer specializing in the middleware layer between hardware and application software. Find him on &lt;a href="https://github.com/pranavhj" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>embedded</category>
      <category>firmware</category>
      <category>mcu</category>
      <category>migration</category>
    </item>
  </channel>
</rss>
