-
-
Notifications
You must be signed in to change notification settings - Fork 8.2k
tinyusb: Support USB device drivers written in Python #9497
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
Conversation
e5acbe5
to
a2957c2
Compare
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #9497 +/- ##
=======================================
Coverage 98.39% 98.39%
=======================================
Files 161 161
Lines 21078 21078
=======================================
Hits 20739 20739
Misses 339 339 ☔ View full report in Codecov by Sentry. |
a2957c2
to
097cbad
Compare
Have updated this branch a bit, and also submitted a draft set of micropython-lib changes at micropython/micropython-lib#558 |
097cbad
to
bcf9adb
Compare
bcf9adb
to
c03147a
Compare
Code size report:
|
I've decided I like pain and so I'm writing a barebones read-only mass storage driver for my use case using this work (not pain because the work's hard to use, just because now I have to understand USB and SCSI at the same time). I'm trying to figure out if it's possible to stall an endpoint with the current implementation? I believe that's what I'm meant to do in the case that I get an invalid request, but I'm not seeing that that's functionality that's exposed. However, I also don't really understand what I'm talking about, so stalling an endpoint might actually be a synonym for something else that is exposed. |
I now understand slightly more, I think, so I can give a better overview. Mass storage mostly works via data being sent through the IN and OUT endpoints, with a couple of exceptions (literally two, and one of those is reset). The correct response to an invalid request coming in on OUT is to stall IN, but I'm not seeing how I'd be able to do this. Now, I have got what I need working on Linux without doing this, but I don't know how robust it is. |
@turmoni Great plan! I think you are correct, there are 3 TinyUSB usbd_priv.h interface function calls: I haven't looked closely at MSC yet. My vague plan was to work up in complexity from HID (mostly done), to MIDI (unexpectedly complex), to CDC, and then MSC (complex because SCSI). However, from a very quick look at the TinyUSB class driver it seems like stalling the endpoint is needed for two cases:
So if your particular implementation is happy to ignore (1) or handle it in a more extreme way, and if it doesn't need (2) because the read-only backing storage is always available, you might be alright without endpoints stalling. If you do want to add those APIs to the Python interface and feel like sending a PR to add them to this branch, then that would also be great - they will certainly be needed before this work can be completed. |
I think I should be OK without having to implement them for my needs. I've got my device working as much as I need it to on Linux, just Windows is causing me issues. Specifically, I keep getting Before realising I had some UARTs I could hook up for better logging I did get what I thought was weird corruption on my two-line LCD when I hit the recursion depth error, but looking at more verbose output over my UART I think it's just supporting my idea that it's the callback happening quickly. I don't know if this may have been something you ran into at any point? I can provide my code that currently reliably has this issue for me on Windows 10, but it's currently between the states of "nice tidy code that has been written before testing any of it" and "reasonable somewhat tidy code that has been debugged and cleaned up", so it's now at "horrible hacky mess with various attempts at making things work shoehorned in" |
Thinking about it, I might as well just attach it here regardless - I do intend to add it to turmoni/micropython-lib when it's in a better (working, cleaner, more documented) state, but with the caveats that I know it's broken and in the middle of debugging I'm not too embarrassed to share the code. To try it out, create an The basic flow is:
What I'm seeing on Windows is that it's mostly working fine, until it ends up failing to do something, and then I don't think I have the ability to get it back in sync, but ideally I shouldn't need to. (I don't think anything ever hits handle_endpoint_control_xfer, so feel free to ignore that.) |
Thanks @turmoni. At a glance this looks quite good to me! I haven't had time to run it, might be a while before I have that time.
EDIT: The above is wrong! The default stack size for rp2 port is 8KB, set via |
OK, what I confidently wrote just before is totally wrong - there are currently a bunch of places where an event hook may call back into For most cases we can put an anti-recursion check into So, it may have to be one of: treat USB xfer callbacks almost like hard interrupts and do no significant work in them, or change the callbacks to be called via |
Thanks, I did quickly do a new firmware build with the REPL on UART 0 and that didn't seem to enlighten me any further, so I suspect it is some kind of unexpected interaction. I had a quick look to see if I could hackily change how the callbacks are called, but I'm neither proficient enough at C nor the micropython codebase to be able to do anything useful there in the short time I have to look at it this evening. |
Right, so, I've done terrible things to your code and I feel dirty, but I think I understand one of the issues I'm having. Firstly, I replaced all my callbacks with short functions that called micropython.schedule() to schedule something that actually does the work, so hopefully if that's an issue, that's no longer a factor with what I'm doing. This still didn't fix it though, so I did some more digging. I've copied and pasted various helper functions from the micropython source to try to figure out what the issue is that I'm now facing, and I think that it's
So one of the first things I realised was that I felt silly because I didn't actually have to do anything with the C code, because all it did was point me to the fact that I should be looking at _xfer_cb because that's the thing that actually invokes the right Python callback. But this does (probably) show the overall flow as it happened; line-by-line:
This makes a lot of sense for the non-recursion-error symptoms I'm seeing, if the callback handler is being called before the callback has had a chance to be written. Edit: of course, my debug code could be affecting the timing here and making it more prone to happen, but I don't see how it would be introducing the issue. |
Well having worked around this (see below) it looks like it was just an error I was exposing for myself, and it wasn't the original problem I was having, where my CSW claims to have been submitted with the right data but goes missing and doesn't make it to my USB capture in Wireshark. I guess that's tomorrow's problem for me to investigate. def _retry_xfer_cb(self, args):
print("Retry!")
(ep_addr, result, xferred_bytes) = args
self._xfer_cb(ep_addr, result, xferred_bytes)
def _xfer_cb(self, ep_addr, result, xferred_bytes):
# Singleton callback from TinyUSB custom class driver when a transfer completes.
print("In _xfer_cb!")
try:
itf, cb = self._eps[ep_addr]
print(f"itf: {itf}, cb: {str(cb)}")
if cb is None:
micropython.schedule(self._retry_xfer_cb, (ep_addr, result, xferred_bytes))
return
self._eps[ep_addr] = (itf, None)
except KeyError:
cb = None
if cb:
print("Got a cb")
cb(ep_addr, result, xferred_bytes) |
Right, I think I've solved my magical disappearing CSW problem, and that aspect was my fault; as per the spec:
I was sending less than dCBWDataTransferLength, but not STALLing (since we can't do that yet). Padding out the response to dCBWDataTransferLength seems to solve my issue. Now to go and see how much of this code I can rip out again... |
I'm now trying to set up a composite device, using both HID and MSC, and my brain has been slightly fried. It looks like whichever interface I (Another issue I ran into is with the Python side where the current logic for picking endpoints doesn't work because once you have an IN endpoint >=0x80, adding one to it won't get you back into OUT endpoint territory, but I'm putting that here more as a note for anyone else trying this.) |
OK so I've now done everything I needed to; to summarise the things I'm working around:
To elaborate on that last one, since I hadn't mentioned it earlier, USB HID allows you to either receive OUT-style requests on the OUT endpoint if you've defined one, or on the control endpoint. I spent a little bit of time trying to figure out how you'd make this work generically before deciding it was easier to just add an OUT endpoint to USB HID for my purposes. Thanks for doing this work, I'm glad I didn't have to do all my logic in C! |
c03147a
to
c4024e7
Compare
This comment was marked as resolved.
This comment was marked as resolved.
bd176f0
to
108e7f0
Compare
Pushed another big update with some breaking API changes that came up in review. Check the included docs for details. Have also updated the size numbers in the first post. This update also squashes everything down to be merge-ready. |
shared/tinyusb/mp_usbd.h
Outdated
mp_obj_t control_xfer_cb; | ||
mp_obj_t xfer_cb; | ||
|
||
mp_usbd_builtin_driver_t builtin_driver; |
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.
Could this just be mp_obj_t
, and it just stores the pointer to the current built-in driver object?
Then you don't need an enum, and the logic to get/set the attribute for this is very simple.
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.
True. I think at the time I didn't want that detail to bleed into mp_usbd.c, but can simply declare extern consts for the various allowed constants. Will change!
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 think it's simpler to have the extern consts than the enum and the enum<->const mapping.
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.
Done, PTAL!
1bcccab
to
9a231ea
Compare
d7f396c
to
fa7a8ff
Compare
I'm really looking forward to see this being finalized ! You are doing a huge job with a high value, thank you for that. |
This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton <angus@redyak.com.au>
Previously USB was always enabled, but this created some conflicts when adding guards to other files on other ports. Note the configuration with USB disabled hasn't been tested and probably won't build or run without further work. Signed-off-by: Angus Gratton <angus@redyak.com.au>
This new machine-module driver provides a "USBDevice" singleton object and a shim TinyUSB "runtime" driver that delegates the descriptors and all of the TinyUSB callbacks to Python functions. This allows writing arbitrary USB devices in pure Python. It's also possible to have a base built-in USB device implemented in C (eg CDC, or CDC+MSC) and a Python USB device added on top of that. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton <angus@redyak.com.au>
This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton <angus@redyak.com.au>
This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton <angus@redyak.com.au>
fa7a8ff
to
7f5d8c4
Compare
Merged!! Thanks @projectgus for all your efforts on this. It's a great feature, will be widely used. |
Congrats @projectgus! Super exciting to see this move forward, I'll see you in the lib pull request 😉 |
terrific feature! Congrats! |
Excellent news, congratulations and thanks! |
Adding new product - Cytron IRIV IO Controller
The goal is to allow implementing USB device drivers in Python via TinyUSB at runtime, in addition to any existing compile-time TinyUSB class drivers (i.e. CDC and optionally MSC).
Python API with the more interesting user-facing code is at micropython/micropython-lib#558 - if checking out this branch in micropython, go to the
lib/micropython-lib
subdirectory and check out thefeature/usbd_python
branch there as well.Port Support
This PR implements this for the
rp2
andsamd
ports.The port-level requirement is that the TinyUSB task is run in a "scheduled" static callback node, as implemented in #12846 for rp2 & samd.) I think this approach can be extended to most of the other ports pretty easily.
Challenging ports (future work):
Binary Size Impact
Where possible, the implementation has been placed into micropython-lib to reduce the baseline binary size impact. If enabled at compile time (currently not the default) the code size change is:
Note: This doesn't include freezing in the "usbd" module from micropython-lib. Currently that module is approximately 15KB.
How To Use
mpconfigport.h
file for your port or board and changeMICROPY_HW_ENABLE_USB_RUNTIME_DEVICE
to 1.manifest.py
file for your port or board and insertrequire("usbd")
.See micropython/micropython-lib#558 for more.
Debugging Tips
REPL debugging via the USB serial port is obviously perilous when the USB stack itself is being debugged, and the whole device needs to re-enumerate regularly for the host to see changes.
Hardware UART
Switch the REPL interface to a hardware UART if you can (for rp2, set
MICROPY_HW_ENABLE_UART_REPL
inports/rps2/mpconfigport.h
.) This makesmpremote mount
very slow, but it means crashes that take down USB are still reported on the REPL!TinyUSB debug
To enable TinyUSB debug output, add the lines
#undef CFG_TUSB_DEBUG
and#define CFG_TUSB_DEBUG 2
(or a different level) to theshared/tinyusb/tusb_config.h
header. Debugging output goes to stdout, so don't do this unless you've also switched to a hardware UART.Semihosting
Another option is semihosting. On rp2 with an SWD debugger connected, can apply this quick patch and then run pyocd as follows:
Read from localhost port 4444 and you'll see stdout at any time that USB-CDC device is not open. (This failover behaviour is useful because semihosting is painfully slow, and also pyocd doesn't support non-UTF-8 input or output so any "raw REPL" traffic will cause it to crash.)
In recent testing the semihosting patch is still not stable when using
mpremote
to run Python code that triggers re-enumeration, it panics trying to write to the CDC while the device is being reset (more debugging needed).This work was funded through GitHub Sponsors.