rust-iot / radio-hal

Embedded rust radio abstraction crate
https://ryan.kurte.nz/notes/2020-01-05-rust-radio
MIT License
69 stars 13 forks source link

IEEE 802.15.4 implementation #11

Open Alexx-G opened 3 years ago

Alexx-G commented 3 years ago

I'm looking into somewhat similar as discussed in #6 (a prototype that relies on OpenThread for communication, based on NRF52*). Can https://github.com/braun-embedded/rust-ieee802.15.4 be used with radio-hal or at least some effort coordinated?

ryankurte commented 3 years ago

Hey, interesting question! This is one of my goals for the radio drivers, however, (afaik) the iee802.15.4 crate doesn't (yet?) implement any MAC (or higher) layers. I have started prototyping what this might look like in rust-lpwan, using the iee802.15.4 crate for types, with the goal of growing this to provide a full LPWAN stack for a variety of interesting protocols.

At the moment it's mostly an experiment in how to effectively abstract different LPWAN components in rust, but, there's a working / extremely minimal radio-hal compatible CSMA MAC implementation that needs a bit more work to be compatible with 802.15.4 (then there are a bunch of fun other layers / MAC implementations ^_^), I just have enough working for now that finishing (and interop testing) this isn't super high on my TODO list...

Feel free to open an issue over there if you'd be interested in working on this! (or, sponsoring third-party work on it I guess). I've been putting off a bit of a cleanup of the repo and work breakdown because so far it's only me, and would be happy to coach / guide / support improvements.

henrikssn commented 3 years ago

I have also a very crude ieee802.15.4 mac in progress, so far it only supports sending/handling associate, ACK and data commands. The problem I am running into is that doing this without any sort of framework means a lot of complex state machines to keep track of, and the MAC constantly needs to babysit the PHY layer (check_receive/check_transmit are in my case timing sensitive since they manage filling/draining of the radio FIFO "on the fly"). I am thinking of moving to a message passing model and letting the PHY layer (and also the MAC layer) be interrupt driven, but that isn't very compatible with the current radio-hal (and I have not yet had time to design an interrupt driven alternative).

The alternative API would be something on the order of transmit(buf: &[u8], done_cb: Callback) with an accompanying poll() method which is called on incoming interrupts.

I hope to OS this when I start to converge on something (well, as soon as my employer allows me to).

ryankurte commented 3 years ago

The problem I am running into is that doing this without any sort of framework means a lot of complex state machines to keep track of, and the MAC constantly needs to babysit the PHY layer

unfortunately in my experience this is always the case with the PHY/MAC (and yeah ideally they need to be interrupt driven but, i haven't found a useful way to abstract this in the PHY yet).

I am thinking of moving to a message passing model and letting the PHY layer (and also the MAC layer) be interrupt driven

this is similar to my intent with the lpwan mac, the idea was to call update both regularly via timer (required for TiSCH things) and radio (for state / fifo responsiveness as you've discovered) IRQs, which manages all the timing-dependent underlying state, then to just pass messages to/from the MAC+PHY for higher level stuff. it's not so easy to pass futures and buffers with lifetimes to an interrupt context, which is why i went in the .update() direction for the WIP MAC. if the MAC holds the RX and TX buffers the problem is vastly simplified as opposed to passing around buffer-pinned-futures between main and interrupt contexts.

The alternative API would be something on the order of transmit(buf: &[u8], done_cb: Callback) with an accompanying poll() method which is called on incoming interrupts.

the trick here is that you need to associate the lifetime of the buffer with the lifetime of the TX/RX/whatever operation. this is basically how the non-blocking impl works, which does Pin the buffer against the future but does not currently support long packets because i hadn't worked out a good abstraction (and the associated types feature required for type bounds on trait returns was(?) nightly-only until recently.

probably this is as simple as removing the automatic implementation to make it opt-in in the same manner as the embedded-hal traits, then it would be reasonably straightforward to directly implement the AsyncReceive and AsyncTransmit traits for a given device to achieve the FIFO buffering that you need. you can totally use the async traits in a non-async context just by calling .poll as appropriate, and it's easy to manage this state under the MAC. technically the waker stuff should be able to be integrated nicely with interrupts but, practically we're a ways from that being usable and, it can be a bit wonky.

do you think an approach like that solve the problems you're experiencing?

henrikssn commented 3 years ago

So far I never managed to keep the rx/tx buffers in the MAC, I have a single 256b buffer in the PHY because start_transmit only gives me access to the send buffer once (and I need somewhere to write data during check_receive). Given that the radio can anyway only do one operation at a time, it isn't such a big deal.

Still, it would be nicer if the MAC just gave "send this data" / "call me on received data" instructions to the PHY and the latter handled the state transitions. Some PHY (e.g. the semtech chips) have support for "auto modes" which allow to do some of the state transitions directly on the hardware.

Obvoiusly this means I have a fair amount of memcpy in my code, if that becomes a problem then I would probably prefer to pass the Frame object directly to the PHY and let the latter do the serialization into the tx buffer (abstracted by e.g. an Encode trait).

I will have to take a closer look at the non-blocking traits, it is indeed elegant to let the caller allocate all the radio buffers. The drawback is that you might end up initializing new buffers for each sent packet (since it is not clear to me how you could transfer ownership of the buffer back to the caller when e.g. the TransmitFuture has completed).

ryankurte commented 3 years ago

The drawback is that you might end up initializing new buffers for each sent packet (since it is not clear to me how you could transfer ownership of the buffer back to the caller when e.g. the TransmitFuture has completed).

this all works really nicely with async Pin and associated lifetimes if you're running in an async context, i modified the nonblocking impl to take arbitrary buffers as an example, but, it's sorta increasingly complex.

So far I never managed to keep the rx/tx buffers in the MAC, I have a single 256b buffer in the PHY because start_transmit only gives me access to the send buffer once (and I need somewhere to write data during check_receive). Given that the radio can anyway only do one operation at a time, it isn't such a big deal.

on thinking more this seems like a really nice solution! the radio constructor could take a buffer B: AsRef<[u8]> + AsMut<[u8]> (so it can be a slice or a vec or whatever, and a length appropriate to the user), then use this during tx / rx operations and just copy in/out as you're doing. it needs to be implemented for each radio but, the FIFO streaming behavior is pretty device specific anyway, and it wouldn't need any callbacks or complex lifetime tricks. a couple of copies sound definitely worth it to me.

Alexx-G commented 3 years ago

@ryankurte @henrikssn thanks! All this looks quite interesting. I have quite little Rust (especially the embedded flavor) knowledge to help with some low-level tasks, however I'd gladly help with some entry-level issues, in case you can split the problem into smaller pieces and label them. I also have an NRF52-DK and a few dongles on their way, so I can help testing some stuff as well. So far I'm determined using Rust for the device I build, but any kind of communication (wireless or even USB) is just impossible. Thus if some Rust projects can benefit from my hobby, it'd be really nice.

henrikssn commented 3 years ago

This is what I have at the moment:

The network looks like this:

So far I am very happy with this setup, I have a temp sensor node powered by a coin cell which should (from power measurements) deliver sensor data to my home automation for multiple years without battery change. Next level would be to implement beaconing so that it is possible to do bidirectional communication with sleeping nodes (right now you need to wait until the node decides to wake up).

If anyone is interested in cooperating on this I can try to cleanup and publish the code.