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.

This will also be about the Embassy framework for building embedded apps in Rust.

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

And for more practice, I got another Rust/Embassy blinky program to compile and run on a STM32 Blue Pill.

Then I got a Rust/Embassy blinky working on an ESP32-C3, which is a RISC-V based SOC.

Finally I got a Rust/Embassy blinky working on the target MCU, the...

Read more »

  • Blinky in Rust on AVR MCUs

    Ken Yap08/06/2026 at 13:04 0 comments

    Just for kicks I decided to see if I could run blinky written in Rust on an Arduino, even though this is a digression from the main goal of this project. The Arduino model I have is a clone of the common UNO which has the ATMega328p MCU.

    I found this guide which outlines the steps. From there I followed the link to the Github repo for avr-hal-template.

    First of all, Rust on AVR chips doesn't use the LLVM compiler for the linking, but rather the gcc compiler. So you need to have this installed separately. If you have installed the Arduino IDE, then you already have it, but you have to make the commands avr-gcc and friends available from the command line. What I did was make symbolic links from /usr/local/bin to the actual executables.

    $ ln -s -t /usr/local/bin ~/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7/bin/*
    

    The version of the avr-gcc package may differ, double check.

    The other thing you need to do is install the nightly Rust toolchain. This will be taken care of in a step further down.

    Now install two auxiliary tools like this:

    $ cargo install cargo-generate
    $ cargo install ravedude

    Ravedude is a front-end to avrdude, which must also be on your $PATH. Now run:

    $ cargo generate --git https://github.com/Rahix/avr-hal-template.git

    This will prompt you for the project name, and the type of Arduino you have and generate the standard Cargo project structure..

    I had to make a couple of changes: In rust-toolchain.toml I changed the channel from nightly-2025-04-27 to just nightly. I also added port = "/dev/ttyUSB0" in the [general] section because autodetect didn't work.

    Now you can do:

    $ cargo build

    If you didn't have the nightly toolchain already installed, then a lot of downloading will happen. After that many crates will be downloaded and compiled, then linked with the blinky in src/main.rs.

    Note that the sample blinky doesn't use the Embassy framework. So this is the blocking version, which looks like this:

    #![no_std]
    #![no_main]
    
    use panic_halt as _;
    
    #[arduino_hal::entry]
    fn main() -> ! {
        let dp = arduino_hal::Peripherals::take().unwrap();
        let pins = arduino_hal::pins!(dp);
    
        /*
         * For examples (and inspiration), head to
         *
         *     https://github.com/Rahix/avr-hal/tree/main/examples
         *
         * NOTE: Not all examples were ported to all boards!  There is a good chance though, that code
         * for a different board can be adapted for yours.  The Arduino Uno currently has the most
         * examples available.
         */
    
        let mut led = pins.d13.into_output();
    
        loop {
            led.toggle();
            arduino_hal::delay_ms(500);
        }
    }

    I made one change, from 1000 ms to 500 ms. Otherwise it's a ½ Hz blinky, not a 1 Hz blinky. Now you can run the blinky:

    $ cargo run
       Compiling avr-rust-blinky v0.1.0 (/home/ken/play/uC/avr/project/avr-rust-blinky)
        Finished `dev` profile [optimized + debuginfo] target(s) in 0.32s
         Running `ravedude target/avr-none/debug/avr-rust-blinky.elf`
           Board Arduino Uno
     Programming target/avr-none/debug/avr-rust-blinky.elf => /dev/ttyUSB0
    Reading 262 bytes for flash from input file avr-rust-blinky.elf
    Writing 262 bytes to flash
    Writing | ################################################## | 100% 0.08 s 
    Reading | ################################################## | 100% 0.05 s 
    262 bytes of flash verified
    
    Avrdude done.  Thank you.
      Programmed target/avr-none/debug/avr-rust-blinky.elf
         Console /dev/ttyUSB0 at 57600 baud
                 CTRL+C to exit.
    

    As you can see, the size of the executable isn't large at all, so there is very little overhead in this case.

  • What the bl**k have I learnt?

    Ken Yap08/05/2026 at 11:28 0 comments

    Ferris the Rustacean

    So what the bl**k have I learnt from implementing the Hello World app of the embedded world using Rust/Embassy on various MCU platforms? Here's list of what I can remember 😉:

    • The Instruction Set Architecture (ISA) doesn't come into it at all. Rust is a high level language and as long as the target processor is decent, it can be available. In practice this means at least 32-bit wide, and sufficient registers, then LLVM can handle it, though some exceptions like AVR exist.
    • Maturity of the implementation depends on available information and/or processor maker sponsorship. The RPi Pico, STM32 series, and Espressif MCUs are well supported. Support for the WCH32 series depends on hacker contributions.
    • The implementations differ in their support for peripherals. Basic peripherals like GPIO, I2C, etc. are not an issue. But if you want to use say BLE, WiFi, etc. check that Embassy crates exist for them.
    • Development status is fluid and documents on the Net can go out of date quickly so be prepared to question what you read. However you can pin your project to particular versions of the toolchain and crates.
    • Rustup and Cargo are the fundamental tools for managing toolchains and projects from the CLI. GUIs exist, but I haven't explored those.
    • The Embassy framework implements efficient embedded multitasking.
    • You will need more code memory because the language is rich, but remember that implementing those features in another language will also cost code memory. In any case even entry level MCUs with adequate specs are cheap now.
    • Probe-rs is a great tool for flashing and debugging and can be used for non-Rust projects too so if you can only take away one thing, let this be it.

  • Getting blinky working on the CH32VF003F4P6

    Ken Yap08/05/2026 at 00:08 0 comments

    So now it's time to try to get a Rust/Embassy blinky working on my target MCU. This is the evaluation board I got just over 2 years ago, together with a WCH-LinkE flashing dongle. I got a C blinky working on that easily and then put it aside until I got a round tuit.

    Searching reveals that there is a Rust/Embassy HAL for the WCH32 line of MCUs and it's a work in progress. Like the STM32 HAL, it caters to a wide range of MCU models by the use of a metapac, which is a database of MCUs and their characteristics. So I cloned the git repo and started looking through the examples, which are arranged by MCU model. The ones I want are under examples/ch32v003. There is a prototype cargo project there with the usual Cargo.toml which specifies the dependencies and build.rs which generates the build script. The src/ directory has a subdirectory called bin/ where all the example Rust apps are found. (This is an alternate structure for cargo projects, instead of a single src/main.rs, there is src/bin/ containing multiple apps to build.) In there I find src/bin/embassy_blinky,rs. So I copy Cargo.toml, build.rs, the hidden file .cargo/config.toml, and embassy_blinky.rs to a new project of mine, created by:

    $ cargo new ch32v003-rust-blinky

    What is the target tuple for this MCU though? The ESP32C3 was a riscv32imc-unknown-none. There is a file called riscv32ec-unknown-none-elf.json in the example project. What does it do? So the CH32V003 has a riscv32ec ISA? I try:

    $ rustup target list

    but this target doesn't exist. I try various cargo invocations using the JSON file, including the -Z flag but nothing works. What's going on?

    At this point I found another useful resource, Getting Started with CH32V003 Firmware in Rust. In it I discover ch32-hal needs to be built with a nightly release of the toolchain because riscv32ec is not an accepted ISA yet. So a JSON file specifies the ISA characteristics and this feature exists only in the nightly toolchains.

    $ rustup install nightly
    $ rustup override set nightly

    override changes the toolchain just for this project. .cargo/config.toml clarifies a few things:

    [build]
    target = "riscv32ec-unknown-none-elf.json"
    
    [target.'cfg(all(target_arch = "riscv32", target_os = "none"))']
    # runner = "riscv64-unknown-elf-gdb -q -x openocd.gdb"
    # runner = "riscv-none-embed-gdb -q -x openocd.gdb"
    # runner = "gdb -q -x openocd.gdb"
    # runner = "wlink -v flash --enable-sdi-print --watch-serial"
    
    # Flash and debug chip with probe-rs. https://probe.rs/
    runner = "probe-rs run --chip ch32v003"
    
    [unstable]
    build-std = ["core"]
    # build-std = ["core", "compiler_builtins"]
    # build-std-features = ["compiler-builtins-mem"]
    json-target-spec = true
    
    [target.riscv32ec-unknown-none-elf]
    #rustflags = [ "-C", "-Tlink.x" ]

    At this point I run into a few glitches, not very enlightening, so what you see is the final config I ended up with. I tried enabling rustflags, but it turns out it's correct to leave it out, as the llvm-lld linker gets the option -Tlink.x elsewhere and specifying this option more than once causes an error. The other change I made is to use probe-rs as the flashing program. wlink does work but probe-rs is better. It's a very useful Swiss Army Knife for flashing that can be used even for non-Rust projects, as it knows a lot of flashing methods.

    Now I try to build the blinky which looks like this:

    #![no_std]
    #![no_main]
    
    use ch32_hal as hal;
    use hal::Peri;
    use embassy_executor::Spawner;
    use embassy_time::Timer;
    use hal::gpio::{AnyPin, Level, Output};
    use hal::println;
    
    #[embassy_executor::task(pool_size = 2)]
    async fn blink(pin: Peri<'static, AnyPin>, interval_ms: u64) {
        let mut led = Output::new(pin, Level::Low, Default::default());
    
        loop {
            led.set_high();
            Timer::after_millis(interval_ms).await;
            led.set_low();
            Timer::after_millis(interval_ms).await;
        }
    }
    
    #[embassy_executor::main(entry = "qingke_rt::entry")]
    async fn main(spawner: Spawner) -> ! {
        hal::debug::SDIPrint::enable();
     let mut config...
    Read more »

  • Finally getting RISC-V blinky working

    Ken Yap08/03/2026 at 11:18 0 comments

    The Seeed Xiao ESP32C3 packs an incredible amount of features on a tiny board. The SOC MCU is an Espressif ESP32C3 running at up to 160 MHz. It comes with 400 kB SRAM and 4 MB flash, plus 2.4 GHz WiFi and BLE 5.0 Bluetooth. I got mine cheap when Seeed was promoting them. So why not try to get this working with Rust/Embassy and get one step closer to being able to build embedded apps for the tiny RISC-V MCU board in the cover picture?

    Are you tired of blinky yet? I'm not. Blinky is not the point. If I wanted to flash an LED at 1 Hz, I could do it with any MCU, or even a 555 chip. The goal of all these blinky exercises is to work out how to use the Rust/Embassy toolchains to write async embedded apps.

    I thought I had found a good tutorial at https://shanesnover.com/2025/01/28/bootstrap-embassy-esp32c3.html. But on trying out the steps and updating the versions of the crates used, it turns out it's out of date, and it's less than 2 years old. For one thing esp-hal-embassy is no longer maintained. Right at the top of the crate page it says:

    🚧 This crate is no longer maintained 🚧
    
    This crates functionality has been merged into esp-rtos, where further development will occur. 

    So I browse the esp-rtos crate which is maintained by Espressif. From there I find that Espressif has a book on programming with Rust on ESP products. So they are supporting Rust on their products. Under toolchain installation I find that I have to do:

    $ rustup target add riscv32imc-unknown-none-elf

    for the C3. Under tooling installation I find that I have to do:

    $ cargo install esp-generate --locked
    $ cargo install espflash --locked
    

    to install 2 auxiliary tools. Great, that should save me a lot of work. So I do:

    $ esp-generate --chip=esp32c3 esp32c3-rust-blinky

    This brings up a ncurses configuration screen:

    I save the setup with s, and it creates my project crate. Looking at src/bin/main.rs, I see:

    #![no_std]
    #![no_main]
    #![deny(
        clippy::mem_forget,
        reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
        holding buffers for the duration of a data transfer."
    )]
    #![deny(clippy::large_stack_frames)]
    
    use esp_hal::clock::CpuClock;
    use esp_hal::main;
    use esp_hal::time::{Duration, Instant};
    
    #[panic_handler]
    fn panic(_: &core::panic::PanicInfo) -> ! {
        loop {}
    }
    
    // This creates a default app-descriptor required by the esp-idf bootloader.
    // For more information see: 
    esp_bootloader_esp_idf::esp_app_desc!();
    
    #[allow(
        clippy::large_stack_frames,
        reason = "it's not unusual to allocate larger buffers etc. in main"
    )]
    #[main]
    fn main() -> ! {
        // generator version: 1.3.0
        // generator parameters: --chip esp32c3
    
        let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
        let _peripherals = esp_hal::init(config);
    
        loop {
            let delay_start = Instant::now();
            while delay_start.elapsed() < Duration::from_millis(500) {}
        }
    
        // for inspiration have a look at the examples at https://github.com/esp-rs/esp-hal/tree/esp-hal-v1.1.0/examples
    }

    Ah no, this is not an async app. The MCU keeps polling the timer until 500 ms have elapsed. Looking again at the configuration screen, I should Add Embassy framework support. For that to be possible, I have to Enable unstable HAL features. Live dangerously I say. So the configuration screen is this:

    Another esp-generate run and I get this for src/bin/main.rs:

    #![no_std]
    #![no_main]
    #![deny(
        clippy::mem_forget,
        reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
        holding buffers for the duration of a data transfer."
    )]
    #![deny(clippy::large_stack_frames)]
    
    use embassy_executor::Spawner;
    use embassy_time::{Duration, Timer};
    use esp_hal::clock::CpuClock;
    use esp_hal::timer::timg::TimerGroup;
    
    #[panic_handler]
    fn panic(_: &core::panic::PanicInfo) -> ! {
        loop {}
    }
    
    // This creates a default app-descriptor required by the esp-idf bootloader.
    // For more information see: 
    esp_bootloader_esp_idf::esp_app_desc!();
    ...
    Read more »

  • Getting another blinky to work

    Ken Yap07/30/2026 at 11:19 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 HAL.

    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.

    Update: Ok, I've figured out how to download the ELF directly to the RP2040. The recommended tool is picotool which is maintained by the Raspberry Pi project. It does a lot more than elf2uf2-rs so can replace it.

    I had to change...

    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 10 project logs

Enjoy this project?

Share

Discussions

Bharbour wrote 08/03/2026 at 11:55 point

Good writeup, thanks

  Are you sure? yes | no

Ken Yap wrote 08/03/2026 at 12:03 point

You're welcome.

  Are you sure? yes | no

Similar Projects

Does this project spark your interest?

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