Просмотр исходного кода

Initial commit as standalone repo

Yukai Li 3 лет назад
Сommit
4d1c54d1af
16 измененных файлов с 560 добавлено и 0 удалено
  1. 3 0
      .gitmodules
  2. 25 0
      README.md
  3. 21 0
      application.fam
  4. 138 0
      hal.c
  5. 35 0
      hal_types.h
  6. BIN
      icons/icon_0.png
  7. BIN
      icons/icon_1.png
  8. BIN
      icons/icon_2.png
  9. BIN
      icons/icon_3.png
  10. BIN
      icons/icon_4.png
  11. BIN
      icons/icon_5.png
  12. BIN
      icons/icon_6.png
  13. BIN
      icons/icon_7.png
  14. 1 0
      lib/tamalib
  15. 38 0
      tama.h
  16. 299 0
      tamagotchi_p1.c

+ 3 - 0
.gitmodules

@@ -0,0 +1,3 @@
+[submodule "lib/tamalib"]
+	path = lib/tamalib
+	url = https://github.com/GMMan/tamalib.git

+ 25 - 0
README.md

@@ -0,0 +1,25 @@
+Tamagotchi P1 Emulator for Flipper Zero
+=======================================
+
+This is a Tamagotchi P1 Emulator app for Flipper Zero, based on [TamaLIB](https://github.com/jcrona/tamalib/).
+
+How to play
+-----------
+Create a `tama_p1` folder in your microSD card, and put the ROM as `rom.bin`.
+Left button is A, OK is B, and right button is C. Hold the back button to exit.
+There is currently no saving, so your progress will be reset when you exit the
+app.
+
+Implemented
+-----------
+- Basic emulation
+- Input
+- Sound
+
+To-do
+-----
+- Saving/loading
+  - Multiple slots?
+- In-game reset
+- Test mode?
+- Volume adjustment

+ 21 - 0
application.fam

@@ -0,0 +1,21 @@
+App(
+    appid="tamagotchi_p1",
+    name="Tamagotchi",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="tamagotchi_p1_app",
+    cdefines=["APP_TAMAGOTCHI_P1"],
+    requires=["gui", "storage"],
+    stack_size=1 * 1024,
+    icon="A_Plugins_14",
+    fap_version=(0,1),
+    fap_category="Games",
+    fap_description="Tamagotchi P1 emulator",
+    fap_author="cyanic",
+    fap_weburl="https://github.com/GMMan/flipperzero-tamagotch-p1",
+    fap_private_libs=[
+        Lib(
+            name="tamalib",
+            cflags=["-Wno-unused-parameter"],
+        ),
+    ]
+)

+ 138 - 0
hal.c

@@ -0,0 +1,138 @@
+#include <furi.h>
+#include <furi_hal.h>
+#include <stdlib.h>
+#include <m-string.h>
+#include <stm32wbxx_ll_tim.h>
+#include "tama.h"
+
+#define TAG_HAL "TamaLIB"
+
+static void* tamagotchi_p1_hal_malloc(u32_t size) {
+    return malloc(size);
+}
+
+static void tamagotchi_p1_hal_free(void* ptr) {
+    free(ptr);
+}
+
+static void tamagotchi_p1_hal_halt(void) {
+    g_ctx->halted = true;
+}
+
+static bool_t tamagotchi_p1_hal_is_log_enabled(log_level_t level) {
+    switch(level) {
+    case LOG_ERROR:
+        return true;
+    case LOG_INFO:
+        return true;
+    case LOG_MEMORY:
+        return false;
+    case LOG_CPU:
+        return false;
+    default:
+        return false;
+    }
+}
+
+static void tamagotchi_p1_hal_log(log_level_t level, char* buff, ...) {
+    if(!tamagotchi_p1_hal_is_log_enabled(level)) return;
+
+    string_t string;
+    va_list args;
+    va_start(args, buff);
+    string_init_vprintf(string, buff, args);
+    va_end(args);
+
+    switch(level) {
+    case LOG_ERROR:
+        FURI_LOG_E(TAG_HAL, "%s", string_get_cstr(string));
+        break;
+    case LOG_INFO:
+        FURI_LOG_I(TAG_HAL, "%s", string_get_cstr(string));
+        break;
+    case LOG_MEMORY:
+    case LOG_CPU:
+    default:
+        FURI_LOG_D(TAG_HAL, "%s", string_get_cstr(string));
+        break;
+    }
+
+    string_clear(string);
+}
+
+static void tamagotchi_p1_hal_sleep_until(timestamp_t ts) {
+    while(true) {
+        uint32_t count = LL_TIM_GetCounter(TIM2);
+        uint32_t delay = ts - count;
+        // FURI_LOG_D(TAG, "delay: %x", delay);
+        // Stolen from furi_delay_until_tick
+        if(delay != 0 && 0 == (delay >> (8 * sizeof(uint32_t) - 1))) {
+            // Not the best place to release mutex, but this is the only place we know whether
+            // we're ahead or behind, otherwise around the step call we'll always have to
+            // delay a tick and run more and more behind.
+            furi_mutex_release(g_state_mutex);
+            furi_delay_tick(1);
+            while(furi_mutex_acquire(g_state_mutex, FuriWaitForever) != FuriStatusOk)
+                furi_delay_tick(1);
+        } else {
+            break;
+        }
+    }
+}
+
+static timestamp_t tamagotchi_p1_hal_get_timestamp(void) {
+    return LL_TIM_GetCounter(TIM2);
+}
+
+static void tamagotchi_p1_hal_update_screen(void) {
+    // Do nothing, covered by main loop
+}
+
+static void tamagotchi_p1_hal_set_lcd_matrix(u8_t x, u8_t y, bool_t val) {
+    if(val)
+        g_ctx->framebuffer[y] |= 1 << x;
+    else
+        g_ctx->framebuffer[y] &= ~(1 << x);
+}
+
+static void tamagotchi_p1_hal_set_lcd_icon(u8_t icon, bool_t val) {
+    if(val)
+        g_ctx->icons |= 1 << icon;
+    else
+        g_ctx->icons &= ~(1 << icon);
+}
+
+static void tamagotchi_p1_hal_play_frequency(bool_t en) {
+    if(en)
+        furi_hal_speaker_start(g_ctx->frequency, 0.5f);
+    else
+        furi_hal_speaker_stop();
+
+    g_ctx->buzzer_on = en;
+}
+
+static void tamagotchi_p1_hal_set_frequency(u32_t freq) {
+    g_ctx->frequency = freq / 10.0F;
+    if(g_ctx->buzzer_on) tamagotchi_p1_hal_play_frequency(true);
+}
+
+static int tamagotchi_p1_hal_handler(void) {
+    // Do nothing
+    return 0;
+}
+
+void tamagotchi_p1_hal_init(hal_t* hal) {
+    hal->malloc = tamagotchi_p1_hal_malloc;
+    hal->free = tamagotchi_p1_hal_free;
+    hal->halt = tamagotchi_p1_hal_halt;
+    hal->is_log_enabled = tamagotchi_p1_hal_is_log_enabled;
+    hal->log = tamagotchi_p1_hal_log;
+    hal->sleep_until = tamagotchi_p1_hal_sleep_until;
+    hal->get_timestamp = tamagotchi_p1_hal_get_timestamp;
+    hal->update_screen = tamagotchi_p1_hal_update_screen;
+    hal->set_lcd_matrix = tamagotchi_p1_hal_set_lcd_matrix;
+    hal->set_lcd_icon = tamagotchi_p1_hal_set_lcd_icon;
+    hal->set_frequency = tamagotchi_p1_hal_set_frequency;
+    hal->play_frequency = tamagotchi_p1_hal_play_frequency;
+    hal->handler = tamagotchi_p1_hal_handler;
+}

+ 35 - 0
hal_types.h

@@ -0,0 +1,35 @@
+/*
+ * TamaLIB - A hardware agnostic Tamagotchi P1 emulation library
+ *
+ * Copyright (C) 2021 Jean-Christophe Rona <jc@rona.fr>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+#ifndef _HAL_TYPES_H_
+#define _HAL_TYPES_H_
+
+#include <furi.h>
+
+typedef bool bool_t;
+typedef uint8_t u4_t;
+typedef uint8_t u5_t;
+typedef uint8_t u8_t;
+typedef uint16_t u12_t;
+typedef uint16_t u13_t;
+typedef uint32_t u32_t;
+typedef uint32_t
+    timestamp_t; // WARNING: Must be an unsigned type to properly handle wrapping (u32 wraps in around 1h11m when expressed in us)
+
+#endif /* _HAL_TYPES_H_ */

BIN
icons/icon_0.png


BIN
icons/icon_1.png


BIN
icons/icon_2.png


BIN
icons/icon_3.png


BIN
icons/icon_4.png


BIN
icons/icon_5.png


BIN
icons/icon_6.png


BIN
icons/icon_7.png


+ 1 - 0
lib/tamalib

@@ -0,0 +1 @@
+Subproject commit d3fd0e05d34f58048924d4cee84419b0e18e91a4

+ 38 - 0
tama.h

@@ -0,0 +1,38 @@
+#pragma once
+
+#include <input/input.h>
+#include <tamalib.h>
+
+#define TAG "TamaP1"
+#define TAMA_ROM_PATH EXT_PATH("tama_p1/rom.bin")
+#define TAMA_SCREEN_SCALE_FACTOR 2
+#define TAMA_LCD_ICON_SIZE 14
+#define TAMA_LCD_ICON_MARGIN 1
+
+typedef struct {
+    FuriThread* thread;
+    hal_t hal;
+    uint8_t* rom;
+    // 32x16 screen, perfectly represented through uint32_t
+    uint32_t framebuffer[16];
+    uint8_t icons;
+    bool halted;
+    bool fast_forward_done;
+    bool buzzer_on;
+    float frequency;
+} TamaApp;
+
+typedef enum {
+    EventTypeInput,
+    EventTypeTick,
+} EventType;
+
+typedef struct {
+    EventType type;
+    InputEvent input;
+} TamaEvent;
+
+extern TamaApp* g_ctx;
+extern FuriMutex* g_state_mutex;
+
+void tamagotchi_p1_hal_init(hal_t* hal);

+ 299 - 0
tamagotchi_p1.c

@@ -0,0 +1,299 @@
+#include <furi.h>
+#include <gui/gui.h>
+#include <input/input.h>
+#include <storage/storage.h>
+#include <stdlib.h>
+#include <stm32wbxx_ll_tim.h>
+#include <tamalib.h>
+#include "tama.h"
+#include "tamagotchi_p1_icons.h"
+
+TamaApp* g_ctx;
+FuriMutex* g_state_mutex;
+
+static const Icon* icons_list[] = {
+    &I_icon_0,
+    &I_icon_1,
+    &I_icon_2,
+    &I_icon_3,
+    &I_icon_4,
+    &I_icon_5,
+    &I_icon_6,
+    &I_icon_7,
+};
+
+static void tamagotchi_p1_draw_callback(Canvas* const canvas, void* cb_ctx) {
+    furi_assert(cb_ctx);
+
+    FuriMutex* const mutex = cb_ctx;
+    if(furi_mutex_acquire(mutex, 25) != FuriStatusOk) return;
+
+    if(g_ctx->rom == NULL) {
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 30, 30, "No ROM");
+    } else if(g_ctx->halted) {
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 30, 30, "Halted");
+    } else {
+        // FURI_LOG_D(TAG, "Drawing frame");
+        // Calculate positioning
+        uint16_t canv_width = canvas_width(canvas);
+        uint16_t canv_height = canvas_height(canvas);
+        uint16_t lcd_matrix_scaled_width = 32 * TAMA_SCREEN_SCALE_FACTOR;
+        uint16_t lcd_matrix_scaled_height = 16 * TAMA_SCREEN_SCALE_FACTOR;
+        uint16_t lcd_matrix_top = (canv_height - lcd_matrix_scaled_height) / 2;
+        uint16_t lcd_matrix_left = (canv_width - lcd_matrix_scaled_width) / 2;
+        uint16_t lcd_icon_upper_top = lcd_matrix_top - TAMA_LCD_ICON_SIZE - TAMA_LCD_ICON_MARGIN;
+        uint16_t lcd_icon_upper_left = lcd_matrix_left;
+        uint16_t lcd_icon_lower_top =
+            lcd_matrix_top + lcd_matrix_scaled_height + TAMA_LCD_ICON_MARGIN;
+        uint16_t lcd_icon_lower_left = lcd_matrix_left;
+        uint16_t lcd_icon_spacing_horiz =
+            (lcd_matrix_scaled_width - (4 * TAMA_LCD_ICON_SIZE)) / 3 + TAMA_LCD_ICON_SIZE;
+
+        // Draw pixels
+        // canvas_draw_frame(
+        //     canvas,
+        //     lcd_matrix_left,
+        //     lcd_matrix_top,
+        //     lcd_matrix_scaled_width,
+        //     lcd_matrix_scaled_height);
+
+        uint16_t y = lcd_matrix_top;
+        for(uint8_t row = 0; row < 16; ++row) {
+            uint16_t x = lcd_matrix_left;
+            uint32_t row_pixels = g_ctx->framebuffer[row];
+            for(uint8_t col = 0; col < 32; ++col) {
+                if(row_pixels & 1) {
+                    canvas_draw_box(
+                        canvas, x, y, TAMA_SCREEN_SCALE_FACTOR, TAMA_SCREEN_SCALE_FACTOR);
+                }
+                x += TAMA_SCREEN_SCALE_FACTOR;
+                row_pixels >>= 1;
+            }
+            y += TAMA_SCREEN_SCALE_FACTOR;
+        }
+
+        // Draw icons
+        uint8_t lcd_icons = g_ctx->icons;
+        // Top
+        y = lcd_icon_upper_top;
+        uint16_t x_ic = lcd_icon_upper_left;
+        for(uint8_t i = 0; i < 4; ++i) {
+            // canvas_draw_frame(canvas, x_ic, y, TAMA_LCD_ICON_SIZE, TAMA_LCD_ICON_SIZE);
+            if(lcd_icons & 1) {
+                canvas_draw_icon(canvas, x_ic, y, icons_list[i]);
+            }
+            x_ic += lcd_icon_spacing_horiz;
+            lcd_icons >>= 1;
+        }
+
+        // Bottom
+        y = lcd_icon_lower_top;
+        x_ic = lcd_icon_lower_left;
+        for(uint8_t i = 4; i < 8; ++i) {
+            // canvas_draw_frame(canvas, x_ic, y, TAMA_LCD_ICON_SIZE, TAMA_LCD_ICON_SIZE);
+            if(lcd_icons & 1) {
+                canvas_draw_icon(canvas, x_ic, y, icons_list[i]);
+            }
+            x_ic += lcd_icon_spacing_horiz;
+            lcd_icons >>= 1;
+        }
+    }
+
+    furi_mutex_release(mutex);
+}
+
+static void tamagotchi_p1_input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) {
+    furi_assert(event_queue);
+
+    TamaEvent event = {.type = EventTypeInput, .input = *input_event};
+    furi_message_queue_put(event_queue, &event, FuriWaitForever);
+}
+
+static void tamagotchi_p1_update_timer_callback(FuriMessageQueue* event_queue) {
+    furi_assert(event_queue);
+
+    TamaEvent event = {.type = EventTypeTick};
+    furi_message_queue_put(event_queue, &event, 0);
+}
+
+static int32_t tamagotchi_p1_worker(void* context) {
+    bool running = true;
+    FuriMutex* mutex = context;
+    while(furi_mutex_acquire(mutex, FuriWaitForever) != FuriStatusOk) furi_delay_tick(1);
+
+    cpu_sync_ref_timestamp();
+    LL_TIM_EnableCounter(TIM2);
+    while(running) {
+        if(furi_thread_flags_get()) {
+            running = false;
+        } else {
+            // FURI_LOG_D(TAG, "Stepping");
+            // for (int i = 0; i < 100; ++i)
+            tamalib_step();
+        }
+    }
+    LL_TIM_DisableCounter(TIM2);
+    furi_mutex_release(mutex);
+    return 0;
+}
+
+static void tamagotchi_p1_init(TamaApp* const ctx) {
+    g_ctx = ctx;
+    memset(ctx, 0, sizeof(TamaApp));
+    tamagotchi_p1_hal_init(&ctx->hal);
+
+    // Load ROM
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FileInfo fi;
+    if(storage_common_stat(storage, TAMA_ROM_PATH, &fi) == FSE_OK) {
+        File* rom_file = storage_file_alloc(storage);
+        if(storage_file_open(rom_file, TAMA_ROM_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) {
+            ctx->rom = malloc((size_t)fi.size);
+            uint8_t* buf_ptr = ctx->rom;
+            size_t read = 0;
+            while(read < fi.size) {
+                size_t to_read = fi.size - read;
+                if(to_read > UINT16_MAX) to_read = UINT16_MAX;
+                uint16_t now_read = storage_file_read(rom_file, buf_ptr, (uint16_t)to_read);
+                read += now_read;
+                buf_ptr += now_read;
+            }
+
+            // Reorder endianess of ROM
+            for(size_t i = 0; i < fi.size; i += 2) {
+                uint8_t b = ctx->rom[i];
+                ctx->rom[i] = ctx->rom[i + 1];
+                ctx->rom[i + 1] = b & 0xF;
+            }
+        }
+
+        storage_file_close(rom_file);
+        storage_file_free(rom_file);
+    }
+    furi_record_close(RECORD_STORAGE);
+
+    if(ctx->rom != NULL) {
+        // Init TIM2
+        // 64KHz
+        LL_TIM_InitTypeDef tim_init = {
+            .Prescaler = 999,
+            .CounterMode = LL_TIM_COUNTERMODE_UP,
+            .Autoreload = 0xFFFFFFFF,
+        };
+        LL_TIM_Init(TIM2, &tim_init);
+        LL_TIM_SetClockSource(TIM2, LL_TIM_CLOCKSOURCE_INTERNAL);
+        LL_TIM_DisableCounter(TIM2);
+        LL_TIM_SetCounter(TIM2, 0);
+
+        // Init TamaLIB
+        tamalib_register_hal(&ctx->hal);
+        tamalib_init((u12_t*)ctx->rom, NULL, 64000);
+        tamalib_set_speed(1);
+
+        // TODO: implement fast forwarding
+        ctx->fast_forward_done = true;
+
+        // Start stepping thread
+        ctx->thread = furi_thread_alloc();
+        furi_thread_set_name(ctx->thread, "TamaLIB");
+        furi_thread_set_stack_size(ctx->thread, 1024);
+        furi_thread_set_callback(ctx->thread, tamagotchi_p1_worker);
+        furi_thread_set_context(ctx->thread, g_state_mutex);
+        furi_thread_start(ctx->thread);
+    }
+}
+
+static void tamagotchi_p1_deinit(TamaApp* const ctx) {
+    if(ctx->rom != NULL) {
+        tamalib_release();
+        furi_thread_free(ctx->thread);
+        free(ctx->rom);
+    }
+}
+
+int32_t tamagotchi_p1_app(void* p) {
+    UNUSED(p);
+
+    TamaApp* ctx = malloc(sizeof(TamaApp));
+    g_state_mutex = furi_mutex_alloc(FuriMutexTypeRecursive);
+    tamagotchi_p1_init(ctx);
+
+    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(TamaEvent));
+
+    ViewPort* view_port = view_port_alloc();
+    view_port_draw_callback_set(view_port, tamagotchi_p1_draw_callback, g_state_mutex);
+    view_port_input_callback_set(view_port, tamagotchi_p1_input_callback, event_queue);
+
+    Gui* gui = furi_record_open(RECORD_GUI);
+    gui_add_view_port(gui, view_port, GuiLayerFullscreen);
+
+    FuriTimer* timer =
+        furi_timer_alloc(tamagotchi_p1_update_timer_callback, FuriTimerTypePeriodic, event_queue);
+    furi_timer_start(timer, furi_kernel_get_tick_frequency() / 30);
+
+    for(bool running = true; running;) {
+        TamaEvent event;
+        FuriStatus event_status = furi_message_queue_get(event_queue, &event, FuriWaitForever);
+        if(event_status == FuriStatusOk) {
+            // Local override with acquired context
+            if(furi_mutex_acquire(g_state_mutex, FuriWaitForever) != FuriStatusOk) continue;
+
+            if(event.type == EventTypeTick) {
+                // FURI_LOG_D(TAG, "EventTypeTick");
+                view_port_update(view_port);
+            } else if(event.type == EventTypeInput) {
+                FURI_LOG_D(
+                    TAG,
+                    "EventTypeInput: %d %d %d",
+                    event.input.sequence,
+                    event.input.key,
+                    event.input.type);
+                InputType input_type = event.input.type;
+                if(input_type == InputTypePress || input_type == InputTypeRelease) {
+                    btn_state_t tama_btn_state = 0;
+                    if(input_type == InputTypePress)
+                        tama_btn_state = BTN_STATE_PRESSED;
+                    else if(input_type == InputTypeRelease)
+                        tama_btn_state = BTN_STATE_RELEASED;
+
+                    if(event.input.key == InputKeyLeft) {
+                        tamalib_set_button(BTN_LEFT, tama_btn_state);
+                    } else if(event.input.key == InputKeyOk) {
+                        tamalib_set_button(BTN_MIDDLE, tama_btn_state);
+                    } else if(event.input.key == InputKeyRight) {
+                        tamalib_set_button(BTN_RIGHT, tama_btn_state);
+                    }
+                }
+
+                if(event.input.key == InputKeyBack && event.input.type == InputTypeLong) {
+                    furi_timer_stop(timer);
+                    running = false;
+                }
+            }
+
+            furi_mutex_release(g_state_mutex);
+        } else {
+            // Timeout
+            // FURI_LOG_D(TAG, "Timed out");
+        }
+    }
+
+    if(ctx->rom != NULL) {
+        furi_thread_flags_set(furi_thread_get_id(ctx->thread), 1);
+        furi_thread_join(ctx->thread);
+    }
+
+    furi_timer_free(timer);
+    view_port_enabled_set(view_port, false);
+    gui_remove_view_port(gui, view_port);
+    furi_record_close(RECORD_GUI);
+    view_port_free(view_port);
+    furi_message_queue_free(event_queue);
+    furi_mutex_free(g_state_mutex);
+    tamagotchi_p1_deinit(ctx);
+    free(ctx);
+
+    return 0;
+}