-
Notifications
You must be signed in to change notification settings - Fork 26
feat(vmu-pro): Add BSP component for the 8BitMods VMU-Pro #664
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
Draft
finger563
wants to merge
2
commits into
main
Choose a base branch
from
feat/vmupro
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # only register the component if the target is esp32s3 | ||
| idf_component_register( | ||
| INCLUDE_DIRS "include" | ||
| SRC_DIRS "src" | ||
| REQUIRES driver esp_driver_i2s fatfs base_component display display_drivers interrupt led spi task | ||
| REQUIRED_IDF_TARGETS "esp32s3" | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| menu "VMU Pro Configuration" | ||
| config VMU_PRO_INTERRUPT_STACK_SIZE | ||
| int "Interrupt stack size" | ||
| default 4096 | ||
| help | ||
| Size of the stack used for the interrupt handler. Shared by all of | ||
| the button callbacks. | ||
|
|
||
| config VMU_PRO_INTERRUPT_PRIORITY | ||
| int "Interrupt task priority" | ||
| default 20 | ||
| range 0 25 | ||
| help | ||
| FreeRTOS priority of the GPIO interrupt handler task (0 = lowest). Raise it | ||
| if interrupt callbacks must run promptly relative to other tasks. | ||
|
|
||
| config VMU_PRO_INTERRUPT_CORE_ID | ||
| int "Interrupt task core ID" | ||
| default -1 | ||
| range -1 1 | ||
| help | ||
| Core to pin the GPIO interrupt handler task to (-1 = not pinned to any core). | ||
| endmenu |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # VMU Pro Board Support Package (BSP) Component | ||
|
|
||
| [](https://components.espressif.com/components/espp/vmu-pro) | ||
|
finger563 marked this conversation as resolved.
|
||
|
|
||
| The [VMU Pro](https://8bitmods.com/vmupro-handheld-visual-memory-card-for-dreamcast-classic-white/) | ||
| by [8BitMods](https://8bitmods.com) is an ESP32-S3 based replacement for the | ||
| Sega Dreamcast Visual Memory Unit (VMU) which doubles as a standalone handheld | ||
| gaming device. It features: | ||
|
|
||
| * 1.5" 240x240 IPS TFT color display | ||
| * D-pad, A, B, Mode, Power, and Bottom buttons | ||
| * Mono speaker (I2S amplifier) | ||
| * micro-SD card slot (mounted at `/sdcard`) | ||
| * Rechargeable battery with USB-C charging | ||
|
|
||
| The `espp::VmuPro` component provides a singleton hardware abstraction for | ||
| initializing the display, buttons, audio, and uSD card subsystems. | ||
|
|
||
| > **Warning:** The GPIO assignments in this component are UNVERIFIED | ||
| > placeholders. The VMU Pro's schematic is not publicly available, so the pin | ||
| > numbers in `include/vmu-pro.hpp` must be corrected against real hardware or | ||
| > vendor documentation before the component will function on the device. The | ||
| > peripheral set itself (display size and color format, button list, audio | ||
| > capabilities, uSD card) comes from the official | ||
| > [VMU Pro SDK](https://github.com/AppCakeLtd/vmupro-sdk) and is accurate. | ||
|
|
||
| ## Example | ||
|
|
||
| The [example](./example) shows how to use the `espp::VmuPro` hardware | ||
| abstraction component to initialize the components on the VMU Pro and build a | ||
| simple LVGL UI (via a `Gui` class) driven by the buttons. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| # The following lines of boilerplate have to be in your project's CMakeLists | ||
| # in this exact order for cmake to work correctly | ||
| cmake_minimum_required(VERSION 3.20) | ||
|
|
||
| set(ENV{IDF_COMPONENT_MANAGER} "0") | ||
| include($ENV{IDF_PATH}/tools/cmake/project.cmake) | ||
|
|
||
| # add the component directories that we want to use | ||
| set(EXTRA_COMPONENT_DIRS | ||
| "../../../components/" | ||
| ) | ||
|
|
||
| set( | ||
| COMPONENTS | ||
| "main esptool_py vmu-pro" | ||
| CACHE STRING | ||
| "List of components to include" | ||
| ) | ||
|
|
||
| project(vmu_pro_example) | ||
|
|
||
| set(CMAKE_CXX_STANDARD 20) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # VMU Pro Example | ||
|
|
||
| This example shows how to use the `espp::VmuPro` hardware abstraction component | ||
| to initialize the components on the [8BitMods VMU | ||
| Pro](https://8bitmods.com/vmupro-handheld-visual-memory-card-for-dreamcast-classic-white/). | ||
|
|
||
| It initializes the display, buttons, audio, and uSD card subsystems and builds | ||
| a simple LVGL UI using a `Gui` class: | ||
|
|
||
| - The D-pad moves a cursor around the screen | ||
| - The A button draws a circle at the cursor (and plays a beep) | ||
| - The B button clears the circles (and plays a lower beep) | ||
| - The Mode button rotates the display through 0/90/180/270 degrees | ||
| - The Bottom button cycles the backlight brightness (25/50/75/100%) | ||
| - The Power button toggles the audio mute | ||
|
|
||
| > **Warning:** The GPIO assignments in the `espp::VmuPro` component are | ||
| > UNVERIFIED placeholders (the VMU Pro's schematic is not publicly available), | ||
| > so this example will build but is not expected to function on real hardware | ||
| > until the pins in `components/vmu-pro/include/vmu-pro.hpp` are corrected. | ||
|
|
||
| ## How to use example | ||
|
|
||
| ### Hardware Required | ||
|
|
||
| This example is designed to run on the 8BitMods VMU Pro. | ||
|
|
||
| ### Build and Flash | ||
|
|
||
| Build the project and flash it to the board, then run monitor tool to view | ||
| serial output: | ||
|
|
||
| ``` | ||
| idf.py -p PORT flash monitor | ||
| ``` | ||
|
|
||
| (Replace PORT with the name of the serial port to use.) | ||
|
|
||
| (To exit the serial monitor, type ``Ctrl-]``.) | ||
|
|
||
| See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| idf_component_register(SRC_DIRS "." INCLUDE_DIRS ".") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,212 @@ | ||
| #include "gui.hpp" | ||
|
|
||
| void Gui::init_ui() { | ||
| std::lock_guard<std::recursive_mutex> lock(mutex_); | ||
| init_background(); | ||
| init_label(); | ||
| init_circle_layer(); | ||
| init_cursor(); | ||
| // start with the cursor in the center of the screen | ||
| auto &vmu = espp::VmuPro::get(); | ||
| cursor_x_ = vmu.rotated_display_width() / 2; | ||
| cursor_y_ = vmu.rotated_display_height() / 2; | ||
| update_cursor(); | ||
| // disable scrolling on the screen | ||
| lv_obj_set_scrollbar_mode(lv_screen_active(), LV_SCROLLBAR_MODE_OFF); | ||
| lv_obj_clear_flag(lv_screen_active(), LV_OBJ_FLAG_SCROLLABLE); | ||
| } | ||
|
|
||
| void Gui::deinit_ui() { | ||
| std::lock_guard<std::recursive_mutex> lock(mutex_); | ||
| lv_obj_clean(lv_screen_active()); | ||
| } | ||
|
|
||
| void Gui::init_background() { | ||
| auto &vmu = espp::VmuPro::get(); | ||
| background_ = lv_obj_create(lv_screen_active()); | ||
| lv_obj_set_size(background_, vmu.lcd_width(), vmu.lcd_height()); | ||
| lv_obj_set_style_bg_color(background_, lv_color_make(0, 0, 0), 0); | ||
| } | ||
|
|
||
| void Gui::init_label() { | ||
| label_ = lv_label_create(lv_screen_active()); | ||
| lv_label_set_text(label_, ""); | ||
| lv_obj_align(label_, LV_ALIGN_TOP_LEFT, 0, 0); | ||
| lv_obj_set_style_text_align(label_, LV_TEXT_ALIGN_LEFT, 0); | ||
| } | ||
|
|
||
| void Gui::init_cursor() { | ||
| cursor_ = lv_obj_create(lv_screen_active()); | ||
| lv_obj_remove_style_all(cursor_); | ||
| lv_obj_set_size(cursor_, CURSOR_RADIUS * 2, CURSOR_RADIUS * 2); | ||
| lv_obj_set_style_radius(cursor_, LV_RADIUS_CIRCLE, 0); | ||
| lv_obj_set_style_border_width(cursor_, 2, 0); | ||
| lv_obj_set_style_border_color(cursor_, lv_palette_main(LV_PALETTE_YELLOW), 0); | ||
| lv_obj_set_style_bg_opa(cursor_, LV_OPA_TRANSP, 0); | ||
| lv_obj_clear_flag(cursor_, LV_OBJ_FLAG_CLICKABLE); | ||
| lv_obj_clear_flag(cursor_, LV_OBJ_FLAG_SCROLLABLE); | ||
| } | ||
|
|
||
| void Gui::init_circle_layer() { | ||
| auto &vmu = espp::VmuPro::get(); | ||
| circle_layer_ = lv_obj_create(lv_screen_active()); | ||
| lv_obj_remove_style_all(circle_layer_); | ||
| lv_obj_set_size(circle_layer_, vmu.lcd_width(), vmu.lcd_height()); | ||
| lv_obj_align(circle_layer_, LV_ALIGN_CENTER, 0, 0); | ||
| lv_obj_clear_flag(circle_layer_, LV_OBJ_FLAG_CLICKABLE); | ||
| lv_obj_clear_flag(circle_layer_, LV_OBJ_FLAG_SCROLLABLE); | ||
| lv_obj_set_style_bg_opa(circle_layer_, LV_OPA_TRANSP, 0); | ||
| lv_obj_set_style_border_width(circle_layer_, 0, 0); | ||
| lv_obj_set_style_outline_width(circle_layer_, 0, 0); | ||
| lv_obj_set_style_shadow_width(circle_layer_, 0, 0); | ||
| lv_obj_add_event_cb(circle_layer_, draw_circle_layer, LV_EVENT_DRAW_MAIN, this); | ||
| } | ||
|
|
||
| bool Gui::update(std::mutex &m, std::condition_variable &cv) { | ||
| { | ||
| std::lock_guard<std::recursive_mutex> lock(mutex_); | ||
| lv_task_handler(); | ||
| } | ||
| std::unique_lock<std::mutex> lock(m); | ||
| cv.wait_for(lock, std::chrono::milliseconds(16)); | ||
| return false; // don't stop the task | ||
| } | ||
|
|
||
| void Gui::set_label_text(std::string_view text) { | ||
| std::lock_guard<std::recursive_mutex> lock(mutex_); | ||
| lv_label_set_text(label_, std::string(text).c_str()); | ||
| } | ||
|
|
||
| void Gui::move_cursor(int dx, int dy) { | ||
| std::lock_guard<std::recursive_mutex> lock(mutex_); | ||
| auto &vmu = espp::VmuPro::get(); | ||
| int width = vmu.rotated_display_width(); | ||
| int height = vmu.rotated_display_height(); | ||
| cursor_x_ = std::clamp(cursor_x_ + dx, CURSOR_RADIUS, width - CURSOR_RADIUS); | ||
| cursor_y_ = std::clamp(cursor_y_ + dy, CURSOR_RADIUS, height - CURSOR_RADIUS); | ||
| update_cursor(); | ||
| } | ||
|
|
||
| void Gui::update_cursor() { | ||
| lv_obj_set_pos(cursor_, cursor_x_ - CURSOR_RADIUS, cursor_y_ - CURSOR_RADIUS); | ||
| lv_obj_move_foreground(cursor_); | ||
| } | ||
|
|
||
| void Gui::draw_at_cursor() { | ||
| std::lock_guard<std::recursive_mutex> lock(mutex_); | ||
| draw_circle_impl(cursor_x_, cursor_y_, CIRCLE_RADIUS); | ||
| } | ||
|
|
||
| void Gui::next_rotation() { | ||
| std::lock_guard<std::recursive_mutex> lock(mutex_); | ||
| auto &vmu = espp::VmuPro::get(); | ||
| clear_circles_impl(); | ||
| auto rotation = lv_display_get_rotation(lv_display_get_default()); | ||
| rotation = static_cast<lv_display_rotation_t>((static_cast<int>(rotation) + 1) % 4); | ||
| lv_display_set_rotation(lv_display_get_default(), rotation); | ||
| // update the size of the screen-filling objects | ||
| lv_obj_set_size(background_, vmu.rotated_display_width(), vmu.rotated_display_height()); | ||
| lv_obj_set_size(circle_layer_, vmu.rotated_display_width(), vmu.rotated_display_height()); | ||
| lv_obj_align(circle_layer_, LV_ALIGN_CENTER, 0, 0); | ||
| lv_obj_invalidate(circle_layer_); | ||
| // re-center the cursor within the (possibly differently-sized) screen | ||
| cursor_x_ = vmu.rotated_display_width() / 2; | ||
| cursor_y_ = vmu.rotated_display_height() / 2; | ||
| update_cursor(); | ||
| } | ||
|
|
||
| void Gui::cycle_brightness() { | ||
| std::lock_guard<std::recursive_mutex> lock(mutex_); | ||
| brightness_index_ = (brightness_index_ + 1) % BRIGHTNESS_LEVELS.size(); | ||
| float new_brightness = BRIGHTNESS_LEVELS[brightness_index_]; | ||
| espp::VmuPro::get().brightness(new_brightness); | ||
| logger_.info("Set brightness to {:.0f}%", new_brightness); | ||
| } | ||
|
|
||
| void Gui::draw_circle_impl(int x, int y, int radius) { | ||
| lv_obj_move_foreground(circle_layer_); | ||
| lv_obj_move_foreground(cursor_); | ||
| Circle previous_circle = circles_[next_circle_index_]; | ||
| circles_[next_circle_index_] = {.x = x, .y = y, .radius = radius, .visible = true}; | ||
| next_circle_index_ = (next_circle_index_ + 1) % circles_.size(); | ||
| if (visible_circle_count_ < circles_.size()) { | ||
| visible_circle_count_++; | ||
| } | ||
| if (previous_circle.visible) { | ||
| invalidate_circle_area(previous_circle); | ||
| } | ||
| invalidate_circle_area(circles_[(next_circle_index_ + circles_.size() - 1) % circles_.size()]); | ||
| } | ||
|
|
||
| void Gui::clear_circles() { | ||
| std::lock_guard<std::recursive_mutex> lock(mutex_); | ||
| clear_circles_impl(); | ||
| } | ||
|
|
||
| void Gui::clear_circles_impl() { | ||
| for (auto &circle : circles_) { | ||
| if (circle.visible) { | ||
| invalidate_circle_area(circle); | ||
| } | ||
| circle.visible = false; | ||
| } | ||
| next_circle_index_ = 0; | ||
| visible_circle_count_ = 0; | ||
| } | ||
|
|
||
| void Gui::invalidate_circle_area(const Circle &circle) { | ||
| if (!circle_layer_ || circle.radius <= 0) { | ||
| return; | ||
| } | ||
| lv_area_t obj_coords; | ||
| lv_obj_get_coords(circle_layer_, &obj_coords); | ||
| lv_area_t coords = { | ||
| .x1 = static_cast<lv_coord_t>(obj_coords.x1 + circle.x - circle.radius), | ||
| .y1 = static_cast<lv_coord_t>(obj_coords.y1 + circle.y - circle.radius), | ||
| .x2 = static_cast<lv_coord_t>(obj_coords.x1 + circle.x + circle.radius - 1), | ||
| .y2 = static_cast<lv_coord_t>(obj_coords.y1 + circle.y + circle.radius - 1), | ||
| }; | ||
| lv_obj_invalidate_area(circle_layer_, &coords); | ||
| } | ||
|
|
||
| void Gui::draw_circle_layer(lv_event_t *e) { | ||
| const auto *gui = static_cast<const Gui *>(lv_event_get_user_data(e)); | ||
| if (!gui) { | ||
| return; | ||
| } | ||
| gui->draw_circles(e); | ||
| } | ||
|
|
||
| void Gui::draw_circles(lv_event_t *e) const { | ||
| if (visible_circle_count_ == 0) { | ||
| return; | ||
| } | ||
|
|
||
| auto *obj = static_cast<lv_obj_t *>(lv_event_get_current_target(e)); | ||
| auto *layer = lv_event_get_layer(e); | ||
| lv_area_t obj_coords; | ||
| lv_obj_get_coords(obj, &obj_coords); | ||
|
|
||
| lv_draw_rect_dsc_t rect_dsc; | ||
| lv_draw_rect_dsc_init(&rect_dsc); | ||
| rect_dsc.base.layer = layer; | ||
| rect_dsc.radius = LV_RADIUS_CIRCLE; | ||
| rect_dsc.bg_opa = LV_OPA_70; | ||
| rect_dsc.bg_color = lv_color_make(0, 255, 255); | ||
| rect_dsc.border_width = 0; | ||
| rect_dsc.outline_width = 0; | ||
| rect_dsc.shadow_width = 0; | ||
|
|
||
| for (const auto &circle : circles_) { | ||
| if (!circle.visible) { | ||
| continue; | ||
| } | ||
| lv_area_t coords = { | ||
| .x1 = static_cast<lv_coord_t>(obj_coords.x1 + circle.x - circle.radius), | ||
| .y1 = static_cast<lv_coord_t>(obj_coords.y1 + circle.y - circle.radius), | ||
| .x2 = static_cast<lv_coord_t>(obj_coords.x1 + circle.x + circle.radius - 1), | ||
| .y2 = static_cast<lv_coord_t>(obj_coords.y1 + circle.y + circle.radius - 1), | ||
| }; | ||
| lv_draw_rect(layer, &rect_dsc, &coords); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.