Author Topic: Can I program a RP2040 in rust without any libraries or crates?  (Read 4829 times)

0 Members and 1 Guest are viewing this topic.

Online brucehoult

  • Super Contributor
  • ***
  • Posts: 6411
  • Country: nz
Re: Can I program a RP2040 in rust without any libraries or crates?
« Reply #25 on: October 10, 2025, 11:27:26 am »
Driving at 100 km/h on a 2-lane road with opposing traffic, and overtaking using their lane from time to time, is fundamentally unsafe.

True. But analogies such as that are silly and detract from the useful discussions.

Why so?

If anything, programming in C or asm is much safer than those things, because you can contemplate your logic in extreme slow motion, and test it thoroughly before releasing it into production to be used billions of times.

Quote
Wherever and whenever possible it is desirable to use dual carriageways with crash barrier dividing the two directions, and to restrict travelling on single carriageway roads to the minimum necessary.

My nearest such road (other than a few hundred metres near major intersections) is 150km away near a major city (1.8m pop). I go there once or twice a year. Much of my driving is on gravel roads.
 

Offline tggzzz

  • Super Contributor
  • ***
  • Posts: 23122
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
Re: Can I program a RP2040 in rust without any libraries or crates?
« Reply #26 on: October 10, 2025, 02:41:33 pm »
Driving at 100 km/h on a 2-lane road with opposing traffic, and overtaking using their lane from time to time, is fundamentally unsafe.

True. But analogies such as that are silly and detract from the useful discussions.

Why so?

Er.. I illustrated that in my next paragraph. Some people would have snipped that in order to (try to) make their point, but you avoided that sin!

Quote
If anything, programming in C or asm is much safer than those things, because you can contemplate your logic in extreme slow motion, and test it thoroughly before releasing it into production to be used billions of times.

Quote
Wherever and whenever possible it is desirable to use dual carriageways with crash barrier dividing the two directions, and to restrict travelling on single carriageway roads to the minimum necessary.

My nearest such road (other than a few hundred metres near major intersections) is 150km away near a major city (1.8m pop). I go there once or twice a year. Much of my driving is on gravel roads.

So your environment constrains you to do the "equivalent" of programming in assembler; fair enough.

Most people don't live under such constraints, and you should realise the advantages that gives them.
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Online brucehoult

  • Super Contributor
  • ***
  • Posts: 6411
  • Country: nz
Re: Can I program a RP2040 in rust without any libraries or crates?
« Reply #27 on: October 11, 2025, 03:15:54 am »
So your environment constrains you to do the "equivalent" of programming in assembler; fair enough.

I enjoy programming in assembler. C is often close enough, and is a lot more portable.

Quote
Most people don't live under such constraints, and you should realise the advantages that gives them.

I've lived in the San Francisco Bay Area. I've lived in Moscow.

Those have advantages, but on the whole I like it here more, especially with Starlink.


 
The following users thanked this post: voltsandjolts

Offline tggzzz

  • Super Contributor
  • ***
  • Posts: 23122
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
Re: Can I program a RP2040 in rust without any libraries or crates?
« Reply #28 on: October 11, 2025, 07:52:38 am »
So your environment constrains you to do the "equivalent" of programming in assembler; fair enough.

I enjoy programming in assembler. C is often close enough, and is a lot more portable.

I've done it since the 70s, and still read assembler when sanity checking C compiler output.

I've never had to do that with other languages.
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 
The following users thanked this post: pardo-bsso

Online brucehoult

  • Super Contributor
  • ***
  • Posts: 6411
  • Country: nz
Re: Can I program a RP2040 in rust without any libraries or crates?
« Reply #29 on: October 11, 2025, 08:56:05 am »
I've done it since the 70s, and still read assembler when sanity checking C compiler output.

I've never had to do that with other languages.

That speaks to limited experience. I was tracking down code generation bugs in the NBS Pascal compiler for PDP-11 in 1981, several years before I first saw a C compiler.

There is no inherent reason why code generation for C would be more error-prone than for any other language, and in particular for similarly powerful languages such as Pascal or PL/I or Algol or BCPL (all of which I've used).
 

Offline tggzzz

  • Super Contributor
  • ***
  • Posts: 23122
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
Re: Can I program a RP2040 in rust without any libraries or crates?
« Reply #30 on: October 11, 2025, 09:12:13 am »
I've done it since the 70s, and still read assembler when sanity checking C compiler output.

I've never had to do that with other languages.

That speaks to limited experience. I was tracking down code generation bugs in the NBS Pascal compiler for PDP-11 in 1981, several years before I first saw a C compiler.

There is no inherent reason why code generation for C would be more error-prone than for any other language, and in particular for similarly powerful languages such as Pascal or PL/I or Algol or BCPL (all of which I've used).


"Error prone" needs explaining. I'm not thinking solely of compiler bugs.

As for those other languages, the Algol-60 compiler worked as expected - unsurprising considering Tony Hoare wrote it! Never needed to bother with PL/1 and BCPL, fortunately.
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Offline az1

  • Regular Contributor
  • *
  • Posts: 61
  • Country: us
Re: Can I program a RP2040 in rust without any libraries or crates?
« Reply #31 on: February 06, 2026, 02:20:57 am »
Is it also possible to do this in rust without using any libraries? Is no_std enough?

Sure.  But then it's a matter of what you hope to gain.  Are you trying to learn Rust?  Are you trying to avoid perceived bloat?  Are you trying to gain more control?

You're not going to learn a ton about Rust by forgoing the support crates. Poking at arbitrary addresses is pretty similar (even if the syntax is a bit different) across most languages. Instead you'd do well to look at the 4? 5? different tools used to generate thin wrappers over an SVD and think about the different approaches each take.

Do I always need to use the unsafe keyword to write to a specific memory address? Do I need the unsafe keyword to read a specific memory address?

https://doc.rust-lang.org/std/ptr/index.html

Safety in Rust is often seen as an all-or-nothing thing when it's more nuanced than that.  You'll find safe wrappers over unsafe bits all over Rust.  The idea is that your safe wrapper provides guarantees about how the unsafe bits act.

Reading/writing to arbitrary addresses is unsafe which is why you get thin abstractions in the form of PACs.  PACs provide safe wrappers that gate access to the unsafe bits.  By using addresses from the SVD to generate wrapper functions you're guaranteed that you're not poking a region of memory you ought not to.  At the HAL level by using a (potentially zero-sized) struct to gate access to the wrappers you're using the compiler to ensure that you're not engaging in unsafe concurrent access.

Keep in mind that most PACs will make heavy use of const functions (they're a different beast than C++ 'const' functions).

https://doc.rust-lang.org/std/keyword.const.html#compile-time-evaluable-functions

I don't get the point though. … Ok, the RP2040 is quite a bit bigger microcontroller than a CH32V003. With up to 16 MB of flash (vs 16 KB on the '003) perhaps you don't even care that a normal Rust "hello world" is 3.5 MB.

I assume you're on a Linux machine?  On my Mac an unoptimized "Hello, World" program written in Rust compiles to a bit over 430k (I'll get to why in a moment).  If I actually make an apples-to-apples comparison, the Rust "Hello, World" compiles down to 66% of the size of the C version.

"Wait. What? Hang on. What? Wait. Wait. What?" — Dara Ó Briain

On most operating system targets Rust has a bit of a reputation for generating large binaries.  This comes down to two things:

1.  Rust links programs statically.  There are exceptions of course, but a general rule of thumb is that if you're compiling with Rust you'll end up with a statically linked binary.  Apple doesn't provide a static version of its C runtime so there's your reason why the Mac "Hello, world" look so much smaller.  Contrast that with clang and gcc where they will typically link everything dynamically.  If you link your C example statically you'll get a binary a bit under 700k.  The Rust binary still looks bigger, but we're getting closer.

2. Rust includes a broader scope of debug symbols and does not garbage collect them.  You can compress them, garbage collect them, or just strip them all out if you want.  If you strip that Rust binary it's smaller than the stripped C binary. Simple as.  The stripped, statically linked C binary weighs in at 586k, the stripped Rust binary at 389k.

But none of that matters in the example of a resource constrained microcontroller.  You're almost certainly not going to stuff a runtime linker in that space, and I can't think of a good reason why you'd be flashing your debug symbols to the MCU.

It's almost like programming a PC.

If you've concerns about Rust codegen you can always plug in whatever on Godbolt and look at the generated assembly.  Last I checked Godbolt supports Cortex-M targets for Rust.

You can always just hand-copy/paste the bits you want, right?

Code: [Select]
pub const GPIOA: gpio::Gpio = unsafe { gpio::Gpio::from_ptr(0x4001_0800usize as _) };
pub const GPIOC: gpio::Gpio = unsafe { gpio::Gpio::from_ptr(0x4001_1000usize as _) };
pub const GPIOD: gpio::Gpio = unsafe { gpio::Gpio::from_ptr(0x4001_1400usize as _) };
pub const ADC1: adc::Adc = unsafe { adc::Adc::from_ptr(0x4001_2400usize as _) };
pub const TIM1: timer::Adtm = unsafe { timer::Adtm::from_ptr(0x4001_2c00usize as _) };
pub const SPI1: spi::Spi = unsafe { spi::Spi::from_ptr(0x4001_3000usize as _) };
pub const USART1: usart::Usart = unsafe { usart::Usart::from_ptr(0x4001_3800usize as _) };

And that uses bits from here: https://github.com/ch32-rs/ch32-metapac/blob/main/src/registers/gpio_v0.rs

Sure, I agree making a struct for GPIO and casting an integer to a pointer to it is less verbose in C. Which is why I use C.

BUT Rust IS SAFE! And BLAZINGLY FAST!

You've just instantiated structs from a PAC manually while completely ignoring that you've also brought safe accessors for the registers and individual fields along for the ride.  You're not creating a raw pointer.  Unsure what that proves.

I've not looked at the CH32 metapac specifically but the typical pattern is that accessors in a PAC are const functions which means compile time not runtime evaluation.  Again, play around with Godbolt if you want to see what that means in terms of codegen.

Your very first problem in Rust for a microcontroller is dealing with mcu registers. Without a volatile keyword in the language you will either depend on a library or create all the ugliness of it in your own code.

In contrast, for C/C++ the volatile is a language keyword so you can deal with mcu registers, and other things, with no outside help-
https://godbolt.org/z/so9WbcT4x

https://doc.rust-lang.org/core/ptr/fn.read_volatile.html

Quote
I also think it will yield an executable with comparable size and memory consumption
let us know how your experiments turn out.

Although even C rp2040 programs come with an unusual amount of overhead, setting up the run-from-serial-flash and such.  Their "blink" example is over 20k.

How much of that is somewhat necessary initialization overhead though?  The RP has four cores across two architectures to initialize.  On the RA4M1 (single Cortex-M4 core) I think I saw claims of 30k for a blink example with the Renesas FSP in the Arduino thread?

With Rust on the RA4M1 I'm at 20k for a simple blink app, 10k if I omit the RTT logging.  That includes 1.25k worth of ARM vector table and flash address space that Renesas set aside for configuration registers.  That's with the Embassy runtime so that means a high level timer API, async support, and all that jazz.

Code: [Select]
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = ra4_hal::init();

    print_clock_config();

    let mut led = Flex::new(p.P102);
    led.set_as_output();

    loop {
        led.toggle();
        Timer::after_millis(333).await;
    }
}


Before everyone starts hyperventilating let's take a look at what a PAC actually does.  Here's a cut down version of one of the timers on the RA.

Code: [Select]
pub struct Agt {
    ptr: *mut u8,
}

impl Agt {
    pub const fn agtcmsr(self) -> crate::common::Reg<regs::Agtcmsr, crate::common::RW> {
        unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x0eusize) as _) }
    }
}

pub struct Agtcmsr(pub u8);
impl Agtcmsr {
    pub const fn toea(&self) -> bool {
        let val = (self.0 >> 1usize) & 0x01;
        val != 0
    }
    pub const fn set_toea(&mut self, val: bool) {
        self.0 = (self.0 & !(0x01 << 1usize)) | (((val as u8) & 0x01) << 1usize);
    }
}

Underneath it all you're keeping track of the AGT instance with a single pointer and the register itself compiles down to a single byte if you're using that.  Note that the accessors are const functions (get your heads out of the C++ gutter).  The accessors contain unsafe pointer arithmetic but they themselves aren't unsafe.  Why? Because the assumption is that the Agt struct was initialized with a valid address, and the constructors are programmatically generated from the SVD. The accessors are safe because you're assuming that their hardcoded offsets point to a valid location relative to the register.  Such bloat.  Much scary.

In this case you wouldn't (typically) manually create an Agt instance outside of the crate.  The PAC defines an unsafe constructor that you can use if needed.  But it also defines constants generated from the SVD.  As is tradition unused constants get optimized out by the linker.  You could do this by hand but why?

Code: [Select]
pub const AGT0: agt::Agt = unsafe { agt::Agt::from_ptr(0x4008_4000usize as _) };
pub const AGT1: agt::Agt = unsafe { agt::Agt::from_ptr(0x4008_4100usize as _) };

And here's what the writers look like.

Code: [Select]
impl<T: Copy, A: Write> Reg<T, A> {
    #[inline(always)]
    pub fn write_value(&self, val: T) {
        unsafe { (self.ptr as *mut T).write_volatile(val) }
    }
}
impl<T: Default + Copy, A: Write> Reg<T, A> {
    #[inline(always)]
    pub fn write(&self, f: impl FnOnce(&mut T)) {
        let mut val = Default::default();
        f(&mut val);
        self.write_value(val);
    }
}

I'll leave it as an exercise for the reader to think about what advantages this has over the typical C pattern and why that's not nearly as scary and bloated as it might seem at first blush.

Suffice to say you could do this by hand, but this generic implementation provides a consistent API across all your registers and compiles down to efficient code.  So why write this by hand? To answer that we're back at: look at the different tools used to create PACs and look at the different tradeoffs they've made.  I've used chiptool for my examples but svd2rust is probably more popular.  Off the top of my head derive-mmio and safe-mmio are the other popular alternatives.
 
The following users thanked this post: tggzzz, I wanted a rude username


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf