Close
0%
0%

A RISCy move

Moving development to the RISC-V series of MCUs

Similar projects worth following
After the 8042/8, 8051, STM8, AVR families what next? And why I am getting Rusty.

Time to progress with my exploration of microcontroller platforms. I'll be focusing on the RISC-V 32 MCUs which are now commodity chips, in particular the WCH 32V series. To be sure, I've already made initial forays in this area.

Instead of writing a long detail document, I'll spin off digressions into logs instead of boring everybody to tears with constant updates where it's hard to discern what has been updated.

First off, a history of the MCU families that I've delved into.

Next, an exploration of candidate MCU families.

A brief survey of the toolchain scene.

The WCH 32V series of MCUs

Now we get to the actual hardware that I've decided to develop for. The CH32V003 family is one of the offerings of the Nanjing Qingheng Microelectronics company. These MCU ICs got the attention of developers and hobbyists for their low price. This family of 20 pin and below MCUs aims to capture some of the base functionality MCU market, which coincidentally or not, have 003 somewhere in the model name, e.g. STM8F003, STM32F003. Higher members of the CH32V family offer more peripherals and more pins.

If you go shopping in the usual bazaars, this is a module you will commonly see:

Another variant has a TSSOP-20 package instead of the QFP-20 package in the middle. Notice the 3 pins to the programming dongle.

I've decided to go for the module shown in the topmost picture, one of the BTE series made by a different factory. It doesn't have the USB-C connector but has an onboard 24 MHz crystal which could come in handy for more precise timing.

I've decided to not attempt to design around the bare MCU but use the BTE module as a component. They pack more on the module than I could with my SMD soldering skills and sell it cheaper than I could buy the components for. You can see how small it is, about the size of a 20 pin 0.6 inch DIP IC. A price of tens of cents for the bare MCU is tempting, but so is the price of only a dollar or two, quantity 10 shipping included, for the module. Tested working too. The drawback is that these module will raise the height of the entire assembly if pulgged into Dupont pin sockets, unless I'm willing to solder them more or less permanently onto the base PCB.

RISC-V IDE and compiler toolchains

The toolchain suggested by the manufacturer is MounRiver Studio. There are secondary sites, as the original site isn't the fastest. Arduino also supports the WCH chips now, but of course with their HAL. The actual compiler tools are available separately if you want to develop from the CLI and use automated build tools, a prebuilt one being the xPack tools. And of course you can also build the compiler tools from the gcc sources, but you need to know which compiler flags to use for the ISA of the WCH chips. Things move quickly in this world, so check that the information you have is current. For example for a long time MounRiver Studio was at 1.92, but now I see that 2.40 has been out for a few months, and it seems it's not based on Eclipse any more but on the VSCode editor.

Rust HAL and libraries

One option I didn't mention in the toolchain survey is the programming language Rust. Writing in C or C++ has got boring so I want to challenge myself with something new. Rust will generate larger binaries than C due to the layers of abstraction so it's just as well that my intended MCU target the WCH32V003 has 16kB flash memory. So I wrote a log entry about installing a Rust toolchain. I'll start with a text editor and the CLI interface and think about IDEs later.

Here's a log detailing how I got a blinky program to compile and run on a RP2040.

(To be continued.)

  • Getting another blinky to work

    Ken Yap9 hours ago 0 comments

    Well that was fun, getting blinky in Rust working on a RPi Pico. But just so that I understand better the steps of building a Rust project using the Embassy framework, I'll try another MCU.

    I would like to try the Espressif ESP32 MCUs. The RISC-V based members like the ESP32-C3 are supported, but the older Xtensa based MCUs require a hacked LLVM compiler as it's not a supported ISA yet. The changes are being merged with the main effort but it takes time. The ESP8266 family of MCUs are not supported at all for Rust.

    So instead I'll try the STM32 MCU family. It looks quite different from the RPi Pico, but it's also ARM based. For the instructions I turn to the Embassy book, specifically the section on the STM32 HAL. There I absorb the information that the STM32 family is very large, just as I noted when trying out the CubeMX IDE, so the variants are managed with a metapac that's a database of all the variants and is used to direct project generation. Ok, I'll stow that knowledge for later.

    Looking for an easy way to start a project, I come across cargo-embassy, a tool that can be added to cargo. So I do:

    $ cargo install cargo-embassy

    then cargo --list shows a new command: embassy.

    $ cargo embassy init stm32-rust-blinky
    error: the following required arguments were not provided:
      --chip
    
    Usage: cargo embassy init --chip  
    
    For more information, try '--help'.
    

    Ah, I have to specify the chip, makes sense. So:

    $ probe-rs chip list | grep -i stm32f103
            STM32F103C4
            STM32F103C6
            STM32F103C8
            STM32F103CB
            STM32F103R4
            STM32F103R6
            STM32F103R8
            STM32F103RB
            STM32F103RC
            STM32F103RD
            STM32F103RE
            STM32F103RF
            STM32F103RG
            STM32F103T4
            STM32F103T6
            STM32F103T8
            STM32F103TB
            STM32F103V8
            STM32F103VB
            STM32F103VC
            STM32F103VD
            STM32F103VE
            STM32F103VF
            STM32F103VG
            STM32F103ZC
            STM32F103ZD
            STM32F103ZE
            STM32F103ZF
            STM32F103ZG 

    Ah, I have to be more specific:

    $ probe-rs chip list | grep -i stm32f103c8
           STM32F103C8

    Now:

    $ cargo embassy init stm32-rust-blinky --chip stm32f103c8

    This gets me a Cargo project called stm32-rust-blinky. Nice when a metatool does a lot of work for you. I just plunge ahead and do:

    $ cd stm32-rust-blinky
    $ cargo build
    info: syncing channel updates for 1.90-x86_64-unknown-linux-gnu
    info: latest update on 2025-09-18 for version 1.90.0 (1159e78c4 2025-09-14)
    info: downloading 8 components

    No no, I didn't want that. 1.90 is an old release of Rust so I interrupt it. Looking at the directory, I see:

    $ ls
    build.rs  Cargo.lock  Cargo.toml  Embed.toml  rust-toolchain.toml  src
    

    I look inside rust-toolchain.toml and see:

    # This file was automatically generated.
    
    [toolchain]
    channel = "1.90"
    components = ["rust-src", "rustfmt"]
    targets = ["thumbv7m-none-eabi"]

    Change that 1.90 to stable and retry:

    $ cargo build
    error: feature `debug` includes `embassy-executor/defmt`, but `embassy-executor` is not a dependency
      --> Cargo.toml:42:9
       |
    42 |   debug = [
       |  _________^
    43 | |     "defmt",
    44 | |     "defmt-rtt",
    45 | |     "panic-probe",
    ...  |
    51 | |     "embassy-stm32/defmt",
    52 | | ]
       | |_^
    error: failed to parse manifest at `stm32-rust-blinky/Cargo.toml`
    

    Much better, new error. Turns out that embassy-executor wasn't listed in the dependencies, so I add this line:

    embassy-executor = { version = "0.10.0", features = ["platform-cortex-m", "executor-thread", "defmt"] }

    and try cargo build again. This time it failed because I didn't have the target thumb7m-none-eabi. The RPi Pico was a thumb6m-none-eabi. So I do:

    $ rustup target add thumb7m-none-eabi

    and try cargo-build again. This time it succeeds and leaves an ELF binary in target/thumbv7m-none-eabi/debug/stm32-rust-blinky

    $ cargo size
        Finished `dev` profile [optimized + debuginfo] target(s) in 0.05s
       text    data     bss     dec     hex filename
      11952      80    1132   13164    336c stm32-rust-blinky
    

    But I look at the source code in src/main.rs.

    #![no_std]
    #![no_main]
    
    mod fmt;
    
    #[cfg(not(feature = "defmt"))]
    use panic_halt as _;
    #[cfg(feature = "defmt")]
    use {defmt_rtt...
    Read more »

  • Getting a Rust blinky application to work

    Ken Yap07/23/2026 at 11:56 0 comments

    Ok, let's get a blinky program to work. As the ARM based RP2040 implementation of embedded Rust is one of the more developed ones and it's easy for me to plug in my Pico to my USB port, I will start with this, and expect to readily be able to port it to the WCH32V platform later.

    Here's a suggested blinky program in Rust. The core of the program is recognisable, you can easily guess what's happening. I forget where I got this example from. No matter, you will see many similar examples on the Internet, and I can tweak the code later as my understanding improves.

    #![no_std]
    #![no_main]
    
    use defmt::info;
    use embassy_executor::Spawner;
    use embassy_rp::gpio::{Level, Output};
    use embassy_time::Timer;
    use {defmt_rtt as _, panic_probe as _};
    
    #[embassy_executor::main]
    async fn main(_spawner: Spawner) {
        let p = embassy_rp::init(Default::default());
        let mut led = Output::new(p.PIN_25, Level::Low);
    
        info!("blinky started");
    
        loop {
            led.set_high();
            Timer::after_millis(500).await;
            led.set_low();
            Timer::after_millis(500).await;
        }
    }

    In a project directory called rp2040-rust-blinky created with cargo new, or cargo init. I put the above code in src/main.rs. We also need a Cargo.toml at the top level which came from the example, which looks like this:

    [package]
    name = "rp2040-rust-blinky"
    version = "0.1.0"
    edition = "2021"
    
    [dependencies]
    defmt = "0.3"
    defmt-rtt = "0.4"
    panic-probe = { version = "0.3", features = ["print-defmt"] }
    cortex-m-rt = "0.7"
    #embassy-rp = { version = "0.4", features = ["rp2040","time-driver"] }
    embassy-rp = { version = "0.10.0", features = ["rp2040","time-driver","critical-section-impl"] }
    #embassy-time = { version = "0.4" }
    embassy-time = "0.5.1"
    #embassy-executor = { version = "0.6", features = ["arch-cortex-m","executor-thread","integrated-timers"] }
    embassy-executor = { version = "0.10.0", features = ["platform-cortex-m","executor-thread"] }
    # RP2040 HAL (Hardware Abstraction Layer)
    #rp2040-hal = { version = "0.10", features = ["rt", "critical-section-impl"] }
    rp2040-hal = { version = "0.12.0", features = ["rt", "critical-section-impl"] }
    rp-pico = "0.9.0"
    cortex-m = "0.7.7"
    
    [profile.release]
    debug = 2          # keep debug symbols for defmt
    lto = true
    codegen-units = 1
    opt-level = "s"

    This file states the dependency crates that are needed for this application. You can see that I updated some of the dependency versions as the example is 5 years old and many advances have been made.

    There's another file called memory.x which contain ld instructions for the RP2040 layout. I won't explain it at this time.

    MEMORY {
        BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100
        FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100
        RAM   : ORIGIN = 0x20000000, LENGTH = 256K
    }
    
    EXTERN(BOOT2_FIRMWARE)
    
    SECTIONS {
        .boot2 ORIGIN(BOOT2) :
        {
            KEEP(*(.boot2));
        } > BOOT2
    } INSERT BEFORE .text;

    Then I did cargo build.

    After a huge amount of downloading of crates (dependencies pull in their dependencies recursively), then compiling, the result was an ELF binary in target/thumbv6m-none-eabi/debug/rp2040-rust-blinky.

    The method I'm used to installing embedded programs on the RP2040 is by mounting it as USB storage and then copying a UF2 binary onto it. When you hold down reset button, plug the RP2040 in a USB port, and release the button, a new USB storage device appears and you can use your desktop manager to mount it at /run/media/<user>/RPI-RP2.

    I found an ELF to UF2 converter. This also needs the libudev-devel package to build, just like probe-rs. I ran the converter on the ELF binary.

    Once the RP2040 was mounted, I copied target/thumbv6m-none-eabi/debug/rp2040-rust-blinky.uf2 to it, and the onboard LED blinked. Note that the RP2040 will disappear as a USB device when the embedded blinky runs.

    I'll figure out how to download the ELF binary directly another time.

    About the binary

    So how big is our blinky binary?

    $ ls -lh target/thumbv6m-none-eabi/debug/rp2040-rust-blinky
    -rwxr-xr-x 2 me users 2.9M Jul 23 21:09 target/thumbv6m-none-eabi/debug/rp2040-rust-blinky...
    Read more »

  • Installing a Rust toolchain

    Ken Yap07/04/2026 at 23:58 0 comments

    This log will be amended as I fill in details that I may have missed.

    Your OS may have Rust available in their package repositories. But due to the rapid pace of development it's best to install a Rust toolchain outside of the package system. Rust developers have streamlined this task.

    You need two important programs: rustup which is a toolchain installer (and maintainer), and cargo, installed by rustup, which is a project manager. The other programs like rustc, the compiler, will be installed by rustup.

    Before you install rustup, you need to decide if you accept the standard location for the software, in the case of Linux, under your home directory in ~/.cargo, as explained here. If like me, you dislike lots of package files under home (unfortunately this is the norm these days with Arduino, PlatformIO, etc. etc.), you can change the directories. There are two environment variables, and I've set them thus:

    export RUSTUP_HOME=/usr/local/lib/rustup CARGO_HOME=/usr/local/lib/cargo
    

    If you choose to change them, edif your ~/.bash_profile to export these environment variables. Also put this command in ~/.bash_profile after so that the correct executables are found.

    . "$CARGO_HOME/env"

    Logout and login again if necessary to ensure that these settings are in place before you install rustup following the instructions at the website.

    The help text for rustup indicates what it does:

    $ rustup --help
    rustup 1.29.0 (28d1352db 2026-03-05)
    
    The Rust toolchain installer
    
    Usage: rustup[EXE] [OPTIONS] [+toolchain] [COMMAND]
    
    Commands:
      install      Install or update the given toolchains, or by default the active toolchain
      uninstall    Uninstall the given toolchains
      toolchain    Install, uninstall, or list toolchains
      default      Set the default toolchain
      show         Show the active and installed toolchains or profiles
      update       Update Rust toolchains and rustup
      check        Check for updates to Rust toolchains and rustup
      target       Modify a toolchain's supported targets
      component    Modify a toolchain's installed components
      override     Modify toolchain overrides for directories
      run          Run a command with an environment configured for a given toolchain
      which        Display which binary will be run for a given command
      doc          Open the documentation for the current toolchain
      man          View the man page for a given command
      self         Modify the rustup installation
      set          Alter rustup settings
      completions  Generate tab-completion scripts for your shell
      help         Print this message or the help of the given subcommand(s)
    
    Arguments:
      [+toolchain]  Release channel (e.g. +stable) or custom toolchain to set override
    
    Options:
      -v, --verbose  Set log level to 'DEBUG' if 'RUSTUP_LOG' is unset
      -q, --quiet    Disable progress output, set log level to 'WARN' if 'RUSTUP_LOG' is unset
      -h, --help     Print help
      -V, --version  Print version
    
    Discussion:
      Rustup installs The Rust Programming Language from the official
      release channels, enabling you to easily switch between stable,
      beta, and nightly compilers and keep them updated. It makes
      cross-compiling simpler with binary builds of the standard library
      for common platforms.
    
      If you are new to Rust consider running `rustup doc --book` to
      learn Rust.
    
    Common commands:
    
      Update Rust toolchains and rustup
    
        $ rustup update
        
      Install the current stable release of Rust for your host platform
    
        $ rustup toolchain install stable
    

    Toolchain means a release channel, e.g. default, nightly. If you are thinking about MCU targets, then the term is target.

    Here's a relevant fact: gcc requires a different toolchain for each MCU family. That's why the toolchain for the classic Arduino starts with avr-, that for the STM32 starts with arm-, and for the WCH32V series riscv-. LLVM however uses the same compiler for all targets, and you can install a new target without installing another compiler. Here are the targets I have installed:

    $ rustup target list | grep installed
    riscv32i-unknown-none-elf (installed)
    thumbv6m-none-eabi (installed)
    x86_64-unknown-linux-gnu (installed)...
    Read more »

  • Toolchains

    Ken Yap06/17/2026 at 02:34 0 comments

    Microcontrollers need firmware and toolchains used to develop them. This is an overview and will undergo revision as I will no doubt overlook some concepts at the time of writing.

    Assemblers

    Unless you have one of those educational kits where you toggle switches (or the equivalent thereof) to program the hardware, it's assumed the basic requirement is an assembler which will take assembly code and turn it into bits to be loaded into the MCU.

    This is the case for the 8048/8042 MCUs I have mentioned. The toolchain is the ASxxxx cross assembler and linker. The MCU is too weak to support a HLL like C. Wikipedia claims that a PL/M compiler existed for the 8048 but I am dubious because the architecture is severely limited.

    Compilers

    I consider the basic requirement for a MCU toolchain to be a C compiler toolchain. This true of all the MCUs I have used from the 8051 onwards, although early PIC models had very restrictive architectures like for example a two-level stack which forces one to flatten out function calls.

    CLI Development Environments

    The first step up from a compiler is something to automate the repeated steps of a embedded app edit and build. You could do it with scripts, but I started off with a text editor and a Makefile which is very familiar from the desktop Linux environment..

    This can be elaborated with any number of improved build tools like CMake, meson + ninja, etc.

    It's worthwhile retaining CLI build facilities even if one is using an IDE, to have a reproducible means of creating production artifacts.

    Integrated Development Environments

    A great boon of a GUI IDE is multiple views. In one window you could be editing source code, another could be a command window, and of course you could have output and debugging windows. When editing, the IDE can help the programmer by suggesting completions for library calls, sparing having to look up the programming manual.

    Many of IDEs are front-ends to traditional CLI programs, simply because that's how the tools are invoked. For example, when you use the Arduino IDE, possibly the best known, you can see the compile commands launched in response to firing off a build. Some IDEs like the Microchip MPLAB X even generate Makefiles to control the build process.

    IDEs can be specific to a platform, e.g. Window, or could be cross-platform. The preference these days is for cross-platform IDEs to capture a large audience. Traditionally cross-platform IDEs relied technology like Java e.g. in the form of the Eclipse platform. Many IDEs, for example Moun River are based on this. But these days cross-platform GUI toolkits are not a big deal, so an IDE like Arduino has support for the main OSes. Java GUIs these days look square and quaint.

    IDEs can also be multi-target. Arduino started off supporting the AVR MCUs, but these days the Espressif, ARM, and RISC-V MCUs are transparently supported. The compiler toolchains moved from the Arduino application package to optional library packages.

    But one great aid that IDEs provide is configuration for multiple models of MCUs. Unlike desktop CPUs where generations of processors can be accomodated in the kernel and runtime libraries, and it's all hidden from you, a Linux app works the same whether you are using a 10 year-old processor, or this year's, MCUs come in a huge variety of models targeted for various fields, e.g. consumer, automotive, instrumentation, with differences in peripherals. It's expensive and unnecessary to make embedded programs work on many models, so builds are configured for just the intended target. You can see this in the Arduino IDE where you have to select the board you are working with.

    This customisation is extensive in IDEs like the ARM CubeMX IDE for STMicro's line of STM32 processors. A separate GUI program takes you through the selection of the company's MCU models and generates a configuration file which is used to influence build configuration.

    Another dimension of IDEs is...

    Read more »

  • Which 32-bit MCU family should I pick?

    Ken Yap05/27/2026 at 14:55 2 comments

    My early attempt at a 32-bit CPU. No, just kidding.

    Having decided to move on from the MCUs I am using, now that 32-bit MCUs are cheap commodities what are the candidates?

    Before that, one advantage is clear: going to 32-bit gets decent HLL support. You can have a gcc/g++ toolchain, or one based on the up and coming rival, LLVM. You can also have other languages like Rust.

    ARM

    ARM is everywhere. A lot of hobbyists got introduced to ARM via Raspberry Pi. But the range extends from small MCU boards like the STM32 Blue Pill, to smartphones and tablets, to desktops, and to 64-bit servers in the datacentres. The PlasticARM project put a minimal Cortex-M0 on a flexible substrate, providing more areas for IoT to colonise.

    ARM will be around for a long time. It should be noted here that the core architecture is licensed but the MCU chip manufacturer has absorbed that cost for you.

    In practical terms, I could develop with the STM32 family or the RP series. I probably will for high performance uses.

    RISC-V

    RISC-V is an open architecture, there is no licensing to deploy it, so it has become attractive to chip makers as an alternative to ARM. I have a GigaDevice 32V dev board, but it's really the WCH series of 32V chips that are challenging the low-end uses of ARM. This makes them attractive to me.

    Xtensa

    Xtensa is the MCU architecture that Espressif Systems used in their popular series of WiFi and Bluetooth capable modules, such as the ESP8266 and ESP32. It's very popular due to the wireless capabilities, just look at the number of projects on Hackaday. I have several Espressif modules that badly need a few round tuits from me, so I will also develop with these.

    But here's the rub: Espressif is shifting their product lines to RISC-V cores. The Xtensa based MCUs will be around for a while, if only because there's so much stock around.

    Also the RP2350 has an interesting design: two ARM cores and two RISC-V cores, the first use of RISC-V in the RP series.

    Which will it be?

    Actually, with a good toolchain, I don't have to stick exclusively to one family. I'll just let the project, the available MCUs, and my wallet provide the decision parameters. More about toolchains in another log.

  • My personal history with MCUs

    Ken Yap05/25/2026 at 11:26 0 comments

    This project #Restoring a Beckman neon display clock is what got me started on MCUs several years ago. It contained an 8048 family MCU and I wrote code to replace the original firmware. I managed to live within the limitations of assembly language, 1kB code space, 64B RAM, and 13 I/O pins, several lost to using external ROM. Definitely no high level language.

    As it turned out I had quite a few of the 8048 family, and the related 8041/2 family chips in my junk box. I think the '48s came from full height 5¼ inch floppy drives, and the '42s came from PC keyboard controllers. I put them to use in projects like #8042 clock #8042 metronome and 8048 clock. No that last link is not a mistake, the instruction set is almost the same for the '48 and the '42 so the code is usable for both.

    Well that was a good way to use up those chips. I have to say there are quite hardy and resisted many accidents. But boy were they power hogs.

    As we know Intel came out with the 8051, the successor to the 8048. The breakthrough improvement was putting peripherals in the I/O address space rather than having specific instructions for each I/O port. The '51 has gone on to be probably the most durable MCU family and descendants live on as programmable cores for many chips.

    It actually is a fairly decent MCU to learn on. The hardware and instruction set are easy to understand and there is even C language support in the form of the GPL Small Device C Compiler (SDCC) and commercial offerings. Descendants have increased the clock rate to be tens of times faster than the original. Some are one-chip wonders only needing a handful of passives for a working system like this. The one above is a STC89C52 workalike for the 8052 with flash ROM for code, but otherwise a drop-in equivalent, used in #89C52 clock board. I used this dev board with them: #Adventures with a STC89C52 development board. Other projects using a '51 include #Ancient 12 hour display and #Modularnixie.

    But ultimately, it's an 8-bit MCU with all the attendant limitations. (Some expanded architectures like the '251 were developed.) So I used up the handful of MCUs and dev boards I bought and bade them farewell.

    Next cab off the rank was the STM8 which I got interested in thanks to #eForth for cheap STM8S gadgets. They appear in the millions if not billions in small gadgets. You probably have a few around the house. (An interesting development was that 8051 derivatives started to muscle in on the STM8 based gadgets by coming out with pin (but not architecture) compatible models, presumably because the '51 core architecture was free to reimplement.) Modules are readily available from AliExpress and the per MCU price was between 10 and 20¢, although it was a while before my SMD soldering skills were up to doing TSSOP-20.

    This has a decent architecture and is also well supported by SDCC. It has a good selection of on-chip peripherals including standards like I2C which wasn't in the '51 descendants until well into its reign. The GPIO pins can do true bidirectional and true push-pull in addition to other modes like hi-Z and open drain, unlike the 8051's weak pullup. The HAL takes a bit of getting used to but it is worth it.

    I liked this MCU and used it in #Repurposing an old nixie thermometer, #Third life for a radio alarm clock, and the driver for #Vertical LED digit display boards. I still have a few chips but won't be starting any new projects with them.

    The AVR family is represented by the Arduino models I have. Strangely I haven't done any projects with them because I think of them as test equipment which I used frequently, as a driver in the above photo. One drawback is that they have 5 V GPIO which means I have to turn to the ESP32 Arduino lookalikes that work with 3.3 V logic.

    I did have a couple of early DIP AVR chips for which I managed to nut out the details of to use in this project, and this project. The architecture is decent, it manages to get gcc support...

    Read more »

View all 6 project logs

Enjoy this project?

Share

Discussions

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates