Skip to content

Abstract support for peripheral drivers for communication protocols #8

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

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

Comments

@cwoodall
Copy link

The following list of protocols are often found as drivers in many embedded systems:

  • SPI
  • I2C
  • CAN
  • UART
  • Ethernet
  • The protocol the WS2812 leds use (very popular in hobby projects)

Each application often needs to use these in slightly different ways, and need support for: interrupts, asyncio of some form, and blocking operation typically. It would be great as a community, especially for the microcontroller/arduino sub-ecosystem to standardize on some sort of interface for these and standard libraries.

An easy and standard API to these peripherals will help produce. The ARM provided CMSIS layer tends to build up some of this standardization, at least trying to make what might be disparate peripherals feel very similar to the application programmer.

@japaric
Copy link
Member

japaric commented Sep 29, 2016

I think it may be possible to leverage traits for this. The trait provides an API over e.g. SPI and then one can write a generic driver for a SPI device (e.g. an accelerometer) that exposes an API that takes e.g. &mut T where T: Spi.

need support for: interrupts, asyncio of some form, and blocking operation

I think we should start experimenting with a blocking API. Async is going to require settling on some async solution first.

I think I've heard @posborne mention this idea of traits as peripheral interfaces once before. Perhaps they have experience with this approach in the RPi sub-ecosystem and can share it?

@cwoodall
Copy link
Author

@japaric I agree that we shoul

I think we should start experimenting with a blocking API. Async is going to require settling on some async solution first.

I agree with this statement, I just wanted to point out that in general I would never want a framework for embedded which didn't have a good story for async, even if that is just callback functions.

@tcr
Copy link

tcr commented Sep 29, 2016

Traits as peripheral interfaces is a great end goal. I want to advise against formalizing this too early; Firmata has many edge cases that (as a generic protocol) prevents it from compatibility with every device. It may be more practical to start with a generic crate with multiple feature-enabled backends (like Johnny-Five's IO adapters) before going straight to traits.

With Tessel (an OpenWRT system with a custom GPIO driver), we've prototyped I2C and Gpio ports, with SPI and UART in the pipeline: https://docs.rs/tessel/0.3.1/tessel/

We've prototyped an async API built on Tokio: https://github.com/Dr-Emann/tessel-rust/tree/new_api

I'd love to experiment with drivers like the MMA8452Q driver written for Tessel and for rust-embedded, probably with an adapter class inbetween.

@posborne
Copy link
Member

Here's something that I started to put together to come up with a standard trait for accessing GPIOs: posborne/rust-gpio#1

@igutekunst
Copy link

@posborne , that looks like a good start.

I'm with @tcr and @cwoodall about the risks of over constraining ourselves too early, but also the need for async.

It's hard to define a universal interface/trait that will satisfy all hardware.
For example, UARTS and SPI interfaces don't have to send 8 bits per write, and can send many more,. I think I've seen up to 32 bits in a single write. They may have different buffering features (8 deep FIFO, multiple FIFOs based on priorities), to name a few.

However, maybe there can be a base type, that only takes in u8s, and emits u8s, and doesn't expose any buffering, and then more complicated interfaces can implement additional traits?

Another part of generic peripheral drivers worth considering is coupling physical GPIO pins. To avoid getting way into the weeds right away, I'll only pose a few few questions: Are GPIO pins configured separately from the peripheral? Are they passed in when initializing a peripheral? Does a UART know how many pins it needs, and in what configuration?

@cwoodall
Copy link
Author

cwoodall commented Oct 6, 2016

@igutekunst and then to get a little more into the weeds, how are pins as a resource handled? Pins can have multiple configurations... May even have multiple configurations throughout a program. A major improvement of the current state would be if there was a concept of ownership applied to pin configurations. Not sure how you could do this, but it seems like rust might actually be able to solve this problem with its built in system of lifetimes and borrowing (or I might also be crazy)

@igutekunst
Copy link

The lifetimes and borrowing idea brings a smile to my face! I'd love to see peripherals actually owning pins in a compiler enforced manner.

Maybe it's time to actually building a few projects for a few different MCUs (let's say an AVR/Arduino and a STM32F4), and see what emerges.

A concern is how to do this in a zero cost way. I'm too new to Rust to have an intuition on whether it's possible to do so without runtime/codesize effects. I don't want to get into the trap I've seen happen with C, where you and up with a struct GPIOpin, with all kinds of members and function pointers. Ideally this abstraction would boil away after compilation leaving almost direct manipulation of registers.

A thing to consider with pin ownership is configuration management/board revisions. Let's say you move stdio to a different UART on a new board spin. Where does this configuration live?

My intuition is telling me it lives in a board definition file or crate, that exposes the right pins to the UART, which in turn exposes the UART to the application layer.

@japaric
Copy link
Member

japaric commented Oct 6, 2016

relevant: zinc has this "platform tree" feature (example), which is just a syntax extension, that assigns pins to peripherals (e.g. PA0 -> UART1) in a declarative manner and validates the assignments (no pin can be shared between two peripherals) at compile time. The syntax extension expands to rust code, so the compile time validation is actually done by rustc borrow checker.

This feature also constrains what can be accessed in the main loop -- only peripherals declared via platform tree can be used.

At least, that's my understanding of what it does. I haven't actually used myself. 😄

@igutekunst
Copy link

That's quite cool. I had only briefly looked at it, but will look at it more.

@jcsoo
Copy link

jcsoo commented Oct 6, 2016

Not sure if anyone here has seen this: Zinc mini post-mortem

Zinc was becoming more and more hard to maintain, as most of the effort was spent to catch up with nightly. The generated ioregs approach was semi-good, the platform tree approach was horribly broken.

Zinc, in the way it is now, is dead, and that's a horse I stopped beating. Zinc want to see some time in 2017 is one that uses stable rust and just makes things work. And is shipped as a bunch of simple to use crates :slight_smile:

This matches up with my experience looking at Zinc, admittedly more than a year ago. There are a lot of ambitious ideas which unfortunately can lead to a big learning curve, and it is tough having basically everything in a single crate.

f3 seems to be a better approach, at least starting out - there are a lot fewer abstractions to learn, and while I'm sure there are ways that you can shoot yourself in the foot, sometimes it's easier to debug straightforward errors than to work through a heavier API.

Also, lighter-weight abstractions can make it a lot easier to translate between the Rust code, vendor hardware documentation, and any C-based sample code that you might have. That's important right now because there's very little Rust embedded code to look at for inspiration.

Right now I'd prefer to see people experimenting with varied approaches and applying them to larger projects to see what really works. It's still a bit early to be standardizing.

@cwoodall
Copy link
Author

cwoodall commented Oct 6, 2016

@jcsoo that us a really awesome piece of info I did not see that post mortem! It all makes a lot of sense to me! Ideally we want a world where we have a bunch of interoperable crates, maybe built up from some core abstractions (like how many concurrency libraries use the future/promises crates and then build up easier to use semantics from there)

I have rethought some of my thinking I think those base projects are key but setting a goal to have an arduino like setup where you can easily do hobby projects as a first big stop would really help us establish ourselves as a community draw in a variety of makers and start getting projects posted to hackaday.com, Hacker news, etc.

@thejpster
Copy link
Contributor

Perhaps there's something to be said for the UNIX character device / ioctl approach. Implementing the Read and Write traits would work for a range of I/O devices, and you can specify peripheral specific setup in the new method.

I think it would be limiting to say this is a UART and this is an SPI device when all I actually need to know is that it's a u8 sink of some sort.

@fbstj
Copy link

fbstj commented Oct 17, 2016

@thejpster yes definitely in some (maybe most) respects the UNIX way makes sense, but there are probably a number of peripherals that would end up being served oddly by this approach. particularly I would much rather be able to send CAN frames directly rather than serialising them to bytes or something? Similarly I would rather send the struct that wraps my structured messages over serial than have to mush it into bytes manually every time? Also wrt SPI devices that /aren't/ merely byte source/sinks, it makes more sense to sit them on top of a specific SPI interface?

@thejpster
Copy link
Contributor

@fbstj I think the trick is building an ioctl interface that's type safe (and it may have to be interface specific to achieve that) but I'd argue that I/O device payloads (to be clear, I mean the data you wish to transfer to the remote system, as opposed to the metadata like the destination address or the baud rate) should fundamentally be expressed in octets. If you need to send structured data, perhaps something like serde-rs should perform the marshalling for you rather than relying on the alignment and packing of your particular platform to match your target.

I would be really interested to know more about any SPI devices that are not merely full-duplex u8 source/sinks. Do you have any examples?

@posborne
Copy link
Member

Perhaps there's something to be said for the UNIX character device / ioctl approach. Implementing the Read and Write traits would work for a range of I/O devices, and you can specify peripheral specific setup in the new method.

I think implementing the Read and Write is a worthwhile activity for devices (like SPI) where many use cases can be distilled down to that simple pattern but I think most interfaces do deserve separate, standard interfaces as well that capture the semantics of configuring and interacting with the device in a way to minimizes the possible runtime errors.

Take, for instance, the interface exposed by one library I have worked on, spidev. Although we can (and do) implement the read/write traits for SPI devices, I also implement a mechanism for doing full-duplex transfer operations as well.

Those APIs match the kernel APIs pretty closely (in terms of capabilities), but they hide the ugliness of actually dealing with ioctls (which are pretty terrible in terms of usability/correctness).

For I2C, I decided it did not really make sense to implement the Read/Write traits directly (at least for now) since I could not think of many cases where composition using these traits would make sense (but maybe I should revisit that decision).

In my dream world, I could write a driver for a device using traits and have this device be supported on multiple vastly different platforms. For instance, consider a few examples from my work on I2C:

Today, these drivers are not suitable for use across platforms, but I have tried to at least move things in that direction by making the drivers only depend on traits. In turn, some of these devices expose a trait interface.

@jamesmunns
Copy link
Member

I believe that this issue essentially describes embedded-hal, and 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
community 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

9 participants