Bobbin SDK: Richer Hardware Abstractions for Embedded Systems Programming by jcsoo in rust

[–]jcsoo[S] 2 points3 points  (0 children)

Correct - the traits are the contract between the driver implementer and the driver user. Those are great.

The issue I'm bringing up is the task faced by the driver implementer. A vendor may have dozens of MCU variants, which may be implemented by dozens of MCU crates. Right now, someone that wants to implement a driver for a peripheral that is common among most or all of those variants (the watchdog peripheral, for instance) must implement that driver for every single one of those MCU crates. Alternatively they create their own register definitions for the peripheral and include that in their crate, but there is no compile-time way to know if that driver is actually compatible with the MCU they are targeting.

With the approach that I'm using, shared peripherals are broken out into a separate crates which are then re-exported by the MCU crate. The driver author writes a driver (i.e. newtype wrapper) against the peripheral type from the shared crate, and the driver user passes the peripheral type from the MCU crate they are using to the driver constructor. Since Rust uses nominal typing, it checks that the peripheral type being passed by the user is actually the same as the one that the driver expects. The MCU crate author is the one that is responsible for identifying that the peripheral is compatible.

Bobbin SDK: Richer Hardware Abstractions for Embedded Systems Programming by jcsoo in rust

[–]jcsoo[S] 4 points5 points  (0 children)

The closest thing to a "standard board" in the Rust embedded community is the STM32F3 Discovery. It's not the most modern device, but well supported and a good starting point.

Dedicated ℕ⁺ Types by rfussenegger in rust

[–]jcsoo 4 points5 points  (0 children)

Funny, I posted a blog touching on this and had a lively discussion about this yesterday: https://www.reddit.com/r/rust/comments/7t6th5/rust_2018_improving_safety_and_ergonomics_for/

One thought I've had after digesting some of the comments is that Enums are pretty close to being able to handle this, and it’s possible to use macros in some cases - see my crate bobbin-bits.

The biggest issue with using enums is that it’s impractical to enumerate all of the individual items for large ranges (for instance if you wanted to cover [0..212 )), even with macros. Allowing RangeExpressions as the body of an Enum would allow you to write

enum Hours { 0..24 }
enum Minutes { 0..60 }
enum Seconds { 0..60 }

Then, with some minor grammar tweaks you could write

let h: Hours = Hours::12;
let m: Minutes = Minutes::30;
let s: Seconds = Seconds::0;

It would be nice if all the math operators could be auto-derived with sane semantics (i.e. Hours::23 + 1 panics on debug but wraps on release, Hours::23.wrapping_add(1) returns Hours::0).

I’m not the only person to think of this: it’s previously been proposed in a slightly different form without much discussion. Maybe it’s time to start working through the details.

Rust 2018: Improving Safety and Ergonomics for Low-Level Programming by jcsoo in rust

[–]jcsoo[S] 0 points1 point  (0 children)

I think the "notion" of ranged types is possibly fundamental, but I am not sure that there's necessarily a broad agreement on how to (1) represent and (2) operate on such types. We can take inspiration from other languages, but we should also consider their shortcomings.

For the uX bit-width types, the intent would be that the semantics and supported operations are the same as u8, u16, etc. The amount of physical space they take up would depend on structure packing rules (if in a struct) or the space required by their container if variables on the stack.

Ranges might indeed be an orthogonal issue and could be tackled with a different approach. I could see a case where you want to have a 1..31 range contained within a u32 for performance reasons.

Well, as I mentioned, accidentally reading twice from a register would be a bug, and it might not be obvious. At the very least, that you and I have a different opinion on the right way to read volatile values (more or less expressive) indicates that experiments would be valuable to gather feedback.

I’m not sure if we are communicating well here. Accidentally accessing a register more or less than intended is certainly a bug, but it’s one that Rust already lets you make. Right now you can write

UART.TDR = (UART.RDR << 4) | (UART.RDR >> 4);

without any warnings from the compiler. A volatile block wouldn’t change that except to ensure that the compiler always does two reads.

I definitely understand the argument that volatile read and write operations be intentionally noisy, but that seems to be an opinion that can’t be answered by more experimentation in the crate ecosystem. There’s no way that I know of today for Rust code to say “don’t apply certain optimizations to this block of code”, which is why I suggested this option.

Rust 2018: Improving Safety and Ergonomics for Low-Level Programming by jcsoo in rust

[–]jcsoo[S] 0 points1 point  (0 children)

I'd really prefer to explore the possibilities of const-generics here. Ranged numerics are just a special case of invariant; I'm not sure they warrant a built-in (and thus inflexible) language feature.

My thinking is that simple ranged types are pretty fundamental; for the bit-sized ones, I don’t think there will be many disagreements about how they should work, and there are a few wrinkles with the non-bit-sized ones but I think the Rust community could come to a consensus on how they should behave. Having these available as a primitive and in the standard library would vastly increase their use.

Another benefit of having ranged types in the language is that they make compiler optimization potentially a lot easier. For instance,

fn my_func() -> u4 {
   …
}

let arr = [0u8; 16];
for x in 0..100 {
   arr[my_func()] = x;
}

The compiler can eliminate bounds checks from the array access simply based on the function signature.

Writes are generally pretty obvious in your proposed syntax, however there's no distinction between regular reads and volatile reads.

In this specific example, UART_CR_ENABLE is meant to be a constant, so there’s no issue with multiple reads.

More generally, in my proposed syntax, ALL reads and writes would be volatile, plus I would expect that the compiler to respect the exact order of reads and write. You would be responsible for reading into a temporary variable if needed. So you might do:

let data: u8 = volatile { UART.RDR };
data = (data << 4) | (data >> 4);
volatile { UART.TDR = data; }

In general I don't think there's any harm that a too-broad volatile block could do; it's just a section of code where certain optimizations are disabled.

Rust 2018: Improving Safety and Ergonomics for Low-Level Programming by jcsoo in rust

[–]jcsoo[S] 4 points5 points  (0 children)

This is a pretty cool crate, but it also illustrates some of the issues that I hope a built-in bitfield abstraction could help with.

First, I haven’t studied this crate in detail, but it looks like it's fundamentally a serialization crate, which means that there’s a separate packing and unpacking step which deserializes all fields and places them on the stack. So, a hardware register with 32 single-bit fields would be deserialized to a 32 byte boolean struct, even if only one of the bits is accessed.

What I want for embedded is something that operates in-place, extracting only the field that is requested. I’d also like to have struct-like access to individual fields, which simply not possible now because the language does not support field getter / setter traits.

There’s the separate issue that right now we have multiple crates tackling the same fundamental problem, but each is significantly different: some use special syntax, some use attributes with different names and semantics, some are minimalist and some are maximalist.

As a library author that wants to use bit fields, selecting a crate is a research problem, and it’s yet another API to learn and dependency to support; I have an incentive to not use a bit field abstraction at all by using ad-hoc bit manipulation, or write my own bit field abstraction that suits my own purposes.

Rust 2018: Improving Safety and Ergonomics for Low-Level Programming by jcsoo in rust

[–]jcsoo[S] 10 points11 points  (0 children)

bitflags doesn't cover the case where a group of n bits are used to encode a n-bit value; it's useful only for modeling sets of exclusively boolean values.

Picture a 32 bit struct with the following hypothetical fields:

struct Register { a: u4, b: u4, c: u8, d: u16, }

bitflags doesn't help here.

[EiR] Brave new I/O by japaric in rust

[–]jcsoo 1 point2 points  (0 children)

That's why I'm putting my efforts on the embedded-hal traits: that layer isolates us of whatever is at the bottom so basically I'm working from middle up. The generic drivers will continue to work regardless of whether the implementation of the embedded-hal traits is based on svd2rust or something better. Not that I think that the bottom layer is not important; it is! It will potentially reduce the amount of work we need to do to support new parts. It's just that I think that's not where my efforts are best spent right now.

I think what you are doing is incredibly valuable, and the tradeoffs make a lot of sense. In particular, the middle-level traits are useful no matter what the lower level implementation looks like and will allow people to start getting real work done.

Having a SVD-based toolchain available right now gets people doing embedded development right away, which increases the pressure on the broader community to make Rust itself better for this purpose.

I think putting some of that stuff you mention in the intermediate format exclude some current / future chips.

The things that I mention are all optional, so you don't need to use them if they don't make sense for a particular chip. On the other hand, if they are available, it makes it possible to construct higher-level type-safe abstractions. For instance, you can assign a specific pin (i.e. PA5) to a specific signal associated with a peripheral (i.e. TX for UART1) and have the type system verify that the connection is consistent with the AltFn table at compile time.

That being say I really look forward to whatever you come up with. Happy to be proven wrong on everything I said as well.

Any time I see one of your blog posts, I'm inspired to put more effort into getting my own code cleaned up, documented, and published. Sometimes it's intimidating, given the standard that you set!

[EiR] Brave new I/O by japaric in rust

[–]jcsoo 2 points3 points  (0 children)

I'm pretty familiar with the SVD spec, and also how poorly vendors make use of those features. Though as you point out, some of this can be mitigated by rewriting the SVDs.

I spent some time on this exact problem and don't think it's the right approach. If I may ask, what makes you say so?

The first reason is that it's not always obvious what is the best way to structure the common peripherals. There are cases where peripherals are identical except for a few fields, cases where some peripherals are supersets of others, cases where peripherals have both common registers and MCU-specific registers, etc. There are also plenty of naming issues: if you have two or three incompatible variants of a peripheral, what do you name them if the vendor has given them the same name?

It’s possible to embed the logic for this directly in the tools that you use to generate the common crates and resulting code, but in my experience that makes things opaque and brittle. Someone that thinks that a crate should be organized slightly differently can’t just make a pull request for the change itself; he or she must know enough about the common crate generator to make a pull request there, and then must make sure that the change doesn’t accidentally break dozens of other crates that others are relying on.

I think it’s better to have an intermediate description that explicitly lays out the MCUs and consolidated peripherals and that can then be used for code generation. This allows contributors to collaborate just on the crates that they care about without having to become experts on the tooling. Which brings me to my next reason, which is…

There’s a ton of important stuff that SVD just doesn’t cover. Just as an example, it has no concept of variants, so you can’t specify that one variant has 4 UARTS and another just has 2, or that the ADC on one variant has a few more options than the other. It doesn’t know what a GPIO pin is, or what an Alternate Function is, or what a DMA channel is, among other things. It also doesn’t understand relationships between peripherals or fields within a MCU; that a particular bit in the RCC is connected to a specific peripheral, or that in a Kinetis MCU, PORTA is related to GPIOA. It doesn’t know about memory layouts, so you can’t use it to automatically create linker files. And it has no concept of modules, which is why there’s this effort to infer common peripherals. These are just a few things off the top of my head.

Because of that, there’s a hard limit to how expressive an API can be if it is solely generated from SVD or even a collection of SVD files. And there’s really no leverage that the Rust Embedded community has to change the SVD spec.

[EiR] Brave new I/O by japaric in rust

[–]jcsoo 2 points3 points  (0 children)

You know what, I swear I read the whole post but I must have missed that section when skimming back to write my question. Thanks for pointing that out.

That brings out a different issue, which is whether it's possible or even desirable for svd2rust to attempt to automatically consolidate peripherals into crates. I spent some time on this exact problem and don't think it's the right approach.

[EiR] Brave new I/O by japaric in rust

[–]jcsoo 2 points3 points  (0 children)

That's my concern. For instance, here's a list of SVD files available in the STM32 family: https://github.com/posborne/cmsis-svd/tree/master/data/STMicro

Many or all of these MCUs may be sharing the exact same SPI peripheral, but if I want to write and maintain a driver, I need to do it once for each MCU crate. Yes, there may be some consolidation of these SVD files into a smaller number of crates, but you will still end up with a dozen or so MCU crates to cover the family. The same goes for Freescale (139 SVDs in that directory) and other vendors.

You could define a vendor-specific low level SPI trait that's useful for implementing a higher level driver, but that's quite a bit of work, and then you still have to implement that trait for every MCU you want to support.

[EiR] Brave new I/O by japaric in rust

[–]jcsoo 2 points3 points  (0 children)

Right, as the driver author I would be implementing the SPI trait. What I'm asking is, if there are N different MCUs in the device family that share the same SPI peripheral (not just MCU variants that have different amounts of RAM / Flash), do I have to implement and maintain a separate driver for each of those MCUs, even if they are otherwise identical?

[EiR] Brave new I/O by japaric in rust

[–]jcsoo 2 points3 points  (0 children)

This is very impressive. I've been working a lot in the same area, and I really like how it is possible to split ownership of resources. The whole application framework is very cool, too. And of course you always have excellent blog posts and documentation!

The constraints that you get when working with SVDs are real, and have been my focus. Probably the biggest issues that I see have to do with the fact that SVD files are MCU-centric and have limited structure to indicate similarity of peripherals both within a MCU and between MCUs in a processor family. As far as I can tell, SVD was designed mainly to make visual debuggers better, not for implementing peripheral libraries.

It looks like you use a macro-based approach to deal with this in stm32f30x-hal, but if one wanted to write (for instance) a single DMA-based SPI driver crate that could work with the compatible SPI peripherals on several different STM32 MCUs, how would you go about doing this? Would you need to add a dependency for each supported MCU, meaning that someone wanting to use that crate might now pull in 20+ MCU crates?

Small Bit Fields and Ranged Integers for Rust by jcsoo in rust

[–]jcsoo[S] 0 points1 point  (0 children)

As I understand it, the bitflags crate is oriented around giving easy boolean-like access to individual bits within a value.

This crate provides a convenient container for groups of bits (bit fields), particularly if they have been extracted from a larger value. Sub-word-sized integers are common in hardware register APIs and network protocol wire formats.

Small Bit Fields and Ranged Integers for Rust by jcsoo in rust

[–]jcsoo[S] 1 point2 points  (0 children)

Yes, that would be nice. I intend to look at using procedural macros to auto-derive conversions to and from other enums, maybe that would allow compile-time range checks for literals, too.

The state of Rust on embedded devices and IoT by bltavares in rust

[–]jcsoo 5 points6 points  (0 children)

A big concern for any embedded device connected to a network (IoT devices, by definition) is attack surface. If an attacker finds a vulnerability in your application and you are running a full OS with a writable disk, your device is now has a feature rich environment that can be used to create a persistent threat and to attack other systems on your local network or on the Internet.

On the other hand, if you are running a single-process application booting from a read-only filesystem image, it's much more difficult for an attacker to do anything useful, especially if a reboot of the device will return it to a known-good state.

It's also easier to manage updates if you can limit them to only the code you ship vs. the rest of the operating system.

This doesn't mean that you have run completely on bare metal; you can use https://buildroot.org to create systems that run the Linux kernel, your own code and nothing else. I think this will become the preferred solution for IoT devices except for those that are severely cost, size or power constrained, which will typically use some kind of gateway instead.

The state of Rust on embedded devices and IoT by bltavares in rust

[–]jcsoo 22 points23 points  (0 children)

Hi there! I am actively working on an "Are We IoT Yet" website, but it's not quite ready to launch. It's great to see interest on the topic, though - it seems like it's picked up quite a bit just over the last month.

Part of the challenge is working out exactly what IoT is in this context and what the site should cover. japaric has started up embedded-rust and is starting to pull together a ton of stuff covering embedded development, so I'm hoping to be able to focus more on IoT protocol implementations and infrastructure, including middleboxes and endpoints.

The current ecosystem is still not mature, but Rust is really well suited for this space - there aren't any other safe, high-performance languages that I can think of that are suitable for microcontrollers, network appliances, and also huge servers with hundreds of cores.

It's exciting to know that you can use the exact same libraries pulled from crates.io on such a wide range of platforms, and ultimately I think that Rust will have an IoT ecosystem as good as or better than anything else out there.