| #include "string.h" | |
| #include <map> | |
| #include <bitset> | |
| #include "32blit.h" | |
| #include "main.h" | |
| #include "sound.hpp" | |
| #include "display.hpp" | |
| #include "gpio.hpp" | |
| #include "file.hpp" | |
| #include "jpeg.hpp" | |
| #include "adc.h" | |
| #include "tim.h" | |
| #include "rng.h" | |
| #include "spi.h" | |
| #include "i2c.h" | |
| #include "i2c-msa301.h" | |
| #include "i2c-lis3dh.h" | |
| #include "i2c-bq24295.h" | |
| #include "fatfs.h" | |
| #include "quadspi.h" | |
| #include "usbd_core.h" | |
| #include "32blit.hpp" | |
| #include "engine/api_private.hpp" | |
| #include "graphics/color.hpp" | |
| #include "engine/running_average.hpp" | |
| #include "stdarg.h" | |
| using namespace blit; | |
| extern char __ltdc_start; | |
| extern char __fb_start; | |
| extern char itcm_text_start; | |
| extern char itcm_text_end; | |
| extern char itcm_data; | |
| extern USBD_HandleTypeDef hUsbDeviceHS; | |
| #define ADC_BUFFER_SIZE 32 | |
| __attribute__((section(".dma_data"))) ALIGN_32BYTES(__IO uint16_t adc1data[ADC_BUFFER_SIZE]); | |
| __attribute__((section(".dma_data"))) ALIGN_32BYTES(__IO uint16_t adc3data[ADC_BUFFER_SIZE]); | |
| FATFS filesystem; | |
| FRESULT SD_Error = FR_INVALID_PARAMETER; | |
| FRESULT SD_FileOpenError = FR_INVALID_PARAMETER; | |
| bool needs_render = true; | |
| bool exit_game = false; | |
| bool take_screenshot = false; | |
| uint32_t flip_cycle_count = 0; | |
| float volume_log_base = 2.0f; | |
| RunningAverage<float> battery_average(8); | |
| float battery = 0.0f; | |
| uint8_t battery_status = 0; | |
| uint8_t battery_fault = 0; | |
| uint16_t accel_address = LIS3DH_DEVICE_ADDRESS; | |
| const uint32_t long_press_exit_time = 1000; | |
| __attribute__((section(".persist"))) Persist persist; | |
| static bool (*do_tick)(uint32_t time) = blit::tick; | |
| // pointers to user code | |
| static bool (*user_tick)(uint32_t time) = nullptr; | |
| static void (*user_render)(uint32_t time) = nullptr; | |
| void DFUBoot(void) | |
| { | |
| // Set the special magic word value that's checked by the assembly entry Point upon boot | |
| // This will trigger a jump into DFU mode upon reboot | |
| *((uint32_t *)0x2001FFFC) = 0xCAFEBABE; // Special Key to End-of-RAM | |
| SCB_CleanDCache(); | |
| NVIC_SystemReset(); | |
| } | |
| static void init_api_shared() { | |
| // Reset button state, this prevents the user app immediately seeing the last button transition used to launch the game | |
| api.buttons.state = 0; | |
| api.buttons.pressed = 0; | |
| api.buttons.released = 0; | |
| // reset shared outputs | |
| api.vibration = 0.0f; | |
| api.LED = Pen(); | |
| } | |
| int blit_debugf(const char * psFormatString, va_list args) | |
| { | |
| return vprintf(psFormatString, args); | |
| } | |
| void blit_debug(std::string message) { | |
| printf(message.c_str()); | |
| screen.pen = Pen(255, 255, 255); | |
| screen.text(message, minimal_font, Point(0, 0)); | |
| } | |
| void enable_us_timer() | |
| { | |
| CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; | |
| DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; | |
| } | |
| uint32_t get_us_timer() | |
| { | |
| uint32_t uTicksPerUs = SystemCoreClock / 1000000; | |
| return DWT->CYCCNT/uTicksPerUs; | |
| } | |
| uint32_t get_max_us_timer() | |
| { | |
| uint32_t uTicksPerUs = SystemCoreClock / 1000000; | |
| return UINT32_MAX / uTicksPerUs; | |
| } | |
| std::string battery_vbus_status() { | |
| switch(battery_status >> 6){ | |
| case 0b00: // Unknown | |
| return "Unknown"; | |
| case 0b01: // USB Host | |
| return "USB Host"; | |
| case 0b10: // Adapter Port | |
| return "Adapter"; | |
| case 0b11: // OTG | |
| return "OTG"; | |
| } | |
| // unreachable | |
| return ""; | |
| } | |
| std::string battery_charge_status() { | |
| switch((battery_status >> 4) & 0b11){ | |
| case 0b00: // Not Charging | |
| return "Nope"; | |
| case 0b01: // Pre-charge | |
| return "Pre"; | |
| case 0b10: // Fast Charging | |
| return "Fast"; | |
| case 0b11: // Charge Done | |
| return "Done"; | |
| } | |
| // unreachable | |
| return ""; | |
| } | |
| static void do_render() { | |
| if(display::needs_render) { | |
| blit::render(blit::now()); | |
| display::enable_vblank_interrupt(); | |
| } | |
| } | |
| void render_yield() { | |
| do_render(); | |
| } | |
| void blit_tick() { | |
| if(exit_game) { | |
| if(blit_user_code_running()) { | |
| blit::LED.r = 0; | |
| blit_switch_execution(0); | |
| } | |
| } | |
| do_render(); | |
| blit_i2c_tick(); | |
| blit_process_input(); | |
| blit_update_led(); | |
| blit_update_vibration(); | |
| do_tick(blit::now()); | |
| } | |
| bool blit_sd_detected() { | |
| return HAL_GPIO_ReadPin(GPIOD, GPIO_PIN_11) == 1; | |
| } | |
| void hook_render(uint32_t time) { | |
| /* | |
| Replace blit::render = ::render; with blit::render = hook_render; | |
| and do silly on-screen debug stuff here for the great justice. | |
| */ | |
| ::render(time); | |
| blit::screen.pen = Pen(255, 255, 255); | |
| for(auto i = 0; i < ADC_BUFFER_SIZE; i++) { | |
| int x = i / 8; | |
| int y = i % 8; | |
| blit::screen.text(std::to_string(adc1data[i]), minimal_font, Point(x * 30, y * 10)); | |
| } | |
| } | |
| enum I2CState { | |
| DELAY, | |
| STOPPED, | |
| SEND_ACL, | |
| RECV_ACL, | |
| PROC_ACL, | |
| SEND_BAT, | |
| RECV_BAT, | |
| PROC_BAT | |
| }; | |
| RunningAverage<float> accel_x(8); | |
| RunningAverage<float> accel_y(8); | |
| RunningAverage<float> accel_z(8); | |
| I2CState i2c_state = SEND_ACL; | |
| uint8_t i2c_buffer[6] = {0}; | |
| uint8_t i2c_reg = 0; | |
| HAL_StatusTypeDef i2c_status = HAL_OK; | |
| uint32_t i2c_delay_until = 0; | |
| I2CState i2c_next_state = SEND_ACL; | |
| void blit_i2c_delay(uint16_t ms, I2CState state) { | |
| i2c_delay_until = HAL_GetTick() + ms; | |
| i2c_next_state = state; | |
| i2c_state = DELAY; | |
| } | |
| void blit_i2c_tick() { | |
| if(i2c_state == STOPPED) { | |
| return; | |
| } | |
| if(i2c_state == DELAY) { | |
| if(HAL_GetTick() >= i2c_delay_until){ | |
| i2c_state = i2c_next_state; | |
| } | |
| } | |
| if(HAL_I2C_GetState(&hi2c4) != HAL_I2C_STATE_READY){ | |
| return; | |
| } | |
| switch(i2c_state) { | |
| case STOPPED: | |
| case DELAY: | |
| break; | |
| case SEND_ACL: | |
| i2c_reg = is_beta_unit ? MSA301_X_ACCEL_RESISTER : (LIS3DH_OUT_X_L | LIS3DH_ADDR_AUTO_INC); | |
| i2c_status = HAL_I2C_Master_Transmit_IT(&hi2c4, accel_address, &i2c_reg, 1); | |
| if(i2c_status == HAL_OK){ | |
| i2c_state = RECV_ACL; | |
| } else { | |
| blit_i2c_delay(16, SEND_ACL); | |
| } | |
| break; | |
| case RECV_ACL: | |
| i2c_status = HAL_I2C_Master_Receive_IT(&hi2c4, accel_address, i2c_buffer, 6); | |
| if(i2c_status == HAL_OK){ | |
| i2c_state = PROC_ACL; | |
| } else { | |
| blit_i2c_delay(16, SEND_ACL); | |
| } | |
| break; | |
| case PROC_ACL: | |
| // LIS3DH & MSA301 - 12-bit left-justified | |
| accel_x.add(((int8_t)i2c_buffer[1] << 6) | (i2c_buffer[0] >> 2)); | |
| accel_y.add(((int8_t)i2c_buffer[3] << 6) | (i2c_buffer[2] >> 2)); | |
| accel_z.add(((int8_t)i2c_buffer[5] << 6) | (i2c_buffer[4] >> 2)); | |
| if(is_beta_unit){ | |
| blit::tilt = Vec3( | |
| accel_x.average(), | |
| accel_y.average(), | |
| -accel_z.average() | |
| ); | |
| } else { | |
| blit::tilt = Vec3( | |
| -accel_x.average(), | |
| -accel_y.average(), | |
| -accel_z.average() | |
| ); | |
| } | |
| blit::tilt.normalize(); | |
| i2c_state = SEND_BAT; | |
| break; | |
| case SEND_BAT: | |
| i2c_reg = BQ24295_SYS_STATUS_REGISTER; | |
| HAL_I2C_Master_Transmit_IT(&hi2c4, BQ24295_DEVICE_ADDRESS, &i2c_reg, 1); | |
| i2c_state = RECV_BAT; | |
| break; | |
| case RECV_BAT: | |
| HAL_I2C_Master_Receive_IT(&hi2c4, BQ24295_DEVICE_ADDRESS, i2c_buffer, 2); | |
| i2c_state = PROC_BAT; | |
| break; | |
| case PROC_BAT: | |
| battery_status = i2c_buffer[0]; | |
| battery_fault = i2c_buffer[1]; | |
| blit_i2c_delay(16, SEND_ACL); | |
| break; | |
| } | |
| } | |
| void blit_update_volume() { | |
| blit::volume = (uint16_t)(65535.0f * log(1.0f + (volume_log_base - 1.0f) * persist.volume) / log(volume_log_base)); | |
| } | |
| static void save_screenshot() { | |
| int index = 0; | |
| char buf[100]; | |
| do { | |
| snprintf(buf, 100, "screenshot%i.bmp", index); | |
| if(!::file_exists(buf)) | |
| break; | |
| index++; | |
| } while(true); | |
| screen.save(buf); | |
| } | |
| void blit_init() { | |
| // enable backup sram | |
| __HAL_RCC_RTC_ENABLE(); | |
| __HAL_RCC_BKPRAM_CLK_ENABLE(); | |
| HAL_PWR_EnableBkUpAccess(); | |
| HAL_PWREx_EnableBkUpReg(); | |
| // need to wit for sram, I tried a few things I found on the net to wait | |
| // based on PWR flags but none seemed to work, a simple delay does work! | |
| HAL_Delay(5); | |
| if(persist.magic_word != persistence_magic_word) { | |
| // Set persistent defaults if the magic word does not match | |
| persist.magic_word = persistence_magic_word; | |
| persist.volume = 0.5f; | |
| persist.backlight = 1.0f; | |
| persist.selected_menu_item = 0; | |
| persist.reset_target = prtFirmware; | |
| persist.reset_error = false; | |
| persist.last_game_offset = 0; | |
| } | |
| init_api_shared(); | |
| blit_update_volume(); | |
| // enable cycle counting | |
| CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; | |
| DWT->CYCCNT = 0; | |
| DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; | |
| HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc1data, ADC_BUFFER_SIZE); | |
| HAL_ADC_Start_DMA(&hadc3, (uint32_t *)adc3data, ADC_BUFFER_SIZE); | |
| CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; | |
| DWT->CYCCNT = 0; | |
| DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; | |
| f_mount(&filesystem, "", 1); // this shouldn't be necessary here right? | |
| if(is_beta_unit){ | |
| msa301_init(&hi2c4, MSA301_CONTROL2_POWR_MODE_NORMAL, 0x00, MSA301_CONTROL1_ODR_62HZ5); | |
| accel_address = MSA301_DEVICE_ADDRESS; | |
| } else { | |
| lis3dh_init(&hi2c4); | |
| } | |
| bq24295_init(&hi2c4); | |
| blit::api.debug = blit_debug; | |
| blit::api.debugf = blit_debugf; | |
| blit::api.now = HAL_GetTick; | |
| blit::api.random = HAL_GetRandom; | |
| blit::api.set_screen_mode = display::set_screen_mode; | |
| blit::api.set_screen_palette = display::set_screen_palette; | |
| display::set_screen_mode(blit::lores); | |
| blit::update = ::update; | |
| blit::render = ::render; | |
| blit::init = ::init; | |
| blit::api.open_file = ::open_file; | |
| blit::api.read_file = ::read_file; | |
| blit::api.write_file = ::write_file; | |
| blit::api.close_file = ::close_file; | |
| blit::api.get_file_length = ::get_file_length; | |
| blit::api.list_files = ::list_files; | |
| blit::api.file_exists = ::file_exists; | |
| blit::api.directory_exists = ::directory_exists; | |
| blit::api.create_directory = ::create_directory; | |
| blit::api.rename_file = ::rename_file; | |
| blit::api.remove_file = ::remove_file; | |
| blit::api.enable_us_timer = ::enable_us_timer; | |
| blit::api.get_us_timer = ::get_us_timer; | |
| blit::api.get_max_us_timer = ::get_max_us_timer; | |
| blit::api.decode_jpeg_buffer = blit_decode_jpeg_buffer; | |
| blit::api.decode_jpeg_file = blit_decode_jpeg_file; | |
| display::init(); | |
| blit::init(); | |
| } | |
| /* | |
| Menu Items | |
| */ | |
| enum MenuItem { | |
| BACKLIGHT, | |
| VOLUME, | |
| SCREENSHOT, | |
| DFU, | |
| SHIPPING, | |
| SWITCH_EXE, | |
| LAST_COUNT // leave me last pls | |
| }; | |
| //pinched from http://www.cplusplus.com/forum/beginner/41790/ | |
| inline MenuItem& operator++(MenuItem& eDOW, int) { | |
| const int i = static_cast<int>(eDOW) + 1; | |
| eDOW = static_cast<MenuItem>((i) % (LAST_COUNT)); | |
| return eDOW; | |
| } | |
| inline MenuItem& operator--(MenuItem& type, int) { | |
| const int i = static_cast<int>(type)-1; | |
| if (i < 0) { // Check whether to cycle to last item if number goes below 0 | |
| type = static_cast<MenuItem>(LAST_COUNT - 1); | |
| } else { // Else set it to current number -1 | |
| type = static_cast<MenuItem>((i) % LAST_COUNT); | |
| } | |
| return type; | |
| } | |
| std::string menu_name (MenuItem item) { | |
| switch (item) { | |
| case BACKLIGHT: return "Backlight"; | |
| case VOLUME: return "Volume"; | |
| case SCREENSHOT: return "Take Screenshot"; | |
| case DFU: return "DFU Mode"; | |
| case SHIPPING: return "Power Off"; | |
| case SWITCH_EXE: return blit_user_code_running() ? "Exit Game" : "Launch Game"; | |
| case LAST_COUNT: return ""; | |
| }; | |
| return ""; | |
| } | |
| MenuItem menu_item = BACKLIGHT; | |
| static const Pen menu_colours[]{ | |
| {0}, | |
| { 30, 30, 50, 200}, // background | |
| {255, 255, 255}, // foreground | |
| { 40, 40, 60}, // bar background | |
| { 50, 50, 70}, // selected item background | |
| {255, 128, 0}, // battery unknown | |
| { 0, 255, 0}, // battery usb host/adapter port | |
| {255, 0, 0}, // battery otg | |
| {100, 100, 255}, // battery charging | |
| }; | |
| static constexpr int num_menu_colours = sizeof(menu_colours) / sizeof(Pen); | |
| static Pen menu_saved_colours[num_menu_colours]; | |
| Point menu_title_origin (MenuItem item) { return Point(5, item * 10 + 20); } | |
| Point press_a_origin (MenuItem item, int screen_width) { return Point(screen_width/2, item * 10 + 20); } | |
| Rect menu_item_frame (MenuItem item, int screen_width) { return Rect (0, item * 10 + 19, screen_width, 9); } | |
| void blit_menu_update(uint32_t time) { | |
| if(blit::buttons.pressed & blit::Button::DPAD_UP) { | |
| menu_item --; | |
| } else if (blit::buttons.pressed & blit::Button::DPAD_DOWN) { | |
| menu_item ++; | |
| } else { | |
| bool button_a = blit::buttons.released & blit::Button::A; | |
| switch(menu_item) { | |
| case BACKLIGHT: | |
| if (blit::buttons & blit::Button::DPAD_LEFT) { | |
| persist.backlight -= 1.0f / 256.0f; | |
| } else if (blit::buttons & blit::Button::DPAD_RIGHT) { | |
| persist.backlight += 1.0f / 256.0f; | |
| } | |
| persist.backlight = std::fmin(1.0f, std::fmax(0.0f, persist.backlight)); | |
| break; | |
| case VOLUME: | |
| if (blit::buttons & blit::Button::DPAD_LEFT) { | |
| persist.volume -= 1.0f / 256.0f; | |
| } else if (blit::buttons & blit::Button::DPAD_RIGHT) { | |
| persist.volume += 1.0f / 256.0f; | |
| } | |
| persist.volume = std::fmin(1.0f, std::fmax(0.0f, persist.volume)); | |
| blit_update_volume(); | |
| break; | |
| case SCREENSHOT: | |
| if(button_a) | |
| take_screenshot = true; | |
| break; | |
| case DFU: | |
| if(button_a){ | |
| DFUBoot(); | |
| } | |
| break; | |
| case SHIPPING: | |
| if(button_a){ | |
| bq24295_enable_shipping_mode(&hi2c4); | |
| } | |
| break; | |
| case SWITCH_EXE: | |
| if(button_a){ | |
| blit_switch_execution(persist.last_game_offset); | |
| } | |
| break; | |
| case LAST_COUNT: | |
| break; | |
| } | |
| } | |
| } | |
| void blit_menu_render(uint32_t time) { | |
| if(user_render) | |
| user_render(time); | |
| else | |
| ::render(time); | |
| // save screenshot before we render the menu over it | |
| if(take_screenshot) { | |
| // restore game colours | |
| if(screen.format == PixelFormat::P) | |
| set_screen_palette(menu_saved_colours, num_menu_colours); | |
| save_screenshot(); | |
| take_screenshot = false; | |
| if(screen.format == PixelFormat::P) | |
| set_screen_palette(menu_colours, num_menu_colours); | |
| } | |
| const int screen_width = blit::screen.bounds.w; | |
| const int screen_height = blit::screen.bounds.h; | |
| auto pallette_col = [](int index) {return screen.format == PixelFormat::P ? Pen(index) : menu_colours[index];}; | |
| const Pen menu_bg_colour = pallette_col(1); | |
| const Pen foreground_colour = pallette_col(2); | |
| const Pen bar_background_color = pallette_col(3); | |
| const Pen selected_item_bg_colour = pallette_col(4); | |
| screen.pen = menu_bg_colour; | |
| screen.clear(); | |
| screen.pen = foreground_colour; | |
| screen.text("System Menu", minimal_font, Point(5, 5)); | |
| screen.text( | |
| "Charge: " + battery_charge_status() + | |
| " VBus: " + battery_vbus_status() + | |
| " Voltage: " + std::to_string(int(battery)) + "." + std::to_string(int((battery - int(battery)) * 10.0f)) + "v", | |
| minimal_font, Point(0, screen_height - 10)); | |
| /* | |
| // Raw register values can be displayed with a fixed-width font using std::bitset<8> for debugging | |
| screen.text( | |
| "Fault: " + std::bitset<8>(battery_fault).to_string() + | |
| " Status: " + std::bitset<8>(battery_status).to_string(), | |
| minimal_font, Point(0, screen_height - 10), false); | |
| */ | |
| screen.text("bat", minimal_font, Point(screen_width / 2, 5)); | |
| uint16_t battery_meter_width = 55; | |
| battery_meter_width = float(battery_meter_width) * (battery - 3.0f) / 1.1f; | |
| battery_meter_width = std::max((uint16_t)0, std::min((uint16_t)55, battery_meter_width)); | |
| screen.pen = bar_background_color; | |
| screen.rectangle(Rect((screen_width / 2) + 20, 6, 55, 5)); | |
| switch(battery_status >> 6){ | |
| case 0b00: // Unknown | |
| screen.pen = pallette_col(5); | |
| break; | |
| case 0b01: // USB Host | |
| screen.pen = pallette_col(6); | |
| break; | |
| case 0b10: // Adapter Port | |
| screen.pen = pallette_col(6); | |
| break; | |
| case 0b11: // OTG | |
| screen.pen = pallette_col(7); | |
| break; | |
| } | |
| screen.rectangle(Rect((screen_width / 2) + 20, 6, battery_meter_width, 5)); | |
| uint8_t battery_charge_status = (battery_status >> 4) & 0b11; | |
| if(battery_charge_status == 0b01 || battery_charge_status == 0b10){ | |
| uint16_t battery_fill_width = uint32_t(time / 500.0f) % battery_meter_width; | |
| battery_fill_width = std::max((uint16_t)0, std::min((uint16_t)battery_meter_width, battery_fill_width)); | |
| screen.pen = pallette_col(8); | |
| screen.rectangle(Rect((screen_width / 2) + 20, 6, battery_fill_width, 5)); | |
| } | |
| // Horizontal Line | |
| screen.pen = foreground_colour; | |
| screen.h_span(Point(0, 15), screen_width); | |
| // Selected item | |
| screen.pen = selected_item_bg_colour; | |
| screen.rectangle(menu_item_frame(menu_item, screen_width)); | |
| // Menu rows | |
| for (int i = BACKLIGHT; i < LAST_COUNT; i++) { | |
| const MenuItem item = (MenuItem)i; | |
| screen.pen = foreground_colour; | |
| screen.text(menu_name(item), minimal_font, menu_title_origin(item)); | |
| switch (item) { | |
| case BACKLIGHT: | |
| screen.pen = bar_background_color; | |
| screen.rectangle(Rect(screen_width / 2, 21, 75, 5)); | |
| screen.pen = foreground_colour; | |
| screen.rectangle(Rect(screen_width / 2, 21, 75 * persist.backlight, 5)); | |
| break; | |
| case VOLUME: | |
| screen.pen = bar_background_color; | |
| screen.rectangle(Rect(screen_width / 2, 31, 75, 5)); | |
| screen.pen = foreground_colour; | |
| screen.rectangle(Rect(screen_width / 2, 31, 75 * persist.volume, 5)); | |
| break; | |
| default: | |
| screen.pen = foreground_colour; | |
| screen.text("Press A", minimal_font, press_a_origin(item, screen_width)); | |
| break; | |
| } | |
| } | |
| // Bottom horizontal Line | |
| screen.pen = foreground_colour; | |
| screen.h_span(Point(0, screen_height - 15), screen_width); | |
| } | |
| void blit_menu() { | |
| if(blit::update == blit_menu_update && do_tick == blit::tick) { | |
| if (user_tick) { | |
| // user code was running | |
| do_tick = user_tick; | |
| blit::render = user_render; | |
| } else { | |
| blit::update = ::update; | |
| blit::render = ::render; | |
| } | |
| // restore game colours | |
| if(screen.format == PixelFormat::P) | |
| set_screen_palette(menu_saved_colours, num_menu_colours); | |
| } | |
| else | |
| { | |
| blit::update = blit_menu_update; | |
| blit::render = blit_menu_render; | |
| do_tick = blit::tick; | |
| if(screen.format == PixelFormat::P) { | |
| memcpy(menu_saved_colours, screen.palette, num_menu_colours * sizeof(Pen)); | |
| set_screen_palette(menu_colours, num_menu_colours); | |
| } | |
| } | |
| } | |
| void blit_update_vibration() { | |
| __HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_1, blit::vibration * 2000.0f); | |
| } | |
| void blit_update_led() { | |
| // RED Led | |
| float compare_r = (blit::LED.r * 10000) / 255; | |
| __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_3, compare_r); | |
| // GREEN Led | |
| float compare_g = (blit::LED.g * 10000) / 255; | |
| __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_4, compare_g); | |
| // BLUE Led | |
| float compare_b = (blit::LED.b * 10000) / 255; | |
| __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_2, compare_b); | |
| // Backlight | |
| __HAL_TIM_SetCompare(&htim15, TIM_CHANNEL_1, 962 - (962 * persist.backlight)); | |
| } | |
| void HAL_ADC_ErrorCallback(ADC_HandleTypeDef* hadc){ | |
| } | |
| void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc){ | |
| if(hadc->Instance == ADC1) { | |
| SCB_InvalidateDCache_by_Addr((uint32_t *) &adc1data[0], ADC_BUFFER_SIZE); | |
| } else if (hadc->Instance == ADC3) { | |
| SCB_InvalidateDCache_by_Addr((uint32_t *) &adc3data[0], ADC_BUFFER_SIZE); | |
| } | |
| } | |
| void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { | |
| if(hadc->Instance == ADC1) { | |
| SCB_InvalidateDCache_by_Addr((uint32_t *) &adc1data[ADC_BUFFER_SIZE / 2], ADC_BUFFER_SIZE / 2); | |
| } else if (hadc->Instance == ADC3) { | |
| SCB_InvalidateDCache_by_Addr((uint32_t *) &adc3data[ADC_BUFFER_SIZE / 2], ADC_BUFFER_SIZE / 2); | |
| } | |
| } | |
| void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { | |
| if(htim == &htim2) { | |
| bool pressed = !HAL_GPIO_ReadPin(BUTTON_MENU_GPIO_Port, BUTTON_MENU_Pin); | |
| if(pressed) { | |
| exit_game = true; | |
| } | |
| HAL_TIM_Base_Stop(&htim2); | |
| HAL_TIM_Base_Stop_IT(&htim2); | |
| } | |
| } | |
| void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { | |
| bool pressed = !HAL_GPIO_ReadPin(BUTTON_MENU_GPIO_Port, BUTTON_MENU_Pin); | |
| if(pressed) { | |
| /* | |
| The timer will generate a spurious interrupt as soon as it's enabled- apparently to load the compare value. | |
| We disable interrupts and clear this early interrupt flag before re-enabling them so that the *real* | |
| interrupt can fire. | |
| */ | |
| if(!((&htim2)->Instance->CR1 & TIM_CR1_CEN)){ | |
| HAL_NVIC_DisableIRQ(TIM2_IRQn); | |
| __HAL_TIM_SetCounter(&htim2, 0); | |
| __HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_1, long_press_exit_time * 10); // press-to-reset-time | |
| HAL_TIM_Base_Start(&htim2); | |
| HAL_TIM_Base_Start_IT(&htim2); | |
| __HAL_TIM_CLEAR_FLAG(&htim2, TIM_SR_UIF); | |
| HAL_NVIC_EnableIRQ(TIM2_IRQn); | |
| } | |
| } else { | |
| if(__HAL_TIM_GetCounter(&htim2) > 200){ // 20ms debounce time | |
| // TODO is it a good idea to swap out the render/update functions potentially in the middle of a loop? | |
| // We were more or less doing this before by handling the menu update between render/update so perhaps it's mostly fine. | |
| blit_menu(); | |
| HAL_TIM_Base_Stop(&htim2); | |
| HAL_TIM_Base_Stop_IT(&htim2); | |
| __HAL_TIM_SetCounter(&htim2, 0); | |
| } | |
| } | |
| } | |
| #define ACCEL_OVER_SAMPLE 16 | |
| uint8_t tilt_sample_offset = 0; | |
| int16_t acceleration_data_buffer[3 * ACCEL_OVER_SAMPLE] = {0}; | |
| void blit_disable_ADC() | |
| { | |
| // TODO: Flesh this out if it's still necessary in interrupt-driven ADC mode | |
| return; | |
| } | |
| void blit_enable_ADC() | |
| { | |
| // TODO: Flesh this out if it's still necessary in interrupt-driven ADC mode | |
| return; | |
| } | |
| void blit_process_input() { | |
| bool joystick_button = false; | |
| // Read buttons | |
| blit::buttons = | |
| (!HAL_GPIO_ReadPin(DPAD_UP_GPIO_Port, DPAD_UP_Pin) ? blit::DPAD_UP : 0) | | |
| (!HAL_GPIO_ReadPin(DPAD_DOWN_GPIO_Port, DPAD_DOWN_Pin) ? blit::DPAD_DOWN : 0) | | |
| (!HAL_GPIO_ReadPin(DPAD_LEFT_GPIO_Port, DPAD_LEFT_Pin) ? blit::DPAD_LEFT : 0) | | |
| (!HAL_GPIO_ReadPin(DPAD_RIGHT_GPIO_Port, DPAD_RIGHT_Pin) ? blit::DPAD_RIGHT : 0) | | |
| (!HAL_GPIO_ReadPin(BUTTON_A_GPIO_Port, BUTTON_A_Pin) ? blit::A : 0) | | |
| (!HAL_GPIO_ReadPin(BUTTON_B_GPIO_Port, BUTTON_B_Pin) ? blit::B : 0) | | |
| (!HAL_GPIO_ReadPin(BUTTON_X_GPIO_Port, BUTTON_X_Pin) ? blit::X : 0) | | |
| (!HAL_GPIO_ReadPin(BUTTON_Y_GPIO_Port, BUTTON_Y_Pin) ? blit::Y : 0) | | |
| (HAL_GPIO_ReadPin(BUTTON_HOME_GPIO_Port, BUTTON_HOME_Pin) ? blit::HOME : 0) | // INVERTED LOGIC! | |
| (!HAL_GPIO_ReadPin(BUTTON_MENU_GPIO_Port, BUTTON_MENU_Pin) ? blit::MENU : 0) | | |
| (!HAL_GPIO_ReadPin(JOYSTICK_BUTTON_GPIO_Port, JOYSTICK_BUTTON_Pin) ? blit::JOYSTICK : 0); | |
| // Process ADC readings | |
| int joystick_x = (adc1data[0] >> 1) - 16384; | |
| joystick_x = std::max(-8192, std::min(8192, joystick_x)); | |
| if(joystick_x < -1024) { | |
| joystick_x += 1024; | |
| } | |
| else if(joystick_x > 1024) { | |
| joystick_x -= 1024; | |
| } else { | |
| joystick_x = 0; | |
| } | |
| blit::joystick.x = joystick_x / 7168.0f; | |
| int joystick_y = (adc1data[1] >> 1) - 16384; | |
| joystick_y = std::max(-8192, std::min(8192, joystick_y)); | |
| if(joystick_y < -1024) { | |
| joystick_y += 1024; | |
| } | |
| else if(joystick_y > 1024) { | |
| joystick_y -= 1024; | |
| } else { | |
| joystick_y = 0; | |
| } | |
| blit::joystick.y = -joystick_y / 7168.0f; | |
| blit::hack_left = (adc3data[0] >> 1) / 32768.0f; | |
| blit::hack_right = (adc3data[1] >> 1) / 32768.0f; | |
| battery_average.add(6.6f * adc3data[2] / 65535.0f); | |
| battery = battery_average.average(); | |
| } | |
| char *get_fr_err_text(FRESULT err){ | |
| switch(err){ | |
| case FR_OK: | |
| return "OK"; | |
| case FR_DISK_ERR: | |
| return "DISK_ERR"; | |
| case FR_INT_ERR: | |
| return "INT_ERR"; | |
| case FR_NOT_READY: | |
| return "NOT_READY"; | |
| case FR_NO_FILE: | |
| return "NO_FILE"; | |
| case FR_NO_PATH: | |
| return "NO_PATH"; | |
| case FR_INVALID_NAME: | |
| return "INVALID_NAME"; | |
| case FR_DENIED: | |
| return "DENIED"; | |
| case FR_EXIST: | |
| return "EXIST"; | |
| case FR_INVALID_OBJECT: | |
| return "INVALID_OBJECT"; | |
| case FR_WRITE_PROTECTED: | |
| return "WRITE_PROTECTED"; | |
| case FR_INVALID_DRIVE: | |
| return "INVALID_DRIVE"; | |
| case FR_NOT_ENABLED: | |
| return "NOT_ENABLED"; | |
| case FR_NO_FILESYSTEM: | |
| return "NO_FILESYSTEM"; | |
| case FR_MKFS_ABORTED: | |
| return "MKFS_ABORTED"; | |
| case FR_TIMEOUT: | |
| return "TIMEOUT"; | |
| case FR_LOCKED: | |
| return "LOCKED"; | |
| case FR_NOT_ENOUGH_CORE: | |
| return "NOT_ENOUGH_CORE"; | |
| case FR_TOO_MANY_OPEN_FILES: | |
| return "TOO_MANY_OPEN_FILES"; | |
| case FR_INVALID_PARAMETER: | |
| return "INVALID_PARAMETER"; | |
| default: | |
| return "INVALID_ERR_CODE"; | |
| } | |
| } | |
| // blit_switch_execution | |
| // | |
| // Switches execution to new location defined by EXTERNAL_LOAD_ADDRESS | |
| // EXTERNAL_LOAD_ADDRESS is the start of the Vector Table location | |
| // | |
| typedef void (*pFunction)(void); | |
| pFunction JumpToApplication; | |
| typedef void(*renderFunction)(uint32_t); | |
| typedef bool(*tickFunction)(uint32_t); | |
| void blit_switch_execution(uint32_t address) | |
| { | |
| if(blit_user_code_running()) | |
| persist.reset_target = prtFirmware; | |
| else | |
| persist.reset_target = prtGame; | |
| init_api_shared(); | |
| // returning from game running on top of the firmware | |
| if(user_tick) { | |
| user_tick = nullptr; | |
| user_render = nullptr; | |
| blit::render = ::render; | |
| blit::update = ::update; | |
| do_tick = blit::tick; | |
| // TODO: may be possible to return to the menu without a hard reset but currently flashing doesn't work | |
| SCB_CleanDCache(); | |
| NVIC_SystemReset(); | |
| } | |
| // switch to user app in external flash | |
| if(EXTERNAL_LOAD_ADDRESS >= 0x90000000) { | |
| qspi_enable_memorymapped_mode(); | |
| auto app_ptr = ((__IO uint32_t*) (EXTERNAL_LOAD_ADDRESS + address)); | |
| uint32_t magic = app_ptr[0]; | |
| if(magic == 0x54494C42 /*BLIT*/) { | |
| persist.last_game_offset = address; | |
| pFunction init = (pFunction) app_ptr[3]; | |
| init(); | |
| blit::render = user_render = (renderFunction) app_ptr[1]; | |
| do_tick = user_tick = (tickFunction) app_ptr[2]; | |
| return; | |
| } | |
| // anything flashed at a non-zero offset should have a valid header | |
| else if(address != 0) | |
| return; | |
| } | |
| // old-style soft-reset to app with linked HAL | |
| // left for compatibility/testing | |
| // Stop the ADC DMA | |
| HAL_ADC_Stop_DMA(&hadc1); | |
| HAL_ADC_Stop_DMA(&hadc3); | |
| // Stop the audio | |
| HAL_TIM_Base_Stop_IT(&htim6); | |
| HAL_DAC_Stop(&hdac1, DAC_CHANNEL_2); | |
| // Stop system button timer | |
| HAL_TIM_Base_Stop_IT(&htim2); | |
| // stop USB | |
| USBD_Stop(&hUsbDeviceHS); | |
| // Disable all the interrupts... just to be sure | |
| HAL_NVIC_DisableIRQ(LTDC_IRQn); | |
| HAL_NVIC_DisableIRQ(ADC_IRQn); | |
| HAL_NVIC_DisableIRQ(ADC3_IRQn); | |
| HAL_NVIC_DisableIRQ(DMA1_Stream0_IRQn); | |
| HAL_NVIC_DisableIRQ(DMA1_Stream1_IRQn); | |
| HAL_NVIC_DisableIRQ(TIM6_DAC_IRQn); | |
| HAL_NVIC_DisableIRQ(OTG_HS_IRQn); | |
| HAL_NVIC_DisableIRQ(EXTI9_5_IRQn); | |
| HAL_NVIC_DisableIRQ(TIM2_IRQn); | |
| volatile uint32_t uAddr = EXTERNAL_LOAD_ADDRESS; | |
| /* Disable I-Cache */ | |
| SCB_DisableICache(); | |
| /* Disable D-Cache */ | |
| SCB_DisableDCache(); | |
| /* Disable Systick interrupt */ | |
| SysTick->CTRL = 0; | |
| /* Initialize user application's Stack Pointer & Jump to user application */ | |
| JumpToApplication = (pFunction) (*(__IO uint32_t*) (EXTERNAL_LOAD_ADDRESS + 4)); | |
| __set_MSP(*(__IO uint32_t*) EXTERNAL_LOAD_ADDRESS); | |
| JumpToApplication(); | |
| /* We should never get here as execution is now on user application */ | |
| while(1) | |
| { | |
| } | |
| } | |
| bool blit_user_code_running() { | |
| // running fully linked code from ext flash | |
| if(APPLICATION_VTOR == 0x90000000) | |
| return true; | |
| // loaded user-only game from flash | |
| return user_tick != nullptr; | |
| } | |
| void blit_reset_with_error() { | |
| persist.reset_error = true; | |
| SCB_CleanDCache(); | |
| NVIC_SystemReset(); | |
| } |