diff --git a/components/box-emu/CMakeLists.txt b/components/box-emu/CMakeLists.txt index 4ee9646..148b201 100644 --- a/components/box-emu/CMakeLists.txt +++ b/components/box-emu/CMakeLists.txt @@ -8,8 +8,9 @@ idf_component_register( "esp_lcd" "esp_psram" "hal" - "usb" - "esp_tinyusb" + "esp_hw_support" + "usb_device" + "sdcard" "lvgl" "codec" "adc" diff --git a/components/box-emu/idf_component.yml b/components/box-emu/idf_component.yml index df13dc4..a2c0832 100644 --- a/components/box-emu/idf_component.yml +++ b/components/box-emu/idf_component.yml @@ -1,17 +1,17 @@ ## IDF Component Manager Manifest File dependencies: idf: '>=5.5' - espressif/esp_tinyusb: '>=2.0' - lvgl/lvgl: '>=9.2.2' - espp/adc: '>=1.0' - espp/aw9523: '>=1.0' - espp/button: '>=1.0' - espp/drv2605: '>=1.0' - espp/esp-box: '>=1.0' - espp/event_manager: '>=1.0' - espp/max1704x: '>=1.0' - espp/mcp23x17: '>=1.0' - espp/task: '>=1.0' - espp/timer: '>=1.0' - espp/serialization: '>=1.0' - espressif/usb: '>=1.4.1' + lvgl/lvgl: '>=9.6.0' + espp/adc: '>=1.3.4' + espp/aw9523: '>=1.3.4' + espp/button: '>=1.3.4' + espp/drv2605: '>=1.3.4' + espp/esp-box: '>=1.3.4' + espp/event_manager: '>=1.3.4' + espp/max1704x: '>=1.3.4' + espp/mcp23x17: '>=1.3.4' + espp/sdcard: '>=1.3.4' + espp/serialization: '>=1.3.4' + espp/task: '>=1.3.4' + espp/timer: '>=1.3.4' + espp/usb_device: '>=1.3.4' diff --git a/components/box-emu/include/box-emu.hpp b/components/box-emu/include/box-emu.hpp index 848ec23..a770b40 100644 --- a/components/box-emu/include/box-emu.hpp +++ b/components/box-emu/include/box-emu.hpp @@ -6,18 +6,10 @@ #include #include -#include #include -// #include #include -#include -#include -#include - -#include - #include "esp-box.hpp" #include "event_manager.hpp" @@ -31,9 +23,11 @@ #include "max1704x.hpp" #include "mcp23x17.hpp" #include "oneshot_adc.hpp" +#include "sdcard.hpp" #include "serialization.hpp" #include "task.hpp" #include "timer.hpp" +#include "usb_device.hpp" #include "battery_info.hpp" #include "gamepad_state.hpp" @@ -130,8 +124,15 @@ class BoxEmu : public espp::BaseComponent { // uSD Card ///////////////////////////////////////////////////////////////////////////// + /// Initialize the uSD card (SPI) and mount its FAT volume at mount_point. + /// \return True if the card was initialized and mounted. bool initialize_sdcard(); + /// The initialized card, or nullptr if there is none. + /// \note While USB mass storage is enabled the card belongs to the USB host + /// and its volume is not mounted for the application. sdmmc_card_t *sdcard() const; + /// The SD card component (nullptr if the card was not initialized). + espp::SdCard *sdcard_component() const; ///////////////////////////////////////////////////////////////////////////// // Memory @@ -187,7 +188,14 @@ class BoxEmu : public espp::BaseComponent { // USB ///////////////////////////////////////////////////////////////////////////// + /// Expose the uSD card to a USB host as a mass storage device. + /// \note The card's volume is unmounted from the application while USB is + /// enabled (the emulator cannot read roms until it is disabled again); + /// the USB-Serial-JTAG console is disconnected as well, since it shares + /// the USB port. + /// \return True if USB mass storage was started. bool initialize_usb(); + /// Stop USB mass storage, reconnect the console and mount the card again. bool deinitialize_usb(); bool is_usb_enabled() const; @@ -322,6 +330,7 @@ class BoxEmu : public espp::BaseComponent { static constexpr gpio_num_t sdcard_miso = GPIO_NUM_13; static constexpr gpio_num_t sdcard_sclk = GPIO_NUM_12; static constexpr auto sdcard_spi_num = SPI3_HOST; + static constexpr int sdcard_max_files = 16; static constexpr int num_rows_in_framebuffer = 30; @@ -334,7 +343,7 @@ class BoxEmu : public espp::BaseComponent { .scl_pullup_en = GPIO_PULLUP_ENABLE}}; // sdcard - sdmmc_card_t *sdcard_{nullptr}; + std::unique_ptr sdcard_{nullptr}; // memory uint8_t *romdata_{nullptr}; @@ -377,8 +386,10 @@ class BoxEmu : public espp::BaseComponent { // usb std::atomic usb_enabled_{false}; - usb_phy_handle_t jtag_phy_; - tinyusb_msc_storage_handle_t msc_storage_handle_{nullptr}; + std::unique_ptr usb_device_{nullptr}; + // The USB-Serial-JTAG console PHY, re-created after USB mass storage is stopped + // so the console comes back (the OTG stack owns the PHY while it runs). + usb_phy_handle_t jtag_phy_{nullptr}; }; // for libfmt printing of the BoxEmu::Version enum diff --git a/components/box-emu/src/box-emu.cpp b/components/box-emu/src/box-emu.cpp index 2d5250a..1e17cd5 100644 --- a/components/box-emu/src/box-emu.cpp +++ b/components/box-emu/src/box-emu.cpp @@ -108,77 +108,51 @@ bool BoxEmu::initialize_sdcard() { logger_.info("Initializing SD card"); - esp_err_t ret; - // Options for mounting the filesystem. If format_if_mount_failed is set to - // true, SD card will be partitioned and formatted in case when mounting - // fails. - esp_vfs_fat_sdmmc_mount_config_t mount_config; - memset(&mount_config, 0, sizeof(mount_config)); - mount_config.format_if_mount_failed = false; + // The card is on its own SPI bus (SPI3); the component initializes the bus. + // By default, the SD card frequency is SDMMC_FREQ_DEFAULT (20MHz), which is the + // maximum for SDSPI. + espp::SdCard::SpiConfig spi; + spi.host = sdcard_spi_num; + spi.cs = sdcard_cs; + spi.initialize_bus = true; + spi.mosi = sdcard_mosi; + spi.miso = sdcard_miso; + spi.sclk = sdcard_sclk; + spi.max_transfer_size = 4096; + + espp::SdCard::Config config; + config.interface = spi; + config.mount_point = mount_point; + config.mount_on_initialize = true; + config.format_if_mount_failed = false; // Dark Forces (TFE) keeps its GOB and LFD archives open while running; allow enough handles. - mount_config.max_files = 16; - mount_config.allocation_unit_size = 2 * 1024; - - // Use settings defined above to initialize SD card and mount FAT filesystem. - // Note: esp_vfs_fat_sdmmc/sdspi_mount is all-in-one convenience functions. - // Please check its source code and implement error recovery when developing - // production applications. - logger_.debug("Using SPI peripheral"); - - // By default, SD card frequency is initialized to SDMMC_FREQ_DEFAULT (20MHz) - // For setting a specific frequency, use host.max_freq_khz (range 400kHz - 20MHz for SDSPI) - // Example: for fixed frequency of 10MHz, use host.max_freq_khz = 10000; - sdmmc_host_t host = SDSPI_HOST_DEFAULT(); - host.slot = sdcard_spi_num; - // host.max_freq_khz = 20 * 1000; - - spi_bus_config_t bus_cfg; - memset(&bus_cfg, 0, sizeof(bus_cfg)); - bus_cfg.mosi_io_num = sdcard_mosi; - bus_cfg.miso_io_num = sdcard_miso; - bus_cfg.sclk_io_num = sdcard_sclk; - bus_cfg.quadwp_io_num = -1; - bus_cfg.quadhd_io_num = -1; - bus_cfg.max_transfer_sz = 4096; - spi_host_device_t host_id = (spi_host_device_t)host.slot; - ret = spi_bus_initialize(host_id, &bus_cfg, SDSPI_DEFAULT_DMA); - if (ret != ESP_OK) { - logger_.error("Failed to initialize bus."); - return false; - } - - // This initializes the slot without card detect (CD) and write protect (WP) signals. - // Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals. - sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); - slot_config.gpio_cs = sdcard_cs; - slot_config.host_id = host_id; - - logger_.debug("Mounting filesystem"); - ret = esp_vfs_fat_sdspi_mount(mount_point, &host, &slot_config, &mount_config, &sdcard_); + config.max_files = sdcard_max_files; + config.allocation_unit_size = 2 * 1024; + config.log_level = espp::Logger::Verbosity::INFO; - if (ret != ESP_OK) { - if (ret == ESP_FAIL) { - logger_.error("Failed to mount filesystem. " - "If you want the card to be formatted, set the CONFIG_EXAMPLE_FORMAT_IF_MOUNT_FAILED menuconfig option."); - return false; - } else { - logger_.error("Failed to initialize the card ({}). " - "Make sure SD card lines have pull-up resistors in place.", esp_err_to_name(ret)); - return false; - } + auto sdcard = std::make_unique(config); + std::error_code ec; + if (!sdcard->initialize(ec)) { + logger_.error("Failed to initialize / mount the SD card: {}. " + "Make sure a FAT formatted card is inserted.", ec.message()); return false; } + sdcard_ = std::move(sdcard); logger_.info("Filesystem mounted"); // Card has been initialized, print its properties - sdmmc_card_print_info(stdout, sdcard_); + sdcard_->print_info(stdout); return true; } sdmmc_card_t *BoxEmu::sdcard() const { - return sdcard_; + return sdcard_ ? sdcard_->card() : nullptr; +} + +espp::SdCard *BoxEmu::sdcard_component() const { + return sdcard_.get(); } ///////////////////////////////////////////////////////////////////////////// @@ -582,54 +556,6 @@ void BoxEmu::set_haptic_effect(int effect) { // USB ///////////////////////////////////////////////////////////////////////////// -#define TUSB_DESC_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_MSC_DESC_LEN) - -enum { - ITF_NUM_MSC = 0, - ITF_NUM_TOTAL -}; - -enum { - EDPT_CTRL_OUT = 0x00, - EDPT_CTRL_IN = 0x80, - - EDPT_MSC_OUT = 0x01, - EDPT_MSC_IN = 0x81, -}; - -static uint8_t const desc_configuration[] = { - // Config number, interface count, string index, total length, attribute, power in mA - TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, TUSB_DESC_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100), - - // Interface number, string index, EP Out & EP In address, EP size - TUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 0, EDPT_MSC_OUT, EDPT_MSC_IN, TUD_OPT_HIGH_SPEED ? 512 : 64), -}; - -static tusb_desc_device_t descriptor_config = { - .bLength = sizeof(descriptor_config), - .bDescriptorType = TUSB_DESC_DEVICE, - .bcdUSB = 0x0200, - .bDeviceClass = TUSB_CLASS_MISC, - .bDeviceSubClass = MISC_SUBCLASS_COMMON, - .bDeviceProtocol = MISC_PROTOCOL_IAD, - .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, - .idVendor = 0x303A, // This is Espressif VID. This needs to be changed according to Users / Customers - .idProduct = 0x4002, - .bcdDevice = 0x100, - .iManufacturer = 0x01, - .iProduct = 0x02, - .iSerialNumber = 0x03, - .bNumConfigurations = 0x01 -}; - -static char const *string_desc_arr[] = { - (const char[]) { 0x09, 0x04 }, // 0: is supported language is English (0x0409) - "Finger563", // 1: Manufacturer - "ESP-Box-Emu", // 2: Product - "123456", // 3: Serials - "Box-Emu uSD Card", // 4. MSC -}; - bool BoxEmu::is_usb_enabled() const { return usb_enabled_; } @@ -649,55 +575,64 @@ bool BoxEmu::initialize_usb() { return false; } - logger_.debug("Deleting JTAG PHY"); - usb_del_phy(jtag_phy_); - - fmt::print("USB MSC initialization\n"); - esp_vfs_fat_mount_config_t fat_mount_config = { - .format_if_mount_failed = false, - .max_files = 16, - .allocation_unit_size = 2 * 1024, // sector size is 512 bytes, this should be between sector size and (128 * sector size). Larger means higher read/write performance and higher overhead for small files. - .disk_status_check_enable = false, // true if you see issues or are unmounted properly; slows down I/O - .use_one_fat = true, - }; - - tinyusb_msc_fatfs_config_t config_msc = { - .base_path = (char*)mount_point, - .config = fat_mount_config, - .do_not_format = true, - .format_flags = 0, - }; + // The card cannot be shared: release the application's FAT volume before + // handing the card to the USB host. + std::error_code ec; + if (!sdcard_->unmount(ec)) { + logger_.error("Could not unmount the SD card: {}", ec.message()); + return false; + } - tinyusb_msc_storage_config_t msc_storage_config = { - .medium = { - .card = card, - }, - .fat_fs = config_msc, - .mount_point = TINYUSB_MSC_STORAGE_MOUNT_USB, + // The USB-Serial-JTAG console and USB-OTG share the PHY; release it so the + // USB device stack can take it (it was re-created by deinitialize_usb()). + if (jtag_phy_) { + logger_.debug("Deleting JTAG PHY"); + usb_del_phy(jtag_phy_); + jtag_phy_ = nullptr; + } + + espp::UsbDevice::MscMedium medium; + medium.type = espp::UsbDevice::MscMedium::Type::SdCard; + medium.sd_card = card; + medium.base_path = mount_point; + medium.max_files = sdcard_max_files; + // The host gets the card right away; if the host ejects the drive the card is + // mounted for the application again (at mount_point) until it is re-attached. + medium.initial_owner = espp::UsbDevice::MscOwner::Host; + + espp::UsbDevice::MscFunction msc; + msc.interface_name = "Box-Emu uSD Card"; + msc.media = {medium}; + msc.auto_handover = true; + msc.on_event = [this](size_t lun, espp::UsbDevice::MscEvent event, espp::UsbDevice::MscOwner owner) { + if (event == espp::UsbDevice::MscEvent::OwnerChanged) { + logger_.info("uSD card now owned by the {}", + owner == espp::UsbDevice::MscOwner::App ? "application" : "USB host"); + } else if (event == espp::UsbDevice::MscEvent::FormatRequired) { + logger_.warn("uSD card has no filesystem; format it from the host"); + } }; - ESP_ERROR_CHECK(tinyusb_msc_new_storage_sdmmc(&msc_storage_config, &msc_storage_handle_)); - // register the callback for the storage mount changed event. - // ESP_ERROR_CHECK(tinyusb_msc_register_callback(TINYUSB_MSC_EVENT_MOUNT_CHANGED, storage_mount_changed_cb)); - - // initialize the tinyusb stack - fmt::print("USB MSC initialization\n"); - // no device_event_handler for tud_mount and tud_unmount callbacks - tinyusb_config_t tusb_cfg = TINYUSB_DEFAULT_CONFIG(); - tusb_cfg.task = TINYUSB_TASK_CUSTOM(4096 /*size */, 4 /* priority */, - 0 /* affinity: 0 - CPU0, 1 - CPU1 ... */); - tusb_cfg.descriptor.device = &descriptor_config; - tusb_cfg.descriptor.string = string_desc_arr; - tusb_cfg.descriptor.string_count = - sizeof(string_desc_arr) / sizeof(string_desc_arr[0]); - tusb_cfg.descriptor.full_speed_config = desc_configuration; - tusb_cfg.phy.skip_setup = false; // was external-phy = false - tusb_cfg.phy.self_powered = false; - tusb_cfg.phy.vbus_monitor_io = -1; - - ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg)); - fmt::print("USB MSC initialization DONE\n"); + espp::UsbDevice::Config config; + config.vid = 0x303A; // Espressif VID + config.pid = 0x4002; + config.manufacturer = "Finger563"; + config.product = "ESP-Box-Emu"; + config.serial_number = "123456"; + config.msc = msc; + config.log_level = espp::Logger::Verbosity::INFO; + + auto usb = std::make_unique(config); + if (!usb->initialize(ec)) { + logger_.error("Failed to initialize the USB device: {}", ec.message()); + // give the card back to the application + usb.reset(); + sdcard_->mount(); + return false; + } + usb_device_ = std::move(usb); usb_enabled_ = true; + logger_.info("USB MSC initialization DONE"); return true; } @@ -707,27 +642,25 @@ bool BoxEmu::deinitialize_usb() { logger_.warn("USB MSC not initialized"); return false; } - esp_err_t err; logger_.info("USB MSC deinitialization"); - // deinit + delete the msc storage handle - err = tinyusb_msc_delete_storage(msc_storage_handle_); - if (err != ESP_OK) { - logger_.error("tinyusb_msc_delete_storage failed: {}", esp_err_to_name(err)); - return false; - } - logger_.info("USB deinitialization"); - err = tinyusb_driver_uninstall(); - if (err != ESP_OK) { - logger_.error("tinyusb_driver_uninstall failed: {}", esp_err_to_name(err)); - return false; - } + // Stops the USB stack and releases the card (the destructor waits for the + // host's pending writes). + usb_device_.reset(); usb_enabled_ = false; - // and reconnect the CDC port, see: + + // reconnect the USB-Serial-JTAG console, see: // https://github.com/espressif/idf-extra-components/pull/229 usb_phy_config_t phy_conf; memset(&phy_conf, 0, sizeof(phy_conf)); phy_conf.controller = USB_PHY_CTRL_SERIAL_JTAG; usb_new_phy(&phy_conf, &jtag_phy_); + + // mount the card for the application again + std::error_code ec; + if (!sdcard_->mount(ec)) { + logger_.error("Could not mount the SD card again: {}", ec.message()); + return false; + } return true; } diff --git a/components/box-emu/src/lvgl_mem.c b/components/box-emu/src/lvgl_mem.c new file mode 100644 index 0000000..b0e9468 --- /dev/null +++ b/components/box-emu/src/lvgl_mem.c @@ -0,0 +1,67 @@ +// LVGL memory in PSRAM. +// +// LVGL is built with CONFIG_LV_USE_CUSTOM_MALLOC (see sdkconfig.defaults), so +// its allocator hooks are implemented here instead of by a fixed-size pool in +// internal RAM. Everything LVGL allocates (objects, styles, draw buffers, the +// rom list of the GUI, the pause menu, ...) goes to the PSRAM heap; only if +// PSRAM is exhausted does an allocation fall back to internal RAM. +// +// The display's DMA buffers are not affected: they are allocated by the BSP +// with the capabilities the LCD driver needs. +#include + +#include "lvgl.h" + +#define LVGL_PSRAM_CAPS (MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT) +#define LVGL_FALLBACK_CAPS (MALLOC_CAP_8BIT) + +void lv_mem_init(void) { + // The heaps are set up by ESP-IDF before app_main(). +} + +void lv_mem_deinit(void) {} + +lv_mem_pool_t lv_mem_add_pool(void *mem, size_t bytes) { + // Not supported: the system heap is the pool. + LV_UNUSED(mem); + LV_UNUSED(bytes); + return NULL; +} + +void lv_mem_remove_pool(lv_mem_pool_t pool) { LV_UNUSED(pool); } + +void *lv_malloc_core(size_t size) { + void *p = heap_caps_malloc(size, LVGL_PSRAM_CAPS); + if (p == NULL) { + p = heap_caps_malloc(size, LVGL_FALLBACK_CAPS); + } + return p; +} + +void *lv_realloc_core(void *p, size_t new_size) { + void *new_p = heap_caps_realloc(p, new_size, LVGL_PSRAM_CAPS); + if (new_p == NULL) { + new_p = heap_caps_realloc(p, new_size, LVGL_FALLBACK_CAPS); + } + return new_p; +} + +void lv_free_core(void *p) { heap_caps_free(p); } + +void lv_mem_monitor_core(lv_mem_monitor_t *mon_p) { + // Report the PSRAM heap, which is where LVGL's memory lives. + multi_heap_info_t info; + heap_caps_get_info(&info, LVGL_PSRAM_CAPS); + mon_p->total_size = info.total_free_bytes + info.total_allocated_bytes; + mon_p->free_cnt = info.free_blocks; + mon_p->free_size = info.total_free_bytes; + mon_p->free_biggest_size = info.largest_free_block; + mon_p->used_cnt = info.allocated_blocks; + mon_p->max_used = mon_p->total_size - info.minimum_free_bytes; + mon_p->used_pct = mon_p->total_size ? (uint8_t)(100 - (100ULL * info.total_free_bytes) / mon_p->total_size) : 0; + mon_p->frag_pct = mon_p->free_size ? (uint8_t)(100 - (100ULL * info.largest_free_block) / mon_p->free_size) : 0; +} + +lv_result_t lv_mem_test_core(void) { + return heap_caps_check_integrity(LVGL_PSRAM_CAPS, false) ? LV_RESULT_OK : LV_RESULT_INVALID; +} diff --git a/main/idf_component.yml b/main/idf_component.yml index 96f2515..5fc569e 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -1,5 +1,5 @@ ## IDF Component Manager Manifest File dependencies: ## Required IDF version - idf: '>=5.4' - espp/monitor: '>=1.0' + idf: '>=5.5' + espp/monitor: '>=1.3.4' diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 071fb96..f683528 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -73,7 +73,6 @@ CONFIG_FATFS_USE_FASTSEEK=y # USB config # CONFIG_TINYUSB_MSC_ENABLED=y -CONFIG_TINYUSB_MSC_MOUNT_PATH="/sdcard" # ESP32-specific # @@ -90,10 +89,11 @@ CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH=y CONFIG_LV_BUILD_EXAMPLES=n -# LVGL allocates from the system heap (PSRAM) instead of a fixed internal pool: -# the GUI (with all of its rom entries) and the emulator menu share one LVGL -# instance, and a fixed 50KB pool ran out in the pause menu (LVGL then halts). -CONFIG_LV_USE_CLIB_MALLOC=y +# LVGL memory lives in PSRAM (components/box-emu/src/lvgl_mem.c) instead of a +# fixed pool in internal RAM: the GUI (with all of its rom entries) and the +# emulator menu share one LVGL instance, and a fixed 50KB pool ran out in the +# pause menu (LVGL then halts). +CONFIG_LV_USE_CUSTOM_MALLOC=y CONFIG_LV_DEF_REFR_PERIOD=16