Skip to content

Commit fca7fb5

Browse files
Add USB drive mode to TinyUSB, SingleFileDisk (#1034)
SingleFileDisk allows for exporting a file from the onboard LittleFS filesystem to a PC through an emulated FAT drive when connected. The PC can open and copy the file, as well as delete it, but the PC has no access to the main onboard LittleFS and no actual on-flash FAT structures are used. This is handy for things like data loggers. They can run connected to USB power for some time, and then connected to a PC to dowmload the CSV log recorded. It's almost 2023, allow LFN (long file names) on the emulated USB disk. Reduce the disk buffer size to 64 bytes. The buffer is statically allocated so it's always present, even in non-USB disk mode, meaning all apps will pay the RAM price for it. 64 bytes is slower to read but works and saves ~1/2KB of heap for all apps.
1 parent 80d6e2f commit fca7fb5

File tree

15 files changed

+743
-12
lines changed

15 files changed

+743
-12
lines changed

cores/rp2040/RP2040USB.cpp

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,11 @@ static int __usb_task_irq;
6868
#define USBD_STR_SERIAL (0x03)
6969
#define USBD_STR_CDC (0x04)
7070

71-
7271
#define EPNUM_HID 0x83
7372

73+
#define USBD_MSC_EPOUT 0x03
74+
#define USBD_MSC_EPIN 0x84
75+
#define USBD_MSC_EPSIZE 64
7476

7577
const uint8_t *tud_descriptor_device_cb(void) {
7678
static tusb_desc_device_t usbd_desc_device = {
@@ -89,7 +91,7 @@ const uint8_t *tud_descriptor_device_cb(void) {
8991
.iSerialNumber = USBD_STR_SERIAL,
9092
.bNumConfigurations = 1
9193
};
92-
if (__USBInstallSerial && !__USBInstallKeyboard && !__USBInstallMouse && !__USBInstallJoystick) {
94+
if (__USBInstallSerial && !__USBInstallKeyboard && !__USBInstallMouse && !__USBInstallJoystick && !__USBInstallMassStorage) {
9395
// Can use as-is, this is the default USB case
9496
return (const uint8_t *)&usbd_desc_device;
9597
}
@@ -103,6 +105,9 @@ const uint8_t *tud_descriptor_device_cb(void) {
103105
if (__USBInstallJoystick) {
104106
usbd_desc_device.idProduct |= 0x0100;
105107
}
108+
if (__USBInstallMassStorage) {
109+
usbd_desc_device.idProduct ^= 0x2000;
110+
}
106111
// Set the device class to 0 to indicate multiple device classes
107112
usbd_desc_device.bDeviceClass = 0;
108113
usbd_desc_device.bDeviceSubClass = 0;
@@ -223,7 +228,7 @@ void __SetupUSBDescriptor() {
223228
if (!usbd_desc_cfg) {
224229
bool hasHID = __USBInstallKeyboard || __USBInstallMouse || __USBInstallJoystick;
225230

226-
uint8_t interface_count = (__USBInstallSerial ? 2 : 0) + (hasHID ? 1 : 0);
231+
uint8_t interface_count = (__USBInstallSerial ? 2 : 0) + (hasHID ? 1 : 0) + (__USBInstallMassStorage ? 1 : 0);
227232

228233
uint8_t cdc_desc[TUD_CDC_DESC_LEN] = {
229234
// Interface number, string index, protocol, report descriptor len, EP In & Out address, size & polling interval
@@ -238,7 +243,12 @@ void __SetupUSBDescriptor() {
238243
TUD_HID_DESCRIPTOR(hid_itf, 0, HID_ITF_PROTOCOL_NONE, hid_report_len, EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 10)
239244
};
240245

241-
int usbd_desc_len = TUD_CONFIG_DESC_LEN + (__USBInstallSerial ? sizeof(cdc_desc) : 0) + (hasHID ? sizeof(hid_desc) : 0);
246+
uint8_t msd_itf = interface_count - 1;
247+
uint8_t msd_desc[TUD_MSC_DESC_LEN] = {
248+
TUD_MSC_DESCRIPTOR(msd_itf, 0, USBD_MSC_EPOUT, USBD_MSC_EPIN, USBD_MSC_EPSIZE)
249+
};
250+
251+
int usbd_desc_len = TUD_CONFIG_DESC_LEN + (__USBInstallSerial ? sizeof(cdc_desc) : 0) + (hasHID ? sizeof(hid_desc) : 0) + (__USBInstallMassStorage ? sizeof(msd_desc) : 0);
242252

243253
uint8_t tud_cfg_desc[TUD_CONFIG_DESC_LEN] = {
244254
// Config number, interface count, string index, total length, attribute, power in mA
@@ -260,6 +270,10 @@ void __SetupUSBDescriptor() {
260270
memcpy(ptr, hid_desc, sizeof(hid_desc));
261271
ptr += sizeof(hid_desc);
262272
}
273+
if (__USBInstallMassStorage) {
274+
memcpy(ptr, msd_desc, sizeof(msd_desc));
275+
ptr += sizeof(msd_desc);
276+
}
263277
}
264278
}
265279
}
@@ -367,4 +381,55 @@ extern "C" void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_r
367381
(void) bufsize;
368382
}
369383

384+
extern "C" int32_t tud_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, void* buffer, uint32_t bufsize) __attribute__((weak));
385+
extern "C" int32_t tud_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, void* buffer, uint32_t bufsize) {
386+
(void) lun;
387+
(void) lba;
388+
(void) offset;
389+
(void) buffer;
390+
(void) bufsize;
391+
return -1;
392+
}
393+
394+
extern "C" bool tud_msc_test_unit_ready_cb(uint8_t lun) __attribute__((weak));
395+
extern "C" bool tud_msc_test_unit_ready_cb(uint8_t lun) {
396+
(void) lun;
397+
return false;
398+
}
399+
400+
extern "C" int32_t tud_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset, uint8_t* buffer, uint32_t bufsize) __attribute__((weak));
401+
extern "C" int32_t tud_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset, uint8_t* buffer, uint32_t bufsize) {
402+
(void) lun;
403+
(void) lba;
404+
(void) offset;
405+
(void) buffer;
406+
(void) bufsize;
407+
return -1;
408+
}
409+
410+
extern "C" int32_t tud_msc_scsi_cb(uint8_t lun, uint8_t const scsi_cmd[16], void* buffer, uint16_t bufsize) __attribute__((weak));
411+
extern "C" int32_t tud_msc_scsi_cb(uint8_t lun, uint8_t const scsi_cmd[16], void* buffer, uint16_t bufsize) {
412+
(void) lun;
413+
(void) scsi_cmd;
414+
(void) buffer;
415+
(void) bufsize;
416+
return 0;
417+
}
418+
419+
extern "C" void tud_msc_capacity_cb(uint8_t lun, uint32_t* block_count, uint16_t* block_size) __attribute__((weak));
420+
extern "C" void tud_msc_capacity_cb(uint8_t lun, uint32_t* block_count, uint16_t* block_size) {
421+
(void) lun;
422+
*block_count = 0;
423+
*block_size = 0;
424+
}
425+
426+
extern "C" void tud_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8], uint8_t product_id[16], uint8_t product_rev[4]) __attribute__((weak));
427+
extern "C" void tud_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8], uint8_t product_id[16], uint8_t product_rev[4]) {
428+
(void) lun;
429+
vendor_id[0] = 0;
430+
product_id[0] = 0;
431+
product_rev[0] = 0;
432+
}
433+
434+
370435
#endif

cores/rp2040/RP2040USB.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ extern void __USBInstallSerial() __attribute__((weak));
2626
extern void __USBInstallKeyboard() __attribute__((weak));
2727
extern void __USBInstallJoystick() __attribute__((weak));
2828
extern void __USBInstallMouse() __attribute__((weak));
29+
extern void __USBInstallMassStorage() __attribute__((weak));
2930

3031
// Big, global USB mutex, shared with all USB devices to make sure we don't
3132
// have multiple cores updating the TUSB state in parallel

docs/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ For the latest version, always check https://github.com/earlephilhower/arduino-p
3939
USB (Arduino and Adafruit_TinyUSB) <usb>
4040
Multicore Processing <multicore>
4141

42+
Single File USB Drive <singlefile>
43+
4244
FreeRTOS SMP (multicore) <freertos>
4345

4446
WiFi (Pico-W Support) <wifi>

docs/singlefile.rst

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
SingleFileDrive
2+
===============
3+
4+
USB drive mode is supported through the ``SingleFileDrive`` class which
5+
allows the Pico to emulate a FAT-formatted USB stick while preserving the
6+
onboard LittleFS filesystem. A single file can be exported this way without
7+
needing to use FAT as the onboard filesystem (FAT is not appropriate for
8+
flash-based devices without complicated wear leveling because of the update
9+
frequency of the FAT tables).
10+
11+
This emulation is very simple and only allows for the reading of the single
12+
file, and deleting it.
13+
14+
Callbacks, Interrupt Safety, and File Operations
15+
------------------------------------------------
16+
17+
The ``SingleFileDrive`` library allows your application to get a callback
18+
when a PC attempts to mount or unmount the Pico as a drive. Your app can
19+
also get a callback if the user attempts to delete the file (but your
20+
sketch does not actually need to delete the file, it's up to you).
21+
22+
Note that when the USB drive is mounted by a PC it is not safe for your
23+
main sketch to make changes to the LittleFS filesystem or the file being
24+
exported. So, normally, your ``onPlug`` callback will set a flag letting
25+
your application know not to touch the filesystem, with the ``onUnplug``
26+
callback clearing this flag.
27+
28+
Also, because the USB port can be connected at any time, it is important
29+
to disable interrupts using ``noInterrupts()`` before writing to a file
30+
you will be exporting (and restoring them with ``interrupts()`` afterwards).
31+
It is also important to ``close()`` the file after each update, or the
32+
on-flash version the ``SingleFileDrive`` will attempt to export may not be
33+
up to date causing issues later on.
34+
35+
See the included ``DataLoggerUSB`` sketch for an example of working with
36+
these limitations.
37+
38+
Using SingleFileDrive
39+
---------------------
40+
41+
Implementing the drive requires including the header file, starting LittleFS,
42+
defining your callbacks, and telling the library what file to export. No
43+
polling or other calls are required outside of your ``setup()``. (Note that
44+
the callback routines allow for a parameter to be passed to them, but in most
45+
cases this can be safely ignored.)
46+
47+
.. code:: cpp
48+
49+
#include <LittleFS.h>
50+
#include <SingleFileDrive.h>
51+
52+
void myPlugCB(uint32_t data) {
53+
// Tell my app not to write to flash, we're connected
54+
}
55+
56+
void myUnplugCB(uint32_t data) {
57+
// I can start writing to flash again
58+
}
59+
60+
void myDeleteDB(uint32_t data) {
61+
// Maybe LittleFS.remove("myfile.txt")? or do nothing
62+
}
63+
64+
void setup() {
65+
LittleFS.begin();
66+
singleFileDrive.onPlug(myPlugCB);
67+
singleFileDrive.onUnplug(myUnplugCB);
68+
singleFileDrive.onDelete(myDeleteCB);
69+
singleFileDrive.begin("littlefsfile.csv", "Data Recorder.csv");
70+
// ... rest of setup ...
71+
}
72+
73+
void loop() {
74+
// Take some measurements, delay, etc.
75+
if (okay-to-write) {
76+
noInterrupts();
77+
File f = LittleFS.open("littlefsfile.csv", "a");
78+
f.printf("%d,%d,%d\n", data1, data2, data3);
79+
f.close();
80+
interrupts();
81+
}
82+
}

include/tusb_config.h

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,14 @@
7272
//------------- CLASS -------------//
7373
#define CFG_TUD_HID (2)
7474
#define CFG_TUD_CDC (1)
75-
#define CFG_TUD_MSC (0)
75+
#define CFG_TUD_MSC (1)
7676
#define CFG_TUD_MIDI (0)
7777
#define CFG_TUD_VENDOR (0)
7878

7979
#define CFG_TUD_CDC_RX_BUFSIZE (256)
8080
#define CFG_TUD_CDC_TX_BUFSIZE (256)
8181

82-
#define CFG_TUD_MIDI_RX_BUFSIZE (64)
83-
#define CFG_TUD_MIDI_TX_BUFSIZE (64)
82+
#define CFG_TUD_MSC_EP_BUFSIZE (64)
8483

8584
// HID buffer size Should be sufficient to hold ID (if any) + Data
8685
#define CFG_TUD_HID_EP_BUFSIZE (64)

lib/libpico-ipv6.a

49 KB
Binary file not shown.

lib/libpico.a

49 KB
Binary file not shown.

libraries/PicoOTA/library.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ maintainer=Earle F. Philhower, III <[email protected]>
55
sentence=Configures requests for OTA bootloader
66
paragraph=Example repository for Ethernet drivers
77
category=Device Control
8-
url=https://github.com/earlephilhower.arduino-pico
8+
url=https://github.com/earlephilhower/arduino-pico
99
architectures=rp2040
1010
dot_a_linkage=true
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Simple logger with USB upload to PC
2+
// Uses SingleFileDrive to export an onboard LittleFS file to the computer
3+
// The PC can open/copy the file, and then the user can delete it to restart
4+
5+
// Released to the public domain, 2022 - Earle F. Philhower, III
6+
7+
#include <SingleFileDrive.h>
8+
#include <LittleFS.h>
9+
10+
uint32_t cnt = 0;
11+
bool okayToWrite = true;
12+
13+
// Make the CSV file and give it a simple header
14+
void headerCSV() {
15+
File f = LittleFS.open("data.csv", "w");
16+
f.printf("sample,millis,temp,rand\n");
17+
f.close();
18+
cnt = 0;
19+
}
20+
21+
// Called when the USB stick connected to a PC and the drive opened
22+
// Note this is from a USB IRQ so no printing to SerialUSB/etc.
23+
void plug(uint32_t i) {
24+
(void) i;
25+
okayToWrite = false;
26+
}
27+
28+
// Called when the USB is ejected or removed from a PC
29+
// Note this is from a USB IRQ so no printing to SerialUSB/etc.
30+
void unplug(uint32_t i) {
31+
(void) i;
32+
okayToWrite = true;
33+
}
34+
35+
// Called when the PC tries to delete the single file
36+
// Note this is from a USB IRQ so no printing to SerialUSB/etc.
37+
void deleteCSV(uint32_t i) {
38+
(void) i;
39+
LittleFS.remove("data.csv");
40+
headerCSV();
41+
}
42+
43+
void setup() {
44+
Serial.begin();
45+
delay(5000);
46+
47+
LittleFS.begin();
48+
49+
// Set up the USB disk share
50+
singleFileDrive.onDelete(deleteCSV);
51+
singleFileDrive.onPlug(plug);
52+
singleFileDrive.onUnplug(unplug);
53+
singleFileDrive.begin("data.csv", "Recorded data from the Raspberry Pi Pico.csv");
54+
55+
// Find the last written data
56+
File f = LittleFS.open("data.csv", "r");
57+
if (!f || !f.size()) {
58+
cnt = 1;
59+
headerCSV();
60+
} else {
61+
if (f.size() > 2048) {
62+
f.seek(f.size() - 1024);
63+
}
64+
do {
65+
String s = f.readStringUntil('\n');
66+
sscanf(s.c_str(), "%lu,", &cnt);
67+
} while (f.available());
68+
f.close();
69+
cnt++;
70+
}
71+
72+
Serial.printf("Starting acquisition at %d\n", cnt);
73+
}
74+
75+
void loop() {
76+
float temp = analogReadTemp();
77+
uint32_t hwrand = rp2040.hwrand32();
78+
// Make sure the USB connect doesn't happen while we're writing!
79+
noInterrupts();
80+
if (okayToWrite) {
81+
Serial.printf("Sampling...%lu\n", cnt);
82+
// Don't want the USB to connect during an update!
83+
File f = LittleFS.open("data.csv", "a");
84+
if (f) {
85+
f.printf("%lu,%lu,%f,%lu\n", cnt++, millis(), temp, hwrand);
86+
f.close();
87+
}
88+
}
89+
interrupts();
90+
91+
delay(10000);
92+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#######################################
2+
# Syntax Coloring Map
3+
#######################################
4+
5+
#######################################
6+
# Datatypes (KEYWORD1)
7+
#######################################
8+
9+
SingleFileDrive KEYWORD1
10+
11+
#######################################
12+
# Methods and Functions (KEYWORD2)
13+
#######################################
14+
15+
singleFileDrive KEYWORD1
16+
onDelete KEYWORD1
17+
onPlug KEYWORD1
18+
onUnplug KEYWORD1
19+
20+
#######################################
21+
# Constants (LITERAL1)
22+
#######################################

0 commit comments

Comments
 (0)