<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://skoopsy.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://skoopsy.dev/" rel="alternate" type="text/html" /><updated>2026-08-10T11:02:03+00:00</updated><id>https://skoopsy.dev/feed.xml</id><title type="html">Dave O’Connor (Skoopsy)’s Blog</title><subtitle>Coding, data visualisation, cardiovascular research, and surfing!</subtitle><author><name>Dave</name></author><entry><title type="html">STM32 #18: Application Storage Layer using FatFs</title><link href="https://skoopsy.dev/stm32/2026/07/30/STM32-19-storage-api.html" rel="alternate" type="text/html" title="STM32 #18: Application Storage Layer using FatFs" /><published>2026-07-30T10:00:00+00:00</published><updated>2026-07-30T10:00:00+00:00</updated><id>https://skoopsy.dev/stm32/2026/07/30/STM32-19-storage-api</id><content type="html" xml:base="https://skoopsy.dev/stm32/2026/07/30/STM32-19-storage-api.html"><![CDATA[<p>In this post I’m going to add my own storage API on top of the FatFs layer that was tested in the last post. This will give the firmware a simple way to record events to a normal file on the SD card, which is a small but important step towards a useful data logger.</p>

<p>I plan to give the rest of the application one clear entry point for storage, rather than peppering FatFs calls all over the place. I want other parts of the code to be able to call things like <code class="language-plaintext highlighter-rouge">storage_log_event("Initialisation successful")</code> or, later, <code class="language-plaintext highlighter-rouge">storage_write_sensor_sample(&amp;sample)</code> without knowing about <code class="language-plaintext highlighter-rouge">f_mount()</code>, <code class="language-plaintext highlighter-rouge">f_open()</code>, <code class="language-plaintext highlighter-rouge">f_write()</code>, file paths, or FatFs objects.</p>

<p>This post is really about stopping FatFs from leaking into the rest of the firmware.</p>

<p>Note: I do need to be careful here, SD card writes can block. FatFs may need to update file data, directory entries, FAT tables etc. The SD card itself can be busy internally writing. In the future the architecture might follow something like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Sensor read -&gt; sample to RAM buffer -&gt; main loop calls storage_process() -&gt; storage layer write chunks to SD card.
</code></pre></div></div>

<p>Files for the storage layer:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src/app/app_storage.c
include/app/app_storage.h
</code></pre></div></div>

<h1 id="storage-api">Storage API</h1>

<p>The requirements for this will be to:</p>

<ol>
  <li>Mount the filesystem once</li>
  <li>Create a log file</li>
  <li>Append log messages</li>
  <li>Keep FatFs specifics out of main.c</li>
</ol>

<p>Here is the app_storage.h:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#pragma once
</span>
<span class="cp">#include</span> <span class="cpf">&lt;stdint.h&gt;</span><span class="cp">
</span>
<span class="c1">// Storage error codes</span>
<span class="k">typedef</span> <span class="k">enum</span> <span class="p">{</span>
        <span class="n">STORAGE_OK</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
        <span class="n">STORAGE_ERR_NOT_READY</span><span class="p">,</span>
        <span class="n">STORAGE_ERR_PARAM</span><span class="p">,</span>
        <span class="n">STORAGE_ERR_MOUNT</span><span class="p">,</span>
        <span class="n">STORAGE_ERR_OPEN</span><span class="p">,</span>
        <span class="n">STORAGE_ERR_WRITE</span><span class="p">,</span>
        <span class="n">STORAGE_ERR_PARTIAL_WRITE</span><span class="p">,</span>
        <span class="n">STORAGE_ERR_READ</span><span class="p">,</span>
        <span class="n">STORAGE_ERR_CLOSE</span><span class="p">,</span>
<span class="p">}</span> <span class="n">storage_status_t</span><span class="p">;</span>

<span class="c1">// Public storage API</span>
<span class="n">storage_status_t</span> <span class="nf">storage_init</span><span class="p">(</span><span class="kt">void</span><span class="p">);</span>
<span class="n">storage_status_t</span> <span class="nf">storage_log_event</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">message</span><span class="p">);</span>
</code></pre></div></div>

<p>The log file to be created will be called: <code class="language-plaintext highlighter-rouge">0:/EVENT.LOG</code>.</p>

<p>I’m sticking with the 8.3 style filenames for FAT32 for now so that I don’t have to add in long file name support making the FatFs memory burden larger.</p>

<h1 id="storage-implementation">Storage implementation</h1>

<p>I am going to reuse the tests developed in the last post to create the functions for app_storage.c so for a more detailed explanation of how I’m using the FatFs API check out my <a href="https://skoopsy.dev/stm32/2026/07/29/STM32-18-using-fatfs-diskio-for-reading-and-writing-microsd.html">last post.</a></p>

<p>Let’s set up app_storage.c:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"app/app_storage.h"</span><span class="cp">
#include</span> <span class="cpf">"bsp/bsp_usart.h"</span><span class="cp">
</span>
<span class="cp">#include</span> <span class="cpf">"ff.h"</span><span class="cp">
#include</span> <span class="cpf">&lt;stdbool.h&gt;</span><span class="cp">
</span>
<span class="c1">// Drive and File path macros:</span>
<span class="cp">#define STORAGE_DRIVE_PATH "0:"
#define STORAGE_PATH_EVENT_LOG "0:/EVENT.LOG"
</span>
<span class="k">static</span> <span class="n">FATFS</span> <span class="n">storage_fs</span><span class="p">;</span>                 <span class="c1">// FatFs filesystem object</span>
<span class="k">static</span> <span class="n">bool</span> <span class="n">storage_initialised</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span> <span class="c1">// Set flag after mounting</span>
</code></pre></div></div>

<p>I’m keeping the filesystem object <code class="language-plaintext highlighter-rouge">storage_fs</code> static so that it is private to the module, but will stay for the lifetime of the program so that it can be used by any of the app_storage functions as and when required.</p>

<p>First we will build the initialisation function, which mounts the FatFs volume:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">storage_status_t</span> <span class="nf">storage_init</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">FRESULT</span> <span class="n">fr</span><span class="p">;</span> <span class="c1">// FatFs return status object</span>
        
        <span class="c1">// Mount the drive</span>
        <span class="n">fr</span> <span class="o">=</span> <span class="n">f_mount</span><span class="p">(</span><span class="o">&amp;</span><span class="n">storage_fs</span><span class="p">,</span>
                     <span class="n">STORAGE_DRIVE_PATH</span><span class="p">,</span>
                     <span class="mi">1</span><span class="p">);</span> <span class="c1">// Mount drive immediately</span>

        <span class="c1">// Basic error handling</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">fr</span> <span class="o">!=</span> <span class="n">FR_OK</span><span class="p">)</span> <span class="p">{</span> 
                <span class="n">storage_initialised</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
                <span class="k">return</span> <span class="n">STORAGE_ERR_MOUNT</span><span class="p">;</span>
        <span class="p">}</span>
    
        <span class="c1">// Success</span>
        <span class="n">storage_initialised</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
        <span class="k">return</span> <span class="n">STORAGE_OK</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then we can write a function to write event logs which is basically our write test from the last post but I’m using different f_open() modes this time - I want to open a file and if the file doesn’t exist already then create it, set it to allow writing to the file, AND make sure to set the read/write pointer to the end of the file, so that new logs don’t overwrite old logs. Here is the [list of f_open mode flags available]
(https://elm-chan.org/fsw/ff/doc/open.html):</p>

<table>
  <thead>
    <tr>
      <th>Flags</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>FA_READ</td>
      <td>Specifies read access to the file. Data can be read from the file.</td>
    </tr>
    <tr>
      <td>FA_WRITE</td>
      <td>Specifies write access to the file. Data can be written to the file. Combine with FA_READ for read-write access.</td>
    </tr>
    <tr>
      <td>FA_OPEN_EXISTING</td>
      <td>Opens the file. The function fails if the file is not existing. (Default)</td>
    </tr>
    <tr>
      <td>FA_CREATE_ALWAYS</td>
      <td>Creates a new file. If the file is existing, the file is truncated and overwritten.</td>
    </tr>
    <tr>
      <td>FA_CREATE_NEW</td>
      <td>Creates a new file. The function fails if the file is existing.</td>
    </tr>
    <tr>
      <td>FA_OPEN_ALWAYS</td>
      <td>Opens the file. If it is not exist, a new file is created.</td>
    </tr>
    <tr>
      <td>FA_OPEN_APPEND</td>
      <td>Same as FA_OPEN_ALWAYS except the read/write pointer is set end of the file.</td>
    </tr>
  </tbody>
</table>

<p>Oh and would you look at that there is are modes exactly for my needs: <code class="language-plaintext highlighter-rouge">FA_OPEN_APPEND</code> and <code class="language-plaintext highlighter-rouge">FA_WRITE</code>, how wonderful.</p>

<p>Here is the function to write logs:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;string.h&gt;</span><span class="cp">
</span>
<span class="n">storage_status_t</span> <span class="nf">storage_log_event</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">message</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">FRESULT</span> <span class="n">fr</span><span class="p">;</span>             <span class="c1">//FatFs function return status object</span>
        <span class="n">FIL</span> <span class="n">event_file</span><span class="p">;</span>         <span class="c1">// FatFs file object</span>
        <span class="n">UINT</span> <span class="n">bytes_written</span> <span class="o">=</span> <span class="mi">0u</span><span class="p">;</span>
        <span class="n">UINT</span> <span class="n">bytes_to_write</span> <span class="o">=</span> <span class="mi">0u</span><span class="p">;</span>

        <span class="c1">// Check if the drive has been mounted:</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">storage_initialised</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">STORAGE_ERR_NOT_READY</span><span class="p">;</span> <span class="p">}</span>

        <span class="c1">// Check if there is actually a message to write:</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">message</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">STORAGE_ERR_PARAM</span><span class="p">;</span> <span class="p">}</span>

        <span class="c1">// Open EVENT.LOG:</span>
        <span class="n">fr</span> <span class="o">=</span> <span class="n">f_open</span><span class="p">(</span><span class="o">&amp;</span><span class="n">event_file</span><span class="p">,</span>
                    <span class="n">STORAGE_PATH_EVENT_LOG</span><span class="p">,</span>
                    <span class="n">FA_WRITE</span> <span class="o">|</span> <span class="n">FA_OPEN_APPEND</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">fr</span> <span class="o">!=</span> <span class="n">FR_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">STORAGE_ERR_OPEN</span><span class="p">;</span> <span class="p">}</span>

        <span class="c1">// Write message to EVENT.LOG</span>
        <span class="n">bytes_to_write</span> <span class="o">=</span> <span class="p">(</span><span class="n">UINT</span><span class="p">)</span><span class="n">strlen</span><span class="p">(</span><span class="n">message</span><span class="p">);</span>
        <span class="n">fr</span> <span class="o">=</span> <span class="n">f_write</span><span class="p">(</span><span class="o">&amp;</span><span class="n">event_file</span><span class="p">,</span>
                     <span class="n">message</span><span class="p">,</span>
                     <span class="n">bytes_to_write</span><span class="p">,</span>
                     <span class="o">&amp;</span><span class="n">bytes_written</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">fr</span> <span class="o">!=</span> <span class="n">FR_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">f_close</span><span class="p">(</span><span class="o">&amp;</span><span class="n">event_file</span><span class="p">);</span>
                <span class="k">return</span> <span class="n">STORAGE_ERR_WRITE</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// You can also do a check here to see if it fully wrote the message or ran out of space and truncated the message by comparing bytes_written:</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">bytes_to_write</span> <span class="o">!=</span> <span class="n">bytes_written</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">f_close</span><span class="p">(</span><span class="o">&amp;</span><span class="n">event_file</span><span class="p">);</span>
                <span class="k">return</span> <span class="n">STORAGE_ERR_PARTIAL_WRITE</span><span class="p">;</span>
        <span class="p">}</span>
        
        <span class="c1">// Write a newline character so that the next log starts on a new line:</span>
        <span class="k">static</span> <span class="k">const</span> <span class="kt">char</span> <span class="n">newline</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"</span><span class="se">\r\n</span><span class="s">"</span><span class="p">;</span>
        <span class="n">bytes_to_write</span> <span class="o">=</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">newline</span><span class="p">)</span> <span class="o">-</span><span class="mi">1u</span><span class="p">;</span> <span class="c1">// -1u means it will truncate the C string null character '\0' at the end</span>
        <span class="n">fr</span> <span class="o">=</span> <span class="n">f_write</span><span class="p">(</span><span class="o">&amp;</span><span class="n">event_file</span><span class="p">,</span>
                     <span class="n">newline</span><span class="p">,</span>
                     <span class="n">bytes_to_write</span><span class="p">,</span>
                     <span class="o">&amp;</span><span class="n">bytes_written</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">fr</span> <span class="o">!=</span> <span class="n">FR_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">f_close</span><span class="p">(</span><span class="o">&amp;</span><span class="n">event_file</span><span class="p">);</span>
                <span class="k">return</span> <span class="n">STORAGE_ERR_WRITE</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">bytes_to_write</span> <span class="o">!=</span> <span class="n">bytes_written</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">f_close</span><span class="p">(</span><span class="o">&amp;</span><span class="n">event_file</span><span class="p">);</span>
                <span class="k">return</span> <span class="n">STORAGE_ERR_PARTIAL_WRITE</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// Close event log:</span>
        <span class="n">fr</span> <span class="o">=</span> <span class="n">f_close</span><span class="p">(</span><span class="o">&amp;</span><span class="n">event_file</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">fr</span> <span class="o">!=</span> <span class="n">FR_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">STORAGE_ERR_CLOSE</span><span class="p">;</span> <span class="p">}</span>

        <span class="c1">// Success:</span>
        <span class="k">return</span> <span class="n">STORAGE_OK</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>As you can see creating and appending to files with FatFs is very simple! For occasional event logs, this is fine. The code opens the log file, appends the message, appends a newline, and closes the file again. That is simple and robust enough for low rate event logging. I would not use this exact pattern for streaming sensor data though. For that I would rather buffer samples in RAM and write larger chunks to the SD card.</p>

<p>Now into our main file we will have a go at using the API, and it is now blissfully simple to write to the log files:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"app/app_storage.h"</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>

        <span class="c1">// ...other initialisation code like clocks, SPI, etc...</span>

        <span class="n">storage_init</span><span class="p">();</span>
        <span class="n">storage_log_event</span><span class="p">(</span><span class="s">"An event that needs to be logged to EVENT.LOG!"</span><span class="p">);</span>

        <span class="c1">// rest of code..</span>
<span class="p">}</span>
</code></pre></div></div>

<p>That is it. Easy peasy. Now you can try adding things like a real time clock readout to the beginning of the message, but that can be done within the app_storage.c file now privately!</p>

<p>Lastly a little trick, that I realised whilst thinking about the status error code flow: later I can add a helper function to convert a <code class="language-plaintext highlighter-rouge">storage_status_t</code> value into a printable string:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="nf">storage_status_string</span><span class="p">(</span><span class="n">storage_status_t</span> <span class="n">status</span><span class="p">);</span>
</code></pre></div></div>

<p>Then I can print the status over UART or include it in a log:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">uart_write_string</span><span class="p">(</span><span class="n">storage_status_string</span><span class="p">(</span><span class="n">status</span><span class="p">));</span>
</code></pre></div></div>

<script src="https://utteranc.es/client.js" repo="skoopsy/skoopsy.github.io" issue-term="pathname" label="blog-embedded18" theme="preferred-color-scheme" crossorigin="anonymous" async="">
</script>

<p>Copyright © 2026 David O’Connor</p>]]></content><author><name>Dave</name></author><category term="STM32" /><summary type="html"><![CDATA[In this post I’m going to add my own storage API on top of the FatFs layer that was tested in the last post. This will give the firmware a simple way to record events to a normal file on the SD card, which is a small but important step towards a useful data logger.]]></summary></entry><entry><title type="html">STM32 #17: Using the FatFs API for Reading and Writing Files</title><link href="https://skoopsy.dev/stm32/2026/07/29/STM32-18-using-fatfs-diskio-for-reading-and-writing-microsd.html" rel="alternate" type="text/html" title="STM32 #17: Using the FatFs API for Reading and Writing Files" /><published>2026-07-29T10:00:00+00:00</published><updated>2026-07-29T10:00:00+00:00</updated><id>https://skoopsy.dev/stm32/2026/07/29/STM32-18-using-fatfs-diskio-for-reading-and-writing-microsd</id><content type="html" xml:base="https://skoopsy.dev/stm32/2026/07/29/STM32-18-using-fatfs-diskio-for-reading-and-writing-microsd.html"><![CDATA[<p>The <a href="https://skoopsy.dev/stm32/2026/07/27/STM32-17-fatfs-mai.html">last post</a> connected the sd_card.c driver we made to the FatFs Media Access Interface (MAI) at diskio.c. Previously we read, wrote, and traversed the SD card manually by reading raw hex and finding the corresponding metadata. Now we should be able to abstract things like sectors, clusters, FAT tables, boot sectors and SPI away and just think about creating, reading, and writing files.</p>

<p>A recap on where we are in the file system stack:</p>

<p><img src="/docs/assets/img/blog-14-architecture.jpg" alt="Storage architecture" /></p>

<p>It is worth clarifying that we are testing the FatFs API which uses the Media Access Interface developed previously. Hopefully this makes the boundary a little clearer:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"FatFs API"</span><span class="o">:</span> <span class="n">f_mount</span><span class="p">(),</span> <span class="n">f_open</span><span class="p">(),</span> <span class="n">f_read</span><span class="p">(),</span> <span class="n">f_write</span><span class="p">(),</span> <span class="n">f_close</span><span class="p">(),</span> <span class="p">...</span>

<span class="s">"Media Access Layer (MAI)"</span><span class="o">:</span> <span class="n">disk_initialize</span><span class="p">(),</span> <span class="n">disk_read</span><span class="p">(),</span> <span class="n">disk_write</span><span class="p">(),</span> <span class="p">...</span>
</code></pre></div></div>

<p>Here is a link to the ChaN site with the <a href="https://elm-chan.org/fsw/ff/doc/appnote.html">FatFs application note</a> which most of the post series is based on.</p>

<p>This post will give a brief description of the FatFs API and will test three things:</p>

<ol>
  <li>Mounting the filesystem</li>
  <li>Write a new file and contents to the card</li>
  <li>Read the written file contents and compare the result</li>
</ol>

<p>I would just put a few FatFss calls in main.c, see that they work and move on, but I wanted to make my testing more usable for later on, because storage bugs are the kind of thing I don’t want to discover later as I keep building my sensor logger. So I have presented testing the code as a self contain hardware integration test.</p>

<h1 id="fatfs-api">FatFs API</h1>

<p>The FatFs API covers File Access, Directory Access, File and Directory management, Volume management and System configuration. For details see the <a href="https://elm-chan.org/fsw/ff/">ChaN site</a>.</p>

<h1 id="mounting-the-file-system">Mounting the file system</h1>

<p>FatFs needs a work area known as a filesystem object for each logical drive. Prior to any read/write operations, a filesystem object needs to be registered with f_mount. Once f_unmount has unmounted the drive the work area can be discarded with free(fs). You can also reinitialise the filesystem after it has been mounted by calling f_mount again.</p>

<p>f_mount would typically be called once on system startup.</p>

<p>Here is the f_mount and f_unmount API:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">FRESULT</span> <span class="nf">f_mount</span> <span class="p">(</span>
  <span class="n">FATFS</span><span class="o">*</span>       <span class="n">fs</span><span class="p">,</span>    <span class="cm">/* [IN] Filesystem object */</span>
  <span class="k">const</span> <span class="n">TCHAR</span><span class="o">*</span> <span class="n">path</span><span class="p">,</span>  <span class="cm">/* [IN] Logical drive number */</span>
  <span class="n">BYTE</span>         <span class="n">opt</span>    <span class="cm">/* [IN] Initialization option */</span>
<span class="p">);</span>
</code></pre></div></div>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">FRESULT</span> <span class="nf">f_unmount</span> <span class="p">(</span>
  <span class="k">const</span> <span class="n">TCHAR</span><span class="o">*</span> <span class="n">path</span>   <span class="cm">/* [IN] Logical drive number */</span>
<span class="p">);</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">fs</code> is a pointer to the filesystem object to be registered or cleared. A null pointer will unregister the filesystem.</p>

<p><code class="language-plaintext highlighter-rouge">path</code> is a pointer to the null-terminated string that specifies the logical drive.</p>

<p><code class="language-plaintext highlighter-rouge">opt</code> is the mounting option:
        - 0: Do not mount now(to be mounted on first access of volume)
        - 1: Force mount the volume to check if it is ready to work.
             If fs is a NULL then this argument has no meaning.</p>

<p>return values: <code class="language-plaintext highlighter-rouge">FR_OK, FR_INVALID_DRIVE, FR_DISK_ERR, FR_NOT_READY, FR_NOT_ENABLED, FR_NO_FILESYSTEM</code></p>

<p>Find out more <a href="https://elm-chan.org/fsw/ff/doc/mount.html">here</a>.</p>

<h2 id="test-mounting-the-filesystem">Test: Mounting the filesystem</h2>

<p>For the following tests I’m going to create a sort of hardware integration test in <code class="language-plaintext highlighter-rouge">tests/hardware/test_fatfs.c</code> which I will manually call in main.c, this way the tests are separated enough that I can reuse them later with a small embedded test runner. If you are following this then don’t forget to add them to your Makefile!</p>

<p>For the hardware test directory I’ve made a header file called test_fatfs.h which contains the following:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#pragma once
</span>
<span class="cp">#include</span> <span class="cpf">&lt;stdbool.h&gt;</span><span class="cp">
</span>
<span class="n">bool</span> <span class="nf">test_fatfs_mount</span><span class="p">(</span><span class="kt">void</span><span class="p">);</span>
<span class="n">bool</span> <span class="nf">test_fatfs_write</span><span class="p">(</span><span class="kt">void</span><span class="p">);</span>
<span class="n">bool</span> <span class="nf">test_fatfs_read</span><span class="p">(</span><span class="kt">void</span><span class="p">);</span>
</code></pre></div></div>

<p>Let’s write a basic test for f_mount() which was covered briefly in the last post, as if this fails everything after it will fail, we are looking for a return value of <code class="language-plaintext highlighter-rouge">FR_OK</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"test_fatfs.h"</span><span class="cp">
</span>
<span class="cp">#include</span> <span class="cpf">"ff.h"</span><span class="cp">
#include</span> <span class="cpf">"bsp/bsp_uart.h"</span><span class="cp">
</span>
<span class="cp">#include</span> <span class="cpf">&lt;string.h&gt;</span><span class="cp">
</span>
<span class="k">static</span> <span class="n">FATFS</span> <span class="n">fs</span><span class="p">;</span> <span class="c1">//File system object</span>
<span class="cm">/* I am keeping the FATFS object as a file scope static inside test_fatfs.c. 
 * That keeps it private to this test module, but gives it program lifetime. 
 * FatFs stores a pointer to the filesystem work area after f_mount(), so I
 * do not want that object disappearing when a test function returns.
 */</span>

<span class="n">bool</span> <span class="nf">test_fatfs_mount</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>

        <span class="n">FRESULT</span> <span class="n">fr</span> <span class="o">=</span> <span class="n">f_mount</span><span class="p">(</span><span class="o">&amp;</span><span class="n">fs</span><span class="p">,</span>       <span class="c1">// Filesystem object</span>
                             <span class="s">"0:"</span><span class="p">,</span>      <span class="c1">// Filesystem path</span>
                             <span class="mi">1</span><span class="p">);</span>        <span class="c1">// Mount option - force mount immediately</span>

        <span class="c1">// Test fail</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">fr</span> <span class="o">!=</span> <span class="n">FR_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">uart_write_string</span><span class="p">(</span><span class="s">"test_fatfs: [FAIL] f_mount</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
                <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// Test pass</span>
        <span class="n">uart_write_string</span><span class="p">(</span><span class="s">"test_fatfs: [PASS] f_mount</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
        <span class="k">return</span> <span class="nb">true</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In main.c we can try out f_mount() - which we know works from the last post! At least it should, if it doesn’t have a go at reformatting the SD card on a different device, and then check your wiring! If it still doesn’t work go back through the posts to check out ways to monitor the SPI lines.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"tests/hardware/test_fatfs.h"</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
        <span class="cm">/*
        * all the board initialisation code here
        */</span>

        <span class="n">test_fatfs_mount</span><span class="p">();</span>

        <span class="k">while</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">{};</span> <span class="c1">// This allows us to monitor the UART via minicom without other code blitzing it</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="writing-a-file">Writing a file</h1>

<p>For writing a file we will need to open the file with f_open, write it with f_write, and close it with f_close.</p>

<p>f_open opens a file and creates a file object which becomes the identifier for subsequent read/write operations to the file. If the function succeeds the file object is valid, if not then the file object becomes invalid.</p>

<p>The file object should be closed with the f_close function after the session of the file access is finished.</p>

<p>For the full API see <a href="https://elm-chan.org/fsw/ff/doc/open.html">here</a></p>

<h2 id="the-f_open-api">The f_open() API:</h2>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">FRESULT</span> <span class="nf">f_open</span> <span class="p">(</span>
  <span class="n">FIL</span><span class="o">*</span> <span class="n">fp</span><span class="p">,</span>           <span class="cm">/* [OUT] Pointer to the file object structure */</span>
  <span class="k">const</span> <span class="n">TCHAR</span><span class="o">*</span> <span class="n">path</span><span class="p">,</span> <span class="cm">/* [IN] File name */</span>
  <span class="n">BYTE</span> <span class="n">mode</span>          <span class="cm">/* [IN] Mode flags */</span>
<span class="p">);</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">fp</code> is the pointer to the blank file object structure.</p>

<p><code class="language-plaintext highlighter-rouge">path</code> is a pointer to the null-terminated string that specifies the file name to open and create.</p>

<p><code class="language-plaintext highlighter-rouge">mode</code> are flags that specify the type of access and open method for the file, we will focus on flag FA_READ, FA_WRITE and FA_CREATE_ALWAYS, see the full list in the API docs.</p>

<p>There are a lot of return values, again we are looking for FR_OK in this test.</p>

<h2 id="the-f_write-api">The f_write() API:</h2>

<p>The write function starts to write data to the file at the file offset pointed by the read/write pointer of the file object. The r/w pointer advances with the number of bytes written.</p>

<p>f_write() API:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">FRESULT</span> <span class="nf">f_write</span> <span class="p">(</span>
  <span class="n">FIL</span><span class="o">*</span> <span class="n">fp</span><span class="p">,</span>          <span class="cm">/* [IN] Pointer to the file object structure */</span>
  <span class="k">const</span> <span class="kt">void</span><span class="o">*</span> <span class="n">buff</span><span class="p">,</span> <span class="cm">/* [IN] Pointer to the data to be written */</span>
  <span class="n">UINT</span> <span class="n">btw</span><span class="p">,</span>         <span class="cm">/* [IN] Number of bytes to write */</span>
  <span class="n">UINT</span><span class="o">*</span> <span class="n">bw</span>          <span class="cm">/* [OUT] Pointer to the variable to return number of bytes written */</span>
<span class="p">);</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">fp</code> is a pointer to the open file object structure.
<code class="language-plaintext highlighter-rouge">buff</code> is a pointer to the data to be written
<code class="language-plaintext highlighter-rouge">btw</code> specifies the number of bytes to write in range of UINT. If the data needs to be written fast it should be written in as large a chunk as possible (more on this in a future post)
<code class="language-plaintext highlighter-rouge">bw</code> is a pointer to the variable UINT type that receives the number of bytes that were written.</p>

<p>There are several return values, we are looking for FR_OK.</p>

<p>After the function succeeds, *bw should be checked to detect if the disk is full, in the case of *bw &lt; btw, it means the volume got full during the write operation.</p>

<h2 id="the-f_close-api">The f_close() API:</h2>

<p>The f_close function closes an open file object. If the file has been changed, the cached information of the file is written back to the volume.</p>

<p>The f_close API is below:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">FRESULT</span> <span class="nf">f_close</span> <span class="p">(</span>
  <span class="n">FIL</span><span class="o">*</span> <span class="n">fp</span>     <span class="cm">/* [IN] Pointer to the file object */</span>
<span class="p">);</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">fp</code> is the pointer to the open file object to be closed.</p>

<p>We are again looking for a return value of FR_OK.</p>

<p>For the full API see <a href="https://elm-chan.org/fsw/ff/doc/close.html">here</a></p>

<h2 id="test-writing-a-file">Test: Writing a file</h2>
<p>Now we have enough to actually write a file! Here is the test function I’ve added to test_fatfs.c file:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bool</span> <span class="nf">test_fatfs_write</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
        
        <span class="k">const</span> <span class="kt">char</span> <span class="n">MSG</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"TEST: FatFs Write</span><span class="se">\r\n</span><span class="s">"</span><span class="p">;</span>
        <span class="k">const</span> <span class="kt">char</span> <span class="n">FPATH</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"0:/TESTW.TXT"</span><span class="p">;</span>
        <span class="n">FIL</span> <span class="n">file</span><span class="p">;</span>                                <span class="c1">// File object</span>
        <span class="n">FRESULT</span> <span class="n">fr</span><span class="p">;</span>                              <span class="c1">// Return code / status</span>
        <span class="n">UINT</span> <span class="n">bytes_written</span><span class="p">;</span>                      <span class="c1">// Write count</span>
        <span class="n">UINT</span> <span class="n">bytes_to_write</span> <span class="o">=</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">MSG</span><span class="p">)</span> <span class="o">-</span><span class="mi">1u</span><span class="p">;</span>   <span class="c1">// To write count</span>

        <span class="c1">// Mount - Only needed if hasn't been mounted earlier but also won't harm anything if has, unless earlier files were not closed with f_close.                   </span>
        <span class="n">fr</span> <span class="o">=</span> <span class="n">f_mount</span><span class="p">(</span><span class="o">&amp;</span><span class="n">fs</span><span class="p">,</span>       <span class="c1">// Filesystem object address</span>
                     <span class="s">"0:"</span><span class="p">,</span>      <span class="c1">// Filesystem path</span>
                     <span class="mi">1</span><span class="p">);</span>        <span class="c1">// Option mount now</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">fr</span> <span class="o">!=</span> <span class="n">FR_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">uart_write_string</span><span class="p">(</span><span class="s">"test_fatfs: [FAIL] Write - Mount</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
                <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
        <span class="p">}</span>                    

        <span class="c1">// Open</span>
        <span class="n">fr</span> <span class="o">=</span> <span class="n">f_open</span><span class="p">(</span><span class="o">&amp;</span><span class="n">file</span><span class="p">,</span>
                    <span class="n">FPATH</span><span class="p">,</span>                            <span class="n">FA_WRITE</span> <span class="o">|</span> <span class="n">FA_CREATE_ALWAYS</span><span class="p">);</span> <span class="c1">// Mode: write and create new file</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">fr</span> <span class="o">!=</span> <span class="n">FR_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">uart_write_string</span><span class="p">(</span><span class="s">"test_fatfs: [FAIL] Write - Open</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
                <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// Write</span>
        <span class="n">fr</span> <span class="o">=</span> <span class="n">f_write</span><span class="p">(</span><span class="o">&amp;</span><span class="n">file</span><span class="p">,</span>
                     <span class="n">MSG</span><span class="p">,</span>               <span class="c1">// Buffer to write</span>
                     <span class="n">bytes_to_write</span><span class="p">,</span>    <span class="c1">// Number of bytes to write</span>
                     <span class="o">&amp;</span><span class="n">bytes_written</span><span class="p">);</span>   <span class="c1">// Number of bytes written</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">fr</span> <span class="o">!=</span> <span class="n">FR_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">f_close</span><span class="p">(</span><span class="o">&amp;</span><span class="n">file</span><span class="p">);</span>
                <span class="n">uart_write_string</span><span class="p">(</span><span class="s">"test_fatfs: [FAIL] Write - Write</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
                <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">bytes_written</span> <span class="o">!=</span> <span class="n">bytes_to_write</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">f_close</span><span class="p">(</span><span class="o">&amp;</span><span class="n">file</span><span class="p">);</span>
                <span class="n">uart_write_string</span><span class="p">(</span><span class="s">"test_fatfs: [FAIL] Write - Partial write</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
                <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
        <span class="p">}</span>
                     
        <span class="c1">// Close</span>
        <span class="n">fr</span> <span class="o">=</span> <span class="n">f_close</span><span class="p">(</span><span class="o">&amp;</span><span class="n">file</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">fr</span> <span class="o">!=</span> <span class="n">FR_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">uart_write_string</span><span class="p">(</span><span class="s">"test_fatfs: [FAIL] Write - Close</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
                <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// Test pass</span>
        <span class="n">uart_write_string</span><span class="p">(</span><span class="s">"test_fatfs: [PASS] Write</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
        <span class="k">return</span> <span class="nb">true</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Have a go at calling this in main, hopefully it passes!</p>

<h1 id="reading-a-file">Reading a file:</h1>

<p>The f_read() function starts to read data from the file at the file offset pointed by the read/write pointer of the file object. The read/write pointer advances as number of bytes read. After the function has succeeded, *br should be checked to detect end of the file.</p>

<p>For more details check the API <a href="https://elm-chan.org/fsw/ff/doc/read.html">here</a></p>

<h2 id="the-f_read-api">The f_read() API:</h2>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">FRESULT</span> <span class="nf">f_read</span> <span class="p">(</span>
  <span class="n">FIL</span><span class="o">*</span> <span class="n">fp</span><span class="p">,</span>     <span class="cm">/* [IN] File object */</span>
  <span class="kt">void</span><span class="o">*</span> <span class="n">buff</span><span class="p">,</span>  <span class="cm">/* [OUT] Buffer to store read data */</span>
  <span class="n">UINT</span> <span class="n">btr</span><span class="p">,</span>    <span class="cm">/* [IN] Number of bytes to read */</span>
  <span class="n">UINT</span><span class="o">*</span> <span class="n">br</span>     <span class="cm">/* [OUT] Number of bytes read */</span>
<span class="p">);</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">fp</code> is a pointer to the open file object structure. If a null pointer is given, the function fails with FR_INVALID_OBJECT.</p>

<p><code class="language-plaintext highlighter-rouge">buff</code> is a pointer to the buffer to store the read data.</p>

<p><code class="language-plaintext highlighter-rouge">btr</code> is the number of bytes to read in range of UINT type. If the file needs to be read fast, it should be read in as large a chunk as possible.</p>

<p><code class="language-plaintext highlighter-rouge">br</code> is a pointer to the variable in UINT type that receives number of bytes read. If the return value is equal to btr, the function return code should be FR_OK.</p>

<p>Return values: <code class="language-plaintext highlighter-rouge">FR_OK, FR_DISK_ERR, FR_INT_ERR, FR_DENIED, FR_INVALID_OBJECT, FR_TIMEOUT</code></p>

<h2 id="test-reading-a-file">Test: Reading a file</h2>

<p>Now back to test_fatfs.c to create the file read test:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bool</span> <span class="nf">test_fatfs_read</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">const</span> <span class="kt">char</span> <span class="n">MSG</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"TEST: FatFs Write</span><span class="se">\r\n</span><span class="s">"</span><span class="p">;</span>
        <span class="k">const</span> <span class="kt">char</span> <span class="n">FPATH</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"0:/TESTW.txt"</span><span class="p">;</span>
        <span class="n">FIL</span> <span class="n">file</span><span class="p">;</span>
        <span class="n">FRESULT</span> <span class="n">fr</span><span class="p">;</span>
        <span class="n">UINT</span> <span class="n">bytes_read</span> <span class="o">=</span> <span class="mi">0u</span><span class="p">;</span>
        <span class="kt">char</span> <span class="n">buffer</span><span class="p">[</span><span class="mi">64</span><span class="p">];</span>

        <span class="c1">// Mount</span>
        <span class="n">fr</span> <span class="o">=</span> <span class="n">f_mount</span><span class="p">(</span><span class="o">&amp;</span><span class="n">fs</span><span class="p">,</span>
                     <span class="s">"0:"</span><span class="p">,</span>
                     <span class="mi">1</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">fr</span> <span class="o">!=</span> <span class="n">FR_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">uart_write_string</span><span class="p">(</span><span class="s">"test_fatfs: [FAIL] Read - Mount</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
                <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// Open</span>
        <span class="n">fr</span> <span class="o">=</span> <span class="n">f_open</span><span class="p">(</span><span class="o">&amp;</span><span class="n">file</span><span class="p">,</span>              <span class="c1">// file object</span>
                    <span class="s">"0:/TESTW.TXT"</span><span class="p">,</span>     <span class="c1">// file path</span>
                    <span class="n">FA_READ</span><span class="p">);</span>           <span class="c1">// Read only flag</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">fr</span> <span class="o">!=</span> <span class="n">FR_OK</span><span class="p">){</span>
                <span class="n">uart_write_string</span><span class="p">(</span><span class="s">"test_fatfs: [FAIL] Read - Open TESTW.TXT</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
                <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// Read</span>
        <span class="n">fr</span> <span class="o">=</span> <span class="n">f_read</span><span class="p">(</span><span class="o">&amp;</span><span class="n">file</span><span class="p">,</span>              <span class="c1">// file object</span>
                    <span class="n">buffer</span><span class="p">,</span>             <span class="c1">// buffer to store read data</span>
                    <span class="k">sizeof</span><span class="p">(</span><span class="n">buffer</span><span class="p">)</span> <span class="o">-</span><span class="mi">1u</span><span class="p">,</span> <span class="c1">// Number of bytes to read, leave 1 byte for null terminator</span>
                    <span class="o">&amp;</span><span class="n">bytes_read</span><span class="p">);</span>       <span class="c1">// Number of bytes actually read</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">fr</span> <span class="o">!=</span> <span class="n">FR_OK</span><span class="p">){</span>
                <span class="n">uart_write_string</span><span class="p">(</span><span class="s">"test_fatfs: [FAIL] Read - Read TESTW.TXT</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
                <span class="n">f_close</span><span class="p">(</span><span class="o">&amp;</span><span class="n">file</span><span class="p">);</span>
                <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
        <span class="p">}</span>
        
        <span class="c1">// Terminate buffer with null character to guarantee valid c string</span>
        <span class="n">buffer</span><span class="p">[</span><span class="n">bytes_read</span><span class="p">]</span> <span class="o">=</span> <span class="sc">'\0'</span><span class="p">;</span>

        <span class="c1">// Close</span>
        <span class="n">fr</span> <span class="o">=</span> <span class="n">f_close</span><span class="p">(</span><span class="o">&amp;</span><span class="n">file</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">fr</span> <span class="o">!=</span> <span class="n">FR_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">uart_write_string</span><span class="p">(</span><span class="s">"test_fatfs: [FAIL] Read - Close TESTW.TXT</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
                <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="n">uart_write_string</span><span class="p">(</span><span class="s">"test_fatfs: TESTW.TXT contents:</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
        <span class="n">uart_write_string</span><span class="p">(</span><span class="n">buffer</span><span class="p">);</span>
        <span class="n">uart_write_string</span><span class="p">(</span><span class="s">"</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
        
        <span class="c1">// Test pass</span>
        <span class="n">uart_write_string</span><span class="p">(</span><span class="s">"test_fatfs: [PASS] Read</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
        <span class="k">return</span> <span class="nb">true</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We can go a step further and check that both the read buffer and original MSG are the same using <code class="language-plaintext highlighter-rouge">memcmp</code> from the standard library. <code class="language-plaintext highlighter-rouge">memcmp</code> compares the first n characters from two buffers, zero is returned if identical. I’ve added this to <code class="language-plaintext highlighter-rouge">test_fatfs_read()</code> above the UART file contents printout:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;string.h&gt;</span><span class="c1"> // Needed for memcmp</span><span class="cp">
</span>
<span class="p">...</span>
        <span class="c1">// Identical buffer check - Being a little lazy with sizeof</span>
        <span class="k">if</span> <span class="p">((</span><span class="n">bytes_read</span> <span class="o">!=</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">MSG</span><span class="p">)</span> <span class="o">-</span><span class="mi">1u</span><span class="p">)</span> <span class="o">||</span>
            <span class="p">(</span><span class="n">memcmp</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="n">MSG</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">MSG</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1u</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">))</span> <span class="p">{</span>
                <span class="n">uart_write_string</span><span class="p">(</span><span class="s">"test_fatfs: [FAIL] Read - buffer != MSG</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
                <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
        <span class="p">}</span>

<span class="p">...</span>
</code></pre></div></div>

<p>Now just call both test_fatfs_write() and test_fatfs_read() in main.c and monitor the UART for a pass/fail and a readout of the file contents!</p>

<script src="https://utteranc.es/client.js" repo="skoopsy/skoopsy.github.io" issue-term="pathname" label="blog-embedded17" theme="preferred-color-scheme" crossorigin="anonymous" async="">
</script>

<p>Copyright © 2026 David O’Connor</p>]]></content><author><name>Dave</name></author><category term="STM32" /><summary type="html"><![CDATA[The last post connected the sd_card.c driver we made to the FatFs Media Access Interface (MAI) at diskio.c. Previously we read, wrote, and traversed the SD card manually by reading raw hex and finding the corresponding metadata. Now we should be able to abstract things like sectors, clusters, FAT tables, boot sectors and SPI away and just think about creating, reading, and writing files.]]></summary></entry><entry><title type="html">STM32 #16: Connecting the SD card driver to the Media Access Interface for FatFs</title><link href="https://skoopsy.dev/stm32/2026/07/27/STM32-17-fatfs-mai.html" rel="alternate" type="text/html" title="STM32 #16: Connecting the SD card driver to the Media Access Interface for FatFs" /><published>2026-07-27T16:00:00+00:00</published><updated>2026-07-27T16:00:00+00:00</updated><id>https://skoopsy.dev/stm32/2026/07/27/STM32-17-fatfs-mai</id><content type="html" xml:base="https://skoopsy.dev/stm32/2026/07/27/STM32-17-fatfs-mai.html"><![CDATA[<p>The last few posts have covered developing a SPI and SD card driver in preparation to interface with the FatFs library developed by <a href="https://elm-chan.org/fsw/ff/">ChaN</a>. I don’t want my application code to be manually decoding MBRs, FAT32 boot sector, root directory entries etc. The stack I’m building is as follows:</p>

<p><img src="/docs/assets/img/blog-14-architecture.jpg" alt="Storage architecture" /></p>

<p>Here I will focus on the Media Access Interface layer.</p>

<p>The SD card driver now provides two basic operations that FatFs needs:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sd_status_t sd_card_read_block(uint32_t block_index, 
                                     uint8_t *buffer);
sd_status_t sd_card_write_block(uint32_t block_index, 
                                      const uint8_t *buffer);
</code></pre></div></div>

<p>(Note: I simplified the function names by removing the CMDxx)</p>

<p>FatFs doesn’t know about SD card protocol like CMD17 and CMD24, SPI, or R1 responses, and busy waits. It expects a small platform specific disk I/O layer that can initialise the drive, read sectors, write sectors, and answer a few control queries.</p>

<h1 id="what-fatfs-needs">What FatFs needs</h1>

<p>FatFs is written to be portable, it doesn’t know how to talk to hardware. The hardware specific part is handled by the provided <code class="language-plaintext highlighter-rouge">diskio.c</code>. The key functions are:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">DSTATUS</span> <span class="nf">disk_status</span><span class="p">(</span><span class="n">BYTE</span> <span class="n">pdrv</span><span class="p">);</span> <span class="c1">// Report whether the disk is ready</span>
<span class="n">DSTATUS</span> <span class="nf">disk_initialize</span><span class="p">(</span><span class="n">BYTE</span> <span class="n">pdrv</span><span class="p">);</span> <span class="c1">// Initialise the physical drive</span>

<span class="n">DRESULT</span> <span class="nf">disk_read</span><span class="p">(</span><span class="n">BYTE</span> <span class="n">pdrv</span><span class="p">,</span> <span class="n">BYTE</span> <span class="o">*</span><span class="n">buff</span><span class="p">,</span> <span class="n">LBA_t</span> <span class="n">sector</span><span class="p">,</span> <span class="n">UINT</span> <span class="n">count</span><span class="p">);</span> <span class="c1">// Read one or more sectors</span>
<span class="n">DRESULT</span> <span class="nf">disk_write</span><span class="p">(</span><span class="n">BYTE</span> <span class="n">pdrv</span><span class="p">,</span> <span class="k">const</span> <span class="n">BYTE</span> <span class="o">*</span><span class="n">buff</span><span class="p">,</span> <span class="n">LBA_t</span> <span class="n">sector</span><span class="p">,</span> <span class="n">UINT</span> <span class="n">count</span><span class="p">);</span> <span class="c1">// Write one or more sectors</span>
<span class="n">DRESULT</span> <span class="nf">disk_ioctl</span><span class="p">(</span><span class="n">BYTE</span> <span class="n">pdrv</span><span class="p">,</span> <span class="n">BYTE</span> <span class="n">cmd</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">buff</span><span class="p">);</span> <span class="c1">// Handle control queries like sector size and sync.</span>
<span class="n">DWORD</span> <span class="nf">get_fattime</span><span class="p">(</span><span class="kt">void</span><span class="p">);</span> <span class="c1">// get the current time</span>
</code></pre></div></div>

<p>The translation from FatFs looks roughly like this for reads:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FatFs asks: "read sector 1234"
disk_read()
sd_card_read_block(1234, buffer)
CMD17 over SPI
</code></pre></div></div>

<p>and for writes:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FatFs asks: "write sector 1234"
disk_write()
sd_card_write_block(1234, buffer)
CMD24 over SPI
</code></pre></div></div>

<h1 id="adding-fatfs-to-the-project">Adding FatFs to the project</h1>

<p>You can download the FatFs library from the official <a href="https://elm-chan.org/fsw/ff/">ChaN FatFs site</a>. It comes with the core filesystem code, headers, configuration file, and the Media Access Interface definitions.</p>

<p>I will lay out FatFs like this in my project directories:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ext_libraries/ 
    fatfs/ 
        source/ 
            ff.c 
            ff.h 
            diskio.h 
            ffconf.h 
            ffsystem.c 
            ffunicode.c 
src/ 
    middleware/
        fatfs/
        diskio.c
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ext_libraries/fatfs/</code> will house the main FatFs files downloaded.</li>
  <li><code class="language-plaintext highlighter-rouge">ffconf.h</code> configures FatFs for this project.</li>
  <li><code class="language-plaintext highlighter-rouge">src/middleware/fatfs/diskio.c</code> Is my implementation of the Media Access Interface and is the file we will be writing in this post, the FatFs source directory contains a template file we can copy over to our src to start from which I will be using.</li>
</ul>

<p>Lastly remember to add the FatFs path to the includes in your makefile:</p>

<pre><code class="language-Makefile">CFILES += ext_libraries/fatfs/source/ff.c
CFILES += src/middleware/fatfs/diskio.c

INCLUDES += -Iext_libraries/fatfs/source
</code></pre>

<p>Depending on the ffconf.h options, ffsystem.c and ffunicode.c may also be needed later. For this first pass, I am keeping the configuration simple and not including them.</p>

<h1 id="modifying-the-diskioc-template">Modifying the diskio.c template</h1>

<p>FatFs provides a template Media Access Interface file: <code class="language-plaintext highlighter-rouge">diskio.c</code>. Make sure you copy this from the FatFs source directory into the project src/middleware/fatfs directory and edit that one, as that is what will be compiled.</p>

<p>The FatFs template includes example mappings for things like RAM, MMC, and USB drives. For now I do not need that, because I only have one physical drive: the microSD card. So we can simplify the template quite a bit.</p>

<p>The FatFs template starts with these includes:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"ff.h"</span><span class="c1">         /* Basic definitions of FatFs */</span><span class="cp">
#include</span> <span class="cpf">"diskio.h"</span><span class="c1">     /* Declarations FatFs MAI */</span><span class="cp">
</span>
<span class="cm">/* Example: Declarations of the platform and disk functions in the project */</span>
<span class="cp">#include</span> <span class="cpf">"platform.h"</span><span class="cp">
#include</span> <span class="cpf">"storage.h"</span><span class="cp">
</span></code></pre></div></div>
<p>We keep the first two as they are real FatFs headers we need. We will replace the second two platform and storage with our SD card driver:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"drivers/sd_card.h"</span><span class="cp">
</span></code></pre></div></div>

<p>Now diskio.c can see the FatFs types and the SD card functions we wrote. The template also contains example drive mappings:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define DEV_FLASH 0 </span><span class="cm">/* Map FTL to physical drive 0 */</span><span class="cp"> 
#define DEV_MMC 1 </span><span class="cm">/* Map MMC/SD card to physical drive 1 */</span><span class="cp"> 
#define DEV_USB 2 </span><span class="cm">/* Map USB MSD to physical drive 2 */</span><span class="cp">
</span></code></pre></div></div>

<p>We don’t need these because there is only one drive, we can delete these and replace them with:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define SD_DRIVE_ID 0u
#define SD_SECTOR_SIZE 512u
</span></code></pre></div></div>

<p>The template will use switch (pdrv) blocks to choose between RAM, MMC, and USB examples. For the first implementation, that is unnecessary. If <code class="language-plaintext highlighter-rouge">pdrv</code> = 0, we use the SD card, if anything else, it will return an error.</p>

<p>We will also add a status flag in case sd_card_init() fails:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">DSTATUS</span> <span class="n">disk_status_flags</span> <span class="o">=</span> <span class="n">STA_NOINIT</span><span class="p">;</span>
</code></pre></div></div>

<h1 id="disk_status">disk_status()</h1>

<p>The template looks like this:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">DSTATUS</span> <span class="nf">disk_status</span> <span class="p">(</span>
	<span class="n">BYTE</span> <span class="n">pdrv</span>		<span class="cm">/* Physical drive number to identify the drive */</span>
<span class="p">)</span>
<span class="p">{</span>
	<span class="n">DSTATUS</span> <span class="n">stat</span><span class="p">;</span>
	<span class="kt">int</span> <span class="n">result</span><span class="p">;</span>

	<span class="k">switch</span> <span class="p">(</span><span class="n">pdrv</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">case</span> <span class="n">DEV_RAM</span> <span class="p">:</span>
		<span class="n">result</span> <span class="o">=</span> <span class="n">RAM_disk_status</span><span class="p">();</span>

		<span class="c1">// translate the result code here</span>

		<span class="k">return</span> <span class="n">stat</span><span class="p">;</span>

	<span class="k">case</span> <span class="n">DEV_MMC</span> <span class="p">:</span>
		<span class="n">result</span> <span class="o">=</span> <span class="n">MMC_disk_status</span><span class="p">();</span>

		<span class="c1">// translate the result code here</span>

		<span class="k">return</span> <span class="n">stat</span><span class="p">;</span>

	<span class="k">case</span> <span class="n">DEV_USB</span> <span class="p">:</span>
		<span class="n">result</span> <span class="o">=</span> <span class="n">USB_disk_status</span><span class="p">();</span>

		<span class="c1">// translate the result code here</span>

		<span class="k">return</span> <span class="n">stat</span><span class="p">;</span>
	<span class="p">}</span>
	<span class="k">return</span> <span class="n">STA_NOINIT</span><span class="p">;</span>
<span class="p">}</span>

</code></pre></div></div>

<p>We don’t have RAM_disk_status(), MMC_disk_status(), or USB_disk_status(). For our project we will replace the whole switch with a single drive check:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">DSTATUS</span> <span class="nf">disk_status</span><span class="p">(</span><span class="n">BYTE</span> <span class="n">pdrv</span><span class="p">)</span> <span class="p">{</span> 
        <span class="k">if</span> <span class="p">(</span><span class="n">pdrv</span> <span class="o">!=</span> <span class="n">SD_DRIVE_ID</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">STA_NOINIT</span><span class="p">;</span> <span class="p">}</span> 
        <span class="k">return</span> <span class="n">disk_status_flags</span><span class="p">;</span> 
<span class="p">}</span>
</code></pre></div></div>
<p>Later we might add card removal detection.</p>

<h1 id="disk_initialize">disk_initialize()</h1>

<p>The template uses the same pattern for multiple types of drives:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">DSTATUS</span> <span class="nf">disk_initialize</span> <span class="p">(</span>
	<span class="n">BYTE</span> <span class="n">pdrv</span>				<span class="cm">/* Physical drive nmuber to identify the drive */</span>
<span class="p">)</span>
<span class="p">{</span>
	<span class="n">DSTATUS</span> <span class="n">stat</span><span class="p">;</span>
	<span class="kt">int</span> <span class="n">result</span><span class="p">;</span>

	<span class="k">switch</span> <span class="p">(</span><span class="n">pdrv</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">case</span> <span class="n">DEV_RAM</span> <span class="p">:</span>
		<span class="n">result</span> <span class="o">=</span> <span class="n">RAM_disk_initialize</span><span class="p">();</span>

		<span class="c1">// translate the reslut code here</span>

		<span class="k">return</span> <span class="n">stat</span><span class="p">;</span>

	<span class="k">case</span> <span class="n">DEV_MMC</span> <span class="p">:</span>
		<span class="n">result</span> <span class="o">=</span> <span class="n">MMC_disk_initialize</span><span class="p">();</span>

		<span class="c1">// translate the reslut code here</span>

		<span class="k">return</span> <span class="n">stat</span><span class="p">;</span>

	<span class="k">case</span> <span class="n">DEV_USB</span> <span class="p">:</span>
		<span class="n">result</span> <span class="o">=</span> <span class="n">USB_disk_initialize</span><span class="p">();</span>

		<span class="c1">// translate the reslut code here</span>

		<span class="k">return</span> <span class="n">stat</span><span class="p">;</span>
	<span class="p">}</span>
	<span class="k">return</span> <span class="n">STA_NOINIT</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Again, we will strip these placeholders out and replace with the sd_card_init(void) function we already developed, we only have to translate the STA_NOINIT flag correctly:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">DSTATUS</span> <span class="nf">disk_initialize</span><span class="p">(</span><span class="n">BYTE</span> <span class="n">pdrv</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">pdrv</span> <span class="o">!=</span> <span class="n">SD_DRIVE_ID</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">STA_NOINIT</span><span class="p">;</span> <span class="p">}</span>

        <span class="n">sd_status_t</span> <span class="n">status</span> <span class="o">=</span> <span class="n">sd_card_init</span><span class="p">();</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">disk_status_flags</span> <span class="o">=</span> <span class="n">STA_NOINIT</span><span class="p">;</span>
                <span class="k">return</span> <span class="n">disk_status_flags</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="n">disk_status_flags</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="k">return</span> <span class="n">disk_status_flags</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="disk_read">disk_read()</h1>

<p>The template provides this function:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">DRESULT</span> <span class="nf">disk_read</span> <span class="p">(</span>
	<span class="n">BYTE</span> <span class="n">pdrv</span><span class="p">,</span>		<span class="cm">/* Physical drive nmuber to identify the drive */</span>
	<span class="n">BYTE</span> <span class="o">*</span><span class="n">buff</span><span class="p">,</span>		<span class="cm">/* Data buffer to store read data */</span>
	<span class="n">LBA_t</span> <span class="n">sector</span><span class="p">,</span>	<span class="cm">/* Start sector in LBA */</span>
	<span class="n">UINT</span> <span class="n">count</span>		<span class="cm">/* Number of sectors to read */</span>
<span class="p">)</span>
<span class="p">{</span>
	<span class="n">DRESULT</span> <span class="n">res</span><span class="p">;</span>
	<span class="kt">int</span> <span class="n">result</span><span class="p">;</span>

	<span class="k">switch</span> <span class="p">(</span><span class="n">pdrv</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">case</span> <span class="n">DEV_RAM</span> <span class="p">:</span>
		<span class="c1">// translate the arguments here</span>

		<span class="n">result</span> <span class="o">=</span> <span class="n">RAM_disk_read</span><span class="p">(</span><span class="n">buff</span><span class="p">,</span> <span class="n">sector</span><span class="p">,</span> <span class="n">count</span><span class="p">);</span>

		<span class="c1">// translate the reslut code here</span>

		<span class="k">return</span> <span class="n">res</span><span class="p">;</span>

	<span class="k">case</span> <span class="n">DEV_MMC</span> <span class="p">:</span>
		<span class="c1">// translate the arguments here</span>

		<span class="n">result</span> <span class="o">=</span> <span class="n">MMC_disk_read</span><span class="p">(</span><span class="n">buff</span><span class="p">,</span> <span class="n">sector</span><span class="p">,</span> <span class="n">count</span><span class="p">);</span>

		<span class="c1">// translate the reslut code here</span>

		<span class="k">return</span> <span class="n">res</span><span class="p">;</span>

	<span class="k">case</span> <span class="n">DEV_USB</span> <span class="p">:</span>
		<span class="c1">// translate the arguments here</span>

		<span class="n">result</span> <span class="o">=</span> <span class="n">USB_disk_read</span><span class="p">(</span><span class="n">buff</span><span class="p">,</span> <span class="n">sector</span><span class="p">,</span> <span class="n">count</span><span class="p">);</span>

		<span class="c1">// translate the reslut code here</span>

		<span class="k">return</span> <span class="n">res</span><span class="p">;</span>
	<span class="p">}</span>

	<span class="k">return</span> <span class="n">RES_PARERR</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Again the example functions here aren’t great for our project so we’ll strip them out. FatFs asks for one or more sectors, but our SD card driver reads one 512 byte block at a time. So we will replace the placeholder drive switch with a loop over count. E.g. When FatFs requests sector 1234, count 3, the Media Access Interface (MAI) calls:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sd_card_read_block</span><span class="p">(</span><span class="mi">1234</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">buff</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> 
<span class="n">sd_card_read_block</span><span class="p">(</span><span class="mi">1235</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">buff</span><span class="p">[</span><span class="mi">512</span><span class="p">])</span> 
<span class="n">sd_card_read_block</span><span class="p">(</span><span class="mi">1236</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">buff</span><span class="p">[</span><span class="mi">1024</span><span class="p">])</span>
</code></pre></div></div>

<p>Here is the new read function:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">DRESULT</span> <span class="nf">disk_read</span><span class="p">(</span><span class="n">BYTE</span> <span class="n">pdrv</span><span class="p">,</span> <span class="c1">// Physical drive number</span>
                  <span class="n">BYTE</span> <span class="o">*</span><span class="n">buff</span><span class="p">,</span> <span class="c1">// Buffer to store data read</span>
                  <span class="n">LBA_t</span> <span class="n">sector</span><span class="p">,</span> <span class="c1">// Start sector in LBA</span>
                  <span class="n">UINT</span> <span class="n">count</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// Number of sectors to read</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">pdrv</span> <span class="o">!=</span> <span class="n">SD_DRIVE_ID</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">RES_PARERR</span><span class="p">;</span> <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">buff</span> <span class="o">==</span> <span class="nb">NULL</span> <span class="o">||</span> <span class="n">count</span> <span class="o">==</span> <span class="mi">0u</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">RES_PARERR</span><span class="p">;</span> <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">disk_status_flags</span> <span class="o">&amp;</span> <span class="n">STA_NOINIT</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">RES_NOTRDY</span><span class="p">;</span> <span class="p">}</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">sector</span> <span class="o">&gt;</span> <span class="p">(</span><span class="n">LBA_t</span><span class="p">)</span><span class="n">UINT32_MAX</span> <span class="o">||</span>
            <span class="p">(</span><span class="n">LBA_t</span><span class="p">)(</span><span class="n">count</span> <span class="o">-</span> <span class="mi">1u</span><span class="p">)</span> <span class="o">&gt;</span> <span class="p">(</span><span class="n">LBA_t</span><span class="p">)</span><span class="n">UINT32_MAX</span> <span class="o">-</span> <span class="n">sector</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">return</span> <span class="n">RES_PARERR</span><span class="p">;</span>
        <span class="p">}</span> <span class="c1">// Ensure the requested sector range fits the driver's 32 bit block address</span>

        <span class="k">for</span> <span class="p">(</span><span class="n">UINT</span> <span class="n">i</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span> <span class="n">i</span><span class="o">&lt;</span><span class="n">count</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">sd_status_t</span> <span class="n">status</span> <span class="o">=</span> <span class="n">sd_card_read_block</span><span class="p">((</span><span class="kt">uint32_t</span><span class="p">)(</span><span class="n">sector</span><span class="o">+</span><span class="n">i</span><span class="p">),</span>
                                                        <span class="o">&amp;</span><span class="n">buff</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">SD_SECTOR_SIZE</span><span class="p">]);</span>

                <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">RES_ERROR</span><span class="p">;</span> <span class="p">}</span>
        <span class="p">}</span>

        <span class="k">return</span> <span class="n">RES_OK</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This implementation supports FatFs multi-sector reads by breaking them up into individual CMD17 operations, in the future I would like to implement the CMD18 multi-block reads which will be more efficient.</p>

<h1 id="disk_write">disk_write()</h1>

<p>The template provides this:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#if FF_FS_READONLY == 0
</span>
<span class="n">DRESULT</span> <span class="nf">disk_write</span> <span class="p">(</span>
	<span class="n">BYTE</span> <span class="n">pdrv</span><span class="p">,</span>	      <span class="cm">/* Physical drive nmuber to identify the drive */</span>
	<span class="k">const</span> <span class="n">BYTE</span> <span class="o">*</span><span class="n">buff</span><span class="p">,</span>     <span class="cm">/* Data to be written */</span>
	<span class="n">LBA_t</span> <span class="n">sector</span><span class="p">,</span>	      <span class="cm">/* Start sector in LBA */</span>
	<span class="n">UINT</span> <span class="n">count</span>	      <span class="cm">/* Number of sectors to write */</span>
<span class="p">)</span>
<span class="p">{</span>
	<span class="n">DRESULT</span> <span class="n">res</span><span class="p">;</span>
	<span class="kt">int</span> <span class="n">result</span><span class="p">;</span>

	<span class="k">switch</span> <span class="p">(</span><span class="n">pdrv</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">case</span> <span class="n">DEV_RAM</span> <span class="p">:</span>
		<span class="c1">// translate the arguments here</span>

		<span class="n">result</span> <span class="o">=</span> <span class="n">RAM_disk_write</span><span class="p">(</span><span class="n">buff</span><span class="p">,</span> <span class="n">sector</span><span class="p">,</span> <span class="n">count</span><span class="p">);</span>

		<span class="c1">// translate the reslut code here</span>

		<span class="k">return</span> <span class="n">res</span><span class="p">;</span>

	<span class="k">case</span> <span class="n">DEV_MMC</span> <span class="p">:</span>
		<span class="c1">// translate the arguments here</span>

		<span class="n">result</span> <span class="o">=</span> <span class="n">MMC_disk_write</span><span class="p">(</span><span class="n">buff</span><span class="p">,</span> <span class="n">sector</span><span class="p">,</span> <span class="n">count</span><span class="p">);</span>

		<span class="c1">// translate the reslut code here</span>

		<span class="k">return</span> <span class="n">res</span><span class="p">;</span>

	<span class="k">case</span> <span class="n">DEV_USB</span> <span class="p">:</span>
		<span class="c1">// translate the arguments here</span>

		<span class="n">result</span> <span class="o">=</span> <span class="n">USB_disk_write</span><span class="p">(</span><span class="n">buff</span><span class="p">,</span> <span class="n">sector</span><span class="p">,</span> <span class="n">count</span><span class="p">);</span>

		<span class="c1">// translate the reslut code here</span>

		<span class="k">return</span> <span class="n">res</span><span class="p">;</span>
	<span class="p">}</span>

	<span class="k">return</span> <span class="n">RES_PARERR</span><span class="p">;</span>
<span class="p">}</span>

<span class="cp">#endif
</span></code></pre></div></div>

<p>Here we will map the FatFs sector writes onto repeated CMD24 block writes, similarly to the read function:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#if FF_FS_READONLY == 0
</span>
<span class="n">DRESULT</span> <span class="nf">disk_write</span><span class="p">(</span><span class="n">BYTE</span> <span class="n">pdrv</span><span class="p">,</span>        <span class="c1">// Physical drive number</span>
                   <span class="k">const</span> <span class="n">BYTE</span> <span class="o">*</span><span class="n">buff</span><span class="p">,</span> <span class="c1">// Buffer to write</span>
                   <span class="n">LBA_t</span> <span class="n">sector</span><span class="p">,</span>     <span class="c1">// Start sector in LBA</span>
                   <span class="n">UINT</span> <span class="n">count</span><span class="p">)</span> <span class="p">{</span>     <span class="c1">// Number of sectors to write</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">pdrv</span> <span class="o">!=</span> <span class="n">SD_DRIVE_ID</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">RES_PARERR</span><span class="p">;}</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">buff</span> <span class="o">==</span> <span class="nb">NULL</span> <span class="o">||</span> <span class="n">count</span> <span class="o">==</span> <span class="mi">0u</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">RES_PARERR</span><span class="p">;</span> <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">disk_status_flags</span> <span class="o">&amp;</span> <span class="n">STA_NOINIT</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">RES_NOTRDY</span><span class="p">;</span> <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">sector</span> <span class="o">&gt;</span> <span class="p">(</span><span class="n">LBA_t</span><span class="p">)</span><span class="n">UINT32_MAX</span> <span class="o">||</span>
            <span class="p">(</span><span class="n">LBA_t</span><span class="p">)(</span><span class="n">count</span> <span class="o">-</span><span class="mi">1u</span><span class="p">)</span> <span class="o">&gt;</span> <span class="p">(</span><span class="n">LBA_t</span><span class="p">)</span><span class="n">UINT32_MAX</span> <span class="o">-</span> <span class="n">sector</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">return</span> <span class="n">RES_PARERR</span><span class="p">;</span>
        <span class="p">}</span> <span class="c1">// Ensure the requested sector range fits the driver's 32 bit block address</span>

        <span class="k">for</span> <span class="p">(</span><span class="n">UINT</span> <span class="n">i</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span> <span class="n">i</span><span class="o">&lt;</span><span class="n">count</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">sd_status_t</span> <span class="n">status</span> <span class="o">=</span> <span class="n">sd_card_write_block</span><span class="p">((</span><span class="kt">uint32_t</span><span class="p">)(</span><span class="n">sector</span> <span class="o">+</span> <span class="n">i</span><span class="p">),</span>
                                                         <span class="o">&amp;</span><span class="n">buff</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">SD_SECTOR_SIZE</span><span class="p">]</span>
                                                        <span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">RES_ERROR</span><span class="p">;</span> <span class="p">}</span> 
        <span class="p">}</span>

        <span class="k">return</span> <span class="n">RES_OK</span><span class="p">;</span>
<span class="p">}</span>

<span class="cp">#endif
</span></code></pre></div></div>
<p>Similarly, multi-sector writes are currently split up into repeated CMD24 operations. In the future I could use CMD25 for more efficient multi-block writes, but one step at a time ey.</p>

<p>Now FatFs can use this to write file data, directory entries, FAT tables, FSInfo data, or other metadata. Since the raw block operations were tested previously, failures here are probably due to the Media Access Interface, FatFs config, or API usage, although the lower levels cannot be completely ruled out.</p>

<h1 id="disk_ioctl">disk_ioctl()</h1>

<p>The disk_ioctl function handles miscellaneous control commands from FatFs, the template gives us this:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">DRESULT</span> <span class="nf">disk_ioctl</span> <span class="p">(</span>
	<span class="n">BYTE</span> <span class="n">pdrv</span><span class="p">,</span>		<span class="cm">/* Physical drive nmuber (0..) */</span>
	<span class="n">BYTE</span> <span class="n">cmd</span><span class="p">,</span>		<span class="cm">/* Control code */</span>
	<span class="kt">void</span> <span class="o">*</span><span class="n">buff</span>		<span class="cm">/* Buffer to send/receive control data */</span>
<span class="p">)</span>
<span class="p">{</span>
	<span class="n">DRESULT</span> <span class="n">res</span><span class="p">;</span>
	<span class="kt">int</span> <span class="n">result</span><span class="p">;</span>

	<span class="k">switch</span> <span class="p">(</span><span class="n">pdrv</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">case</span> <span class="n">DEV_RAM</span> <span class="p">:</span>

		<span class="c1">// Process of the command for the RAM drive</span>

		<span class="k">return</span> <span class="n">res</span><span class="p">;</span>

	<span class="k">case</span> <span class="n">DEV_MMC</span> <span class="p">:</span>

		<span class="c1">// Process of the command for the MMC/SD card</span>

		<span class="k">return</span> <span class="n">res</span><span class="p">;</span>

	<span class="k">case</span> <span class="n">DEV_USB</span> <span class="p">:</span>

		<span class="c1">// Process of the command the USB drive</span>

		<span class="k">return</span> <span class="n">res</span><span class="p">;</span>
	<span class="p">}</span>

	<span class="k">return</span> <span class="n">RES_PARERR</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Depending on the FatFs config, disk_ioctl() may need to handle the following commands. For this implementation, we will support the common sector information and synchronisation queries:</p>

<table>
  <thead>
    <tr>
      <th>Command</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CTRL_SYNC</td>
      <td>Make sure pending writes have completed</td>
    </tr>
    <tr>
      <td>GET_SECTOR_SIZE</td>
      <td>Return sector size in bytes</td>
    </tr>
    <tr>
      <td>GET_SECTOR_COUNT</td>
      <td>Return the number of sectors on the disk</td>
    </tr>
    <tr>
      <td>GET_BLOCK_SIZE</td>
      <td>Return erase block size in sectors</td>
    </tr>
  </tbody>
</table>

<p>Which is great because we have covered the core of this in previous posts with our sd_card() driver pulling the CSD register. So we just have to build out these functions a little:</p>

<p>CTRL_SYNC is used by FatFs to make sure that any pending writes have finished. The CMD24 write function already waits until the SD card has finished its internal write cycle before returning so for now if the disk is initialised CTRL_SYNC can return RES_OK:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="n">CTRL_SYNC</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">RES_OK</span><span class="p">;</span>
</code></pre></div></div>

<p>FatFs also needs to know the logical sector size. Because we are only building this for an SD card we can set this to 512 byte sectors:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define SD_SECTOR_SIZE 512u
</span>
<span class="k">case</span> <span class="n">GET_SECTOR_SIZE</span><span class="p">:</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">buff</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">RES_PARERR</span><span class="p">;</span> <span class="p">}</span>

        <span class="o">*</span><span class="p">(</span><span class="n">WORD</span> <span class="o">*</span><span class="p">)</span><span class="n">buff</span> <span class="o">=</span> <span class="n">SD_SECTOR_SIZE</span><span class="p">;</span>
        <span class="k">return</span> <span class="n">RES_OK</span><span class="p">;</span>
</code></pre></div></div>

<p>Get sector count is a bit more interesting because we will need to parse the CSD register for this information. For a SDHC or SDXC card using CSD v2.0, the sector count is calculated from the C_SIZE field. Each increment of C_SIZE represents 512 KiB of capacity, and since the logical sector size is 512 bytes. 512 KiB = 524288 Bytes, so 524288 / 512 = 1024:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sector_count = (C_SIZE + 1) * 1024
</code></pre></div></div>

<p>So in our sd_card.c driver we can parse the CSD register and store the sector count after initialisation.</p>

<p><code class="language-plaintext highlighter-rouge">C_SIZE</code> is stored in bits <code class="language-plaintext highlighter-rouge">[69:48]</code> of the CSD, which is:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">csd</span><span class="p">[</span><span class="mi">7</span><span class="p">]</span> <span class="o">-</span> <span class="n">bits</span> <span class="p">[</span><span class="mi">5</span><span class="o">:</span><span class="mi">0</span><span class="p">]</span>
 <span class="n">csd</span><span class="p">[</span><span class="mi">8</span><span class="p">]</span> <span class="o">-</span> <span class="n">bits</span> <span class="p">[</span><span class="mi">7</span><span class="o">:</span><span class="mi">0</span><span class="p">]</span>
 <span class="n">csd</span><span class="p">[</span><span class="mi">9</span><span class="p">]</span> <span class="o">-</span> <span class="n">bits</span> <span class="p">[</span><span class="mi">7</span><span class="o">:</span><span class="mi">0</span><span class="p">]</span>
</code></pre></div></div>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define SD_CSD_STRUCTURE_V1 0u
#define SD_CSD_STRUCTURE_V2 1u
</span>
<span class="k">static</span> <span class="n">sd_status_t</span> <span class="nf">sd_parse_csd_sector_count</span><span class="p">(</span><span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">csd</span><span class="p">,</span>
                                             <span class="kt">uint32_t</span> <span class="o">*</span><span class="n">sector_count</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">((</span><span class="n">csd</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="o">||</span> <span class="p">(</span><span class="n">sector_count</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">))</span> <span class="p">{</span>
                <span class="k">return</span> <span class="n">SD_ERR_PARAM</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="kt">uint8_t</span> <span class="n">csd_structure</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint8_t</span><span class="p">)(</span><span class="n">csd</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">&gt;&gt;</span> <span class="mi">6</span><span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">csd_structure</span> <span class="o">==</span> <span class="n">SD_CSD_STRUCTURE_V2</span><span class="p">)</span> <span class="p">{</span>
                <span class="c1">// CSD v2.0 used by SDHC and SDXC</span>
                <span class="kt">uint32_t</span> <span class="n">c_size</span> <span class="o">=</span> <span class="p">(((</span><span class="kt">uint32_t</span><span class="p">)</span><span class="n">csd</span><span class="p">[</span><span class="mi">7</span><span class="p">]</span> <span class="o">&amp;</span> <span class="mh">0x3Fu</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">16</span><span class="p">)</span>
                                 <span class="o">|</span> <span class="p">((</span><span class="kt">uint32_t</span><span class="p">)</span><span class="n">csd</span><span class="p">[</span><span class="mi">8</span><span class="p">]</span> <span class="o">&lt;&lt;</span> <span class="mi">8</span> <span class="p">)</span>
                                 <span class="o">|</span> <span class="p">((</span><span class="kt">uint32_t</span><span class="p">)</span><span class="n">csd</span><span class="p">[</span><span class="mi">9</span><span class="p">]);</span>
                <span class="o">*</span><span class="n">sector_count</span> <span class="o">=</span> <span class="p">(</span><span class="n">c_size</span> <span class="o">+</span> <span class="mi">1u</span><span class="p">)</span> <span class="o">*</span> <span class="mi">1024u</span><span class="p">;</span>
                <span class="k">return</span> <span class="n">SD_OK</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="k">return</span> <span class="n">SD_ERR_UNSUPPORTED_CARD</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>and</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">uint32_t</span> <span class="n">sd_sector_count</span> <span class="o">=</span> <span class="mi">0u</span><span class="p">;</span>

<span class="kt">uint32_t</span> <span class="nf">sd_card_get_sector_count</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">sd_sector_count</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now during the sd_card_init(), after the card has successfully initialised, the driver can read the CSD register and store the sector count:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">uint8_t</span> <span class="n">csd</span><span class="p">[</span><span class="n">SD_REG_SIZE</span><span class="p">];</span> 

<span class="n">status</span> <span class="o">=</span> <span class="n">sd_read_register</span><span class="p">(</span><span class="n">SD_CMD9</span><span class="p">,</span> <span class="n">csd</span><span class="p">);</span> 
<span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">status</span><span class="p">;</span> <span class="p">}</span> 

<span class="n">status</span> <span class="o">=</span> <span class="n">sd_parse_csd_sector_count</span><span class="p">(</span><span class="n">csd</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">sd_sector_count</span><span class="p">);</span> 
<span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">status</span><span class="p">;</span> <span class="p">}</span>
</code></pre></div></div>

<p>Now back in disk_ioctl() we can wrap it in GET_SECTOR_COUNT:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="n">GET_SECTOR_COUNT</span><span class="p">:</span> 
        <span class="k">if</span> <span class="p">(</span><span class="n">buff</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">RES_PARERR</span><span class="p">;</span> <span class="p">}</span>

        <span class="o">*</span><span class="p">(</span><span class="n">LBA_t</span> <span class="o">*</span><span class="p">)</span><span class="n">buff</span> <span class="o">=</span> <span class="p">(</span><span class="n">LBA_t</span><span class="p">)</span><span class="n">sd_card_get_sector_count</span><span class="p">();</span> 
        <span class="k">return</span> <span class="n">RES_OK</span><span class="p">;</span>
</code></pre></div></div>

<p>For GET_BLOCK_SIZE it needs to return the card’s erase block size in units of sectors, not bytes. This is used by FatFs when formatting the card to improve alignment. We are not currently reading the erase block size, so FatFs allows us to return 1, meaning one 512 byte sector:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="n">GET_BLOCK_SIZE</span><span class="p">:</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">buff</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span><span class="k">return</span> <span class="n">RES_PARERR</span><span class="p">;}</span>

        <span class="o">*</span><span class="p">(</span><span class="n">DWORD</span> <span class="o">*</span><span class="p">)</span><span class="n">buff</span> <span class="o">=</span> <span class="mi">1u</span><span class="p">;</span>
        <span class="k">return</span> <span class="n">RES_OK</span><span class="p">;</span>
</code></pre></div></div>
<p>This is a valid default and should not affect normal file reads or writes, fingers crossed.</p>

<p>Now we can put all of this together into the disk_ioctl() function:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">DRESULT</span> <span class="nf">disk_ioctl</span><span class="p">(</span><span class="n">BYTE</span> <span class="n">pdrv</span><span class="p">,</span>     <span class="c1">// Physical drive number</span>
                   <span class="n">BYTE</span> <span class="n">cmd</span><span class="p">,</span>      <span class="c1">// Command code</span>
                   <span class="kt">void</span> <span class="o">*</span><span class="n">buff</span><span class="p">)</span> <span class="p">{</span>  <span class="c1">// buffer to send/receive</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">pdrv</span> <span class="o">!=</span> <span class="n">SD_DRIVE_ID</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">RES_PARERR</span><span class="p">;</span> <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">disk_status_flags</span> <span class="o">&amp;</span> <span class="n">STA_NOINIT</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">RES_NOTRDY</span><span class="p">;</span> <span class="p">}</span>

        <span class="k">switch</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">case</span> <span class="n">CTRL_SYNC</span><span class="p">:</span>
                <span class="k">return</span> <span class="n">RES_OK</span><span class="p">;</span>
        
        <span class="k">case</span> <span class="n">GET_SECTOR_SIZE</span><span class="p">:</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">buff</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">RES_PARERR</span><span class="p">;</span> <span class="p">}</span>

                <span class="o">*</span><span class="p">(</span><span class="n">WORD</span> <span class="o">*</span><span class="p">)</span><span class="n">buff</span> <span class="o">=</span> <span class="n">SD_SECTOR_SIZE</span><span class="p">;</span>
                <span class="k">return</span> <span class="n">RES_OK</span><span class="p">;</span>
        
        <span class="k">case</span> <span class="n">GET_SECTOR_COUNT</span><span class="p">:</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">buff</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">RES_PARERR</span><span class="p">;</span> <span class="p">}</span>

                <span class="o">*</span><span class="p">(</span><span class="n">LBA_t</span> <span class="o">*</span><span class="p">)</span><span class="n">buff</span> <span class="o">=</span> <span class="p">(</span><span class="n">LBA_t</span><span class="p">)</span><span class="n">sd_card_get_sector_count</span><span class="p">();</span>
                <span class="k">return</span> <span class="n">RES_OK</span><span class="p">;</span>

        <span class="k">case</span> <span class="n">GET_BLOCK_SIZE</span><span class="p">:</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">buff</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">RES_PARERR</span><span class="p">;</span> <span class="p">}</span>

                <span class="o">*</span><span class="p">(</span><span class="n">DWORD</span> <span class="o">*</span><span class="p">)</span><span class="n">buff</span> <span class="o">=</span> <span class="mi">1u</span><span class="p">;</span>
                <span class="k">return</span> <span class="n">RES_OK</span><span class="p">;</span>
        
        <span class="nl">default:</span>
                <span class="k">return</span> <span class="n">RES_PARERR</span><span class="p">;</span>
        <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="get_fattime">get_fattime()</h1>
<p>FatFs can also use a function called get_fattime() to timestamp files. The return value is a packed FAT timestamp:</p>

<table>
  <thead>
    <tr>
      <th>Bit Range</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>31:25</td>
      <td>year from 1980</td>
    </tr>
    <tr>
      <td>24:21</td>
      <td>month</td>
    </tr>
    <tr>
      <td>20:16</td>
      <td>day</td>
    </tr>
    <tr>
      <td>15:11</td>
      <td>hour</td>
    </tr>
    <tr>
      <td>10:5</td>
      <td>minute</td>
    </tr>
    <tr>
      <td>4:0</td>
      <td>second / 2</td>
    </tr>
  </tbody>
</table>

<p>I haven’t got a real time clock connected yet so for now we will just return a placeholder timestamp, we will add this to the diskio.c file:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">DWORD</span> <span class="nf">get_fattime</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span> 
        <span class="k">return</span> <span class="p">((</span><span class="n">DWORD</span><span class="p">)(</span><span class="mi">2026u</span> <span class="o">-</span> <span class="mi">1980u</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">25</span><span class="p">)</span> 
                <span class="o">|</span> <span class="p">((</span><span class="n">DWORD</span><span class="p">)</span><span class="mi">6u</span> <span class="o">&lt;&lt;</span> <span class="mi">21</span><span class="p">)</span>
                <span class="o">|</span> <span class="p">((</span><span class="n">DWORD</span><span class="p">)</span><span class="mi">17u</span> <span class="o">&lt;&lt;</span> <span class="mi">16</span><span class="p">)</span>
                <span class="o">|</span> <span class="p">((</span><span class="n">DWORD</span><span class="p">)</span><span class="mi">12u</span> <span class="o">&lt;&lt;</span> <span class="mi">11</span><span class="p">)</span>
                <span class="o">|</span> <span class="p">((</span><span class="n">DWORD</span><span class="p">)</span><span class="mi">0u</span> <span class="o">&lt;&lt;</span> <span class="mi">5</span><span class="p">)</span>
                <span class="o">|</span> <span class="p">((</span><span class="n">DWORD</span><span class="p">)</span><span class="mi">0u</span><span class="p">);</span> 
<span class="p">}</span>
</code></pre></div></div>

<p>That represents 2026-06-17 12:00:00.</p>

<h1 id="fatfs-mai-smoke-test">FatFs MAI smoke test</h1>

<p>This might have been a bit boring but that is sort of the point, we did all the fun stuff in the bsp_spi.c and sd_card.c and now we are just gluing that functionality into the FatFs Media Access Interface.</p>

<p>We can try a really small FatFs test now… Mounting the filesystem:</p>

<p>In main.c:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"ff.h"</span><span class="cp">
</span>
<span class="n">FATFS</span> <span class="n">fs</span><span class="p">;</span>
<span class="n">FRESULT</span> <span class="n">fr</span><span class="p">;</span>

<span class="n">fr</span> <span class="o">=</span> <span class="n">f_mount</span><span class="p">(</span><span class="o">&amp;</span><span class="n">fs</span><span class="p">,</span> <span class="s">"0:"</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">fr</span> <span class="o">!=</span> <span class="n">FR_OK</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">bsp_uart1_write_string</span><span class="p">(</span><span class="s">"f_mount failed</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
        <span class="k">while</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>

<span class="n">bsp_uart1_write_string</span><span class="p">(</span><span class="s">"f_mount OK</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
</code></pre></div></div>

<p>If this works FatFs has successfully called through the MAI, read the boot sector from the SD card and recognised the filesystem.</p>

<p>So we’ve flashed that and pulled up minicom, reset the micro and have seen the very underwhelming “f_mount OK” message so it’s working, but yeah, not the most interesting progress.</p>

<p>In the next post I’ll give more of an overview on the FatFs API, and tackle making files, writing them, and verifying them on a laptop.</p>

<script src="https://utteranc.es/client.js" repo="skoopsy/skoopsy.github.io" issue-term="pathname" label="blog-embedded16" theme="preferred-color-scheme" crossorigin="anonymous" async="">
</script>

<p>Copyright © 2026 David O’Connor</p>]]></content><author><name>Dave</name></author><category term="STM32" /><summary type="html"><![CDATA[The last few posts have covered developing a SPI and SD card driver in preparation to interface with the FatFs library developed by ChaN. I don’t want my application code to be manually decoding MBRs, FAT32 boot sector, root directory entries etc. The stack I’m building is as follows:]]></summary></entry><entry><title type="html">STM32 #15: Writing raw blocks on a microSD with SPI</title><link href="https://skoopsy.dev/stm32/2026/06/16/STM32-16-sd-card-block-write.html" rel="alternate" type="text/html" title="STM32 #15: Writing raw blocks on a microSD with SPI" /><published>2026-06-16T19:40:00+00:00</published><updated>2026-06-16T19:40:00+00:00</updated><id>https://skoopsy.dev/stm32/2026/06/16/STM32-16-sd-card-block-write</id><content type="html" xml:base="https://skoopsy.dev/stm32/2026/06/16/STM32-16-sd-card-block-write.html"><![CDATA[<p>The <a href="https://skoopsy.dev/stm32/2026/06/16/STM32-15-sd-card-block-read.html">last post</a> finished the raw block read path. 
So this time I want to build and test the other half of our low level SD driver: writing a raw 512 byte block from the STM32 using <code class="language-plaintext highlighter-rouge">CMD24</code>.</p>

<p>One warning before going further: this is still not filesystem aware writing. I am not asking FatFs to create a file, allocate a cluster, update a directory entry, or modify the FAT tables. I am just writing 512 bytes directly to a block address. If I write to the wrong block, I can corrupt the card. So this test belongs on a disposable card, or at least a card whose contents I do not care about.</p>

<p>The goals for this post are to:</p>

<ul>
  <li>write 512 bytes to a known test block</li>
  <li>read the same block back with CMD17</li>
  <li>compare the transmitted and received buffers</li>
</ul>

<p>If that works, then the driver can provide the two primitives FatFs will need:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sd_status_t</span> <span class="nf">sd_card_cmd17_read_block</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">block_index</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">buffer</span><span class="p">);</span>
<span class="n">sd_status_t</span> <span class="nf">sd_card_cmd24_write_block</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">block_index</span><span class="p">,</span> <span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">buffer</span><span class="p">);</span>
</code></pre></div></div>

<p>At that point, <code class="language-plaintext highlighter-rouge">disk_read()</code> and <code class="language-plaintext highlighter-rouge">disk_write()</code> should mostly become translation wrappers between FatFs and the lower level SD card driver.</p>

<h1 id="cmd24-write-block">CMD24: Write block</h1>

<p>For the SDHC/SDXC card I’m using, the argument passed to CMD24 is a block number. So if I want to write block 100,000, I send 100,000 as the command argument. For older SDSC cards, the argument is a byte address, so the block index would need to be multiplied by 512 via the helper function developed in the last post.</p>

<p>The CMD24 command byte is: <code class="language-plaintext highlighter-rouge">0x40 | 24 = 0x58</code>. So the command packet for writing to block 100000 should look like 58 [address bytes] FF. The FF at the end is a dummy CRC byte. In SPI mode after initialisation, CRC checking is normally disabled unless explicitly enabled.</p>

<h1 id="the-write-sequence">The write sequence</h1>

<ol>
  <li>Select the card</li>
  <li>Send CMD24 with the block address</li>
  <li>Wait for an R1 response</li>
  <li>Check that R1 is 0x00</li>
  <li>Send the 0xFE data token</li>
  <li>Send 512 bytes of block data</li>
  <li>Send two dummy CRC bytes</li>
  <li>Read the data response token</li>
  <li>Wait while the card is busy programming the flash</li>
  <li>Deselect the card</li>
</ol>

<h1 id="new-macros-to-add">New macros to add</h1>

<p>These are the extra definitions I need for the single block write:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define SD_CMD24 24u
</span>
<span class="cp">#define SD_BLOCK_SIZE 512u // in last post
#define SD_DATA_TOKEN 0xFEu // in last post
#define SD_CRC_SIZE 2u // in last post
</span>
<span class="cp">#define SD_DATA_RESPONSE_MASK 0x1Fu 
#define SD_DATA_ACCEPTED 0x05u 
#define SD_WRITE_BUSY_ATTEMPTS 10000u
</span></code></pre></div></div>

<p>The write data token is the same used for a single block read payload. This time, for the write the host will send the <code class="language-plaintext highlighter-rouge">0xFE</code> data token to the card, then the 512 byte payload. After receiving the block the card returns a data response token. The lower 5 bits are most interesting for us:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>xxxxx101 = data accepted 
xxxxx010 = CRC error 
xxxxx011 = write error
</code></pre></div></div>

<p>So to check if data has not been accepted run this code:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">((</span><span class="n">data_response</span> <span class="o">&amp;</span> <span class="n">SD_DATA_RESPONSE_MASK</span><span class="p">)</span> <span class="o">!=</span> <span class="n">SD_DATA_ACCEPTED</span><span class="p">)</span> <span class="p">{</span> 
        <span class="k">return</span> <span class="n">SD_ERR_BAD_RESPONSE</span><span class="p">;</span> 
<span class="p">}</span>
</code></pre></div></div>

<p>Only the lower 5 bits have to equal 0x05.</p>

<h1 id="waiting-for-the-card-to-finish-writing">Waiting for the card to finish writing</h1>

<p>After the card has accepted data, it might hold the MISO line low while it writes to the internal flash. When the card is busy, reads return <code class="language-plaintext highlighter-rouge">0x00</code>. When the card is ready again, reads return <code class="language-plaintext highlighter-rouge">0xFF</code>.</p>

<p>So we need to build a helper that keeps clocking the card until it releases the bus:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">sd_status_t</span> <span class="nf">sd_wait_write_complete</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">SD_WRITE_BUSY_ATTEMPTS</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="kt">uint8_t</span> <span class="n">value</span> <span class="o">=</span> <span class="mh">0x00u</span><span class="p">;</span>

                <span class="n">bsp_spi_status_t</span> <span class="n">spi_status</span> <span class="o">=</span> <span class="n">bsp_spi1_transfer</span><span class="p">(</span><span class="mh">0xFFu</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">value</span><span class="p">);</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">spi_status</span> <span class="o">!=</span> <span class="n">BSP_SPI_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_ERR_SPI</span><span class="p">;</span> <span class="p">}</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">value</span> <span class="o">==</span> <span class="mh">0xFFu</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_OK</span><span class="p">;</span> <span class="p">}</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="n">SD_ERR_TIMEOUT</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This is a very crude timeout, a better version would use a hardware timer so that it is not based on loop count, but for now this is fine, and stops the driver hanging if the card never finishes the write cycle.</p>

<h1 id="the-write-block-function">The write block function</h1>

<p>Now we will build the function to use <code class="language-plaintext highlighter-rouge">CMD24</code> to write a block:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sd_status_t</span> <span class="nf">sd_card_cmd24_write_block</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">block_index</span><span class="p">,</span>
                                      <span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">buffer</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">buffer</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_ERR_PARAM</span><span class="p">;</span> <span class="p">}</span>

        <span class="kt">uint32_t</span> <span class="n">address</span> <span class="o">=</span> <span class="n">sd_block_to_card_address</span><span class="p">(</span><span class="n">block_index</span><span class="p">);</span>
        <span class="kt">uint8_t</span> <span class="n">r1</span> <span class="o">=</span> <span class="mh">0xFFu</span><span class="p">;</span>

        <span class="c1">// Select the card</span>
        <span class="n">sd_select</span><span class="p">();</span>

        <span class="c1">// Send CMD24 with the block address</span>
        <span class="n">sd_status_t</span> <span class="n">status</span> <span class="o">=</span> <span class="n">sd_write_command_packet</span><span class="p">(</span><span class="n">SD_CMD24</span><span class="p">,</span>
                                                     <span class="n">address</span><span class="p">,</span>
                                                     <span class="n">SD_DUMMY_CRC</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span> 
                <span class="n">sd_deselect</span><span class="p">();</span>
                <span class="k">return</span> <span class="n">status</span><span class="p">;</span> 
        <span class="p">}</span>
        
        <span class="c1">// Wait for an R1 response</span>
        <span class="n">status</span> <span class="o">=</span> <span class="n">sd_wait_r1</span><span class="p">(</span><span class="o">&amp;</span><span class="n">r1</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">sd_deselect</span><span class="p">();</span>
                <span class="k">return</span> <span class="n">status</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// Check that R1 is 0x00</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">r1</span> <span class="o">!=</span> <span class="n">SD_R1_READY_STATE</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">sd_deselect</span><span class="p">();</span>
                <span class="k">return</span> <span class="n">SD_ERR_BAD_RESPONSE</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// Send the 0xFE data token</span>
        <span class="kt">uint8_t</span> <span class="n">token</span> <span class="o">=</span> <span class="n">SD_DATA_TOKEN</span><span class="p">;</span>
        <span class="n">bsp_spi_status_t</span> <span class="n">spi_status</span> <span class="o">=</span> <span class="n">bsp_spi1_write_buffer</span><span class="p">(</span><span class="o">&amp;</span><span class="n">token</span><span class="p">,</span> <span class="mi">1u</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">spi_status</span> <span class="o">!=</span> <span class="n">BSP_SPI_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">sd_deselect</span><span class="p">();</span>
                <span class="k">return</span> <span class="n">SD_ERR_SPI</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// Send 512 bytes of block data</span>
        <span class="n">spi_status</span> <span class="o">=</span> <span class="n">bsp_spi1_write_buffer</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="n">SD_BLOCK_SIZE</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">spi_status</span> <span class="o">!=</span> <span class="n">BSP_SPI_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">sd_deselect</span><span class="p">();</span>
                <span class="k">return</span> <span class="n">SD_ERR_SPI</span><span class="p">;</span>
        <span class="p">}</span>
        
        <span class="c1">// Send two dummy CRC bytes</span>
        <span class="kt">uint8_t</span> <span class="n">crc</span><span class="p">[</span><span class="n">SD_CRC_SIZE</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="mh">0xFFu</span><span class="p">,</span> <span class="mh">0xFFu</span><span class="p">};</span>
        <span class="n">spi_status</span> <span class="o">=</span> <span class="n">bsp_spi1_write_buffer</span><span class="p">(</span><span class="n">crc</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">crc</span><span class="p">));</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">spi_status</span> <span class="o">!=</span> <span class="n">BSP_SPI_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">sd_deselect</span><span class="p">();</span>
                <span class="k">return</span> <span class="n">SD_ERR_SPI</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// Read the data response token</span>
        <span class="kt">uint8_t</span> <span class="n">data_response</span> <span class="o">=</span> <span class="mh">0xFFu</span><span class="p">;</span>
        <span class="n">spi_status</span> <span class="o">=</span> <span class="n">bsp_spi1_transfer</span><span class="p">(</span><span class="mh">0xFFu</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">data_response</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">spi_status</span> <span class="o">!=</span> <span class="n">BSP_SPI_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">sd_deselect</span><span class="p">();</span>
                <span class="k">return</span> <span class="n">SD_ERR_SPI</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="k">if</span> <span class="p">((</span><span class="n">data_response</span> <span class="o">&amp;</span> <span class="n">SD_DATA_RESPONSE_MASK</span><span class="p">)</span> <span class="o">!=</span> <span class="n">SD_DATA_ACCEPTED</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">sd_deselect</span><span class="p">();</span>
                <span class="k">return</span> <span class="n">SD_ERR_BAD_RESPONSE</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// Wait while the card is busy programming the flash</span>
        <span class="n">status</span> <span class="o">=</span> <span class="n">sd_wait_write_complete</span><span class="p">();</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">sd_deselect</span><span class="p">();</span>
                <span class="k">return</span> <span class="n">status</span><span class="p">;</span>
        <span class="p">}</span>
        
        <span class="c1">// Deselect the card</span>
        <span class="n">sd_deselect</span><span class="p">();</span>
        <span class="k">return</span> <span class="n">SD_OK</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>It might look like a lot but it is all things we’ve seen before and just follows the write sequence outlined earlier, with some error handling. The sd_wait_write_complete() part towards the end is important, if we skip this and send another command whilst the card is busy writing flash the command will probably fail or worse.</p>

<p>We have to be careful as we are just writing random blocks here without using FatFS, but because we created and accessed the HELLO.TXT file in the last post and I still have it on the card, I now have a known sector that belongs to the existing HELLO.TXT file, so I can overwrite that sector and check whether the file contents change.</p>

<p>HELLO.TXT had these directory details from the last post:</p>

<ul>
  <li>first_cluster = 182</li>
  <li>file_size = 20 bytes</li>
  <li>file_lba = 70946</li>
</ul>

<p>We have to be careful to not change the file size. I am not updating the FAT directory entry, so macOS will still think the file is 20 bytes long. I am only overwriting the existing file data inside its already allocated cluster.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HELLO      5
_          1  = 6
FROM       4  = 10
_          1  = 11
SKOOPSY    7  = 18
\r\n       2  = 20
</code></pre></div></div>
<p>Let’s replace it with another 20 byte string: <code class="language-plaintext highlighter-rouge">HELLO_FROM_STM32!!\r\n</code></p>

<h1 id="comparing-buffers">Comparing buffers</h1>
<p>We can write this function so that we can compare the buffer on the micro after a write then read of the same block. But we will also probably check on the laptop for good measure. Here is the compare function:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">uint32_t</span> <span class="nf">compare_buffers</span><span class="p">(</span><span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">a</span><span class="p">,</span>
                               <span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">b</span><span class="p">,</span>
                               <span class="kt">uint32_t</span> <span class="n">length</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">uint32_t</span> <span class="n">errors</span> <span class="o">=</span> <span class="mi">0u</span><span class="p">;</span>

        <span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">i</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span> <span class="n">i</span><span class="o">&lt;</span><span class="n">length</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">a</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">!=</span> <span class="n">b</span><span class="p">[</span><span class="n">i</span><span class="p">])</span> <span class="p">{</span>
                        <span class="o">++</span><span class="n">errors</span><span class="p">;</span>
                <span class="p">}</span>
        <span class="p">}</span>

        <span class="k">return</span> <span class="n">errors</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Let’s build the test in main now, we will read the HELLO.TXT sector contents again, then modify the first 20 bytes of the file, write it back, then read it again and run the buffer compare, along with printing it out in hex over UART:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define HELLO_FILE_LBA 70946u // LBA of the first data sector for HELLO.TXT
</span><span class="kt">uint8_t</span> <span class="n">tx_block</span><span class="p">[</span><span class="n">SD_BLOCK_SIZE</span><span class="p">];</span>
<span class="kt">uint8_t</span> <span class="n">rx_block</span><span class="p">[</span><span class="n">SD_BLOCK_SIZE</span><span class="p">];</span>

<span class="c1">// Initialise card</span>
<span class="n">sd_status_t</span> <span class="n">status</span> <span class="o">=</span> <span class="n">sd_card_init</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">bsp_uart1_write_string</span><span class="p">(</span><span class="s">"SD init failed</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
    <span class="k">while</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>

<span class="c1">// Read existing HELLO.TXT data sector into tx_block</span>
<span class="n">status</span> <span class="o">=</span> <span class="n">sd_card_cmd17_read_block</span><span class="p">(</span><span class="n">HELLO_FILE_LBA</span><span class="p">,</span> <span class="n">tx_block</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">bsp_uart1_write_string</span><span class="p">(</span><span class="s">"Read HELLO.TXT sector failed</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
    <span class="k">while</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="n">debug_uart_print_hex_buffer</span><span class="p">(</span><span class="s">"HELLO sector readback"</span><span class="p">,</span> <span class="n">tx_block</span><span class="p">,</span> <span class="mi">32u</span><span class="p">);</span>

<span class="c1">// Create the new message</span>
<span class="c1">// This must stay the same length as the existing file: 20 bytes.</span>
<span class="k">const</span> <span class="kt">char</span> <span class="n">message</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"HELLO_FROM_STM32!!</span><span class="se">\r\n</span><span class="s">"</span><span class="p">;</span>
<span class="c1">// message includes the string terminator, so write sizeof(message) - 1</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">message</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1u</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">tx_block</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint8_t</span><span class="p">)</span><span class="n">message</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
<span class="p">}</span>

<span class="c1">// Write the modified 512 byte sector back</span>
<span class="n">status</span> <span class="o">=</span> <span class="n">sd_card_cmd24_write_block</span><span class="p">(</span><span class="n">HELLO_FILE_LBA</span><span class="p">,</span> <span class="n">tx_block</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">bsp_uart1_write_string</span><span class="p">(</span><span class="s">"Write HELLO.TXT sector failed</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
    <span class="k">while</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="n">bsp_uart1_write_string</span><span class="p">(</span><span class="s">"Write HELLO.TXT sector OK</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>

<span class="c1">// Read the block we've just written</span>
<span class="n">status</span> <span class="o">=</span> <span class="n">sd_card_cmd17_read_block</span><span class="p">(</span><span class="n">HELLO_FILE_LBA</span><span class="p">,</span> <span class="n">rx_block</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">bsp_uart1_write_string</span><span class="p">(</span><span class="s">"Readback failed</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
    <span class="k">while</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>

<span class="c1">// Compare the results</span>
<span class="kt">uint32_t</span> <span class="n">errors</span> <span class="o">=</span> <span class="n">compare_buffers</span><span class="p">(</span><span class="n">tx_block</span><span class="p">,</span> <span class="n">rx_block</span><span class="p">,</span> <span class="n">SD_BLOCK_SIZE</span><span class="p">);</span>

<span class="k">if</span> <span class="p">(</span><span class="n">errors</span> <span class="o">==</span> <span class="mi">0u</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">bsp_uart1_write_string</span><span class="p">(</span><span class="s">"Raw block verify OK</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="n">bsp_uart1_write_string</span><span class="p">(</span><span class="s">"Raw block verify failed</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span>

<span class="n">debug_uart_print_hex_buffer</span><span class="p">(</span><span class="s">"HELLO sector readback"</span><span class="p">,</span> <span class="n">rx_block</span><span class="p">,</span> <span class="mi">32u</span><span class="p">);</span>
</code></pre></div></div>

<p>I’ve uploaded to the board and run a minicom terminal to monitor UART and we have this output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HELLO sector readback: 48 45 4C 4C 4F 5F 46 52 4F 4D 5F 53 4B 4F 4F 50 53 59 0D 0A 00 00 00 00 00 00 00 00 00 00 00 00

Write HELLO.TXT sector OK

Raw block verify OK

HELLO sector readback: 48 45 4C 4C 4F 5F 46 52 4F 4D 5F 53 54 4D 33 32 21 21 0D 0A 00 00 00 00 00 00 00 00 00 00 00 00
</code></pre></div></div>

<p>Great, so the block changed from:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>48 45 4C 4C 4F 5F 46 52 4F 4D 5F 53 4B 4F 4F 50 53 59 0D 0A
=
HELLO_FROM_SKOOPSY\r\n
</code></pre></div></div>
<p>to</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>48 45 4C 4C 4F 5F 46 52 4F 4D 5F 53 54 4D 33 32 21 21 0D 0A
=
HELLO_FROM_STM32!!\r\n
</code></pre></div></div>

<p>I’m happy with that; we can now confidently read and write raw blocks with the SD card driver. A note on this though, this worked because HELLO.TXT was already a file, and I knew where the file contents were so could overwrite it fairly easily without changing the file size or allocation. I didn’t create a new file, update timestamps, or allocate clusters. That is for FatFS to do!</p>

<script src="https://utteranc.es/client.js" repo="skoopsy/skoopsy.github.io" issue-term="pathname" label="blog-embedded15" theme="preferred-color-scheme" crossorigin="anonymous" async="">
</script>

<p>Copyright © 2026 David O’Connor</p>]]></content><author><name>Dave</name></author><category term="STM32" /><summary type="html"><![CDATA[The last post finished the raw block read path. So this time I want to build and test the other half of our low level SD driver: writing a raw 512 byte block from the STM32 using CMD24.]]></summary></entry><entry><title type="html">STM32 #14: Reading raw blocks on a microSD with SPI</title><link href="https://skoopsy.dev/stm32/2026/06/16/STM32-15-sd-card-block-read.html" rel="alternate" type="text/html" title="STM32 #14: Reading raw blocks on a microSD with SPI" /><published>2026-06-16T13:00:00+00:00</published><updated>2026-06-16T13:00:00+00:00</updated><id>https://skoopsy.dev/stm32/2026/06/16/STM32-15-sd-card-block-read</id><content type="html" xml:base="https://skoopsy.dev/stm32/2026/06/16/STM32-15-sd-card-block-read.html"><![CDATA[<p>The <a href="https://skoopsy.dev/stm32/2026/06/14/STM32-14-sd-card-init-finish.html">last post</a> finally finished the SD card initialisation sequence. The card left idle state using <code class="language-plaintext highlighter-rouge">CMD55</code> + <code class="language-plaintext highlighter-rouge">ACMD41</code>, <code class="language-plaintext highlighter-rouge">CMD58</code> returned a valid OCR value, and I used <code class="language-plaintext highlighter-rouge">CMD9</code> and <code class="language-plaintext highlighter-rouge">CMD10</code> to read the CSD and CID registers. This proves a few core foundations:</p>

<ol>
  <li>The card can be initialised into SPI mode.</li>
  <li>The driver can detect SDHC/SDXC block addressing.</li>
  <li>The driver can read register data after a <code class="language-plaintext highlighter-rouge">0xFE</code> data token.</li>
  <li>The SPI command/response layer is working with some reliability.</li>
</ol>

<p>Now it’s time to stop asking the card about itself and try to read some storage. However, this post still won’t be using FatFS. Before adding that, I want to prove out the low level drivers and their functions fully before integrating them into the FatFS layer. We need to prove that the sd driver can read and write a single raw 512 byte block. If this works, then the eventual FatFS layer should be easier, because <code class="language-plaintext highlighter-rouge">disk_read()</code> and <code class="language-plaintext highlighter-rouge">disk_write()</code> should be able to be simple translation wrappers to the lower level sd card driver.</p>

<h1 id="why-bother-with-raw-blocks-for-fatfs">Why bother with raw blocks for FatFS</h1>
<p>FatFS does the filesystem work for us. It knows how to create files, find free clusters, update FAT tables, and manage directories. FatFS still needs the low level drivers to provide the actual sector access. The application layers will look like the ones posted in <a href="https://skoopsy.dev/stm32/2026/06/09/STM32-13-sd-card-transport.html">STM32 #12: Talking to a microSD card over SPI</a>. So first we need to build and prove out two core functions:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sd_status_t</span> <span class="nf">sd_card_read_block</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">block_index</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">buffer</span><span class="p">);</span>
<span class="n">sd_status_t</span> <span class="nf">sd_card_write_block</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">block_index</span><span class="p">,</span> <span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">buffer</span><span class="p">);</span>
</code></pre></div></div>

<p>This post we will just build the read_block function.</p>

<h1 id="block-addressing">Block addressing</h1>

<p>For SDHC/SDXC cards, the address passed to <code class="language-plaintext highlighter-rouge">CMD17</code> (Read a block) and <code class="language-plaintext highlighter-rouge">CMD24</code> (write a block) is a 512 byte block number. For older cards, the address is a byte address. For example, if we wanted to access block 100 000:</p>

<ul>
  <li>SDSC address = 100 000 * 512</li>
  <li>SDHC/SDXC address = 100 000</li>
</ul>

<p>Let’s create a simple helper to abstract that conversion away:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">uint32_t</span> <span class="nf">sd_block_to_card_address</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">block_index</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">sd_card_type</span> <span class="o">==</span> <span class="n">SD_CARD_TYPE_SDHC_OR_SDXC</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">block_index</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">block_index</span> <span class="o">*</span> <span class="n">SD_BLOCK_SIZE</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now the rest of the driver can work in logical 512 byte block numbers.</p>

<h1 id="commands-and-macros-for-writing-and-reading-blocks">Commands and Macros for Writing and Reading Blocks</h1>

<p>Here are the commands we will focus on:</p>

<table>
  <thead>
    <tr>
      <th>CMD</th>
      <th>Meaning</th>
      <th>Command byte</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CMD17</td>
      <td>Read single block</td>
      <td><code class="language-plaintext highlighter-rouge">0x40 bitwiseOR 17 = 0x51</code></td>
    </tr>
    <tr>
      <td>CMD24</td>
      <td>Write single block</td>
      <td><code class="language-plaintext highlighter-rouge">0x40 bitwiseOR 24 = 0x58</code></td>
    </tr>
  </tbody>
</table>

<p>Here are some macros that will help us use these commands in an easy to understand way:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define SD_CMD17 17u // Read single block
</span>
<span class="cp">#define SD_BLOCK_SIZE 512u
#define SD_DATA_TOKEN 0xFEu
</span>
<span class="cp">#define SD_CRC_SIZE 2u
</span>
<span class="cp">#define SD_DATA_RESPONSE_MASK 0x1Fu
#define SD_DATA_ACCEPTED 0x05u
</span>
<span class="cp">#define SD_WRITE_BUSY_ATTEMPTS 10000u
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">SD_DATA_TOKEN</code> was covered in the last post but I’m keeping it here for brevity. It is used at the beginning of a block read, and for block writes the host sends it before the 512 byte payload. The full sequence is:</p>

<ol>
  <li>Select the card</li>
  <li>Send <code class="language-plaintext highlighter-rouge">CMD17</code> with block address</li>
  <li>Wait for an R1 response</li>
  <li>Wait for the 0xFE data token</li>
  <li>Read 512 bytes</li>
  <li>Read 2 CRC bytes</li>
  <li>Deselect the card</li>
</ol>

<p>So let’s create a function following that sequence:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sd_status_t</span> <span class="nf">sd_card_read_block</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">block_index</span><span class="p">,</span> 
                               <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">buffer</span><span class="p">)</span> <span class="p">{</span>

        <span class="k">if</span> <span class="p">(</span> <span class="n">buffer</span> <span class="o">==</span> <span class="nb">NULL</span> <span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_ERR_PARAM</span><span class="p">;</span> <span class="p">}</span>

        <span class="c1">// Setup parameters</span>
        <span class="kt">uint32_t</span> <span class="n">address</span> <span class="o">=</span> <span class="n">sd_block_to_card_address</span><span class="p">(</span><span class="n">block_index</span><span class="p">);</span>
        <span class="kt">uint8_t</span> <span class="n">r1</span> <span class="o">=</span> <span class="mh">0xFFu</span><span class="p">;</span>

        <span class="c1">// Select the card</span>
        <span class="n">sd_select</span><span class="p">();</span>

        <span class="c1">// Send read single block command</span>
        <span class="n">sd_status_t</span> <span class="n">status</span> <span class="o">=</span> <span class="n">sd_write_command_packet</span><span class="p">(</span><span class="n">SD_CMD17</span><span class="p">,</span>
                                                     <span class="n">address</span><span class="p">,</span>
                                                     <span class="n">SD_DUMMY_CRC</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span> 
                <span class="n">sd_deselect</span><span class="p">();</span>
                <span class="k">return</span> <span class="n">status</span><span class="p">;</span> 
        <span class="p">}</span>

        <span class="c1">// Wait for R1 response</span>
        <span class="n">status</span> <span class="o">=</span> <span class="n">sd_wait_r1</span><span class="p">(</span><span class="o">&amp;</span><span class="n">r1</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">sd_deselect</span><span class="p">();</span>
                <span class="k">return</span> <span class="n">status</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">r1</span> <span class="o">!=</span> <span class="n">SD_R1_READY_STATE</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">sd_deselect</span><span class="p">();</span>
                <span class="k">return</span> <span class="n">SD_ERR_BAD_RESPONSE</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// Wait for data token</span>
        <span class="n">status</span> <span class="o">=</span> <span class="n">sd_wait_data_token</span><span class="p">();</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">sd_deselect</span><span class="p">();</span>
                <span class="k">return</span> <span class="n">status</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// Read data</span>
        <span class="n">status</span> <span class="o">=</span> <span class="n">sd_read_bytes</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="n">SD_BLOCK_SIZE</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">sd_deselect</span><span class="p">();</span>
                <span class="k">return</span> <span class="n">status</span><span class="p">;</span>
        <span class="p">}</span>
        
        <span class="c1">// Read CRC bytes</span>
        <span class="kt">uint8_t</span> <span class="n">crc</span><span class="p">[</span><span class="n">SD_CRC_SIZE</span><span class="p">];</span>
        <span class="n">status</span> <span class="o">=</span> <span class="n">sd_read_bytes</span><span class="p">(</span><span class="n">crc</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">crc</span><span class="p">));</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">sd_deselect</span><span class="p">();</span>
                <span class="k">return</span> <span class="n">status</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// Deselect the card</span>
        <span class="n">sd_deselect</span><span class="p">();</span>
        <span class="k">return</span> <span class="n">SD_OK</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This was pretty straight forward because of the way we set this up in the last post with <code class="language-plaintext highlighter-rouge">sd_write_command_packet()</code>, <code class="language-plaintext highlighter-rouge">sd_wait_r1()</code>, <code class="language-plaintext highlighter-rouge">sd_wait_data_token()</code>, and <code class="language-plaintext highlighter-rouge">sd_read_bytes()</code> all being reused.</p>

<h1 id="reading-block-0">Reading block 0</h1>

<p>Block 0 should contain a master boot record, partition table or filesystem boot sector depending on how the card has been formatted. Let’s just confirm we get 512 bytes back first.</p>

<p>Now there are again two ways to test this, by calling the previous UART print functionality developed, or to use a logic analyzer. You can write some code to convert the block to hex and print to the UART, for now I’m just going to show you the logic analyser, because we can directly tap into the MISO line and check the block bytes being sent back by the SD card, which will be a solid confirmation of the protocol implementation. So to test this my main code will have this snippet, you’ll have to make sure SD_BLOCK_SIZE is in the sd_card.h file or main to make it publicly accessible.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="c1">// SD Card - Init</span>
    <span class="kt">uint8_t</span> <span class="n">block</span><span class="p">[</span><span class="n">SD_BLOCK_SIZE</span><span class="p">];</span>
    <span class="n">sd_status_t</span> <span class="n">status</span> <span class="o">=</span> <span class="n">sd_card_init</span><span class="p">();</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">bsp_uart1_write_string</span><span class="p">(</span><span class="s">"SD init failed</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
        <span class="k">while</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">{}</span>
    <span class="p">}</span>

    <span class="c1">// SD Card - Read block 0</span>
    <span class="n">status</span> <span class="o">=</span> <span class="n">sd_card_cmd17_read_block</span><span class="p">(</span><span class="mi">0u</span><span class="p">,</span> <span class="n">block</span><span class="p">);</span>
    <span class="k">if</span><span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">bsp_uart1_write_string</span><span class="p">(</span><span class="s">"Read block 0 failed"</span><span class="p">);</span>
        <span class="k">while</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">{}</span>
    <span class="p">}</span>
    <span class="n">bsp_uart1_write_string</span><span class="p">(</span><span class="s">"Block read OK</span><span class="se">\r\n</span><span class="s">"</span><span class="p">);</span>
</code></pre></div></div>

<p>It’s a good sign if both pass and do not report an error over UART. I’ve also added a bit to print out the first 64 bytes of the block to check it’s not empty:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="c1">// Print block read on uart</span>
    <span class="n">debug_uart_print_hex_buffer</span><span class="p">(</span><span class="s">"BLOCK0[0:64]"</span><span class="p">,</span> <span class="n">block</span><span class="p">,</span> <span class="mi">64u</span><span class="p">);</span>
    <span class="k">while</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">{}</span>
</code></pre></div></div>

<p>The while loops just make it easier to read the UART and are not integral.</p>

<p>The UART output works but let’s get the logic analyser on it to take a look at the whole block and check the protocol sequence:</p>

<p><img src="/docs/assets/img/blog-16-scope-mosi.png" alt="MOSI line on scope" /></p>

<p>There is a lot here but a quick poke around shows everything seems to be in order, here is the initialisation sequence and the read block request, remember the command packet is 6 bytes, everything else is a dummy clock, here is a printout of the logic analyser decoding, I;’ve tried to split it up to make it easier to read:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> FF FF FF FF FF FF FF FF FF FF // 80 clock dummy bytes
 40 00 00 00 00 95             // CMD0 + CRC
 FF FF FF                      // Wait on CMD0
 48 00 00 01 AA 87             // CMD8
 FF FF FF FF FF FF FF          // Dummy blocks to read R1 + R7 
 77 00 00 00 00 FF             // CMD55
 FF FF FF                      // Dummy clocks
 69 40 00 00 00 FF             // ACMD41
 FF FF FF                      // Dummy clocks
 77 00 00 00 00 FF             // Repeated CMD55 + ACMD41 Loop
 FF FF FF 
 69 40 00 00 00 FF 
 FF FF FF 
 77 00 00 00 00 FF 
 FF FF FF 
 69 40 00 00 00 FF 
 FF FF FF 
 7A 00 00 00 00 FF 
 FF FF FF FF FF FF FF 
 51 00 00 00 00 FF             // CMD17 - Read block 0
 FF FF FF ... // Lots of FF to clock out the block data
</code></pre></div></div>

<p>The MOSI line shows the expected init sequence and then sends CMD17 for block 0. The actual proof of the read comes from MISO, where the card should return R1 = 0x00, then a 0xFE data token, then 512 bytes of block data followed by two CRC bytes:</p>

<p><img src="/docs/assets/img/blog-16-scope-miso.png" alt="MISO line on scope" /></p>

<p>Here is the decoded output:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FF FF FF FF FF FF FF FF 
FF FF FF FF FF FF FF FF FF 
01 
FF FF FF FF FF FF FF FF 
01 00 00 01 AA FF 
FF FF FF FF FF FF FF 
01 
FF FF FF FF FF FF FF FF 
01 
FF FF FF FF FF FF FF FF 
01 
FF FF FF FF FF FF FF FF 
01 
FF FF FF FF FF FF FF FF 
01 
FF FF FF FF FF FF FF FF 
00                               // ACMD41 R1 response ready 
FF FF FF FF FF FF FF FF 
00 C0 FF 80 00                   // CMD58 response R1 + OCR register
FF FF FF FF FF FF FF FF 
00                               // CMD17 R1 response - ready no error
FF FF FF FF FF FF FF FF FF FF FF 
FE                               // CMD17 Data token - 512 byte block starting

// 512 bytes here, I've split it into 16 columns 32 rows, and given it row numbering at the start to make it easier to parse through:

0 : FA 33 C0 8E D0 BC 00 7C 8B F4 50 07 50 1F FB FC 
1 : BF 00 06 B9 00 01 F2 A5 EA 1D 06 00 00 BE BE 07
2 : B3 04 80 3C 80 74 0E 80 3C 00 75 1C 83 C6 10 FE 
3 : CB 75 EF CD 18 8B 14 8B 4C 02 8B EE 83 C6 10 FE
4 : CB 74 1A 80 3C 00 74 F4 BE 8B 06 AC 3C 00 74 0B 
5 : 56 BB 07 00 B4 0E CD 10 5E EB F0 EB FE BF 05 00
6 : BB 00 7C B8 01 02 57 CD 13 5F 73 0C 33 C0 CD 13 
7 : 4F 75 ED BE A3 06 EB D3 BE C2 06 BF FE 7D 81 3D
8 : 55 AA 75 C7 8B F5 EA 00 7C 00 00 49 6E 76 61 6C 
9 : 69 64 20 70 61 72 74 69 74 69 6F 6E 20 74 61 62
10: 6C 65 00 45 72 72 6F 72 20 6C 6F 61 64 69 6E 67 
11: 20 6F 70 65 72 61 74 69 6E 67 20 73 79 73 74 65
12: 6D 00 4D 69 73 73 69 6E 67 20 6F 70 65 72 61 74 
13: 69 6E 67 20 73 79 73 74 65 6D 00 00 00 00 00 00
14: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
15: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
16: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
17: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
18: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
19: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
21: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
22: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
23: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
24: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
25: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
26: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
27: 00 00 00 00 00 00 00 00 E6 B2 46 D5 00 00 80 FE
28: FF FF 0B FE FF FF 50 8A 00 00 B0 FD A1 03 00 00 
29: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
31: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 AA 

E3 01                           // 2 CRC bytes
FF                              // Final deselect clock
</code></pre></div></div>

<p>The 512 byte block itself is not anything to do with the SD card protocol remember, this is just storage, I formatted this disk on a mac as FAT32. We read the first block on the card. If I smash this into an online hex to string converter we get:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ú3ÀÐ¼|ôPPûü¿¹ò¥ê¾¾³&lt;t&lt;uÆþËuïÍLîÆþËt&lt;tô¾¬&lt;tV»´Í^ëðëþ¿»|¸WÍ_s3ÀÍOuí¾£ëÓ¾Â¿þ}=UªuÇõê|Invalid partition tableError loading operating systemMissing operating systemæ²FÕþÿÿþÿÿP°ý¡Uªãÿæ²FÕþÿÿþÿÿP°ý¡Uª
</code></pre></div></div>

<p>There are some interesting lines there: “Invalid partition table, Error loading operating system, Missing operating system”. This is pretty cool, the first line indicates this likely is the start of a MBR style sector, the last two bytes of the block <code class="language-plaintext highlighter-rouge">55 AA</code> are interesting too from the <a href="https://wiki.osdev.org/MBR_%28x86%29?">MBR os dev wiki</a>; it says the last two bytes should be <code class="language-plaintext highlighter-rouge">0x55</code> then <code class="language-plaintext highlighter-rouge">0xAA</code> for a “Valid bootsector”. I’m fairly confident it is an MBR sector, but FAT boot sectors also end in <code class="language-plaintext highlighter-rouge">55 AA</code>.</p>

<p>At this point I had already proved the SD protocol <code class="language-plaintext highlighter-rouge">CMD17</code> returned a valid R1 response, a data token, 512 bytes, and two CRC bytes. Everything after this is no longer really SD card protocol. It is just the content stored inside those 512 byte sectors. I went a little further than planned here, because it was really interesting to explore the card a little. So if you just want to read about the write sequence then skip this rest of this post and go to the next one.</p>

<h1 id="walking-the-filesystem">Walking the filesystem</h1>

<p>This is beyond what the SD driver needs to understand. The driver only needs to return sectors. I am walking the filesystem manually here purely as a sanity check before adding FatFs, and because it is interesting.</p>

<p>So let’s try to decode it as a MBR anyway because the strings from the hex conversion earlier.</p>

<p>Here is the full MBR layout structure according to the osdev wiki, I’ve added an offset decimal conversion column to make it easier to use:</p>

<table>
  <thead>
    <tr>
      <th>Offset</th>
      <th>Decimal byte index</th>
      <th>Size (bytes)</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0x000</td>
      <td>0</td>
      <td>440</td>
      <td>MBR Bootstrap (flat binary executable code)</td>
    </tr>
    <tr>
      <td>0x1B8</td>
      <td>440</td>
      <td>4</td>
      <td>Optional “Unique Disk ID / Signature”</td>
    </tr>
    <tr>
      <td>0x1BC</td>
      <td>444</td>
      <td>2</td>
      <td>Optional, reserved 0x0000</td>
    </tr>
    <tr>
      <td>0x1BE</td>
      <td>446</td>
      <td>16</td>
      <td>First partition table entry</td>
    </tr>
    <tr>
      <td>0x1CE</td>
      <td>462</td>
      <td>16</td>
      <td>Second partition table entry</td>
    </tr>
    <tr>
      <td>0x1DE</td>
      <td>478</td>
      <td>16</td>
      <td>Third partition table entry</td>
    </tr>
    <tr>
      <td>0x1EE</td>
      <td>494</td>
      <td>16</td>
      <td>Fourth partition table entry</td>
    </tr>
    <tr>
      <td>0x1FE</td>
      <td>510</td>
      <td>2</td>
      <td>(0x55, 0xAA) “Valid bootsector” signature bytes</td>
    </tr>
  </tbody>
</table>

<p>The first partition table entry starts at offset 0x1BE = byte 446 and is 16 bytes long. In my capture, that region is:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>80 FE FF FF 0B FE FF FF 50 8A 00 00 B0 FD A1 03
</code></pre></div></div>
<p>The partition table entry format has this structure from the os dev wiki:</p>

<table>
  <thead>
    <tr>
      <th>Offset</th>
      <th>Offset byte (from start of table)</th>
      <th>Size (bytes)</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0x00</td>
      <td>0</td>
      <td>1</td>
      <td>Drive attributes (bit 7 set = active or bootable)</td>
    </tr>
    <tr>
      <td>0x01</td>
      <td>1</td>
      <td>3</td>
      <td>CHS Address of partition start</td>
    </tr>
    <tr>
      <td>0x04</td>
      <td>4</td>
      <td>1</td>
      <td>Partition type</td>
    </tr>
    <tr>
      <td>0x05</td>
      <td>5</td>
      <td>3</td>
      <td>CHS address of last partition sector</td>
    </tr>
    <tr>
      <td>0x08</td>
      <td>8</td>
      <td>4</td>
      <td>LBA of partition start</td>
    </tr>
    <tr>
      <td>0x0C</td>
      <td>12</td>
      <td>4</td>
      <td>Number of sectors in partition</td>
    </tr>
  </tbody>
</table>

<p>Let’s decode some of it (the LBA fields are little endian so the least significant bytes comes first):</p>

<table>
  <thead>
    <tr>
      <th>Description</th>
      <th>starting byte</th>
      <th>Hex value</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://en.wikipedia.org/wiki/Partition_type">Partition type</a></td>
      <td>Byte 4</td>
      <td>0B</td>
      <td>Fat32 with CHS addressing</td>
    </tr>
    <tr>
      <td>LBA partition start</td>
      <td>Byte 8</td>
      <td>50 8A 00 00</td>
      <td>Little endian decimal conversion: 35408</td>
    </tr>
    <tr>
      <td>Number of sectors</td>
      <td>Byte 12</td>
      <td>B0 FD A1 03</td>
      <td>Little endian decimal conversion: 60947888</td>
    </tr>
  </tbody>
</table>

<p>The partition type 0B can be found on the wiki and correctly identifies as a FAT32 partition. Also, if we take the number of sectors and multiply it by the block size we might get the card memory size because FAT32 sectors are normally 512 bytes: 60947888 * 512 = 31,205,318,656 bytes = 31.2 gigabytes! That sounds right, the card is a 32 GB card. If we convert to binary GiB we get 29.06 GiB which is exactly what F3 reported at the end of the last post.</p>

<p>Now we know where the first sector of the FAT32 volume is, let’s try to read that block, in main we will change <code class="language-plaintext highlighter-rouge">status = sd_card_cmd17_read_block(0u, block);</code> to <code class="language-plaintext highlighter-rouge">status = sd_card_cmd17_read_block(35408u, block);</code>.</p>

<p>Here it is:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FE 
EB 58 90 42 53 44 20 20 34 2E 34 00 02 20 20 00 
02 00 00 00 00 F8 00 00 20 00 FF 00 50 8A 00 00 
B0 FD A1 03 19 3A 00 00 00 00 00 00 02 00 00 00 
01 00 06 00 00 00 00 00 00 00 00 00 00 00 00 00 
80 00 29 04 1E 2E A2 49 4E 54 53 54 52 41 50 20 
20 20 46 41 54 33 32 20 20 20 FA 31 C0 8E D0 BC 
00 7C FB 8E D8 E8 00 00 5E 83 C6 19 BB 07 00 FC 
AC 84 C0 74 06 B4 0E CD 10 EB F5 30 E4 CD 16 CD 
19 0D 0A 4E 6F 6E 2D 73 79 73 74 65 6D 20 64 69 
73 6B 0D 0A 50 72 65 73 73 20 61 6E 79 20 6B 65 
79 20 74 6F 20 72 65 62 6F 6F 74 0D 0A 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
(...18 rows of 00s here) 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 AA 
1C B8 // CRC bytes
FF
</code></pre></div></div>

<p>Putting it into a hex to string converter:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ëXBSD  4.4  ø ÿP°ý¡:).¢INTSTRAP   FAT32   ú1ÀÐ¼|ûØè^Æ»ü¬Àt´Íëõ0äÍÍ
Non-system disk
Press any key to reboot
Uª
</code></pre></div></div>
<p>Hey we have FAT32 written, great news. Now I flicked through the <a href="https://academy.cba.mit.edu/classes/networking_communications/SD/FAT.pdf">Microsoft FAT specification</a> document but went back to the osdev wiki to find the decoding table, here is a reference table I’ve made from that to decode our block here:</p>

<table>
  <thead>
    <tr>
      <th>Offset</th>
      <th>Size</th>
      <th>Field</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0x0B</td>
      <td>2</td>
      <td>BPB_BytsPerSec</td>
      <td>512</td>
    </tr>
    <tr>
      <td>0x0D</td>
      <td>1</td>
      <td>BPB_SecPerClus</td>
      <td>32</td>
    </tr>
    <tr>
      <td>0x0E</td>
      <td>2</td>
      <td>BPB_RsvdSecCnt</td>
      <td>32</td>
    </tr>
    <tr>
      <td>0x10</td>
      <td>1</td>
      <td>BPB_NumFATs</td>
      <td>2</td>
    </tr>
    <tr>
      <td>0x1C</td>
      <td>4</td>
      <td>BPB_HiddSec</td>
      <td>35408</td>
    </tr>
    <tr>
      <td>0x20</td>
      <td>4</td>
      <td>BPB_TotSec32</td>
      <td>60947888</td>
    </tr>
    <tr>
      <td>0x24</td>
      <td>4</td>
      <td>BPB_FATSz32</td>
      <td>14873</td>
    </tr>
    <tr>
      <td>0x2C</td>
      <td>4</td>
      <td>BPB_RootClus</td>
      <td>2</td>
    </tr>
    <tr>
      <td>0x30</td>
      <td>2</td>
      <td>BPB_FSInfo</td>
      <td>1</td>
    </tr>
    <tr>
      <td>0x32</td>
      <td>2</td>
      <td>BPB_BkBootSec</td>
      <td>6</td>
    </tr>
    <tr>
      <td>0x52</td>
      <td>8</td>
      <td>BS_FilSysType</td>
      <td>“FAT32   “</td>
    </tr>
    <tr>
      <td>0x1FE</td>
      <td>2</td>
      <td>Signature</td>
      <td>55 AA</td>
    </tr>
  </tbody>
</table>

<p>Everything seems to line up here so I’m convinced now we are following the FAT32 links. One cool thing we can derive from this is the sectors per cluster = 32, so a cluster size = 32*512 = 16 KiB. Finally now with this if we use the BPB_RootClus value we find where the root directory starts. So to find the first data sector:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>first_data_sector = partition_table_lba 
                    + reserved_sector_count 
                    + number_of_fats * fat_size_32
                  = 35408 +32 + 2*14873 
                  = 65186
</code></pre></div></div>

<p>Okay so if we have a freshly formatted card and we try to put a small ASCII text file on the card and we might be able to spot it in the first data sector if we are lucky. I used the following terminal commands to try to write a simple file and possibly avoid the extra mac rubbish it puts on removable media:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> /Volumes/SDCARD
<span class="nb">printf</span> <span class="s2">"HELLO_FROM_SKOOPSY</span><span class="se">\r\n</span><span class="s2">"</span> <span class="o">&gt;</span> HELLO.TXT
<span class="nb">sync</span>
</code></pre></div></div>
<p>That should make a HELLO.TXT file with HELLO_FROM_SKOOPSY inside. Now change the read line in main:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">status</span> <span class="o">=</span> <span class="n">sd_card_cmd17_read_block</span><span class="p">(</span><span class="mi">65186u</span><span class="p">,</span> <span class="n">block</span><span class="p">);</span>
</code></pre></div></div>

<p>Here is the MISO decoding:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FE 
49 4E 54 53 54 52 41 50 20 20 20 28 00 00 00 00 
00 00 00 00 00 00 C8 63 D1 5C 00 00 00 00 00 00 
42 30 00 30 00 00 00 FF FF FF FF 0F 00 21 FF FF 
FF FF FF FF FF FF FF FF FF FF 00 00 FF FF FF FF 
01 2E 00 53 00 70 00 6F 00 74 00 0F 00 21 6C 00 
69 00 67 00 68 00 74 00 2D 00 00 00 56 00 31 00 
53 50 4F 54 4C 49 7E 31 20 20 20 12 00 71 46 B3 
CF 5C CF 5C 00 00 46 B3 CF 5C 03 00 00 00 00 00 
E5 2E 00 66 00 73 00 65 00 76 00 0F 00 DA 65 00 
6E 00 74 00 73 00 64 00 00 00 00 00 FF FF FF FF 
E5 53 45 56 45 4E 7E 31 20 20 20 12 00 75 46 B3 
CF 5C CF 5C 00 00 46 B3 CF 5C 06 00 00 00 00 00 
48 45 4C 4C 4F 20 20 20 54 58 54 20 00 95 84 63 
D1 5C D1 5C 00 00 84 63 D1 5C B6 00 14 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
(... 16 rows of 00s here)
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
7A 23 
FF
</code></pre></div></div>

<p>Then putting that through a hex to string converter we get:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>INTSTRAP   (ÈcÑ\B00ÿÿÿÿ!ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ.Spot!light-V1SPOTLI~1   qF³Ï\Ï\F³Ï\å.fsevÚentsdÿÿÿÿåSEVEN~1   uF³Ï\Ï\F³Ï\HELLO   TXT cÑ\Ñ\cÑ\¶
</code></pre></div></div>

<p>INTSRAP is the name I gave to the newly formatted drive. Then we can see MacOS has inevitably added some files to the storage such as <code class="language-plaintext highlighter-rouge">.Spotlight-V100</code> and <code class="language-plaintext highlighter-rouge">.fseventsd</code> and then finally we see the ASCII text file name that we added <code class="language-plaintext highlighter-rouge">HELLO   TXT</code>! This is at row 13: <code class="language-plaintext highlighter-rouge">48 45 4C 4C 4F 20 20 20 54 58 54</code>, the full 32 byte entry is:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>48 45 4C 4C 4F 20 20 20 54 58 54 20 00 95 84 63 
D1 5C D1 5C 00 00 84 63 D1 5C B6 00 14 00 00 00
</code></pre></div></div>

<p>The osdev wiki comes to the rescue again with the table for the <a href="http://wiki.osdev.org/FAT#Standard_8.3_format">FAT Standard 8.3 format</a>.</p>

<table>
  <thead>
    <tr>
      <th>Offset</th>
      <th>Size</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>11</td>
      <td>Filename, padded with spaces</td>
    </tr>
    <tr>
      <td>+11</td>
      <td>1</td>
      <td>Attributes</td>
    </tr>
    <tr>
      <td>+12</td>
      <td>1</td>
      <td>Reserved / NT case info</td>
    </tr>
    <tr>
      <td>+13</td>
      <td>1</td>
      <td>Creation time fine resolution</td>
    </tr>
    <tr>
      <td>+14</td>
      <td>2</td>
      <td>Creation time</td>
    </tr>
    <tr>
      <td>+16</td>
      <td>2</td>
      <td>Creation date</td>
    </tr>
    <tr>
      <td>+18</td>
      <td>2</td>
      <td>Last access date</td>
    </tr>
    <tr>
      <td>+20</td>
      <td>2</td>
      <td>First cluster high word</td>
    </tr>
    <tr>
      <td>+22</td>
      <td>2</td>
      <td>Last modified time</td>
    </tr>
    <tr>
      <td>+24</td>
      <td>2</td>
      <td>Last modified date</td>
    </tr>
    <tr>
      <td>+26</td>
      <td>2</td>
      <td>First cluster low word</td>
    </tr>
    <tr>
      <td>+28</td>
      <td>4</td>
      <td>File size in bytes</td>
    </tr>
  </tbody>
</table>

<p>So for the file name and extension we have:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>48 45 4C 4C 4F 20 20 20   54 58 54
H  E  L  L  O  sp sp sp   T  X  T
</code></pre></div></div>

<p>The first cluster is <code class="language-plaintext highlighter-rouge">00 00</code> for the high word, and <code class="language-plaintext highlighter-rouge">B6 00</code> for the low word. Both are little endian so the first cluster is at 182. The file size at +28: <code class="language-plaintext highlighter-rouge">14 00 00 00</code>, again little endian: <code class="language-plaintext highlighter-rouge">0x00000014</code> = 20 bytes. Now we can work out where the file data is:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>first_data_sector. = 65186
sectors_per_cluster = 32

first_lba = first_data_sector + (first_cluster - 2) * sectors_per_cluster
</code></pre></div></div>

<p>So for the values we have:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>file_lba = 65186 + (182 -2) * 32 = 70946
</code></pre></div></div>

<p>Let’s check it out:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>48 45 4C 4C 4F 5F 46 52 4F 4D 5F 53 4B 4F 4F 50 
53 59 0D 0A 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
(... 28 rows of 00s here)
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
76 46 FF
</code></pre></div></div>

<p>Okay so most of this block is empty, but it is good to see the two CRCs at the end and the final dummy FF. If we copy the first populated values from the block: <code class="language-plaintext highlighter-rouge">48 45 4C 4C 4F 5F 46 52 4F 4D 5F 53 4B 4F 4F 50 53 59 0D 0A </code> into a hex to string converter we get <code class="language-plaintext highlighter-rouge">HELLO_FROM_SKOOPSY</code>, brilliant!</p>

<p>This was far more filesystem decoding than I originally planned for a raw block read test, but it proved the important point. The driver can read arbitrary 512 byte sectors from the card. Starting from block 0, I could follow the MBR to the FAT32 boot sector, follow the BPB to the root directory, find a file written by macOS, decode its first cluster, and finally read the actual file contents.</p>

<script src="https://utteranc.es/client.js" repo="skoopsy/skoopsy.github.io" issue-term="pathname" label="blog-embedded13" theme="preferred-color-scheme" crossorigin="anonymous" async="">
</script>

<p>Copyright © 2026 David O’Connor</p>]]></content><author><name>Dave</name></author><category term="STM32" /><summary type="html"><![CDATA[The last post finally finished the SD card initialisation sequence. The card left idle state using CMD55 + ACMD41, CMD58 returned a valid OCR value, and I used CMD9 and CMD10 to read the CSD and CID registers. This proves a few core foundations:]]></summary></entry><entry><title type="html">STM32 #13: Finishing microSD initialisation and asking for its birthday</title><link href="https://skoopsy.dev/stm32/2026/06/14/STM32-14-sd-card-init-finish.html" rel="alternate" type="text/html" title="STM32 #13: Finishing microSD initialisation and asking for its birthday" /><published>2026-06-14T21:00:00+00:00</published><updated>2026-06-14T21:00:00+00:00</updated><id>https://skoopsy.dev/stm32/2026/06/14/STM32-14-sd-card-init-finish</id><content type="html" xml:base="https://skoopsy.dev/stm32/2026/06/14/STM32-14-sd-card-init-finish.html"><![CDATA[<p><a href="https://skoopsy.dev/stm32/2026/06/09/STM32-13-sd-card-transport.html">Last time</a> I managed to get the STM32F103x talking to a microSD card over SPI. The card responded correctly to <code class="language-plaintext highlighter-rouge">CMD0</code> and <code class="language-plaintext highlighter-rouge">CMD8</code>, which showed that the wiring, SPI mode, CMD packet format, dummy clocking, and R1/R7 response handling were functional. However I did not finish the initialisation process so let’s do that now. This post will finish the initialisation process and ask it for some information about itself!</p>

<p>Where we stopped:</p>

<ul>
  <li>CMD0 response = 01 - Card entered idle state</li>
  <li>CMD8 response = 01 00 00 01 AA - Card accepted the voltage/check pattern command</li>
</ul>

<p>The remaining sequence to complete initialisation is:</p>
<ul>
  <li>CMD55 + ACMD41 repeatedly until R1 = 0x00</li>
  <li>CMD58 to read the OCR</li>
  <li>Check the CCS bit to detect SDHC/SDXC</li>
  <li>Increase the SPI speed (Optional)</li>
</ul>

<p>After that, the card is ready for data token type reads. Before jumping into CMD17 (not in this post), I’m going to use <code class="language-plaintext highlighter-rouge">CMD9</code> and <code class="language-plaintext highlighter-rouge">CMD10</code> to read the CSD and CID registers.</p>

<h1 id="cmd55-and-acmd41">CMD55 and ACMD41</h1>

<p><code class="language-plaintext highlighter-rouge">ACMD41</code> is the command used to ask the SD card to finish initialising. It is an “application specific” command, so it is not sent by itself, you have to send <code class="language-plaintext highlighter-rouge">CMD55</code> first, which tells the card an application specific command is coming next.</p>

<p>The sequence is:</p>

<ul>
  <li>CMD55</li>
  <li>ACMD41</li>
  <li>Check R1 response</li>
  <li>Repeat until R1 = 0x00 (Ready, no error flag)</li>
</ul>

<p>For SD v2 cards including SDHC and SDXC,<code class="language-plaintext highlighter-rouge">ACMD41</code> should set the High Capacity Support (HCS) bit:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define SD_ACMD41_HCS 0x40000000u
</span></code></pre></div></div>
<p>The HCS bit is not the card’s capacity, that is reported later with the CCS bit, this is just to tell the card that the host can support HCS.</p>

<p>I’m going to create some new macros to add to our existing code from the last post to make the code a bit more meaningful rather than having raw values in the main code block:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define SD_CMD55 55u // Send application command
#define SD_ACMD41 41u // Initialise command
#define SD_CMD58 58u // Read OCR register
</span>
<span class="cp">#define SD_DUMMY_CRC 0xFFu
</span>
<span class="cp">#define SD_ACMD41_HCS 0x40000000u
#define SD_OCR_CCS 0x40000000u
</span>
<span class="cp">#define SD_R1_READY_STATE 0x00u
#define SD_R1_VALID_MASK 0x80u
</span>
<span class="cp">#define SD_BLOCK_SIZE 512u
#define SD_DATA_TOKEN 0xFEu
</span>
<span class="cp">#define SD_ACMD41_ATTEMPTS 10000u
#define SD_DATA_TOKEN_ATTEMPTS 100000u
</span></code></pre></div></div>

<h1 id="tracking-the-card-type">Tracking the card type</h1>
<p>The driver needs to remember whether the card uses block or byte addressing. For SDHC and SDXC cards, the address passed to <code class="language-plaintext highlighter-rouge">CMD17</code> is a 512 byte block number. For SDSC cards, the address needs to be a byte address. So we will make a card type enum:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">enum</span> <span class="p">{</span>
    <span class="n">SD_CARD_TYPE_UNKNOWN</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
    <span class="n">SD_CARD_TYPE_SDSC</span><span class="p">,</span>
    <span class="n">SD_CARD_TYPE_SDHC_OR_SDXC</span><span class="p">,</span>
<span class="p">}</span> <span class="n">sd_card_type_t</span><span class="p">;</span>

<span class="k">static</span> <span class="n">sd_card_type_t</span> <span class="n">sd_card_type</span> <span class="o">=</span> <span class="n">SD_CARD_TYPE_UNKNOWN</span><span class="p">;</span>
</code></pre></div></div>

<h1 id="waiting-for-acmd41-to-complete">Waiting for ACMD41 to complete</h1>
<p>The next function to build will repeatedly send CMD55 + ACMD41 until the card leaves idle.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">sd_status_t</span> <span class="nf">sd_wait_ready_cmd55_acmd41</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">uint8_t</span> <span class="n">r1</span> <span class="o">=</span> <span class="mh">0xFFu</span><span class="p">;</span>

    <span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">i</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span> <span class="n">i</span><span class="o">&lt;</span><span class="n">SD_ACMD41_ATTEMPTS</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">sd_status_t</span> <span class="n">status</span><span class="p">;</span>
    
        <span class="c1">// Send CMD55 - Prefix before application specific command</span>
        <span class="n">status</span> <span class="o">=</span> <span class="n">sd_send_command</span><span class="p">(</span><span class="n">SD_CMD55</span><span class="p">,</span>
                                <span class="mh">0x00000000u</span><span class="p">,</span>
                                <span class="n">SD_DUMMY_CRC</span><span class="p">,</span>
                                <span class="o">&amp;</span><span class="n">r1</span><span class="p">,</span>
                                <span class="nb">NULL</span><span class="p">,</span>
                                <span class="mi">0u</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">status</span><span class="p">;</span> <span class="p">}</span>
        <span class="k">if</span> <span class="p">((</span><span class="n">r1</span> <span class="o">&amp;</span> <span class="n">SD_R1_ILLEGAL_COMMAND</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">0u</span><span class="p">)</span> <span class="p">{</span> 
            <span class="k">return</span> <span class="n">SD_ERR_UNSUPPORTED_CARD</span><span class="p">;</span> 
        <span class="p">}</span>
        <span class="k">if</span> <span class="p">((</span><span class="n">r1</span> <span class="o">!=</span> <span class="n">SD_R1_IDLE_STATE</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="n">r1</span> <span class="o">!=</span> <span class="n">SD_R1_READY_STATE</span><span class="p">))</span> <span class="p">{</span> 
            <span class="k">return</span> <span class="n">SD_ERR_BAD_RESPONSE</span><span class="p">;</span> 
        <span class="p">}</span>

        <span class="c1">// SEND ACMD41 - Application specific, Initialise card, with HCS bit set</span>
        <span class="n">status</span> <span class="o">=</span> <span class="n">sd_send_command</span><span class="p">(</span><span class="n">SD_ACMD41</span><span class="p">,</span>
                                 <span class="n">SD_ACMD41_HCS</span><span class="p">,</span>
                                 <span class="n">SD_DUMMY_CRC</span><span class="p">,</span>
                                 <span class="o">&amp;</span><span class="n">r1</span><span class="p">,</span>
                                 <span class="nb">NULL</span><span class="p">,</span>
                                 <span class="mi">0u</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">status</span><span class="p">;</span> <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">r1</span> <span class="o">==</span> <span class="n">SD_R1_READY_STATE</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_OK</span><span class="p">;</span> <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">r1</span> <span class="o">!=</span> <span class="n">SD_R1_IDLE_STATE</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_ERR_BAD_RESPONSE</span><span class="p">;</span> <span class="p">}</span>
    
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">SD_ERR_TIMEOUT</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The loop treats SD_R1_IDLE_STATE as acceptable while the card is initialising. If the card returns SD_R1_READY_STATE the function returns SD_OK. If the card reports an illegal command it returns SD_ERR_UNSUPPORTED_CARD.</p>

<p>For the SDHC card I am using, ACMD41 should be supported, so an illegal command here would be a strong hint that something is wrong with the command sequence or the card is a different type.</p>

<p>The timeout values are arbitrary at this stage. It is there to stop the driver getting stuck if the card never leaves idle state.</p>

<h1 id="reading-the-ocr-register-with-cmd58">Reading the OCR register with CMD58</h1>

<p>Once ACMD41 completes successfully, the card should be ready. So now we will build a function to read the Operating Conditions Register (OCR).</p>

<p>In SPI mode, CMD58 should return an R3 response: R1 (1 byte) + OCR (4 bytes).</p>

<p>The sd_send_command() function already supports this because it can read an optional response tail after the first R1 byte. Let’s build the function:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">sd_status_t</span> <span class="nf">sd_cmd58_read_ocr</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="o">*</span><span class="n">ocr</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">ocr</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_ERR_PARAM</span><span class="p">;</span> <span class="p">}</span>

    <span class="kt">uint8_t</span> <span class="n">r1</span> <span class="o">=</span> <span class="mh">0xFFu</span><span class="p">;</span>
    <span class="kt">uint8_t</span> <span class="n">r3_ocr</span><span class="p">[</span><span class="mi">4</span><span class="p">];</span>

    <span class="n">sd_status_t</span> <span class="n">status</span> <span class="o">=</span> <span class="n">sd_send_command</span><span class="p">(</span><span class="n">SD_CMD58</span><span class="p">,</span>
                                         <span class="mh">0x00000000u</span><span class="p">,</span>
                                         <span class="n">SD_DUMMY_CRC</span><span class="p">,</span>
                                         <span class="o">&amp;</span><span class="n">r1</span><span class="p">,</span>
                                         <span class="n">r3_ocr</span><span class="p">,</span>
                                         <span class="k">sizeof</span><span class="p">(</span><span class="n">r3_ocr</span><span class="p">));</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">status</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">r1</span> <span class="o">!=</span> <span class="n">SD_R1_READY_STATE</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_ERR_BAD_RESPONSE</span><span class="p">;</span> <span class="p">}</span>

    <span class="c1">// Split the OCR register into individual bytes</span>
    <span class="o">*</span><span class="n">ocr</span> <span class="o">=</span> <span class="p">((</span><span class="kt">uint32_t</span><span class="p">)</span><span class="n">r3_ocr</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">&lt;&lt;</span> <span class="mi">24</span><span class="p">)</span> <span class="o">|</span>
           <span class="p">((</span><span class="kt">uint32_t</span><span class="p">)</span><span class="n">r3_ocr</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">&lt;&lt;</span> <span class="mi">16</span><span class="p">)</span> <span class="o">|</span>
           <span class="p">((</span><span class="kt">uint32_t</span><span class="p">)</span><span class="n">r3_ocr</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">&lt;&lt;</span> <span class="mi">8</span><span class="p">)</span> <span class="o">|</span>
           <span class="p">((</span><span class="kt">uint32_t</span><span class="p">)</span><span class="n">r3_ocr</span><span class="p">[</span><span class="mi">3</span><span class="p">]);</span>

    <span class="k">return</span> <span class="n">SD_OK</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Like before the OCR bytes are returned most significant byte first, so the code rebuilds the 32 bit register by bit shifting left each into uhhh… the right position.</p>

<p>The Card Capacity Status (CCS) bit is key here, if it is set then the card is SDHC or SDXC and uses block addressing. If CCS is clear, then the card is an SDSC and uses byte addressing.</p>

<h1 id="updating-sd_card_init">Updating sd_card_init()</h1>

<p>Now we can finish the init function we started in the last post by adding the functions we have just built to the bottom of the init:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// CMD55 &amp; ACMD41</span>
    <span class="n">status</span> <span class="o">=</span> <span class="n">sd_wait_ready_cmd55_acmd41</span><span class="p">();</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span>  <span class="p">{</span> <span class="k">return</span> <span class="n">status</span><span class="p">;</span> <span class="p">}</span>

    <span class="c1">// Read OCR CMD58</span>
    <span class="kt">uint32_t</span> <span class="n">ocr</span> <span class="o">=</span> <span class="mi">0u</span><span class="p">;</span>
    <span class="n">status</span> <span class="o">=</span> <span class="n">sd_cmd58_read_ocr</span><span class="p">(</span><span class="o">&amp;</span><span class="n">ocr</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">status</span><span class="p">;</span> <span class="p">}</span>
    
    <span class="c1">// Check CCS bit and set card type</span>
    <span class="k">if</span> <span class="p">((</span><span class="n">ocr</span> <span class="o">&amp;</span> <span class="n">SD_OCR_CCS</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">0u</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">sd_card_type</span> <span class="o">=</span> <span class="n">SD_CARD_TYPE_SDHC_OR_SDXC</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="n">sd_card_type</span> <span class="o">=</span> <span class="n">SD_CARD_TYPE_SDSC</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// Set SPI speed to fast</span>
    <span class="n">spi_status</span> <span class="o">=</span> <span class="n">bsp_spi1_set_fast</span><span class="p">();</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">spi_status</span> <span class="o">!=</span> <span class="n">BSP_SPI_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_ERR_SPI</span><span class="p">;</span> <span class="p">}</span>
</code></pre></div></div>

<h1 id="testing-the-full-sd_card_init-sequence">Testing the full sd_card_init() sequence</h1>

<p>Now we can have a crack at running the full init and checking on the scope, if you don’t have a scope then you’ll need to write some <a href="https://skoopsy.dev/stm32/2025/09/14/STM32-7-uart.html">UART logging</a> to a terminal or something.</p>

<p>The first byte of each command packet has the form <code class="language-plaintext highlighter-rouge">01xxxxxx</code>, where the lower six bits contain the command index. This is why <code class="language-plaintext highlighter-rouge">CMD58</code> becomes <code class="language-plaintext highlighter-rouge">0x40 | 0x3A = 0x7A</code>. For the logic analyser results to make sense we will create a conversion table for the commands:</p>

<table>
  <thead>
    <tr>
      <th>CMD</th>
      <th>Unsigned Int</th>
      <th>6 bit hex conversion with prepend</th>
      <th>Hex result</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CMD0</td>
      <td>0u</td>
      <td>0x40 | 0x00</td>
      <td>0x40</td>
    </tr>
    <tr>
      <td>CMD8</td>
      <td>8u</td>
      <td>0x40 | 0x08</td>
      <td>0x48</td>
    </tr>
    <tr>
      <td>ACMD41</td>
      <td>41u</td>
      <td>0x40 | 0x29</td>
      <td>0x69</td>
    </tr>
    <tr>
      <td>CMD55</td>
      <td>55u</td>
      <td>0x40 | 0x37</td>
      <td>0x77</td>
    </tr>
    <tr>
      <td>CMD58</td>
      <td>58u</td>
      <td>0x40 | 0x3A</td>
      <td>0x7A</td>
    </tr>
  </tbody>
</table>

<p>Let’s get a capture on the MOSI line first:</p>

<p><img src="/docs/assets/img/blog-15-scope-mosi-full.png" alt="Oscilloscope MOSI Full" /></p>

<p>There is a lot going on there so let’s zoom in to the start:
<img src="/docs/assets/img/blog-15-scope-mosi-0.png" alt="Oscilloscope MOSI segment 0" /></p>

<p>Here we see the sequence we have covered in the last post so everything looks good so far, let’s move onto the next segment
<img src="/docs/assets/img/blog-15-scope-mosi-1.png" alt="Oscilloscope MOSI segment 1" /></p>

<p>Brilliant, we can see the repeating sequence:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">77 00 00 00 00 FF</code> = CMD55</li>
  <li><code class="language-plaintext highlighter-rouge">69 40 00 00 00 FF</code> = ACMD41</li>
</ul>

<p>Then if we scroll forward we see:
<img src="/docs/assets/img/blog-15-scope-mosi-2.png" alt="Oscilloscope MOSI segment 2" /></p>

<p>We have another repeat of CMD55 + ACMD41, then we move onto:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">7A 00 00 00 00 FF</code> = CMD58</li>
</ul>

<p>Brilliant so it looks like the last command in the init sequence was run on the MOSI, let’s check the responses on the MISO line:
<img src="/docs/assets/img/blog-15-scope-miso-full.png" alt="Oscilloscope MISO Full" /></p>

<p>Zooming into the first segment:
<img src="/docs/assets/img/blog-15-scope-miso-0.png" alt="Oscilloscope MISO segment 0" />
Here we can see:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">01</code> = Response to CMD0 - Card Idle</li>
  <li><code class="language-plaintext highlighter-rouge">01 00 00 01 AA FF</code> =  Response to CMD8</li>
</ul>

<p>scrolling along:
<img src="/docs/assets/img/blog-15-scope-miso-1.png" alt="Oscilloscope MISO segment 1" /></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">01</code> = Response to ACMD41 - Card still idle</li>
</ul>

<p>Then finally we have:
<img src="/docs/assets/img/blog-15-scope-miso-2.png" alt="Oscilloscope MISO segment 2" /></p>

<p>First we have another response to ACMD41. Then finally we see:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">00</code> = ACMD41 response - Card ready</li>
  <li><code class="language-plaintext highlighter-rouge">00 C0 FF 80 00</code> = CMD58 response R1 + OCR  value</li>
</ul>

<p>Converting the 32 bit hex OCR value to binary we get: 11000000 11111111 10000000 00000000 
The OCR is telling us:</p>

<ul>
  <li>bit 31 = 1 Power up initialisation complete</li>
  <li>bit 30 = 1 which indicates the CCS is set so SDHC or SDXC.</li>
  <li>bit 29 = 0 UHS-II is not indicated</li>
  <li>bit 24 = 0 1.8v switching not accepted</li>
  <li>bit 23:15 = all 1s so the card supports the full 2.7-3.6v operating range</li>
  <li>bit 14:0 reserved</li>
</ul>

<p>The initialisation appears to have been a success and we can account for all the results on the MISO line mostly (Confidence would be closer to 100% if I could have had the CS line on the logic analyser. Alas, I must work with 2 probes for now.)</p>

<h1 id="lets-have-a-chat-with-the-card">Let’s have a chat with the card</h1>

<p>There are some other commands you can use to find out more about the card once it is initialised:</p>

<table>
  <thead>
    <tr>
      <th>CMD</th>
      <th>Purpose</th>
      <th>Decimal</th>
      <th>6 bit Hex conversion prepended</th>
      <th>Resulting Hex</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CMD9</td>
      <td>Read Card Specific Data (CSD)</td>
      <td>9u</td>
      <td>0x40 | 0x9</td>
      <td>0x49</td>
    </tr>
    <tr>
      <td>CMD10</td>
      <td>Read Card Identification Data (CID)</td>
      <td>10u</td>
      <td>0x40 | 0xA</td>
      <td>0x4A</td>
    </tr>
  </tbody>
</table>

<p>The CSD register contains card capability information such as capacity encoding, read block length, and other low level characteristics. The CID register contains the manufacturer information such as manufacture ID, product name, revision, serial number, and manufacturing date.</p>

<p>Based on the code I’ve previously written I need to refactor a little here. The <code class="language-plaintext highlighter-rouge">sd_send_command()</code> function was developed for the initialisation sequence but the sequence for <code class="language-plaintext highlighter-rouge">CMD9</code> and <code class="language-plaintext highlighter-rouge">CMD10</code> are not quite compatible with it. The <code class="language-plaintext highlighter-rouge">CMD8</code> and <code class="language-plaintext highlighter-rouge">CMD58</code> sequence is below:</p>

<ol>
  <li>send command</li>
  <li>wait R1</li>
  <li>Read response tail immediately</li>
  <li>deselect</li>
</ol>

<p>Whereas the sequence for <code class="language-plaintext highlighter-rouge">CMD9</code> and <code class="language-plaintext highlighter-rouge">CMD10</code> is:</p>

<ol>
  <li>Send command</li>
  <li>Wait R1</li>
  <li>Wait for data token 0xFE</li>
  <li>Read 16 byte register payload</li>
  <li>Read 2 CRC bytes</li>
  <li>Deselect</li>
</ol>

<p>The current sd_send_command() function follows this sequence:</p>
<ol>
  <li>sd_select()</li>
  <li>Send packet</li>
  <li>Wait R1</li>
  <li>Read optional response_tail</li>
  <li>sd_deselect()</li>
</ol>

<p>So if we used CMD9 it would deselect the card before the card sends the <code class="language-plaintext highlighter-rouge">0xFE</code> data token and 16 byte CSD.</p>

<p>From sd_send_command() we will extract the command packet logic and put it inside its own function so that we don’t have to write it again for the CMD9 &amp; 10 logic:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">sd_status_t</span> <span class="nf">sd_write_command_packet</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="n">cmd</span><span class="p">,</span>
                                           <span class="kt">uint32_t</span> <span class="n">arg</span><span class="p">,</span>
                                           <span class="kt">uint8_t</span> <span class="n">crc</span><span class="p">)</span> <span class="p">{</span>

    <span class="c1">// Reject command if its greater than 6 bits</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">cmd</span> <span class="o">&gt;</span> <span class="n">SD_MAX_COMMAND_INDEX</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_ERR_PARAM</span><span class="p">;</span> <span class="p">}</span>

    <span class="c1">// 6 byte SD command packet</span>
    <span class="kt">uint8_t</span> <span class="n">packet</span><span class="p">[</span><span class="mi">6</span><span class="p">];</span>

    <span class="c1">// Sort each packet out</span>
    <span class="n">packet</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">SD_COMMAND_PREFIX</span> <span class="o">|</span> <span class="n">cmd</span><span class="p">;</span>
    <span class="n">packet</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint8_t</span><span class="p">)(</span><span class="n">arg</span> <span class="o">&gt;&gt;</span> <span class="mi">24</span><span class="p">);</span>
    <span class="n">packet</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint8_t</span><span class="p">)(</span><span class="n">arg</span> <span class="o">&gt;&gt;</span> <span class="mi">16</span><span class="p">);</span>
    <span class="n">packet</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint8_t</span><span class="p">)(</span><span class="n">arg</span> <span class="o">&gt;&gt;</span> <span class="mi">8</span><span class="p">);</span>
    <span class="n">packet</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint8_t</span><span class="p">)</span><span class="n">arg</span><span class="p">;</span>
    <span class="n">packet</span><span class="p">[</span><span class="mi">5</span><span class="p">]</span> <span class="o">=</span> <span class="n">crc</span><span class="p">;</span>

    <span class="n">bsp_spi_status_t</span> <span class="n">spi_status</span> <span class="o">=</span> <span class="n">bsp_spi1_write_buffer</span><span class="p">(</span><span class="n">packet</span><span class="p">,</span> 
                                                        <span class="k">sizeof</span><span class="p">(</span><span class="n">packet</span><span class="p">));</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">spi_status</span> <span class="o">!=</span> <span class="n">BSP_SPI_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_ERR_SPI</span><span class="p">;</span> <span class="p">}</span>

    <span class="k">return</span> <span class="n">SD_OK</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then the sd_send_command() will just have this for that section of logic:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">...</span>
<span class="n">sd_select</span><span class="p">();</span>

<span class="n">sd_status_t</span> <span class="n">status</span> <span class="o">=</span> <span class="n">sd_write_command_packet</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">arg</span><span class="p">,</span> <span class="n">crc</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">sd_deselect</span><span class="p">();</span>
    <span class="k">return</span> <span class="n">status</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">...</span>
</code></pre></div></div>

<p>We need to create a more universal wait sequence for the data token to be returned too:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">sd_status_t</span> <span class="nf">sd_wait_data_token</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">SD_DATA_TOKEN_ATTEMPTS</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">uint8_t</span> <span class="n">token</span> <span class="o">=</span> <span class="mh">0xFFu</span><span class="p">;</span>

        <span class="c1">// send dummy bytes until token is returned</span>
        <span class="n">bsp_spi_status_t</span> <span class="n">spi_status</span> <span class="o">=</span> <span class="n">bsp_spi1_transfer</span><span class="p">(</span><span class="mh">0xFFu</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">token</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">spi_status</span> <span class="o">!=</span> <span class="n">BSP_SPI_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_ERR_SPI</span><span class="p">;</span> <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">token</span> <span class="o">==</span> <span class="n">SD_DATA_TOKEN</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_OK</span><span class="p">;</span> <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">token</span> <span class="o">!=</span> <span class="mh">0xFFu</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_ERR_BAD_RESPONSE</span><span class="p">;</span> <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">SD_ERR_TIMEOUT</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Let’s create some macros for the two new commands and register sizes:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define SD_CMD9 9u
#define SD_CMD10 10u
</span>
<span class="cp">#define SD_REG_SIZE 16u
#define SD_CRC_SIZE 2u
</span></code></pre></div></div>
<p>Now we can make a separate function to read the specific register for CMD9 and CMD10 response:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sd_status_t</span> <span class="nf">sd_read_register</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="n">cmd</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">buffer</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">buffer</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_ERR_PARAM</span><span class="p">;</span> <span class="p">}</span>

    <span class="kt">uint8_t</span> <span class="n">r1</span> <span class="o">=</span> <span class="mh">0xFFu</span><span class="p">;</span>

    <span class="c1">// Write the CMD</span>
    <span class="n">sd_select</span><span class="p">();</span>
    <span class="n">sd_status_t</span> <span class="n">status</span> <span class="o">=</span> <span class="n">sd_write_command_packet</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span>
                                                 <span class="mh">0x00000000u</span><span class="p">,</span>
                                                 <span class="n">SD_DUMMY_CRC</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">sd_deselect</span><span class="p">();</span>
        <span class="k">return</span> <span class="n">status</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// Wait for and read response</span>
    <span class="n">status</span> <span class="o">=</span> <span class="n">sd_wait_r1</span><span class="p">(</span><span class="o">&amp;</span><span class="n">r1</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">sd_deselect</span><span class="p">();</span>
        <span class="k">return</span> <span class="n">status</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">r1</span> <span class="o">!=</span> <span class="n">SD_R1_READY_STATE</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">sd_deselect</span><span class="p">();</span>
        <span class="k">return</span> <span class="n">SD_ERR_BAD_RESPONSE</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// Wait for data token</span>
    <span class="n">status</span> <span class="o">=</span> <span class="n">sd_wait_data_token</span><span class="p">();</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">sd_deselect</span><span class="p">();</span>
        <span class="k">return</span> <span class="n">status</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// read the data token buffer using function developed in the last post</span>
    <span class="n">status</span> <span class="o">=</span> <span class="n">sd_read_bytes</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="n">SD_REG_SIZE</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">sd_deselect</span><span class="p">();</span>
        <span class="k">return</span> <span class="n">status</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// Read CRC</span>
    <span class="kt">uint8_t</span> <span class="n">crc</span><span class="p">[</span><span class="n">SD_CRC_SIZE</span><span class="p">];</span>
    <span class="n">status</span> <span class="o">=</span> <span class="n">sd_read_bytes</span><span class="p">(</span><span class="n">crc</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">crc</span><span class="p">));</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">sd_deselect</span><span class="p">();</span>
        <span class="k">return</span> <span class="n">status</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">sd_deselect</span><span class="p">();</span>
    <span class="k">return</span> <span class="n">SD_OK</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now we can just make custom wrappers for <code class="language-plaintext highlighter-rouge">CMD9</code> and <code class="language-plaintext highlighter-rouge">CMD10</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sd_status_t</span> <span class="nf">sd_card_cmd9_read_csd</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="n">csd</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">sd_read_register</span><span class="p">(</span><span class="n">SD_CMD9</span><span class="p">,</span> <span class="n">csd</span><span class="p">);</span>
<span class="p">}</span>

<span class="n">sd_status_t</span> <span class="nf">sd_card_cmd10_read_cid</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="n">cid</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">sd_read_register</span><span class="p">(</span><span class="n">SD_CMD10</span><span class="p">,</span> <span class="n">cid</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now in our main we can call sd_card_init(), then sd_cmd9_read_csd() and sd_cmd10_read_cid():</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">sd_status_t</span> <span class="n">sd_status</span> <span class="o">=</span> <span class="n">sd_card_init</span><span class="p">();</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">sd_status</span> <span class="o">==</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">uint8_t</span> <span class="n">csd</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span>
        <span class="n">sd_card_cmd9_read_csd</span><span class="p">(</span><span class="n">csd</span><span class="p">);</span>
    
        <span class="kt">uint8_t</span> <span class="n">cid</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span>
        <span class="n">sd_card_cmd10_read_cid</span><span class="p">(</span><span class="n">cid</span><span class="p">);</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>Okay so before I show you the scope traces I want to say its not all perfect, at this point I had to spend some time trying to understand why my scope trace was wrong… it was sending CMD9 and CMD10 one after the other without waiting for a response because I had not added my wait_r1() function into the refactored send_command_packet() function. The scope trace was showing CMD9 and CMD10 being sent on the MOSI but nothing on the MISO. So the scope/logic analyser is great for a quick analysis to understand what went wrong… Now back to how the traces should look:</p>

<p>For the MOSI line:</p>

<p><img src="/docs/assets/img/blog-15-scope-cmd9-10-mosi-full.png" alt="Oscilloscope MOSI CMD9&amp;10 Full" /></p>

<p>You can just about see here we have the sd_card_init() sequence, then a pause on the clock line, then most commands, and they appear to be at a faster clock rate, which is great because the last thing in the sd_card_init() function is to switch to the SPI high speed setting. Let’s zoom in to the data after the initialisation sequence at the end and take a look:</p>

<p><img src="/docs/assets/img/blog-15-scope-cmd9-10-mosi-zoom.png" alt="Oscilloscope MOSI CMD9&amp;10 zoom" /></p>

<p>Firstly manually measuring the clock frequency shows 556 kHz, it should be closer to 562.5 kHz theoretically but we will leave that discrepancy for now. If I take a longer measurement across the whole segment it measures 558.43 kHz which is closer but not quite there.</p>

<p>Importantly we see the following packets:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">49 00 00 00 00 FF</code> = <code class="language-plaintext highlighter-rouge">CMD9</code> + FF from chip deselect</li>
  <li><code class="language-plaintext highlighter-rouge">4A 00 00 00 00 FF</code> = <code class="language-plaintext highlighter-rouge">CMD10</code> + FF from chip deselect</li>
</ul>

<p>With some large waiting gaps in between, let’s check out the MISO:</p>

<p><img src="/docs/assets/img/blog-15-scope-cmd9-10-miso.png" alt="Oscilloscope MISO CMD9&amp;10 zoom" /></p>

<p>For CMD9 we have:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">00 FF FE 40 0E 00 32 5B 59 00 00 E8 F7 7F 80 0A 40 00 39 7B 18 FF</code></li>
</ul>

<p>This consists of the 00 R1, then FF wait byte, the FE data token, then a 16 byte register, two CRC bytes and a final dummy/wait byte which fits exactly as we would expect. Let’s break it down:</p>

<table>
  <thead>
    <tr>
      <th>Bytes</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>00</td>
      <td>R1 response: ready</td>
    </tr>
    <tr>
      <td>FF</td>
      <td>wait/dummy byte</td>
    </tr>
    <tr>
      <td>FE</td>
      <td>data token</td>
    </tr>
    <tr>
      <td>40 0E 00 32 5B 59 00 00 E8 F7 7F 80 0A 40 00 39</td>
      <td>CSD register, 16 bytes</td>
    </tr>
    <tr>
      <td>7B 18</td>
      <td>Data CRC bytes</td>
    </tr>
    <tr>
      <td>FF</td>
      <td>Finally dummy/wait byte</td>
    </tr>
  </tbody>
</table>

<p>We can have a go at decoding the CSD register by referring back to the SD Physical Layer Simplified Specification linked to in a previous post. It is at CSD table 5-3 and CSD v2.0 table 5-16.</p>

<p>Let’s say byte 0 is the first byte and byte 15 is the last (furthest right byte), some key data would be:</p>

<table>
  <thead>
    <tr>
      <th>Bit Field</th>
      <th>CSD bit slice</th>
      <th>Byte Index</th>
      <th>Extraction</th>
      <th>Value</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CSD_STRUCTURE</td>
      <td>[127:126]</td>
      <td>byte 0, bits 7:6</td>
      <td>csd[0] » 6</td>
      <td>1</td>
      <td>CSD v2.0, used by SDHC/SDXC</td>
    </tr>
    <tr>
      <td>TRAN_SPEED</td>
      <td>[103:96]</td>
      <td>byte 3</td>
      <td>csd[3]</td>
      <td>0x32</td>
      <td>Encoded transfer speed.</td>
    </tr>
    <tr>
      <td>READ_BL_LEN</td>
      <td>[83:80]</td>
      <td>byte 5, bits 3:0</td>
      <td>csd[5] &amp; 0x0F</td>
      <td>9</td>
      <td>max read block length = 2^9 = 512 bytes</td>
    </tr>
    <tr>
      <td>C_SIZE</td>
      <td>[69:48]</td>
      <td>byte 7 bits 5:0, byte 8, byte 9</td>
      <td>((csd[7] &amp; 0x3F) « 16) | (csd[8] « 8) | csd[9]</td>
      <td>0x00E8F7</td>
      <td>capacity field</td>
    </tr>
    <tr>
      <td>WRITE_BL_LEN</td>
      <td>[25:22]</td>
      <td>byte 12 bits 1:0, byte 13 bits 7:6</td>
      <td>((csd[12] &amp; 0x03) « 2) | (csd[13] » 6)</td>
      <td>9</td>
      <td>max write block length = 2^9 = 512 bytes</td>
    </tr>
  </tbody>
</table>

<p>Here is the code I used to extract the values:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">uint8_t</span> <span class="n">csd_structure</span> <span class="o">=</span> <span class="p">(</span><span class="n">csd</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">&gt;&gt;</span> <span class="mi">6</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0x03u</span><span class="p">;</span>
<span class="kt">uint8_t</span> <span class="n">tran_speed</span>    <span class="o">=</span> <span class="n">csd</span><span class="p">[</span><span class="mi">3</span><span class="p">];</span>
<span class="kt">uint8_t</span> <span class="n">read_bl_len</span>   <span class="o">=</span> <span class="n">csd</span><span class="p">[</span><span class="mi">5</span><span class="p">]</span> <span class="o">&amp;</span> <span class="mh">0x0Fu</span><span class="p">;</span>

<span class="kt">uint32_t</span> <span class="n">c_size</span> <span class="o">=</span> <span class="p">((</span><span class="kt">uint32_t</span><span class="p">)(</span><span class="n">csd</span><span class="p">[</span><span class="mi">7</span><span class="p">]</span> <span class="o">&amp;</span> <span class="mh">0x3Fu</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">16</span><span class="p">)</span> <span class="o">|</span>
                  <span class="p">((</span><span class="kt">uint32_t</span><span class="p">)</span><span class="n">csd</span><span class="p">[</span><span class="mi">8</span><span class="p">]</span> <span class="o">&lt;&lt;</span> <span class="mi">8</span><span class="p">)</span> <span class="o">|</span>
                  <span class="p">((</span><span class="kt">uint32_t</span><span class="p">)</span><span class="n">csd</span><span class="p">[</span><span class="mi">9</span><span class="p">]);</span>

<span class="kt">uint8_t</span> <span class="n">write_bl_len</span> <span class="o">=</span> <span class="p">((</span><span class="n">csd</span><span class="p">[</span><span class="mi">12</span><span class="p">]</span> <span class="o">&amp;</span> <span class="mh">0x03u</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">2</span><span class="p">)</span> <span class="o">|</span>
                       <span class="p">((</span><span class="n">csd</span><span class="p">[</span><span class="mi">13</span><span class="p">]</span> <span class="o">&gt;&gt;</span> <span class="mi">6</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0x03u</span><span class="p">);</span>
</code></pre></div></div>

<p>Also for CSD v2.0, the capacity is calculated as <code class="language-plaintext highlighter-rouge">(C_SIZE + 1) * 512 * 1024;</code>. Using <code class="language-plaintext highlighter-rouge">C_SIZE = 0x00E8F7 = 59639</code>, the capacity is <code class="language-plaintext highlighter-rouge">(59639 + 1) * 512 KiB = 31,268,536,320 bytes</code>, which is about 31.27 GB or 29.12 GiB. That is consistent with a nominal 32 GB card.</p>

<p>For the maximum transmission speed per data line of 0x32 = 0011 0010. The spec defines bits 2:0 as the transfer rate and bits 6:3 as the time value multiplier so 2.5 * 10 Mbit/s = 25 Mbit/s. So now we know the card’s encoded maximum transfer rate per data line. It does not mean my SPI bus is currently running at 25 Mbit/s; the actual SPI clock is still set by the STM32 SPI prescaler.</p>

<p>For CMD10 response we have:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">00 FF FE 00 00 00 00 50 FF FF F8 00 12 80 04 4E 00 E8 8F 3C 8D FF</code></li>
</ul>

<p>This is at CID table 5-2 in the sd card docs. Again, if we name the left hand byte 0 and the furthest right hand byte 15 we can map out some key bit fields on the CID register:</p>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th style="text-align: right">CID bit slice</th>
      <th>Byte Index.</th>
      <th style="text-align: right">Value</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MID</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">[127:120]</code></td>
      <td>byte 0</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x00</code></td>
      <td>Manufacturer ID</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">OID</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">[119:104]</code></td>
      <td>bytes 1–2</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x0000</code></td>
      <td>OEM/Application ID</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PNM</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">[103:64]</code></td>
      <td>bytes 3–7</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">00 50 FF FF F8</code></td>
      <td>Product name, not clean ASCII here</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PRV</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">[63:56]</code></td>
      <td>byte 8</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x00</code></td>
      <td>Product revision <code class="language-plaintext highlighter-rouge">0.0</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PSN</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">[55:24]</code></td>
      <td>bytes 9–12</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x1280044E</code></td>
      <td>Product serial number</td>
    </tr>
    <tr>
      <td>reserved</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">[23:20]</code></td>
      <td>byte 13 bits 7:4</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x0</code></td>
      <td>Reserved</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MDT</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">[19:8]</code></td>
      <td>byte 13 bits 3:0 + byte 14</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x0E8</code></td>
      <td>Manufacturing date</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CRC</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">[7:1]</code></td>
      <td>byte 15 bits 7:1</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x47</code></td>
      <td>CID internal CRC7</td>
    </tr>
    <tr>
      <td>end bit</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">[0:0]</code></td>
      <td>byte 15 bit 0</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">1</code></td>
      <td>Always 1</td>
    </tr>
  </tbody>
</table>

<p>Here is the extraction code:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">uint8_t</span> <span class="n">mid</span> <span class="o">=</span> <span class="n">cid</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>

<span class="kt">uint16_t</span> <span class="n">oid</span> <span class="o">=</span> <span class="p">((</span><span class="kt">uint16_t</span><span class="p">)</span><span class="n">cid</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">&lt;&lt;</span> <span class="mi">8</span><span class="p">)</span> <span class="o">|</span>
               <span class="p">((</span><span class="kt">uint16_t</span><span class="p">)</span><span class="n">cid</span><span class="p">[</span><span class="mi">2</span><span class="p">]);</span>

<span class="kt">char</span> <span class="n">pnm</span><span class="p">[</span><span class="mi">6</span><span class="p">];</span>
<span class="n">pnm</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">char</span><span class="p">)</span><span class="n">cid</span><span class="p">[</span><span class="mi">3</span><span class="p">];</span>
<span class="n">pnm</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">char</span><span class="p">)</span><span class="n">cid</span><span class="p">[</span><span class="mi">4</span><span class="p">];</span>
<span class="n">pnm</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">char</span><span class="p">)</span><span class="n">cid</span><span class="p">[</span><span class="mi">5</span><span class="p">];</span>
<span class="n">pnm</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">char</span><span class="p">)</span><span class="n">cid</span><span class="p">[</span><span class="mi">6</span><span class="p">];</span>
<span class="n">pnm</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">char</span><span class="p">)</span><span class="n">cid</span><span class="p">[</span><span class="mi">7</span><span class="p">];</span>
<span class="n">pnm</span><span class="p">[</span><span class="mi">5</span><span class="p">]</span> <span class="o">=</span> <span class="sc">'\0'</span><span class="p">;</span>

<span class="kt">uint8_t</span> <span class="n">prv_major</span> <span class="o">=</span> <span class="p">(</span><span class="n">cid</span><span class="p">[</span><span class="mi">8</span><span class="p">]</span> <span class="o">&gt;&gt;</span> <span class="mi">4</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0x0Fu</span><span class="p">;</span>
<span class="kt">uint8_t</span> <span class="n">prv_minor</span> <span class="o">=</span> <span class="n">cid</span><span class="p">[</span><span class="mi">8</span><span class="p">]</span> <span class="o">&amp;</span> <span class="mh">0x0Fu</span><span class="p">;</span>

<span class="kt">uint32_t</span> <span class="n">psn</span> <span class="o">=</span> <span class="p">((</span><span class="kt">uint32_t</span><span class="p">)</span><span class="n">cid</span><span class="p">[</span><span class="mi">9</span><span class="p">]</span> <span class="o">&lt;&lt;</span> <span class="mi">24</span><span class="p">)</span> <span class="o">|</span>
               <span class="p">((</span><span class="kt">uint32_t</span><span class="p">)</span><span class="n">cid</span><span class="p">[</span><span class="mi">10</span><span class="p">]</span> <span class="o">&lt;&lt;</span> <span class="mi">16</span><span class="p">)</span> <span class="o">|</span>
               <span class="p">((</span><span class="kt">uint32_t</span><span class="p">)</span><span class="n">cid</span><span class="p">[</span><span class="mi">11</span><span class="p">]</span> <span class="o">&lt;&lt;</span> <span class="mi">8</span><span class="p">)</span> <span class="o">|</span>
               <span class="p">((</span><span class="kt">uint32_t</span><span class="p">)</span><span class="n">cid</span><span class="p">[</span><span class="mi">12</span><span class="p">]);</span>

<span class="kt">uint16_t</span> <span class="n">mdt</span> <span class="o">=</span> <span class="p">((</span><span class="kt">uint16_t</span><span class="p">)(</span><span class="n">cid</span><span class="p">[</span><span class="mi">13</span><span class="p">]</span> <span class="o">&amp;</span> <span class="mh">0x0Fu</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">8</span><span class="p">)</span> <span class="o">|</span>
               <span class="p">((</span><span class="kt">uint16_t</span><span class="p">)</span><span class="n">cid</span><span class="p">[</span><span class="mi">14</span><span class="p">]);</span>

<span class="kt">uint16_t</span> <span class="n">year</span> <span class="o">=</span> <span class="mi">2000u</span> <span class="o">+</span> <span class="p">((</span><span class="n">mdt</span> <span class="o">&gt;&gt;</span> <span class="mi">4</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xFFu</span><span class="p">);</span>
<span class="kt">uint8_t</span> <span class="n">month</span> <span class="o">=</span> <span class="n">mdt</span> <span class="o">&amp;</span> <span class="mh">0x0Fu</span><span class="p">;</span>

<span class="kt">uint8_t</span> <span class="n">cid_crc7</span> <span class="o">=</span> <span class="p">(</span><span class="n">cid</span><span class="p">[</span><span class="mi">15</span><span class="p">]</span> <span class="o">&gt;&gt;</span> <span class="mi">1</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0x7Fu</span><span class="p">;</span>
<span class="kt">uint8_t</span> <span class="n">end_bit</span> <span class="o">=</span> <span class="n">cid</span><span class="p">[</span><span class="mi">15</span><span class="p">]</span> <span class="o">&amp;</span> <span class="mh">0x01u</span><span class="p">;</span>
</code></pre></div></div>

<p>There is not much here that we can really make use of other than the serial number for the card, and the manufacturing date: The CID manufacturing date field is 0x0E8. The upper 8 bits give the year offset from 2000, and the lower 4 bits give the month. That gives year 2000 + 14 = 2014 and month 8, so the card reports August 2014.</p>

<p>The CID register reports a manufacturing date of August 2014, which is odd because this card was bought recently. The other CID fields also do not decode into a clean manufacturer/product string, so I’m curious as to what that might mean. CMD10 returned a valid looking 16 byte payload after the expected data token which is great. For those interested I’m now going to investigate the card capacity further due to the strange results from CMD10.</p>

<h1 id="investigating-my-microsd-card-further">Investigating my microSD card further</h1>

<p>Well first things first I plug the microSD card into the laptop to find an empty DCIM folder on there which is not a good start as this is a fresh card from the pack.</p>

<p>Here are some pictures of my card which is a <a href="https://www.sandisk.com/en-gb/products/memory-cards/microsd-cards/sandisk-ultra-uhs-i-microsd?sku=SDSQUA4-032G-GN6MA">SanDisk Ultra 32GB microSD</a>:
<img src="/docs/assets/img/blog-15-microsd-compressed.jpg" alt="microSD card front and back closeup" /></p>

<p>For a SanDisk Ultra I can find a few typical CID values online which do not agree with the ones I have read:
MID: 0x03
OID: 0x5344, which is ASCII for “SD”
PNM: Often something like SDxxx, SUxxG or SCxxG</p>

<p>Furthermore the markings on the back look like a fairly modern production marking, which doesn’t really align with the 2014 reported manufacturing date! I found some online discussions suggesting that back side markings can differ between genuine and counterfeit cards, but I do not want to rely on that alone. The mismatch between the SanDisk label and the odd CID fields, plus the full capacity and speed test results are curious.</p>

<p>I have installed <code class="language-plaintext highlighter-rouge">F3 Write 10.0</code> with homebrew, there are equivalents for Windows such as H2testw and linux too. This will access the card, and try to write files to fill the reported capacity to test how close it actually is. This is due to many fakes being found that may be a much lower capacity card reporting at a higher capacity. I’ve plugged the card in and run <code class="language-plaintext highlighter-rouge">f3write /Volumes/SUSMICROSD/</code>.</p>

<p>F3 reports the free space at about 29.06 GB. That is close to the CSD derived 29.12 GiB once formatting overhead and decimal/binary unit differences are considered, so the capacity itself does not look fake from this test. The average F3 write speed was around 8.69 MB/s, which is below the labelled Class 10 expectation. That is another suspicious sign, although I should be careful here because the result can depend on the reader, USB hub, filesystem, write pattern, and test setup.</p>

<p>Here is a plot of the write speeds per 1GB file:</p>

<p><img src="/docs/assets/img/blog-15-f3-microsd-write-speed.png" alt="F3 write speeds" /></p>

<p>To be fair the write speed is very consistent, although every fifth file seems to have a lower speed bound.</p>

<p>The writes are complete so we’ll run <code class="language-plaintext highlighter-rouge">f3read /Volumes/SUSMICROSD/</code> to see how well it will read the files back, there should be no corruptions here. Hmm it is starting to look suspicious again, I would expect the F3 read speeds to be much higher given the classification but currently I’m seeing 20 MB/s average speeds.</p>

<p><img src="/docs/assets/img/blog-15-f3-microsd-read-speed.png" alt="F3 read speeds" /></p>

<p>I expected a SanDisk Ultra card to read faster than this, but the full F3 read passed without corruption, so this looks more like a suspicious/performance concern than a fake capacity result. The CID and performance are suspicious enough that I would not use it as a trusted reference card, but it is fine for my purposes for SPI logging at the moment.</p>

<p>That concludes the post for today, next I’ll tackle reading and writing raw blocks.</p>

<script src="https://utteranc.es/client.js" repo="skoopsy/skoopsy.github.io" issue-term="pathname" label="blog-embedded13" theme="preferred-color-scheme" crossorigin="anonymous" async="">
</script>

<p>Copyright © 2026 David O’Connor</p>]]></content><author><name>Dave</name></author><category term="STM32" /><summary type="html"><![CDATA[Last time I managed to get the STM32F103x talking to a microSD card over SPI. The card responded correctly to CMD0 and CMD8, which showed that the wiring, SPI mode, CMD packet format, dummy clocking, and R1/R7 response handling were functional. However I did not finish the initialisation process so let’s do that now. This post will finish the initialisation process and ask it for some information about itself!]]></summary></entry><entry><title type="html">STM32 #12: Talking to a microSD card over SPI</title><link href="https://skoopsy.dev/stm32/2026/06/09/STM32-13-sd-card-transport.html" rel="alternate" type="text/html" title="STM32 #12: Talking to a microSD card over SPI" /><published>2026-06-09T22:00:00+00:00</published><updated>2026-06-09T22:00:00+00:00</updated><id>https://skoopsy.dev/stm32/2026/06/09/STM32-13-sd-card-transport</id><content type="html" xml:base="https://skoopsy.dev/stm32/2026/06/09/STM32-13-sd-card-transport.html"><![CDATA[<p>In the previous two posts I looked at why I want removable local storage on the STM32F103x, then built a small SPI1 board support package using libopencm3. The full storage stack I’m working towards looks like this:</p>

<p align="center">
  <img src="/docs/assets/img/blog-14-architecture.jpg" alt="Architecture diagram" width="300" />
</p>

<p>This post focuses on the SD card transport layer. The goal is to talk to the card at the block level using the bsp_spi.h API so that I can read and write raw 512 byte sectors before messing with FatFs, because it will have no knowledge of how the micro is wired to the microSD card. That will happen through the disk I/O adapter layer between FatFs and SD Card transport. So the sd card driver will expose very basic functions such as:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sd_status_t</span> <span class="nf">sd_card_init</span><span class="p">(</span><span class="kt">void</span><span class="p">);</span>
<span class="n">sd_status_t</span> <span class="nf">sd_card_read_block</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">block</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">buffer</span><span class="p">);</span>
<span class="n">sd_status_t</span> <span class="nf">sd_card_write_block</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">block</span><span class="p">,</span> <span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">buffer</span><span class="p">);</span>
</code></pre></div></div>
<p>Once we confirm these functions working the FatFs implementation should be a little less difficult to figure out and debug. There is a lot of great information on microSD over SPI on the <a href="https://elm-chan.org/docs/mmc/mmc_e.html">elm-ChaN site</a>.</p>

<h1 id="a-note-on-the-spi-bsp-developed-in-the-last-post">A note on the SPI BSP developed in the last post</h1>
<p>In the <a href="https://skoopsy.dev/stm32/2026/06/06/STM32-12-spi.html">last post</a> I kept the SPI bsp deliberately small to understand what the core concepts were and to make it easy to follow. I have since spent many hours digging deeper into it making the package a little more defensive. I’ve summarised the main changes here, which I may cover in the future:</p>

<ul>
  <li>SPI transfers now return a status instead of assuming things worked out okay</li>
  <li>Wait loops now have timeout limits</li>
  <li>Basic STM32 SPI errors such as overrun and mode fault are checked</li>
  <li>Buffer functions validate their parameters</li>
  <li>the baud rate is only changed once the SPI peripheral is not busy</li>
</ul>

<p>The extra robustness is fairly important because an SD card is a removable piece of hardware. So it may not respond, it may be absent, the wiring may be wrong (<em>holds breath</em>), or the card might be busy internally. I didn’t want the final data logger to be sitting forever in a while loop.</p>

<p>The SD card layer does not call those low level wait helpers directly. It only sees the public SPI BSP API. Internally, though, the SPI BSP now uses timeout based waits so that calls like <code class="language-plaintext highlighter-rouge">bsp_spi1_transfer()</code> and <code class="language-plaintext highlighter-rouge">bsp_spi1_write_buffer()</code> can fail cleanly instead of blocking forever.</p>

<h1 id="what-actually-is-sd-over-spi">What actually is SD over SPI?</h1>

<p>When an SD card powers up, by default it boots into its native “SD mode” which is very fast but uses quite a few connections to make it happen as explained in the last post. To use it over SPI, the host has to send a reset command while chip select is asserted in the correct way.</p>

<p>Once the card is in the SPI mode, commands and data are exchanged as normal SPI bytes. The STM32 is the controller (master), and the microSD card is the peripheral (slave), and every byte sent by the STM32 also clocks one byte back from the SD card. This is why in the SPI BSP we added the transfer function rather than only separate read and write:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bsp_spi_status_t</span> <span class="nf">bsp_spi1_transfer</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="n">tx_data</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">rx_data</span><span class="p">);</span>
</code></pre></div></div>

<p>Even if we only care about reading data, we still have to transmit dummy bytes, usually 0xFF, to generate clock pulses.</p>

<h1 id="sd-card-initialisation-sequence">SD Card initialisation sequence</h1>

<p>The initialisation sequence for SPI is this:</p>

<ul>
  <li>Power on</li>
  <li>Keep CS (chip select) high</li>
  <li>Send at least 74 dummy clock pulses</li>
  <li>Pull CS low</li>
  <li>Send CMD0 to access idle state</li>
  <li>Send CMD8 to check the voltage range and card version</li>
  <li>Repeatedly send CMD55 + ACMD41 until the card leaves idle state (R1=0x00)</li>
  <li>Detect whether the card is SDHC/SDXC or SDSC</li>
  <li>Increase the SPI speed</li>
</ul>

<p>So the first goal for developing this driver will be to reset the card, ID it, and get it out of the idle state.</p>

<h1 id="sd-commands-are-six-byte-packets">SD commands are six byte packets</h1>

<p>In SPI mode, most SD commands are sent as six bytes:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Command] [arg3] [arg2] [arg1] [arg0] [CRC]
</code></pre></div></div>

<p>The command byte includes the command index. For example CMD0 is sent as command index 0 with the required start/transmission bits added. The argument is a 32-bit value split into four bytes. The CRC byte is ignored in SPI mode after initialisation, but CMD0 and CMD8 still need to have valid CRC values during the initialisation.</p>

<p>Here is a good <a href="https://problemkaputt.de/gbatek-dsi-sd-mmc-protocol-command-response-register-summary.htm">summary of the command/response registers</a> from a nintendo hardware hacking site of all places. On this page there is a index with great coverage for the <a href="https://problemkaputt.de/gbatek-dsi-sd-mmc-protocol-and-i-o-ports.htm">SD protocol</a>.</p>

<h1 id="waiting-for-a-response">Waiting for a response</h1>

<p>SD cards are a little different to most SPI peripherals because the response does not necessarily appear immediately. After sending a command the card might return <code class="language-plaintext highlighter-rouge">0xFF</code> for several bytes before returning a useful response whilst it is reading or writing. So the driver needs a helper function that keeps clocking the bus and waiting for either a valid response, or a timeout to be hit. This is why the timeout additions in the SPI BSP became important. The SD card layer shouldn’t assume the card will always respond immediately or at all.</p>

<h1 id="sd-command-responses-in-spi-mode">SD command responses in SPI mode</h1>
<p>There are 3 main response types that will appear during initialisation</p>

<table>
  <thead>
    <tr>
      <th>Response</th>
      <th style="text-align: right">Size in SPI mode</th>
      <th>Used by</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>R1</td>
      <td style="text-align: right">1 byte</td>
      <td>CMD0, CMD55, ACMD41, CMD17, CMD24</td>
      <td>Main status response. Each bit is a flag.</td>
    </tr>
    <tr>
      <td>R3</td>
      <td style="text-align: right">5 bytes</td>
      <td>CMD58</td>
      <td>R1 byte followed by 4 OCR bytes.</td>
    </tr>
    <tr>
      <td>R7</td>
      <td style="text-align: right">5 bytes</td>
      <td>CMD8</td>
      <td>R1 byte followed by 4 bytes echoing voltage/check-pattern information.</td>
    </tr>
  </tbody>
</table>

<p>Both R3 and R7 start with a R1 byte, so the driver can wait for the R1 response and then read the extra four bytes if the command expects them.</p>

<h1 id="r1-response-byte">R1 response byte</h1>

<p>During initialisation the SD card mostly returns an R1 response. This is a single status byte where each bit is a flag.</p>

<p>A response of <code class="language-plaintext highlighter-rouge">0x00</code> means no error flags are set and the card is ready. During initialisation, 0x01 is also expected because bit 0 means the card is still in idle state:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: right">Bit</th>
      <th style="text-align: right">Mask</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: right">7</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x80</code></td>
      <td>Always 0 in a valid R1 response</td>
    </tr>
    <tr>
      <td style="text-align: right">6</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x40</code></td>
      <td>Parameter error</td>
    </tr>
    <tr>
      <td style="text-align: right">5</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x20</code></td>
      <td>Address error</td>
    </tr>
    <tr>
      <td style="text-align: right">4</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x10</code></td>
      <td>Erase sequence error</td>
    </tr>
    <tr>
      <td style="text-align: right">3</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x08</code></td>
      <td>CRC error</td>
    </tr>
    <tr>
      <td style="text-align: right">2</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x04</code></td>
      <td>Illegal command</td>
    </tr>
    <tr>
      <td style="text-align: right">1</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x02</code></td>
      <td>Erase reset</td>
    </tr>
    <tr>
      <td style="text-align: right">0</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x01</code></td>
      <td>In idle state</td>
    </tr>
  </tbody>
</table>

<p>Some examples of the R1 responses:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="mh">0x01</span> <span class="o">=</span> <span class="mo">0000</span> <span class="mo">0001</span> <span class="o">=</span> <span class="n">valid</span> <span class="n">R1</span><span class="p">,</span> <span class="n">card</span> <span class="n">is</span> <span class="n">idle</span>
<span class="mh">0x00</span> <span class="o">=</span> <span class="mo">0000</span> <span class="mo">0000</span> <span class="o">=</span> <span class="n">valid</span> <span class="n">R1</span><span class="p">,</span> <span class="n">ready</span><span class="o">/</span><span class="n">no</span> <span class="n">error</span>
<span class="mh">0x05</span> <span class="o">=</span> <span class="mo">0000</span> <span class="mo">0101</span> <span class="o">=</span> <span class="n">valid</span> <span class="n">R1</span><span class="p">,</span> <span class="n">idle</span> <span class="o">+</span> <span class="n">illegal</span> <span class="n">command</span>
<span class="mh">0xFF</span> <span class="o">=</span> <span class="mi">1111</span> <span class="mi">1111</span> <span class="o">=</span> <span class="n">not</span> <span class="n">a</span> <span class="n">valid</span> <span class="n">R1</span> <span class="n">response</span><span class="p">,</span> <span class="n">bit</span> <span class="mi">7</span> <span class="n">is</span> <span class="mi">1</span><span class="p">,</span> <span class="n">keep</span> <span class="n">waiting</span>
</code></pre></div></div>

<p>The important… bit… is that R1 is a bit field. More than one flag can be set at once. So <code class="language-plaintext highlighter-rouge">0x05</code> is just:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="mh">0x05</span> <span class="o">=</span> <span class="mh">0x04</span> <span class="o">|</span> <span class="mh">0x01</span>
</code></pre></div></div>

<p>Which is a valid R1 format response, but reports idle + illegal command</p>

<p>Bit 7 is different from the others. In a valid R1 response it should always be zero. That is useful because while the SD card is not ready to respond, it will often keep returning 0xFF. Therefore the driver can keep clocking the bus until it receives a byte where bit 7 has cleared.</p>

<h1 id="cmd0-reset-the-card-into-spi-mode">CMD0: Reset the card into SPI mode</h1>

<p>CMD0 is the reset command, it’s also the command that should put the card into SPI mode if following the initialisation sequence mentioned earlier. If that is done the expected response should be R1 = <code class="language-plaintext highlighter-rouge">0x01</code> meaning the card is in idle state.</p>

<h1 id="cmd8-check-voltage-range-and-card-version">CMD8: Check voltage range and card version</h1>

<p>CMD8 is used to check whether the card supports the expected voltage range and to help differentiate between the newer SD v2 cards and the older ones. For a modern SDHC card, we should expect CMD8 to return a R7 response which contains the voltage pattern that was sent in the command argument. This is important to differentiate between older SDSC and newer SDHC/SDXC cards.</p>

<p>The common argument is <code class="language-plaintext highlighter-rouge">0x000001AA</code> and the expected echoed check pattern should be <code class="language-plaintext highlighter-rouge">0xAA</code>.</p>

<h1 id="acmd41-wait-for-the-card-to-finish-initialising">ACMD41: Wait for the card to finish initialising</h1>
<p>ACMD41 is an application specific command, so it has to be preceded by CMD55. The sequence should look like this:</p>

<ol>
  <li>CMD55</li>
  <li>ACMD41</li>
  <li>Check response</li>
  <li>Repeat until response is 0x00 or timeout</li>
  <li>Card should leave idle state</li>
</ol>

<p>If a timeout error happens here then something is wrong with the setup like no card, bad wiring, incorrect power, or a bad command sequence.</p>

<p>For SD v2 cards (e.g. SDHC and SDXC), the High Capacity Support (HCS) bit can be set in the argument: <code class="language-plaintext highlighter-rouge">ACMD41 argument = 0x40000000</code>.</p>

<h1 id="cmd58-read-ocr-and-detect-sdhc">CMD58: Read OCR and detect SDHC</h1>

<p>After the card has initialised, CMD58 reads the SD cards Operating Conditions Register (OCR) which is a 32 bit register containing the cards Vdd voltage profile and init status. This should return an R3 response which contains an R1 response followed by the 4 byte OCR contents. Here is the bit flag table, ONLY for over SPI, the SDMMC table is <a href="https://problemkaputt.de/gbatek-dsi-sd-mmc-protocol-ocr-register-32bit-operation-conditions-register.htm">different</a>, here is the bit field of the SPI response:</p>

<table>
  <thead>
    <tr>
      <th>R3 response bits</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>39:32</td>
      <td>R1 response byte</td>
    </tr>
    <tr>
      <td>31:0</td>
      <td>OCR register</td>
    </tr>
  </tbody>
</table>

<p>OCR Register:</p>

<table>
  <thead>
    <tr>
      <th>Bit</th>
      <th>Flag</th>
      <th>0</th>
      <th>1</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>31</td>
      <td>Power up status</td>
      <td>Busy</td>
      <td>Ready</td>
    </tr>
    <tr>
      <td>30</td>
      <td>Capacity (CCS)</td>
      <td>SDSC</td>
      <td>SDHC or SDXC</td>
    </tr>
    <tr>
      <td>29</td>
      <td>UHS-II support</td>
      <td>no</td>
      <td>yes</td>
    </tr>
    <tr>
      <td>28:25</td>
      <td>Reserved</td>
      <td> </td>
      <td> </td>
    </tr>
    <tr>
      <td>24</td>
      <td>Switching to 1.8v accepted (UHS-I)</td>
      <td>no</td>
      <td>yes</td>
    </tr>
    <tr>
      <td>23</td>
      <td>VDD Operating Voltage 3.5 - 3.6 supported</td>
      <td>no</td>
      <td>yes</td>
    </tr>
    <tr>
      <td>22</td>
      <td>VDD Operating Voltage 3.4 - 3.5 supported</td>
      <td>no</td>
      <td>yes</td>
    </tr>
    <tr>
      <td>21</td>
      <td>VDD Operating Voltage 3.3 - 3.4 supported</td>
      <td>no</td>
      <td>yes</td>
    </tr>
    <tr>
      <td>20</td>
      <td>VDD Operating Voltage 3.2 - 3.3 supported</td>
      <td>no</td>
      <td>yes</td>
    </tr>
    <tr>
      <td>19</td>
      <td>VDD Operating Voltage 3.1 - 3.2 supported</td>
      <td>no</td>
      <td>yes</td>
    </tr>
    <tr>
      <td>18</td>
      <td>VDD Operating Voltage 3.0 - 3.1 supported</td>
      <td>no</td>
      <td>yes</td>
    </tr>
    <tr>
      <td>17</td>
      <td>VDD Operating Voltage 2.9 - 3.0 supported</td>
      <td>no</td>
      <td>yes</td>
    </tr>
    <tr>
      <td>16</td>
      <td>VDD Operating Voltage 2.8 - 2.9 supported</td>
      <td>no</td>
      <td>yes</td>
    </tr>
    <tr>
      <td>15</td>
      <td>VDD Operating Voltage 2.7 - 2.8 supported</td>
      <td>no</td>
      <td>yes</td>
    </tr>
    <tr>
      <td>14:0</td>
      <td>Reserved</td>
      <td> </td>
      <td> </td>
    </tr>
  </tbody>
</table>

<p>The CCS bit tells us if the card uses block addressing. For SDHC/SDXC cards, reads and writes use 512 byte block numbers directly. For older SDSC cards, the command argument is a byte address.</p>

<h1 id="block-addressing">Block addressing</h1>
<p>For the SDHC and SDXC cards, the address passed to CMD17 or CMD24 is a block number. For the older SDSC cards, the address is a byte address.</p>

<p>So for SDHC:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">address</span> <span class="o">=</span> <span class="n">block_number</span><span class="p">;</span>
</code></pre></div></div>

<p>For SDSC:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">address</span> <span class="o">=</span> <span class="n">block_number</span> <span class="o">*</span> <span class="mi">512</span><span class="p">;</span>
</code></pre></div></div>

<p>For this application I’m using an SDHC microSD card but the driver should still know which type of card it has initialised.</p>

<h1 id="cmd17-reading-a-512-byte-block">CMD17: reading a 512 byte block</h1>
<p>Once initialisation is good, the next step is to read a single raw block. The single block read command is CMD17. The sequence for this is:</p>

<ol>
  <li>Send CMD17 with block address</li>
  <li>Wait for R1 response 0x00</li>
  <li>Wait for a data token 0xFE</li>
  <li>Read 512 bytes</li>
  <li>Read and discard 2 CRC bytes</li>
  <li>Release CS</li>
  <li>Send an extra dummy byte</li>
</ol>

<p>At this point we’re still below FatFS, just asking the card for a numbered 512 byte sector. I will leave this to the next post, as this one is getting big.</p>

<h1 id="cmd24-writing-a-512-byte-block">CMD24: Writing a 512 byte block</h1>
<p>This command writes a single block, here is the sequence:</p>

<ol>
  <li>Send CMD24 with block address</li>
  <li>Wait for R1 response 0x00</li>
  <li>Send data token 0xFE</li>
  <li>Send 512 bytes</li>
  <li>Send 2 dummy CRC bytes</li>
  <li>Read data response token</li>
  <li>Wait while the card is busy</li>
  <li>Release CS</li>
  <li>Send one extra dummy byte</li>
</ol>

<p>Write handling needs much more care than reading because the card could remain busy internally after the command/data is accepted. This will also be left to the next post.</p>

<h1 id="the-crc-byte">The CRC byte</h1>
<p>In an SPI mode driver, CRC checking is usually not used after the initial startup commands, so dummy CRC bytes such as <code class="language-plaintext highlighter-rouge">0xFF</code> are commonly sent. <code class="language-plaintext highlighter-rouge">CMD0</code> and <code class="language-plaintext highlighter-rouge">CMD8</code> are startup exceptions:</p>

<table>
  <thead>
    <tr>
      <th>Command</th>
      <th>CRC</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CMD0</td>
      <td><code class="language-plaintext highlighter-rouge">0x95</code></td>
    </tr>
    <tr>
      <td>CMD8</td>
      <td><code class="language-plaintext highlighter-rouge">0x87</code></td>
    </tr>
  </tbody>
</table>

<p>This is explained on ChaNs website in the links posted in the previous post.</p>

<h1 id="summary-of-the-commands">Summary of the commands</h1>

<table>
  <thead>
    <tr>
      <th>Command</th>
      <th style="text-align: right">Sent byte</th>
      <th style="text-align: right">Argument</th>
      <th>CRC byte</th>
      <th>Response</th>
      <th>Used for</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CMD0</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x40</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x00000000</code></td>
      <td><code class="language-plaintext highlighter-rouge">0x95</code></td>
      <td>R1</td>
      <td>Reset card and enter idle/SPI mode</td>
    </tr>
    <tr>
      <td>CMD8</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x48</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x000001AA</code></td>
      <td><code class="language-plaintext highlighter-rouge">0x87</code></td>
      <td>R7</td>
      <td>Check SD v2 support and voltage range</td>
    </tr>
    <tr>
      <td>CMD55</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x77</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x00000000</code></td>
      <td><code class="language-plaintext highlighter-rouge">0xFF</code></td>
      <td>R1</td>
      <td>Prefix before application-specific command</td>
    </tr>
    <tr>
      <td>ACMD41</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x69</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x40000000</code></td>
      <td><code class="language-plaintext highlighter-rouge">0xFF</code></td>
      <td>R1</td>
      <td>Application specific, Initialise card, with HCS bit set</td>
    </tr>
    <tr>
      <td>CMD58</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x7A</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x00000000</code></td>
      <td><code class="language-plaintext highlighter-rouge">0xFF</code></td>
      <td>R3</td>
      <td>Read OCR and detect SDHC/SDXC</td>
    </tr>
    <tr>
      <td>CMD17</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x51</code></td>
      <td style="text-align: right">block address</td>
      <td><code class="language-plaintext highlighter-rouge">0xFF</code></td>
      <td>R1 + data token</td>
      <td>Read one 512-byte block</td>
    </tr>
    <tr>
      <td>CMD24</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0x58</code></td>
      <td style="text-align: right">block address</td>
      <td><code class="language-plaintext highlighter-rouge">0xFF</code></td>
      <td>R1 + data response</td>
      <td>Write one 512-byte block</td>
    </tr>
  </tbody>
</table>

<h1 id="first-implementation-send-commands-read-responses">First implementation: send commands, read responses</h1>
<p>Before getting into full block read and writes, let’s build the smallest useful part of the SD card transport layer, which will consist of sending a command and reading the response.</p>

<p>From the previous post the SPI BSP has its own status enum because it knows about the STM32 SPI level issues:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">enum</span> <span class="p">{</span>
    <span class="n">BSP_SPI_OK</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
    <span class="n">BSP_SPI_ERR_TIMEOUT</span><span class="p">,</span>
    <span class="n">BSP_SPI_ERR_OVERRUN</span><span class="p">,</span>
    <span class="n">BSP_SPI_ERR_MODE_FAULT</span><span class="p">,</span>
    <span class="n">BSP_SPI_ERR_PARAM</span><span class="p">,</span>
<span class="p">}</span> <span class="n">bsp_spi_status_t</span><span class="p">;</span>
</code></pre></div></div>

<p>The SD card transport layer will have its own status enum because it should report SD card level errors too:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">enum</span> <span class="p">{</span>
    <span class="n">SD_OK</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
    <span class="n">SD_ERR_SPI</span><span class="p">,</span>
    <span class="n">SD_ERR_TIMEOUT</span><span class="p">,</span>
    <span class="n">SD_ERR_PARAM</span><span class="p">,</span>
    <span class="n">SD_ERR_BAD_RESPONSE</span><span class="p">,</span>
    <span class="n">SD_ERR_UNSUPPORTED_CARD</span><span class="p">,</span>
<span class="p">}</span> <span class="n">sd_status_t</span><span class="p">;</span>
</code></pre></div></div>
<p>You could make a mapping to translate the bsp_spi1 errors through the sd_card layer but I’m going to leave it for this post.</p>

<p>The SD driver does not need to expose all the SPI STM32 detail to the layers above. If the SPI transfer fails, the SD layer can initially report <code class="language-plaintext highlighter-rouge">SD_ERR_SPI</code>. The error status can be quite helpful when debugging over UART.</p>

<p>Once CS has been pulled low, every exit path should release the card again. Both the success and error paths need to leave the SPI bus in the same state: CS high, followed by one extra dummy byte to provide clocks after deselection, so I’m creating a helper to keep the code clean called <code class="language-plaintext highlighter-rouge">sd_deselect()</code> and whilst I’m at it let’s create a wrapper for making the CS pin low to make the naming more consistent:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"bsp_spi.h"</span><span class="c1"> // from the last post</span><span class="cp">
</span>
<span class="k">static</span> <span class="kt">void</span> <span class="nf">sd_select</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">bsp_spi1_cs_low</span><span class="p">();</span>
<span class="p">}</span>

<span class="k">static</span> <span class="kt">void</span> <span class="nf">sd_deselect</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">bsp_spi1_cs_high</span><span class="p">();</span>
    <span class="c1">// Provide 8 extra clocks after deselecting the card.</span>
    <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">bsp_spi1_transfer</span><span class="p">(</span><span class="mh">0xFFu</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>On to writing the dummy bytes needed to set the card into SPI mode…</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">sd_status_t</span> <span class="nf">sd_clock_dummy_bytes</span><span class="p">(</span><span class="kt">size_t</span> <span class="n">count</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span> <span class="n">i</span><span class="o">&lt;</span><span class="n">count</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">bsp_spi_status_t</span> <span class="n">spi_status</span> <span class="o">=</span> <span class="n">bsp_spi1_transfer</span><span class="p">(</span><span class="mh">0xFFu</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
        
        <span class="k">if</span> <span class="p">(</span><span class="n">spi_status</span> <span class="o">!=</span> <span class="n">BSP_SPI_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_ERR_SPI</span><span class="p">;</span> <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">SD_OK</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The next helper will wait for an R1 response, because the card may return 0xFF for a while before it gives a useful response. This function will keep clocking the bus until the top bit clears or a timeout is reached.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define SD_RESPONSE_TIMEOUT 100u
</span>
<span class="k">static</span> <span class="n">sd_status_t</span> <span class="nf">sd_wait_r1</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="n">r1</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">r1</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_ERR_PARAM</span><span class="p">;</span> <span class="p">}</span>

    <span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">i</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span> <span class="n">i</span><span class="o">&lt;</span><span class="n">SD_RESPONSE_TIMEOUT</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">uint8_t</span> <span class="n">response</span> <span class="o">=</span> <span class="mh">0xFFu</span><span class="p">;</span>

        <span class="n">bsp_spi_status_t</span> <span class="n">spi_status</span> <span class="o">=</span> <span class="n">bsp_spi1_transfer</span><span class="p">(</span><span class="mh">0xFFu</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">response</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">spi_status</span> <span class="o">!=</span> <span class="n">BSP_SPI_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_ERR_SPI</span><span class="p">;</span> <span class="p">}</span>
    
        <span class="c1">// Check for valid R1 response = bit 7 is always 0.</span>
        <span class="k">if</span> <span class="p">((</span><span class="n">response</span> <span class="o">&amp;</span> <span class="mh">0x80u</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0u</span><span class="p">)</span> <span class="p">{</span>
            <span class="o">*</span><span class="n">r1</span> <span class="o">=</span> <span class="n">response</span><span class="p">;</span>
            <span class="k">return</span> <span class="n">SD_OK</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">SD_ERR_TIMEOUT</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>To reiterate, the reason for checking <code class="language-plaintext highlighter-rouge">0x80</code> is that bit 7 is always zero in a valid R1 response. While the card is not ready it will usually keep returning <code class="language-plaintext highlighter-rouge">0xFF</code>, where bit 7 is still set. So the helper clocks the bus until it sees a byte where bit 7 has cleared.</p>

<p>We are going to add a helper function that will read the <code class="language-plaintext highlighter-rouge">response_tail</code> bytes that follow the R1 response for CMD8</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">sd_status_t</span> <span class="nf">sd_read_bytes</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="n">buffer</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">len</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">((</span><span class="n">buffer</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="n">len</span> <span class="o">!=</span> <span class="mi">0u</span><span class="p">))</span> <span class="p">{</span><span class="k">return</span> <span class="n">SD_ERR_PARAM</span><span class="p">;}</span>

    <span class="n">bsp_spi_status_t</span> <span class="n">spi_status</span> <span class="o">=</span> <span class="n">bsp_spi1_read_buffer</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="n">len</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">spi_status</span> <span class="o">!=</span> <span class="n">BSP_SPI_OK</span><span class="p">)</span> <span class="p">{</span><span class="k">return</span> <span class="n">SD_ERR_SPI</span><span class="p">;}</span>

    <span class="k">return</span> <span class="n">SD_OK</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now the command helper coming up has the tools to send the standard six byte SD command packet and read the response</p>

<p>The first byte of the command packet contains the command index in the format: <code class="language-plaintext highlighter-rouge">01xxxxxx</code>, where the lower six bits contain the command index. We will do this by using <code class="language-plaintext highlighter-rouge">0x40</code> which is binary <code class="language-plaintext highlighter-rouge">0100 0000</code> then bitwise OR the <code class="language-plaintext highlighter-rouge">cmd</code> inserts it into the lower 6 bits. E.g. for <code class="language-plaintext highlighter-rouge">CMD8</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="mh">0x40</span> <span class="o">|</span> <span class="mi">8</span>   <span class="o">=</span> <span class="mh">0x48</span>  <span class="c1">// CMD8</span>
</code></pre></div></div>

<p>I’ve also added some protection into converting the command, all commands must be 6 bit so we can control that to prevent a bad <code class="language-plaintext highlighter-rouge">cmd</code> parameter from altering the upper bits with <code class="language-plaintext highlighter-rouge">cmd &amp; 0x3Fu</code>. This works by always ensuring the upper bits of the command are zeroed out as <code class="language-plaintext highlighter-rouge">0x3Fu = 0011 1111</code> then when this is bitwise OR by 0x40u the correct upper bits will always be set.</p>

<p>The argument bytes are isolated from the full 32 bit sequence by shifting the relevant bits to the right and casting to a <code class="language-plaintext highlighter-rouge">uint8_t</code> which keeps only the lowest 8 bits.</p>

<p>The command helper takes a pointer for the first R1 response byte. It also takes an optional response_tail buffer for commands such as CMD8 and CMD58, where the R1 byte is followed by four more bytes.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">sd_status_t</span> <span class="nf">sd_send_command</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="n">cmd</span><span class="p">,</span>
                                   <span class="kt">uint32_t</span> <span class="n">arg</span><span class="p">,</span>
                                   <span class="kt">uint8_t</span> <span class="n">crc</span><span class="p">,</span>
                                   <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">r1</span><span class="p">,</span>
                                   <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">response_tail</span><span class="p">,</span>
                                   <span class="kt">size_t</span> <span class="n">response_tail_len</span><span class="p">)</span> <span class="p">{</span>
    
    <span class="c1">// Parameter error checking</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">r1</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_ERR_PARAM</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">if</span> <span class="p">((</span><span class="n">response_tail</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="n">response_tail_len</span> <span class="o">!=</span> <span class="mi">0u</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">SD_ERR_PARAM</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// CMD packet:</span>
    <span class="kt">uint8_t</span> <span class="n">packet</span><span class="p">[</span><span class="mi">6</span><span class="p">];</span>
    <span class="n">packet</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mh">0x40u</span> <span class="o">|</span> <span class="p">(</span><span class="n">cmd</span> <span class="o">&amp;</span> <span class="mh">0x3Fu</span><span class="p">);</span>

    <span class="c1">// Argument bytes, most significant byte first</span>
    <span class="c1">// Casting isolates the lowest 8 bits:</span>
    <span class="n">packet</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint8_t</span><span class="p">)(</span><span class="n">arg</span> <span class="o">&gt;&gt;</span> <span class="mi">24</span><span class="p">);</span>
    <span class="n">packet</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint8_t</span><span class="p">)(</span><span class="n">arg</span> <span class="o">&gt;&gt;</span> <span class="mi">16</span><span class="p">);</span>
    <span class="n">packet</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint8_t</span><span class="p">)(</span><span class="n">arg</span> <span class="o">&gt;&gt;</span> <span class="mi">8</span><span class="p">);</span>
    <span class="n">packet</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint8_t</span><span class="p">)(</span><span class="n">arg</span><span class="p">);</span>
    <span class="n">packet</span><span class="p">[</span><span class="mi">5</span><span class="p">]</span> <span class="o">=</span> <span class="n">crc</span><span class="p">;</span>

    <span class="c1">// CS pin low</span>
    <span class="n">sd_select</span><span class="p">();</span>
    
    <span class="c1">// Write to the buffer</span>
    <span class="n">bsp_spi_status_t</span> <span class="n">spi_status</span> <span class="o">=</span> <span class="n">bsp_spi1_write_buffer</span><span class="p">(</span><span class="n">packet</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">packet</span><span class="p">));</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">spi_status</span> <span class="o">!=</span> <span class="n">BSP_SPI_OK</span><span class="p">)</span> <span class="p">{</span> 
        <span class="n">sd_deselect</span><span class="p">();</span>
        <span class="k">return</span> <span class="n">SD_ERR_SPI</span><span class="p">;</span> 
    <span class="p">}</span>

    <span class="c1">// Wait for r1 response</span>
    <span class="n">sd_status_t</span> <span class="n">status</span> <span class="o">=</span> <span class="n">sd_wait_r1</span><span class="p">(</span><span class="n">r1</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">sd_deselect</span><span class="p">();</span>
        <span class="k">return</span> <span class="n">status</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// Check if response contains more than just r1 and read</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">response_tail_len</span> <span class="o">!=</span> <span class="mi">0u</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">status</span> <span class="o">=</span> <span class="n">sd_read_bytes</span><span class="p">(</span><span class="n">response_tail</span><span class="p">,</span> <span class="n">response_tail_len</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">sd_deselect</span><span class="p">();</span>
            <span class="k">return</span> <span class="n">status</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="n">sd_deselect</span><span class="p">();</span>

    <span class="k">return</span> <span class="n">SD_OK</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>For the first test I’m not going to fully initialise the card, but start the process just to prove that the card can enter SPI mode and respond to CMD0/CMD8, so let’s give the first pass of <code class="language-plaintext highlighter-rouge">sd_card_init()</code> a go:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sd_status_t</span> <span class="nf">sd_card_init</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">uint8_t</span> <span class="n">r1</span> <span class="o">=</span> <span class="mh">0xFFu</span><span class="p">;</span>
    
    <span class="c1">// Set transfer speed</span>
    <span class="n">bsp_spi_status_t</span> <span class="n">spi_status</span> <span class="o">=</span> <span class="n">bsp_spi1_set_slow</span><span class="p">();</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">spi_status</span> <span class="o">!=</span> <span class="n">BSP_SPI_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_ERR_SPI</span><span class="p">;</span> <span class="p">}</span>

    <span class="c1">// SD cards need at least 74 clock pulses with CS high before CMD0</span>
    <span class="c1">// 1 SPI byte transfer produces 8 clock pulses, so 10 dummy bytes gives 80 clock pulses</span>
    <span class="n">bsp_spi1_cs_high</span><span class="p">();</span>
    <span class="n">sd_status_t</span> <span class="n">status</span> <span class="o">=</span> <span class="n">sd_clock_dummy_bytes</span><span class="p">(</span><span class="mi">10</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">status</span><span class="p">;</span> <span class="p">}</span>
    
    <span class="c1">// CMD0: Reset card, enter idle state</span>
    <span class="n">status</span> <span class="o">=</span> <span class="n">sd_send_command</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mh">0x00000000u</span><span class="p">,</span> <span class="mh">0x95u</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">r1</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="mi">0u</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">status</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">r1</span> <span class="o">!=</span> <span class="mh">0x01u</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_ERR_BAD_RESPONSE</span><span class="p">;</span> <span class="p">}</span>

    <span class="c1">// CMD8: Check voltage range and SD v2 support</span>
    <span class="kt">uint8_t</span> <span class="n">r7_tail</span><span class="p">[</span><span class="mi">4</span><span class="p">];</span>
    <span class="n">status</span> <span class="o">=</span> <span class="n">sd_send_command</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> 
                             <span class="mh">0x000001AAu</span><span class="p">,</span> 
                             <span class="mh">0x87u</span><span class="p">,</span> 
                             <span class="o">&amp;</span><span class="n">r1</span><span class="p">,</span> 
                             <span class="n">r7_tail</span><span class="p">,</span> 
                             <span class="k">sizeof</span><span class="p">(</span><span class="n">r7_tail</span><span class="p">));</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">SD_OK</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">status</span><span class="p">;</span> <span class="p">}</span>

    <span class="c1">// For a modern SDHC card, CMD8 should be accepted while still idle.</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">r1</span> <span class="o">!=</span> <span class="mh">0x01u</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SD_ERR_BAD_RESPONSE</span><span class="p">;</span> <span class="p">}</span>

    <span class="c1">// Expected R7 tail for CMD8 argument 0x000001AA:</span>
    <span class="c1">// r7_tail[0] = 0x00</span>
    <span class="c1">// r7_tail[1] = 0x00</span>
    <span class="c1">// r7_tail[2] = 0x01</span>
    <span class="c1">// r7_tail[3] = 0xAA</span>
    <span class="k">if</span> <span class="p">((</span><span class="n">r7_tail</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">!=</span> <span class="mh">0x01u</span><span class="p">)</span> <span class="o">||</span> <span class="p">(</span><span class="n">r7_tail</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">!=</span> <span class="mh">0xAAu</span><span class="p">))</span> <span class="p">{</span> 
        <span class="k">return</span> <span class="n">SD_ERR_BAD_RESPONSE</span><span class="p">;</span> 
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">SD_OK</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="testing">Testing</h1>

<p>Check out the earlier post about <a href="https://skoopsy.dev/stm32/2026/06/01/STM32-11-microsd.html">microSD and wiring to an STM32f103x for SPI</a> if you missed that.</p>

<p>If you have a UART module written, you can use that to report the response values back to a terminal connected to the STM32F1xx. However, for fun, and in case you do not have UART debug output written yet, you can also inspect the SPI lines directly with a 2-channel oscilloscope with logic analyser features.</p>

<p>Since I only have two channels available, I am doing this as two separate captures:</p>

<ul>
  <li>SCK + MOSI, to check what the STM32 sends</li>
  <li>SCK + MISO, to check what the SD card returns</li>
</ul>

<p>For this smoke test, <code class="language-plaintext highlighter-rouge">CMD0</code> should return <code class="language-plaintext highlighter-rouge">0x01</code> and <code class="language-plaintext highlighter-rouge">CMD8</code> should return <code class="language-plaintext highlighter-rouge">0x01</code> followed by <code class="language-plaintext highlighter-rouge">00 00 01 AA</code>.</p>

<p>It looks like a lot, but that is because my contacts are on a ribbon cable, I’m only using two of them with the probes stuffed in the other end, which is rough and ready, but will work for this test:
<img src="/docs/assets/img/blog-14-hardware.jpg" alt="Image of STM32 dev board with scope probes" /></p>

<h2 id="scope-sck--mosi">Scope: SCK + MOSI</h2>
<p>The STM32F103x should be sending:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">CMD0:</span> <span class="mi">40</span> <span class="mo">00</span> <span class="mo">00</span> <span class="mo">00</span> <span class="mo">00</span> <span class="mi">95</span>
<span class="n">CMD8</span><span class="o">:</span> <span class="mi">48</span> <span class="mo">00</span> <span class="mo">00</span> <span class="mo">01</span> <span class="n">AA</span> <span class="mi">87</span>
</code></pre></div></div>
<p>Let’s take a look: The blue trace is the clock signal needed for the logic analyser, and the red trace is the MOSI line (which has a 10k pull up on it):</p>

<p><img src="/docs/assets/img/blog-14-scope-mosi.png" alt="Oscilloscope MOSI send" /></p>

<p>As you can see, the STM32 sends the 10 dummy bytes of <code class="language-plaintext highlighter-rouge">0xFF</code> first. That gives the SD card 80 clock pulses with CS high before <code class="language-plaintext highlighter-rouge">CMD0</code>.</p>

<p>Then the STM32 sends:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CMD0: 40 00 00 00 00 95
</code></pre></div></div>
<p>After that there are a few 0xFF bytes while the driver clocks the bus and waits for the card response, followed by the dummy byte sent after deselecting the card.</p>

<p>Then the STM32 sends:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CMD8: 48 00 00 01 AA 87
</code></pre></div></div>
<p>After <code class="language-plaintext highlighter-rouge">CMD8</code>, the driver again sends dummy <code class="language-plaintext highlighter-rouge">0xFF</code> bytes while waiting for the R1 response, reading the R7 response tail, and finally deselecting the card.</p>

<h2 id="scope-sck--miso">Scope: SCK + MISO</h2>
<p>I’ve now hooked up the second probe to the MISO line, reset the device and captured the init sequence again. The SD card should be responding with:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">CMD0</span> <span class="n">response</span><span class="o">:</span> <span class="mo">01</span>
<span class="n">CMD8</span> <span class="n">response</span><span class="o">:</span> <span class="mo">01</span> <span class="mo">00</span> <span class="mo">00</span> <span class="mo">01</span> <span class="n">AA</span>
</code></pre></div></div>

<p>Let’s take a look on the scope. The blue trace is the clock signal again, and the red trace is now connected to the MISO line:</p>

<p><img src="/docs/assets/img/blog-14-scope-miso.png" alt="Oscilloscope MISO response" /></p>

<p>Wow it is responding!</p>

<p>At the start, the card returns 0xFF while the STM32 sends the initial dummy clocks and the CMD0 packet. Then the card returns: <code class="language-plaintext highlighter-rouge">01</code>. That is the expected <code class="language-plaintext highlighter-rouge">CMD0</code> response, meaning the card is now in idle state.</p>

<p>The card then returns more 0xFF bytes while the STM32 sends the CMD8 packet and clocks the bus. Finally, the card returns: <code class="language-plaintext highlighter-rouge">01 00 00 01 AA</code>. This is exactly what I wanted to see. The first byte, <code class="language-plaintext highlighter-rouge">0x01</code>, is the R1 response showing that the card is still in idle state. The remaining four bytes are the R7 response tail from <code class="language-plaintext highlighter-rouge">CMD8</code>, and the final <code class="language-plaintext highlighter-rouge">0xAA</code> confirms that the check pattern was echoed back correctly.</p>

<p>So at this stage the card is not fully initialised yet, but the SPI wiring, command packet format, dummy clocking, R1 response handling, and <code class="language-plaintext highlighter-rouge">CMD8</code> R7 response read are all working.</p>

<p>Next we will take a look at finishing the init sequence, then reading and writing memory blocks!</p>

<script src="https://utteranc.es/client.js" repo="skoopsy/skoopsy.github.io" issue-term="pathname" label="blog-embedded12" theme="preferred-color-scheme" crossorigin="anonymous" async="">
</script>

<p>Copyright © 2026 David O’Connor</p>]]></content><author><name>Dave</name></author><category term="STM32" /><summary type="html"><![CDATA[In the previous two posts I looked at why I want removable local storage on the STM32F103x, then built a small SPI1 board support package using libopencm3. The full storage stack I’m working towards looks like this:]]></summary></entry><entry><title type="html">STM32 #11: A simple SPI driver on STM32F1xx with libopencm3</title><link href="https://skoopsy.dev/stm32/2026/06/06/STM32-12-spi.html" rel="alternate" type="text/html" title="STM32 #11: A simple SPI driver on STM32F1xx with libopencm3" /><published>2026-06-06T22:00:00+00:00</published><updated>2026-06-06T22:00:00+00:00</updated><id>https://skoopsy.dev/stm32/2026/06/06/STM32-12-spi</id><content type="html" xml:base="https://skoopsy.dev/stm32/2026/06/06/STM32-12-spi.html"><![CDATA[<p>In the previous post I looked at adding microSD storage to an STM32F1xx project. The useful conclusion was that a microSD card can be used over SPI, with FatFS sitting above a low-level block read/write layer. Before getting anywhere near FatFS though, I need a clean way to actually use SPI from the STM32F1xx.</p>

<p>For this project I am using:</p>

<ul>
  <li>STM32F103C8T6</li>
  <li>libopencm3</li>
  <li>Neovim</li>
  <li>SPI1</li>
  <li>manual chip select using GPIO</li>
</ul>

<p>The SPI driver will live in a board support package:</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src/
  bsp/
    bsp_spi.c

include/
  bsp/
    bsp_spi.h
</code></pre></div></div>

<p>This keeps the SPI setup and libopencm3 api separate from the SD card logic.</p>

<h2 id="why-write-a-board-level-spi-driver">Why write a board level SPI driver?</h2>
<p>It’s tempting to just put the SPI setup directly inside the SD card driver and get on with it. That works, but it also couples two things that are not really the same; SPI is a board level peripheral, the SD card is just one possible device on that bus. I might want to use a different peripheral on SPI later, or move the SD card code around, or test the SPI later separately.</p>

<p>my file bsp_spi.c will do the following:</p>
<ul>
  <li>Configure the STM32F103xx SPI pins</li>
  <li>Configure the SPI1 peripheral</li>
  <li>Control the chip select</li>
  <li>Send and recieve bytes</li>
  <li>Allow changes to the SPI speed.</li>
</ul>

<p>The microSD card layer with FatFS will sit above this layer later.</p>

<h2 id="spi-whistle-stop-tour-i-swear">SPI Whistle Stop Tour (I swear!)</h2>
<p>SPI is a synchronous serial bus. The controller (master) provides the clock, and data is shifted in and out at the same time. The <a href="https://en.wikipedia.org/wiki/Serial_Peripheral_Interface">Wikipedia entry for SPI</a> is comprehensive.</p>

<p>SPI works over 4 signal lines:</p>

<table>
  <thead>
    <tr>
      <th>Signal Name</th>
      <th>Meaning</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>SCK</td>
      <td>Serial clock</td>
      <td>Set the clock rate for bit read and writes</td>
    </tr>
    <tr>
      <td>COPI (MOSI)</td>
      <td>Controller Out, Peripheral In</td>
      <td>Controller sends data to the peripheral that is listening</td>
    </tr>
    <tr>
      <td>CIPO (MISO)</td>
      <td>Controller In, Peripheral Out</td>
      <td>Peripheral sending data back to the controller</td>
    </tr>
    <tr>
      <td>CS (SS)</td>
      <td>Chip Select</td>
      <td>Selects which peripheral on the shared bus to communicate with</td>
    </tr>
  </tbody>
</table>

<p>If it wasn’t clear the STM32F1xx is the controller, and the microSD card would be the peripheral.</p>

<p>One weird thing with SPI is that it does not really define the chip select behaviour universally. The STM32 SPI peripheral can do a hardware managed CS which is often referenced as a Negative Slave Select (NSS) on most pinout diagrams. For an SD card, I want to decide exactly when the card is selected and released, so I treat CS as an ordinary GPIO output.</p>

<h1 id="spi-pins-on-stm32f103c8t6">SPI pins on STM32F103C8T6</h1>

<p>For SPI1 I am using the following pins:</p>

<table>
  <thead>
    <tr>
      <th>Pin</th>
      <th>Function</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>PA4</td>
      <td>Chip select (NSS), or manual GPIO</td>
    </tr>
    <tr>
      <td>PA5</td>
      <td>SCK</td>
    </tr>
    <tr>
      <td>PA6</td>
      <td>CIPO</td>
    </tr>
    <tr>
      <td>PA7</td>
      <td>COPI</td>
    </tr>
  </tbody>
</table>

<p>Note that I am not going to use PA4 as hardware NSS. It is just a GPIO output that I drive low before the transaction, and high afterwards. SPI1 can also be remapped to alternative pins, but for this board I am using the default SPI1 pinout. The STM32F103 also has an SPI2 peripheral available on PB12 to PB15:</p>

<table>
  <thead>
    <tr>
      <th>Pin</th>
      <th>Function</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>PB12</td>
      <td>Chip select</td>
    </tr>
    <tr>
      <td>PB13</td>
      <td>SCK</td>
    </tr>
    <tr>
      <td>PB14</td>
      <td>CIPO</td>
    </tr>
    <tr>
      <td>PB15</td>
      <td>COPI</td>
    </tr>
  </tbody>
</table>

<h1 id="spi-settings-for-a-microsd-card-peripheral">SPI settings for a microSD card peripheral</h1>

<h3 id="spi-mode">SPI Mode</h3>
<p>There are <a href="https://www.totalphase.com/blog/2024/07/understanding-spi-clock-signals-polarity-phase-clock-edges-spi-modes/">4 modes that SPI can be configured in for the clock signal phase</a>. SD cards use SPI mode 0 during SPI-mode operation. That means the clock idles low and data is sampled on the first clock transition. In libopencm3 this maps to <code class="language-plaintext highlighter-rouge">CPOL = 0</code> and <code class="language-plaintext highlighter-rouge">CPHA = 0</code>.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">SPI_CR1_CPOL_CLK_TO_0_WHEN_IDLE</span>
<span class="n">SPI_CR1_CPHA_CLK_TRANSITION_1</span>
</code></pre></div></div>

<h3 id="msb-or-lsb">MSB or LSB</h3>
<p>SD card commands and data are sent with the Most Significant Bit (MSB) first, so in libopencm3 this would be set as <code class="language-plaintext highlighter-rouge">SPI_CR1_MSBFIRST</code>.</p>

<h3 id="packet-size">Packet size</h3>
<p>The SD standard states that all packets are moved in streams of 8-bit values such as commands, responses, data blocks, and CRC bytes. Although a data block on the SD card might be 512 bytes as discussed previously, the SPI peripheral still shifts it one byte at a time. So <code class="language-plaintext highlighter-rouge">SPI_CR1_DFF_8BIT</code> will be set in libopencm3.</p>

<h3 id="initialisation-clock-frequency">Initialisation clock frequency</h3>
<p>During power-up and SPI-mode entry, you should keep the SPI clock in the low-speed initialisation range. ChaN’s MMC/SDC notes describe the SPI-mode initialisation procedure as setting the SPI clock between 100 kHz and 400 kHz, keeping DI and CS high, and applying at least 74 clock pulses before sending CMD0.</p>

<p>So for our board with a 72 MHz APB2 clock, the SPI clock will be divided by the <code class="language-plaintext highlighter-rouge">SPI_CR1_BAUDRATE_FPCLK_DIV_XXX</code>, so to pull 72 MHz below 400 kHz we can use the 256 prescaler:</p>

<p>72 MHz / 256 = 281.25 KHz</p>

<p>This is done with <code class="language-plaintext highlighter-rouge">SPI_CR1_BAUDRATE_FPCLK_DIV_256</code>. The prescalers defined in libopencm3 SPI peripheral baud rate prescalers go from 2 to 256 in 2^n increments.</p>

<h3 id="runtime-clock-frequency">Runtime clock frequency</h3>
<p>After the card has been initialised the SPI clock can be increased. Assuming the SPI1 is clocked from 72 MHz APB2, then based on the libopencm3 prescaler options here are the following clock frequencies you can try:</p>

<table>
  <thead>
    <tr>
      <th>Prescaler</th>
      <th>SPI Clock based on APB2 72 MHz</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>SPI_CR1_BAUDRATE_FPCLK_DIV_256</td>
      <td>281.25 kHz</td>
    </tr>
    <tr>
      <td>SPI_CR1_BAUDRATE_FPCLK_DIV_128</td>
      <td>562.5 kHz</td>
    </tr>
    <tr>
      <td>SPI_CR1_BAUDRATE_FPCLK_DIV_64</td>
      <td>1.125 MHz</td>
    </tr>
    <tr>
      <td>SPI_CR1_BAUDRATE_FPCLK_DIV_32</td>
      <td>2.25 MHz</td>
    </tr>
    <tr>
      <td>SPI_CR1_BAUDRATE_FPCLK_DIV_16</td>
      <td>4.5 MHz</td>
    </tr>
    <tr>
      <td>SPI_CR1_BAUDRATE_FPCLK_DIV_8</td>
      <td>9 MHz</td>
    </tr>
    <tr>
      <td>SPI_CR1_BAUDRATE_FPCLK_DIV_4</td>
      <td>18 MHz</td>
    </tr>
    <tr>
      <td>SPI_CR1_BAUDRATE_FPCLK_DIV_2</td>
      <td>36 MHz</td>
    </tr>
  </tbody>
</table>

<p>Probably not wise to turn it up to 10 on your first try but ramp it up with some testing. I’m going to ramp up to 562.5 kHz for my first go which give me a good chance to see it easily on the scope and test the speed up functionality, and honestly is going to be far more than enough for the sensor data I want to log. Do keep in mind the speed classification of the microSD card and that the classification is based on the SD protocol not single data line SPI.</p>

<p>Here is a summary of the settings:</p>

<table>
  <thead>
    <tr>
      <th>Setting</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Mode</td>
      <td>SPI Mode 0</td>
    </tr>
    <tr>
      <td>Clock polarity</td>
      <td>Idle Low</td>
    </tr>
    <tr>
      <td>Clock phase</td>
      <td>Sample on first edge</td>
    </tr>
    <tr>
      <td>Bit order</td>
      <td>MSB first</td>
    </tr>
    <tr>
      <td>Data size</td>
      <td>8-bit</td>
    </tr>
    <tr>
      <td>Initial speed</td>
      <td>below 400 kHz</td>
    </tr>
    <tr>
      <td>Runtime speed</td>
      <td>&gt; 400 kHz</td>
    </tr>
  </tbody>
</table>

<h1 id="the-spi-driver">The SPI driver</h1>

<p>This is deliberately not a generic multi bus SPI framework. It is a small board support module for SPI1 on this board. If I later need SPI2 or multiple chip select lines, I can extend the API.</p>

<p>Spoiler alert I’m going to show you the header file first, with functions based on the requirements for the driver that I set out at the beginning of this post:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#pragma once
#include</span> <span class="cpf">&lt;stdint.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stddef.h&gt;</span><span class="cp">
</span>
<span class="kt">void</span> <span class="nf">bsp_spi1_init</span><span class="p">(</span><span class="kt">void</span><span class="p">);</span>

<span class="kt">void</span> <span class="nf">bsp_spi1_set_slow</span><span class="p">(</span><span class="kt">void</span><span class="p">);</span> <span class="c1">// &lt; 400 kHz</span>
<span class="kt">void</span> <span class="nf">bsp_spi1_set_fast</span><span class="p">(</span><span class="kt">void</span><span class="p">);</span> <span class="c1">// &gt; 400 kHz</span>

<span class="kt">uint8_t</span> <span class="nf">bsp_spi1_transfer</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="n">data</span><span class="p">);</span>

<span class="kt">void</span> <span class="nf">bsp_spi1_write_buffer</span><span class="p">(</span><span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">data</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">len</span><span class="p">);</span>
<span class="kt">void</span> <span class="nf">bsp_spi1_read_buffer</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="n">data</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">len</span><span class="p">);</span>
<span class="kt">void</span> <span class="nf">bsp_spi1_transfer_buffer</span><span class="p">(</span><span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">tx</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">rx</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">len</span><span class="p">);</span>

<span class="kt">void</span> <span class="nf">bsp_spi1_cs_low</span><span class="p">(</span><span class="kt">void</span><span class="p">);</span>
<span class="kt">void</span> <span class="nf">bsp_spi1_cs_high</span><span class="p">(</span><span class="kt">void</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="initialisation">Initialisation</h2>
<p>Let’s start the bsp_spi.c file, I’m going to define the pins at the top of the file with macros to make it more understandable when I inevitably forget what I’ve done, then we will run through the initialisation function with the settings discussed above, - I’ve covered most of these GPIO inits in previous posts. I’ve stuck with MISO and MOSI rather than the newer COPI and CIPO because the libopencm3 version I’m using uses master and slave in some of their function calls and I don’t want to confuse myself even more than I already am:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="cp">#include</span> <span class="cpf">"bsp_spi.h"</span><span class="cp">
</span>
<span class="cp">#include</span> <span class="cpf">&lt;libopencm3/stm32/rcc.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;libopencm3/stm32/gpio.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;libopencm3/stm32/spi.h&gt;</span><span class="cp">
</span>
<span class="c1">// Pin macros</span>
<span class="cp">#define BSP_SPI1_PORT GPIOA
#define BSP_SPI1_CS GPIO4
#define BSP_SPI1_SCK GPIO5
#define BSP_SPI1_MISO GPIO6
#define BSP_SPI1_MOSI GPIO7
</span>
<span class="kt">void</span> <span class="nf">bsp_spi1_init</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>

    <span class="c1">// Init clocks</span>
    <span class="n">rcc_periph_clock_enable</span><span class="p">(</span><span class="n">RCC_GPIOA</span><span class="p">);</span>
    <span class="n">rcc_periph_clock_enable</span><span class="p">(</span><span class="n">RCC_AFIO</span><span class="p">);</span> <span class="c1">// PA5 (SCK) &amp; PA7 (MOSI)</span>
    <span class="n">rcc_periph_clock_enable</span><span class="p">(</span><span class="n">RCC_SPI1</span><span class="p">);</span>

    <span class="c1">// Init GPIO: CS - Preload register so card isn't selected on init, then init the pin</span>
    <span class="n">gpio_set</span><span class="p">(</span><span class="n">BSP_SPI1_PORT</span><span class="p">,</span> <span class="n">BSP_SPI1_CS</span><span class="p">);</span>
    <span class="n">gpio_set_mode</span><span class="p">(</span><span class="n">BSP_SPI1_PORT</span><span class="p">,</span>
                  <span class="n">GPIO_MODE_OUTPUT_50_MHZ</span><span class="p">,</span>
                  <span class="n">GPIO_CNF_OUTPUT_PUSHPULL</span><span class="p">,</span>
                  <span class="n">BSP_SPI1_CS</span><span class="p">);</span>

    <span class="c1">// Init GPIO: SCK and MOSI</span>
    <span class="n">gpio_set_mode</span><span class="p">(</span><span class="n">BSP_SPI1_PORT</span><span class="p">,</span>
                  <span class="n">GPIO_MODE_OUTPUT_50_MHZ</span><span class="p">,</span>
                  <span class="n">GPIO_CNF_OUTPUT_ALTFN_PUSHPULL</span><span class="p">,</span>
                  <span class="n">BSP_SPI1_SCK</span> <span class="o">|</span> <span class="n">BSP_SPI1_MOSI</span><span class="p">);</span>

    <span class="c1">// Init GPIO: MISO</span>
    <span class="n">gpio_set_mode</span><span class="p">(</span><span class="n">BSP_SPI1_PORT</span><span class="p">,</span>
                  <span class="n">GPIO_MODE_INPUT</span><span class="p">,</span>
                  <span class="n">GPIO_CNF_INPUT_FLOAT</span><span class="p">,</span> <span class="c1">// for SD card it should drive this pin, if not connected then it is actually floating!</span>
                  <span class="n">BSP_SPI1_MISO</span><span class="p">);</span>

    <span class="c1">// Configure SPI</span>
    <span class="n">spi_disable</span><span class="p">(</span><span class="n">SPI1</span><span class="p">);</span>
    <span class="n">spi_set_master_mode</span><span class="p">(</span><span class="n">SPI1</span><span class="p">);</span>
    <span class="n">spi_set_baudrate_prescaler</span><span class="p">(</span><span class="n">SPI1</span><span class="p">,</span> <span class="n">SPI_CR1_BR_FPCLK_DIV_256</span><span class="p">);</span>
    <span class="n">spi_set_clock_polarity_0</span><span class="p">(</span><span class="n">SPI1</span><span class="p">);</span>
    <span class="n">spi_set_clock_phase_0</span><span class="p">(</span><span class="n">SPI1</span><span class="p">);</span>
    <span class="n">spi_send_msb_first</span><span class="p">(</span><span class="n">SPI1</span><span class="p">);</span>
    <span class="n">spi_set_full_duplex_mode</span><span class="p">(</span><span class="n">SPI1</span><span class="p">);</span>
    <span class="n">spi_set_unidirectional_mode</span><span class="p">(</span><span class="n">SPI1</span><span class="p">);</span> <span class="c1">// Disables STM32's one-wire bidirectional mode</span>

    <span class="c1">// because using manual gpio cs pin</span>
    <span class="n">spi_enable_software_slave_management</span><span class="p">(</span><span class="n">SPI1</span><span class="p">);</span>
    <span class="n">spi_set_nss_high</span><span class="p">(</span><span class="n">SPI1</span><span class="p">);</span>

    <span class="n">spi_enable</span><span class="p">(</span><span class="n">SPI1</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="spi-frequency-speed">SPI frequency (“speed”)</h2>
<p>Once the card has been initialised we can increase the SPI speed, I’ll create two functions for this; a slow and a fast setting. Libopencm3 provides safe ways to do this:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">bsp_spi1_set_slow</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>

    <span class="n">spi_disable</span><span class="p">(</span><span class="n">SPI1</span><span class="p">);</span>
    <span class="n">spi_set_baudrate_prescaler</span><span class="p">(</span><span class="n">SPI1</span><span class="p">,</span> <span class="n">SPI_CR1_BR_FPCLK_DIV_256</span><span class="p">);</span><span class="c1">// 281.25 kHz at 72MHz(APB2)/256</span>
    <span class="n">spi_enable</span><span class="p">(</span><span class="n">SPI1</span><span class="p">);</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="nf">bsp_spi1_set_fast</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    
    <span class="n">spi_disable</span><span class="p">(</span><span class="n">SPI1</span><span class="p">);</span>
    <span class="n">spi_set_baudrate_prescaler</span><span class="p">(</span><span class="n">SPI1</span><span class="p">,</span> <span class="n">SPI_CR1_BR_FPCLK_DIV_128</span><span class="p">);</span><span class="c1">// 562.5 kHz at 72MHz(APB2)/128</span>
    <span class="n">spi_enable</span><span class="p">(</span><span class="n">SPI1</span><span class="p">);</span>

<span class="p">}</span>
</code></pre></div></div>

<h2 id="chip-select">Chip select</h2>
<p>The chip select will just be toggling a GPIO pin:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">bsp_spi1_cs_low</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">gpio_clear</span><span class="p">(</span><span class="n">BSP_SPI1_PORT</span><span class="p">,</span> <span class="n">BSP_SPI1_CS</span><span class="p">);</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="nf">bsp_spi1_cs_high</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">gpio_set</span><span class="p">(</span><span class="n">BSP_SPI1_PORT</span><span class="p">,</span> <span class="n">BSP_SPI1_CS</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>For the SD card the driver above this layer, I’ll pull CS low before sending a command or reading a block, then release it afterwards.</p>

<h2 id="transferring-one-byte">Transferring one byte</h2>

<p>Transfer a single byte via spi_xfer() which writes a byte and returns the byte recieved at the same time:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// wrapper for SPI1 transfer</span>
<span class="kt">uint8_t</span> <span class="nf">bsp_spi1_transfer</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="n">data</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">spi_xfer</span><span class="p">(</span><span class="n">SPI1</span><span class="p">,</span> <span class="n">data</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The transfer function deliberately does not touch CS. Higher-level drivers decide where a transaction begins and ends.</p>

<p>For example you can send a 0xFF to run the SPI clock and then return whatever the card sends back:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">uint8_t</span> <span class="n">response</span> <span class="o">=</span> <span class="n">bsp_spi1_transfer</span><span class="p">(</span><span class="mh">0xFF</span><span class="p">);</span>
</code></pre></div></div>

<p>For our use case sending <code class="language-plaintext highlighter-rouge">0xFF</code> is a common SPI trick for SD cards, it keeps MOSI high while still generating clock pulses, allowing the card to return data on MISO.</p>

<p>Looking at <code class="language-plaintext highlighter-rouge">spi_xfer()</code> in libopencm3, this is a blocking byte transfer API: it writes to the SPI data register and waits until received data is available. For a small project that is fine, but I will probably add timeout handling, and possibly DMA for larger transfers later on.</p>

<h1 id="buffer-helper-functions">Buffer helper functions</h1>

<p>I’m going to create a read and write helper to make the transfer function cleaner when in use:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c1">// Write </span>
<span class="kt">void</span> <span class="nf">bsp_spi1_write_buffer</span><span class="p">(</span><span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">data</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">len</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span><span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">bsp_spi1_transfer</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="n">i</span><span class="p">]);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Read</span>
<span class="kt">void</span> <span class="nf">bsp_spi1_read_buffer</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="n">data</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">len</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">data</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">bsp_spi1_transfer</span><span class="p">(</span><span class="mh">0xFF</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Write Read</span>
<span class="kt">void</span> <span class="nf">bsp_spi1_transfer_buffer</span><span class="p">(</span><span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">tx</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">rx</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">len</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">uint8_t</span> <span class="n">out</span> <span class="o">=</span> <span class="n">tx</span> <span class="o">?</span> <span class="n">tx</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">:</span> <span class="mh">0xFF</span><span class="p">;</span> <span class="c1">// If tx is null send 0xFF</span>
        <span class="kt">uint8_t</span> <span class="n">in</span> <span class="o">=</span> <span class="n">bsp_spi1_transfer</span><span class="p">(</span><span class="n">out</span><span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">rx</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">rx</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">in</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="testing-the-spi-driver">Testing the SPI driver</h2>

<p>To test the SPI functionality I will hook up the SPI lines on the board to an oscilloscope and check:</p>

<ul>
  <li>CS should idle high</li>
  <li>CS should go low during transfer</li>
  <li>SCK should toggle</li>
  <li>MOSI should show the transmitted byte</li>
  <li>MISO will float or remain high if nothing is connected</li>
</ul>

<p>Let’s first check that the CS pin goes high and low, so I’ve cobbled together this to go in a main.c file. The <code class="language-plaintext highlighter-rouge">nop</code> delay is rough, but it is enough for a quick hardware sanity check.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"bsp_spi.h"</span><span class="cp">
#include</span> <span class="cpf">&lt;libopencm3/stm32/rcc.h&gt;</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    
    <span class="n">rcc_clock_setup_pll</span><span class="p">(</span><span class="o">&amp;</span><span class="n">rcc_hse_configs</span><span class="p">[</span><span class="n">RCC_CLOCK_HSE8_72MHZ</span><span class="p">]);</span>
    <span class="n">bsp_spi1_init</span><span class="p">();</span>

    <span class="k">while</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">bsp_spi1_cs_low</span><span class="p">();</span>
        <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">100000</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span><span class="n">__asm__</span><span class="p">(</span><span class="s">"nop"</span><span class="p">);}</span>

        <span class="n">bsp_spi1_cs_high</span><span class="p">();</span>
        <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">100000</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span><span class="n">__asm__</span><span class="p">(</span><span class="s">"nop"</span><span class="p">);}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="/docs/assets/img/blog-13-scope-cs.png" alt="Oscilloscope measurement showing chip select working" /></p>

<p>That seems to work. Now let’s check the clock speed and some data packets:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">while</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">bsp_spi1_cs_low</span><span class="p">();</span>
    <span class="n">bsp_spi1_transfer</span><span class="p">(</span><span class="mh">0xAA</span><span class="p">);</span> <span class="c1">// 0b10101010</span>
    <span class="n">bsp_spi1_transfer</span><span class="p">(</span><span class="mh">0xAF</span><span class="p">);</span> <span class="c1">// 0b10101111</span>
    <span class="n">bsp_spi1_cs_high</span><span class="p">();</span>

    <span class="k">for</span> <span class="p">(</span><span class="k">volatile</span> <span class="kt">uint32_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">100000</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">);</span> <span class="c1">//bit of a pause to make it easier to spot</span>
<span class="p">}</span>
</code></pre></div></div>
<p>I’ve used Picoscope 7 which has some nice logic analyser features built in, they have a specific setting for SPI - SDIO which only needs the clock and MOSI line (perfect as I only have 2 channels on my scope):</p>

<p><img src="/docs/assets/img/blog-13-scope-clk-freq.png" alt="Oscilloscope measurement showing 281.3 kHz Frequency clock and AA AF data" /></p>

<p>You can see the clock frequency measured is bang on 281.2 kHz on the red trace (B) and both data packets are there on the blue trace (A) too!</p>

<h1 id="next">Next?</h1>
<p>Now I have a small SPI1 board support package that can:</p>

<ul>
  <li>Initialise SPI1</li>
  <li>Control chip select manually</li>
  <li>Send and receive bytes</li>
  <li>Read and write buffers</li>
  <li>Start slow and switch faster later</li>
</ul>

<p>The next step is to write the SD card driver! That driver will send SD commands such as CMD0, CMD8 and ACMD41, move the card into SPI mode, and eventually provide the 512-byte block read/write functions that FatFS expects.</p>

<script src="https://utteranc.es/client.js" repo="skoopsy/skoopsy.github.io" issue-term="pathname" label="blog-embedded12" theme="preferred-color-scheme" crossorigin="anonymous" async="">
</script>

<p>Copyright © 2026 David O’Connor</p>]]></content><author><name>Dave</name></author><category term="STM32" /><summary type="html"><![CDATA[In the previous post I looked at adding microSD storage to an STM32F1xx project. The useful conclusion was that a microSD card can be used over SPI, with FatFS sitting above a low-level block read/write layer. Before getting anywhere near FatFS though, I need a clean way to actually use SPI from the STM32F1xx.]]></summary></entry><entry><title type="html">STM32 #10: An introduction to microSD Storage on STM32F1xx: FAT(fs), SPI, and the Hardware Setup</title><link href="https://skoopsy.dev/stm32/2026/06/01/STM32-11-microsd.html" rel="alternate" type="text/html" title="STM32 #10: An introduction to microSD Storage on STM32F1xx: FAT(fs), SPI, and the Hardware Setup" /><published>2026-06-01T10:00:00+00:00</published><updated>2026-06-01T10:00:00+00:00</updated><id>https://skoopsy.dev/stm32/2026/06/01/STM32-11-microsd</id><content type="html" xml:base="https://skoopsy.dev/stm32/2026/06/01/STM32-11-microsd.html"><![CDATA[<p>This post will document some of the ground work for understanding how to connect and use a microSD in a simple manner with the STM32F103x but will leave full implementation until a later post.</p>
<h2 id="why-add-local-storage">Why add local storage</h2>

<p>I have built a small device that is streaming I2C sensor data over UART(USB) to a custom logging web GUI. I now want to make the device wearable and so two initial phases come to mind; Making it battery powered, and giving it local storage for the sensor data logs.</p>

<h1 id="sd-card-storage-on-the-stm32f1xx">SD Card Storage on the STM32F1xx</h1>

<p>For this project, the easiest way to add removable local storage is probaly via microSD card over SPIm with FatFS provding the filesystem layer. FatFS was developed by <a href="https://elm-chan.org/fsw/ff/">ChaN</a>. If using as FAT12, FAT16, or FAT32 <a href="https://elm-chan.org/fsw/ff/doc/appnote.html">the license</a> is very similar to a BSD style. If using the exFAT implementation then a license via microsoft may be required. Sticking with FAT32 should be acceptable (giving an upper limit of 2TB (512bytes/sector) on the SDCard volume size, and maximum OS compatability) for most embedded applications.</p>

<h2 id="ways-to-organise-memory-for-embedded">Ways to organise memory for embedded</h2>
<p>Here is a quick orientation on some of the options when adding memory media management to an embedded project. This is not a complete filesystem comparison, but it was enough to orient my own decision making:</p>

<table>
  <thead>
    <tr>
      <th>File System</th>
      <th>Safety Type</th>
      <th>Corruption Risk</th>
      <th>Unwritten Data Retention</th>
      <th>SD Physical Wear</th>
      <th>Boot Recovery Speed</th>
      <th>Native Compatability</th>
      <th>Commercial Licensing</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>FAT32 (FatFs)</td>
      <td>None</td>
      <td>High</td>
      <td>Loses last unclosed file</td>
      <td>Low to medium</td>
      <td>Fast</td>
      <td>PC, Mac, Linux</td>
      <td>1-Clause BSD</td>
    </tr>
    <tr>
      <td>exFAT (FatFs)</td>
      <td>None</td>
      <td>High</td>
      <td>Loses last unclosed file</td>
      <td>Low to medium</td>
      <td>Fast</td>
      <td>PC, Mac, Linux</td>
      <td>Microsoft Licensing</td>
    </tr>
    <tr>
      <td>LittleFs</td>
      <td>Copy-on-write</td>
      <td>Low</td>
      <td>Loses data from uncommited write cycle</td>
      <td>Low</td>
      <td>Instant</td>
      <td>All with tools</td>
      <td>BSD-3-Clause or Apache 2.0</td>
    </tr>
    <tr>
      <td>Ext4 (lwext4)</td>
      <td>Metadata Journal</td>
      <td>Low</td>
      <td>Loses data from uncommited write cycle</td>
      <td>High</td>
      <td>Medium</td>
      <td>Linux, others with tools</td>
      <td>BSD-3-Clause and GPLv2</td>
    </tr>
    <tr>
      <td>UDF</td>
      <td>VATs</td>
      <td>Low</td>
      <td>Loses data from uncommited write cycle</td>
      <td>Low</td>
      <td>Instant</td>
      <td>PC, Mac, Linux</td>
      <td>Open, royalty-free ISO standard</td>
    </tr>
    <tr>
      <td>Writing Raw Blocks</td>
      <td>Up to you</td>
      <td>Up to you</td>
      <td>Loses data from MCU RAM</td>
      <td>Low</td>
      <td>Instant (Direct Pointer)</td>
      <td>None</td>
      <td>Your own</td>
    </tr>
  </tbody>
</table>

<p>There is also SPIFFS but apprently it is not appropriate for SD Cards due to wear.</p>

<p>From the table above there are several interesting options with LittleFS and UDF being the most interesting, but with FAT32 being the lowest barrier to access with FatFS being readily available, and the easiest royalty free system to develop with. In the future I may try LittleFS.</p>

<h2 id="introduction-to-fatfs-and-the-fat-filesystem-on-a-microsd">Introduction to FatFS and the FAT filesystem on a microSD</h2>
<p>Firstly, a microSD card has no understanding of files and folders. It is essentially just a large array of storage blocks that are numbered. FatFS is a software layer that gives structure to the memory blocks.</p>

<p>FatFS is fairly easy to use e.g:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">f_open</span><span class="p">(</span><span class="o">&amp;</span><span class="n">file</span><span class="p">,</span> <span class="s">"data.txt"</span><span class="p">,</span> <span class="n">FA_WRITE</span> <span class="o">|</span> <span class="n">FA_CREATE_ALWAYS</span><span class="p">);</span>
<span class="n">f_write</span><span class="p">(</span><span class="o">&amp;</span><span class="n">file</span><span class="p">,</span> <span class="n">buffer</span><span class="p">,</span> <span class="n">bytes</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">bw</span><span class="p">);</span>
<span class="n">f_close</span><span class="p">(</span><span class="o">&amp;</span><span class="n">file</span><span class="p">);</span>
</code></pre></div></div>

<p>Underneath that API, FatFS is managing sectors, clusters, directories, and volumes.</p>

<h3 id="sectors">Sectors</h3>
<p>For SDHC cards, the logical block length is fixed at 512 bytes. That maps cleanly onto FatFS, because the low-level disk interface reads and writes numbered 512-byte sectors.</p>

<p>So the SD card will be organised at the lowest level into sector 0, sector 1, sector 2 etc. When FatFS wants to read a file, behind the api FatFS will be running:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">disk_read</span><span class="p">(</span><span class="n">pdrv</span><span class="p">,</span> <span class="n">buffer</span><span class="p">,</span> <span class="n">sector_number</span><span class="p">,</span> <span class="n">sector_count</span><span class="p">);</span>
</code></pre></div></div>

<p>for example it might read 1 sector starting at sector 2048 which means read 512 bytes from sector 2048.</p>

<p>The SD card just knows these numbered sectors/blocks.</p>

<h3 id="clusters">Clusters</h3>
<p>A group of one or more sectors. FatFS allocates storage in clusters, for example a cluster might equal 8 sectors = 8 x 512 bytes = 4096 bytes.</p>

<p>A file will typically occupy multiple clusters, these might be contiguous or fragmented in the cluster order. The File Allocation Table (FAT) records which cluster comes next e.g. cluster 14 -&gt; cluster 64. Cluster 64 -&gt; 872. The table tells the file system how data is linked together.</p>

<h3 id="directories">Directories</h3>
<p>This is just a special file that contrains a list of file entries e.g. LOG.TXT, SENSOR.TXT, CONFIG.TXT. The directories are used when you run f_open or f_close to look up the file name entry which tells FatFS where the first file cluster is and the file size. Then FatFS uses the FAT table to follow the linked clusters.</p>

<h3 id="volumes">Volumes</h3>
<p>A volume is just the formatted filesystem area so generally you would see a microSD card setup as one FAT volume. You could have multiple volumes on the card but for our embedded application it is easier to use one volume with folders such as 0:/config/ 0:/calibrations/ 0:/updates 0:/logs/ 0:/data/</p>

<h3 id="basic-layout-of-a-fat-formatted-microsd-card">Basic layout of a FAT-formatted microSD card</h3>
<p>I won’t go into too much detail here but you typically have these main areas:</p>
<ul>
  <li>Boot sector / BPB (Contains information about the filesystem layout)</li>
  <li>FAT table 1</li>
  <li>FAT table 2, optional copy</li>
  <li>Root directory</li>
  <li>Data area (file and directory clusters)</li>
</ul>

<h1 id="a-note-on-cluster-size">A note on cluster size</h1>
<p>The set cluster size will define the minimum amount of memory taken up by a file. For example if you have a file which contains 1KB of data but the minimum cluster size is 32KB then the file will be 32KB in size with a wasted 31 KB. So if lots of files are logged per data this could accumulate into significant amounts of memory very quickly! Therefore ingeneral for this application fewer files which are larger in size is going to be more efficinent unless we start controlling each file size.</p>

<p>For SDHC, one sector is 512 bytes, so the cluster size is normally set as a number of sectors per cluster:</p>

<p>1 sector per cluster = 512 bytes
8 sectors per cluster = 4 KB
32 sectors per cluster = 16 KB
64 sectors per cluster = 32 KB</p>

<p>You can’t just make the cluster size equal to 1 sector in all cases, there are obviously drawbacks to that. The FAT table must store one entry for every cluster and so for a 100 MB file using 512 byte clusters that would result in 204,800 FAT entries which is 819,200 bytes in comparison to 32 KB clusters which would result in 3200 FAT entries which is 12800 bytes. So tiny clusters make the FAT table much larger and makes FatFS do more work when following or extending a file. Furthermore the actual hardware implementation of the SD memory may be working at much larger memory sizes to erase data.</p>

<p>Essentially tiny clusters are only good when you have lots of tiny files to write such as config files, tiny text logs, tiny marker files, tiny calibration files and you are very tight on space. Larger clusters are going to be good when you have larger files that require sequential writes such as bigger data logging files from sensor capture.</p>

<h2 id="file-names">File Names</h2>
<p>Short File Name (SFN) vs Long File Name (LFN): SFNs use the <a href="https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/18e63b13-ba43-4f5f-a5b7-11e871b71f14">DOS 8.3 style</a> which is up to 8 characters for the name, a period, and up to 3 charaters for the extension, and stored as upper-case (Although itis case insensitive in the FatFS implementation) e.g. SENSOR01.TXT. LFNs allow up to 255 characters and retain mixed case lettering and spaces, this must be explicitly enabled in FatFS.</p>

<p>Why use SFNs:</p>

<ul>
  <li>
    <p>Exach directory entry requires exactly 32 bytes of disk space giving it minimal memory footprint</p>
  </li>
  <li>
    <p>This also requires minimal RAM usage of 32 bytes, compared to an LFN using up to 512 bytes, which is a concern as the STM32F103 only has 20KB.</p>
  </li>
  <li>
    <p>It makes it faster to open, read, and write, because the directory search requires less bytes</p>
  </li>
  <li>
    <p>Universal compatibility across all FAT systems.</p>
  </li>
</ul>

<p>Considering the intial files be logged will be time series data over days and months a format such as YYMMDDxx.csv is likely most appropriate for now. The “xx” may be used to denote a sequence of log files in a day, or if I decide to separate data out into filesand I stay safe by sticking to 0-9 and A-Z for the “xx” then we have 10 numbers + 26 letters which gives 36 options per character, since we have two characters we have 36 x 36 = 1296 unique possibilities, keeping upper case only to remove any issues with string searches in C which are case sensitive, whereas FatFS is case insensitive.</p>

<p>If you really want to optimise the RAM usage then setting the filenames in hexidecimal in your code will slightly optimise the C code and still give you 16 x 16 = 256 unique filenames per day, but makes them a pain to read manually without a post-hoc conversion.</p>

<p>Alternatively you can use a small amount of extra space by designating the files into directories that are 8.3 compliant for much more organisation. e.g. 0:/DATA/PPG/250101XX.CSV - This still uses less RAM than enabling the LFN, and if more than 1 file lands in each directory then you’ve saved memory foot print too as the directory is only logged once in the FAT.</p>

<p>That was way too much on filenames.</p>

<h2 id="connecting-a-microsd-to-stm32f1xx-via-spi">Connecting a microSD to STM32F1xx via SPI</h2>
<p>microSD supports the SPI bus protocol natively. By default they operate in a native 4-bit SD mode, but can be forced into SPI mode with an initialisation command. This means you can directly wire the stm32f103x to the microSD card and it should work, as long as it is operating at 3.3v. There are published <a href="https://www.sdcard.org/downloads/pls/">“simplified” standards</a> that are freely available for standard (SD upto 2GB) and high capacity (SDHC upto 32GB) SD memory cards which includes the bus protocol and pinouts, along with a whole heap of the functionality. I’m not covering SDXC up to 2TB but it is backwards compatible to an extent with SD and SDHC protocols, <a href="https://www.sdcard.org/developers/sd-standard-overview/capacity-sd-sdhc-sdxc-sduc/">there is also SDUC - up to 128TB…</a>. Most importantly the SDHC standard uses a FAT32 file system which we will focus upon, if using cards smaller than 2GB it will be the SD standard which uses FAT 12, and FAT 16.</p>

<p>Some specifications for the SDHC standard</p>

<ul>
  <li>Operating voltage: 2.7 - 3.6 V</li>
  <li>Default mode: 12.6 MB/sec (Clock 0-25 MHz)</li>
  <li>High-speed mode: 25 MB/sec (Clock 0-50 MHZ)</li>
  <li>Password protection</li>
  <li>9 pins: clock, command, 3 power, and 4 data</li>
</ul>

<p>The pinouts for SD cards are available in the simplified standards, but not for microSD, this appears to be in an addendum I cannot find, maybe it is behind the SD association paywall. Maybe I have missed something but the pinouts were frustratingly difficult to find from an official source, even microSD card datasheets don’t seem to have it, so I ended up going onto mouser, searching up microSD connectors and looking at the datasheets, here is the pinout:</p>

<p><img src="/docs/assets/img/blog-12-microsd-pinout.png" alt="pinout for microSD micro SD and UHS-II specification" /></p>

<table>
  <thead>
    <tr>
      <th>Pin</th>
      <th>Micro SD &amp; Express Mode</th>
      <th>SPI Mode</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Data Line [Bit 2]</td>
      <td>N/a</td>
    </tr>
    <tr>
      <td>2</td>
      <td>Card detect / Data line [Bit 3]</td>
      <td>Chip Select</td>
    </tr>
    <tr>
      <td>3</td>
      <td>Command / Response</td>
      <td>COPI (MOSI)</td>
    </tr>
    <tr>
      <td>4</td>
      <td>Supply voltage (3.3v)</td>
      <td>Supply voltage (3.3v)</td>
    </tr>
    <tr>
      <td>5</td>
      <td>Clock</td>
      <td>SCLK</td>
    </tr>
    <tr>
      <td>6</td>
      <td>Supply voltage ground</td>
      <td>Supply voltage ground</td>
    </tr>
    <tr>
      <td>7</td>
      <td>Data Line [Bit 0] / RCLK +</td>
      <td>CIPO (MISO)</td>
    </tr>
    <tr>
      <td>8</td>
      <td>Data Line [Bit 1] / RCLK -</td>
      <td>N/a</td>
    </tr>
    <tr>
      <td> </td>
      <td><em>Below are only for UHS-II</em></td>
      <td> </td>
    </tr>
    <tr>
      <td>9</td>
      <td>Supply voltage (1.8v)</td>
      <td> </td>
    </tr>
    <tr>
      <td>10</td>
      <td>Supply voltage ground</td>
      <td> </td>
    </tr>
    <tr>
      <td>11</td>
      <td>UHS-II D0+</td>
      <td> </td>
    </tr>
    <tr>
      <td>12</td>
      <td>UHS-II D0-</td>
      <td> </td>
    </tr>
    <tr>
      <td>13</td>
      <td>Supply voltage ground</td>
      <td> </td>
    </tr>
    <tr>
      <td>14</td>
      <td>UHS-II D1-</td>
      <td> </td>
    </tr>
    <tr>
      <td>15</td>
      <td>UHS-II D1+</td>
      <td> </td>
    </tr>
    <tr>
      <td>16</td>
      <td>Supply voltage ground</td>
      <td> </td>
    </tr>
    <tr>
      <td>17</td>
      <td>Single Wire Protocol Interface</td>
      <td> </td>
    </tr>
  </tbody>
</table>

<p>I added the UHS-II pins because they were there and I thought it may save me effort finding it again later!</p>

<p>So with that you could directly solder to a microSD connector and be away with SPI, or more reasonably you might have a custom PCB with a microSD connector, or you might have a breakout board such as the Adafruit microSD to SPI and SPIO</p>

<p>So I have connected the following pins, I’ve included the adafruit breakout but have not tried it:</p>

<table>
  <thead>
    <tr>
      <th>STM32F1xx</th>
      <th>microSD Pin</th>
      <th>Adafruit Breakout</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>3.3v</td>
      <td>Pin 4 V Supply</td>
      <td>3V Supply</td>
    </tr>
    <tr>
      <td>GND</td>
      <td>Pin 6 GND</td>
      <td>GND</td>
    </tr>
    <tr>
      <td>PA4 (GPIO CS)</td>
      <td>Pin 2 CS</td>
      <td>CS</td>
    </tr>
    <tr>
      <td>PA5 (SPI1_SCK)</td>
      <td>Pin 5 SCLK</td>
      <td>SCK / CLK</td>
    </tr>
    <tr>
      <td>PA6 (SPI1_MISO)</td>
      <td>Pin 7 CIPO</td>
      <td>MISO / DO</td>
    </tr>
    <tr>
      <td>PA7 (SPI1_MOSI)</td>
      <td>Pin 3 COPI</td>
      <td>MOSI / DI</td>
    </tr>
  </tbody>
</table>

<p>Here is the abomination, it took me two tries until I switch from lead-free to the good stuff:
<img src="/docs/assets/img/blog-12-microsd-conn-solder.jpg" alt="Image of SPI wires soldered to a microSD connector" /></p>

<p><img src="/docs/assets/img/blog-12-microsd-conn-solder2.jpg" alt="Image of SPI wires soldered to a microSD connector" /></p>

<p>I have also added 0603 10kOhm pull ups between 3.3v and CMD, MOSI, MISO, and DAT1, DAT2 lines (for piece of mind incase there are issues with floating pins) and a 100nF(close to the microSD), and 10uF cap between V supply and GND to smooth out any noise and provide a reserve for current spikes at startup… Just use a dedicated breakout board unless you need the space savings!</p>

<h1 id="microsd-spi-mode-introduction">microSD SPI Mode Introduction</h1>

<p>We don’t need to cover too much of the protocol as FatFS will handle that but here are some key points that are good to know:</p>

<ul>
  <li>SPI mode can be selected during the first reset command after a power cycle (CMD0) and cannot be changed once powered on.</li>
  <li>SPI mode is slower than standard SD mode due to using a single data line but makes the implementation fairly straight forward.</li>
  <li>SPI mode uses a subset of commands and functions from the SD mode before version 2.00 of the standard, and it does not work for SDUC cards.</li>
  <li>Every data token must be aligned with 8 clock cycles as a byte along with the CS signal.</li>
  <li>The host starts a bus transaction by pulling the CS pin low.</li>
  <li>If an error occurs with data retrieval the card will return an error response rather than the expected data block.</li>
  <li>For SDHC the data block length is fixed to 512 bytes, so partial read/write operations are not possible.</li>
  <li>Write protected commands are not available (CMD28, CMD29, CMD30)</li>
</ul>

<h1 id="what-next">What next?</h1>
<p>Next I will write a SPI driver using libopencm3 for the STM32F103xx so that I can communicate with the card. After that  I will develop the FatFs implementation to start storing the sensor data</p>

<script src="https://utteranc.es/client.js" repo="skoopsy/skoopsy.github.io" issue-term="pathname" label="blog-embedded12" theme="preferred-color-scheme" crossorigin="anonymous" async="">
</script>

<p>Copyright © 2026 David O’Connor</p>]]></content><author><name>Dave</name></author><category term="STM32" /><summary type="html"><![CDATA[This post will document some of the ground work for understanding how to connect and use a microSD in a simple manner with the STM32F103x but will leave full implementation until a later post. Why add local storage]]></summary></entry><entry><title type="html">STM32 #9: A simple I2C driver on STM32F1xx with libopencm3</title><link href="https://skoopsy.dev/stm32/2025/12/20/STM32-10-i2c.html" rel="alternate" type="text/html" title="STM32 #9: A simple I2C driver on STM32F1xx with libopencm3" /><published>2025-12-20T10:00:00+00:00</published><updated>2025-12-20T10:00:00+00:00</updated><id>https://skoopsy.dev/stm32/2025/12/20/STM32-10-i2c</id><content type="html" xml:base="https://skoopsy.dev/stm32/2025/12/20/STM32-10-i2c.html"><![CDATA[<p>After a bit of searching it seemed I was not going to find a nice library to use some of the peripheral boards I have in mind, especially whilst trying to avoid the STM32cubeIDE. Which lead me writing drivers…</p>

<h1 id="i2c-basics">I2C Basics:</h1>

<p>Inter-Intergrated Circuit protocol (I2C, pronounced “I squared C”) is a synchronous, multi-master, multi-slave, single-ended serial bus which was invented by Phillips Semiconductor. It is often used for communicating with low-speed peripherals from a micro in short distance applications such as intraboard comms.</p>

<p>2 connections:</p>

<ul>
  <li>SDA - Serial Dataline Line</li>
  <li>SCL - Serial Clock Line - at the same frequency as the data bits</li>
</ul>

<p>Both SDA and SCL are normally open-drain lines. Devices can pull the line low, but they do not actively drive it high. Adding pull-up resistors bring the lines back to 3.3 V when nobody is pulling them down.</p>

<p>You need to know the data length (packet size) in bits, the clock frequency for both the TX and RX (only defined by the controller), and the device address. The master TX can communicate with multiple slave RX via the use of a slave address. It will send the slave address first, and then the data.</p>

<p>Because SDA is shared for both transmit and recieve, data only moves in one direction at a time, making I2C half-duplex rather than full-duplex.</p>

<p>Typical data rates:</p>

<ul>
  <li>Standard mode = 100 kbit/s</li>
  <li>Fast mode = 400 kbit/s</li>
  <li>Fast mode plus = 1 Mbit/s</li>
  <li>High speed mode = 3.4 Mbit/s</li>
  <li>Ultra fast mode = 5 Mbit/s</li>
</ul>

<p>An explanation of I2C can be found on the <a href="https://learn.sparkfun.com/tutorials/i2c/all">sparkfun website</a></p>

<h2 id="i2c-protocol">I2C Protocol</h2>

<table>
  <tbody>
    <tr>
      <td>Start condition</td>
      <td>7 bit address</td>
      <td>r/w bit</td>
      <td>ACK / NACK bit</td>
      <td>8 data bits</td>
      <td>ACK/NACK bit</td>
      <td>…repeat</td>
      <td>stop condition</td>
    </tr>
  </tbody>
</table>

<p>ACK - Acknowledge from RX
NACK - Not acknowledge bit from RX</p>

<p>Most I2C sensors expose internal registers. To read one of these registers, the master usually writes the register address first, then performs a repeated-start and reads the returned byte or bytes. This is why embedded I2C drivers often provide a write-read function rather than only separate write and read functions.</p>

<h1 id="some-considerations-for-the-stm32f1x">Some considerations for the STM32F1x</h1>
<p>Some common snags seem to consistently be mentioned online are:</p>
<ul>
  <li>The clock tree and why I2C CR2 register matters</li>
  <li>The GPIO config - Open drain, AF mode, 50 MHz drive speed</li>
  <li>The timing registors CCR and TRISE</li>
  <li>Startup sequence that prevents bus lock</li>
  <li>Difference between write, read, repeated start, ACK control, and stop generation</li>
</ul>

<h1 id="stm32f103x">STM32F103x</h1>

<p>Which bus is I2C on - check RM0008
	- APB1</p>

<p>Which pins are I2C on - Check RM0009
	I2C1 - PB6 (SCL), PB7 (SDA)
	I2C2 - PB10 (SCL), PB11 (SDA)</p>

<h1 id="building-an-i2c-module-with-libopencm3-for-stm32f103x">Building an I2C module with libopencm3 for STM32F103x</h1>

<p>We will need to include a few modules from libopencm3:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;libopencm3/stm32/rcc.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;libopencm3/stm32/gpio.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;libopencm3/stm32/i2c.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;libopencm3/stm32/common/i2c_common_v1.h&gt;</span><span class="cp">
</span></code></pre></div></div>

<p>Lets begin with i2c initialisation, we will use the i2c1 pins:</p>

<table>
  <thead>
    <tr>
      <th>Pin</th>
      <th>Function</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>PB6</td>
      <td>SCL1</td>
    </tr>
    <tr>
      <td>PB7</td>
      <td>SDA1</td>
    </tr>
  </tbody>
</table>

<p>On both of these pins I have added 4.7 kOhm pullups to 3.3v</p>

<p>Lets write the initialisation function, starting with the clocks and GPIO pin setups that we have covered before, these pins are on GPIO bus B, you will also have to activate the clock for I2C1:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">i2c1_init</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Initialise i2c1 on PB6 and PB7</span>

    <span class="c1">// Clocks</span>
    <span class="n">rcc_periph_clock_enable</span><span class="p">(</span><span class="n">RCC_GPIOB</span><span class="p">);</span>
    <span class="n">rcc_periph_clock_enable</span><span class="p">(</span><span class="n">RCC_I2C1</span><span class="p">);</span>

    <span class="c1">// GPIOs</span>
    <span class="n">gpio_set_mode</span><span class="p">(</span><span class="n">GPIOB</span><span class="p">,</span>
                  <span class="n">GPIO_MODE_OUTPUT_50_MHZ</span><span class="p">,</span>
                  <span class="n">GPIO_CNF_OUTPUT_ALTFN_OPENDRAIN</span><span class="p">,</span>
                  <span class="n">GPIO6</span> <span class="o">|</span> <span class="n">GPIO7</span><span class="p">);</span>

<span class="p">}</span>
</code></pre></div></div>
<p>Here we have set PB6 and 7 to be open drain for the I2C to function.</p>

<p>Now we move onto the I2C config which will be inside the same init function as above, I have given some details in the comments:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//i2c1_init(void) { continued...</span>

    <span class="c1">// i2c config</span>
    <span class="n">i2c_prtipheral_disable</span><span class="p">(</span><span class="n">I2C1</span><span class="p">);</span> <span class="c1">// The docs recommend this before changing any configs</span>
    <span class="n">i2c_set_clock_frequency_</span><span class="p">(</span><span class="n">I2C1</span><span class="p">,</span> <span class="mi">36</span><span class="p">);</span> <span class="c1">// Must match the APB1 frequency, the prescaler is 2 so 72/2</span>
    <span class="n">i2c_set_standard_mode</span><span class="p">(</span><span class="n">I2C1</span><span class="p">);</span> <span class="c1">// 100kHz, can go to fast mode for 400 kHz if needed</span>
</code></pre></div></div>

<p>Now setting the CCR frequency confused me to no end and I had to do a deep dive into the docs for this. The system clock on the STM32f103x is set to SYSCLK=72MHz, the ABP1 bus is half this due to the prescaler being 2 so APB1=36MHz, and f_PCLK1 = SYSCLK/2 = 36 MHz, so if the target speed is standard mode which is 100 kHz (Fslc) then to calculate the CCR value use the following equation:</p>

<p>CCR = fpclk1 / (2*Fscl) = 36,000,000 / (2 * 100,000) = 180</p>

<p>Setting the rise time for each bit has a simialr equation in the docs:</p>

<p>trise = (trise(max) / TPCLK1) + 1</p>

<p>trise(max) is typically 1000 ns for standard I2C protocol</p>

<p>TPCLK = 1 / f_PCLK1 = 1 / 36 Mhz = 27.78 ns</p>

<p>So,</p>

<p>trise = (1000 / 27.78) + 1 = 37</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">i2c_set_ccr</span><span class="p">(</span><span class="n">I2C1</span><span class="p">,</span> <span class="mi">180</span><span class="p">);</span>
    <span class="n">i2c_set_trise</span><span class="p">(</span><span class="n">I2C1</span><span class="p">,</span> <span class="mi">37</span><span class="p">)</span>
</code></pre></div></div>

<p>Finally in the init function we can now enable the i2c peripheral:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">i2c_peripheral_enable</span><span class="p">(</span><span class="n">I2C1</span><span class="p">);</span>
</code></pre></div></div>

<p>Don’t forget to close the brackets!</p>

<p>Now create a header file (.h) and add the function</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bsp_i2c1_init</span><span class="p">(</span><span class="kt">void</span><span class="p">);</span>
</code></pre></div></div>

<p>make sure to include this new header file in your i2c.c file!</p>

<p>Now you can initialise an I2C connection, next lets add a write then read function so that we can address a register and read it’s contents:</p>

<h1 id="adding-a-small-i2c-wrapper">Adding a small I2C wrapper</h1>
<p>I don’t really want to have the rest of my project calling libopencm3s i2c_transfer7() function directly everywhere so we will make a small wrapper for it. For this we will start with two simple operations</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">i2c_write</span><span class="p">();</span>
<span class="n">i2c_write_read</span><span class="p">();</span>
</code></pre></div></div>

<p>The first is used when writing data to a device, like writing a value into a sensor register. The second is used more commonly for writing a register address and then read the data back. Which is how many I2C peripheral sensors work.</p>

<p>Before writing the functions its useful to define some status types for debugging:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">enum</span> <span class="p">{</span>
    <span class="n">I2C_OK</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
    <span class="n">I2C_ERR_TIMEOUT</span><span class="p">,</span>
    <span class="n">I2C_ERR_BUS</span><span class="p">,</span>
    <span class="n">I2C_ERR_PARAM</span><span class="p">,</span>
<span class="p">}</span> <span class="n">i2c_status_t</span><span class="p">;</span>
</code></pre></div></div>

<p>This makes the driver interface a bit cleaner, rather than returning random numbers it can return something useful, for example</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">!=</span> <span class="n">I2C_OK</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Do somehting about the error!</span>
<span class="p">}</span>
</code></pre></div></div>

<p>At this stage I’m only going to implement I2C_OK and I2C_ERR_PARAM, but having a timeout and bus errors will be useful for the future.</p>

<h1 id="write-only-transaction">Write only transaction</h1>
<p>Mostly used to configure a I2C peripheral.</p>

<p>e.g. if i wanted to write 0x03 into register 0x09, the transmit buffer would contain:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">uint8_t</span> <span class="n">data</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="mh">0x09</span><span class="p">,</span> <span class="mh">0x03</span><span class="p">};</span>
</code></pre></div></div>

<p>so a simple write function to a 7 bit address using libopencm3 I2C_transfer7() would look like</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">i2c_status_t</span> <span class="nf">i2c_write</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="n">add7</span><span class="p">,</span>
                       <span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">tx</span><span class="p">,</span>
                       <span class="kt">size_t</span> <span class="n">tx_len</span><span class="p">)</span>

<span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">tx</span> <span class="o">||</span> <span class="n">tx_len</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">return</span> <span class="n">I2C_ERR_PARAM</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="n">I2C_transfer7</span><span class="p">(</span><span class="n">I2C1</span><span class="p">,</span>
                      <span class="n">addr7</span><span class="p">,</span>
                      <span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="p">)</span><span class="n">tx</span><span class="p">,</span>
                      <span class="n">tx_len</span><span class="p">,</span>
                      <span class="nb">NULL</span><span class="p">,</span>
                      <span class="mi">0</span><span class="p">);</span>

        <span class="k">return</span> <span class="n">I2C_OK</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The parameters are:</p>

<ul>
  <li>uint8t addr7 - A 7 bit address of the I2C device.</li>
  <li>const uint8_t *tx - A pointer to the bytes to send.</li>
  <li>size_t tx_len - The number of bytes to send.</li>
</ul>

<p>The NULL, 0 at the end just tells the transfer function that there is no read phase.
So this function does the following:</p>

<p>START -&gt; address + write bit -&gt; send bytes -&gt; STOP</p>

<h1 id="write-then-read-transaction">Write then read transaction</h1>

<p>Reading from a register is slightly different. The micro normally has to write the register address first, and then read the returned data. A generic write the read would look like:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">i2c_status_t</span> <span class="nf">i2c_write_read</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="n">addr7</span><span class="p">,</span>
                            <span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">tx</span><span class="p">,</span>
                            <span class="kt">size_t</span> <span class="n">tx_len</span><span class="p">,</span>
                            <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">rx</span><span class="p">,</span>
                            <span class="kt">size_t</span> <span class="n">rx_len</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">tx</span> <span class="o">||</span> <span class="o">!</span><span class="n">rx</span> <span class="o">||</span> <span class="n">tx_len</span> <span class="o">==</span> <span class="mi">0</span> <span class="o">||</span> <span class="n">rx_len</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">I2C_ERR_PARAM</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">i2c_transfer7</span><span class="p">(</span><span class="n">I2C1</span><span class="p">,</span>
                  <span class="n">addr7</span><span class="p">,</span>
                  <span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="p">)</span><span class="n">tx</span><span class="p">,</span>
                  <span class="n">tx_len</span><span class="p">,</span>
                  <span class="n">rx</span><span class="p">,</span>
                  <span class="n">rx_len</span><span class="p">);</span>

    <span class="k">return</span> <span class="n">I2C_OK</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>an example of using this would be:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">uint8_t</span> <span class="n">reg</span> <span class="o">=</span> <span class="mh">0x0F</span><span class="p">;</span>
<span class="kt">uint8_t</span> <span class="n">value</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

<span class="n">i2c_write_read</span><span class="p">(</span><span class="mh">0x58</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">reg</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">value</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
</code></pre></div></div>

<p>This will do the following:</p>

<ol>
  <li>Talk to the device at 0x58</li>
  <li>Write one byte: 0x0F</li>
  <li>Read one byte back into value</li>
</ol>

<p>So on the bus this is what you would see:</p>

<p>START -&gt; address + write bit -&gt; register address -&gt; repeated start -&gt; address + read bit -&gt; read byte -&gt; STOP</p>

<p>The repeated start matters because it keeps the transaction connected. The device sees the first byte as the register address, then immediately returns the data from that register during the read phase.</p>

<p>Here is what the header file would look like to give you some assistance in building this:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#pragma once
</span>
<span class="cp">#include</span> <span class="cpf">&lt;stdint.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stddef.h&gt;</span><span class="cp">
</span>
<span class="k">typedef</span> <span class="k">enum</span> <span class="p">{</span>
    <span class="n">I2C_OK</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
    <span class="n">I2C_ERR_TIMEOUT</span><span class="p">,</span>
    <span class="n">I2C_ERR_BUS</span><span class="p">,</span>
    <span class="n">I2C_ERR_PARAM</span><span class="p">,</span>
<span class="p">}</span> <span class="n">i2c_status_t</span><span class="p">;</span>

<span class="kt">void</span> <span class="nf">i2c1_init</span><span class="p">(</span><span class="kt">void</span><span class="p">);</span>

<span class="n">i2c_status_t</span> <span class="nf">i2c_write</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="n">addr7</span><span class="p">,</span>
                       <span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">tx</span><span class="p">,</span>
                       <span class="kt">size_t</span> <span class="n">tx_len</span><span class="p">);</span>

<span class="n">i2c_status_t</span> <span class="nf">i2c_write_read</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="n">addr7</span><span class="p">,</span>
                            <span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">tx</span><span class="p">,</span>
                            <span class="kt">size_t</span> <span class="n">tx_len</span><span class="p">,</span>
                            <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">rx</span><span class="p">,</span>
                            <span class="kt">size_t</span> <span class="n">rx_len</span><span class="p">);</span>
</code></pre></div></div>

<p>A basic register write example:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">uint8_t</span> <span class="n">config</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
    <span class="mh">0x09</span><span class="p">,</span>   <span class="c1">// register address</span>
    <span class="mh">0x03</span>    <span class="c1">// value to write</span>
<span class="p">};</span>

<span class="n">i2c_write</span><span class="p">(</span><span class="mh">0x57</span><span class="p">,</span> <span class="n">config</span><span class="p">,</span> <span class="mi">2</span><span class="p">);</span>
</code></pre></div></div>

<p>A register read example:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">uint8_t</span> <span class="n">reg</span> <span class="o">=</span> <span class="mh">0xFF</span><span class="p">;</span>
<span class="kt">uint8_t</span> <span class="n">part_id</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

<span class="n">i2c_write_read</span><span class="p">(</span><span class="mh">0x57</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">reg</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">part_id</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
</code></pre></div></div>

<p>This gives enough of an I2C layer to start writing sensor drivers without dragging all of the low-level setup into the application code.</p>

<script src="https://utteranc.es/client.js" repo="skoopsy/skoopsy.github.io" issue-term="pathname" label="blog-embedded9" theme="preferred-color-scheme" crossorigin="anonymous" async="">
</script>

<p>Copyright © 2025 David O’Connor</p>]]></content><author><name>Dave</name></author><category term="STM32" /><summary type="html"><![CDATA[After a bit of searching it seemed I was not going to find a nice library to use some of the peripheral boards I have in mind, especially whilst trying to avoid the STM32cubeIDE. Which lead me writing drivers…]]></summary></entry></feed>