embedded-hal icon indicating copy to clipboard operation
embedded-hal copied to clipboard

async: add Serial

Open Dirbaio opened this issue 2 years ago • 3 comments

Write mirrors the blocking trait. Read is new.

Unresolved issue: how does Read work. See previous thread https://github.com/rust-embedded/embedded-hal/pull/334#discussion_r773423253

Dirbaio avatar Jan 12 '22 12:01 Dirbaio

r? @ryankurte

(rust-highfive has picked a reviewer for you, use r? to override)

rust-highfive avatar Jan 12 '22 12:01 rust-highfive

okay, so:

There's 2 ways of writing a UART HAL drivers:

  • Buffered: The driver has a reasonably-sized buffer in RAM. Hardware places Incoming bytes are placed into the buffer automatically without user code having to do anything (via DMA, or an ISR). User code pops bytes out of the buffer. Bytes are only lost if received while buffer is full.
  • Unbuffered: The driver has no buffer (or a pathetically small buffer: stm32 and nrf have a 4 byte buffer in hardware). User code does "read" operations that pop bytes directly from the hardware into the user's buffer. Bytes received while the user code is not actively waiting on a read are lost.

For example:

In both buffered and unbuffered, if you're using RTS/CTS data never gets lost. If you're not using RTS/CTS, it's super easy to lose data with unbuffered UARTs. People do use UART without RTS/CTS in practice, because out of pins in the MCU, or in a cable, or they simply forgot, or their protocol doesn't allow it (RS-485). IMO the traits have to be usable without RTS/CTS.

So, the question is: are the traits for buffered or unbuffered, or both?

For buffered, an "io::Read-like" trait that allows short reads works fine: fn read(&mut self, read: &mut [Word]) -> Result<usize, Error>. The impl returns when there's at least 1 byte received. If user code gets fewer bytes than it wants, it calls read() again. No bytes are lost thanks to the buffer.

For unbuffered, such "io::Read-like" trait does not work in practice. Some data arrives, the impl returns early, user code calls read() to get more bytes, but if it takes too long between read() calls, bytes will already be lost. Drivers need much more precise control in this case. either "read exactly N bytes, never ever early return", or read until line idle or buffer full if they don't know the size of incoming packets.

I don't think "allowing but not require early return" is a good solution. It'd make writing drivers harder, behavior should be consistent between all impls.

So I can think of these solutions:

  • Mandate buffered impls, make the traits io-like (must do early return).
  • Allow unbuffered impls, make the traits lower level (read exactly N bytes, read until idle. ..?)
  • Do both.

In my experience, buffered UART has been MUCH nicer to work with. For "AT-command-like" stuff, unbuffered is incredibly fiddly to get right. You often have to do "read until newline", where you don't know how many bytes it'll be. If you use RTS/CTS you can get away with reading bytes one by one, but that's slow: there's big pauses between bytes, since RTS gets toggled for each one. If you don't have RTS/CTS, there's no way to do it reliably at all. Unbuffered read is only usable for "packet-like" comms where you either know the length upfront or use "read until idle", and ideally only with CRCs/acks/retries.

The downside for buffered is it requires more RAM (for the buffer), and it's much harder to impl for HALs (only sane way is to use DMA, really).

So... I don't know what's best! :sweat_smile:

Speaking of "io-like" traits, if we pick that option maybe we should have a set of "general" IO traits instead, and have Serial use the io traits, instead of having Serial-specific traits. This'd allow running the same code (an interactive shell maybe?) over serial and, say, a TCP connection, for example! I've been working on a set of IO traits here (because I need them for Embassy) . I think this might be starting to get out of scope for embedded-hal though.

Also, this applies to both blocking and async UART read. IMO we should decide on some solution, then add it in both blocking and async forms. Not having blocking UART read is not very nice.

cc @lulf

Dirbaio avatar May 04 '22 00:05 Dirbaio

good summary!

In my experience, buffered UART has been MUCH nicer to work with.

agreed, and i think this is the right level of abstraction for the majority of drivers (as well as being implied by the existing Write::flush method).

Unbuffered read is only usable for "packet-like" comms where you either know the length upfront or use "read until idle", and ideally only with CRCs/acks/retries.

i think if you need this you're going to have to implement it for your hardware anyway, so while we could provide some kind of a ReadByte trait it'd be okay to elide this (especially for now).

ryankurte avatar May 04 '22 01:05 ryankurte

Hi, @Dirbaio What would it take to settle to a conclusion for this PR? It would be great to be able to write async drivers with serial interface.

My personal opinion would be to allow as much use case as possible and let the user choose what is best for him.

I guess we need:

  • an io::Read-like which can return early and return the number of read bytes,
  • a read exact kind of trait for unbuffered which does not return until the provided buffer is filled,
  • a read_until_idle trait for hardware capable of detecting it which returns the number of read bytes

I would be happy to help if needed.

guillaume-michel avatar Dec 14 '22 17:12 guillaume-michel

I also think we need the 3, yes. Note that io-like traits already exist at embedded-io.

@ryankurte @eldruin My proposal to unblock this:

  • Keep read to be "read until buffer full", with no early return.
  • Add read_until_idle.
  • Add docs explaining "buffered" vs "unbuffered" uarts and their tradeoffs. Explain the traits in embedded_hal::uart are for unbuffered UART, point people to embedded-io for traits for buffered uart.

Dirbaio avatar Dec 14 '22 22:12 Dirbaio

That sounds fine to me. I have not been deeply involved in the discussion, though. You would need to document what "idle" means as well. i.e. does silence for X time also count as idle?

eldruin avatar Jan 16 '23 09:01 eldruin

Hi, As far as I know idle means silence for 1 period. This is actually how the nrf embassy implementation detects idle.

guillaume-michel avatar Jan 16 '23 12:01 guillaume-michel

Okay, following the previous discussions:

  • I've split async write into #442 since that should be uncontroversial.
  • Renamed Read into ReadExact. Matches the std::io name and makes it more obvious there's no "early return".
  • Added ReadUntilIdle
  • Added docs explaining the difference between buffered and unbuffered reads, and clarify the traits here are for unbuffered serial port interfaces, and their caveats.

Should be ready for review.

Dirbaio avatar Mar 08 '23 00:03 Dirbaio

Discussed in today's WG meeting https://matrix.to/#/!BHcierreUuwCMxVqOf:matrix.org/$36Ghk5FUs3y2c-2eoOZKugKx4sS_xJ6VE8uS_78EyrE?via=matrix.org&via=psion.agg.io&via=tchncs.de

Discussed what exactly are the differences between buffered and unbuffered, and whether we need both. No conclusions reached.

Dirbaio avatar Mar 21 '23 20:03 Dirbaio

though again it would be great to have example impls in a couple hals / a driver

embassy-rp, embassy-stm32, embassy-nrf have implementations of these methods (not with the trait though, because it doesn't exist yet)

Dirbaio avatar Mar 28 '23 11:03 Dirbaio

Rebased to fix conflicts. Could you take a look @ryankurte @eldruin @therealprof ?

Dirbaio avatar Mar 28 '23 11:03 Dirbaio

In my opinion buffered and unbuffered describe exactly what is happening under the hood and are therefore a perfect fit. This naming scheme will probably result in the least amount of confusion.

Sympatron avatar Mar 29 '23 11:03 Sympatron

After discussions in the WG meetings, we decided it's better to not have traits for unbuffered UART.

  • The vast majority of drivers need buffered to work correctly, and the few who could use unbuffered can also use buffered (at the cost of some extra RAM).
  • There's a high complexity cost in having two traits for UART. The distinction between them is very subtle, it WILL cause confusion and mistakes.

Next steps:

  • Should the buffered traits be embedded-io, or something UART-specific inside embedded-hal?
  • If it's embedded-io, shall we move into the WG?
  • Should we remove the serial::Write trait, in favor of embedded-io's Write?
  • Should embedded-io get a nonblocking flavor, aside from the current blocking and asynch? Or is the embedded_hal_nb::serial::Read trait enough?

Dirbaio avatar Apr 06 '23 18:04 Dirbaio