Skip to content
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

usbus/msc: add initial Mass Storage Class support #19242

Merged
merged 4 commits into from
Mar 3, 2023

Conversation

dylad
Copy link
Member

@dylad dylad commented Feb 3, 2023

Contribution description

This PR adds the initial support for Mass Storage Class in USBUS. This PR relies on the RIOT MTD implementation to implement the Mass Storage Class support. With the provided test application, a MTD device will be accessible as a normal storage device on your host computer.
Read and Write operations are allowed.
Multiple LUNs are supported so several MTD devices can be exported through USB.
The MSC relies on SCSI protocol to operate.

Currently there are some limitations:
Supported host : Linux & Windows (macOS is untested)
MSC cannot be used if MTD page size > 4096
MTD device must have at least 512 bytes of memory to be exported.

Please be aware that performance are not so great.

Testing procedure

Flash tests/usbus_msc application on a board with at least one MTD device.
Once the shell has started, prepare one or several MTD devices to be exported using add_lun command.
Once ready, start the USB connection with usb_attach

All MTD exported should appear as /dev/sdX on Linux.

Issues/PRs references

Supersede #15941

@github-actions github-actions bot added Area: doc Area: Documentation Area: Kconfig Area: Kconfig integration Area: sys Area: System Area: tests Area: tests and testing framework Area: tools Area: Supplementary tools Area: USB Area: Universal Serial Bus labels Feb 3, 2023
Copy link
Contributor

@benpicco benpicco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know much about the USB part, but I can take a look at the MTD part.

sys/include/usb/usbus/msc/scsi.h Show resolved Hide resolved
if (mtd->page_size <= block_size) {
/* Write one or multiple pages */
printf("WRITE:%d bytes\n", block_size);
ret = mtd_write_page(mtd, msc->buffer, msc->block, 0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you are writing a full page, you can use mtd_write_page_raw() and avoid the read-modify-write cycle

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see you actually want this for cases where the block size is > 4096.
We should probably add a fast-path to mtd_write_page() then to skip the read-modify-write cycle if the whole sector is written - #19252

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to implement both mtd_write_page() and mtd_write_page_raw() for some time but since this PR stalled I think we should just stick to one of them for now.
But I don't mind to switch to mtd_write_page_raw() if you think it is more suitable.

sys/usb/usbus/msc/msc.c Outdated Show resolved Hide resolved

/* Internal buffer to handle size difference between MTD layer and USB
endpoint size as some MTD implementation (like mtd_sdcard) doesn't allow
endpoint size transfer */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh the MTD layer should take care of splitting up the transfers.
What do you mean here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This buffer is used to store data coming from the endpoint. Since USB endpoints transfers (for Full-Speed device) are capped at 64 bytes, this buffer stores each transfer until it reaches the MTD device flash page size.
For example, if the MTD page size is 512 bytes. We need to have 8 endpoints transfer of 64 bytes to fill this buffer so we can trigger a write to the MTD device.
Moreover, since I cannot report the real page size of the MTD device if it's less than 512 bytes (e.g for EEPROM). This buffer will then have to store multiples pages at once.
But I agree that the wording is weird. I can come up with something better.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to do this if you erase the sector on the first block and always write a whole sector. (mtd_write_page_raw() will not to the read-modify-write but just write and you don't need to write the whole sector at once).

There might be a slight performance benefit to it, but I'm not sure.

If you are the sole user of the MTD device you could also use mtd->work_area which is a per-device, dynamically allocated sector-sized buffer.

That would be a bit hacky though - but I also don't see a problem with just writing 64 bytes at a time.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to do this if you erase the sector on the first block and always write a whole sector.

Unfortunately, I am not sure I can guarantee I'll always write a whole sector. USB Host will use the reported page size and doesn't know about the sector size of the MTD device. So it may write only one or a few pages.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then how do you decide when to flush?
Or are those writes just lost if the sector is not completed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

USB will always transfer a page¹ (in 64 byte chunks) but since we can only erase a sector, we have to do a read-modify write cycle.

Very true.

Buffering the page brings that down to one RMW cycle.

That's the idea.

To sum up, there are 3 cases to properly handle:

  • USB reported page size == MTD page size

    In this case, (512, 1024, 2048, 4096), this is pretty easy. We store the 64 bytes chunks into a buffer (allocated dynamically or not) Since we have a whole MTD page (not a MTD sector) we can write it directly to the memory. (And we rely on the MTD implementation to handle the erase part).

  • USB reported page size > MTD page size

This is the case for a few MTD type like EEPROM. In this case, we report an USB page size of 512 (the minimum allowed). So if MTD page size is 256. We have to write two MTD pages for each USB page.
This is also why we cannot export MTD device which have less than 512 bytes of memory. (Well we could cheat but this will just add more complexity for a small corner case and I believe this PR is already complex enough...)

  • USB reported page size < MTD page size

This is a more tricky one, which is not yet support by this PR (even though there is some code laying around). For this case, we need to do some RMW cycle. But this will belong to a followup PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would propose the following:
if the 'USB page' is smaller than the sector size, allocate a temporary buffer as you do here, but you may use malloc (as that's already used by mtd_pagewise - I think malloc on init is fine.
if the 'USB page' is greater or equal the sector size, don't allocate a buffer and write the bytes as you get them with mtd_write_page_raw(), ereasing the sector at the first block.

That way you don't have the memory overhead in the common case and don't have to tune config variables to fit the storage.

What do you think?

Performance-wise, the second idea is great but I need to check if it won't make the code unreadable because of the corner cases.
For the first idea, I think this will the most common pattern. I can switch to dynamically allocated buffer since we let the user chose which MTD to export through the test application. malloc() can then be call only and only if the user request the MTD device to be exported. This may save some memory on boards which have a several MTD that one can export but only need one to be exported.

Copy link
Contributor

@benpicco benpicco Feb 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why tie this to page size? A page is just the largest transfer unit, but the MTD will already take care of that, so on the application/interface level it's rather arbitrary.

Sector size is the much more interesting value as it's the erase unit. If you report the sector size (and you will always get a full sector through MSC) you can just erase the sector at the start of the transfer and then write the 64 byte blocks as they come (without the RMW).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point. This will increase performance for some MTD devices. So you suggest to erase the sector from USBUS then call mtd_write_page_raw() for each USB endpoints chunks received to avoid storing the content of a page ?
I can try to give it a go. I don't think such rework would be too painful.
Obviously, this won't work at all for sector size > 4096.

Copy link
Member Author

@dylad dylad Feb 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am making progress here, I have a working version using sector size instead of page size.
I am now looking at the buffer side and I think we shouldn't drop it even if the USB block == MTD sector size:
This might impact performance on read operation. Currently, a single mtd_read() is performed and stored into the buffer. Then we slice the buffer through USB chunks. Dropping the buffer would force us to call mtd_read() for each USB transfer chunks and it seems sub-optimal to me.

Regarding the dynamical allocation of the buffer, I am not against. This can be done during the USB_MSC_SETUP_REQ_GML request. This way we can iterate through all exported MTD devices to USB. Determine which device has the greatest sector size and allocate this sector size to the buffer. No need for user configuration.

Note that even if multiple LUNs are used at the same time. Operation can only be done one LUN at a time. So having one buffer is completely fine for all LUNs (read or write operations).

sys/usb/usbus/msc/scsi.c Outdated Show resolved Hide resolved
sys/usb/usbus/msc/scsi.c Outdated Show resolved Hide resolved
sys/usb/usbus/msc/scsi.c Outdated Show resolved Hide resolved
sys/usb/usbus/msc/scsi.c Outdated Show resolved Hide resolved
@dylad
Copy link
Member Author

dylad commented Feb 25, 2023

Lastest commit changes the following:

  • Use MTD sector size instead of MTD page size as USB block.
  • Dynamically allocate the internal buffer depending on the MTD with the greatest sector size value.

@dylad dylad force-pushed the pr/usbus/add_msc_support branch from 856c9c4 to 051b325 Compare February 25, 2023 20:15
Copy link
Contributor

@benpicco benpicco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one, works like a charm on my same54-xpro 😃
Read speeds are ~600kB/s for both SD card and SPI flash, I wonder if this could benefit from #17091 as a follow-up.

Some doc comments mostly:

sys/include/usb/usbus/msc.h Outdated Show resolved Hide resolved
sys/usb/usbus/msc/msc.c Outdated Show resolved Hide resolved
sys/usb/usbus/msc/msc.c Show resolved Hide resolved
sys/include/usb/usbus/msc.h Outdated Show resolved Hide resolved
sys/include/usb/usbus/msc.h Outdated Show resolved Hide resolved
sys/usb/usbus/msc/msc.c Outdated Show resolved Hide resolved
@dylad
Copy link
Member Author

dylad commented Feb 26, 2023

I think this PR would benefit #17091. At least, it will lower a bit the code complexity of this PR.
But since #17091 doesn't have any use case in master, I guess it's better to have this one first so @bergzand could give it a quick rebase right after ;)

Copy link
Contributor

@benpicco benpicco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please squash (and rebase)

btw, #17091 implements URBs for CDC ECM.

sys/usb/usbus/msc/msc.c Show resolved Hide resolved
sys/usb/usbus/msc/scsi.c Outdated Show resolved Hide resolved
pkt->type = 0;
pkt->removable = 1;
pkt->version = 0x01;
pkt->length = len - 4;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why - 4?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the additional packet length minus the "default" data of this packet.
a SCSI inquiry response will always include the 4 first bytes, but you may have to pass additional information. Thus, you pass the number additional bytes through this field.

I'll add a comment.

/* Report the size of the struct minus the 7 first bytes */
pkt->add_len = len - 7;
pkt->asc = 0x30;
pkt->ascq = 0x01;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are those magic numbers?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is undocumented black magic numbers, a valid answer here ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did you get them from?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think it was on some source code provided by ST for a STM32F1 that I used back in 2019.
I did have a look at Zephyr a few months ago too, but it was the same. (Just hexadecimal value used to fill their buffer).
I'll have a look at it tonight. I have a bunch of USB MSC related document on my home laptop. I'll see if I can give you more insight then.

@dylad dylad force-pushed the pr/usbus/add_msc_support branch from 6d2c6f0 to fe8e493 Compare February 28, 2023 20:49
@dylad
Copy link
Member Author

dylad commented Feb 28, 2023

Rebased and squashed.
@benpicco Regarding the ACS, ACSQ "black magic" stuff: I've just add a comment for now.
These values are in fact used to report to the host additional information regarding different kind of failures.
I'll make use of this in a later PR, let's stick to default values for now, as more work is required.

@dylad dylad force-pushed the pr/usbus/add_msc_support branch from fe8e493 to da2e989 Compare February 28, 2023 20:59
@benpicco benpicco added the CI: ready for build If set, CI server will compile all applications for all available boards for the labeled PR label Feb 28, 2023
@@ -0,0 +1,29 @@

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will also need a Makefile.ci
You can create it with

make generate-Makefile.ci

@riot-ci
Copy link

riot-ci commented Feb 28, 2023

Murdock results

✔️ PASSED

3671b00 tests: add test application for usbus msc

Success Failures Total Runtime
6919 0 6919 11m:58s

Artifacts

Signed-off-by: Dylan Laduranty <dylan.laduranty@mesotic.com>
@dylad dylad force-pushed the pr/usbus/add_msc_support branch 2 times, most recently from 0f08ff1 to 9290fca Compare March 2, 2023 20:18
dylad added 2 commits March 2, 2023 22:04
Signed-off-by: Dylan Laduranty <dylan.laduranty@mesotic.com>
Signed-off-by: Dylan Laduranty <dylan.laduranty@mesotic.com>
@dylad dylad force-pushed the pr/usbus/add_msc_support branch from 9290fca to 6496918 Compare March 2, 2023 21:04
Signed-off-by: Dylan Laduranty <dylan.laduranty@mesotic.com>
@dylad dylad force-pushed the pr/usbus/add_msc_support branch from 6496918 to 3671b00 Compare March 2, 2023 21:18
@benpicco
Copy link
Contributor

benpicco commented Mar 2, 2023

bors merge

@benpicco
Copy link
Contributor

benpicco commented Mar 2, 2023

bors cancel
bors merge

@dylad
Copy link
Member Author

dylad commented Mar 2, 2023

Do we need a bors stop drinking again ?

bors bot added a commit that referenced this pull request Mar 2, 2023
@benpicco
Copy link
Contributor

benpicco commented Mar 2, 2023

I think bors already went to sleep

bors ping

@bors
Copy link
Contributor

bors bot commented Mar 2, 2023

pong

@bors
Copy link
Contributor

bors bot commented Mar 3, 2023

Build succeeded:

@bors bors bot merged commit 743ae3f into RIOT-OS:master Mar 3, 2023
@dylad dylad deleted the pr/usbus/add_msc_support branch March 3, 2023 06:34
@dylad
Copy link
Member Author

dylad commented Mar 3, 2023

@benpicco Thanks for the review & merge !

@bors
Copy link
Contributor

bors bot commented Mar 3, 2023

try

Timed out.

Copy link
Contributor

@benpicco benpicco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been trying to combine this with stdio_cdc_acm but so far no luck.

Is there some inherent conflict or is the issue just that we initialize the USBus twice?

/* Initialize Mass Storage Class */
usbus_msc_init(&usbus, &msc);
/* Create USBUS thread */
usbus_create(_stack, USBUS_STACKSIZE, USBUS_PRIO, USBUS_TNAME, &usbus);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed here if we already have auto_init_usbus?

assert(usbdev);

/* Initialize basic usbus struct, don't start the thread yet */
usbus_init(&usbus, usbdev);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

usbus_init(&usbus, usbdev);

/* Initialize Mass Storage Class */
usbus_msc_init(&usbus, &msc);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be moved to auto_init_usb.c?

@dylad
Copy link
Member Author

dylad commented Mar 7, 2023

Because It was easier to setup things this way.
The idea was to merge this stuff and let the users decided which MTD devices they want to use at runtime. And yes, it doesn't get along with CDC for now because I still need a bit of time to find the right solution for this.
Nevertheless, I should have a describe it in the Documentation, my bad.

I don't like the idea of initialize and export all MTD devices unconditionally. And I don't like the idea of an always ON MSC implementation either. It'll just increase the overall USB transfer for nothing with "ping" transfer aka TEST_UNIT_READY.

To me, the MSC and the MTD devices selection should be a user runtime choice.
Obviously, there are some uses cases to export all available MTD devices (this is why I implemented the multiple LUNs support) and to have a MSC always ON (hobbyst or academic playground). But I don't think it should be the default behavior.

@MrKevinWeiss MrKevinWeiss added this to the Release 2023.04 milestone Apr 25, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area: doc Area: Documentation Area: Kconfig Area: Kconfig integration Area: sys Area: System Area: tests Area: tests and testing framework Area: tools Area: Supplementary tools Area: USB Area: Universal Serial Bus CI: ready for build If set, CI server will compile all applications for all available boards for the labeled PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants