-
Notifications
You must be signed in to change notification settings - Fork 250
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
USB Improvements #344
USB Improvements #344
Conversation
Sorry for the huge patch! USB serial was consistently hanging for me, and one thing led to another while digging into the issue. Since we're not running on an RTOS, TinyUSB seems a bit finicky about how we service it. But this setup seems fairly stable for me. Only tested against a Linux host, not macOS. |
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.
Caveat: I have no privileges in this repo, just an interested bystander :)
This is great. I really appreciate that you refactored the command code and made the serial code actually do something useful.
Without trying it, I'm not entirely convinced the 256 byte buffer is going to solve all our problems (see comment), but this may be because I don't properly understand the constraints (I have basically no experience with this sort of the low level stuff). Any explanations gratefully received.
if (s_write_buf_len > 0) { | ||
int32_t to_write = s_write_buf_len; | ||
size_t written = 0; | ||
// Write in chunks of 32 bytes at most |
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 is the sort of comment that I like having a 'because' on.
It seems to me you're trying to fight against the code in tud_cdc_write, which automatically flushes at > 64 bytes (i.e. theoretically you don't need the intermediate tud_cdc_write_flushes here if this was reasonable).
Should we just be patching tinyusb?
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.
You caught me using weasel words in the comments. :) To be honest, I bashed out the USB tweaks months ago while trying to get prints working, and didn't leave any comments for my future self. It's entirely possible that this change isn't needed, but it's been stable for me, so I figured I should just get what I have into a PR for now. From what I recall, it was quite sensitive to the TC0 timer period + how much data we try to place in the FIFO, and required a lot of guess-and-checking of various parameters.
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.
Ok, I tried hooking _write()
directly up to tud_cdc_write()
, and it more or less seems to work. Just need to make sure CFG_TUD_CDC_TX_BUFSIZE
is large enough, and need to call tud_cdc_write_flush()
, or the data won't actually be sent until several characters are written. I'll play around with it and push an updated patch.
* Basic write buffer for USB CDC serial. | ||
*/ | ||
|
||
#define CDC_WRITE_BUF_SZ (256) |
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.
So, the implication of this is (afaik) that we can't read or write more than 256 bytes per loop, right? This seems a little low to me (e.g. it might end up interfering with debugging statements, and might make filesystem_cat malfunction?)
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.
Yep, anything longer than this will get truncated until the next loop. _write()
will properly return 0 bytes written, so the caller does have a chance to handle this scenario.
#include "tusb.h" | ||
|
||
/* | ||
* Basic write buffer for USB CDC serial. |
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.
What would happen if we didn't have this buffer and just relied on TinyUSB's buffer like before? i.e. just did tud_cdc_write immediately in _write but increased the TinyUSB buffer size.
If the reason is that there's some kind of relationship because tud_task and cdc_task, then it might be a problem that their calling locations have now been separated.
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.
That was my initial approach, but I just wasn't able to get it working reliably. I suspect it should be possible, but tud_cdc_write()
does have the side effect of potentially dropping into USB transfer code when it flushes, which I suspect was triggering some programmatic errors.
As for tud_task
, and cdc_task
, they're intentionally separated, as it turns out the tud user-facing API should not be called from an interrupt context. I originally had it all serviced in the TC0 ISR, and it started behaving much better once I broke it out.
watch-library/hardware/main.c
Outdated
@@ -78,8 +78,11 @@ int main(void) { | |||
|
|||
while (1) { | |||
bool usb_enabled = hri_usbdevice_get_CTRLA_ENABLE_bit(USB); | |||
bool can_sleep = app_loop(); | |||
if (usb_enabled) { | |||
watch_handle_usb(); |
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.
I'm assuming this is added because we can easily loop faster than TC0 fires.
This does sort of make me wonder if we need to use TC0 for the usb handling at all, though I guess it defends us from dodgy watch face code...
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.
Oops, just realized I'm calling tud_task()
both here and in the TC0 handler. That's definitely incorrect, standby...
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.
Ok, yeah, only calling tud_task()
from main loop immediately breaks things, and only calling it from the TC0 ISR works fine. So I think this is just a leftover hunk from testing that I forgot to clean up.
As a data point, when I did the 'what happens if we have no buffers' experiment here it mostly seemed to work: #212 The issue was I made it a blocking write (obviously wrong), and I believe this failed if there was something that needed to be done first (e.g. if you pasted a lot into the buffer, it wouldn't let you write anything because something was still sitting in read, so it would block forever). This makes me wonder if it would be possible to do something like a blocking write with a 'now handle any reads' inside the write. i.e. basically make sure any read-type usb operation would work. I know this seems a bit wrong from the perspective of actually running the watch code properly, but USB mode is quite different... (and doesn't have the display attached). EDIT: actually, getting my head back into this, https://github.com/micropython/micropython/blob/d68e3b03b1053a6de0c7eb28f5989132c138364b/ports/samd/mphalport.c does not have a separate write buffer and instead uses a timeout. |
Went back in and tested various ways of calling tud read directly inside Edit: IIRC, I messed around with with adding critical sections to prevent TC0/USB interrupt servicing, as well. And then also calling some of the internal tud servicing functions in a loop to keep kicking it. I don't think that went anywhere.
|
27afef5
to
c0784bd
Compare
Changes:
|
With the new
Results: https://gist.github.com/Hylian/71d6e1c30b70c1a6d23671729d0b2865 Analysis: With direct tud writes, characters will start dropping after a certain amount of writes. Increasing With the buffered approach (+ chunked 32 byte writes), writes are extremely consistent, with no dropped characters. However, the number of bytes that you can write in one app loop is clamped by Given this, I think keeping the write buffer is a reasonable option, as it has consistent behavior independent of the user app loop. The tradeoff being the limit on maximum characters per loop. I've adjusted the PR to bump EDIT: Alternative approaches would be to rate limit writes, come up with a smarter method of servicing tusb, or figure out if there's a tusb backpressure mechanism we can use (fifo size?). But I don't have a ton of time to dig into those right now, so I'd like to get this merged first. |
The way I read the results is that the double-buffered approach drops more characters for the same total buffer size in all situations ;) True, which characters it drops within a loop are more predictable, but you could get that effect without the double buffering by introducing a counter (i.e. track how many chars you've written in that loop, stop writing at a certain size, then reset this counter at the next loop). I'm not seeing that the buffering adds anything. My instinct would be to prefer the micropython approach (i.e. write directly, but add a bit of a wait so that we can handle it when the only issue is that the code is pushing faster than the baud as opposed to something else blocking our writes). |
(very much appreciate you did the tests, though, despite my whinging!) |
Haha, fair enough, I'll stop being lazy and see if I can get some sort of backpressure working. |
Update: I did get writes working fairly well using an exponential backoff delay in |
Are you blocking indefinitely on the write now? I would backoff for a bit then give up, similar to the micropython approach, which should prevent any freeze-ups. Of course, if you're seeing some other kind of crash... (my assumption is that not ever servicing the reads - or some other usb interaction - can lead to the inability to write at all after the buffer is filled... i.e. if we wanted a truly blocking write we'd have to service any usb 'thing' inside the blocking write) |
The issue isn't the write per-se, but the moment a read comes in. I'm not really confident about figuring it out without attaching a debugger at this point, since I can't exactly printf debug over USB. :) Looking at micropython's implementation for nrf, they actually do something similar: https://github.com/micropython/micropython/blob/9feb0689eeaca5ce88aedcc680f997a3b4d0221c/ports/nrf/drivers/usb/usb_cdc.c#L124 They serialize reads + writes in |
Ok, I think I've come up with a decent solution to handle reads and writes outside the app loop. I set up TC1 and ran |
Nice! (Re micropython, you linked to the NRF port. If you go to the SAMD one I linked earlier, it appears they don't have an intermediate buffer: |
* Introduce shell module for basic serial shell with argument parsing * Introduce shell_cmd_list module for basic compile-time command registration * Harden USB handling to hang less and drop fewer inputs - Service tud_task() with periodic TC0 timer interrupt - Service cdc_task() with periodic TC1 timer interrupt - Handle shell servicing in main app loop - Add a circular buffering layer for reads/writes * Change newline prints to also send carriage return * Refactor filesystem commands for shell subsystem * Introduce new shell commands: - 'help' command - 'flash' command to reset into bootloader - 'stress' command to stress CDC writes Testing: * Shell validated on Sensor Watch Blue w/ Linux host * Shell validated in emscripten emulator * Tuned by spamming inputs during `stress` cmd until stack didn't crash
c0784bd
to
5b762d0
Compare
Ready for review! After inspecting the micropython code, I decided to make both reads and writes use a circular buffer. I then serviced the buffer in the TC1 timer ISR, at a lower priority than TC0. Read/write flushing is now independent from the app loop. Reads are also handled in the middle of large writes, to not crash the stack. Tuned by calling |
I don't have a lot of cycles to review this, but given that USB is AFAICT totally broken right now, and this code doesn't really touch anything outside of USB-land, I'm fairly inclined to lean towards just merging this. If anyone has concerns about that, speak up now! (cc @joeycastillo?) @Hylian I am curious about the |
@WesleyAC Thanks for taking a look!
|
Ping :) Is this good to be merged? If there are any specific tests you'd like to see, I can try and run them. |
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.
Looks great to me! Is it OK if I merge this on my branch?
Linux kernel's terminal subsystem has options for automatically translating between For more details: |
@matheusmoreira Go for it! The more test time we can get on the patch, the better. :) In particular, I haven't really used it on my wrist much. None of the code should really run when not plugged into USB, but always good to check. |
@Hylian Great, I'll go ahead and merge this into my branch then. I plan to merge as many pull requests as possible before flashing them into my watch for daily use. This also needs
|
- Change newline prints to also send carriage return - Introduce shell module for serial shell with argument parsing - Introduce shell command list for compile time command registration - Refactor file system commands for shell subsystem - Introduce new shell commands: - 'help' command - 'flash' command to reset into bootloader - 'stress' tests CDC serial writes of various lengths - optional delay parameter - Harden USB handling - Hangs less - Drops fewer inputs - Circular buffers for both reads and writes Reported-by: Edward Shin <[email protected]> Tested-by: Edward Shin <[email protected]> Reviewed-by: James Haggerty <[email protected]> Reviewed-by: Wesley Aptekar-Cassels <[email protected]> Reviewed-by: Matheus Afonso Martins Moreira <[email protected]> Signed-off-by: Matheus Afonso Martins Moreira <[email protected]> GitHub-Pull-Request: joeycastillo#344
- Change newline prints to also send carriage return - Introduce shell module for serial shell with argument parsing - Introduce shell command list for compile time command registration - Refactor file system commands for shell subsystem - Introduce new shell commands: - 'help' command - 'flash' command to reset into bootloader - 'stress' tests CDC serial writes of various lengths - optional delay parameter - Harden USB handling - Hangs less - Drops fewer inputs - Circular buffers for both reads and writes Reported-by: Edward Shin <[email protected]> Tested-by: Edward Shin <[email protected]> Reviewed-by: James Haggerty <[email protected]> Reviewed-by: Wesley Aptekar-Cassels <[email protected]> Reviewed-by: Matheus Afonso Martins Moreira <[email protected]> Signed-off-by: Matheus Afonso Martins Moreira <[email protected]> GitHub-Pull-Request: joeycastillo#344
- Change newline prints to also send carriage return - Introduce shell module for serial shell with argument parsing - Introduce shell command list for compile time command registration - Refactor file system commands for shell subsystem - Introduce new shell commands: - 'help' command - 'flash' command to reset into bootloader - 'stress' tests CDC serial writes of various lengths - optional delay parameter - Harden USB handling - Hangs less - Drops fewer inputs - Circular buffers for both reads and writes Reported-by: Edward Shin <[email protected]> Tested-by: Edward Shin <[email protected]> Reviewed-by: James Haggerty <[email protected]> Reviewed-by: Wesley Aptekar-Cassels <[email protected]> Reviewed-by: Matheus Afonso Martins Moreira <[email protected]> Signed-off-by: Matheus Afonso Martins Moreira <[email protected]> GitHub-Pull-Request: joeycastillo#344
- Change newline prints to also send carriage return - Introduce shell module for serial shell with argument parsing - Introduce shell command list for compile time command registration - Refactor file system commands for shell subsystem - Introduce new shell commands: - 'help' command - 'flash' command to reset into bootloader - 'stress' tests CDC serial writes of various lengths - optional delay parameter - Harden USB handling - Hangs less - Drops fewer inputs - Circular buffers for both reads and writes Reported-by: Edward Shin <[email protected]> Tested-by: Edward Shin <[email protected]> Reviewed-by: James Haggerty <[email protected]> Reviewed-by: Wesley Aptekar-Cassels <[email protected]> Reviewed-by: Matheus Afonso Martins Moreira <[email protected]> Signed-off-by: Matheus Afonso Martins Moreira <[email protected]> GitHub-Pull-Request: joeycastillo#344
Am now running this code on the watch. No issues so far. |
Testing: