Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tooling: Ideal developer experience #6

Closed
japaric opened this issue Sep 29, 2016 · 13 comments
Closed

Tooling: Ideal developer experience #6

japaric opened this issue Sep 29, 2016 · 13 comments
Labels
feb-2019-cleanup These issues are proposed to be closed, as part of a cleanup of issues in February 2019 microcontroller

Comments

@japaric
Copy link
Member

japaric commented Sep 29, 2016

Building programs, written in C or Rust, "manually" for microcontrollers is quite complicated as it
involves several device specific files (e.g. linker scripts) and configuration (e.g. compiler flags).

This issue is about what the ideal experience would look like for embedded Rust developers. I think
it's informative to study first what the experience of C/C++ developers looks like:

Embedded C/C++ developers usually (always?) use an IDE and a SDK to make the process
straightforward. Here's how the process looks like for them (disclaimer: this is my vague
recollection)

  • User: Picks the target device (via a GUI)
    • The SDK generates the required linker scripts, chooses what flags to pass to gcc and also what
      libraries (the soft float variant or the hard float variant?) to link to.
  • User: Configure the device (via a GUI): e.g. pick the clock frequency
    • The SDK generates initialization code (crt0.o-ish stuff + configuration of clock, PLL, etc.)
      that runs before main.
  • User: Writes their program in a high level API
    • The SDK will compiles the right implementation of the high level API for the target device
  • User: Clicks "Build & Debug"
    • The SDK compiles the program, flashes the binary to the device and starts a debug session
    • Apart from the standard debug experience: step by step execution, breakpoints, disassembly,
      inspection of memory, etc. the SDK also provides "registers maps" that describe the contents of
      register in a human readable way: instead of hex-speak 0xdeadbeef, you get "the CEN bit of
      the CR register is set" but in a more graphical way.

I think those are, at a "minimum", the conveniences that SDKs provide. SDKs may also include an
OS/RTOS but that sort of escapes the realm of tooling.

What are other niceties that C SDKs provide? And how do you picture the ideal Rust developer
experience? Finally, how could we materialize all those conveniences using Cargo? We probably want
to spawn more issues about this last question.

@japaric
Copy link
Member Author

japaric commented Sep 29, 2016

For starters, I'd like something like this to just work:

// src/main.rs
#![no_std]

// Crate for the STM32F3DISCOVERY
#[macro_use]
extern crate f3;

#[start(f3)]
fn main() -> ! {
    println!("Hello, world!");

    loop {}
}
$ cargo build --target thumbv7em-none-eabihf
  • No need to mess with a .json file. The thumb target is in the compiler.
  • No need to write a linker script. The f3 crate creates one for me.
  • No need to mess with RUSTFLAGS, link-args and other nonsense. The f3 crate injects the required linker flags (probably -nostarfiles and -Tsome_linker_script.ld) and rustc flags (probably -C relocation-model=static).
  • No need to use no_mangle magic. The #[start] takes cares of mangling and verifying the signature of main against a signature that the f3 crate specifies.
  • f3 depends on core. Cargo will build that for me. No need for me to build it manually or reach out for Xargo.

@japaric
Copy link
Member Author

japaric commented Oct 5, 2016

So, today (or actually by the time the next nightly is released), using the f3 crate this is as close as I can get to my ideal:

  • This code:
#![no_main]
#![no_std]

#[macro_use]
extern crate f3;

#[export_name = "main"]
pub fn main() -> ! {
    iprintln!("Hello, world!");

    loop {}
}
  • plus this .cargo/config
[target.thumbv7em-none-eabihf]
rustflags = [
    "-C",
    "link-arg=-Tstm32f3discovery.ld",
    "-C",
    "link-arg=-nostartfiles",
]
  • and Xargo.
$ xargo build --target thumbv7em-none-eabihf

produce a binary that Just Works.

Comparing it to my ideal:

  • No need to mess with a .json file. Done. The thumbv* have landed in master.
  • No need to write a linker script. Done. The build script in f3 provides the linker script.
  • No need to mess with RUSTFLAGS, link-args and other nonsense. This can't be done today. We need a new Cargo feature for this: build script need to be able to pass arbitrary linker arguments to rustc (then rustc can pass those to arm-none-eabi-gcc).
  • No need to use no_mangle magic. I'm still relying on unmangled symbols to do this. That's why you see #[export_name] and pub fn.
  • f3 depends on core. Cargo will build that for me. Not yet implemented but, hopefully, will be by the end of year ...

It's not that bad except for the .cargo/config. I'd really like to get rid of it.

Also note that this example is relatively simple because I'm only targetting one device. But, ideally, whatever features we request in rustc/Cargo must be able to "scale" and support more complicated scenarios where one has to compile a program for a bunch of different devices that have different memory layouts (linker scripts) and must be compiled with different optimizations flags (-mcpu).

@igutekunst
Copy link

Targeting different environments is extremely complicated, especially once it comes to making it scalable. In addition, as much as I'm sure we'd all like to provide a solution to everyone's problems, we can't foresee every possibility, so we should probably focus on something that is easy for beginners (sane defaults), but is easily expandable to work in a larger tool ecosystem.

Platform IO has taken the approach of categorizing environments with three characteristics: platform, framework, board, with the goal of making it very nice to work within the PlatformIO world.

Frameworks defines what the "core" standard library is, e.g. the functions provided by the Arduino environment. Libraries can then be written against the Arduino framework. This then allows any platform to implement the Arduino framework. Boards are constrained to a single platform, but usually support multiple frameworks.

Bringing this over to the rust world, a platform could hold a lot of the same meaning. It could imply an entry point into the user application, doing whatever is necessary to call a clean, unmangled main entry.

Going back to @japaric 's ideal, there are some potential areas worth investigating:

  1. Will there be a crate for every conceivable hardware configuration? This doesn't scale particularly well, and may involve a lot of duplication of configuration. A hierarchy like PlatformIO could alleviate this.
  2. Why should the user need to specify --target thumbv7em-none-eabihf ? Wouldn't it be nice if that just happened magically with a cargo build
  3. What about loading binaries to the board? There are countless ways to do this. Should cargo/xargo be responsible for this?

These three questions bring me to a more philosophical question: Where do you draw the line on what rust's responsibilities end? A batteries included approach is nice for beginners, but may end up being difficult for professional projects that need to integrate with other tools, whereas a unix "do one thing well" philosophy may be harder for beginners, but easier to include into bespoke development environments typical in the industry.

I wrote this hoping to have a somewhat complete thought, but instead, I feel I only reached the tip of an iceberg of complications.

Does anyone know any of the developers from PlatfromIO? Perhaps it's worth engaging them, even at this stage?

@jcsoo
Copy link

jcsoo commented Oct 6, 2016

I'm going to throw in one of my favorite quotes from Larry Wall: "Easy things should be easy, and hard things should be possible."

The top priority should be making it as easy as possible for a moderately experienced Rust programmer to go from reading about embedded Rust programming online to having a working demo blinking a LED on an actual board. This includes every barrier that you can think of along the way, some of which may not have much to do with Rust, including

  • Finding out that this is possible at all
  • Figuring out what hardware is recommended and how to purchase it
  • Figuring out what Rust tools are needed and how to install them (preferably automated)
  • Figuring out what third-party tools are needed and how to install them (preferably automated)
  • A well documented demo program for that specific board that can also serve as a skeleton
  • Instructions and tools that can take you from the cloned demo repository to a blinking LED including firmware upload (preferably automated)
  • Instructions and tools (where available) to help diagnose problems when things go wrong
  • Access to people that can help with all of these steps

It's most important that this entire chain works really well for one or two boards first so that we can get as many Rust developers (and non-Rust embedded developers that are interested in learning Rust) on board and contributing both feedback and code. Command line tools and scripts are more appropriate at this early stage and can in many cases be integrated into IDEs, with Makefiles as a common denominator if needed.

This is just as important for seasoned professionals that are evaluating Rust for serious projects. The quality of the out-of-box experience is a good proxy for the maturity of the ecosystem, plus professionals hate getting stuck even more than new hobbyists - this is work, after all, and they have enough experience to know that it's not simply their own lack of knowledge.

Give a good developer a great experience, and he or she may be eager to do the work to port it to a different board. A frustrated developer is going to give up and look for something else.

@japaric
Copy link
Member Author

japaric commented Oct 6, 2016

@igutekunst

Why should the user need to specify --target thumbv7em-none-eabihf ?

That's an interesting thought. Some crates are meant to be compiled for a particular target so it
makes sense to be able to omit this flag if one of those crates is present as a dependency.

Right now you can omit the target in the command line if you specify it in the build.target field of
.cargo/config but .cargo/config is a local file and not really attached to a crate itself so Cargo
ignores the .cargo/configs of dependencies when building a crate.

We could move that build.target key to Cargo.toml itself and have Cargo use that value if the key
is found in any dependency but that raises the question of a what should happen if a crate depends
on two dependencies that specify a different default target.

This is certainly RFC material.

What about loading binaries to the board? There are countless ways to do this. Should cargo/xargo
be responsible for this?

Cargo itself shouldn't deal with this. But users can create their custom Cargo subcommands: if e.g.
cargo-gdb is in PATH then cargo gdb can be used to call it. This gives the feel of cargo-gdb
being integrated into Cargo but it's actually not -- Cargo doesn't pass any extra information to
cargo-gdb. So one could create a cargo gdb subcommand to flash a program and immediately start a
GDB session and another cargo flash subcommand to only flash the program.

If would be up to the community to come up with these tools to flash and debug Rust programs. I
don't know if it's possible to come up with a single tool that can support every single device out
there but it's definitely an space worth exploring.

@japaric
Copy link
Member Author

japaric commented Oct 6, 2016

@jcsoo That sounds a lot like what I'm trying to do with my f3 crate and my Copper book.

The f3 crate provides a high level, Arduino like API that people can use to play with the STM32F3DISCOVERY. Its main goal is to demonstrate that "you can totally hack embedded systems using Rust and it's actually easy".

OTOH, the Copper book wants to be a reference that explains every detail/concept that you need to know to build a framework like the f3 crate from scratch.

To address the particular points you mention:

Finding out that this is possible at all
Figuring out what hardware is recommended and how to purchase it

The f3 can be used for this. It targets a single development board and if I add pictures (GIF) to it we could publitize it more.

Figuring out what Rust tools are needed and how to install them (preferably automated)

The copper book has instructions for this. And about making it automatic, @brson has plans to add SDK feature to rustup so it can install all the related, non-Rust tools with a simple rustup target add $triple command or something like that. We could leverage that.

A well documented demo program for that specific board that can also serve as a skeleton
Instructions and tools that can take you from the cloned demo repository to a blinking LED
including firmware upload (preferably automated)

The f3 has documentation for this and also more examples. But there's no automatic way to build the "blink" example.

Instructions and tools (where available) to help diagnose problems when things go wrong

The copper book has instructions about how to use OpenOCD+GDB to debug Rust programs that are running on a microcontroller. A related nicety is that the f3 has a iprint! family of macros to send formatted messages to the host machine so you can use that for "printf" debugging as well.

Access to people that can help with all of these steps

I'm here \o/ but I don't know what would be the most accessible channel to have me communicate with interested people. IRC, gitter, slack?

Command line tools and scripts are more appropriate at this early stage and can in many cases be
integrated into IDEs,

I have experience doing embedded development with Eclipse and think that it wouldn't be too hard to integrate the existing tooling that the f3 crate uses (which is very similar to the tooling that you would for C development) but I don't know if I'll have the time this month to sit down and do it.

@igutekunst
Copy link

I'm going to throw in one of my favorite quotes from Larry Wall: "Easy things should be easy, and hard things should be possible."

Couldn't agree with that more. A great quote!

We could move that build.target key to Cargo.toml itself and have Cargo use that value if the key
is found in any dependency but that raises the question of a what should happen if a crate depends
on two dependencies that specify a different default target.

I'm trying to not think about details just yet, but rather raise the more general question about build systems and dependency trees.

I'd say that if two crates require a different default target, they can't both be built.

Maybe it's worth defining platform, framework and board so I can express myself better.

Platform
Some CPU that can be targeted with similar compiler flags and linker scripts. There may be variations in the amount of RAM, or placement of symbols, but the instruction set and general compilation process is the same. Currently this is what rust calls a target, with the addition of a linker script

Framework
The set of default APIs available to a project. This usually defines the application entry point. Arduino for example has a setup and loop function that are called "magically" by the framework. A framework with an RTOS might have the entry point be the first Task.

Board
A particular hardware configuration, with a set of peripherals. It will have (for now) one supported platform, and one or more supported framework.

So would this actually play out?

I'm thinking there would be crates that provide frameworks for different platforms (targets), with most of them being board agnostic.

There could then be creates for various boards that bring together a framework and a platform into something usable to start an application layer project.

So if I wanted something Arduino like, there would be an arduino-stm32f4-discovery crate that would implement the "Arduino" framework (setup, loop functions). Most "application layer" creates that needed the Arduino API could depend on the Arduino platform, but that platform could be implemented by a number of different crates.

I don't think the current dependency system has a good way to express this kind of dependency tree. I'm too much of a beginning to see how to do this, but it's almost like you would want an Arduino trait, a "Embedded Rust" trait, a "Free RTOS" trait, and then various crates could implement the traits needed.

I just don't know how to express this using the crate system.

Do people understand what I'm proposing? If so, what do you think? If not, what part of this doesn't make sense. I'd really recommend looking at platformio.org. I'm not in any way affiliated with them, but really like their way of making the developer experience super smooth. It really does go well, even when it require multiple compilers, multiple bootloading tools, all workingon many different host operating systems.

@jcsoo
Copy link

jcsoo commented Oct 6, 2016

@jcsoo That sounds a lot like what I'm trying to do with my f3 crate and my Copper book.

The f3 crate provides a high level, Arduino like API that people can use to play with the STM32F3DISCOVERY. Its main goal is to demonstrate that "you can totally hack embedded systems using Rust and it's actually easy".

OTOH, the Copper book wants to be a reference that explains every detail/concept that you need to know to build a framework like the f3 crate from scratch.

Aha, I had stumbled across the Copper repository but hadn't actually looked at the book. It looks wonderful, and it looks like you have things very well covered. I'm going to have to pull out my STM32 board and try it out myself now!

And about making it automatic, @brson has plans to add SDK feature to rustup so it can install all the related, non-Rust tools with a simple rustup target add $triple command or something like that. We could leverage that.

This would be incredibly useful. Being able to install everything through a single rustup command immediately makes things simpler than almost all of the open source and commercial toolchains that I've seen.

Combining that with a small set of cargo / xargo subcommands for flashing, gdb, and logging allows us to put a user friendly facade around the various utilities that are needed.

Ideally we have simple subcommands that cover the basic tasks, along with concise, centralized help documentation. In addition to that we add the option to display the command lines that we are being submitted to the underlying tools such as OpenOCD or gdb as well as pointers to tool-specific documentation and a way to pass additional parameters. This makes it easy for expert users to see what we are doing and customize further as needed.

I'm here \o/ but I don't know what would be the most accessible channel to have me communicate with interested people. IRC, gitter, slack?

Probably the most important is Github itself. There's a lot of value to having people create Issues there - it gives them a chance to see if someone else has run into the same problem and forces them put a little bit of thought into describing the problem (often leading them to discover the solution on their own). Plus, solutions are there for people to see including links to bug fixes in case there are any that are required.

It's also one of the more scalable tools we have since a lot of people can participate without having to be logged in to a particular system. I'm OK subscribing to a Github repo but I really can't keep IRC, Gitter + Slack windows open and still get work done.

@japaric
Copy link
Member Author

japaric commented Oct 6, 2016

@igutekunst

This is how I'd map the concepts you mention to Rust code:

  • A framework is a crate that defines interfaces (traits) to peripherals.
// crate: arduino -- the Arduino "framework"

pub trait I2C {
    fn write(&mut self, byte: u8);
    fn read(&mut self) -> u8;
    // ..
}
  • There are also device-agnostic crates that target one or more frameworks:
// crate adxl345 -- an accelerometer

#[cfg(feature = "arduino")]
extern crate arduino;

#[cfg(feature = "arduino")]
use arduino::I2C;

#[cfg(feature = "arduino")]
fn get_accel_x<I>(&mut I) -> f32 where I: I2C {
    // ..
}

// (implementations for other frameworks go here)
  • Then there is a crate that implements a framework for a particular board. This crate also provides a linker script (through its build script) that defines the memory layout of the microcontroller.
// crate: arduino-stm32f3discovery -- Arduino framework for the STM32F3DISCOVERY

extern crate arduino;

use arduino::I2C;

// (re: static mut. this is just a example, we could expose this in different ways)
// in this case, the GPIO pins associated this I2C instance are
// hard-coded (not configurable by the user)
/// An instance of an I2C peripheral
pub static I2C1: I2C_Struct = (..);

pub struct I2C_Struct { .. }

impl I2C for I2C_Struct {
    // ..
}

// The real entry point of all programs
#[no_mangle]
pub fn _start() {
    extern {
        // the user entry point
        fn main() -> !;
    }

    zero_bss();
    init_data();

    I2C1::init();
    // ...
    main()
}
  • Finally user code would look like this:
# Cargo.toml
[dependencies]
arduino-stm32f3discovery = "1.0.0"
adxl345 = { version = "1.0.0", optional = true }

[features]
# build the adxl345 library for the arduino framework
default = ["adxl345/arduino"]
// src/main.rs
extern crate adxl345;
extern crate arduino_stm32f3discovery;

use arduino_stm32f3discovery::I2C1;

#[no_mangle]
fn main() -> ! {
   let ax = adxl345::get_accel_x(&mut I2C1);
   // ...
}

Oh, and the platform is just a built-in target like thumbv7em-none-eabihf plus extra optimization flags like -mcpu=cortex-m4, etc.

@japaric
Copy link
Member Author

japaric commented Oct 6, 2016

You could even break the arduino_stm32f3discovery crate even further in:

  • A crate that provides device-agnostic (no addresses) register maps for each peripheral
// crate stm32

// There are only struct declarations in this crate
mod gpio {
    // Registers common to all GPIO peripherals
    pub struct Registers {
        pub cr: RW<u32>,
        pub odr: WO<u32>,
        pub idr: RO<u32>,
        // ..
    }
}
  • Then an stm32f3discovery crate that gives addresses to those register maps:
// crate: stm32f3discovery

// GPIOA peripheral
pub fn gpioa() -> &'static gpio::Registers {
    unsafe { &*(0xdeadbeef as *const _) }
}
  • And finally the arduino_stm32f3discovery that implement the Arduino framework for the
    STM32F3DISCOVERY:
// crate: arduino-stm32f3discovery

extern crate arduino;
extern crate stm32f3discovery;

use arduino::Serial;

pub static mut SerialPort: Serial_Struct = (..);

pub struct Serial_Struct { .. }

impl Serial for Serial_Struct {
    // for example, use stm32f3discovery::gpioa and stm32f3discovery::uart1 here
}

@nastevens
Copy link
Member

Something that @japaric mentioned that I would like to expand on is the potential power of the cargo-foo binary naming plugging into the cargo command. A very complicated setup of cargo config, linker script, etc. can be made almost transparent by creating tools that tap into the cargo namespace. A totally contrived example:

cargo arduino new --board=uno - generates a template project with Cargo config setup for an Arduino Uno and includes any necessary dependencies in the Cargo.toml. This could potentially put a facade over the no_mangle portions of code as well, using a build.rs to dynamically generate the entry point code. Installing the cargo-arduino code would be simple as well using the cargo install command. We can even put a facade over cargo build that provides the necessary --target by calling something like cargo arduino build.

@jamesmunns
Copy link
Member

This issue has not been updated for a number of years. I do think that we need a better story around developer experience and tooling, however I am not sure if this issue captures the current state of the art. I would propose that we close this issue.

Marking this for a cleanup sweep. If we would like this to stay open, please provide an update to what this issue should be focused on.

@jamesmunns jamesmunns added the feb-2019-cleanup These issues are proposed to be closed, as part of a cleanup of issues in February 2019 label Feb 3, 2019
@jamesmunns
Copy link
Member

I am closing this issue, please feel free to open another issue if you would like this discussed further.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feb-2019-cleanup These issues are proposed to be closed, as part of a cleanup of issues in February 2019 microcontroller
Projects
None yet
Development

No branches or pull requests

5 participants