-
Notifications
You must be signed in to change notification settings - Fork 769
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
stm32: Ringbuffer rewrite #3336
stm32: Ringbuffer rewrite #3336
Conversation
9b5195c
to
0e5d6c7
Compare
let's see if ringbuffered uart tests pass bender run |
Another thing is that the state machine basically stays on Underrun state until the user calls clear so clients that don't handle Underrun may get stuck. (This is explicitly defined in the stateful property testing here) This may be unwanted. We could implicitly clear instead but then we need to decide what happens with the cleared write buffer queue. Do we empty it? Do we assume it is full on clear? Half full? The current clear implementation for the write buffer assumes it is full with the previous buffer contents. |
bender run |
|
bender run |
0e5d6c7
to
b965288
Compare
.write_index | ||
.diff(self.cap(), &mut self.read_index) | ||
.try_into() | ||
.unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This causes a panic for me because the write index seems to get zeroed out before the read index, causing the isize -> usize cast to fail. But presumably that should not even happen in the first place, and I am still trying to investigate what is causing the issue for me.
Unless a panic here should signify that some invariant has been broken, it could be changed to
match self.write_index.diff(self.cap(), &mut self.read_index).try_into() {
Ok(diff) if diff <= self.cap() => Ok(diff),
_ => Err(OverrunError),
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implicit invariant is that the read index is always behind the write index. It should not happen since we only udvance the read index up to the diff between it and the write index.
Could the ringbuffer be extended with an That would make it easy to implement read_ready() for the RingbufferUart and other ringbuffer backed IO |
I think it makes sense, will do. |
I simplified the implementation by inlining some methods. |
Thanks 👍 I'll get onto adding ReadReady once this is merged. |
2232ac3
to
628dd36
Compare
What is the expected behavior of read_ready()? With |
Yea I was thinking this may be problematic, I can revert to a mutating version. The read-only version will not be updated until a new read/write so that doesn't make a lot of sense in retrospect. |
For As I interpret the docs of ReadReady that does not go both ways. Since its meant to allow for non-blocking reading it should be fine for a false negative. So read_ready returning false while an immediate call to read would return Poll::Pending is fine I think. It should not only update when read & write are called however. It takes |
In that case, I think making |
Two main issues that I believe still remain to be properly discussed: Overrun recoveryCurrently, the read ring-buffer version directly syncs to the current DMA location. I think this is pretty straightforward and covers most use cases properly. The write version on the other hand directly syncs Broken Invariants DetectionThe implementation assumes that the RingBuffers are only used while the DMA is both 1) enabled and 2) not modified other than through the Some of this can be detected through negative |
For the invariant thing, I am still most in favor of just returning an error. An Another little thing, dma_sync can be simplified by assigning directly to the fn dma_sync(&mut self, cap: usize, dma: &mut impl DmaCtrl) {
let first_pos = cap - dma.get_remaining_transfers();
self.complete_count += dma.reset_complete_count();
self.pos = cap - dma.get_remaining_transfers();
// If the latter call to get_remaining_transfers() returned a smaller value than the first, the dma
// has wrapped around between calls and we must check if the complete count also incremented.
if self.pos < first_pos {
self.complete_count += dma.reset_complete_count();
}
} As for the WritableDmaRingBuffer I have only started looking at it now |
Apart from the initial added delay, what is the drawback of just giving the most leeway when starting/clearing the WritableDmaRingBuffer? Presumably whoever decides the length of the DMA buffer takes into account the acceptable delay of the whole system, and the worst-case execution time. If a possible latency of Unless there is some clear immediate need for a custom initial offset, defaulting to the maximum is probably fine. |
My main problematic case goes something like this: This is recoverable, just requires some thought in the application logic. I agree that giving the max length is a good default hence why I made it so, just thought I should get some feedback first before committing. |
Also some bikesheding, should clear be renamed to sync or something? Clear makes |
Could both not just be called reset? The doc comment already describes them as a reset |
I think I have addressed most of my issues. |
I would very much prefer not to panic. It would really suck to have my drone fall out of the sky just because one of my RC packets (for whatever reason) caused invariant we have tried to model in the buffer, not to be upheld. Especially when one lost packet and a DMA restart later is all it takes to get back on track. It would be another thing if the entire peripheral locked up unrecoverably, but that is not the case so I prefer the error. Also can the if..if..else at (and same for the writer) be combined into an if..ifelse..else? |
I agree with the reasoning for avoiding the panic. Also simplified the if/else as requested. |
bender run |
f783a07
to
3ccceac
Compare
bender run |
@Dirbaio I don't think I have anything else to address. I am open to feedback but otherwise I think this mostly does it. @peterkrull Do you agree? |
I do not have anything else to add |
The above was wrong, the original code was fine. |
6cab295
to
28d0353
Compare
The current ring buffer implementation is reportedly a bit flaky and hard to audit.
This PR aims to rewrite the ring buffer module with a simpler implementation that is easier to audit and has clear semantics.
The new module includes stateful property tests which can encode and test the semantics of the module to ensure correctness.
I have been testing this implementation with a real-life DSP project, for both the
USARTX
code and theI2S
codec processing. I of course welcome others to test the PR to get feedback.One point worth discussing is the semantics of the
new
andclear
methods for theWritableDmaRingBuffer
.After these calls, the ring buffer needs to assume some existing length, otherwise it will
Underrun
on the nextwrite
call.Currently, these calls directly force a
length
equal to thecapacity
giving the maximum leeway for the DMA to catchup, but we may want to make these configurable. This has the disadvantage of directly using the previous contents of the underlying buffer at a non user-controllable offset. This is not a great idea due to the lack of user control, but is not fixable without changing the interface.I am also a bit skeptical about the recently introduced
write_immediate
buffer method.I think another approach is worth experimenting with, that of directly constructing/resetting an empty
WritableDmaRingBuffer
and having to hydrate with a version ofwrite
that does not produce anUnderrun
error.A possible issue with the PR is that it removes the
get_complete_count
from theDmaCtrl
trait since it uses thereset_complete_count
instead. It may be worth reverting this to squeeze some extra performance.As always, thanks for the great project and I hope this PR is useful.