<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>0x646b - openocd</title>
    <subtitle>Personal Commentary</subtitle>
    <link rel="self" type="application/atom+xml" href="https://Karthik-d-k.github.io/tags/openocd/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://Karthik-d-k.github.io"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-06-23T00:00:00+00:00</updated>
    <id>https://Karthik-d-k.github.io/tags/openocd/atom.xml</id>
    <entry xml:lang="en">
        <title>Hardware Debugging is Hard</title>
        <published>2026-06-23T00:00:00+00:00</published>
        <updated>2026-06-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Karthik
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://Karthik-d-k.github.io/blog/hardware-debugging-is-hard/"/>
        <id>https://Karthik-d-k.github.io/blog/hardware-debugging-is-hard/</id>
        
        <content type="html" xml:base="https://Karthik-d-k.github.io/blog/hardware-debugging-is-hard/">&lt;h2 id=&quot;motivation&quot;&gt;Motivation&lt;&#x2F;h2&gt;
&lt;p&gt;I’m the core &lt;code&gt;firmware developer&lt;&#x2F;code&gt; in the &lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.bosch-softwaretechnologies.com&#x2F;en&#x2F;products-and-solutions&#x2F;products-and-solutions&#x2F;trusted-v&#x2F;&quot;&gt;Trusted-V Team&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;, working on different parts of the low-level firmware stack like the &lt;strong&gt;RTOS&lt;&#x2F;strong&gt;, &lt;strong&gt;board bringup&lt;&#x2F;strong&gt;, and &lt;strong&gt;PAC&#x2F;HAL crates&lt;&#x2F;strong&gt; for a couple of RISC-V MCU boards, using &lt;strong&gt;Rust&lt;&#x2F;strong&gt; as the primary language of choice for every layer.&lt;&#x2F;p&gt;
&lt;p&gt;Recently I was working on porting our Trusted-V RTOS to one of our client’s MCU boards (name withheld for privacy) and found out that the functionality works without any issues under &lt;strong&gt;QEMU&lt;&#x2F;strong&gt; emulation but not on the real hardware. So I set out to debug&#x2F;fix this issue.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;setup&quot;&gt;Setup&lt;&#x2F;h2&gt;
&lt;p&gt;I will provide brief details about the MCU which are generic in the MCU world, like what kind of debugging tools I had access to and how I was flashing the firmware and so on.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;QEMU&lt;&#x2F;strong&gt;: QEMU emulation was the default target for our RTOS to run and test our code before we move onto real MCU boards. QEMU has support for RISC-V firmware, so it was easier to use this as our initial RISC-V target.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;FTDI FT2232H&lt;&#x2F;strong&gt;: This MCU has an onboard FTDI FT2232H chip with its dual independent channels configured for &lt;strong&gt;JTAG&lt;&#x2F;strong&gt; and &lt;strong&gt;UART&lt;&#x2F;strong&gt; support.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;DC Barrel Jack&lt;&#x2F;strong&gt;: This is used to power up the MCU board.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;USB-C Cable&lt;&#x2F;strong&gt;: A Type-C USB cable is used for connecting the MCU to the PC for programming and debugging purposes.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;OpenOCD and GDB&lt;&#x2F;strong&gt;: OpenOCD and GDB are used for flashing&#x2F;loading the firmware onto the MCU and debugging.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;problem&quot;&gt;Problem&lt;&#x2F;h2&gt;
&lt;p&gt;We are writing a Real Time Operating System (RTOS) from scratch using Rust, targeting both RISC-V 32-bit and 64-bit MCUs which could be used in IoT, industrial and consumer electronics. I won’t go into any details about this unreleased OS (maybe a topic for the future) but one thing we need for an OS to work is to have some bare-metal startup code which does all the necessary initialization for the RISC-V cores present on the MCU, which helps in bringing up the board to a safe state, after which we can run bare-metal applications or add support for our OS on top of it.&lt;&#x2F;p&gt;
&lt;p&gt;In the Rust embedded world, we have the &lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;riscv-rt&#x2F;latest&#x2F;riscv_rt&#x2F;&quot;&gt;riscv-rt crate&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; which has startup code and a minimal runtime for RISC-V CPUs. We could make use of this because it has generic support for many types of RISC-V cores present in the market.
We do have the option of writing the assembly code ourselves for board bringup, though.&lt;&#x2F;p&gt;
&lt;p&gt;Different MCUs have different requirements and do unique things at startup to support their unique features, so we decided to use assembly code instead of depending on the riscv-rt crate. That said, the riscv-rt source code is a good starting point for understanding the basics of startup code specific to RISC-V cores.&lt;&#x2F;p&gt;
&lt;p&gt;Our RTOS was running fine under QEMU targeting the exact same machine type as our MCU hardware, printing necessary info over UART.
But when run on real hardware, nothing was printing over the UART terminal.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;exploration&quot;&gt;Exploration&lt;&#x2F;h2&gt;
&lt;p&gt;This MCU configures &lt;code&gt;UART0&lt;&#x2F;code&gt; in its bootloader bootrom image, so whenever we press the reset pin on the MCU, a useful text banner about the company is printed by default — meaning the bootloader was successfully setting up UART0 for serial communication.
So an easy way to debug embedded firmware issues is to first configure UART and then print things over it at every step (printf debugging).
Referring to the UART register map gives us the base address, status register and transmit FIFO register address, which is the minimum requirement to write a small UART driver to send text over UART. This can be done simply by polling the TX_FULL bit clear from the status register and then writing bytes using the transmit FIFO register. The specific&#x2F;exact details may vary depending on which UART you are using (refer to the datasheet). This model matched QEMU, so the same driver (with a tiny variation) was used on both the MCU and QEMU.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;A simple UART Driver in Rust could be written something like below:&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;rs&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-rs &quot;&gt;&lt;code class=&quot;language-rs&quot; data-lang=&quot;rs&quot;&gt;&lt;span&gt;#[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;inline&lt;&#x2F;span&gt;&lt;span&gt;]
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;pub fn &lt;&#x2F;span&gt;&lt;span style=&quot;color:#8fa1b3;&quot;&gt;console_putc&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;byte&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;u8&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;TX_OFFSET&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;usize &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;8&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;STATUS_OFFSET&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;usize &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;4&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;TX_FULL_MASK&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;u16 &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;1 &lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&amp;lt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#65737e;&quot;&gt;&#x2F;&#x2F; wait for TX_FULL clear
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;unsafe &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;let&lt;&#x2F;span&gt;&lt;span&gt; base = peripherals::&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;UART0_BASE&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;let&lt;&#x2F;span&gt;&lt;span&gt; status = (base + &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;STATUS_OFFSET&lt;&#x2F;span&gt;&lt;span&gt;) as &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;*const u16&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;while &lt;&#x2F;span&gt;&lt;span&gt;core::ptr::read_volatile(status) &amp;amp; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;TX_FULL_MASK &lt;&#x2F;span&gt;&lt;span&gt;!= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;0 &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;            core::hint::spin_loop();
&lt;&#x2F;span&gt;&lt;span&gt;        }
&lt;&#x2F;span&gt;&lt;span&gt;        core::ptr::write_volatile((base + &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;TX_OFFSET&lt;&#x2F;span&gt;&lt;span&gt;) as &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;*mut u8&lt;&#x2F;span&gt;&lt;span&gt;, byte);
&lt;&#x2F;span&gt;&lt;span&gt;    }
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;firmware-execution-models&quot;&gt;Firmware Execution models&lt;&#x2F;h3&gt;
&lt;p&gt;There are different ways to flash or load the binary onto the MCU. I list below 2 common ways:&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ram-execution&quot;&gt;RAM Execution&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;In this model, the firmware is loaded to RAM, the program counter is set to the start address, and then we run it.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;This mode is the preferred one during development as it offers faster loading and debugging capabilities because we are not dependent on flash read&#x2F;writes anywhere.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Please be aware that the firmware is erased after each power cycle, so we have to reload it because RAM is volatile memory.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;We can either use openOCD, or use gdb with openOCD, to achieve this.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;In OpenOCD terms, command will be something like:&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;sh&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;openocd -f &lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;CFG_FILE&amp;gt; -c &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;load_image &amp;lt;ELF_FILE&amp;gt;; reg pc &amp;lt;START_ADDR&amp;gt;; resume&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;In GDB terms, command will be something like:&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;sh&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;openocd -f &lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;CFG_FILE&amp;gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;riscv64-unknown-elf-gdb &lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;ELF_FILE&amp;gt; -ex &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;target remote :3333&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt; -ex &lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;load&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt; -ex &lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;set &lt;&#x2F;span&gt;&lt;span&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;pc&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt; = &amp;lt;START_ADDR&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt; -ex &lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;continue&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;flash-xip-execution&quot;&gt;FLASH&#x2F;XIP Execution&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;In this model, the firmware is loaded to FLASH and executed directly from flash because Execute In Place (XIP) supports it.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;This model survives a power cycle as FLASH is non-volatile memory.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Hence this is usually used during deployment of the final firmware.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;In OpenOCD terms, command will be something like:&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;sh&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;openocd -f &lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;CFG_FILE&amp;gt; -c &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;flash_write &amp;lt;BIN_FILE&amp;gt; &amp;lt;START_ADDR&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt; -c &lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;resume&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Each and every model uses &lt;strong&gt;linker scripts&lt;&#x2F;strong&gt; to precisely place the .text, .data, .bss and other required sections in RAM&#x2F;FLASH accordingly. One more hint: we can choose based on the size of our firmware and whether it fits completely into RAM&#x2F;FLASH.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;I was using &lt;strong&gt;RAM Execution&lt;&#x2F;strong&gt; as loading my RTOS into Flash was taking too long and setting it up correctly is kind of difficult.&lt;&#x2F;p&gt;
&lt;p&gt;Okay, now coming back to my original problem, my RTOS had the basic things set up in the startup code which are necessary for RISC-V code execution, namely:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Setting the stack pointer and global pointer&lt;&#x2F;li&gt;
&lt;li&gt;Zeroing the .bss section&lt;&#x2F;li&gt;
&lt;li&gt;Copying the .data section from its LMA (Load Memory Address — FLASH&#x2F;RAM) to its VMA (Virtual&#x2F;Runtime Memory Address — RAM). Note that in the RAM Execution model the LMA and VMA are both in RAM, so this copy is effectively a no-op; it only does real work in the XIP&#x2F;flash model.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;When I ran the firmware, the code was stuck in &lt;em&gt;Zeroing the .bss section&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;So I started trying different possible solutions to hunt down this bug.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hunting&quot;&gt;Hunting&lt;&#x2F;h2&gt;
&lt;h4 id=&quot;printing-characters-over-uart&quot;&gt;Printing Characters over UART&lt;&#x2F;h4&gt;
&lt;p&gt;As a first step, I used the UART to print a single character (using assembly) at every stage of the startup assembly routine explained above. Interestingly, all the characters were printed in the console and the RTOS ran without issues.
But here’s the twist: just when I thought I had solved the issue — that it was simply a power cycle I needed to do to successfully run the RTOS — NO… after I removed the placeholder characters, it got stuck in the same .bss loop again.&lt;&#x2F;p&gt;
&lt;p&gt;I had been running the code successfully for a month because my firmware had those placeholder characters present in the startup routine; they were never deleted. The issue resurfaced as soon as I removed them.&lt;&#x2F;p&gt;
&lt;p&gt;So this bug went unnoticed for a very long time without an actual fix.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;using-c-sdk-examples-instead-of-rust-rtos&quot;&gt;Using C SDK examples instead of Rust RTOS&lt;&#x2F;h4&gt;
&lt;p&gt;We had access to a C SDK which had a few different working examples to test the board and its functionality over various peripherals.
The SDK had quite a few archived object files (.a &#x2F; .o) for startup&#x2F;print&#x2F;misc things which were linked into all the application example code.&lt;&#x2F;p&gt;
&lt;p&gt;Due to this, the application size was over 200 KB even for a simple hello world example.
So I discarded that object file and wrote my own startup routine in assembly which set the stack pointer and then jumped to the main function.&lt;&#x2F;p&gt;
&lt;p&gt;Believe it or not, this compiled and ran on the real RISC-V hardware without issues. Strictly speaking, RISC-V hardware only needs a valid program counter to start executing — &lt;code&gt;sp&lt;&#x2F;code&gt;, &lt;code&gt;gp&lt;&#x2F;code&gt;, &lt;code&gt;.bss&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;.data&lt;&#x2F;code&gt; are all software&#x2F;ABI conventions, not hardware preconditions. To run C &lt;em&gt;safely&lt;&#x2F;em&gt; you also set up a stack pointer (the calling convention uses it for saved registers and locals) before jumping to &lt;code&gt;main&lt;&#x2F;code&gt;. A trivial &lt;code&gt;main&lt;&#x2F;code&gt; that fits entirely in registers — like this one, which just polls the UART and writes a string — could even run without a valid &lt;code&gt;sp&lt;&#x2F;code&gt;, but you don’t want to rely on that the moment &lt;code&gt;main&lt;&#x2F;code&gt; grows a function call or a local that spills to the stack.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Reference assembly code:&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;asm&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-asm &quot;&gt;&lt;code class=&quot;language-asm&quot; data-lang=&quot;asm&quot;&gt;&lt;span style=&quot;color:#8fa1b3;&quot;&gt;    .&lt;&#x2F;span&gt;&lt;span style=&quot;color:#96b5b4;&quot;&gt;section &lt;&#x2F;span&gt;&lt;span style=&quot;color:#8fa1b3;&quot;&gt;.text.init   &#x2F;&lt;&#x2F;span&gt;&lt;span&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#8fa1b3;&quot;&gt;@ADDR = &amp;lt;START_ADDR&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;*&lt;&#x2F;span&gt;&lt;span style=&quot;color:#8fa1b3;&quot;&gt;&#x2F;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#8fa1b3;&quot;&gt;    .globl _start
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#8fa1b3;&quot;&gt;    .&lt;&#x2F;span&gt;&lt;span style=&quot;color:#96b5b4;&quot;&gt;align &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;2
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#8fa1b3;&quot;&gt;_start:
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#8fa1b3;&quot;&gt;    la    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;sp&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#8fa1b3;&quot;&gt;_stack
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#8fa1b3;&quot;&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;call  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#8fa1b3;&quot;&gt;main
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now that I had basic example code working, I changed the startup routine to be similar to what I had in Rust, and ran the code — which triggered the same issue. This hinted that Rust was never the problem. I had thought maybe the Rust target was wrong, or the compiler&#x2F;linker&#x2F;optimizer had mangled the Rust code somehow. But this was not the case: even the C firmware hit the same issue.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;other-misc-tries&quot;&gt;Other Misc Tries&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;I thought maybe a single UART write before clearing the .bss section would fix the issue, since sprinkling UART characters over the startup routine had somehow worked fine — but this failed.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;After this I tried masking interrupts before .bss clearing; even this failed.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;After this I set up &lt;code&gt;mtvec&lt;&#x2F;code&gt; to catch any error from .bss clearing, but even this failed — because clearing the .bss section never hit any exceptions in the first place.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;disassembling-the-c-sdk-startup-library&quot;&gt;Disassembling the C SDK startup library&lt;&#x2F;h4&gt;
&lt;p&gt;After trying so many things as mentioned above, I explored the C SDK for any hints&#x2F;gotchas, but I couldn’t find any in the source.
Then I tried to look into the linked archive object file using &lt;code&gt;objdump&lt;&#x2F;code&gt;, and disassembling the startup object revealed something I had been overlooking: in the SDK’s reference startup, each iteration of the .bss zeroing and .data copying loops was followed by a &lt;code&gt;fence.i&lt;&#x2F;code&gt; instruction.&lt;&#x2F;p&gt;
&lt;p&gt;This is &lt;em&gt;not&lt;&#x2F;em&gt; something you typically see in startup code for generic RISC-V cores or under QEMU — for instance, the riscv-rt startup routine does a plain store loop with no per-store fence. The vendor’s own reference startup clearly fenced after every store for a reason, so I matched that behaviour in my own startup routine — and the &lt;strong&gt;issue was gone&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;I won’t speculate here about the exact micro-architectural reason this is required on this particular hardware; the takeaway is that the vendor’s reference startup encodes hard-won bring-up knowledge, and “rolling your own” means you have to respect what that reference code is doing and &lt;em&gt;why&lt;&#x2F;em&gt;. A software issue that had been hiding for a few weeks was solved by adding essentially one instruction — &lt;strong&gt;fence.i&lt;&#x2F;strong&gt; — in the right place.&lt;&#x2F;p&gt;
&lt;p&gt;I was a bit frustrated about how I could have missed this, or that I should have tried this approach first. But I was relieved to finally have it fixed, and because I stumbled onto this almost as a last resort, I learned quite a few things along the way about execution models, startup code, RISC-V targets and so on. So I guess it was a win-win situation.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Thanks to my friend &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.linkedin.com&#x2F;in&#x2F;imrank03&#x2F;&quot;&gt;&lt;strong&gt;Imran K&lt;&#x2F;strong&gt;&lt;&#x2F;a&gt; for reviewing the draft version of this blog.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rust-embedded.org&#x2F;embedonomicon&#x2F;&quot;&gt;The Embedonomicon&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;riscv-rt&#x2F;latest&#x2F;riscv_rt&#x2F;index.html&quot;&gt;riscv-rt crate&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;qemu&#x2F;qemu&#x2F;blob&#x2F;master&#x2F;hw&#x2F;riscv&#x2F;virt.c&quot;&gt;QEMU source code&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;openocd-org&#x2F;openocd&#x2F;&quot;&gt;OpenOCD&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
</feed>
