<?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>Forem: lmilz</title>
    <description>The latest articles on Forem by lmilz (@lmilz).</description>
    <link>https://forem.com/lmilz</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1483432%2F3dfe916c-6225-495d-a3a4-19b906f36b1c.png</url>
      <title>Forem: lmilz</title>
      <link>https://forem.com/lmilz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/lmilz"/>
    <language>en</language>
    <item>
      <title>Dr. Strangelove or How I Learned to Stop Worrying and Love Testing</title>
      <dc:creator>lmilz</dc:creator>
      <pubDate>Wed, 13 May 2026 19:51:24 +0000</pubDate>
      <link>https://forem.com/lmilz/dr-strangelove-or-how-i-learned-to-stop-worrying-and-love-testing-576</link>
      <guid>https://forem.com/lmilz/dr-strangelove-or-how-i-learned-to-stop-worrying-and-love-testing-576</guid>
      <description>&lt;p&gt;When I was still programming at university, my “testing” looked roughly like this:&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="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;cout&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s"&gt;"Value: "&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;endl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;along with some classic debugging: setting breakpoints, inspecting variables, stepping through the code. Looking back, this was clearly not an effective approach. I don’t even want to know how many bugs went unnoticed back then. Over several professional roles, I gradually improved my testing skills. In my first job, I initially continued exactly as I had done at university. Later, I had the chance to dive deeper into testing. At that time, my workflow looked like this: implement changes, document them, integrate, deploy, and only then start testing. This naturally led to discovering bugs late, being unable to fix them right away, and shipping bugfixes only with the next release.&lt;/p&gt;

&lt;p&gt;This became increasingly frustrating for me. At the same time, I noticed that many developers “automate” their manual debugging process by reproducing the implementation inside their tests. This is a classic anti-pattern: you end up testing the implementation instead of the behavior. If you simply reproduce the production code in your tests, you're effectively writing the same code twice and that adds no value.&lt;/p&gt;

&lt;p&gt;I started with small steps: whenever I worked on a new feature, I wrote tests immediately. In parallel, I began adding tests to our legacy codebase to gain experience building a meaningful and maintainable test suite.&lt;br&gt;&lt;br&gt;
Early on, I fell into the usual traps myself: &lt;em&gt;I recreated the implementation in the test&lt;/em&gt; or &lt;em&gt;wrote tests that were so granular that the suite became hard to maintain.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Along the way, I realized that testing is not just testing. In particular, we distinguish between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verification: Are we building the product right?&lt;/li&gt;
&lt;li&gt;Validation: Are we building the right product?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consider a simple example: a football field has exact specifications. If those are implemented correctly, verification is satisfied. But whether you can play well on that field is a matter of validation. A field can meet all technical requirements and still be unplayable if the ground is uneven or sloped.&lt;/p&gt;

&lt;p&gt;Eventually, I had a kind of Eureka moment: proper testing is a learning process. In a way, it felt like my own little Dr. Strangelove moment. I stopped worrying about breaking things and started to love testing.  Because once your tests define the expected behavior, fear turns into confidence.&lt;br&gt;
Tests help me understand how the code is meant to behave and provide immediate feedback. The only problem was that my feedback always came too late. So I looked for ways to move it earlier in the process.&lt;/p&gt;

&lt;p&gt;After many iterations, I finally found the answer to my problem: I must not test the implementation, I must test the behavior. The method that enforces exactly this is called Test-Driven Development (TDD).&lt;/p&gt;

&lt;p&gt;TDD is often misunderstood. It’s not a testing technique but a design method. Tests are the tool we use to shape the design. TDD can be used for both verification and validation.&lt;/p&gt;

&lt;p&gt;To demonstrate my approach, I’ll use the calculation of the inverse square root. This function is used to normalize vectors. From this, we can derive verifiable mathematical properties as well as validation tests based on intended behavior. These properties must hold regardless of the internal implementation of &lt;code&gt;inverse_sqrt&lt;/code&gt;. Below is an example of the tests (with the input data &lt;code&gt;inputs&lt;/code&gt; and &lt;code&gt;vectors&lt;/code&gt; omitted for readability):&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;// ------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// 1. Positivity Test&lt;/span&gt;
&lt;span class="c1"&gt;//    f(x) &amp;gt; 0 for all x &amp;gt; 0&lt;/span&gt;
&lt;span class="c1"&gt;// ------------------------------------------------------------&lt;/span&gt;
&lt;span class="nd"&gt;#[test]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;always_positive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;inverse_sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.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="c1"&gt;// ------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// 2. Monotonicity Test&lt;/span&gt;
&lt;span class="c1"&gt;//    f(x) decreases as x increases&lt;/span&gt;
&lt;span class="c1"&gt;// ------------------------------------------------------------&lt;/span&gt;
&lt;span class="nd"&gt;#[test]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;monotony&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="nf"&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;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;inverse_sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;inverse_sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// 3. Product Rule Test&lt;/span&gt;
&lt;span class="c1"&gt;//    f(a·b) ≈ f(a)·f(b)&lt;/span&gt;
&lt;span class="c1"&gt;// ------------------------------------------------------------&lt;/span&gt;
&lt;span class="nd"&gt;#[test]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;product_rule&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="nf"&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;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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;let&lt;/span&gt; &lt;span class="n"&gt;x1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inverse_sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;x2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inverse_sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;inverse_sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;x1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;x2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.abs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.25&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="c1"&gt;// ------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// 4. Division Rule Test&lt;/span&gt;
&lt;span class="c1"&gt;//    f(a/b) ≈ f(a)/f(b)&lt;/span&gt;
&lt;span class="c1"&gt;// ------------------------------------------------------------&lt;/span&gt;
&lt;span class="nd"&gt;#[test]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;division_rule&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="nf"&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;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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;let&lt;/span&gt; &lt;span class="n"&gt;x1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inverse_sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;x2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inverse_sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;inverse_sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;x1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;x2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.abs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;2.5&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="c1"&gt;// ------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// 5. Validation: Normalization Length&lt;/span&gt;
&lt;span class="c1"&gt;//    A vector normalized with this function should have length ≈ 1&lt;/span&gt;
&lt;span class="c1"&gt;// ------------------------------------------------------------&lt;/span&gt;
&lt;span class="nd"&gt;#[test]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;normalization_length_is_one&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;vectors&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="na"&gt;.1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="na"&gt;.1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="na"&gt;.2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="na"&gt;.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.sqrt&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nd"&gt;assert!&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="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.abs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1e-6&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;Now it gets practical: you write the implementation and the tests immediately verify whether it behaves as expected. This instant feedback is one of the greatest strengths of TDD. The test run produces results like these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;running 5 tests
&lt;span class="nb"&gt;test &lt;/span&gt;tests::monotony ... ok
&lt;span class="nb"&gt;test &lt;/span&gt;tests::division_rule ... ok
&lt;span class="nb"&gt;test &lt;/span&gt;tests::always_positive ... ok
&lt;span class="nb"&gt;test &lt;/span&gt;tests::normalization_length_is_one ... ok
&lt;span class="nb"&gt;test &lt;/span&gt;tests::product_rule ... ok

&lt;span class="nb"&gt;test &lt;/span&gt;result: ok. 5 passed&lt;span class="p"&gt;;&lt;/span&gt; 0 failed&lt;span class="p"&gt;;&lt;/span&gt; 0 ignored&lt;span class="p"&gt;;&lt;/span&gt; 0 measured&lt;span class="p"&gt;;&lt;/span&gt; 0 filtered out&lt;span class="p"&gt;;&lt;/span&gt; finished &lt;span class="k"&gt;in &lt;/span&gt;0.00s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This illustrates the essence of TDD: define the behavior first, then write the implementation that fulfills it.&lt;/p&gt;

&lt;p&gt;Another essential benefit: once tests clearly define the expected behavior, you can refactor the implementation without fear. The tests act as a safety net. They ensure that the visible behavior remains unchanged, even if the internals change dramatically.&lt;/p&gt;

&lt;p&gt;To make this even more concrete, here’s a small example that shows the real power of testing for behavior: you can completely change the implementation, yet the tests and therefore the behavior remain unchanged.&lt;/p&gt;

&lt;p&gt;Consider a simple function that filters out negative numbers. The tests describe what the function must do, not how it must do it:&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="nd"&gt;#[test]&lt;/span&gt; 
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;filters_out_negative_numbers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;     
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&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="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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&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="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;     
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;filter_positive&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;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     
    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;vec!&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="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;  

&lt;span class="nd"&gt;#[test]&lt;/span&gt; 
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;keeps_order&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;     
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&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;2&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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;filter_positive&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;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     
    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&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;These tests specify two behaviors:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Negative numbers must be removed.&lt;/li&gt;
&lt;li&gt;The order of the remaining elements must be preserved.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now here is the first, simple implementation, fully correct but not particularly elegant:&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="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;filter_positive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;i32&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;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;     
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;         
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;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;result&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;v&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="n"&gt;result&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later, you might want to refactor it into a more idiomatic and functional style:&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="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;filter_positive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;    
    &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.copied&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.filter&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.collect&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 internal implementation is now completely different, yet the behavior remains exactly the same.&lt;/p&gt;

&lt;p&gt;And that's the crucial point: the tests still pass without changing a single line of them. They don't care how the behavior is achieved. They only care that it is achieved. This is exactly the kind of freedom and safety TDD provides: the ability to evolve and improve internal code confidently, without risking regressions.&lt;/p&gt;

&lt;p&gt;This is how software is created that not only works but also remains maintainable and evolvable over time, a core principle of good software design.&lt;/p&gt;

&lt;p&gt;Looking back, I don’t write tests today because I’m “supposed to”. I write them because they make me faster, calmer, and better at my craft.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>tdd</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>When 'Close to the Hardware' Isn't Close Enough</title>
      <dc:creator>lmilz</dc:creator>
      <pubDate>Tue, 12 May 2026 07:04:58 +0000</pubDate>
      <link>https://forem.com/lmilz/when-close-to-the-hardware-isnt-close-enough-46p8</link>
      <guid>https://forem.com/lmilz/when-close-to-the-hardware-isnt-close-enough-46p8</guid>
      <description>&lt;p&gt;I recently bought myself an STM32 Nucleo microcontroller board to play around with. What fascinated me was how much more flexible things are at this level, how much more you can do yourself. With an ESP32 that's not really the case, you're always tied to ESP-IDF or some other framework.&lt;/p&gt;

&lt;p&gt;I started with a first simple example, the kind everyone knows and has done before: the famous &lt;em&gt;Hello World&lt;/em&gt;. It's simple, and that's exactly why it's useful. You don't learn a language's syntax with it, it's too small for that. You learn how to actually &lt;em&gt;use&lt;/em&gt; the language. What file format, how to compile, how to link, how to run the result.&lt;/p&gt;

&lt;p&gt;That's why I always reach for &lt;em&gt;Hello World&lt;/em&gt; first whenever I pick up a new language or a new environment. It forces me to run the whole build system once before I do anything else. Two things I learned this way that weren't obvious to me at the start. First: you can learn a surprising amount from a simple example if you take it seriously. Second: simple is almost never really simple. Most things that look easy are easy because someone else hid the complexity for you.&lt;/p&gt;

&lt;p&gt;The embedded world has a &lt;em&gt;Hello World&lt;/em&gt; too: the blinking LED. Most microcontroller boards have an onboard LED that you turn on and off at some frequency. Sounds trivial. And it is, if you use a Hardware Abstraction Layer (HAL) and some ready-made project template.&lt;/p&gt;

&lt;p&gt;I've been working in automotive software for years, and before that on physics simulations at university. In my head I'd always been "close to the hardware", I write embedded software after all, not frontend. &lt;a href="https://lmilz.dev/blog/2025/11/08/From-C-to-Rust-Evolving-Programming-Languages-in-Automotive-Development.html" rel="noopener noreferrer"&gt;A while back I wrote about the roles of C, C++, and Rust in automotive&lt;/a&gt; and quietly took for granted that "embedded equals close to the hardware". At some point it hit me that this was a delusion. MCAL, AUTOSAR OS, RTE: there are more layers between me and the silicon than between a web app and the kernel. I wanted to actually get down to the bottom for once. No HAL, no framework, no vendor black box. Just the reference manual and the compiler.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Blinky in Rust
&lt;/h2&gt;

&lt;p&gt;In the Rust ecosystem the example quickly ends up looking like this:&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="nd"&gt;#![no_std]&lt;/span&gt;
&lt;span class="nd"&gt;#![no_main]&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;cortex_m_rt&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="n"&gt;panic_halt&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;stm32f4xx_hal&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;pac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;prelude&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="nd"&gt;#[entry]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;pac&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Peripherals&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;rcc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="py"&gt;.RCC&lt;/span&gt;&lt;span class="nf"&gt;.constrain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;clocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rcc&lt;/span&gt;&lt;span class="py"&gt;.cfgr&lt;/span&gt;&lt;span class="nf"&gt;.sysclk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="nf"&gt;.MHz&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="nf"&gt;.freeze&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;gpiob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="py"&gt;.GPIOB&lt;/span&gt;&lt;span class="nf"&gt;.split&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;led1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gpiob&lt;/span&gt;&lt;span class="py"&gt;.pb0&lt;/span&gt;&lt;span class="nf"&gt;.into_push_pull_output&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;led2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gpiob&lt;/span&gt;&lt;span class="py"&gt;.pb7&lt;/span&gt;&lt;span class="nf"&gt;.into_push_pull_output&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="py"&gt;.TIM1&lt;/span&gt;&lt;span class="nf"&gt;.delay_ms&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;clocks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;led1&lt;/span&gt;&lt;span class="nf"&gt;.toggle&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="nf"&gt;.delay_ms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400u32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;led2&lt;/span&gt;&lt;span class="nf"&gt;.toggle&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="nf"&gt;.delay_ms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100u32&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;It's short, type-safe, and it works. The compiler keeps me from toggling an input pin. The clock config goes through a builder pattern. Pin types carry their configuration in the type system, so &lt;code&gt;toggle()&lt;/code&gt; on an input pin is a compile error. The delay is timed off SYSCLK. And everything you &lt;em&gt;don't&lt;/em&gt; see, the vector table, the reset handler, the copy loop for &lt;code&gt;.data&lt;/code&gt;, the zeroing of &lt;code&gt;.bss&lt;/code&gt;, all of that comes from the &lt;code&gt;cortex-m-rt&lt;/code&gt; crate. The linker just gets a small &lt;code&gt;memory.x&lt;/code&gt; that tells it where flash and RAM are.&lt;/p&gt;

&lt;p&gt;That's exactly the problem I wanted to dig into. Not in the "the HAL is bad" sense (I actually came away appreciating it more), but: &lt;strong&gt;I wanted to see what the HAL does for me.&lt;/strong&gt; So the same thing again, but in C, no HAL, no CMSIS, just register addresses straight out of the reference manual.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hello World, embedded
&lt;/h2&gt;

&lt;p&gt;The board I picked was the Nucleo-F446ZE, and I started reading the docs (Reference Manual RM0390, chapter 6 for RCC and chapter 8 for GPIO).&lt;/p&gt;

&lt;p&gt;The blinky itself is quickly explained. Enable the GPIOB clock, configure PB0 as an output, in a loop toggle the output register. In C, with no abstraction, it looks like this. First the registers as macros, then &lt;code&gt;main()&lt;/code&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;stdbool.h&amp;gt;&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;
&lt;/span&gt;
&lt;span class="cp"&gt;#define RCC_BASE   0x40023800UL
#define GPIOB_BASE 0x40020400UL
&lt;/span&gt;
&lt;span class="cp"&gt;#define RCC_AHB1ENR  (*(volatile uint32_t*)(RCC_BASE  + 0x30UL))
#define GPIOB_MODER  (*(volatile uint32_t*)(GPIOB_BASE + 0x00UL))
#define GPIOB_ODR    (*(volatile uint32_t*)(GPIOB_BASE + 0x14UL))
&lt;/span&gt;
&lt;span class="cp"&gt;#define RCC_AHB1ENR_GPIOBEN (1UL &amp;lt;&amp;lt; 1)
#define LED_PIN             0U
&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;delay&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;n&lt;/span&gt;&lt;span class="p"&gt;)&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="n"&gt;n&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;__asm__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"nop"&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="kt"&gt;int&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;RCC_AHB1ENR&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="n"&gt;RCC_AHB1ENR_GPIOBEN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;GPIOB_MODER&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;=&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3UL&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LED_PIN&lt;/span&gt; &lt;span class="o"&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;GPIOB_MODER&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1UL&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LED_PIN&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&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="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;GPIOB_ODR&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1UL&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;LED_PIN&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;500000&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;Three things about this code need explaining.&lt;/p&gt;

&lt;p&gt;First: &lt;code&gt;*(volatile uint32_t*)(...)&lt;/code&gt;. That's memory-mapped I/O in its purest form. The hardware exposes certain addresses that don't point to ordinary RAM cells, but to registers of the peripherals. Writing to &lt;code&gt;RCC_AHB1ENR&lt;/code&gt; doesn't mean "write into a memory cell", it means "tell the RCC block which clocks to enable". The &lt;code&gt;volatile&lt;/code&gt; cast isn't a style choice, it's mandatory. Without &lt;code&gt;volatile&lt;/code&gt;, the compiler wouldn't care how often you write, it would optimize the accesses away as dead stores, and the blinky would silently do nothing. &lt;code&gt;volatile&lt;/code&gt; is the contract with the compiler: "hands off, every access has a side effect you can't see."&lt;/p&gt;

&lt;p&gt;Second: initializing &lt;code&gt;GPIOB_MODER&lt;/code&gt;. I clear the two mode bits for PB0 first, then set them to &lt;code&gt;01&lt;/code&gt; (General Purpose Output). Read-modify-write with &lt;code&gt;&amp;amp;=&lt;/code&gt; and &lt;code&gt;|=&lt;/code&gt;, so that other pins in the same register stay untouched. On Cortex-M, by the way, this is &lt;em&gt;not&lt;/em&gt; atomic, that's three instructions (&lt;code&gt;LDR&lt;/code&gt;, &lt;code&gt;ORR&lt;/code&gt;/&lt;code&gt;BIC&lt;/code&gt;, &lt;code&gt;STR&lt;/code&gt;), and an ISR could fire in between. It works here because no interrupts are active during init. If you actually need atomicity, you use the bit-band region (where available, it's gone on the Cortex-M7) or &lt;code&gt;LDREX&lt;/code&gt;/&lt;code&gt;STREX&lt;/code&gt;. For pure set-or-clear on GPIO output pins there's also the &lt;code&gt;BSRR&lt;/code&gt; register, which is specifically designed to let you set or reset individual bits atomically in one write, no read-modify-write required.&lt;/p&gt;

&lt;p&gt;Third: &lt;code&gt;delay()&lt;/code&gt;. The combination of &lt;code&gt;volatile&lt;/code&gt; on the parameter and the explicit &lt;code&gt;nop&lt;/code&gt; isn't decoration. Without &lt;code&gt;volatile&lt;/code&gt;, and depending on the optimization level, the compiler may simply skip decrementing the counter, because nobody reads the value. Without the &lt;code&gt;nop&lt;/code&gt;, it's free to collapse the loop body. Together they force the loop to actually run. The comment "500 ms at 16 MHz" is wishful thinking, since the real duration depends on the optimizer, flash wait states, and the pipeline. For a blinky that's fine, in production you'd use SysTick.&lt;/p&gt;

&lt;p&gt;So much for the functionality. The really interesting question isn't what's &lt;em&gt;in&lt;/em&gt; &lt;code&gt;main()&lt;/code&gt;, it's: how does &lt;code&gt;main()&lt;/code&gt; ever get called in the first place? On a PC the operating system does that. On a microcontroller there is no operating system, no loader, no process, nothing that reads in code, allocates memory, or prepares a runtime. Someone has to do all of this by hand. That's where it got interesting for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hardware doesn't know about &lt;code&gt;main()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;When the ARM Cortex-M4 in the STM32 powers on, it does something very concrete. It reads 4 bytes from address &lt;code&gt;0x08000000&lt;/code&gt; and loads them as the initial stack pointer. Then it reads the next 4 bytes from &lt;code&gt;0x08000004&lt;/code&gt;, interprets them as an address, and jumps there. That's not a software instruction, that's circuit logic, set in silicon. Everything that happens after that is software.&lt;/p&gt;

&lt;p&gt;One detail that can cost you hours if you don't know it: bit 0 of the reset vector address has to be set. The Cortex-M4 only knows the Thumb instruction set, and the CPU uses bit 0 of the jump address as a mode bit. If it's zero, you get a HardFault right after reset. The linker usually takes care of this for you, but anyone who builds the vector table by hand and has to cast a function pointer symbol themselves will learn this one the hard way.&lt;/p&gt;

&lt;p&gt;Which gives us a clear requirement: at address &lt;code&gt;0x08000000&lt;/code&gt; exactly the right thing has to be sitting there. This structure is called the vector table, and it's really just an array of function pointers. First entry is the stack pointer (cast as a function pointer, the hardware doesn't care about the type, it just reads 4 bytes). Second entry is the address of the reset handler. After that come NMI, HardFault, and the other handlers. On an interrupt, the hardware looks into this table, reads the address, jumps there. It's a hardware jump table, not a software dispatch.&lt;/p&gt;

&lt;p&gt;In code, heavily shortened, it looks like this. The full table also has MemManage, BusFault, UsageFault, SVCall, PendSV, SysTick, and then the roughly 80 STM32-specific IRQs:&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="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;".isr_vector"&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="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;vector_table&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="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="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;void&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;_estack&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;Reset_Handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Default_Handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* NMI */&lt;/span&gt;
    &lt;span class="n"&gt;Default_Handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* HardFault */&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;section(".isr_vector")&lt;/code&gt; attribute matters. It tells the compiler: this data belongs in a specially named section. Where that section ends up in memory, though, isn't decided here. That was the first moment I realized the compiler and the hardware don't talk to each other directly. Something's missing in between.&lt;/p&gt;

&lt;p&gt;Since the Cortex-M4 is a licensed ARM core, none of this is STM-specific. It works the same way on boards from NXP, Microchip, or TI. Once you've understood it once, you can dive right in on a different board.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sections floating in nothing
&lt;/h2&gt;

&lt;p&gt;The STM32 has two memory regions. Flash, non-volatile, starting at &lt;code&gt;0x08000000&lt;/code&gt;. RAM, volatile, starting at &lt;code&gt;0x20000000&lt;/code&gt;. Both on the same 32-bit address bus. From the CPU's point of view both regions are equally addressable; which addresses point to flash and which to RAM is decided by how the chip is wired.&lt;/p&gt;

&lt;p&gt;The C compiler knows none of this. It takes &lt;code&gt;main.c&lt;/code&gt;, produces machine code, puts it into a section called &lt;code&gt;.text&lt;/code&gt;. Constants go into &lt;code&gt;.rodata&lt;/code&gt;, initialized variables into &lt;code&gt;.data&lt;/code&gt;, uninitialized variables into &lt;code&gt;.bss&lt;/code&gt;. These are all just names. The compiler has no idea that &lt;code&gt;.text&lt;/code&gt; is supposed to end up in flash later and &lt;code&gt;.bss&lt;/code&gt; in RAM. It doesn't even know that flash and RAM exist. The sections have no absolute addresses. They're just floating in nothing.&lt;/p&gt;

&lt;p&gt;So someone has to decide which section ends up at which physical address. That's the job of the linker script.&lt;/p&gt;

&lt;h2&gt;
  
  
  The linker script is the floor plan
&lt;/h2&gt;

&lt;p&gt;A linker script is a text file with a &lt;code&gt;.ld&lt;/code&gt; extension. It describes two things: which memory regions exist, and which section goes where.&lt;/p&gt;

&lt;p&gt;The line&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ENTRY(Reset_Handler)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;tells the linker where the entry point is.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;MEMORY&lt;/code&gt; block lists the physical regions. The numbers come straight from the chip's datasheet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
    RAM   (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;SECTIONS&lt;/code&gt; block every section gets assigned to a region. &lt;code&gt;.isr_vector&lt;/code&gt; goes at the beginning of flash, because that's where the hardware reads its first 4 bytes. The &lt;code&gt;KEEP(*(.isr_vector))&lt;/code&gt; keeps the linker from throwing the vector table away, since nothing in the C code explicitly references the &lt;code&gt;vector_table&lt;/code&gt; symbol.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.text&lt;/code&gt; and &lt;code&gt;.rodata&lt;/code&gt; go into flash, because they're supposed to be non-volatile. &lt;code&gt;.bss&lt;/code&gt; goes into RAM, because it gets filled with zeros at runtime.&lt;/p&gt;

&lt;p&gt;My favorite part is &lt;code&gt;.data&lt;/code&gt;. These are variables with an initial value: &lt;code&gt;int baud_rate = 9600;&lt;/code&gt;. At runtime they need to live in RAM, otherwise they aren't writable. But the initial value has to be stored somewhere before the board ever gets power. So the initial value has to live in flash and get copied into RAM at startup.&lt;/p&gt;

&lt;p&gt;The linker script solves this by giving &lt;code&gt;.data&lt;/code&gt; two addresses. A virtual address (VMA) in RAM, that's the address the code expects the variable at. And a load address (LMA) in flash, that's where the initial values physically sit. Who actually does the copying, the linker script doesn't say. It only emits boundary markers as symbols: &lt;code&gt;_sidata&lt;/code&gt; (start of the initial values in flash), &lt;code&gt;_sdata&lt;/code&gt; and &lt;code&gt;_edata&lt;/code&gt; (start and end in RAM), &lt;code&gt;_sbss&lt;/code&gt; and &lt;code&gt;_ebss&lt;/code&gt; for the &lt;code&gt;.bss&lt;/code&gt; section.&lt;/p&gt;

&lt;p&gt;These symbols aren't variables in the usual sense. They don't occupy any memory. They're just numbers that the linker stamps in at the end. In C you access them by declaring them &lt;code&gt;extern&lt;/code&gt; and then taking their address:&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="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;_sidata&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;_sdata&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;_edata&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That confused me for a minute, because they look like variables but aren't. You never read the &lt;em&gt;value&lt;/em&gt; of &lt;code&gt;_sdata&lt;/code&gt;, you always use &lt;code&gt;&amp;amp;_sdata&lt;/code&gt;. The "variable" &lt;em&gt;is&lt;/em&gt; its own address.&lt;/p&gt;

&lt;h2&gt;
  
  
  The startup code is a mini OS
&lt;/h2&gt;

&lt;p&gt;The linker script sets the boundaries. Filling everything in is the job of the startup code. Traditionally a file called &lt;code&gt;startup.s&lt;/code&gt; in assembly, nowadays often &lt;code&gt;startup.c&lt;/code&gt; or inline in the same file. I kept mine inline in the blinky, for maximum transparency.&lt;/p&gt;

&lt;p&gt;The startup code is the reset handler. It does three things. It copies &lt;code&gt;.data&lt;/code&gt; from flash to RAM so that initialized variables have their initial values. It fills &lt;code&gt;.bss&lt;/code&gt; with zeros so the C standard for uninitialized variables is upheld. Then it calls &lt;code&gt;main()&lt;/code&gt;. With C++ there's a fourth step: calling global constructors, which runs through a section called &lt;code&gt;__init_array&lt;/code&gt;. In an AUTOSAR project this exact work lives inside the supplier's startup code and runs before &lt;code&gt;EcuM_Init&lt;/code&gt; ever sees a register.&lt;/p&gt;

&lt;p&gt;In the blinky it looks 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="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Reset_Handler&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="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;src&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;_sidata&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dst&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;_sdata&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="n"&gt;dst&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;_edata&lt;/span&gt;&lt;span class="p"&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;dst&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;src&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;dst&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;_sbss&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="n"&gt;dst&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;_ebss&lt;/span&gt;&lt;span class="p"&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;dst&lt;/span&gt;&lt;span class="o"&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="n"&gt;main&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="nb"&gt;true&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;Two things stood out to me when I wrote this for the first time.&lt;/p&gt;

&lt;p&gt;First: the startup code uses the linker symbols directly as loop bounds. That's the contract between the two files. The linker script promises that the symbols are there and point to the right addresses. The startup code just trusts it blindly. If you rename the symbols in the linker script, the startup code breaks silently. Nothing warns you. As an aside: the copy loop runs over &lt;code&gt;uint32_t*&lt;/code&gt;, not &lt;code&gt;uint8_t*&lt;/code&gt;. That's faster, one word per bus transaction instead of four bytes, and it works because the linker aligns the sections to 4 bytes. Unaligned 32-bit accesses can trigger a fault on Cortex-M depending on the configuration.&lt;/p&gt;

&lt;p&gt;Second: the &lt;code&gt;while (true) {}&lt;/code&gt; after &lt;code&gt;main()&lt;/code&gt;. On a PC, &lt;code&gt;main()&lt;/code&gt; returns to the operating system. Here there is no operating system. If &lt;code&gt;main()&lt;/code&gt; accidentally returns, the processor has to go somewhere. The infinite loop is insurance against it running wild through memory.&lt;/p&gt;

&lt;p&gt;The only reason the startup code can run at all, before the C environment is set up, is that it uses only stack-local variables. And the stack already works because the hardware loaded the stack pointer from the vector table at reset. The whole sequence is a domino chain. Each step has exactly the one precondition the previous step just created.&lt;/p&gt;

&lt;h2&gt;
  
  
  From &lt;code&gt;.c&lt;/code&gt; to bytes in flash
&lt;/h2&gt;

&lt;p&gt;What happens between source and a blinking LED is also not one step, but several. The preprocessor resolves includes and macros. The compiler translates each &lt;code&gt;.c&lt;/code&gt; file individually into assembly, architecture-specific via &lt;code&gt;-mcpu=cortex-m4 -mthumb&lt;/code&gt;. The assembler produces object files in ELF format, with relative addresses and unresolved symbols. The linker collects all sections of the same name from all object files, assigns them absolute addresses via the linker script, and resolves the symbols. What used to say "jump to &lt;code&gt;delay&lt;/code&gt;" now carries the concrete flash address of &lt;code&gt;delay&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The final product is an ELF file. It contains the machine code, but also program headers (which bytes go where in memory), the symbol table (function and variable names with their addresses, for the debugger), and optionally debug information (the mapping of machine code to source lines). A &lt;code&gt;.bin&lt;/code&gt; file, which you produce via &lt;code&gt;objcopy -O binary&lt;/code&gt;, is just the raw bytes without any metadata.&lt;/p&gt;

&lt;p&gt;To flash it, you use a tool like probe-rs or OpenOCD. The tool talks over a debug adapter (ST-Link, J-Link, or CMSIS-DAP) to the Cortex-M4's SWD port (Serial Wire Debug). The debug port has direct access to the entire address space of the chip, regardless of whether the CPU is running. The tool reads the ELF file, extracts the program headers, writes the bytes into flash, and triggers a reset. After which the whole cycle starts over. Hardware reads the vector table, jumps to the reset handler, startup initializes, &lt;code&gt;main()&lt;/code&gt; runs, LED blinks.&lt;/p&gt;

&lt;p&gt;I want to stress again that this is a little learning project of mine, not production code. In production you'd put an abstraction on top, either a vendor HAL (for example ST's) or the Cortex Microcontroller Software Interface Standard (CMSIS), which is vendor-agnostic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;p&gt;Two things stuck with me.&lt;/p&gt;

&lt;p&gt;The first: a &lt;em&gt;Hello World&lt;/em&gt; is very much not a waste of time if you take it seriously. I could have written the blinky with a HAL in five lines, done. Instead I wrote the vector table myself, the reset handler, read the linker script, understood the boundary markers. And I learned more about the system than I would have in weeks of framework tutorials.&lt;/p&gt;

&lt;p&gt;The second: simple is almost never actually simple. The blinky with a HAL isn't any easier than the blinky with registers, it's just further away from what's actually happening. Between &lt;code&gt;make flash&lt;/code&gt; and a blinking LED sit the linker script, the startup code, the ELF, the flashing, the hardware reset. All of this is there, even when you don't see it.&lt;/p&gt;

&lt;p&gt;For those of us in automotive this is particularly interesting. In a classical AUTOSAR project we never see the vector table, never see the reset handler, never see the linker script. The MCAL, the supplier's startup code, the OS init, BswM scheduling: all of that arrives as a black box, and we write runnables that get called from the RTE. Between power-on and &lt;code&gt;Rte_MainFunction_*&lt;/code&gt; there's the exact same chain as here. Hardware reads 4 bytes, jumps to the reset handler, someone initializes &lt;code&gt;.data&lt;/code&gt; and &lt;code&gt;.bss&lt;/code&gt;, someone calls the OS init, someone starts the tasks. It's just that each of those steps is buried inside an AUTOSAR configuration we normally don't open. Once you've built this foundation yourself, you read an MCAL doc differently. It also sharpens the language question. &lt;a href="https://lmilz.dev/blog/2025/11/08/From-C-to-Rust-Evolving-Programming-Languages-in-Automotive-Development.html" rel="noopener noreferrer"&gt;In the earlier post I argued that C, C++, and Rust each have their layer&lt;/a&gt;, but the layer we usually work on in AUTOSAR is several steps above the one where that choice really matters. The reset handler, the &lt;code&gt;.data&lt;/code&gt; copy, the linker script, those are all C, regardless of what we write on top. The supplier picks the language down there, we don't.&lt;/p&gt;

&lt;p&gt;Not seeing it is comfortable as long as everything works. The moment it doesn't, you have to go a level deeper. And then it's good to have been there before.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>iot</category>
      <category>learning</category>
      <category>programming</category>
    </item>
    <item>
      <title>From C to Rust - Evolving Programming Languages in Automotive Development</title>
      <dc:creator>lmilz</dc:creator>
      <pubDate>Sun, 10 May 2026 19:50:55 +0000</pubDate>
      <link>https://forem.com/lmilz/from-c-to-rust-evolving-programming-languages-in-automotive-development-186a</link>
      <guid>https://forem.com/lmilz/from-c-to-rust-evolving-programming-languages-in-automotive-development-186a</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In recent years, the automotive software landscape has changed dramatically. What used to be small, specialized electronic control units (ECUs) with narrowly defined functions has evolved into highly networked systems with powerful System-on-Chips (SoCs) and a growing fusion between traditional embedded and high-performance software development.&lt;/p&gt;

&lt;p&gt;This evolution brings new challenges, especially when it comes to choosing the right programming language. The language doesn’t just impact performance and resource usage; it also defines maintainability, safety, and ultimately the certifiability of the software.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Automotive Software Landscape
&lt;/h2&gt;

&lt;p&gt;Low-level microcontrollers still handle classic tasks such as engine control, sensor interfaces, or basic actuators. At the same time, high-performance SoCs power increasingly complex domains like driver assistance, infotainment, and connectivity.&lt;/p&gt;

&lt;p&gt;Each layer has its own constraints and challenges. Many ECUs must meet real-time requirements while operating under tight memory and processing limits. Some controllers still run with only a few kilobytes of RAM or Flash, demanding highly efficient and deterministic code. Meanwhile, system complexity continues to grow, with vehicle platforms ranging from dozens of distributed ECUs to centralized zone architectures, each with unique communication and software requirements.&lt;/p&gt;

&lt;p&gt;In such environments, many language features are off-limits: dynamic heap allocation, uncontrolled pointers, and large standard libraries are often forbidden. That leaves us with three real contenders for automotive software development: C, C++, and Rust.&lt;/p&gt;

&lt;h2&gt;
  
  
  C - The Embedded Classic
&lt;/h2&gt;

&lt;p&gt;If you work in automotive software, C is unavoidable. Since the early days of electronic control units, it has been the de facto standard and for good reason.&lt;/p&gt;

&lt;p&gt;C was designed for system programming, and that’s exactly why it fits so well in the automotive domain: it offers direct hardware access, minimal abstraction, and precise control over timing and resources. When you need to process a signal within microseconds or trigger an actuator at exactly the right time, C is unbeatable.&lt;/p&gt;

&lt;p&gt;Why C still rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Full hardware control: Direct access to registers, memory, and interrupts, essential for real-time control.&lt;/li&gt;
&lt;li&gt;Minimal overhead: No hidden runtime mechanisms, no unnecessary memory use.&lt;/li&gt;
&lt;li&gt;Excellent toolchain support: C is universally supported across microcontroller families, including certified compilers, debuggers, and analysis tools.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The downsides:&lt;br&gt;
C lacks type safety and relies entirely on the developer to manage memory and concurrency correctly. Pointer errors or race conditions can lead to unpredictable behavior. Large C codebases, often grown over decades, are notoriously hard to maintain.&lt;/p&gt;

&lt;p&gt;Still, C remains the bedrock of embedded software: predictable, efficient, and deeply integrated into automotive safety and toolchain ecosystems.&lt;br&gt;
Modern languages build on its shoulders, but they don’t replace its role in low-level, deterministic control.&lt;/p&gt;

&lt;h2&gt;
  
  
  C++ - Structured Power for Complex Systems
&lt;/h2&gt;

&lt;p&gt;As ECUs grow more powerful and interconnected, C++ has gained significant traction in the automotive world. It brings structure, modularity, and abstraction, qualities that are becoming essential in modern vehicle software.&lt;/p&gt;

&lt;p&gt;Why C++ matters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Object-oriented design: Enables clear separation of concerns and better code organization in large projects.&lt;/li&gt;
&lt;li&gt;Templates and generic programming: Combine type safety and code reuse without runtime cost.&lt;/li&gt;
&lt;li&gt;RAII and stronger type checking: Improve memory safety and resource management.&lt;/li&gt;
&lt;li&gt;Modern language features: &lt;code&gt;constexpr&lt;/code&gt;, &lt;code&gt;auto&lt;/code&gt;, and &lt;code&gt;std::array&lt;/code&gt; improve readability and reliability at zero runtime cost.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In practice, this means we can design flexible, modular interfaces for instance, a unified abstraction layer for radar, camera, and lidar sensors to enable sensor fusion. In C, that would require far more boilerplate and unsafe pointer tricks.&lt;/p&gt;

&lt;p&gt;However, embedded C++ comes with caveats. Many automotive compilers only support subsets of the language, and standard libraries (&lt;code&gt;&amp;lt;iostream&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;string&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;vector&amp;gt;&lt;/code&gt;) are often restricted due to dynamic memory usage or exceptions. Templates and inlining can easily bloat the binary size, and long compile times are common.&lt;/p&gt;

&lt;p&gt;In short: C++ has become the bridge between low-level efficiency and modern software design. When used with discipline and clear safety guidelines like MISRA, it enables robust and maintainable systems without sacrificing performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rust – The Rising Challenger
&lt;/h2&gt;

&lt;p&gt;When talking about the future of automotive software, one language always comes up: Rust.&lt;/p&gt;

&lt;p&gt;Rust brings something that C and C++ have struggled to achieve memory and thread safety at compile time, without sacrificing performance.&lt;br&gt;
In a safety-critical industry, that’s a big deal.&lt;/p&gt;

&lt;p&gt;What makes Rust so exciting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compile-time memory safety: No null pointers, no use-after-free, no buffer-overflows.&lt;/li&gt;
&lt;li&gt;Ownership and borrowing: Clear rules for who owns memory and how it can be accessed.&lt;/li&gt;
&lt;li&gt;Zero-cost abstractions: High-level expressiveness without garbage collection or runtime overhead.&lt;/li&gt;
&lt;li&gt;Safe concurrency: The type system prevents data races by design.&lt;/li&gt;
&lt;li&gt;Growing embedded ecosystem: Frameworks like &lt;code&gt;embedded-hal&lt;/code&gt; and &lt;code&gt;no_std&lt;/code&gt; make Rust viable on microcontrollers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Of course, Rust isn’t production-ready everywhere yet. Certified toolchains and ISO 26262 support are still emerging, and the learning curve can be steep. But after that initial hurdle, developers gain an unprecedented level of reliability and maintainability.&lt;/p&gt;

&lt;p&gt;To me, Rust feels like the natural evolution of C and C++: the same low-level control and efficiency, but with built-in safety guarantees. Especially in application layers or safety-critical software, Rust has the potential to fundamentally reshape how we build automotive systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outlook: Balancing the Proven and the New
&lt;/h2&gt;

&lt;p&gt;C, C++, and Rust are not competitors, they complement each other.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;C remains indispensable for hardware-near programming, where every cycle and byte matters.&lt;/li&gt;
&lt;li&gt;C++ provides structure and abstraction for complex, modular systems and middleware.&lt;/li&gt;
&lt;li&gt;Rust introduces memory safety, concurrency safety, and modern reliability to the mix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The future of automotive software will be hybrid. C will stay the foundation for low-level, deterministic code; C++ will handle structured, object-oriented logic; and Rust will take over where safety, reliability, and modern software practices meet.&lt;/p&gt;

&lt;p&gt;Over the next few years, we’ll see how quickly Rust gains traction in production automotive environments. But one thing is certain: the future won’t belong to a single language, it will belong to those who choose the right tool for the right layer. Combining efficiency, maintainability, and safety is what will define the next generation of automotive software.&lt;/p&gt;

</description>
      <category>iot</category>
      <category>programming</category>
      <category>rust</category>
      <category>softwaredevelopment</category>
    </item>
  </channel>
</rss>
