In the most recent post in the RISC-V Bytes series, we had our first experience with real hardware, exploring the bootloader on an ESP32-C3-DevKitC-02 module. We were using esp-idf, which, behind the scenes uses an implementation of FreeRTOS. In this post, we’ll swap out the Espressif FreeRTOS implementation for Zephyr, exploring some similarities and differences between the build process and produced artifacts. We’ll also see how we can slim down a Zephyr installation, only fetching the code and tools that we need to target the ESP32-C3 chipset.


Please excuse my attempt at an illustration of inside of a Trojan Horse.

Sections Link to heading

Installing Tools Link to heading

Zephyr encourages the use of west, which essentially is an extensible wrapper around a variety of other tools. If you are familiar with the underlying tools, using west can feel a bit cumbersome, but opting out of doing so means that much of the guides and documentation will not be applicable to your workflow. I found myself using west in somewhat of a minimal fashion, but not going fully into using Zephyr without west.

west-based workflows allow developing Zephyr applications via three different models:

  • repository: building an application within the Zephyr repository
  • workspace: building an application in the same workspace as the Zephyr repository (i.e. sharing a parent directory)
  • freestanding: building an application that is configured to point to some installation of Zephyr

While repository seems to be the most straightforward for building the in-tree sample applications, and workspace appears to be the recommended route for most projects, I found freestanding to be more familiar to my typical development workflow. That being said, there are pros and cons of all three options and you should select the one that is most appropriate for your project. We’ll explore the resulting directory structure when using a freestanding model in the next section.

Initial tooling setup mostly looks the same regardless of what model you are going to use. The full steps for installing cmake, python, and dtc are outlined in the Getting Started Guide. I opted, partially due to my desire to use the freestanding model, to do a global installation of west.

$ pip3 install --user -U west

Setting up a Workspace Link to heading

With tools in place, it is now time to download Zephyr itself. Rather than using west init ~/zephyrproject, which will clone the latest Zephyr version into ~/zephyrproject/zephyr, I created a new subdirectory in my existing ~/code/ directory for workspaces, and a subdirectory within it for the workspace we will use for the remainder of this blog post.

$ mkdir -p workspaces/v3.3.0

As the name suggests, this workspace will be set up to with Zephyr v3.3.0, which is the latest release. Within the v3.3.0 directory, rather than running a bare west init as the guide suggests, we’ll create a manifest repository, which will inform the version of Zephyr we want and any additional dependencies.

Note: if we were using the workspace application model, the manifest repository could be our application repository itself. If we were using the repository application model, the manifest repository would be Zephyr.


    - name: zephyr
      revision: v3.3.0
      west-commands: scripts/west-commands.yml
          - hal_espressif

Here we are specifying that we want the v3.3.0 release of Zephyr, and we only want the hal_espressif project imported from the list of projects in its west.yml. Now we can run west init in the v3.3.0 directory and point it to our local manifest repository.

$ west init -l manifest/
=== Initializing from existing manifest repository manifest
--- Creating /home/hasheddan/code/ and local configuration file
=== Initialized. Now run "west update" inside /home/hasheddan/code/

All this will do is setup west to use the local manifest for future operations. Following the instructions in the output, along with some extra flags, will download Zephyr and the hal_espressif.

$ west update --narrow -o=--depth=1
=== updating zephyr (zephyr):
--- zephyr: initializing
Initialized empty Git repository in /home/hasheddan/code/
--- zephyr: fetching, need revision v3.3.0
remote: Enumerating objects: 28549, done.
remote: Counting objects: 100% (28549/28549), done.
remote: Compressing objects: 100% (22159/22159), done.
remote: Total 28549 (delta 6571), reused 14659 (delta 4784), pack-reused 0
Receiving objects: 100% (28549/28549), 57.04 MiB | 23.41 MiB/s, done.
Resolving deltas: 100% (6571/6571), done.
 * tag                 v3.3.0     -> FETCH_HEAD
Updating files: 100% (24352/24352), done.
HEAD is now at 07c6af3b release: Zephyr 3.3.0
HEAD is now at 07c6af3b release: Zephyr 3.3.0
=== updating hal_espressif (modules/hal/espressif):
--- hal_espressif: initializing
Initialized empty Git repository in /home/hasheddan/code/
--- hal_espressif: fetching, need revision 73e7af1e2ed64571ce49ff9f07dc02690b9f2df5
remote: Enumerating objects: 11222, done.
remote: Counting objects: 100% (11222/11222), done.
remote: Compressing objects: 100% (9234/9234), done.
remote: Total 11222 (delta 2547), reused 6008 (delta 1450), pack-reused 0
Receiving objects: 100% (11222/11222), 64.90 MiB | 22.79 MiB/s, done.
Resolving deltas: 100% (2547/2547), done.
 * branch            73e7af1e2ed64571ce49ff9f07dc02690b9f2df5 -> FETCH_HEAD
Updating files: 100% (9878/9878), done.
HEAD is now at 73e7af1 hal: clock: esp32c3: Fix default cpu frequency magnitude
HEAD is now at 73e7af1 hal: clock: esp32c3: Fix default cpu frequency magnitude

Without the name-allowlist restriction, we would instead download all projects in the Zephyr west.yml, which is much more than we need. There is a tradeoff in this slimmer approach in that if we wanted to target multiple chipsets, or just expand functionality, with the same project, we would need to either add to the allowlist, or setup a new workspace. The philosophy of Zephyr’s getting started guide ensures that users will have everything they need when getting started, but it also means that they will likely download a number of projects they will never use.

The --narrow and -o=--depth=1 flags are also useful in reducing the amount of content we download and store. Without the former, --tags is passed to git fetch, causing all tags to be fetched from the remote. The latter passes the --depth flag to git fetch. The result of these operations can be observed by viewing the git history of Zephyr and hal_espressif.

$ git -C zephyr/ log
commit 07c6af3b8c35c1e49186578ca61a25c76e2fb308 (grafted, HEAD, manifest-rev)
Author: Stephanos Ioannidis <>
Date:   Sat Feb 18 07:13:22 2023 +0900

    release: Zephyr 3.3.0
    This commit sets the Zephyr version to v3.3.0.
    Signed-off-by: Stephanos Ioannidis <>
    Signed-off-by: Lauren Murphy <>
$ git -C modules/hal/espressif/ log
commit 73e7af1e2ed64571ce49ff9f07dc02690b9f2df5 (grafted, HEAD, manifest-rev)
Author: Lucas Tamborrino <>
Date:   Mon Jan 9 10:22:24 2023 -0300

    hal: clock: esp32c3: Fix default cpu frequency magnitude
    The value must be corrected in hal after fetching it from dts
    Signed-off-by: Lucas Tamborrino <>

Note: If your application depends on functionality that is only available in binary form, it may be necessary to fetch blobs. Examples of blobs that may be required when targeting ESP32 systems can be found in the hal_espressif module.yml file, and instructions for how to fetch are present in the ESP32-C3 guide.

This may not be a great strategy for other project layouts where it is expected that versions may be updated. However, if you plan to use a single version per workspace strategy, as we have chosen here, we are not concerned about having a partial history. The only subsequent update operations we should be performing in this workspace would be to add a new project, which would not require any changes to the existing Zephyr installation or any modules.

The last steps are to export this version of Zephyr as a CMake package and install all Python dependencies. The latter can be done with the following command, which will tell CMake where to look for configuration for this version of Zephyr. This will allow us to add the Zephyr package to our freestanding project a bit later.

$ west zephyr-export
Zephyr (/home/hasheddan/code/
has been added to the user package registry in:

ZephyrUnittest (/home/hasheddan/code/
has been added to the user package registry in:

Installing Python dependencies is a simple as doing a pip install on the Zephyr repository’s requirements.txt.

pip3 install --user -r zephyr/scripts/requirements.txt

Getting a Toolchain Link to heading

We have all of the code we will need, but we need a toolchain in order to build our image. Zephyr has made this quite straightforward via the Zephyr SDK. It provides toolchains for every Zephyr supported target. However, the getting started guide will once again have you install all of them by default. This is RISC-V Bytes and we only need our RISC-V toolchain to target the ESP32-C3, so we’ll instead use the minimal bundle and only specify the toolchain we want when performing setup.

$ cd ~
$ wget
--2023-04-16 20:46:36--
Resolving (
Connecting to (||:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: [following]
--2023-04-16 20:46:36--
Resolving (,,, ...
Connecting to (||:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 41512804 (40M) [application/octet-stream]
Saving to: ‘zephyr-sdk-0.16.0_linux-x86_64_minimal.tar.xz’

zephyr-sdk-0.16.0_linux-x86_64_minimal.tar.xz      100%[===============================================================================================================>]  39.59M  27.6MB/s    in 1.4s    

2023-04-16 20:46:38 (27.6 MB/s) - ‘zephyr-sdk-0.16.0_linux-x86_64_minimal.tar.xz’ saved [41512804/41512804]

$ wget -O - | shasum --check --ignore-missing
--2023-04-16 20:46:38--
Resolving (
Connecting to (||:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: [following]
--2023-04-16 20:46:43--
Resolving (,,, ...
Connecting to (||:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 12148 (12K) [application/octet-stream]
Saving to: ‘STDOUT’

-                                                  100%[===============================================================================================================>]  11.86K  --.-KB/s    in 0.001s  

2023-04-16 20:46:48 (15.7 MB/s) - written to stdout [12148/12148]

zephyr-sdk-0.16.0_linux-x86_64_minimal.tar.xz: OK

The minimal bundle includes a script that will prompt the user for toolchains that should be installed. We can extract the bundle, then run

$ tar xvf zephyr-sdk-0.16.0_linux-x86_64_minimal.tar.xz -C ~/.local/

$ ~/.local/zephyr-sdk-0.16.0/ 
Zephyr SDK 0.16.0 Setup

** NOTE **
You only need to run this script once after extracting the Zephyr SDK
distribution bundle archive.

Install toolchains for all targets [y/n]? n
Install 'aarch64-zephyr-elf' toolchain [y/n]? n
Install 'arc64-zephyr-elf' toolchain [y/n]? n
Install 'arc-zephyr-elf' toolchain [y/n]? n
Install 'arm-zephyr-eabi' toolchain [y/n]? n
Install 'microblazeel-zephyr-elf' toolchain [y/n]? n
Install 'mips-zephyr-elf' toolchain [y/n]? n
Install 'nios2-zephyr-elf' toolchain [y/n]? n
Install 'riscv64-zephyr-elf' toolchain [y/n]? y
Install 'sparc-zephyr-elf' toolchain [y/n]? n
Install 'x86_64-zephyr-elf' toolchain [y/n]? n
Install 'xtensa-espressif_esp32_zephyr-elf' toolchain [y/n]? n
Install 'xtensa-espressif_esp32s2_zephyr-elf' toolchain [y/n]? n
Install 'xtensa-espressif_esp32s3_zephyr-elf' toolchain [y/n]? n
Install 'xtensa-intel_apl_adsp_zephyr-elf' toolchain [y/n]? n
Install 'xtensa-intel_s1000_zephyr-elf' toolchain [y/n]? n
Install 'xtensa-nxp_imx_adsp_zephyr-elf' toolchain [y/n]? n
Install 'xtensa-nxp_imx8m_adsp_zephyr-elf' toolchain [y/n]? n
Install 'xtensa-sample_controller_zephyr-elf' toolchain [y/n]? n
Install host tools [y/n]? y
Register Zephyr SDK CMake package [y/n]? y

Installing 'riscv64-zephyr-elf' toolchain ...
toolchain_linux-x86_64_riscv64-zephyr-elf.tar.xz   100%[===============================================================================================================>] 105.29M  35.9MB/s    in 2.9s    

Installing host tools ...

Registering Zephyr SDK CMake package ...
Zephyr-sdk (/home/hasheddan/.local/zephyr-sdk-0.16.0/cmake)
has been added to the user package registry in:

All done.
Press any key to exit ...

Note: You may have noticed that the installed toolchain is for 64-bit RISC-V (riscv64), but we are targeting a 32-bit chipset. The 64-bit toolchain can target both 32-bit and 64-bit architectures (as specified by flags).

You should see all RISC-V tools now present.

$ ls ~/.local/zephyr-sdk-0.16.0/riscv64-zephyr-elf/bin/
riscv64-zephyr-elf-addr2line  riscv64-zephyr-elf-cpp           riscv64-zephyr-elf-gcc-ar      riscv64-zephyr-elf-gdb               riscv64-zephyr-elf-ld        riscv64-zephyr-elf-ranlib
riscv64-zephyr-elf-ar         riscv64-zephyr-elf-ct-ng.config  riscv64-zephyr-elf-gcc-nm      riscv64-zephyr-elf-gdb-add-index     riscv64-zephyr-elf-ld.bfd    riscv64-zephyr-elf-readelf
riscv64-zephyr-elf-as         riscv64-zephyr-elf-elfedit       riscv64-zephyr-elf-gcc-ranlib  riscv64-zephyr-elf-gdb-add-index-py  riscv64-zephyr-elf-lto-dump  riscv64-zephyr-elf-size
riscv64-zephyr-elf-c++        riscv64-zephyr-elf-g++           riscv64-zephyr-elf-gcov        riscv64-zephyr-elf-gdb-py            riscv64-zephyr-elf-nm        riscv64-zephyr-elf-strings
riscv64-zephyr-elf-cc         riscv64-zephyr-elf-gcc           riscv64-zephyr-elf-gcov-dump   riscv64-zephyr-elf-gprof             riscv64-zephyr-elf-objcopy   riscv64-zephyr-elf-strip
riscv64-zephyr-elf-c++filt    riscv64-zephyr-elf-gcc-12.2.0    riscv64-zephyr-elf-gcov-tool   riscv64-zephyr-elf-gprof-py          riscv64-zephyr-elf-objdump

Building the Image Link to heading

After quite a bit of setup, we are now ready to build! We can setup a simple freestanding repository that builds a similar image to the sample hello_world application.

Create a new directory outside of any Zephyr workspace (I chose ~/code/, and create a CMakeLists.txt file, as well as src/main.c.

$ mkdir -p zephyr-freestanding/src
$ cd zephyr-freestanding && touch CMakeLists.txt src/main.c
$ tree
├── CMakeLists.txt
└── src
    └── main.c

1 directory, 2 files

All that our application will do is print a message and exit. Including zepher/kernel.h gives us access to printk, which allows for printing kernel debugging messages.


#include <zephyr/kernel.h>

int main(void)
	printk("Hello, RISC-V Bytes!\n");
	return 0;

In CMakeLists.txt, we’ll need to inform the build system where to find our Zephyr installation, as well as where the source(s) for our application live.


cmake_minimum_required(VERSION 3.20.0)

find_package(Zephyr 3.3.0 REQUIRED)

target_sources(app PRIVATE src/main.c)

CMake will be able to identify the Zephyr package due to the west zephyr-export command we ran earlier. We add the version restriction (3.3.0) and the REQUIRED directive such that we will fail early if CMake is unable to identify a suitable package. However, in order to use CMake via the recommended west build command, we’ll still need to set ZEPHYR_BASE. This is due to the fact that the build command is implemented as a plugin that lives in the Zephyr repository, rather than a built-in command. The only argument that needs to be supplied is --board, which informs our target, and correspondingly, toolchain and required modules. Let’s run the build and analyze the output.

Note: We are specifying esp32c3_devkitm despite using the ESP32-C3-DevKitC-02. Because these two boards both use the ESP32-C3 chip, the configuration is mostly the same.

$ export ZEPHYR_BASE=~/code/
$ west build -b esp32c3_devkitm
-- west build: generating a build system
Loading Zephyr default modules (Zephyr base).
-- Application: /home/hasheddan/code/
-- CMake version: 3.22.1
-- Found Python3: /usr/bin/python3.10 (found suitable exact version "3.10.6") found components: Interpreter 
-- Cache files will be written to: /home/hasheddan/.cache/zephyr
-- Zephyr version: 3.3.0 (/home/hasheddan/code/
-- Found west (found suitable version "1.0.0", minimum required is "0.7.1")
-- Board: esp32c3_devkitm
-- ZEPHYR_TOOLCHAIN_VARIANT not set, trying to locate Zephyr SDK
-- Found host-tools: zephyr 0.16.0 (/home/hasheddan/.local/zephyr-sdk-0.16.0)
-- Found toolchain: zephyr 0.16.0 (/home/hasheddan/.local/zephyr-sdk-0.16.0)
-- Found Dtc: /home/hasheddan/.local/zephyr-sdk-0.16.0/sysroots/x86_64-pokysdk-linux/usr/bin/dtc (found suitable version "1.6.0", minimum required is "1.4.6") 
-- Found BOARD.dts: /home/hasheddan/code/
-- Generated zephyr.dts: /home/hasheddan/code/
-- Generated devicetree_generated.h: /home/hasheddan/code/
-- Including generated dts.cmake file: /home/hasheddan/code/
Parsing /home/hasheddan/code/
Loaded configuration '/home/hasheddan/code/'
Configuration saved to '/home/hasheddan/code/'
Kconfig header saved to '/home/hasheddan/code/'
-- The C compiler identification is GNU 12.2.0
-- The CXX compiler identification is GNU 12.2.0
-- The ASM compiler identification is GNU
-- Found assembler: /home/hasheddan/.local/zephyr-sdk-0.16.0/riscv64-zephyr-elf/bin/riscv64-zephyr-elf-gcc
-- Configuring done
-- Generating done
-- Build files have been written to: /home/hasheddan/code/
-- west build: building application
[1/179] Preparing syscall dependency handling

[2/179] Generating include/generated/version.h
-- Zephyr version: 3.3.0 (/home/hasheddan/code/, build: 07c6af3b8c35
[162/179] Performing configure step for 'EspIdfBootloader'
-- Found Git: /usr/bin/git (found version "2.34.1") 
-- Building ESP-IDF components for target esp32c3
-- Project sdkconfig file /home/hasheddan/code/
-- Adding linker script /home/hasheddan/code/
-- Adding linker script /home/hasheddan/code/
-- Adding linker script /home/hasheddan/code/
-- Adding linker script /home/hasheddan/code/
-- Adding linker script /home/hasheddan/code/
-- Adding linker script /home/hasheddan/code/
-- Adding linker script /home/hasheddan/code/
-- Components: bootloader bootloader_support efuse esp32c3 esp_common esp_hw_support esp_rom esp_system esptool_py freertos hal log main micro-ecc newlib partition_table riscv soc spi_flash
-- Component paths: /home/hasheddan/code/ /home/hasheddan/code/ /home/hasheddan/code/ /home/hasheddan/code/ /home/hasheddan/code/ /home/hasheddan/code/ /home/hasheddan/code/ /home/hasheddan/code/ /home/hasheddan/code/ /home/hasheddan/code/ /home/hasheddan/code/ /home/hasheddan/code/ /home/hasheddan/code/ /home/hasheddan/code/ /home/hasheddan/code/ /home/hasheddan/code/ /home/hasheddan/code/ /home/hasheddan/code/ /home/hasheddan/code/
-- Configuring done
-- Generating done
-- Build files have been written to: /home/hasheddan/code/
[163/179] Performing build step for 'EspIdfBootloader'
[1/88] Generating project_elf_src_esp32c3.c
[2/88] Building C object esp-idf/soc/CMakeFiles/__idf_soc.dir/soc_include_legacy_warn.c.obj
[86/88] Linking C executable bootloader.elf
[87/88] Generating binary image from built executable v3.3
Creating esp32c3 image...
Merged 1 ELF section
Successfully created esp32c3 image.
Generated /home/hasheddan/code/
[88/88] cd /home/hasheddan/code/ && /usr/bin/python3.10 /home/hasheddan/code/ --offset 0x8000 bootloader 0x0 /home/hasheddan/code/
Bootloader binary size 0x4ba0 bytes. 0x3460 bytes (41%) free.
[169/179] Linking C executable zephyr/zephyr_pre0.elf

[173/179] Linking C executable zephyr/zephyr_pre1.elf

[179/179] Linking C executable zephyr/zephyr.elf
Memory region         Used Size  Region Size  %age Used
     mcuboot_hdr:          32 B         32 B    100.00%
        metadata:          28 B         32 B     87.50%
             ROM:       32164 B    4194240 B      0.77%
     iram0_0_seg:       11376 B       320 KB      3.47%
     irom0_0_seg:       83568 B    4194272 B      1.99%
     drom0_0_seg:        2008 B    4194240 B      0.05%
     dram0_0_seg:       21268 B       320 KB      6.49%
    rtc_iram_seg:          0 GB         8 KB      0.00%
        IDT_LIST:          0 GB         8 KB      0.00% v3.3
Creating esp32c3 image...
Merged 6 ELF sections
Successfully created esp32c3 image.

If some of this output looks familiar, it is likely because you have seen many of the same steps when using / The Zephyr build system wraps these tools from hal_espressif, which is essentially a fork of esp-idf. In fact, until we get to building the actual Zephyr application, we are building the same second stage bootloader from esp-idf that we saw in the last post.

$ ls build/esp-idf/build/bootloader
bootloader.bin  CMakeCache.txt  cmake_install.cmake    config      esp-idf     project_elf_src_esp32c3.c
bootloader.elf     CMakeFiles      compile_commands.json  config.env  project_description.json

If we examine the bootloader.elf file, we’ll see the same steps to load and jump to the application image.

$ ~/.local/zephyr-sdk-0.16.0/riscv64-zephyr-elf/bin/riscv64-zephyr-elf-objdump -D -j .iram.text build/esp-idf/build/bootloader/bootloader.elf | head -48

build/esp-idf/build/bootloader/bootloader.elf:     file format elf32-littleriscv

Disassembly of section .iram.text:

403ce000 <call_start_cpu0>:
403ce000:	7131                	addi	sp,sp,-192
403ce002:	df06                	sw	ra,188(sp)
403ce004:	00000793          	li	a5,0
403ce008:	c789                	beqz	a5,403ce012 <call_start_cpu0+0x12>
403ce00a:	00000097          	auipc	ra,0x0
403ce00e:	000000e7          	jalr	zero # 0 <mapped-0x3fcd6000>
403ce012:	288d                	jal	403ce084 <bootloader_init>
403ce014:	c119                	beqz	a0,403ce01a <call_start_cpu0+0x1a>
403ce016:	444030ef          	jal	ra,403d145a <bootloader_reset>
403ce01a:	00000793          	li	a5,0
403ce01e:	c789                	beqz	a5,403ce028 <call_start_cpu0+0x28>
403ce020:	00000097          	auipc	ra,0x0
403ce024:	000000e7          	jalr	zero # 0 <mapped-0x3fcd6000>
403ce028:	0a000613          	li	a2,160
403ce02c:	4581                	li	a1,0
403ce02e:	0808                	addi	a0,sp,16
403ce030:	ffc32097          	auipc	ra,0xffc32
403ce034:	324080e7          	jalr	804(ra) # 40000354 <memset>
403ce038:	0808                	addi	a0,sp,16
403ce03a:	076030ef          	jal	ra,403d10b0 <bootloader_utility_load_partition_table>
403ce03e:	e10d                	bnez	a0,403ce060 <call_start_cpu0+0x60>
403ce040:	5be020ef          	jal	ra,403d05fe <esp_log_early_timestamp>
403ce044:	85aa                	mv	a1,a0
403ce046:	3fcd6637          	lui	a2,0x3fcd6
403ce04a:	3fcd6537          	lui	a0,0x3fcd6
403ce04e:	14c60613          	addi	a2,a2,332 # 3fcd614c <_data_end+0x48>
403ce052:	15450513          	addi	a0,a0,340 # 3fcd6154 <_data_end+0x50>
403ce056:	ffc32097          	auipc	ra,0xffc32
403ce05a:	fea080e7          	jalr	-22(ra) # 40000040 <esp_rom_printf>
403ce05e:	bf65                	j	403ce016 <call_start_cpu0+0x16>
403ce060:	0808                	addi	a0,sp,16
403ce062:	27c030ef          	jal	ra,403d12de <bootloader_utility_get_selected_boot_partition>
403ce066:	f9d00793          	li	a5,-99
403ce06a:	c62a                	sw	a0,12(sp)
403ce06c:	faf505e3          	beq	a0,a5,403ce016 <call_start_cpu0+0x16>
403ce070:	4501                	li	a0,0
403ce072:	2029                	jal	403ce07c <bootloader_common_get_reset_reason>
403ce074:	45b2                	lw	a1,12(sp)
403ce076:	0808                	addi	a0,sp,16
403ce078:	400030ef          	jal	ra,403d1478 <bootloader_utility_load_boot_image>

The application it loads lives at build/zephyr/zephyr.elf. We can see the assembly dump of our main() function in the .flash.text section.

$ ~/.local/zephyr-sdk-0.16.0/riscv64-zephyr-elf/bin/riscv64-zephyr-elf-objdump -D -j .flash.text build/zephyr/zephyr.elf | head -20

build/zephyr/zephyr.elf:     file format elf32-littleriscv

Disassembly of section .flash.text:

42010020 <_OffsetAbsSyms>:
42010020:	00008067          	ret

42010024 <main>:
42010024:	3c000537          	lui	a0,0x3c000
42010028:	ff010113          	addi	sp,sp,-16
4201002c:	04050513          	addi	a0,a0,64 # 3c000040 <__rodata_region_start>
42010030:	00112623          	sw	ra,12(sp)
42010034:	fe373097          	auipc	ra,0xfe373
42010038:	ad4080e7          	jalr	-1324(ra) # 40382b08 <printk>
4201003c:	00c12083          	lw	ra,12(sp)
42010040:	00000513          	li	a0,0
42010044:	01010113          	addi	sp,sp,16
42010048:	00008067          	ret

Lastly, we can find our partition table at build/esp-idf/build/partitions_singleapp.bin, which looks very similar to the one we saw when using esp-idf directly.

$ hexdump build/esp-idf/build/partitions_singleapp.bin 
0000000 50aa 0201 9000 0000 6000 0000 766e 0073
0000010 0000 0000 0000 0000 0000 0000 0000 0000
0000020 50aa 0101 f000 0000 1000 0000 6870 5f79
0000030 6e69 7469 0000 0000 0000 0000 0000 0000
0000040 50aa 0000 0000 0001 0000 0010 6166 7463
0000050 726f 0079 0000 0000 0000 0000 0000 0000
0000060 ebeb ffff ffff ffff ffff ffff ffff ffff
0000070 adf4 454f 5638 5d4b 3574 2cb6 b675 2495
0000080 ffff ffff ffff ffff ffff ffff ffff ffff

Flashing and Running Link to heading

With our three binaries constructed, all that is left to do is flashing and running. One thing we skipped in the getting started guide was setting up udev rules that allow us to access the board. The Zephyr SDK bundles rules from OpenOCD, but we need to ensure that it includes a rule that gives us access to the USB-to-UART bridge that lives on the ESP32-C3-DevKitC-02 board. We can use lsusb to determine the vendor and product ID for the bridge.

$ lsusb | grep UART
Bus 002 Device 009: ID 10c4:ea60 Silicon Labs CP210x UART Bridge

The bundled rules can be found in the sysroots/x86_64-pokysdk-linux/usr/share/openocd/contrib/60-openocd.rules file. There is not a matching entry for the 10c4 vendor ID and ea60 product ID, but we can add one in the following format.

# Silicon Labs USB-UART bridge
ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", MODE="660", GROUP="plugdev", TAG+="uaccess"

After doing so, we can copy these rules into the /etc/udev/rules.d/ directory, then reload the rules.

$ sudo cp ~/.local/zephyr-sdk-0.16.0/sysroots/x86_64-pokysdk-linux/usr/share/openocd/contrib/60-openocd.rules /etc/udev/rules.d/
$ sudo udevadm control --reload

Now we are ready to run west flash. This time we’ll supply the -v flag to instruct west to emit verbose output, which will allow us to see exactly how it is wrapping our Espressif tools.

Note: west flash is also a subcommand provided by Zephyr, not built into west. Ensure that your ZEPHYR_BASE is still set to the proper path to use it.

$ west -v flash
-- west flash: rebuilding
cmake version 3.22.1 is OK; minimum version is 3.13.1
Running CMake: /usr/bin/cmake --build /home/hasheddan/code/
ninja: no work to do.
-- west flash: using runner esp32
-- runners.esp32: Flashing esp32 chip on None (921600bps)
runners.esp32: /usr/bin/python3 /home/hasheddan/code/ --chip auto --baud 921600 --before default_reset --after hard_reset write_flash -u --flash_mode dio --flash_freq 40m --flash_size detect 0x0 /home/hasheddan/code/ 0x8000 /home/hasheddan/code/ 0x10000 /home/hasheddan/code/ v3.3
Found 1 serial ports
Serial port /dev/ttyUSB0
Detecting chip type... ESP32-C3
Chip is ESP32-C3 (revision 3)
Features: Wi-Fi
Crystal is 40MHz
MAC: 58:cf:79:16:7d:a0
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 921600
Configuring flash size...
Auto-detected Flash size: 4MB
Flash will be erased from 0x00000000 to 0x00004fff...
Flash will be erased from 0x00008000 to 0x00008fff...
Flash will be erased from 0x00010000 to 0x00034fff...
Flash params set to 0x0220
Wrote 32768 bytes at 0x00000000 in 0.5 seconds (556.5 kbit/s)...
Hash of data verified.
Wrote 16384 bytes at 0x00008000 in 0.3 seconds (497.4 kbit/s)...
Hash of data verified.
Wrote 163840 bytes at 0x00010000 in 2.2 seconds (589.2 kbit/s)...
Hash of data verified.

Hard resetting via RTS pin...

We are once again using the esptool write_flash command and writing our three binaries to flash in the same locations we did when building with esp-idf. We can use the west espressif monitor command to reset the board and stream the output, or we can connect with minicom and use the RST button.

$ minicom -D /dev/ttyUSB0

Build:Feb  7 2021
rst:0x1 (POWERON),boot:0xc (SPI_FAST_FLASH_BOOT)
mode:DIO, clock div:2
SHA-256 comparison failed:
Calculated: b77f73687acc559bca77cc4a4bea5409988930863749b80f171cc4a0501ff313
Expected: 318a48640313d299386e53a1953994f766ddfffed726be1320e4e027aa578799
Attempting to boot anyway...
entry 0x403ce000
I (49) boot: ESP-IDF 73e7af1 2nd stage bootloader
I (49) boot: compile time 22:29:26
I (49) boot: chip revision: 3
I (51) boot.esp32c3: SPI Speed      : 40MHz
I (56) boot.esp32c3: SPI Mode       : DIO
I (60) boot.esp32c3: SPI Flash Size : 4MB
I (65) boot: Enabling RNG early entropy source...
I (70) boot: Partition Table:
I (74) boot: ## Label            Usage          Type ST Offset   Length
I (81) boot:  0 nvs              WiFi data        01 02 00009000 00006000
I (89) boot:  1 phy_init         RF data          01 01 0000f000 00001000
I (96) boot:  2 factory          factory app      00 00 00010000 00100000
I (104) boot: End of partition table
I (108) esp_image: segment 0: paddr=00010020 vaddr=00000020 size=0001ch (    28) 
I (116) esp_image: segment 1: paddr=00010044 vaddr=3fc85020 size=000b8h (   184) load
I (125) esp_image: segment 2: paddr=00010104 vaddr=3fc850d8 size=0023ch (   572) load
I (133) esp_image: segment 3: paddr=00010348 vaddr=40380000 size=02c68h ( 11368) load
I (145) esp_image: segment 4: paddr=00012fb8 vaddr=00000000 size=0d080h ( 53376) 
I (162) esp_image: segment 5: paddr=00020040 vaddr=3c000040 size=007d8h (  2008) map
I (162) esp_image: segment 6: paddr=00020820 vaddr=00000000 size=0f7f8h ( 63480) 
I (181) esp_image: segment 7: paddr=00030020 vaddr=42010020 size=04670h ( 18032) map
I (186) boot: Loaded app from partition at offset 0x10000
I (186) boot: Disabling RNG early entropy source...
�*** Booting Zephyr OS build 07c6af3b8c35 ***
Hello, RISC-V Bytes!

Concluding Thoughts Link to heading

Though Zephyr introduces its own flavors of tooling and workflow, we are still using the same underlying protocols and bootloader when working with ESP32 chips. It is not until we actually boot the application that we start to see where Zephyr and FreeRTOS diverge. We’ll look deeper into these differences in a future RISC-V Bytes post.

As always, these posts are meant to serve as a useful resource for folks who are interested in learning more about RISC-V and low-level software in general. If I can do a better job of reaching that goal, or you have any questions or comments, please feel free to send me a message @hasheddan on Twitter or on Mastodon!