Rust for Arduboy

Hi dear community

I’m excited to announce: The crate is now Production ready.
The first projects are now done, with the crate a list will be at the end.

Read the readme.md careful, there is all the information you will need to set it up for you.
This Rust crate does not have all the functions that are provided by Arduboy2. But I will add more functions from time to time.

Things that already work:

  • Arduboy2 80%+ covered
  • EEPROM memory access
  • Progmem access
  • Arduboy2Tones 95% covered
  • ArdVoice 100% covered
  • Sprites loading.
  • Excluding unused libraries to save some resources
  • Heapless Strings and Vectors are now implemented
  • Rust for Arduboy Docs

Other things that already work you can find in the example’s folder.

If you have never done anything with rust, I don’t think this repo is for you.
If you want to help or have any questions, feel free to text me on Discord
Username: zenndev

:crab: Rust runs everywhere :crab:

Usage of the crate

In the Readme.md are instructions on how to run games and how to develop your own.

The future

We will implement:

  • ArduboyFX
  • FMsynth / FMop
  • Arduboy2 to 100%

I am experimenting with ways to implement the Arduboy2 library native, but at this point the Leonard AVR is not ready.

Edit:

Edit 2:
I made 3 pages for image conversion to rust.

It’s a fork of the Team ARG Converters but changed to be compatible with Rust.

Usage:
Just drag and drop your images on the converter.

Games written in the crate

Contributors

@ZennDev1337
@Primm

6 Likes

Heck yeah! I can’t wait for it to have 100% coverage; this is awesome!

(Rust programmer here, fyi): Do you have any benchmarks of like… caveats to using rust on Arduboy? Does it generate larger binaries, or perhaps use more/less globals at runtime? I mean I know rust can often have better performance than equivalent c programs compiled with gcc just because of like, guarantees with memory access patterns and such, but I also know it is often slightly slower or larger (not just because of static linking) whether thanks to llvm or… whatever. Or are these programs so simple it barely matters? I know the ISA is very small, and I don’t even know what compiler the arduino tools are using, so maybe it doesn’t matter at all, just wondering!

1 Like

benchmarks are difficult to realize because the libraries don’t run on my pc. in theory the total size should not grow immeasurably. the rust code is compiled to an .a file so the hex file is created normally from a cpp project at the end. basically all unused parts of the library should not end up in the final hex code.

unfortunately i haven’t programmed a big game yet so i haven’t reached the limits of memory yet. for now it was only important to me to get rust running optimization comes later ^^

ps the 40-50% are in my opinion the most important functions for the start but i will add new ones bit by bit. also i try to leave out all this linking of cpp libraries but i am not that professional in embbeded developmendt yet.

pps i am happy to get suggestions which functionality would be very important which one you don’t have yet. in the cargo docs you can see all the functions and constants

ppps if you look at the .cargo/config.toml you see the flag opt-levl =“z” this is for less performance but smaller size if the project gets to slow you can change this to 1-3

Does this actually manage to differentiate between RAM and progmem or is that something you haven’t got worked out yet?

As far as I can tell it doesn’t appear to be differentiating between the to, so you may end up running into some memory bugs at runtime.

In particular I notice that you’ve only wrapped the version of arduboy.print that prints from RAM (i.e. accepts const char *) and not the one that prints from progmem (i.e. accepts a const __FlashStringHelper *).


It also seems to miss the point of EEPROM.get and EEPROM.put. Those are supposed to be generic functions that work for any type. If you only make them work for uint8_t then they’re functionally no different to read and update.

Given the nature of what you’re trying to do (i.e. wrap C functions), you’d likely be better off skipping Arduino’s EEPROM entirely and using the underlying avr-libc EEPROM handling.

Like so:

#include "eeprom.h"

#include <avr/eeprom.h>

uint8_t arduboy_eeprom_read(uint16_t address)
{
	return eeprom_read_byte(reinterpret_cast<const uint8_t *>(address));
}

void arduboy_eeprom_update(uint16_t address, uint8_t value)
{
	return eeprom_update_byte(reinterpret_cast<uint8_t *>(address), value);
}

// You probably don't actually want this
// because people shouldn't be using it,
// but I'm leaving it here for completeness.
// void arduboy_eeprom_write(uint16_t address, uint8_t value)
// {
// 	return eeprom_write_byte(reinterpret_cast<uint8_t *>(address), value);
// }

void arduboy_eeprom_get(uint16_t address, uint8_t * object, size_t size)
{
	eeprom_read_block(object, reinterpret_cast<const uint8_t *>(address), size);
}

void arduboy_eeprom_put(uint16_t address, const uint8_t * object, size_t size)
{
	eeprom_update_block(object, reinterpret_cast<uint8_t *>(address), size);
}

After which you may be able to find some way of coercing a generic type into a uint8_t *.

Something with signatures like:

fn eeprom_put<T>(address: u16, object: T) { ... }
fn eeprom_get<T>(address: u16) -> T { ... }

I’m only lightly familiar with Rust, but given its low-level intent I presume there must be some way to inspect the bytes of a struct and/or coerce a pointer to a struct into a pointer to a sequence of bytes, even if it requires the struct to have a special attribute applied or inherit a particular trait (like Copy).

With some preliminary research, I think MaybeUninit<T> could be used for eeprom_get’s implementation.

E.g.

fn eeprom_get<T>(address: u16) -> T
{
	let mut buffer = MaybeUninit<T>::uninit();
	
	// Somehow turn 'buffer' into a 'uint8_t *'
	// to pass to 'arduboy_eeprom_get'.
	
	unsafe { buffer.assume_init() };
}

I’m somewhat guessing, but I think a full implementation could potentially be something along the lines of:

extern
{
	fn arduboy_eeprom_get(address: u16, object: * mut u8, size: usize);
	fn arduboy_eeprom_put(address: u16, object: * const u8, size: usize);
}

fn eeprom_get_direct<T>(address: u16) -> T
{
	let buffer = std::mem::MaybeUninit::<T>::uninit();
	
	let pointer = buffer.as_mut_ptr();
	let object_pointer = pointer as * mut u8;
	let object_size = std::mem::size_of::<T>();
	
	unsafe { arduboy_eeprom_get(address, object_pointer, object_size); };
	
	return unsafe { buffer.assume_init() };
}

fn eeprom_get<T>(address: u16, object: & mut T)
{	
	let pointer = object as * mut T;
	let object_pointer = pointer as * mut u8;
	let object_size = std::mem::size_of::<T>();
	
	unsafe { arduboy_eeprom_get(address, object_pointer, object_size); };
}

fn eeprom_put<T>(address: u16, object: & T)
{
	let pointer = object as * const T;
	let object_pointer = pointer as * const u8;
	let object_size = std::mem::size_of::<T>();
	
	unsafe { arduboy_eeprom_put(address, object_pointer, object_size); };
}

(I’m basing most of this on what I managed to discern from this SO answer.)

There’s a chance that some of this is undefined behaviour (I’m not exactly familiar with Rust’s rules), but theoretically it makes sense. I’m most worried about whether I’m using MaybeUninit correctly and whether it’s valid to just convert any old * T to a * u8.

There’s also a chance that #[repr(C)] might be needed on any struct written to or read from EEPROM. Rust’s standard object layout might be suitable, I’m not sure. I also wonder if that would affect the size of the stored struct. (Obviously using as little EEPROM as necessary is highly desirable. #[repr(packed)] may be beneficial, though it might increase progmem size. Or it may make no difference whatsoever given AVR is an 8-bit platform.)

Using * std::ffi::c_void might have been another option, but I didn’t look into it much because it seemed like it would possibly be more complicated than using * u8. It might just be as simple as using as * c_void, but I couldn’t find anything confirming either way.

During my research I also saw std::mem::transmute mentioned quite a bit, but most places heavily discouraged it, so I presumed it would be ‘heavy handed’ for some reason. (Though it might just be one of those cases where people make it sound scarier than it is to discourage the inexperienced lest they misuse it.)

This is great! I’ve always wanted to get into Rust but have always procrastinated, now I finally have a reason to!

okkey where do i begin?

for the part about eeprom get and put Holy i didn’t noticed that lol i was always questioning why are there 2 methodes to do the same ^^ i will try to fix that in the next few days thx for the hint.

and yes progmem is absolutly the same as ram. but in my implementation is it like static / mutable stuff is in the ram, and const stuff like bitmaps and tone sequenzes are in progmem i guess the memory management internaly differents the two idk

there is a link feature

#[link_section = ".progmem.data"]//<--- this is what i meant
static player_sprite2: [u8; 34] = [
    16, 16, 0xfc, 0x02, 0x19, 0x25, 0x25, 0x19, 0x01, 0x01, 0x01, 0x01, 0x19, 0x25, 0x25, 0x19,
    0x02, 0xfc, 0x3f, 0x40, 0x80, 0x98, 0x8c, 0x86, 0x82, 0x82, 0x82, 0x82, 0x86, 0x8c, 0x98, 0x80,
    0x40, 0x3f,
];

but i’m realy not shure if it makes any diffrence I’m realy new into embedded programming with rust.
if i understand it right there is something like the dynamic RAM(2560 Bytes) and besides that there is other memory(PROGMEM 28672 Bytes). and this is, i guess the diffrence between the two examples here:

#[link_section = ".progmem.data"]//<--- progmem
static player_sprite2: [u8; 34] = [
    16, 16, 0xfc, 0x02, 0x19, 0x25, 0x25, 0x19, 0x01, 0x01, 0x01, 0x01, 0x19, 0x25, 0x25, 0x19,
    0x02, 0xfc, 0x3f, 0x40, 0x80, 0x98, 0x8c, 0x86, 0x82, 0x82, 0x82, 0x82, 0x86, 0x8c, 0x98, 0x80,
    0x40, 0x3f,
];

const  player_sprite2: [u8; 34] = [
    16, 16, 0xfc, 0x02, 0x19, 0x25, 0x25, 0x19, 0x01, 0x01, 0x01, 0x01, 0x19, 0x25, 0x25, 0x19,
    0x02, 0xfc, 0x3f, 0x40, 0x80, 0x98, 0x8c, 0x86, 0x82, 0x82, 0x82, 0x82, 0x86, 0x8c, 0x98, 0x80,
    0x40, 0x3f,
];

for the print thing…
I implemented print for:

pub trait Printable
where
    Self: Sized,
{....
    fn print(self) {....}
}

impl Printable for i16 {....}
impl Printable for u16 {....}
impl Printable for i32 {....}
impl Printable for u32 {....}
impl Printable for &[u8] {....}//<--- this is the *const [u8]
impl Printable for &str {....}//<---- This will be converted to a *const [u8]

i wasn’t aware of the const __FlashStringHelper * struct but i will research that and look if i can implement that.

one quesstion i have is:
is it possibe to flash a .elf file to the arduboy? i see always only a .hex file upload…
i have a arduino uno and there i can write native rust code and upload it via a .elf file.
but on the arduboy it fails always to upload because i would be very happy to port the Arduboy2 library native to rust. because the wrapper method is kinda janky

thx for the feedback. so far this is a insane wholesome and nice community here :hearts:

1 Like

well i did test the overhead now

Test 1

i took the demo2 from the make-your-own-arduboy-game-part-2

in C

//Jonathan Holmes (crait)
//October 18th, 2016
//Printing Text

#include <Arduboy2.h>
Arduboy2 arduboy;

void setup() {
  // put your setup code here, to run once:
  arduboy.begin();
  arduboy.clear();
  arduboy.print("Holmes is cool!");
  arduboy.display();
}

void loop() {
  // put your main code here, to run repeatedly:

}

and in Rust

#![no_std]
//Include the Arduboy Library
//Initialize the arduboy object
use arduboy_rust::prelude::*;
//The setup() function runs once when you turn your Arduboy on
#[no_mangle]
pub unsafe extern "C" fn setup() {
    // put your setup code here, to run once:
    arduboy.begin();
    arduboy.clear();
    arduboy.print("Holmes is cool!\0");
    arduboy.display();
}
#[no_mangle]
#[export_name = "loop"]
pub unsafe extern "C" fn loop_() {
    // put your main code here, to run repeatedly:
}

after build and upload of the Cversion:

  • 6’362 Bytes (22%) PROGMEM (Max 28’672 Bytes)
  • 1’209 Bytes (47%) dynamic RAM (Max 2’560 Bytes)

after build and upload of the Rust version:

  • Flash: [===///////////////] 25.2% (used 7226 bytes from 28672 bytes)
  • RAM: [=====////////////] 48.4% (used 1238 bytes from 2560 bytes)

Test 2

in C

and in Rust

after build and upload of the C version:

  • 9’976 Bytes (34%) PROGMEM (Max 28’672 Bytes)
  • 1’790 Bytes (69%) dynamic RAM (Max 2’560 Bytes)

after build and upload of the Rust version:

  • Flash: [==== ] 36.8% (used 10548 bytes from 28672 bytes)
  • RAM: [======= ] 66.9% (used 1712 bytes from 2560 bytes)

yes there is a small overhead but in this case ignorable i think.
i will update these mesurements when i did a larger project.

i dont have the std in my projects.

i guess it does differentiate between RAM and progmem as far as i see in the logs my bitmaps and tone sequenses are in the progmem

The thing is, internally it really isn’t.

Reading from RAM uses different machine code instructions to reading from progmem (for the AVR architecture, which is the CPU architecture the Arduboy uses).

That’s why in C++ Arduboy code people have to use macros like pgm_read_byte that are implemented with inline assembly and translate to specialised instructions.

And it’s why Arduino’s Print class has both print(const char *) and print(const __FlashStringPointer *). They use different machine code instructions to read the characters.

(Note: platforms like ARM don’t have this issue - ARM uses the same machine code instruction to read from both SRAM and flash memory, so no special pointer handling is necessary. Unfortunately AVR isn’t like that and does require the special handling at the library level.)

That likely does the same thing as what the PROGMEM macro does, which would be a decent chunk of the problem solved.

It isn’t actually a struct. It’s a clever/nastly little trick that relies on a quirk of C++.

The code declares struct __FlashStringHelper;, but never defines it, so it becomes legal to create a __FlashStringHelper *, but not a __FlashStringHelper, because the struct is never properly defined.

From what little I know of Rust, wrapping a const char * in a struct (or union) should do the job. All you really need is some kind of wrapper that can ‘wrap’ and ‘unwrap’ a const char *. At the end of the day the trick is just creating a new ‘dummy’ type that can be used to differentiate between ‘pointer to string in RAM’ and ‘pointer to string in progmem’.

E.g. something like:

#[derive(Copy)]
struct PString
{
	pointer: * const char
}

impl Printable for PString
{
	// ...
}

And then you would just have to be careful to create a PString from any strings stored in progmem.


I’m not completely certain, but I would presume not.

A .hex is a textual representation of the in-memory code image (i.e. all machine code and data), in Intel Hex format.

What tool do you use to do that?

Are you sure it’s not just uploading a .hex that’s generated from the .elf file?


For solving the progmem issue, theoretically it ought to be possible to go even crazier and do something like:

struct ProgmemPointer<T>
{
	pointer: * const T
}

impl From<* const T> for ProgmemPointer<T>
{
	fn from(value: * const T) -> ProgmemPointer<T>
	{
		ProgmemPointer<T> { value }
	}
}

impl Into<* const T> for ProgmemPointer<T>
{
	fn into(self: & const ProgmemPointer<T>) -> * const T
	{
		self.pointer
	}
}

impl Deref for ProgmemPointer<T>
{
	type Target = T 

	fn deref(&self) -> T
	{
		// Somehow copy the data from progmem
		// into a new 'T' object
		// and then return that object.
	}
}

(It’s actually possible to do something like this in C++ too. I wrote a library that does something like this, but never got around to publishing it.)


I noticed core::mem was used somewhere in your code, so you should be able to use that instead.

Without needing #[link_section = ".progmem.data"]?

And are the images drawn correctly?

Crucially, what do you get if you try to dereference a pointer that points to something stored in progmem?

Big Thanks to @Pharap
I got the print form progmem implemented the repo have now a new demo to show how to work with bitmaps, tone sequences and with Text in Rust.

1 Like

Ah ok, seems like there’s some kind of constant overhead. That seems reasonable, though I’d have to see how it fares when (if) it all gets into rust. Thank you for testing!

1 Like

i’m on the way to fix / minimize the overhead i have now for example added the f macro to save all the text to the progmem and also fixed some weird rust things under the hood i have pushed 6 commits to the repo ^^

Like Java, ‘write once, debug everywhere’ :slight_smile:

2 Likes

i’m surprised to see it compared to java; i debug my rust programs the least of any that i work on. it’s a very cozy language… after the initial headache of learning it!

1 Like

I know of Rust but have never used it. Are you using it for work? If so, in what context?

Oh goodness no, I could never convince my boss to use rust, he’d say “I can’t hire rust developers!”. I use C# at work (which I used to love and now kinda dislike for various reasons)

I use rust for personal projects. I wrote my thesis on compilers and hardware optimization, so I always loved working with lower level languages. But there’s a lot of stuff where you can’t REALLY use C, unless you absolutely need performance (because you’re just wasting tons of time), so I was never able to use a low level language for larger projects. Someone introduced me to rust about 5 years ago and I thought it was cool, but then learned it and thought it was stupid lol. A few years later I went back to it and now I love it, it actually lets me program big personal projects in a low level language without feeling like it’s a waste of time. I really enjoy it! I won’t say it’s the best language, it’s not for everyone and I don’t agree with people who are like “Everyone should use it!”. I don’t think everyone should use python either, they’re just tools for different jobs. Like… I wouldn’t want to write a UI in rust. I do have opinions on C++ though…

Oops, I kinda went on and on, sorry!

No problem.

Sort of why I asked … there is a reluctance to change directions in business if the result is more expensive or limited resources. Another issue is that unless its ‘greenfields’ development, you tend to use whatever has been used previously as its juts easier.

Explains while COBOL developers are still in demand :slight_smile:

Bingo!

Do tell (and start a religious war).

1 Like

I will! But I don’t have much to say really. I think C++ was a very important language, so I won’t say it’s a mistake or whatever. But it’s trying to be two things at once and failing at one of them spectacularly. The other thing it usually does well, in spite of its best efforts to ruin it. There are languages you can easily use and deploy today that are far better suited to the various things C++ is trying to solve, that’s all. It’s like… people stopped using Ada when C++ came around, and now there are things better than C++. Just like you said, I think it’s just impetus that keeps us using it.

Also I don’t like Mr. Stroustrup (as a programmer, I don’t hate him as a person) but that’s neither here nor there lol.

1 Like

I can see @pharap about to chime in. :slight_smile:

Screenshot 2023-08-09 at 11.33.45 am

Screenshot 2023-08-09 at 11.37.42 am

Just making fun at your expense @pharap … sorry.

3 Likes

To be fair Ada is still alive and kicking, even if it’s not a particularly popular language. Its last major version was released in 2012. Compare that to something like Smalltalk that hasn’t had a stable release since 1980 - the year Ada was released.

It’s definitely not a language for the faint of heart given how many features it has, but its features were surprisingly nice for the time.


Don’t get your hopes up, I’m too tired to start an argument over the merits and pitfalls of C++.

Though I will say that the thing that made it popular in the first place, attempting to be ‘C with classes’, is also the thing that came to drastically hold it back. Even Bjarne Stroustrup has said “Within C++, there is a much smaller and cleaner language struggling to get out”.

Though I’ll throw in another Stroustrup quote for free:
“There are only two kinds of languages: the ones people complain about and the ones nobody uses”.

2 Likes