SD Card Shenanigans - Using the ESP32-S3 in USBMSC
USBMSC - USB Mass Storage Class - we can map our SD Card through the ESP32 so it can be mounted on our computer.
I've been doing some more work on my ESP32-TV. We got streaming from the SD Card working previously. So I really wanted to make it easier to get videos onto the SD Card without having to take it out and plug it into my computer.
Enter USBMSC - USB Mass Storage Class. This is a standard way of connecting a USB device to a computer and making it look like a disk drive.
It's really easy to get this to work on the ESP32-S3. We just need to supply three callback functions.
#include "USBMSC.h"
USBMSC msc;
msc.onRead(onRead);
msc.onWrite(onWrite);
msc.onStartStop(onStartStop);
The onRead and onWrite functions are called when the computer wants to read or write data to the SD Card. The onStartStop function is called when the computer wants to eject the SD Card.
Both the onRead and onWrite take a sector number and a buffer. The buffer is always a multiple of the sector size (512 bytes). The onRead function should fill the buffer with the data from the sector(s). The onWrite function should write the data from the buffer to the sector(s).
Typically I found that I received a buffer size of 4096 bytes - which is the equivalent of eight 512 byte sectors.
The Arduino SD class has two functions we can use for writing to sectors:
SD.writeRAW(sector, buffer);
SD.readRAW(sector, buffer);
So I wired it all up and... it's really slow.
Writing is terrible 270KBytes/s - we'd be much better off just disconnecting the SDCard from the ESP32 and plugging it in directly.
Surely we can do better than this?
It certainly not a limitation of our SD Card - if we plug it directly into my laptop we get 26MBytes/s write and 90MBytes/s read.
But we're not plugging it directly into our computer, we're running our ESP32 as a mass storage controller and connecting through that to the SD Card.
The ESP32-S3 only supports USB1.1 full speed mode. This gives us a maximum transfer rate of 12Mbit/s over the USB connection - theoretically, this could give us 1.5MBytes/s - but there are overhead in the USB protocol. It feels like 1MBytes/s is probably much closer to what is achievable.
We've also got the limitations of our ESP32 connection to the SDCard - we're connecting to it using SPI - the actual clock speed this connects at will depend on several factors, but with the cards and wiring I'm using I seem to get 20MHz reliably. With better wiring we might be able to get 40MHz, but for that we'll need a proper PCB with an SD Card slot on it.
It’s pretty amazing that we can get 20MHz with the current wiring and bodged SD Card reader.
Reading and writing raw sectors to the SD Card I get 1MBytes/s write speed and 1.7MBytes/s read speed. So it feels like 1MByte/s writing and 1MByte/s reading should be achievable.
We can of course use SDIO in 4 bit mode to talk to our SD Card - that involves some extra GPIO pins, but it does give us much better performance.
If we connect up a card in this mode we get 2.34MBytes/s for writing and an amazing 8.39MBytes/s reading.
So, why is our speed so slow when we connect to the card over USB through the ESP32? 270KBytes per second is almost a quarter of what we should be getting. It's not great.
The problem is the Arduino readRaw and writeRaw functions. One issue is that they don't seem to be available on the SDMMC class - so we can only use them in SPI mode. The other issues is that they can only write one sector at a time.
Frustratingly if you drill into the functions they are actually calling functions that can write multiple sectors at once - it's just not exposed to us.
So, I've ignored the Arduino code and gone straight to the IDF functions. These have a couple of friendly functions: sdmmc_write_sectors and sdmmc_read_sectors. What's more these functions work in both SPI and SDIO mode.
In SPI mode using these function to write and read the data all in one go we improve the write performance by over 70% - it's now writing at 480KBytes/s instead of 270KBytes/s.
Our read speed has also improved by 32%. We're getting 671KBytes/s instead of 507KByte/s.
If we use the SDIO 4 bit connection we get even better performance - we can now write at 0.66MBytes/s and reading is an incredible 1MBytes/s - that's probably as fast as we can go over the USB1.1 connection.
It's a pretty amazing improvement from some very simple code changes.
Reading is now performing as fast as I think is possible. But it feels like writing could still be improved.
So I thought I'd try something a bit dodgy. We don't actually need to wait for the writing to the SDCard to complete before returning.
What if we ran the writing to the SDCard in the background and returned straight away.
We would lose any proper error handling and we'll need to be careful to wait for writes to complete before allowing any reading, but we could get quite a performance improvement as the transfers to the SD Card could be overlapped with the transfers coming from the USB connection.
So I've given that a go. With the SPI connection can now get almost 1MByte/s write performance. It's actually 915KBytes/s which is 229% faster than our original 270KBytes/s and 90% faster than our multisector writing method.
With the SDIO 4 bit connection we get similar results - so I suspect we've definitely hit the limits of what's possible with our USB connection.
Obviously there's no impact on the read speed, this stays pretty much exactly the same as our multi sector read version.
The big question we have to ask is, is this actually safe to use.
And in answer, I really have no idea. We now have no error handling when writes fail so potentially we could get our card into quite a weird state. But lots of hard disk controllers buffer writing so maybe it's fine.
Is it good enough for our ESP32-TV project?
Short videos around 5 minutes end up 50Mbytes in size once they've been converted to AVI files. So to copy that to our SDCard we'd need about 1 minute. That might be acceptable acceptable, but to me it's annoyingly slow.
We're going to need some kind of alternative.
The simple solution is to just let people take the SDCard out and plug it directly into their PC - and that's my fallback option.
But I've also got a slightly bonkers idea.
There is a really cheap IC for interfacing SD Cards with USB - the GL823. This will run over USB2.0 at a whopping 480Mbs which would give us much faster access to our SD Card.
I've also found a handy USB multiplexer switch which is also really cheap.
And I've found another switch IC that would let us switch the SD Card between the ESP32 and the GL823. It's got six lines so we could run the ESP32 is SDIO 4bit mode if needed.
Now potentially this could completely fail, but I think it's worth a try.
The way I'm thinking this will work is that we'll start off with the USB connections routed to the ESP32 and the GL823 powered down or held in reset mode. We'll also have the SDCard powered up and routed through to the ESP32.
If the user wants to connect to the SD Card we'll shutdown the ESP32s USB peripheral and switch the USB connection over to the GL823. We'll then turn the SD Card off and connect it to the GL823.
We'll power up the GL823 and the SDCard and in theory it should connect up to the computer.
I can't think of any way of switching off this mode - there doesn't seem to be any signal from the GL823 to indicate that the device has been ejected, so we'll have to rely on the user unplugging the USB.
It's a long shot, but I think this could work.
The other option would be to transfer the files using WiFi and write them directly to the SDCard. But I really want to the get the USB solution working now.
The GitHub repo with my tests is here: https://github.com/atomic14/esp32-sdcard-msc