MX %!s(int64=2) %!d(string=hai) anos
achega
54a49e98df
Modificáronse 31 ficheiros con 4717 adicións e 0 borrados
  1. 76 0
      README.md
  2. BIN=BIN
      Screenshot1.png
  3. BIN=BIN
      Screenshot2.png
  4. BIN=BIN
      Screenshot3.png
  5. BIN=BIN
      Screenshot4.png
  6. 12 0
      application.fam
  7. 66 0
      compiled/assets_icons.h
  8. 146 0
      hal.c
  9. 35 0
      hal_types.h
  10. BIN=BIN
      icons/icon_0.png
  11. BIN=BIN
      icons/icon_1.png
  12. BIN=BIN
      icons/icon_2.png
  13. BIN=BIN
      icons/icon_3.png
  14. BIN=BIN
      icons/icon_4.png
  15. BIN=BIN
      icons/icon_5.png
  16. BIN=BIN
      icons/icon_6.png
  17. BIN=BIN
      icons/icon_7.png
  18. BIN=BIN
      tama.gif
  19. 42 0
      tama.h
  20. BIN=BIN
      tamaIcon.png
  21. 1191 0
      tama_p1.c
  22. 339 0
      tamalib/LICENSE
  23. 64 0
      tamalib/README.md
  24. 2033 0
      tamalib/cpu.c
  25. 215 0
      tamalib/cpu.h
  26. 89 0
      tamalib/hal.h
  27. 32 0
      tamalib/hal_types.h.template
  28. 134 0
      tamalib/hw.c
  29. 50 0
      tamalib/hw.h
  30. 128 0
      tamalib/tamalib.c
  31. 65 0
      tamalib/tamalib.h

+ 76 - 0
README.md

@@ -0,0 +1,76 @@
+Tama P1 Emulator for Flipper Zero
+=======================================
+
+This is a tama P1 Emulator app for Flipper Zero, based on [TamaLIB](https://github.com/jcrona/tamalib/).
+
+![Alt Text](tama.gif)
+
+How to play
+-----------
+Create a `tama_p1` folder in your microSD card, and put the ROM as `rom.bin`.
+Use a search engine to find the Tamagotchi ROM. There is a file named `tama.b`. 
+Rename this to `rom.bin`. 
+
+*Controls in portrait mode are the same as landscape mode, but turned 90 degrees.*
+- Left button is A.
+- Down or OK is B. 
+- Right button is C. 
+- Up button takes you to the emulator menu.
+- Hold the Back button to save and exit.
+
+![Alt Text](Screenshot1.png)
+![Alt Text](Screenshot2.png)
+
+Building
+--------
+Move this folder into flippers `applications/plugins/tama_p1`. 
+
+
+Launching the app, directly from console to flipper: 
+```
+./fbt launch_app APPSRC=applications/plugins/tama_p1
+```
+
+Run the following to compile icons:
+```
+scripts/assets.py icons applications/plugins/tama_p1/icons applications/plugins/tama_p1/compiled
+```
+
+Note: you may also need to add `-Wno-unused-parameter` to `CCFLAGS` in
+`site_cons/cc.scons` to suppress unused parameter errors in TamaLIB.
+
+Debugging
+---------
+Using the serial script from [FlipperScripts](https://github.com/DroomOne/FlipperScripts/blob/main/serial_logger.py) 
+it is easy to add direct logging after running the application: 
+```
+`python .\serial_logger.py`
+
+`./fbt launch_app APPSRC=applications/plugins/tama_p1; python .\serial_logger.py`
+```
+Alternatively, follow the directions here: https://flipper.atmanos.com/docs/debugging/viewing/
+
+Implemented
+-----------
+- Menu options:
+  - Switch between portrait and landscape
+  - A+C shortcut (mute/change in-game time)
+  - Double / quadruple speed
+
+![Alt Text](Screenshot3.png)
+
+To-Do
+-----
+- Fix bugs: 
+  - When not on 1x speed, after mashing buttons in quick succession, buttons stop responding for a few seconds. But the rom still runs.
+- Stuff to do when bored:
+  - optimization and bug fixing (see above)
+  - add to this list
+  - portrait menu
+  - Add "loading bar" when saving
+  - "Advanced" settings
+  - saving and loading, multiple save states, with the date and time of of each save.
+  - Autosave and changing autosave frequency
+  - Save settings to /tama_p1/settings.txt
+
+![Alt Text](Screenshot4.png)

BIN=BIN
Screenshot1.png


BIN=BIN
Screenshot2.png


BIN=BIN
Screenshot3.png


BIN=BIN
Screenshot4.png


+ 12 - 0
application.fam

@@ -0,0 +1,12 @@
+App(
+    appid="tama_p1",
+    name="TAMA P1",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="tama_p1_app",
+    cdefines=["APP_TAMA_P1"],
+    requires=["gui", "storage"],
+    stack_size=2 * 1024,
+    order=215,
+    fap_icon="tamaIcon.png",
+    fap_category="Games",
+)

+ 66 - 0
compiled/assets_icons.h

@@ -0,0 +1,66 @@
+#include <gui/icon_i.h>
+
+const uint8_t _I_icon_0_0[] = {
+    0x01, 0x00, 0x1a, 0x00, 0x00, 0x0d, 0xaa, 0x1d, 0x7e, 0x00, 0x9c, 0x3e, 0xf9, 0x0f, 0x9e,
+    0x43, 0xe3, 0x00, 0x12, 0x9c, 0x43, 0xa7, 0x10, 0xc9, 0xe4, 0x30, 0x0a, 0x31, 0x08, 0x60,
+};
+const uint8_t* const _I_icon_0[] = {_I_icon_0_0};
+
+const uint8_t _I_icon_1_0[] = {
+    0x00, 0x00, 0x00, 0x40, 0x04, 0x04, 0x04, 0xf0, 0x11, 0xf9, 0x1b, 0xf8, 0x07, 0x8c, 0x06,
+    0xed, 0x36, 0xac, 0x26, 0xe8, 0x02, 0x52, 0x0b, 0x02, 0x18, 0xe0, 0x01, 0xe0, 0x01,
+};
+const uint8_t* const _I_icon_1[] = {_I_icon_1_0};
+
+const uint8_t _I_icon_2_0[] = {
+    0x00, 0x00, 0x00, 0x0e, 0x00, 0x13, 0x00, 0x21, 0x3c, 0x21, 0x3e, 0x23, 0x3f, 0x9f, 0x1f,
+    0xc0, 0x0f, 0xe0, 0x07, 0xf0, 0x01, 0x7c, 0x00, 0x1f, 0x00, 0x06, 0x00, 0x06, 0x00,
+};
+const uint8_t* const _I_icon_2[] = {_I_icon_2_0};
+
+const uint8_t _I_icon_3_0[] = {
+    0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x1c, 0x40, 0x3a, 0xc0, 0x36, 0xf0, 0x37, 0x18, 0x2d,
+    0x0c, 0x2b, 0x0e, 0x02, 0x1f, 0x06, 0x3e, 0x07, 0xfe, 0x00, 0x7f, 0x00, 0x18, 0x00,
+};
+const uint8_t* const _I_icon_3[] = {_I_icon_3_0};
+
+const uint8_t _I_icon_4_0[] = {
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x00, 0xc7, 0x3c, 0x82, 0x2f, 0xf2, 0x26, 0xc7, 0x2c,
+    0x69, 0x28, 0x2f, 0x2c, 0xe7, 0x27, 0x02, 0x20, 0x02, 0x30, 0x06, 0x1c, 0xfc, 0x0f,
+};
+const uint8_t* const _I_icon_4[] = {_I_icon_4_0};
+
+const uint8_t _I_icon_5_0[] = {
+    0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x01, 0xfe, 0x0f, 0x03, 0x38, 0xc9, 0x22, 0x9a, 0x32,
+    0xa2, 0x28, 0x24, 0x2c, 0x21, 0x20, 0x61, 0x30, 0x21, 0x10, 0xf3, 0x11, 0x1e, 0x0f,
+};
+const uint8_t* const _I_icon_5[] = {_I_icon_5_0};
+
+const uint8_t _I_icon_6_0[] = {
+    0x01, 0x00, 0x17, 0x00, 0x00, 0x44, 0x62, 0xfd, 0x38, 0xbf, 0xcf, 0xb7, 0xf3, 0xf8,
+    0xfc, 0x6e, 0x3f, 0x1a, 0xff, 0xc0, 0x3f, 0xf0, 0x1f, 0xf4, 0x02, 0x71, 0x00,
+};
+const uint8_t* const _I_icon_6[] = {_I_icon_6_0};
+
+const uint8_t _I_icon_7_0[] = {
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x41, 0x0e, 0xc4, 0x1f, 0x94, 0x20,
+    0x00, 0x21, 0x22, 0x1f, 0x1d, 0x0a, 0x63, 0x20, 0xde, 0x20, 0x80, 0x1f, 0x00, 0x0e,
+};
+const uint8_t* const _I_icon_7[] = {_I_icon_7_0};
+
+const Icon I_icon_0 =
+    {.width = 14, .height = 14, .frame_count = 1, .frame_rate = 0, .frames = _I_icon_0};
+const Icon I_icon_1 =
+    {.width = 14, .height = 14, .frame_count = 1, .frame_rate = 0, .frames = _I_icon_1};
+const Icon I_icon_2 =
+    {.width = 14, .height = 14, .frame_count = 1, .frame_rate = 0, .frames = _I_icon_2};
+const Icon I_icon_3 =
+    {.width = 14, .height = 14, .frame_count = 1, .frame_rate = 0, .frames = _I_icon_3};
+const Icon I_icon_4 =
+    {.width = 14, .height = 14, .frame_count = 1, .frame_rate = 0, .frames = _I_icon_4};
+const Icon I_icon_5 =
+    {.width = 14, .height = 14, .frame_count = 1, .frame_rate = 0, .frames = _I_icon_5};
+const Icon I_icon_6 =
+    {.width = 14, .height = 14, .frame_count = 1, .frame_rate = 0, .frames = _I_icon_6};
+const Icon I_icon_7 =
+    {.width = 14, .height = 14, .frame_count = 1, .frame_rate = 0, .frames = _I_icon_7};

+ 146 - 0
hal.c

@@ -0,0 +1,146 @@
+#include <furi.h>
+#include <furi_hal.h>
+#include <stdlib.h>
+#include <stm32wbxx_ll_tim.h>
+#include "tama.h"
+
+#define TAG_HAL "TamaLIB"
+
+static void* tama_p1_hal_malloc(u32_t size) {
+    return malloc(size);
+}
+
+static void tama_p1_hal_free(void* ptr) {
+    free(ptr);
+}
+
+static void tama_p1_hal_halt(void) {
+    g_ctx->halted = true;
+}
+
+static bool_t tama_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 tama_p1_hal_log(log_level_t level, char* buff, ...) {
+    if(!tama_p1_hal_is_log_enabled(level)) return;
+
+    FuriString* string = furi_string_alloc();
+    va_list args;
+    va_start(args, buff);
+    furi_string_cat_vprintf(string, buff, args);
+    va_end(args);
+
+    switch(level) {
+    case LOG_ERROR:
+        FURI_LOG_E(TAG_HAL, "%s", furi_string_get_cstr(string));
+        break;
+    case LOG_INFO:
+        FURI_LOG_I(TAG_HAL, "%s", furi_string_get_cstr(string));
+        break;
+    case LOG_MEMORY:
+        break;
+    case LOG_CPU:
+        FURI_LOG_D(TAG_HAL, "%s", furi_string_get_cstr(string));
+        break;
+    default:
+        FURI_LOG_D(TAG_HAL, "%s", furi_string_get_cstr(string));
+        break;
+    }
+
+    furi_string_free(string);
+}
+
+static void tama_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 tama_p1_hal_get_timestamp(void) {
+    return LL_TIM_GetCounter(TIM2);
+}
+
+static void tama_p1_hal_update_screen(void) {
+    // Do nothing, covered by main loop
+}
+
+static void tama_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 tama_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 tama_p1_hal_play_frequency(bool_t en) {
+    if(en) {
+        if(furi_hal_speaker_is_mine() || furi_hal_speaker_acquire(30)) {
+            furi_hal_speaker_start(g_ctx->frequency, 0.5f);
+        }
+    } else {
+        if(furi_hal_speaker_is_mine()) {
+            furi_hal_speaker_stop();
+            furi_hal_speaker_release();
+        }
+    }
+
+    g_ctx->buzzer_on = en;
+}
+
+static void tama_p1_hal_set_frequency(u32_t freq) {
+    g_ctx->frequency = freq / 10.0F;
+    if(g_ctx->buzzer_on) tama_p1_hal_play_frequency(true);
+}
+
+static int tama_p1_hal_handler(void) {
+    // Do nothing
+    return 0;
+}
+
+void tama_p1_hal_init(hal_t* hal) {
+    hal->malloc = tama_p1_hal_malloc;
+    hal->free = tama_p1_hal_free;
+    hal->halt = tama_p1_hal_halt;
+    hal->is_log_enabled = tama_p1_hal_is_log_enabled;
+    hal->log = tama_p1_hal_log;
+    hal->sleep_until = tama_p1_hal_sleep_until;
+    hal->get_timestamp = tama_p1_hal_get_timestamp;
+    hal->update_screen = tama_p1_hal_update_screen;
+    hal->set_lcd_matrix = tama_p1_hal_set_lcd_matrix;
+    hal->set_lcd_icon = tama_p1_hal_set_lcd_icon;
+    hal->set_frequency = tama_p1_hal_set_frequency;
+    hal->play_frequency = tama_p1_hal_play_frequency;
+    hal->handler = tama_p1_hal_handler;
+}

+ 35 - 0
hal_types.h

@@ -0,0 +1,35 @@
+/*
+ * TamaLIB - A hardware agnostic tama 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=BIN
icons/icon_0.png


BIN=BIN
icons/icon_1.png


BIN=BIN
icons/icon_2.png


BIN=BIN
icons/icon_3.png


BIN=BIN
icons/icon_4.png


BIN=BIN
icons/icon_5.png


BIN=BIN
icons/icon_6.png


BIN=BIN
icons/icon_7.png


BIN=BIN
tama.gif


+ 42 - 0
tama.h

@@ -0,0 +1,42 @@
+#pragma once
+
+#include <input/input.h>
+#include "tamalib/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
+
+#define STATE_FILE_MAGIC "TLST"
+#define STATE_FILE_VERSION 2
+#define TAMA_SAVE_PATH EXT_PATH("tama_p1/save.bin")
+
+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 tama_p1_hal_init(hal_t* hal);

BIN=BIN
tamaIcon.png


+ 1191 - 0
tama_p1.c

@@ -0,0 +1,1191 @@
+#include <furi.h>
+#include <furi_hal_bus.h>
+#include <gui/gui.h>
+#include <input/input.h>
+#include <storage/storage.h>
+#include <stdlib.h>
+#include <stm32wbxx_ll_tim.h>
+#include "tamalib/tamalib.h"
+#include "tama.h"
+#include "compiled/assets_icons.h"
+
+TamaApp* g_ctx;
+FuriMutex* g_state_mutex;
+uint8_t layout_mode = 0; // 3: portrait => 4: portrait <=
+// 0: landscape (small) 1: landscape (big) 2: landscape (full)
+bool in_menu = false;
+
+uint8_t speed = 1;
+const uint8_t speed_options[] = {1, 2, 4};
+const uint8_t min_speed = 1;
+const uint8_t max_speed = 4;
+const uint8_t speed_options_size = 3;
+// = sizeof(speed_options) / sizeof(speed_options[0]);
+
+uint8_t menu_cursor = 3; // 0: layout mode; 1: speed; 2: A+C;
+const uint8_t menu_items = 4; // 3: Close menu, Save & Exit
+uint8_t sub_menu_buttons = 0;
+uint8_t sub_menu_last = 0;
+
+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 InputKey m = InputKeyUp;
+static InputKey a = InputKeyLeft;
+static InputKey b = InputKeyDown;
+static InputKey c = InputKeyRight;
+
+static void speed_up() {
+    switch(speed) {
+    case max_speed:
+        speed = speed_options[0];
+        break;
+    default:
+        for(uint8_t i = 0; i < speed_options_size - 1; i++) {
+            if(speed == speed_options[i]) {
+                speed = speed_options[i + 1];
+                break;
+            }
+        }
+        break;
+    }
+    tamalib_set_speed(speed);
+}
+static void speed_down() {
+    switch(speed) {
+    case min_speed:
+        speed = speed_options[speed_options_size - 1];
+        break;
+    default:
+        for(uint8_t i = speed_options_size - 1; i > 0; i--) {
+            if(speed == speed_options[i]) {
+                speed = speed_options[i - 1];
+                break;
+            }
+        }
+        break;
+    }
+    tamalib_set_speed(speed);
+}
+
+// static void draw_landscape(Canvas* const canvas, void* cb_ctx)
+static void draw_landscape(Canvas* const canvas, uint8_t scale) {
+    // 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 * scale;
+    uint16_t lcd_matrix_scaled_height = 16 * scale;
+    uint16_t lcd_matrix_top = (canv_height - lcd_matrix_scaled_height) / 2; // 0
+    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_lower_top = lcd_matrix_top + lcd_matrix_scaled_height + TAMA_LCD_ICON_MARGIN;
+    uint16_t lcd_icon_upper_left = lcd_matrix_left;
+    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;
+
+    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, scale, scale);
+            }
+            x += scale;
+            row_pixels >>= 1;
+        }
+        y += scale;
+    }
+
+    // Start drawing icons
+    uint8_t lcd_icons = g_ctx->icons;
+
+    // Draw top icons
+    y = 0;
+    uint16_t x_ic = lcd_icon_upper_left;
+    for(uint8_t i = 0; i < 4; ++i) {
+        if(lcd_icons & 1) {
+            canvas_draw_icon(canvas, x_ic, y, icons_list[i]);
+        }
+        // x_ic += TAMA_LCD_ICON_SIZE + 4;
+        // if(scale == 3) {
+        //     y += 16;
+        // } else {
+        x_ic += lcd_icon_spacing_horiz;
+        // }
+        lcd_icons >>= 1;
+    }
+
+    // Draw bottom icons
+    y = 64 - TAMA_LCD_ICON_SIZE;
+    // if(scale == 3) {
+    // y = 0;
+    // x_ic = 128 - TAMA_LCD_ICON_SIZE;
+    //     x_ic = 0;
+    // } else {
+    // y = 64 - TAMA_LCD_ICON_SIZE;
+    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]);
+        }
+        // if(scale == 3) {
+        //     y += 16;
+        // } else {
+        x_ic += lcd_icon_spacing_horiz;
+        // }
+        lcd_icons >>= 1;
+    }
+}
+// static void draw_portrait_right(Canvas* const canvas, void* cb_ctx)
+static void draw_portrait_right(Canvas* const canvas, uint8_t scale) {
+    // 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 * scale;
+    uint16_t lcd_matrix_scaled_height = 16 * scale;
+    // uint16_t lcd_matrix_top = 0;
+    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_matrix_left = 64 - TAMA_LCD_ICON_SIZE;
+    uint16_t lcd_icon_upper_left = lcd_matrix_left;
+    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;
+
+    uint16_t y = lcd_matrix_top; // 64
+    for(uint8_t row = 0; row < 16; ++row) {
+        uint16_t x = 128; // 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, y + 32, x - 66, scale, scale);
+            }
+            x -= scale;
+            row_pixels >>= 1;
+        }
+        y += scale;
+    }
+
+    // Start drawing icons
+    uint8_t lcd_icons = g_ctx->icons;
+
+    // Draw top icons
+    // y = lcd_icon_upper_top;
+    y = 30;
+    // y = 64 - TAMA_LCD_ICON_SIZE;
+    uint16_t x_ic = lcd_icon_upper_left;
+    // uint16_t x_ic = 64 - TAMA_LCD_ICON_SIZE;
+    for(uint8_t i = 0; i < 4; ++i) {
+        if(lcd_icons & 1) {
+            canvas_draw_icon(canvas, y, x_ic, icons_list[i]);
+        }
+        x_ic -= lcd_icon_spacing_horiz; // TAMA_LCD_ICON_SIZE + 4;
+        lcd_icons >>= 1;
+    }
+
+    // Draw bottom icons
+    y = 84; // lcd_icon_lower_top
+    x_ic = lcd_icon_lower_left; // 64 - TAMA_LCD_ICON_SIZE
+    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, y, x_ic, icons_list[i]);
+        }
+        x_ic -= lcd_icon_spacing_horiz;
+        lcd_icons >>= 1;
+    }
+}
+static void draw_portrait_left(Canvas* const canvas, uint8_t scale) {
+    // 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 * scale;
+    // uint16_t lcd_matrix_scaled_height = 16 * scale;
+    // uint16_t lcd_matrix_top = 0;
+    // 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_matrix_left = 0;
+    uint16_t lcd_icon_upper_left = lcd_matrix_left;
+    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;
+
+    // uint16_t y = 64 + lcd_matrix_top;
+    uint16_t y = 64;
+    for(uint8_t row = 0; row < 16; ++row) {
+        uint16_t x = 0; // 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, y, x, scale, scale);
+            }
+            x += scale;
+            row_pixels >>= 1;
+        }
+        y -= scale;
+    }
+
+    // Start drawing icons
+    uint8_t lcd_icons = g_ctx->icons;
+
+    // Draw top icons
+    // y = lcd_icon_upper_top;
+    y = 70;
+    // y = 64 - TAMA_LCD_ICON_SIZE;
+    uint16_t x_ic = lcd_icon_upper_left;
+    // uint16_t x_ic = 64 - TAMA_LCD_ICON_SIZE;
+    for(uint8_t i = 0; i < 4; ++i) {
+        if(lcd_icons & 1) {
+            canvas_draw_icon(canvas, y, x_ic, icons_list[i]);
+        }
+        x_ic += lcd_icon_spacing_horiz; // TAMA_LCD_ICON_SIZE + 4;
+        lcd_icons >>= 1;
+    }
+
+    // Draw bottom icons
+    y = 16; // lcd_icon_lower_top
+    x_ic = lcd_icon_lower_left; // 64 - TAMA_LCD_ICON_SIZE
+    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, y, x_ic, icons_list[i]);
+        }
+        x_ic += lcd_icon_spacing_horiz;
+        lcd_icons >>= 1;
+    }
+}
+// static void draw_mini(Canvas* const canvas, uint16_t inX, uint16_t inY)
+static void draw_mini(Canvas* const canvas) {
+    // Calculate positioning
+    // uint16_t y = inY;
+    const uint16_t y = 34;
+    const uint16_t x = 84;
+
+    uint16_t y1 = y;
+    for(uint8_t row = 0; row < 16; ++row) {
+        // uint16_t x = inX;
+        uint16_t x1 = x;
+        uint32_t row_pixels = g_ctx->framebuffer[row];
+        for(uint8_t col = 0; col < 32; ++col) {
+            if(row_pixels & 1) {
+                canvas_draw_dot(canvas, x1, y1);
+            }
+            x1 += 1;
+            row_pixels >>= 1;
+        }
+        y1 += 1;
+    }
+
+    // Start drawing icons
+    uint8_t lcd_icons = g_ctx->icons;
+
+    // Draw top icons
+    uint16_t y2 = y - 2;
+    uint16_t x_ic = x;
+    for(uint8_t i = 0; i < 4; ++i) {
+        if(lcd_icons & 1) {
+            // canvas_draw_icon(canvas, x_ic, y, icons_list[i]);
+            canvas_draw_line(canvas, x_ic, y2, x_ic + 6, y2);
+        }
+        x_ic += 8;
+        lcd_icons >>= 1;
+    }
+
+    // Draw bottom icons
+    y2 = y + 17;
+    x_ic = x;
+    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]);
+            canvas_draw_line(canvas, x_ic, y2, x_ic + 6, y2);
+        }
+        x_ic += 8;
+        lcd_icons >>= 1;
+    }
+}
+
+static void draw_menu(Canvas* const canvas) {
+    canvas_draw_frame(canvas, 0, 0, 128, 64);
+    canvas_draw_str_aligned(canvas, 64, 6, AlignCenter, AlignCenter, "Menu");
+    canvas_draw_line(canvas, 0, 10, 128, 10);
+    draw_mini(canvas);
+    // draw_mini(canvas, 34, 84);
+
+    switch(menu_cursor) {
+    case 0:
+        canvas_draw_triangle(canvas, 4, 16, 6, 6, CanvasDirectionLeftToRight);
+        break;
+    case 1:
+        canvas_draw_triangle(canvas, 4, 26, 6, 6, CanvasDirectionLeftToRight);
+        break;
+    case 2:
+        switch(sub_menu_buttons) {
+        case 0:
+            canvas_draw_triangle(canvas, 4, 36, 6, 6, CanvasDirectionLeftToRight);
+            break;
+        case 1:
+            canvas_draw_triangle(canvas, 47, 44, 6, 4, CanvasDirectionBottomToTop);
+            break;
+        case 2:
+            canvas_draw_triangle(canvas, 57, 44, 6, 4, CanvasDirectionBottomToTop);
+            break;
+        case 3:
+            canvas_draw_triangle(canvas, 67, 44, 6, 4, CanvasDirectionBottomToTop);
+            break;
+        default:
+            break;
+        }
+        break;
+    case menu_items - 1:
+        switch(sub_menu_last) {
+        case 0:
+            canvas_draw_triangle(canvas, 4, 56, 6, 6, CanvasDirectionLeftToRight);
+            break;
+        case 1:
+            canvas_draw_triangle(canvas, 36, 56, 6, 6, CanvasDirectionLeftToRight);
+            break;
+        case 2:
+            canvas_draw_triangle(canvas, 67, 56, 6, 6, CanvasDirectionLeftToRight);
+            break;
+        default:
+            break;
+        }
+        break;
+    }
+    switch(layout_mode) {
+    case 0:
+        canvas_draw_str(canvas, 12, 20, "Layout: Landscape (small)");
+        break;
+    case 1:
+        canvas_draw_str(canvas, 12, 20, "Layout: Landscape (big)");
+        break;
+    case 2:
+        canvas_draw_str(canvas, 12, 20, "Layout: Landscape (full)");
+        break;
+    case 3:
+        canvas_draw_str(canvas, 12, 20, "Layout: Portrait =>");
+        break;
+    case 4:
+        canvas_draw_str(canvas, 12, 20, "Layout: Portrait <=");
+        break;
+    default:
+        canvas_draw_str(canvas, 12, 20, "Layout: ???");
+        break;
+    }
+    switch(speed) { // match with speed_options
+    // case 0: // freeze menu too
+    //     canvas_draw_str(canvas, 12, 30, "Speed: 0x");
+    //     break;
+    case 1:
+        canvas_draw_str(canvas, 12, 30, "Speed: 1x");
+        break;
+    case 2:
+        canvas_draw_str(canvas, 12, 30, "Speed:  2x");
+        break;
+    case 4:
+        canvas_draw_str(canvas, 12, 30, "Speed:   4x (max)");
+        break;
+    default:
+        canvas_draw_str(canvas, 12, 30, "Speed ??x");
+        break;
+    }
+    canvas_draw_str(canvas, 12, 40, "A+C");
+    canvas_draw_str(canvas, 45, 40, "A");
+    canvas_draw_str(canvas, 55, 40, "B");
+    canvas_draw_str(canvas, 65, 40, "C");
+
+    canvas_draw_str(canvas, 12, 60, "Close");
+    canvas_draw_str(canvas, 44, 60, "Save");
+    canvas_draw_str(canvas, 75, 60, "Save & Exit");
+}
+
+static void tama_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 {
+        if(in_menu) {
+            // switch(layout_mode)
+            // draw_menu_landscape(canvas);
+            draw_menu(canvas);
+        } else {
+            switch(layout_mode) {
+            case 0:
+                draw_landscape(canvas, TAMA_SCREEN_SCALE_FACTOR); // 2
+                break;
+            case 1:
+                draw_landscape(canvas, 3);
+                break;
+            case 2:
+                draw_landscape(canvas, 4);
+                break;
+            case 3:
+                draw_portrait_right(canvas, TAMA_SCREEN_SCALE_FACTOR);
+                break;
+            case 4:
+                draw_portrait_left(canvas, TAMA_SCREEN_SCALE_FACTOR);
+                break;
+            default:
+                break;
+            }
+        }
+    }
+    furi_mutex_release(mutex);
+}
+
+static void tama_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 tama_p1_update_timer_callback(FuriMessageQueue* event_queue) {
+    furi_assert(event_queue);
+
+    TamaEvent event = {.type = EventTypeTick};
+    furi_message_queue_put(event_queue, &event, 0);
+}
+
+static void tama_p1_load_state() {
+    state_t* state;
+    uint8_t buf[4];
+    bool error = false;
+    state = tamalib_get_state();
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    File* file = storage_file_alloc(storage);
+    if(storage_file_open(file, TAMA_SAVE_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) {
+        storage_file_read(file, &buf, 4);
+        if(buf[0] != (uint8_t)STATE_FILE_MAGIC[0] || buf[1] != (uint8_t)STATE_FILE_MAGIC[1] ||
+           buf[2] != (uint8_t)STATE_FILE_MAGIC[2] || buf[3] != (uint8_t)STATE_FILE_MAGIC[3]) {
+            FURI_LOG_E(TAG, "FATAL: Wrong state file magic in \"%s\" !\n", TAMA_SAVE_PATH);
+            error = true;
+        }
+
+        storage_file_read(file, &buf, 1);
+        if(buf[0] != STATE_FILE_VERSION) {
+            FURI_LOG_E(TAG, "FATAL: Unsupported version");
+            error = true;
+        }
+        if(!error) {
+            FURI_LOG_D(TAG, "Reading save.bin");
+
+            storage_file_read(file, &buf, 2);
+            *(state->pc) = buf[0] | ((buf[1] & 0x1F) << 8);
+
+            storage_file_read(file, &buf, 2);
+            *(state->x) = buf[0] | ((buf[1] & 0xF) << 8);
+
+            storage_file_read(file, &buf, 2);
+            *(state->y) = buf[0] | ((buf[1] & 0xF) << 8);
+
+            storage_file_read(file, &buf, 1);
+            *(state->a) = buf[0] & 0xF;
+
+            storage_file_read(file, &buf, 1);
+            *(state->b) = buf[0] & 0xF;
+
+            storage_file_read(file, &buf, 1);
+            *(state->np) = buf[0] & 0x1F;
+
+            storage_file_read(file, &buf, 1);
+            *(state->sp) = buf[0];
+
+            storage_file_read(file, &buf, 1);
+            *(state->flags) = buf[0] & 0xF;
+
+            storage_file_read(file, &buf, 4);
+            *(state->tick_counter) = buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24);
+
+            storage_file_read(file, &buf, 4);
+            *(state->clk_timer_timestamp) = buf[0] | (buf[1] << 8) | (buf[2] << 16) |
+                                            (buf[3] << 24);
+
+            storage_file_read(file, &buf, 4);
+            *(state->prog_timer_timestamp) = buf[0] | (buf[1] << 8) | (buf[2] << 16) |
+                                             (buf[3] << 24);
+
+            storage_file_read(file, &buf, 1);
+            *(state->prog_timer_enabled) = buf[0] & 0x1;
+
+            storage_file_read(file, &buf, 1);
+            *(state->prog_timer_data) = buf[0];
+
+            storage_file_read(file, &buf, 1);
+            *(state->prog_timer_rld) = buf[0];
+
+            storage_file_read(file, &buf, 4);
+            *(state->call_depth) = buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24);
+
+            FURI_LOG_D(TAG, "Restoring Interupts");
+            for(uint32_t i = 0; i < INT_SLOT_NUM; i++) {
+                storage_file_read(file, &buf, 1);
+                state->interrupts[i].factor_flag_reg = buf[0] & 0xF;
+
+                storage_file_read(file, &buf, 1);
+                state->interrupts[i].mask_reg = buf[0] & 0xF;
+
+                storage_file_read(file, &buf, 1);
+                state->interrupts[i].triggered = buf[0] & 0x1;
+            }
+
+            /* First 640 half bytes correspond to the RAM */
+            FURI_LOG_D(TAG, "Restoring RAM");
+            for(uint32_t i = 0; i < MEM_RAM_SIZE; i++) {
+                storage_file_read(file, &buf, 1);
+                SET_RAM_MEMORY(state->memory, i + MEM_RAM_ADDR, buf[0] & 0xF);
+            }
+
+            /* I/Os are from 0xF00 to 0xF7F */
+            FURI_LOG_D(TAG, "Restoring I/O");
+            for(uint32_t i = 0; i < MEM_IO_SIZE; i++) {
+                storage_file_read(file, &buf, 1);
+                SET_IO_MEMORY(state->memory, i + MEM_IO_ADDR, buf[0] & 0xF);
+            }
+            FURI_LOG_D(TAG, "Refreshing Hardware");
+            tamalib_refresh_hw();
+        }
+    }
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+}
+
+static void tama_p1_save_state() {
+    // Saving state
+    FURI_LOG_D(TAG, "Saving Gamestate");
+    uint8_t buf[4];
+    state_t* state;
+    uint32_t offset = 0;
+    state = tamalib_get_state();
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    File* file = storage_file_alloc(storage);
+
+    if(storage_file_open(file, TAMA_SAVE_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
+        buf[0] = (uint8_t)STATE_FILE_MAGIC[0];
+        buf[1] = (uint8_t)STATE_FILE_MAGIC[1];
+        buf[2] = (uint8_t)STATE_FILE_MAGIC[2];
+        buf[3] = (uint8_t)STATE_FILE_MAGIC[3];
+        offset += storage_file_write(file, &buf, sizeof(buf));
+
+        buf[0] = STATE_FILE_VERSION & 0xFF;
+        offset += storage_file_write(file, &buf, 1);
+
+        buf[0] = *(state->pc) & 0xFF;
+        buf[1] = (*(state->pc) >> 8) & 0x1F;
+        offset += storage_file_write(file, &buf, 2);
+
+        buf[0] = *(state->x) & 0xFF;
+        buf[1] = (*(state->x) >> 8) & 0xF;
+        offset += storage_file_write(file, &buf, 2);
+
+        buf[0] = *(state->y) & 0xFF;
+        buf[1] = (*(state->y) >> 8) & 0xF;
+        offset += storage_file_write(file, &buf, 2);
+
+        buf[0] = *(state->a) & 0xF;
+        offset += storage_file_write(file, &buf, 1);
+
+        buf[0] = *(state->b) & 0xF;
+        offset += storage_file_write(file, &buf, 1);
+
+        buf[0] = *(state->np) & 0x1F;
+        offset += storage_file_write(file, &buf, 1);
+
+        buf[0] = *(state->sp) & 0xFF;
+        offset += storage_file_write(file, &buf, 1);
+
+        buf[0] = *(state->flags) & 0xF;
+        offset += storage_file_write(file, &buf, 1);
+
+        buf[0] = *(state->tick_counter) & 0xFF;
+        buf[1] = (*(state->tick_counter) >> 8) & 0xFF;
+        buf[2] = (*(state->tick_counter) >> 16) & 0xFF;
+        buf[3] = (*(state->tick_counter) >> 24) & 0xFF;
+        offset += storage_file_write(file, &buf, sizeof(buf));
+
+        buf[0] = *(state->clk_timer_timestamp) & 0xFF;
+        buf[1] = (*(state->clk_timer_timestamp) >> 8) & 0xFF;
+        buf[2] = (*(state->clk_timer_timestamp) >> 16) & 0xFF;
+        buf[3] = (*(state->clk_timer_timestamp) >> 24) & 0xFF;
+        offset += storage_file_write(file, &buf, sizeof(buf));
+
+        buf[0] = *(state->prog_timer_timestamp) & 0xFF;
+        buf[1] = (*(state->prog_timer_timestamp) >> 8) & 0xFF;
+        buf[2] = (*(state->prog_timer_timestamp) >> 16) & 0xFF;
+        buf[3] = (*(state->prog_timer_timestamp) >> 24) & 0xFF;
+        offset += storage_file_write(file, &buf, sizeof(buf));
+
+        buf[0] = *(state->prog_timer_enabled) & 0x1;
+        offset += storage_file_write(file, &buf, 1);
+
+        buf[0] = *(state->prog_timer_data) & 0xFF;
+        offset += storage_file_write(file, &buf, 1);
+
+        buf[0] = *(state->prog_timer_rld) & 0xFF;
+        offset += storage_file_write(file, &buf, 1);
+
+        buf[0] = *(state->call_depth) & 0xFF;
+        buf[1] = (*(state->call_depth) >> 8) & 0xFF;
+        buf[2] = (*(state->call_depth) >> 16) & 0xFF;
+        buf[3] = (*(state->call_depth) >> 24) & 0xFF;
+        offset += storage_file_write(file, &buf, sizeof(buf));
+
+        for(uint32_t i = 0; i < INT_SLOT_NUM; i++) {
+            buf[0] = state->interrupts[i].factor_flag_reg & 0xF;
+            offset += storage_file_write(file, &buf, 1);
+
+            buf[0] = state->interrupts[i].mask_reg & 0xF;
+            offset += storage_file_write(file, &buf, 1);
+
+            buf[0] = state->interrupts[i].triggered & 0x1;
+            offset += storage_file_write(file, &buf, 1);
+        }
+
+        /* First 640 half bytes correspond to the RAM */
+        for(uint32_t i = 0; i < MEM_RAM_SIZE; i++) {
+            buf[0] = GET_RAM_MEMORY(state->memory, i + MEM_RAM_ADDR) & 0xF;
+            offset += storage_file_write(file, &buf, 1);
+        }
+
+        /* I/Os are from 0xF00 to 0xF7F */
+        for(uint32_t i = 0; i < MEM_IO_SIZE; i++) {
+            buf[0] = GET_IO_MEMORY(state->memory, i + MEM_IO_ADDR) & 0xF;
+            offset += storage_file_write(file, &buf, 1);
+        }
+    }
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+
+    FURI_LOG_D(TAG, "Finished Writing %lu", offset);
+}
+
+static int32_t tama_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);
+
+    tama_p1_load_state();
+
+    while(running) {
+        if(furi_thread_flags_get()) {
+            running = false;
+        } else {
+            // FURI_LOG_D(TAG, "Stepping"); // enabling this cause blank screen somehow
+            // for (int i = 0; i < 100; ++i)
+            tamalib_step(); // tamalib_mainloop();
+        }
+    }
+    LL_TIM_DisableCounter(TIM2);
+    furi_mutex_release(mutex);
+    return 0;
+}
+
+static void tama_p1_init(TamaApp* const ctx) {
+    g_ctx = ctx;
+    memset(ctx, 0, sizeof(TamaApp));
+    tama_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
+
+        furi_hal_bus_enable(FuriHalBusTIM2);
+
+        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(speed);
+
+        // 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, 2 * 1024);
+        furi_thread_set_callback(ctx->thread, tama_p1_worker);
+        furi_thread_set_context(ctx->thread, g_state_mutex);
+        furi_thread_start(ctx->thread);
+    }
+}
+
+static void tama_p1_deinit(TamaApp* const ctx) {
+    if(ctx->rom != NULL) {
+        tamalib_release();
+        furi_thread_free(ctx->thread);
+        furi_hal_bus_disable(FuriHalBusTIM2);
+        free(ctx->rom);
+    }
+}
+
+int32_t tama_p1_app(void* p) {
+    UNUSED(p);
+
+    TamaApp* ctx = malloc(sizeof(TamaApp));
+    g_state_mutex = furi_mutex_alloc(FuriMutexTypeRecursive);
+    tama_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, tama_p1_draw_callback, g_state_mutex);
+    view_port_input_callback_set(view_port, tama_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(tama_p1_update_timer_callback, FuriTimerTypePeriodic, event_queue);
+    furi_timer_start(timer, furi_kernel_get_tick_frequency() / 30);
+
+    // in_menu = false;
+    // menu_cursor = 2;
+
+    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: %ld %d %d",
+                    event.input.sequence,
+                    event.input.key,
+                    event.input.type);
+                // InputType input_type = event.input.type; // idk why this is a variable
+                btn_state_t tama_btn_state = 0; // BTN_STATE_RELEASED is 0
+
+                if(in_menu) {
+                    // if(menu_cursor == 2 &&
+                    // (event.input.key == InputKeyUp || event.input.key == InputKeyDown)) {
+                    // tama_btn_state = BTN_STATE_RELEASED;
+                    // }
+                    if(event.input.key == InputKeyBack) {
+                        tama_btn_state = BTN_STATE_RELEASED;
+                        in_menu = false;
+                    } else if(event.input.key == InputKeyUp && event.input.type == InputTypePress) {
+                        tama_btn_state = BTN_STATE_RELEASED;
+                        if(menu_cursor > 0) {
+                            menu_cursor -= 1;
+                        } else {
+                            menu_cursor = menu_items - 1;
+                        }
+                        if(menu_cursor >= menu_items - 2 && sub_menu_last > 0) {
+                            sub_menu_buttons = 1;
+                            sub_menu_last = 1;
+                        } else {
+                            sub_menu_buttons = 0;
+                            sub_menu_last = 0;
+                        }
+                    } else if(event.input.key == InputKeyDown && event.input.type == InputTypePress) {
+                        tama_btn_state = BTN_STATE_RELEASED;
+                        if(menu_cursor < menu_items - 1) {
+                            menu_cursor += 1;
+                        } else {
+                            menu_cursor = 0;
+                        }
+                        if(menu_cursor >= menu_items - 2 && sub_menu_buttons > 0) {
+                            sub_menu_buttons = 1;
+                            sub_menu_last = 1;
+                        } else {
+                            sub_menu_buttons = 0;
+                            sub_menu_last = 0;
+                        }
+                    } else if(event.input.key == InputKeyLeft && event.input.type == InputTypePress) {
+                        switch(menu_cursor) {
+                        case 0:
+                            switch(layout_mode) {
+                            case 0:
+                                layout_mode = 4;
+                                m = InputKeyRight;
+                                a = InputKeyUp;
+                                b = InputKeyLeft;
+                                c = InputKeyDown;
+                                break;
+                            case 1:
+                                layout_mode -= 1; // 0
+                                m = InputKeyUp;
+                                a = InputKeyLeft;
+                                b = InputKeyDown;
+                                c = InputKeyRight;
+                                break;
+                            case 2:
+                                layout_mode -= 1; // 1
+                                m = InputKeyUp;
+                                a = InputKeyLeft;
+                                b = InputKeyDown;
+                                c = InputKeyRight;
+                                break;
+                            case 3:
+                                layout_mode -= 1; // 2
+                                m = InputKeyUp;
+                                a = InputKeyLeft;
+                                b = InputKeyDown;
+                                c = InputKeyRight;
+                                break;
+                            case 4:
+                                layout_mode -= 1; // 3
+                                m = InputKeyLeft;
+                                a = InputKeyDown;
+                                b = InputKeyRight;
+                                c = InputKeyUp;
+                                break;
+                            }
+                            break;
+                        case 1:
+                            speed_down();
+                            break;
+                        case 2:
+                            tama_btn_state = BTN_STATE_RELEASED;
+                            switch(sub_menu_buttons) {
+                            case 0:
+                                sub_menu_buttons = 3;
+                                break;
+                            case 1:
+                            case 2:
+                            case 3:
+                                sub_menu_buttons -= 1;
+                                break;
+                            default:
+                                break;
+                            }
+                            break;
+                        case menu_items - 1:
+                            switch(sub_menu_last) {
+                            case 0:
+                                sub_menu_last = 2;
+                                break;
+                            case 1:
+                            case 2:
+                                sub_menu_last -= 1;
+                                break;
+                            default:
+                                break;
+                            }
+                            break;
+                        default:
+                            break;
+                        }
+                    } else if(event.input.key == InputKeyRight && event.input.type == InputTypePress) {
+                        switch(menu_cursor) {
+                        case 0:
+                            switch(layout_mode) {
+                            case 0:
+                                layout_mode += 1; // 1
+                                m = InputKeyUp;
+                                a = InputKeyLeft;
+                                b = InputKeyDown;
+                                c = InputKeyRight;
+                                break;
+                            case 1:
+                                layout_mode += 1; // 2
+                                m = InputKeyUp;
+                                a = InputKeyLeft;
+                                b = InputKeyDown;
+                                c = InputKeyRight;
+                                break;
+                            case 2:
+                                layout_mode += 1; // 3
+                                m = InputKeyLeft;
+                                a = InputKeyDown;
+                                b = InputKeyRight;
+                                c = InputKeyUp;
+                                break;
+                            case 3:
+                                layout_mode += 1; // 4
+                                m = InputKeyRight;
+                                a = InputKeyUp;
+                                b = InputKeyLeft;
+                                c = InputKeyDown;
+                                break;
+                            case 4:
+                                layout_mode = 0;
+                                m = InputKeyUp;
+                                a = InputKeyLeft;
+                                b = InputKeyDown;
+                                c = InputKeyRight;
+                                break;
+                            }
+                            break;
+                        case 1:
+                            speed_up();
+                            break;
+                        case 2:
+                            tama_btn_state = BTN_STATE_RELEASED;
+                            switch(sub_menu_buttons) {
+                            case 0:
+                            case 1:
+                            case 2:
+                                sub_menu_buttons += 1;
+                                break;
+                            case 3:
+                                sub_menu_buttons = 0;
+                                break;
+                            default:
+                                break;
+                            }
+                            break;
+                        case menu_items - 1:
+                            switch(sub_menu_last) {
+                            case 0:
+                            case 1:
+                                sub_menu_last += 1;
+                                break;
+                            case 2:
+                                sub_menu_last = 0;
+                                break;
+                            default:
+                                break;
+                            }
+                            break;
+                        default:
+                            break;
+                        }
+                    } else if(event.input.key == InputKeyOk) {
+                        switch(menu_cursor) {
+                        case 0:
+                            if(event.input.type == InputTypePress) {
+                                switch(layout_mode) {
+                                case 0:
+                                    layout_mode += 1; // 1
+                                    m = InputKeyUp;
+                                    a = InputKeyLeft;
+                                    b = InputKeyDown;
+                                    c = InputKeyRight;
+                                    break;
+                                case 1:
+                                    layout_mode += 1; // 2
+                                    m = InputKeyUp;
+                                    a = InputKeyLeft;
+                                    b = InputKeyDown;
+                                    c = InputKeyRight;
+                                    break;
+                                case 2:
+                                    layout_mode += 1; // 3
+                                    m = InputKeyLeft;
+                                    a = InputKeyDown;
+                                    b = InputKeyRight;
+                                    c = InputKeyUp;
+                                    break;
+                                case 3:
+                                    layout_mode += 1; // 4
+                                    m = InputKeyRight;
+                                    a = InputKeyUp;
+                                    b = InputKeyLeft;
+                                    c = InputKeyDown;
+                                    break;
+                                case 4:
+                                    layout_mode = 0;
+                                    m = InputKeyUp;
+                                    a = InputKeyLeft;
+                                    b = InputKeyDown;
+                                    c = InputKeyRight;
+                                    break;
+                                }
+                            }
+                            break;
+                        case 1:
+                            if(event.input.type == InputTypePress) {
+                                speed_up();
+                            }
+                            break;
+                        case 2:
+                            if(event.input.type == InputTypePress)
+                                tama_btn_state = BTN_STATE_PRESSED;
+                            else if(event.input.type == InputTypeRelease)
+                                tama_btn_state = BTN_STATE_RELEASED;
+
+                            switch(sub_menu_buttons) {
+                            case 0: // A+C
+                                tamalib_set_button(BTN_LEFT, tama_btn_state);
+                                tamalib_set_button(BTN_RIGHT, tama_btn_state);
+                                break;
+                            case 1: // A
+                                tamalib_set_button(BTN_LEFT, tama_btn_state);
+                                break;
+                            case 2: // B
+                                tamalib_set_button(BTN_MIDDLE, tama_btn_state);
+                                break;
+                            case 3: // C
+                                tamalib_set_button(BTN_RIGHT, tama_btn_state);
+                                break;
+                            default:
+                                break;
+                            }
+                            break;
+                        case menu_items - 1:
+                        default:
+                            if(event.input.type == InputTypePress) {
+                                switch(sub_menu_last) {
+                                case 0: // close menu
+                                    in_menu = false;
+                                    break;
+                                case 1: // Save
+                                    if(speed != 1) {
+                                        uint8_t temp = speed;
+                                        speed = 1;
+                                        tamalib_set_speed(speed);
+                                        furi_timer_stop(timer);
+                                        tama_p1_save_state();
+                                        furi_timer_start(
+                                            timer, furi_kernel_get_tick_frequency() / 30);
+                                        speed = temp;
+                                        tamalib_set_speed(speed);
+                                    } else {
+                                        furi_timer_stop(timer);
+                                        tama_p1_save_state();
+                                        furi_timer_start(
+                                            timer, furi_kernel_get_tick_frequency() / 30);
+                                    }
+                                    break;
+                                case 2: // Save & Exit
+                                    if(speed != 1) {
+                                        speed = 1;
+                                        tamalib_set_speed(speed);
+                                    }
+                                    furi_timer_stop(timer);
+                                    tama_p1_save_state();
+                                    running = false;
+                                    break;
+                                default:
+                                    break;
+                                }
+                            }
+                            break;
+                        }
+                    }
+                } else { // out of menu // TODO: clean up code -.-
+                    if(event.input.key == InputKeyBack && event.input.type == InputTypeLong) {
+                        if(speed != 1) {
+                            speed = 1;
+                            tamalib_set_speed(speed);
+                        }
+                        furi_timer_stop(timer);
+                        tama_p1_save_state();
+                        running = false;
+                    } else if(
+                        event.input.type == InputTypePress ||
+                        event.input.type == InputTypeRelease) {
+                        if(event.input.key != InputKeyBack && event.input.key != m) {
+                            if(event.input.type == InputTypePress)
+                                tama_btn_state = BTN_STATE_PRESSED;
+                            else if(event.input.type == InputTypeRelease)
+                                tama_btn_state = BTN_STATE_RELEASED;
+                        } else {
+                            tama_btn_state = BTN_STATE_RELEASED;
+                        }
+                        if(event.input.key == m && event.input.type == InputTypePress) {
+                            in_menu = true;
+                        } else if(event.input.key == a) {
+                            tamalib_set_button(BTN_LEFT, tama_btn_state);
+                        } else if(event.input.key == b) {
+                            tamalib_set_button(BTN_MIDDLE, tama_btn_state);
+                        } else if(event.input.key == c) {
+                            tamalib_set_button(BTN_RIGHT, tama_btn_state);
+                        } else if(event.input.key == InputKeyOk) {
+                            tamalib_set_button(BTN_MIDDLE, tama_btn_state);
+                        }
+                    }
+                }
+            }
+            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);
+    tama_p1_deinit(ctx);
+    free(ctx);
+    return 0;
+}

+ 339 - 0
tamalib/LICENSE

@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.

+ 64 - 0
tamalib/README.md

@@ -0,0 +1,64 @@
+# TamaLIB - A hardware agnostic Tamagotchi P1 emulation library
+
+
+## Synopsis
+
+TamaLib is a hardware agnostic Tamagotchi P1 emulation library built from scratch. It is self-contained and aims at running on any platform powerful enough, from microcontrollers (MCUs) to desktop computers, thus spreading virtual life across the digital world.
+
+So far, it has been successfully implemented on different platforms:
+- Desktop computers (check out [TamaTool](https://github.com/jcrona/tamatool/) for more information) 
+- STM32F072 MCU based board (check out [MCUGotchi](https://github.com/jcrona/mcugotchi/) for more information).
+- OpenTama which is an STM32L072 MCU based board (check out [OpenTama](https://github.com/Sparkr-tech/opentama) and [MCUGotchi](https://github.com/jcrona/mcugotchi/) for more information).
+- Arduino UNO (check out [ArduinoGotchi](https://github.com/GaryZ88/ArduinoGotchi/) for more information).
+
+## Importing TamaLIB
+
+TamaLIB cannot be used as is. In order to create life on a specific target, you need to import all TamaLIB related __.c__ and __.h__ files in your project (for instance in a __lib__ subfolder), to create a __hal_types.h__ file using the template provided and to implement the __hal_t__ structure, that will act as an abstraction layer between TamaLIB and your OS or SDK (detailed information can be found in __hal.h__). This abstraction layer basically connects TamaLIB to your target's buttons, clock, audio and screen, while also defining the C types that TamaLIB should use to represent 4-bit, 5-bit, 8-bit, 12-bit, 13-bit and 32-bit variables. Once done, you will be able to call the TamaLIB API from your project.
+
+
+## Using the TamaLIB API
+
+Basically:
+```
+/* ... */
+
+/* Register the HAL */
+tamalib_register_hal(&my_hal);
+
+/* ... */
+
+/* Initialize TamaLIB */
+tamalib_init(my_program, my_breakpoints, 1000000); // my_breakpoints can be NULL, 1000000 means that timestamps will be expressed in us
+
+/* ... */
+
+/* Enter TamaLIB's loop */
+tamalib_mainloop();
+
+/* ... */
+
+/* Release TamaLIB */
+tamalib_release();
+
+/* ... */
+```
+Your main project should then forward any button input to TamaLIB using the `tamalib_set_button()` function.
+
+As an alternative to `tamalib_mainloop()`, you can call `tamalib_step()` directly if your execution flow requires something more complex than a simple mainloop. In that case, TamaLIB will neither call the HAL `handler()` function, nor the HAL `update_screen()` function by itslef.
+
+
+## License
+
+TamaLIB is distributed under the GPLv2 license. See the LICENSE file for more information.
+
+
+## Hardware information
+
+The Tamagotchi P1 is based on an
+[E0C6S46 Epson MCU](https://download.epson-europe.com/pub/electronics-de/asmic/4bit/62family/technicalmanual/tm_6s46.pdf),
+and runs at 32,768 kHz. Its LCD is 32x16 B/W pixels, with 8 icons.
+To my knowledge, the ROM available online has been extracted from a high-res picture of a die. The ROM mask was clear enough to be optically read. The pictures can be seen [there](https://siliconpr0n.org/map/bandai/tamagotchi-v1/) (thx asterick for the link !).  
+I would love to see the same work done on a P2 and add support for it in TamaLIB/TamaTool !
+
+__  
+Copyright (C) 2021 Jean-Christophe Rona

+ 2033 - 0
tamalib/cpu.c

@@ -0,0 +1,2033 @@
+/*
+ * 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.
+ */
+#include "cpu.h"
+#include "hw.h"
+#include "hal.h"
+
+#define TICK_FREQUENCY 32768 // Hz
+
+#define TIMER_1HZ_PERIOD 32768 // in ticks
+#define TIMER_256HZ_PERIOD 128 // in ticks
+
+#define MASK_4B 0xF00
+#define MASK_6B 0xFC0
+#define MASK_7B 0xFE0
+#define MASK_8B 0xFF0
+#define MASK_10B 0xFFC
+#define MASK_12B 0xFFF
+
+#define PCS (pc & 0xFF)
+#define PCSL (pc & 0xF)
+#define PCSH ((pc >> 4) & 0xF)
+#define PCP ((pc >> 8) & 0xF)
+#define PCB ((pc >> 12) & 0x1)
+#define TO_PC(bank, page, step) ((step & 0xFF) | ((page & 0xF) << 8) | (bank & 0x1) << 12)
+#define NBP ((np >> 4) & 0x1)
+#define NPP (np & 0xF)
+#define TO_NP(bank, page) ((page & 0xF) | (bank & 0x1) << 4)
+#define XHL (x & 0xFF)
+#define XL (x & 0xF)
+#define XH ((x >> 4) & 0xF)
+#define XP ((x >> 8) & 0xF)
+#define YHL (y & 0xFF)
+#define YL (y & 0xF)
+#define YH ((y >> 4) & 0xF)
+#define YP ((y >> 8) & 0xF)
+#define M(n) get_memory(n)
+#define SET_M(n, v) set_memory(n, v)
+#define RQ(i) get_rq(i)
+#define SET_RQ(i, v) set_rq(i, v)
+#define SPL (sp & 0xF)
+#define SPH ((sp >> 4) & 0xF)
+
+#define FLAG_C (0x1 << 0)
+#define FLAG_Z (0x1 << 1)
+#define FLAG_D (0x1 << 2)
+#define FLAG_I (0x1 << 3)
+
+#define C !!(flags & FLAG_C)
+#define Z !!(flags & FLAG_Z)
+#define D !!(flags & FLAG_D)
+#define I !!(flags & FLAG_I)
+
+#define SET_C() \
+    { flags |= FLAG_C; }
+#define CLEAR_C() \
+    { flags &= ~FLAG_C; }
+#define SET_Z() \
+    { flags |= FLAG_Z; }
+#define CLEAR_Z() \
+    { flags &= ~FLAG_Z; }
+#define SET_D() \
+    { flags |= FLAG_D; }
+#define CLEAR_D() \
+    { flags &= ~FLAG_D; }
+#define SET_I() \
+    { flags |= FLAG_I; }
+#define CLEAR_I() \
+    { flags &= ~FLAG_I; }
+
+#define REG_CLK_INT_FACTOR_FLAGS 0xF00
+#define REG_SW_INT_FACTOR_FLAGS 0xF01
+#define REG_PROG_INT_FACTOR_FLAGS 0xF02
+#define REG_SERIAL_INT_FACTOR_FLAGS 0xF03
+#define REG_K00_K03_INT_FACTOR_FLAGS 0xF04
+#define REG_K10_K13_INT_FACTOR_FLAGS 0xF05
+#define REG_CLOCK_INT_MASKS 0xF10
+#define REG_SW_INT_MASKS 0xF11
+#define REG_PROG_INT_MASKS 0xF12
+#define REG_SERIAL_INT_MASKS 0xF13
+#define REG_K00_K03_INT_MASKS 0xF14
+#define REG_K10_K13_INT_MASKS 0xF15
+#define REG_PROG_TIMER_DATA_L 0xF24
+#define REG_PROG_TIMER_DATA_H 0xF25
+#define REG_PROG_TIMER_RELOAD_DATA_L 0xF26
+#define REG_PROG_TIMER_RELOAD_DATA_H 0xF27
+#define REG_K00_K03_INPUT_PORT 0xF40
+#define REG_K10_K13_INPUT_PORT 0xF42
+#define REG_K40_K43_BZ_OUTPUT_PORT 0xF54
+#define REG_CPU_OSC3_CTRL 0xF70
+#define REG_LCD_CTRL 0xF71
+#define REG_LCD_CONTRAST 0xF72
+#define REG_SVD_CTRL 0xF73
+#define REG_BUZZER_CTRL1 0xF74
+#define REG_BUZZER_CTRL2 0xF75
+#define REG_CLK_WD_TIMER_CTRL 0xF76
+#define REG_SW_TIMER_CTRL 0xF77
+#define REG_PROG_TIMER_CTRL 0xF78
+#define REG_PROG_TIMER_CLK_SEL 0xF79
+
+#define INPUT_PORT_NUM 2
+
+typedef struct {
+    char* log;
+    u12_t code;
+    u12_t mask;
+    u12_t shift_arg0;
+    u12_t mask_arg0; // != 0 only if there are two arguments
+    u8_t cycles;
+    void (*cb)(u8_t arg0, u8_t arg1);
+} op_t;
+
+typedef struct {
+    u4_t states;
+} input_port_t;
+
+/* Registers */
+static u13_t pc, next_pc;
+static u12_t x, y;
+static u4_t a, b;
+static u5_t np;
+static u8_t sp;
+
+/* Flags */
+static u4_t flags;
+
+static const u12_t* g_program = NULL;
+static MEM_BUFFER_TYPE memory[MEM_BUFFER_SIZE];
+
+static input_port_t inputs[INPUT_PORT_NUM] = {{0}};
+
+/* Interrupts (in priority order) */
+static interrupt_t interrupts[INT_SLOT_NUM] = {
+    {0x0, 0x0, 0, 0x0C}, // Prog timer
+    {0x0, 0x0, 0, 0x0A}, // Serial interface
+    {0x0, 0x0, 0, 0x08}, // Input (K10-K13)
+    {0x0, 0x0, 0, 0x06}, // Input (K00-K03)
+    {0x0, 0x0, 0, 0x04}, // Stopwatch timer
+    {0x0, 0x0, 0, 0x02}, // Clock timer
+};
+
+static breakpoint_t* g_breakpoints = NULL;
+
+static u32_t call_depth = 0;
+
+static u32_t clk_timer_timestamp = 0; // in ticks
+static u32_t prog_timer_timestamp = 0; // in ticks
+static bool_t prog_timer_enabled = 0;
+static u8_t prog_timer_data = 0;
+static u8_t prog_timer_rld = 0;
+
+static u32_t tick_counter = 0;
+static u32_t ts_freq;
+static u8_t speed_ratio = 1;
+static timestamp_t ref_ts;
+
+static state_t cpu_state = {
+    .pc = &pc,
+    .x = &x,
+    .y = &y,
+    .a = &a,
+    .b = &b,
+    .np = &np,
+    .sp = &sp,
+    .flags = &flags,
+
+    .tick_counter = &tick_counter,
+    .clk_timer_timestamp = &clk_timer_timestamp,
+    .prog_timer_timestamp = &prog_timer_timestamp,
+    .prog_timer_enabled = &prog_timer_enabled,
+    .prog_timer_data = &prog_timer_data,
+    .prog_timer_rld = &prog_timer_rld,
+
+    .call_depth = &call_depth,
+
+    .interrupts = interrupts,
+
+    .memory = memory,
+};
+
+void cpu_add_bp(breakpoint_t** list, u13_t addr) {
+    breakpoint_t* bp;
+
+    bp = (breakpoint_t*)g_hal->malloc(sizeof(breakpoint_t));
+    if(!bp) {
+        g_hal->log(LOG_ERROR, "Cannot allocate memory for breakpoint 0x%04X!\n", addr);
+        return;
+    }
+
+    bp->addr = addr;
+
+    if(*list != NULL) {
+        bp->next = *list;
+    } else {
+        /* List is empty */
+        bp->next = NULL;
+    }
+
+    *list = bp;
+}
+
+void cpu_free_bp(breakpoint_t** list) {
+    breakpoint_t *bp = *list, *tmp;
+
+    while(bp != NULL) {
+        tmp = bp->next;
+        g_hal->free(bp);
+        bp = tmp;
+    }
+
+    *list = NULL;
+}
+
+void cpu_set_speed(u8_t speed) {
+    speed_ratio = speed;
+}
+
+state_t* cpu_get_state(void) {
+    return &cpu_state;
+}
+
+u32_t cpu_get_depth(void) {
+    return call_depth;
+}
+
+static void generate_interrupt(int_slot_t slot, u8_t bit) {
+    /* Set the factor flag no matter what */
+    interrupts[slot].factor_flag_reg = interrupts[slot].factor_flag_reg | (0x1 << bit);
+
+    /* Trigger the INT only if not masked */
+    if(interrupts[slot].mask_reg & (0x1 << bit)) {
+        interrupts[slot].triggered = 1;
+    }
+}
+
+void cpu_set_input_pin(pin_t pin, pin_state_t state) {
+    /* Set the I/O */
+    inputs[pin & 0x4].states = (inputs[pin & 0x4].states & ~(0x1 << (pin & 0x3))) |
+                               (state << (pin & 0x3));
+
+    /* Trigger the interrupt (TODO: handle relation register) */
+    if(state == PIN_STATE_LOW) {
+        switch((pin & 0x4) >> 2) {
+        case 0:
+            generate_interrupt(INT_K00_K03_SLOT, pin & 0x3);
+            break;
+
+        case 1:
+            generate_interrupt(INT_K10_K13_SLOT, pin & 0x3);
+            break;
+        }
+    }
+}
+
+void cpu_sync_ref_timestamp(void) {
+    ref_ts = g_hal->get_timestamp();
+}
+
+static u4_t get_io(u12_t n) {
+    u4_t tmp;
+
+    switch(n) {
+    case REG_CLK_INT_FACTOR_FLAGS:
+        /* Interrupt factor flags (clock timer) */
+        tmp = interrupts[INT_CLOCK_TIMER_SLOT].factor_flag_reg;
+        interrupts[INT_CLOCK_TIMER_SLOT].factor_flag_reg = 0;
+        return tmp;
+
+    case REG_SW_INT_FACTOR_FLAGS:
+        /* Interrupt factor flags (stopwatch) */
+        tmp = interrupts[INT_STOPWATCH_SLOT].factor_flag_reg;
+        interrupts[INT_STOPWATCH_SLOT].factor_flag_reg = 0;
+        return tmp;
+
+    case REG_PROG_INT_FACTOR_FLAGS:
+        /* Interrupt factor flags (prog timer) */
+        tmp = interrupts[INT_PROG_TIMER_SLOT].factor_flag_reg;
+        interrupts[INT_PROG_TIMER_SLOT].factor_flag_reg = 0;
+        return tmp;
+
+    case REG_SERIAL_INT_FACTOR_FLAGS:
+        /* Interrupt factor flags (serial) */
+        tmp = interrupts[INT_SERIAL_SLOT].factor_flag_reg;
+        interrupts[INT_SERIAL_SLOT].factor_flag_reg = 0;
+        return tmp;
+
+    case REG_K00_K03_INT_FACTOR_FLAGS:
+        /* Interrupt factor flags (K00-K03) */
+        tmp = interrupts[INT_K00_K03_SLOT].factor_flag_reg;
+        interrupts[INT_K00_K03_SLOT].factor_flag_reg = 0;
+        return tmp;
+
+    case REG_K10_K13_INT_FACTOR_FLAGS:
+        /* Interrupt factor flags (K10-K13) */
+        tmp = interrupts[INT_K10_K13_SLOT].factor_flag_reg;
+        interrupts[INT_K10_K13_SLOT].factor_flag_reg = 0;
+        return tmp;
+
+    case REG_CLOCK_INT_MASKS:
+        /* Clock timer interrupt masks */
+        return interrupts[INT_CLOCK_TIMER_SLOT].mask_reg;
+
+    case REG_SW_INT_MASKS:
+        /* Stopwatch interrupt masks */
+        return interrupts[INT_STOPWATCH_SLOT].mask_reg & 0x3;
+
+    case REG_PROG_INT_MASKS:
+        /* Prog timer interrupt masks */
+        return interrupts[INT_PROG_TIMER_SLOT].mask_reg & 0x1;
+
+    case REG_SERIAL_INT_MASKS:
+        /* Serial interface interrupt masks */
+        return interrupts[INT_SERIAL_SLOT].mask_reg & 0x1;
+
+    case REG_K00_K03_INT_MASKS:
+        /* Input (K00-K03) interrupt masks */
+        return interrupts[INT_K00_K03_SLOT].mask_reg;
+
+    case REG_K10_K13_INT_MASKS:
+        /* Input (K10-K13) interrupt masks */
+        return interrupts[INT_K10_K13_SLOT].mask_reg;
+
+    case REG_PROG_TIMER_DATA_L:
+        /* Prog timer data (low) */
+        return prog_timer_data & 0xF;
+
+    case REG_PROG_TIMER_DATA_H:
+        /* Prog timer data (high) */
+        return (prog_timer_data >> 4) & 0xF;
+
+    case REG_PROG_TIMER_RELOAD_DATA_L:
+        /* Prog timer reload data (low) */
+        return prog_timer_rld & 0xF;
+
+    case REG_PROG_TIMER_RELOAD_DATA_H:
+        /* Prog timer reload data (high) */
+        return (prog_timer_rld >> 4) & 0xF;
+
+    case REG_K00_K03_INPUT_PORT:
+        /* Input port (K00-K03) */
+        return inputs[0].states;
+
+    case REG_K10_K13_INPUT_PORT:
+        /* Input port (K10-K13) */
+        return inputs[1].states;
+
+    case REG_K40_K43_BZ_OUTPUT_PORT:
+        /* Output port (R40-R43) */
+        return GET_IO_MEMORY(memory, n);
+
+    case REG_CPU_OSC3_CTRL:
+        /* CPU/OSC3 clocks switch, CPU voltage switch */
+        return GET_IO_MEMORY(memory, n);
+
+    case REG_LCD_CTRL:
+        /* LCD control */
+        return GET_IO_MEMORY(memory, n);
+
+    case REG_LCD_CONTRAST:
+        /* LCD contrast */
+        break;
+
+    case REG_SVD_CTRL:
+        /* SVD */
+        return GET_IO_MEMORY(memory, n) & 0x7; // Voltage always OK
+
+    case REG_BUZZER_CTRL1:
+        /* Buzzer config 1 */
+        return GET_IO_MEMORY(memory, n);
+
+    case REG_BUZZER_CTRL2:
+        /* Buzzer config 2 */
+        return GET_IO_MEMORY(memory, n) & 0x3; // Buzzer ready
+
+    case REG_CLK_WD_TIMER_CTRL:
+        /* Clock/Watchdog timer reset */
+        break;
+
+    case REG_SW_TIMER_CTRL:
+        /* Stopwatch stop/run/reset */
+        break;
+
+    case REG_PROG_TIMER_CTRL:
+        /* Prog timer stop/run/reset */
+        return !!prog_timer_enabled;
+
+    case REG_PROG_TIMER_CLK_SEL:
+        /* Prog timer clock selection */
+        break;
+
+    default:
+        g_hal->log(LOG_ERROR, "Read from unimplemented I/O 0x%03X - PC = 0x%04X\n", n, pc);
+    }
+
+    return 0;
+}
+
+static void set_io(u12_t n, u4_t v) {
+    switch(n) {
+    case REG_CLOCK_INT_MASKS:
+        /* Clock timer interrupt masks */
+        /* Assume 1Hz timer INT enabled (0x8) */
+        interrupts[INT_CLOCK_TIMER_SLOT].mask_reg = v;
+        break;
+
+    case REG_SW_INT_MASKS:
+        /* Stopwatch interrupt masks */
+        /* Assume all INT disabled */
+        interrupts[INT_STOPWATCH_SLOT].mask_reg = v;
+        break;
+
+    case REG_PROG_INT_MASKS:
+        /* Prog timer interrupt masks */
+        /* Assume Prog timer INT enabled (0x1) */
+        interrupts[INT_PROG_TIMER_SLOT].mask_reg = v;
+        break;
+
+    case REG_SERIAL_INT_MASKS:
+        /* Serial interface interrupt masks */
+        /* Assume all INT disabled */
+        interrupts[INT_K10_K13_SLOT].mask_reg = v;
+        break;
+
+    case REG_K00_K03_INT_MASKS:
+        /* Input (K00-K03) interrupt masks */
+        /* Assume all INT disabled */
+        interrupts[INT_SERIAL_SLOT].mask_reg = v;
+        break;
+
+    case REG_K10_K13_INT_MASKS:
+        /* Input (K10-K13) interrupt masks */
+        /* Assume all INT disabled */
+        interrupts[INT_K10_K13_SLOT].mask_reg = v;
+        break;
+
+    case REG_PROG_TIMER_RELOAD_DATA_L:
+        /* Prog timer reload data (low) */
+        prog_timer_rld = v | (prog_timer_rld & 0xF0);
+        break;
+
+    case REG_PROG_TIMER_RELOAD_DATA_H:
+        /* Prog timer reload data (high) */
+        prog_timer_rld = (prog_timer_rld & 0xF) | (v << 4);
+        break;
+
+    case REG_K00_K03_INPUT_PORT:
+        /* Input port (K00-K03) */
+        /* Write not allowed */
+        break;
+
+    case REG_K40_K43_BZ_OUTPUT_PORT:
+        /* Output port (R40-R43) */
+        //g_hal->log(LOG_INFO, "Output/Buzzer: 0x%X\n", v);
+        hw_enable_buzzer(!(v & 0x8));
+        break;
+
+    case REG_CPU_OSC3_CTRL:
+        /* CPU/OSC3 clocks switch, CPU voltage switch */
+        /* Assume 32,768 OSC1 selected, OSC3 off, battery >= 3,1V (0x1) */
+        break;
+
+    case REG_LCD_CTRL:
+        /* LCD control */
+        break;
+
+    case REG_LCD_CONTRAST:
+        /* LCD contrast */
+        /* Assume medium contrast (0x8) */
+        break;
+
+    case REG_SVD_CTRL:
+        /* SVD */
+        /* Assume battery voltage always OK (0x6) */
+        break;
+
+    case REG_BUZZER_CTRL1:
+        /* Buzzer config 1 */
+        hw_set_buzzer_freq(v & 0x7);
+        break;
+
+    case REG_BUZZER_CTRL2:
+        /* Buzzer config 2 */
+        break;
+
+    case REG_CLK_WD_TIMER_CTRL:
+        /* Clock/Watchdog timer reset */
+        /* Ignore watchdog */
+        break;
+
+    case REG_SW_TIMER_CTRL:
+        /* Stopwatch stop/run/reset */
+        break;
+
+    case REG_PROG_TIMER_CTRL:
+        /* Prog timer stop/run/reset */
+        if(v & 0x2) {
+            prog_timer_data = prog_timer_rld;
+        }
+
+        if((v & 0x1) && !prog_timer_enabled) {
+            prog_timer_timestamp = tick_counter;
+        }
+
+        prog_timer_enabled = v & 0x1;
+        break;
+
+    case REG_PROG_TIMER_CLK_SEL:
+        /* Prog timer clock selection */
+        /* Assume 256Hz, output disabled */
+        break;
+
+    default:
+        g_hal->log(LOG_ERROR, "Write 0x%X to unimplemented I/O 0x%03X - PC = 0x%04X\n", v, n, pc);
+    }
+}
+
+static void set_lcd(u12_t n, u4_t v) {
+    u8_t i;
+    u8_t seg, com0;
+
+    seg = ((n & 0x7F) >> 1);
+    com0 = (((n & 0x80) >> 7) * 8 + (n & 0x1) * 4);
+
+    for(i = 0; i < 4; i++) {
+        hw_set_lcd_pin(seg, com0 + i, (v >> i) & 0x1);
+    }
+}
+
+static u4_t get_memory(u12_t n) {
+    u4_t res = 0;
+
+    if(n < MEM_RAM_SIZE) {
+        /* RAM */
+        g_hal->log(LOG_MEMORY, "RAM              - ");
+        res = GET_RAM_MEMORY(memory, n);
+    } else if(n >= MEM_DISPLAY1_ADDR && n < (MEM_DISPLAY1_ADDR + MEM_DISPLAY1_SIZE)) {
+        /* Display Memory 1 */
+        g_hal->log(LOG_MEMORY, "Display Memory 1 - ");
+        res = GET_DISP1_MEMORY(memory, n);
+    } else if(n >= MEM_DISPLAY2_ADDR && n < (MEM_DISPLAY2_ADDR + MEM_DISPLAY2_SIZE)) {
+        /* Display Memory 2 */
+        g_hal->log(LOG_MEMORY, "Display Memory 2 - ");
+        res = GET_DISP2_MEMORY(memory, n);
+    } else if(n >= MEM_IO_ADDR && n < (MEM_IO_ADDR + MEM_IO_SIZE)) {
+        /* I/O Memory */
+        g_hal->log(LOG_MEMORY, "I/O              - ");
+        res = get_io(n);
+    } else {
+        g_hal->log(LOG_ERROR, "Read from invalid memory address 0x%03X - PC = 0x%04X\n", n, pc);
+        return 0;
+    }
+
+    g_hal->log(LOG_MEMORY, "Read  0x%X - Address 0x%03X - PC = 0x%04X\n", res, n, pc);
+
+    return res;
+}
+
+static void set_memory(u12_t n, u4_t v) {
+    /* Cache any data written to a valid address, and process it */
+    if(n < MEM_RAM_SIZE) {
+        /* RAM */
+        SET_RAM_MEMORY(memory, n, v);
+        g_hal->log(LOG_MEMORY, "RAM              - ");
+    } else if(n >= MEM_DISPLAY1_ADDR && n < (MEM_DISPLAY1_ADDR + MEM_DISPLAY1_SIZE)) {
+        /* Display Memory 1 */
+        SET_DISP1_MEMORY(memory, n, v);
+        set_lcd(n, v);
+        g_hal->log(LOG_MEMORY, "Display Memory 1 - ");
+    } else if(n >= MEM_DISPLAY2_ADDR && n < (MEM_DISPLAY2_ADDR + MEM_DISPLAY2_SIZE)) {
+        /* Display Memory 2 */
+        SET_DISP2_MEMORY(memory, n, v);
+        set_lcd(n, v);
+        g_hal->log(LOG_MEMORY, "Display Memory 2 - ");
+    } else if(n >= MEM_IO_ADDR && n < (MEM_IO_ADDR + MEM_IO_SIZE)) {
+        /* I/O Memory */
+        SET_IO_MEMORY(memory, n, v);
+        set_io(n, v);
+        g_hal->log(LOG_MEMORY, "I/O              - ");
+    } else {
+        g_hal->log(
+            LOG_ERROR, "Write 0x%X to invalid memory address 0x%03X - PC = 0x%04X\n", v, n, pc);
+        return;
+    }
+
+    g_hal->log(LOG_MEMORY, "Write 0x%X - Address 0x%03X - PC = 0x%04X\n", v, n, pc);
+}
+
+void cpu_refresh_hw(void) {
+    static const struct range {
+        u12_t addr;
+        u12_t size;
+    } refresh_locs[] = {
+        {MEM_DISPLAY1_ADDR, MEM_DISPLAY1_SIZE}, /* Display Memory 1 */
+        {MEM_DISPLAY2_ADDR, MEM_DISPLAY2_SIZE}, /* Display Memory 2 */
+        {REG_BUZZER_CTRL1, 1}, /* Buzzer frequency */
+        {REG_K40_K43_BZ_OUTPUT_PORT, 1}, /* Buzzer enabled */
+
+        {0, 0}, // end of list
+    };
+
+    for(int i = 0; refresh_locs[i].size != 0; i++) {
+        for(u12_t n = refresh_locs[i].addr; n < (refresh_locs[i].addr + refresh_locs[i].size);
+            n++) {
+            set_memory(n, GET_MEMORY(memory, n));
+        }
+    }
+}
+
+static u4_t get_rq(u12_t rq) {
+    switch(rq & 0x3) {
+    case 0x0:
+        return a;
+
+    case 0x1:
+        return b;
+
+    case 0x2:
+        return M(x);
+
+    case 0x3:
+        return M(y);
+    }
+
+    return 0;
+}
+
+static void set_rq(u12_t rq, u4_t v) {
+    switch(rq & 0x3) {
+    case 0x0:
+        a = v;
+        break;
+
+    case 0x1:
+        b = v;
+        break;
+
+    case 0x2:
+        SET_M(x, v);
+        break;
+
+    case 0x3:
+        SET_M(y, v);
+        break;
+    }
+}
+
+/* Instructions */
+static void op_pset_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    np = arg0;
+}
+
+static void op_jp_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    next_pc = arg0 | (np << 8);
+}
+
+static void op_jp_c_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    if(flags & FLAG_C) {
+        next_pc = arg0 | (np << 8);
+    }
+}
+
+static void op_jp_nc_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    if(!(flags & FLAG_C)) {
+        next_pc = arg0 | (np << 8);
+    }
+}
+
+static void op_jp_z_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    if(flags & FLAG_Z) {
+        next_pc = arg0 | (np << 8);
+    }
+}
+
+static void op_jp_nz_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    if(!(flags & FLAG_Z)) {
+        next_pc = arg0 | (np << 8);
+    }
+}
+
+static void op_jpba_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    next_pc = a | (b << 4) | (np << 8);
+}
+
+static void op_call_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    pc = (pc + 1) & 0x1FFF; // This does not actually change the PC register
+    SET_M(sp - 1, PCP);
+    SET_M(sp - 2, PCSH);
+    SET_M(sp - 3, PCSL);
+    sp = (sp - 3) & 0xFF;
+    next_pc = TO_PC(PCB, NPP, arg0);
+    call_depth++;
+}
+
+static void op_calz_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    pc = (pc + 1) & 0x1FFF; // This does not actually change the PC register
+    SET_M(sp - 1, PCP);
+    SET_M(sp - 2, PCSH);
+    SET_M(sp - 3, PCSL);
+    sp = (sp - 3) & 0xFF;
+    next_pc = TO_PC(PCB, 0, arg0);
+    call_depth++;
+}
+
+static void op_ret_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    next_pc = M(sp) | (M(sp + 1) << 4) | (M(sp + 2) << 8) | (PCB << 12);
+    sp = (sp + 3) & 0xFF;
+    call_depth--;
+}
+
+static void op_rets_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    next_pc = M(sp) | (M(sp + 1) << 4) | (M(sp + 2) << 8) | (PCB << 12);
+    sp = (sp + 3) & 0xFF;
+    next_pc = (pc + 1) & 0x1FFF;
+    call_depth--;
+}
+
+static void op_retd_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    next_pc = M(sp) | (M(sp + 1) << 4) | (M(sp + 2) << 8) | (PCB << 12);
+    sp = (sp + 3) & 0xFF;
+    SET_M(x, arg0 & 0xF);
+    SET_M(x + 1, (arg0 >> 4) & 0xF);
+    x = ((x + 2) & 0xFF) | (XP << 8);
+    call_depth--;
+}
+
+static void op_nop5_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+}
+
+static void op_nop7_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+}
+
+static void op_halt_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    g_hal->halt();
+}
+
+static void op_inc_x_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    x = ((x + 1) & 0xFF) | (XP << 8);
+}
+
+static void op_inc_y_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    y = ((y + 1) & 0xFF) | (YP << 8);
+}
+
+static void op_ld_x_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    x = arg0 | (XP << 8);
+}
+
+static void op_ld_y_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    y = arg0 | (YP << 8);
+}
+
+static void op_ld_xp_r_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    x = XHL | (RQ(arg0) << 8);
+}
+
+static void op_ld_xh_r_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    x = XL | (RQ(arg0) << 4) | (XP << 8);
+}
+
+static void op_ld_xl_r_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    x = RQ(arg0) | (XH << 4) | (XP << 8);
+}
+
+static void op_ld_yp_r_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    y = YHL | (RQ(arg0) << 8);
+}
+
+static void op_ld_yh_r_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    y = YL | (RQ(arg0) << 4) | (YP << 8);
+}
+
+static void op_ld_yl_r_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    y = RQ(arg0) | (YH << 4) | (YP << 8);
+}
+
+static void op_ld_r_xp_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    SET_RQ(arg0, XP);
+}
+
+static void op_ld_r_xh_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    SET_RQ(arg0, XH);
+}
+
+static void op_ld_r_xl_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    SET_RQ(arg0, XL);
+}
+
+static void op_ld_r_yp_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    SET_RQ(arg0, YP);
+}
+
+static void op_ld_r_yh_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    SET_RQ(arg0, YH);
+}
+
+static void op_ld_r_yl_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    SET_RQ(arg0, YL);
+}
+
+static void op_adc_xh_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    u8_t tmp;
+
+    tmp = XH + arg0 + C;
+    x = XL | ((tmp & 0xF) << 4) | (XP << 8);
+    if(tmp >> 4) {
+        SET_C();
+    } else {
+        CLEAR_C();
+    }
+    if(!(tmp & 0xF)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_adc_xl_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    u8_t tmp;
+
+    tmp = XL + arg0 + C;
+    x = (tmp & 0xF) | (XH << 4) | (XP << 8);
+    if(tmp >> 4) {
+        SET_C();
+    } else {
+        CLEAR_C();
+    }
+    if(!(tmp & 0xF)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_adc_yh_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    u8_t tmp;
+
+    tmp = YH + arg0 + C;
+    y = YL | ((tmp & 0xF) << 4) | (YP << 8);
+    if(tmp >> 4) {
+        SET_C();
+    } else {
+        CLEAR_C();
+    }
+    if(!(tmp & 0xF)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_adc_yl_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    u8_t tmp;
+
+    tmp = YL + arg0 + C;
+    y = (tmp & 0xF) | (YH << 4) | (YP << 8);
+    if(tmp >> 4) {
+        SET_C();
+    } else {
+        CLEAR_C();
+    }
+    if(!(tmp & 0xF)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_cp_xh_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    if(XH < arg0) {
+        SET_C();
+    } else {
+        CLEAR_C();
+    }
+    if(XH == arg0) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_cp_xl_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    if(XL < arg0) {
+        SET_C();
+    } else {
+        CLEAR_C();
+    }
+    if(XL == arg0) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_cp_yh_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    if(YH < arg0) {
+        SET_C();
+    } else {
+        CLEAR_C();
+    }
+    if(YH == arg0) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_cp_yl_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    if(YL < arg0) {
+        SET_C();
+    } else {
+        CLEAR_C();
+    }
+    if(YL == arg0) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_ld_r_i_cb(u8_t arg0, u8_t arg1) {
+    SET_RQ(arg0, arg1);
+}
+
+static void op_ld_r_q_cb(u8_t arg0, u8_t arg1) {
+    SET_RQ(arg0, RQ(arg1));
+}
+
+static void op_ld_a_mn_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    a = M(arg0);
+}
+
+static void op_ld_b_mn_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    b = M(arg0);
+}
+
+static void op_ld_mn_a_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    SET_M(arg0, a);
+}
+
+static void op_ld_mn_b_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    SET_M(arg0, b);
+}
+
+static void op_ldpx_mx_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    SET_M(x, arg0);
+    x = ((x + 1) & 0xFF) | (XP << 8);
+}
+
+static void op_ldpx_r_cb(u8_t arg0, u8_t arg1) {
+    SET_RQ(arg0, RQ(arg1));
+    x = ((x + 1) & 0xFF) | (XP << 8);
+}
+
+static void op_ldpy_my_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    SET_M(y, arg0);
+    y = ((y + 1) & 0xFF) | (YP << 8);
+}
+
+static void op_ldpy_r_cb(u8_t arg0, u8_t arg1) {
+    SET_RQ(arg0, RQ(arg1));
+    y = ((y + 1) & 0xFF) | (YP << 8);
+}
+
+static void op_lbpx_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    SET_M(x, arg0 & 0xF);
+    SET_M(x + 1, (arg0 >> 4) & 0xF);
+    x = ((x + 2) & 0xFF) | (XP << 8);
+}
+
+static void op_set_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    flags |= arg0;
+}
+
+static void op_rst_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    flags &= arg0;
+}
+
+static void op_scf_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    SET_C();
+}
+
+static void op_rcf_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    CLEAR_C();
+}
+
+static void op_szf_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    SET_Z();
+}
+
+static void op_rzf_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    CLEAR_Z();
+}
+
+static void op_sdf_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    SET_D();
+}
+
+static void op_rdf_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    CLEAR_D();
+}
+
+static void op_ei_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    SET_I();
+}
+
+static void op_di_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    CLEAR_I();
+}
+
+static void op_inc_sp_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    sp = (sp + 1) & 0xFF;
+}
+
+static void op_dec_sp_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    sp = (sp - 1) & 0xFF;
+}
+
+static void op_push_r_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    sp = (sp - 1) & 0xFF;
+    SET_M(sp, RQ(arg0));
+}
+
+static void op_push_xp_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    sp = (sp - 1) & 0xFF;
+    SET_M(sp, XP);
+}
+
+static void op_push_xh_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    sp = (sp - 1) & 0xFF;
+    SET_M(sp, XH);
+}
+
+static void op_push_xl_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    sp = (sp - 1) & 0xFF;
+    SET_M(sp, XL);
+}
+
+static void op_push_yp_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    sp = (sp - 1) & 0xFF;
+    SET_M(sp, YP);
+}
+
+static void op_push_yh_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    sp = (sp - 1) & 0xFF;
+    SET_M(sp, YH);
+}
+
+static void op_push_yl_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    sp = (sp - 1) & 0xFF;
+    SET_M(sp, YL);
+}
+
+static void op_push_f_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    sp = (sp - 1) & 0xFF;
+    SET_M(sp, flags);
+}
+
+static void op_pop_r_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    SET_RQ(arg0, M(sp));
+    sp = (sp + 1) & 0xFF;
+}
+
+static void op_pop_xp_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    x = XL | (XH << 4) | (M(sp) << 8);
+    sp = (sp + 1) & 0xFF;
+}
+
+static void op_pop_xh_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    x = XL | (M(sp) << 4) | (XP << 8);
+    sp = (sp + 1) & 0xFF;
+}
+
+static void op_pop_xl_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    x = M(sp) | (XH << 4) | (XP << 8);
+    sp = (sp + 1) & 0xFF;
+}
+
+static void op_pop_yp_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    y = YL | (YH << 4) | (M(sp) << 8);
+    sp = (sp + 1) & 0xFF;
+}
+
+static void op_pop_yh_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    y = YL | (M(sp) << 4) | (YP << 8);
+    sp = (sp + 1) & 0xFF;
+}
+
+static void op_pop_yl_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    y = M(sp) | (YH << 4) | (YP << 8);
+    sp = (sp + 1) & 0xFF;
+}
+
+static void op_pop_f_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg0);
+    UNUSED(arg1);
+    flags = M(sp);
+    sp = (sp + 1) & 0xFF;
+}
+
+static void op_ld_sph_r_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    sp = SPL | (RQ(arg0) << 4);
+}
+
+static void op_ld_spl_r_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    sp = RQ(arg0) | (SPH << 4);
+}
+
+static void op_ld_r_sph_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    SET_RQ(arg0, SPH);
+}
+
+static void op_ld_r_spl_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    SET_RQ(arg0, SPL);
+}
+
+static void op_add_r_i_cb(u8_t arg0, u8_t arg1) {
+    u8_t tmp;
+
+    tmp = RQ(arg0) + arg1;
+    if(D) {
+        if(tmp >= 10) {
+            SET_RQ(arg0, (tmp - 10) & 0xF);
+            SET_C();
+        } else {
+            SET_RQ(arg0, tmp);
+            CLEAR_C();
+        }
+    } else {
+        SET_RQ(arg0, tmp & 0xF);
+        if(tmp >> 4) {
+            SET_C();
+        } else {
+            CLEAR_C();
+        }
+    }
+    if(!RQ(arg0)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_add_r_q_cb(u8_t arg0, u8_t arg1) {
+    u8_t tmp;
+
+    tmp = RQ(arg0) + RQ(arg1);
+    if(D) {
+        if(tmp >= 10) {
+            SET_RQ(arg0, (tmp - 10) & 0xF);
+            SET_C();
+        } else {
+            SET_RQ(arg0, tmp);
+            CLEAR_C();
+        }
+    } else {
+        SET_RQ(arg0, tmp & 0xF);
+        if(tmp >> 4) {
+            SET_C();
+        } else {
+            CLEAR_C();
+        }
+    }
+    if(!RQ(arg0)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_adc_r_i_cb(u8_t arg0, u8_t arg1) {
+    u8_t tmp;
+
+    tmp = RQ(arg0) + arg1 + C;
+    if(D) {
+        if(tmp >= 10) {
+            SET_RQ(arg0, (tmp - 10) & 0xF);
+            SET_C();
+        } else {
+            SET_RQ(arg0, tmp);
+            CLEAR_C();
+        }
+    } else {
+        SET_RQ(arg0, tmp & 0xF);
+        if(tmp >> 4) {
+            SET_C();
+        } else {
+            CLEAR_C();
+        }
+    }
+    if(!RQ(arg0)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_adc_r_q_cb(u8_t arg0, u8_t arg1) {
+    u8_t tmp;
+
+    tmp = RQ(arg0) + RQ(arg1) + C;
+    if(D) {
+        if(tmp >= 10) {
+            SET_RQ(arg0, (tmp - 10) & 0xF);
+            SET_C();
+        } else {
+            SET_RQ(arg0, tmp);
+            CLEAR_C();
+        }
+    } else {
+        SET_RQ(arg0, tmp & 0xF);
+        if(tmp >> 4) {
+            SET_C();
+        } else {
+            CLEAR_C();
+        }
+    }
+    if(!RQ(arg0)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_sub_cb(u8_t arg0, u8_t arg1) {
+    u8_t tmp;
+
+    tmp = RQ(arg0) - RQ(arg1);
+    if(D) {
+        if(tmp >> 4) {
+            SET_RQ(arg0, (tmp - 6) & 0xF);
+        } else {
+            SET_RQ(arg0, tmp);
+        }
+    } else {
+        SET_RQ(arg0, tmp & 0xF);
+    }
+    if(tmp >> 4) {
+        SET_C();
+    } else {
+        CLEAR_C();
+    }
+    if(!RQ(arg0)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_sbc_r_i_cb(u8_t arg0, u8_t arg1) {
+    u8_t tmp;
+
+    tmp = RQ(arg0) - arg1 - C;
+    if(D) {
+        if(tmp >> 4) {
+            SET_RQ(arg0, (tmp - 6) & 0xF);
+        } else {
+            SET_RQ(arg0, tmp);
+        }
+    } else {
+        SET_RQ(arg0, tmp & 0xF);
+    }
+    if(tmp >> 4) {
+        SET_C();
+    } else {
+        CLEAR_C();
+    }
+    if(!RQ(arg0)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_sbc_r_q_cb(u8_t arg0, u8_t arg1) {
+    u8_t tmp;
+
+    tmp = RQ(arg0) - RQ(arg1) - C;
+    if(D) {
+        if(tmp >> 4) {
+            SET_RQ(arg0, (tmp - 6) & 0xF);
+        } else {
+            SET_RQ(arg0, tmp);
+        }
+    } else {
+        SET_RQ(arg0, tmp & 0xF);
+    }
+    if(tmp >> 4) {
+        SET_C();
+    } else {
+        CLEAR_C();
+    }
+    if(!RQ(arg0)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_and_r_i_cb(u8_t arg0, u8_t arg1) {
+    SET_RQ(arg0, RQ(arg0) & arg1);
+    if(!RQ(arg0)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_and_r_q_cb(u8_t arg0, u8_t arg1) {
+    SET_RQ(arg0, RQ(arg0) & RQ(arg1));
+    if(!RQ(arg0)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_or_r_i_cb(u8_t arg0, u8_t arg1) {
+    SET_RQ(arg0, RQ(arg0) | arg1);
+    if(!RQ(arg0)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_or_r_q_cb(u8_t arg0, u8_t arg1) {
+    SET_RQ(arg0, RQ(arg0) | RQ(arg1));
+    if(!RQ(arg0)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_xor_r_i_cb(u8_t arg0, u8_t arg1) {
+    SET_RQ(arg0, RQ(arg0) ^ arg1);
+    if(!RQ(arg0)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_xor_r_q_cb(u8_t arg0, u8_t arg1) {
+    SET_RQ(arg0, RQ(arg0) ^ RQ(arg1));
+    if(!RQ(arg0)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_cp_r_i_cb(u8_t arg0, u8_t arg1) {
+    if(RQ(arg0) < arg1) {
+        SET_C();
+    } else {
+        CLEAR_C();
+    }
+    if(RQ(arg0) == arg1) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_cp_r_q_cb(u8_t arg0, u8_t arg1) {
+    if(RQ(arg0) < RQ(arg1)) {
+        SET_C();
+    } else {
+        CLEAR_C();
+    }
+    if(RQ(arg0) == RQ(arg1)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_fan_r_i_cb(u8_t arg0, u8_t arg1) {
+    if(!(RQ(arg0) & arg1)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_fan_r_q_cb(u8_t arg0, u8_t arg1) {
+    if(!(RQ(arg0) & RQ(arg1))) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_rlc_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    u8_t tmp;
+
+    tmp = (RQ(arg0) << 1) | C;
+    if(RQ(arg0) & 0x8) {
+        SET_C();
+    } else {
+        CLEAR_C();
+    }
+    SET_RQ(arg0, tmp & 0xF);
+    /* No need to set Z (issue in DS) */
+}
+
+static void op_rrc_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    u8_t tmp;
+
+    tmp = (RQ(arg0) >> 1) | (C << 3);
+    if(RQ(arg0) & 0x1) {
+        SET_C();
+    } else {
+        CLEAR_C();
+    }
+    SET_RQ(arg0, tmp & 0xF);
+    /* No need to set Z (issue in DS) */
+}
+
+static void op_inc_mn_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    u8_t tmp;
+
+    tmp = M(arg0) + 1;
+    SET_M(arg0, tmp & 0xF);
+    if(tmp >> 4) {
+        SET_C();
+    } else {
+        CLEAR_C();
+    }
+    if(!M(arg0)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_dec_mn_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    u8_t tmp;
+
+    tmp = M(arg0) - 1;
+    SET_M(arg0, tmp & 0xF);
+    if(tmp >> 4) {
+        SET_C();
+    } else {
+        CLEAR_C();
+    }
+    if(!M(arg0)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+static void op_acpx_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    u8_t tmp;
+
+    tmp = M(x) + RQ(arg0) + C;
+    if(D) {
+        if(tmp >= 10) {
+            SET_M(x, (tmp - 10) & 0xF);
+            SET_C();
+        } else {
+            SET_M(x, tmp);
+            CLEAR_C();
+        }
+    } else {
+        SET_M(x, tmp & 0xF);
+        if(tmp >> 4) {
+            SET_C();
+        } else {
+            CLEAR_C();
+        }
+    }
+    if(!M(x)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+    x = ((x + 1) & 0xFF) | (XP << 8);
+}
+
+static void op_acpy_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    u8_t tmp;
+
+    tmp = M(y) + RQ(arg0) + C;
+    if(D) {
+        if(tmp >= 10) {
+            SET_M(y, (tmp - 10) & 0xF);
+            SET_C();
+        } else {
+            SET_M(y, tmp);
+            CLEAR_C();
+        }
+    } else {
+        SET_M(y, tmp & 0xF);
+        if(tmp >> 4) {
+            SET_C();
+        } else {
+            CLEAR_C();
+        }
+    }
+    if(!M(y)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+    y = ((y + 1) & 0xFF) | (YP << 8);
+}
+
+static void op_scpx_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    u8_t tmp;
+
+    tmp = M(x) - RQ(arg0) - C;
+    if(D) {
+        if(tmp >> 4) {
+            SET_M(x, (tmp - 6) & 0xF);
+        } else {
+            SET_M(x, tmp);
+        }
+    } else {
+        SET_M(x, tmp & 0xF);
+    }
+    if(tmp >> 4) {
+        SET_C();
+    } else {
+        CLEAR_C();
+    }
+    if(!M(x)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+    x = ((x + 1) & 0xFF) | (XP << 8);
+}
+
+static void op_scpy_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    u8_t tmp;
+
+    tmp = M(y) - RQ(arg0) - C;
+    if(D) {
+        if(tmp >> 4) {
+            SET_M(y, (tmp - 6) & 0xF);
+        } else {
+            SET_M(y, tmp);
+        }
+    } else {
+        SET_M(y, tmp & 0xF);
+    }
+    if(tmp >> 4) {
+        SET_C();
+    } else {
+        CLEAR_C();
+    }
+    if(!M(y)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+    y = ((y + 1) & 0xFF) | (YP << 8);
+}
+
+static void op_not_cb(u8_t arg0, u8_t arg1) {
+    UNUSED(arg1);
+    SET_RQ(arg0, ~RQ(arg0) & 0xF);
+    if(!RQ(arg0)) {
+        SET_Z();
+    } else {
+        CLEAR_Z();
+    }
+}
+
+/* The E0C6S46 supported instructions */
+static const op_t ops[] = {
+    {"PSET #0x%02X            ", 0xE40, MASK_7B, 0, 0, 5, &op_pset_cb}, // PSET
+    {"JP   #0x%02X            ", 0x000, MASK_4B, 0, 0, 5, &op_jp_cb}, // JP
+    {"JP   C #0x%02X          ", 0x200, MASK_4B, 0, 0, 5, &op_jp_c_cb}, // JP_C
+    {"JP   NC #0x%02X         ", 0x300, MASK_4B, 0, 0, 5, &op_jp_nc_cb}, // JP_NC
+    {"JP   Z #0x%02X          ", 0x600, MASK_4B, 0, 0, 5, &op_jp_z_cb}, // JP_Z
+    {"JP   NZ #0x%02X         ", 0x700, MASK_4B, 0, 0, 5, &op_jp_nz_cb}, // JP_NZ
+    {"JPBA                  ", 0xFE8, MASK_12B, 0, 0, 5, &op_jpba_cb}, // JPBA
+    {"CALL #0x%02X            ", 0x400, MASK_4B, 0, 0, 7, &op_call_cb}, // CALL
+    {"CALZ #0x%02X            ", 0x500, MASK_4B, 0, 0, 7, &op_calz_cb}, // CALZ
+    {"RET                   ", 0xFDF, MASK_12B, 0, 0, 7, &op_ret_cb}, // RET
+    {"RETS                  ", 0xFDE, MASK_12B, 0, 0, 12, &op_rets_cb}, // RETS
+    {"RETD #0x%02X            ", 0x100, MASK_4B, 0, 0, 12, &op_retd_cb}, // RETD
+    {"NOP5                  ", 0xFFB, MASK_12B, 0, 0, 5, &op_nop5_cb}, // NOP5
+    {"NOP7                  ", 0xFFF, MASK_12B, 0, 0, 7, &op_nop7_cb}, // NOP7
+    {"HALT                  ", 0xFF8, MASK_12B, 0, 0, 5, &op_halt_cb}, // HALT
+    {"INC  X #0x%02X          ", 0xEE0, MASK_12B, 0, 0, 5, &op_inc_x_cb}, // INC_X
+    {"INC  Y #0x%02X          ", 0xEF0, MASK_12B, 0, 0, 5, &op_inc_y_cb}, // INC_Y
+    {"LD   X #0x%02X          ", 0xB00, MASK_4B, 0, 0, 5, &op_ld_x_cb}, // LD_X
+    {"LD   Y #0x%02X          ", 0x800, MASK_4B, 0, 0, 5, &op_ld_y_cb}, // LD_Y
+    {"LD   XP R(#0x%02X)      ", 0xE80, MASK_10B, 0, 0, 5, &op_ld_xp_r_cb}, // LD_XP_R
+    {"LD   XH R(#0x%02X)      ", 0xE84, MASK_10B, 0, 0, 5, &op_ld_xh_r_cb}, // LD_XH_R
+    {"LD   XL R(#0x%02X)      ", 0xE88, MASK_10B, 0, 0, 5, &op_ld_xl_r_cb}, // LD_XL_R
+    {"LD   YP R(#0x%02X)      ", 0xE90, MASK_10B, 0, 0, 5, &op_ld_yp_r_cb}, // LD_YP_R
+    {"LD   YH R(#0x%02X)      ", 0xE94, MASK_10B, 0, 0, 5, &op_ld_yh_r_cb}, // LD_YH_R
+    {"LD   YL R(#0x%02X)      ", 0xE98, MASK_10B, 0, 0, 5, &op_ld_yl_r_cb}, // LD_YL_R
+    {"LD   R(#0x%02X) XP      ", 0xEA0, MASK_10B, 0, 0, 5, &op_ld_r_xp_cb}, // LD_R_XP
+    {"LD   R(#0x%02X) XH      ", 0xEA4, MASK_10B, 0, 0, 5, &op_ld_r_xh_cb}, // LD_R_XH
+    {"LD   R(#0x%02X) XL      ", 0xEA8, MASK_10B, 0, 0, 5, &op_ld_r_xl_cb}, // LD_R_XL
+    {"LD   R(#0x%02X) YP      ", 0xEB0, MASK_10B, 0, 0, 5, &op_ld_r_yp_cb}, // LD_R_YP
+    {"LD   R(#0x%02X) YH      ", 0xEB4, MASK_10B, 0, 0, 5, &op_ld_r_yh_cb}, // LD_R_YH
+    {"LD   R(#0x%02X) YL      ", 0xEB8, MASK_10B, 0, 0, 5, &op_ld_r_yl_cb}, // LD_R_YL
+    {"ADC  XH #0x%02X         ", 0xA00, MASK_8B, 0, 0, 7, &op_adc_xh_cb}, // ADC_XH
+    {"ADC  XL #0x%02X         ", 0xA10, MASK_8B, 0, 0, 7, &op_adc_xl_cb}, // ADC_XL
+    {"ADC  YH #0x%02X         ", 0xA20, MASK_8B, 0, 0, 7, &op_adc_yh_cb}, // ADC_YH
+    {"ADC  YL #0x%02X         ", 0xA30, MASK_8B, 0, 0, 7, &op_adc_yl_cb}, // ADC_YL
+    {"CP   XH #0x%02X         ", 0xA40, MASK_8B, 0, 0, 7, &op_cp_xh_cb}, // CP_XH
+    {"CP   XL #0x%02X         ", 0xA50, MASK_8B, 0, 0, 7, &op_cp_xl_cb}, // CP_XL
+    {"CP   YH #0x%02X         ", 0xA60, MASK_8B, 0, 0, 7, &op_cp_yh_cb}, // CP_YH
+    {"CP   YL #0x%02X         ", 0xA70, MASK_8B, 0, 0, 7, &op_cp_yl_cb}, // CP_YL
+    {"LD   R(#0x%02X) #0x%02X   ", 0xE00, MASK_6B, 4, 0x030, 5, &op_ld_r_i_cb}, // LD_R_I
+    {"LD   R(#0x%02X) Q(#0x%02X)", 0xEC0, MASK_8B, 2, 0x00C, 5, &op_ld_r_q_cb}, // LD_R_Q
+    {"LD   A M(#0x%02X)       ", 0xFA0, MASK_8B, 0, 0, 5, &op_ld_a_mn_cb}, // LD_A_MN
+    {"LD   B M(#0x%02X)       ", 0xFB0, MASK_8B, 0, 0, 5, &op_ld_b_mn_cb}, // LD_B_MN
+    {"LD   M(#0x%02X) A       ", 0xF80, MASK_8B, 0, 0, 5, &op_ld_mn_a_cb}, // LD_MN_A
+    {"LD   M(#0x%02X) B       ", 0xF90, MASK_8B, 0, 0, 5, &op_ld_mn_b_cb}, // LD_MN_B
+    {"LDPX MX #0x%02X         ", 0xE60, MASK_8B, 0, 0, 5, &op_ldpx_mx_cb}, // LDPX_MX
+    {"LDPX R(#0x%02X) Q(#0x%02X)", 0xEE0, MASK_8B, 2, 0x00C, 5, &op_ldpx_r_cb}, // LDPX_R
+    {"LDPY MY #0x%02X         ", 0xE70, MASK_8B, 0, 0, 5, &op_ldpy_my_cb}, // LDPY_MY
+    {"LDPY R(#0x%02X) Q(#0x%02X)", 0xEF0, MASK_8B, 2, 0x00C, 5, &op_ldpy_r_cb}, // LDPY_R
+    {"LBPX #0x%02X            ", 0x900, MASK_4B, 0, 0, 5, &op_lbpx_cb}, // LBPX
+    {"SET  #0x%02X            ", 0xF40, MASK_8B, 0, 0, 7, &op_set_cb}, // SET
+    {"RST  #0x%02X            ", 0xF50, MASK_8B, 0, 0, 7, &op_rst_cb}, // RST
+    {"SCF                   ", 0xF41, MASK_12B, 0, 0, 7, &op_scf_cb}, // SCF
+    {"RCF                   ", 0xF5E, MASK_12B, 0, 0, 7, &op_rcf_cb}, // RCF
+    {"SZF                   ", 0xF42, MASK_12B, 0, 0, 7, &op_szf_cb}, // SZF
+    {"RZF                   ", 0xF5D, MASK_12B, 0, 0, 7, &op_rzf_cb}, // RZF
+    {"SDF                   ", 0xF44, MASK_12B, 0, 0, 7, &op_sdf_cb}, // SDF
+    {"RDF                   ", 0xF5B, MASK_12B, 0, 0, 7, &op_rdf_cb}, // RDF
+    {"EI                    ", 0xF48, MASK_12B, 0, 0, 7, &op_ei_cb}, // EI
+    {"DI                    ", 0xF57, MASK_12B, 0, 0, 7, &op_di_cb}, // DI
+    {"INC  SP               ", 0xFDB, MASK_12B, 0, 0, 5, &op_inc_sp_cb}, // INC_SP
+    {"DEC  SP               ", 0xFCB, MASK_12B, 0, 0, 5, &op_dec_sp_cb}, // DEC_SP
+    {"PUSH R(#0x%02X)         ", 0xFC0, MASK_10B, 0, 0, 5, &op_push_r_cb}, // PUSH_R
+    {"PUSH XP               ", 0xFC4, MASK_12B, 0, 0, 5, &op_push_xp_cb}, // PUSH_XP
+    {"PUSH XH               ", 0xFC5, MASK_12B, 0, 0, 5, &op_push_xh_cb}, // PUSH_XH
+    {"PUSH XL               ", 0xFC6, MASK_12B, 0, 0, 5, &op_push_xl_cb}, // PUSH_XL
+    {"PUSH YP               ", 0xFC7, MASK_12B, 0, 0, 5, &op_push_yp_cb}, // PUSH_YP
+    {"PUSH YH               ", 0xFC8, MASK_12B, 0, 0, 5, &op_push_yh_cb}, // PUSH_YH
+    {"PUSH YL               ", 0xFC9, MASK_12B, 0, 0, 5, &op_push_yl_cb}, // PUSH_YL
+    {"PUSH F                ", 0xFCA, MASK_12B, 0, 0, 5, &op_push_f_cb}, // PUSH_F
+    {"POP  R(#0x%02X)         ", 0xFD0, MASK_10B, 0, 0, 5, &op_pop_r_cb}, // POP_R
+    {"POP  XP               ", 0xFD4, MASK_12B, 0, 0, 5, &op_pop_xp_cb}, // POP_XP
+    {"POP  XH               ", 0xFD5, MASK_12B, 0, 0, 5, &op_pop_xh_cb}, // POP_XH
+    {"POP  XL               ", 0xFD6, MASK_12B, 0, 0, 5, &op_pop_xl_cb}, // POP_XL
+    {"POP  YP               ", 0xFD7, MASK_12B, 0, 0, 5, &op_pop_yp_cb}, // POP_YP
+    {"POP  YH               ", 0xFD8, MASK_12B, 0, 0, 5, &op_pop_yh_cb}, // POP_YH
+    {"POP  YL               ", 0xFD9, MASK_12B, 0, 0, 5, &op_pop_yl_cb}, // POP_YL
+    {"POP  F                ", 0xFDA, MASK_12B, 0, 0, 5, &op_pop_f_cb}, // POP_F
+    {"LD   SPH R(#0x%02X)     ", 0xFE0, MASK_10B, 0, 0, 5, &op_ld_sph_r_cb}, // LD_SPH_R
+    {"LD   SPL R(#0x%02X)     ", 0xFF0, MASK_10B, 0, 0, 5, &op_ld_spl_r_cb}, // LD_SPL_R
+    {"LD   R(#0x%02X) SPH     ", 0xFE4, MASK_10B, 0, 0, 5, &op_ld_r_sph_cb}, // LD_R_SPH
+    {"LD   R(#0x%02X) SPL     ", 0xFF4, MASK_10B, 0, 0, 5, &op_ld_r_spl_cb}, // LD_R_SPL
+    {"ADD  R(#0x%02X) #0x%02X   ", 0xC00, MASK_6B, 4, 0x030, 7, &op_add_r_i_cb}, // ADD_R_I
+    {"ADD  R(#0x%02X) Q(#0x%02X)", 0xA80, MASK_8B, 2, 0x00C, 7, &op_add_r_q_cb}, // ADD_R_Q
+    {"ADC  R(#0x%02X) #0x%02X   ", 0xC40, MASK_6B, 4, 0x030, 7, &op_adc_r_i_cb}, // ADC_R_I
+    {"ADC  R(#0x%02X) Q(#0x%02X)", 0xA90, MASK_8B, 2, 0x00C, 7, &op_adc_r_q_cb}, // ADC_R_Q
+    {"SUB  R(#0x%02X) Q(#0x%02X)", 0xAA0, MASK_8B, 2, 0x00C, 7, &op_sub_cb}, // SUB
+    {"SBC  R(#0x%02X) #0x%02X   ", 0xB40, MASK_6B, 4, 0x030, 7, &op_sbc_r_i_cb}, // SBC_R_I
+    {"SBC  R(#0x%02X) Q(#0x%02X)", 0xAB0, MASK_8B, 2, 0x00C, 7, &op_sbc_r_q_cb}, // SBC_R_Q
+    {"AND  R(#0x%02X) #0x%02X   ", 0xC80, MASK_6B, 4, 0x030, 7, &op_and_r_i_cb}, // AND_R_I
+    {"AND  R(#0x%02X) Q(#0x%02X)", 0xAC0, MASK_8B, 2, 0x00C, 7, &op_and_r_q_cb}, // AND_R_Q
+    {"OR   R(#0x%02X) #0x%02X   ", 0xCC0, MASK_6B, 4, 0x030, 7, &op_or_r_i_cb}, // OR_R_I
+    {"OR   R(#0x%02X) Q(#0x%02X)", 0xAD0, MASK_8B, 2, 0x00C, 7, &op_or_r_q_cb}, // OR_R_Q
+    {"XOR  R(#0x%02X) #0x%02X   ", 0xD00, MASK_6B, 4, 0x030, 7, &op_xor_r_i_cb}, // XOR_R_I
+    {"XOR  R(#0x%02X) Q(#0x%02X)", 0xAE0, MASK_8B, 2, 0x00C, 7, &op_xor_r_q_cb}, // XOR_R_Q
+    {"CP   R(#0x%02X) #0x%02X   ", 0xDC0, MASK_6B, 4, 0x030, 7, &op_cp_r_i_cb}, // CP_R_I
+    {"CP   R(#0x%02X) Q(#0x%02X)", 0xF00, MASK_8B, 2, 0x00C, 7, &op_cp_r_q_cb}, // CP_R_Q
+    {"FAN  R(#0x%02X) #0x%02X   ", 0xD80, MASK_6B, 4, 0x030, 7, &op_fan_r_i_cb}, // FAN_R_I
+    {"FAN  R(#0x%02X) Q(#0x%02X)", 0xF10, MASK_8B, 2, 0x00C, 7, &op_fan_r_q_cb}, // FAN_R_Q
+    {"RLC  R(#0x%02X)         ", 0xAF0, MASK_8B, 0, 0, 7, &op_rlc_cb}, // RLC
+    {"RRC  R(#0x%02X)         ", 0xE8C, MASK_10B, 0, 0, 5, &op_rrc_cb}, // RRC
+    {"INC  M(#0x%02X)         ", 0xF60, MASK_8B, 0, 0, 7, &op_inc_mn_cb}, // INC_MN
+    {"DEC  M(#0x%02X)         ", 0xF70, MASK_8B, 0, 0, 7, &op_dec_mn_cb}, // DEC_MN
+    {"ACPX R(#0x%02X)         ", 0xF28, MASK_10B, 0, 0, 7, &op_acpx_cb}, // ACPX
+    {"ACPY R(#0x%02X)         ", 0xF2C, MASK_10B, 0, 0, 7, &op_acpy_cb}, // ACPY
+    {"SCPX R(#0x%02X)         ", 0xF38, MASK_10B, 0, 0, 7, &op_scpx_cb}, // SCPX
+    {"SCPY R(#0x%02X)         ", 0xF3C, MASK_10B, 0, 0, 7, &op_scpy_cb}, // SCPY
+    {"NOT  R(#0x%02X)         ", 0xD0F, 0xFCF, 4, 0, 7, &op_not_cb}, // NOT
+
+    {NULL, 0, 0, 0, 0, 0, NULL},
+};
+
+static timestamp_t wait_for_cycles(timestamp_t since, u8_t cycles) {
+    timestamp_t deadline;
+
+    tick_counter += cycles;
+
+    if(speed_ratio == 0) {
+        /* Emulation will be as fast as possible */
+        return g_hal->get_timestamp();
+    }
+
+    deadline = since + (cycles * ts_freq) / (TICK_FREQUENCY * speed_ratio);
+    g_hal->sleep_until(deadline);
+
+    return deadline;
+}
+
+static void process_interrupts(void) {
+    u8_t i;
+
+    /* Process interrupts in priority order */
+    for(i = 0; i < INT_SLOT_NUM; i++) {
+        if(interrupts[i].triggered) {
+            //printf("IT %u !\n", i);
+            SET_M(sp - 1, PCP);
+            SET_M(sp - 2, PCSH);
+            SET_M(sp - 3, PCSL);
+            sp = (sp - 3) & 0xFF;
+            CLEAR_I();
+            np = TO_NP(NBP, 1);
+            pc = TO_PC(PCB, 1, interrupts[i].vector);
+            call_depth++;
+
+            ref_ts = wait_for_cycles(ref_ts, 12);
+            interrupts[i].triggered = 0;
+        }
+    }
+}
+
+static void print_state(u8_t op_num, u12_t op, u13_t addr) {
+    u8_t i;
+
+    if(!g_hal->is_log_enabled(LOG_CPU)) {
+        return;
+    }
+
+    g_hal->log(LOG_CPU, "0x%04X: ", addr);
+
+    for(i = 0; i < call_depth; i++) {
+        g_hal->log(LOG_CPU, "  ");
+    }
+
+    if(ops[op_num].mask_arg0 != 0) {
+        /* Two arguments */
+        g_hal->log(
+            LOG_CPU,
+            ops[op_num].log,
+            (op & ops[op_num].mask_arg0) >> ops[op_num].shift_arg0,
+            op & ~(ops[op_num].mask | ops[op_num].mask_arg0));
+    } else {
+        /* One argument */
+        g_hal->log(LOG_CPU, ops[op_num].log, (op & ~ops[op_num].mask) >> ops[op_num].shift_arg0);
+    }
+
+    if(call_depth < 10) {
+        for(i = 0; i < (10 - call_depth); i++) {
+            g_hal->log(LOG_CPU, "  ");
+        }
+    }
+
+    g_hal->log(LOG_CPU, " ; 0x%03X - ", op);
+    for(i = 0; i < 12; i++) {
+        g_hal->log(LOG_CPU, "%s", ((op >> (11 - i)) & 0x1) ? "1" : "0");
+    }
+    g_hal->log(
+        LOG_CPU,
+        " - PC = 0x%04X, SP = 0x%02X, NP = 0x%02X, X = 0x%03X, Y = 0x%03X, A = 0x%X, B = 0x%X, F = 0x%X\n",
+        pc,
+        sp,
+        np,
+        x,
+        y,
+        a,
+        b,
+        flags);
+}
+
+void cpu_reset(void) {
+    u13_t i;
+
+    /* Registers and variables init */
+    pc = TO_PC(0, 1, 0x00); // PC starts at bank 0, page 1, step 0
+    np = TO_NP(0, 1); // NP starts at page 1
+    a = 0; // undef
+    b = 0; // undef
+    x = 0; // undef
+    y = 0; // undef
+    sp = 0; // undef
+    flags = 0;
+
+    /* Init RAM to zeros */
+    for(i = 0; i < MEM_BUFFER_SIZE; i++) {
+        memory[i] = 0;
+    }
+
+    SET_IO_MEMORY(memory, REG_K40_K43_BZ_OUTPUT_PORT, 0xF); // Output port (R40-R43)
+    SET_IO_MEMORY(memory, REG_LCD_CTRL, 0x8); // LCD control
+    /* TODO: Input relation register */
+
+    cpu_sync_ref_timestamp();
+}
+
+bool_t cpu_init(const u12_t* program, breakpoint_t* breakpoints, u32_t freq) {
+    g_program = program;
+    g_breakpoints = breakpoints;
+    ts_freq = freq;
+
+    cpu_reset();
+
+    return 0;
+}
+
+void cpu_release(void) {
+}
+
+int cpu_step(void) {
+    u12_t op;
+    u8_t i;
+    breakpoint_t* bp = g_breakpoints;
+    static u8_t previous_cycles = 0;
+
+    op = g_program[pc];
+
+    /* Lookup the OP code */
+    for(i = 0; ops[i].log != NULL; i++) {
+        if((op & ops[i].mask) == ops[i].code) {
+            break;
+        }
+    }
+
+    if(ops[i].log == NULL) {
+        g_hal->log(LOG_ERROR, "Unknown op-code 0x%X (pc = 0x%04X)\n", op, pc);
+        return 1;
+    }
+
+    next_pc = (pc + 1) & 0x1FFF;
+
+    /* Display the operation along with the current state of the processor */
+    print_state(i, op, pc);
+
+    /* Match the speed of the real processor
+	 * NOTE: For better accuracy, the final wait should happen here, however
+	 * the downside is that all interrupts will likely be delayed by one OP
+	 */
+    ref_ts = wait_for_cycles(ref_ts, previous_cycles);
+
+    /* Process the OP code */
+    if(ops[i].cb != NULL) {
+        if(ops[i].mask_arg0 != 0) {
+            /* Two arguments */
+            ops[i].cb(
+                (op & ops[i].mask_arg0) >> ops[i].shift_arg0,
+                op & ~(ops[i].mask | ops[i].mask_arg0));
+        } else {
+            /* One arguments */
+            ops[i].cb((op & ~ops[i].mask) >> ops[i].shift_arg0, 0);
+        }
+    }
+
+    /* Prepare for the next instruction */
+    pc = next_pc;
+    previous_cycles = ops[i].cycles;
+
+    if(i > 0) {
+        /* OP code is not PSET, reset NP */
+        np = (pc >> 8) & 0x1F;
+    }
+
+    /* Handle timers using the internal tick counter */
+    if(tick_counter - clk_timer_timestamp >= TIMER_1HZ_PERIOD) {
+        do {
+            clk_timer_timestamp += TIMER_1HZ_PERIOD;
+        } while(tick_counter - clk_timer_timestamp >= TIMER_1HZ_PERIOD);
+
+        generate_interrupt(INT_CLOCK_TIMER_SLOT, 3);
+    }
+
+    if(prog_timer_enabled && tick_counter - prog_timer_timestamp >= TIMER_256HZ_PERIOD) {
+        do {
+            prog_timer_timestamp += TIMER_256HZ_PERIOD;
+            prog_timer_data--;
+
+            if(prog_timer_data == 0) {
+                prog_timer_data = prog_timer_rld;
+                generate_interrupt(INT_PROG_TIMER_SLOT, 0);
+            }
+        } while(tick_counter - prog_timer_timestamp >= TIMER_256HZ_PERIOD);
+    }
+
+    /* Check if there is any pending interrupt */
+    if(I && i > 0) { // Do not process interrupts after a PSET operation
+        process_interrupts();
+    }
+
+    /* Check if we could pause the execution */
+    while(bp != NULL) {
+        if(bp->addr == pc) {
+            return 1;
+        }
+
+        bp = bp->next;
+    }
+
+    return 0;
+}

+ 215 - 0
tamalib/cpu.h

@@ -0,0 +1,215 @@
+/*
+ * 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 _CPU_H_
+#define _CPU_H_
+
+#include "hal.h"
+
+#define MEMORY_SIZE 4096 // 4096 x 4 bits (640 x 4 bits of RAM)
+
+#define MEM_RAM_ADDR 0x000
+#define MEM_RAM_SIZE 0x280
+#define MEM_DISPLAY1_ADDR 0xE00
+#define MEM_DISPLAY1_SIZE 0x050
+#define MEM_DISPLAY2_ADDR 0xE80
+#define MEM_DISPLAY2_SIZE 0x050
+#define MEM_IO_ADDR 0xF00
+#define MEM_IO_SIZE 0x080
+
+/* Define this if you want to reduce the footprint of the memory buffer from 4096 u4_t (most likely bytes)
+ * to 464 u8_t (bytes for sure), while increasing slightly the number of operations needed to read/write from/to it.
+ */
+#define LOW_FOOTPRINT
+
+#ifdef LOW_FOOTPRINT
+/* Invalid memory areas are not buffered to reduce the footprint of the library in memory */
+#define MEM_BUFFER_SIZE (MEM_RAM_SIZE + MEM_DISPLAY1_SIZE + MEM_DISPLAY2_SIZE + MEM_IO_SIZE) / 2
+
+/* Maps the CPU memory to the memory buffer */
+#define RAM_TO_MEMORY(n) ((n - MEM_RAM_ADDR) / 2)
+#define DISP1_TO_MEMORY(n) ((n - MEM_DISPLAY1_ADDR + MEM_RAM_SIZE) / 2)
+#define DISP2_TO_MEMORY(n) ((n - MEM_DISPLAY2_ADDR + MEM_RAM_SIZE + MEM_DISPLAY1_SIZE) / 2)
+#define IO_TO_MEMORY(n) \
+    ((n - MEM_IO_ADDR + MEM_RAM_SIZE + MEM_DISPLAY1_SIZE + MEM_DISPLAY2_SIZE) / 2)
+
+#define SET_RAM_MEMORY(buffer, n, v)                                                         \
+    {                                                                                        \
+        buffer[RAM_TO_MEMORY(n)] = (buffer[RAM_TO_MEMORY(n)] & ~(0xF << (((n) % 2) << 2))) | \
+                                   ((v)&0xF) << (((n) % 2) << 2);                            \
+    }
+#define SET_DISP1_MEMORY(buffer, n, v)                                                           \
+    {                                                                                            \
+        buffer[DISP1_TO_MEMORY(n)] = (buffer[DISP1_TO_MEMORY(n)] & ~(0xF << (((n) % 2) << 2))) | \
+                                     ((v)&0xF) << (((n) % 2) << 2);                              \
+    }
+#define SET_DISP2_MEMORY(buffer, n, v)                                                           \
+    {                                                                                            \
+        buffer[DISP2_TO_MEMORY(n)] = (buffer[DISP2_TO_MEMORY(n)] & ~(0xF << (((n) % 2) << 2))) | \
+                                     ((v)&0xF) << (((n) % 2) << 2);                              \
+    }
+#define SET_IO_MEMORY(buffer, n, v)                                                        \
+    {                                                                                      \
+        buffer[IO_TO_MEMORY(n)] = (buffer[IO_TO_MEMORY(n)] & ~(0xF << (((n) % 2) << 2))) | \
+                                  ((v)&0xF) << (((n) % 2) << 2);                           \
+    }
+#define SET_MEMORY(buffer, n, v)                                   \
+    {                                                              \
+        if((n) < (MEM_RAM_ADDR + MEM_RAM_SIZE)) {                  \
+            SET_RAM_MEMORY(buffer, n, v);                          \
+        } else if((n) < MEM_DISPLAY1_ADDR) {                       \
+            /* INVALID_MEMORY */                                   \
+        } else if((n) < (MEM_DISPLAY1_ADDR + MEM_DISPLAY1_SIZE)) { \
+            SET_DISP1_MEMORY(buffer, n, v);                        \
+        } else if((n) < MEM_DISPLAY2_ADDR) {                       \
+            /* INVALID_MEMORY */                                   \
+        } else if((n) < (MEM_DISPLAY2_ADDR + MEM_DISPLAY2_SIZE)) { \
+            SET_DISP2_MEMORY(buffer, n, v);                        \
+        } else if((n) < MEM_IO_ADDR) {                             \
+            /* INVALID_MEMORY */                                   \
+        } else if((n) < (MEM_IO_ADDR + MEM_IO_SIZE)) {             \
+            SET_IO_MEMORY(buffer, n, v);                           \
+        } else {                                                   \
+            /* INVALID_MEMORY */                                   \
+        }                                                          \
+    }
+
+#define GET_RAM_MEMORY(buffer, n) ((buffer[RAM_TO_MEMORY(n)] >> (((n) % 2) << 2)) & 0xF)
+#define GET_DISP1_MEMORY(buffer, n) ((buffer[DISP1_TO_MEMORY(n)] >> (((n) % 2) << 2)) & 0xF)
+#define GET_DISP2_MEMORY(buffer, n) ((buffer[DISP2_TO_MEMORY(n)] >> (((n) % 2) << 2)) & 0xF)
+#define GET_IO_MEMORY(buffer, n) ((buffer[IO_TO_MEMORY(n)] >> (((n) % 2) << 2)) & 0xF)
+#define GET_MEMORY(buffer, n)                                                     \
+    ((buffer                                                                      \
+          [((n) < (MEM_RAM_ADDR + MEM_RAM_SIZE))           ? RAM_TO_MEMORY(n) :   \
+           ((n) < MEM_DISPLAY1_ADDR)                       ? 0 :                  \
+           ((n) < (MEM_DISPLAY1_ADDR + MEM_DISPLAY1_SIZE)) ? DISP1_TO_MEMORY(n) : \
+           ((n) < MEM_DISPLAY2_ADDR)                       ? 0 :                  \
+           ((n) < (MEM_DISPLAY2_ADDR + MEM_DISPLAY2_SIZE)) ? DISP2_TO_MEMORY(n) : \
+           ((n) < MEM_IO_ADDR)                             ? 0 :                  \
+           ((n) < (MEM_IO_ADDR + MEM_IO_SIZE))             ? IO_TO_MEMORY(n) :    \
+                                                             0] >>                            \
+      (((n) % 2) << 2)) &                                                         \
+     0xF)
+
+#define MEM_BUFFER_TYPE u8_t
+#else
+#define MEM_BUFFER_SIZE MEMORY_SIZE
+
+#define SET_MEMORY(buffer, n, v) \
+    { buffer[n] = v; }
+#define SET_RAM_MEMORY(buffer, n, v) SET_MEMORY(buffer, n, v)
+#define SET_DISP1_MEMORY(buffer, n, v) SET_MEMORY(buffer, n, v)
+#define SET_DISP2_MEMORY(buffer, n, v) SET_MEMORY(buffer, n, v)
+#define SET_IO_MEMORY(buffer, n, v) SET_MEMORY(buffer, n, v)
+
+#define GET_MEMORY(buffer, n) (buffer[n])
+#define GET_RAM_MEMORY(buffer, n) GET_MEMORY(buffer, n)
+#define GET_DISP1_MEMORY(buffer, n) GET_MEMORY(buffer, n)
+#define GET_DISP2_MEMORY(buffer, n) GET_MEMORY(buffer, n)
+#define GET_IO_MEMORY(buffer, n) GET_MEMORY(buffer, n)
+
+#define MEM_BUFFER_TYPE u4_t
+#endif
+
+typedef struct breakpoint {
+    u13_t addr;
+    struct breakpoint* next;
+} breakpoint_t;
+
+/* Pins (TODO: add other pins) */
+typedef enum {
+    PIN_K00 = 0x0,
+    PIN_K01 = 0x1,
+    PIN_K02 = 0x2,
+    PIN_K03 = 0x3,
+    PIN_K10 = 0X4,
+    PIN_K11 = 0X5,
+    PIN_K12 = 0X6,
+    PIN_K13 = 0X7,
+} pin_t;
+
+typedef enum {
+    PIN_STATE_LOW = 0,
+    PIN_STATE_HIGH = 1,
+} pin_state_t;
+
+typedef enum {
+    INT_PROG_TIMER_SLOT = 0,
+    INT_SERIAL_SLOT = 1,
+    INT_K10_K13_SLOT = 2,
+    INT_K00_K03_SLOT = 3,
+    INT_STOPWATCH_SLOT = 4,
+    INT_CLOCK_TIMER_SLOT = 5,
+    INT_SLOT_NUM,
+} int_slot_t;
+
+typedef struct {
+    u4_t factor_flag_reg;
+    u4_t mask_reg;
+    bool_t triggered; /* 1 if triggered, 0 otherwise */
+    u8_t vector;
+} interrupt_t;
+
+typedef struct {
+    u13_t* pc;
+    u12_t* x;
+    u12_t* y;
+    u4_t* a;
+    u4_t* b;
+    u5_t* np;
+    u8_t* sp;
+    u4_t* flags;
+
+    u32_t* tick_counter;
+    u32_t* clk_timer_timestamp;
+    u32_t* prog_timer_timestamp;
+    bool_t* prog_timer_enabled;
+    u8_t* prog_timer_data;
+    u8_t* prog_timer_rld;
+
+    u32_t* call_depth;
+
+    interrupt_t* interrupts;
+
+    MEM_BUFFER_TYPE* memory;
+} state_t;
+
+void cpu_add_bp(breakpoint_t** list, u13_t addr);
+void cpu_free_bp(breakpoint_t** list);
+
+void cpu_set_speed(u8_t speed);
+
+state_t* cpu_get_state(void);
+
+u32_t cpu_get_depth(void);
+
+void cpu_set_input_pin(pin_t pin, pin_state_t state);
+
+void cpu_sync_ref_timestamp(void);
+
+void cpu_refresh_hw(void);
+
+void cpu_reset(void);
+
+bool_t cpu_init(const u12_t* program, breakpoint_t* breakpoints, u32_t freq);
+void cpu_release(void);
+
+int cpu_step(void);
+
+#endif /* _CPU_H_ */

+ 89 - 0
tamalib/hal.h

@@ -0,0 +1,89 @@
+/*
+ * 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_H_
+#define _HAL_H_
+
+#include "../hal_types.h"
+
+#ifndef NULL
+#define NULL 0
+#endif
+
+typedef enum {
+    LOG_ERROR = 0x1,
+    LOG_INFO = (0x1 << 1),
+    LOG_MEMORY = (0x1 << 2),
+    LOG_CPU = (0x1 << 3),
+} log_level_t;
+
+/* The Hardware Abstraction Layer
+ * NOTE: This structure acts as an abstraction layer between TamaLIB and the OS/SDK.
+ * All pointers MUST be implemented, but some implementations can be left empty.
+ */
+typedef struct {
+    /* Memory allocation functions
+	 * NOTE: Needed only if breakpoints support is required.
+	 */
+    void* (*malloc)(u32_t size);
+    void (*free)(void* ptr);
+
+    /* What to do if the CPU has halted
+	 */
+    void (*halt)(void);
+
+    /* Log related function
+	 * NOTE: Needed only if log messages are required.
+	 */
+    bool_t (*is_log_enabled)(log_level_t level);
+    void (*log)(log_level_t level, char* buff, ...);
+
+    /* Clock related functions
+	 * NOTE: Timestamps granularity is configured with tamalib_init(), an accuracy
+	 * of ~30 us (1/32768) is required for a cycle accurate emulation.
+	 */
+    void (*sleep_until)(timestamp_t ts);
+    timestamp_t (*get_timestamp)(void);
+
+    /* Screen related functions
+	 * NOTE: In case of direct hardware access to pixels, the set_XXXX() functions
+	 * (called for each pixel/icon update) can directly drive them, otherwise they
+	 * should just store the data in a buffer and let update_screen() do the actual
+	 * rendering (at 30 fps).
+	 */
+    void (*update_screen)(void);
+    void (*set_lcd_matrix)(u8_t x, u8_t y, bool_t val);
+    void (*set_lcd_icon)(u8_t icon, bool_t val);
+
+    /* Sound related functions
+	 * NOTE: set_frequency() changes the output frequency of the sound, while
+	 * play_frequency() decides whether the sound should be heard or not.
+	 */
+    void (*set_frequency)(u32_t freq);
+    void (*play_frequency)(bool_t en);
+
+    /* Event handler from the main app (if any)
+	 * NOTE: This function usually handles button related events, states loading/saving ...
+	 */
+    int (*handler)(void);
+} hal_t;
+
+extern hal_t* g_hal;
+
+#endif /* _HAL_H_ */

+ 32 - 0
tamalib/hal_types.h.template

@@ -0,0 +1,32 @@
+/*
+ * 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_
+
+typedef unsigned char bool_t;
+typedef unsigned char u4_t;
+typedef unsigned char u5_t;
+typedef unsigned char u8_t;
+typedef unsigned short u12_t;
+typedef unsigned short u13_t;
+typedef unsigned int u32_t;
+typedef unsigned int 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_ */

+ 134 - 0
tamalib/hw.c

@@ -0,0 +1,134 @@
+/*
+ * 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.
+ */
+#include "hw.h"
+#include "cpu.h"
+#include "hal.h"
+
+/* SEG -> LCD mapping */
+static u8_t seg_pos[40] = {0,  1,  2,  3,  4,  5,  6,  7,  32, 8,  9,  10, 11, 12,
+                           13, 14, 15, 33, 34, 35, 31, 30, 29, 28, 27, 26, 25, 24,
+                           36, 23, 22, 21, 20, 19, 18, 17, 16, 37, 38, 39};
+
+bool_t hw_init(void) {
+    /* Buttons are active LOW */
+    cpu_set_input_pin(PIN_K00, PIN_STATE_HIGH);
+    cpu_set_input_pin(PIN_K01, PIN_STATE_HIGH);
+    cpu_set_input_pin(PIN_K02, PIN_STATE_HIGH);
+
+    return 0;
+}
+
+void hw_release(void) {
+}
+
+void hw_set_lcd_pin(u8_t seg, u8_t com, u8_t val) {
+    if(seg_pos[seg] < LCD_WIDTH) {
+        g_hal->set_lcd_matrix(seg_pos[seg], com, val);
+    } else {
+        /*
+		 * IC n -> seg-com|...
+		 * IC 0 ->  8-0 |18-3 |19-2
+		 * IC 1 ->  8-1 |17-0 |19-3
+		 * IC 2 ->  8-2 |17-1 |37-12|38-13|39-14
+		 * IC 3 ->  8-3 |17-2 |18-1 |19-0
+		 * IC 4 -> 28-12|37-13|38-14|39-15
+		 * IC 5 -> 28-13|37-14|38-15
+		 * IC 6 -> 28-14|37-15|39-12
+		 * IC 7 -> 28-15|38-12|39-13
+		 */
+        if(seg == 8 && com < 4) {
+            g_hal->set_lcd_icon(com, val);
+        } else if(seg == 28 && com >= 12) {
+            g_hal->set_lcd_icon(com - 8, val);
+        }
+    }
+}
+
+void hw_set_button(button_t btn, btn_state_t state) {
+    pin_state_t pin_state = (state == BTN_STATE_PRESSED) ? PIN_STATE_LOW : PIN_STATE_HIGH;
+
+    switch(btn) {
+    case BTN_LEFT:
+        cpu_set_input_pin(PIN_K02, pin_state);
+        break;
+
+    case BTN_MIDDLE:
+        cpu_set_input_pin(PIN_K01, pin_state);
+        break;
+
+    case BTN_RIGHT:
+        cpu_set_input_pin(PIN_K00, pin_state);
+        break;
+    }
+}
+
+void hw_set_buzzer_freq(u4_t freq) {
+    u32_t snd_freq = 0;
+
+    switch(freq) {
+    case 0:
+        /* 4096.0 Hz */
+        snd_freq = 40960;
+        break;
+
+    case 1:
+        /* 3276.8 Hz */
+        snd_freq = 32768;
+        break;
+
+    case 2:
+        /* 2730.7 Hz */
+        snd_freq = 27307;
+        break;
+
+    case 3:
+        /* 2340.6 Hz */
+        snd_freq = 23406;
+        break;
+
+    case 4:
+        /* 2048.0 Hz */
+        snd_freq = 20480;
+        break;
+
+    case 5:
+        /* 1638.4 Hz */
+        snd_freq = 16384;
+        break;
+
+    case 6:
+        /* 1365.3 Hz */
+        snd_freq = 13653;
+        break;
+
+    case 7:
+        /* 1170.3 Hz */
+        snd_freq = 11703;
+        break;
+    }
+
+    if(snd_freq != 0) {
+        g_hal->set_frequency(snd_freq);
+    }
+}
+
+void hw_enable_buzzer(bool_t en) {
+    g_hal->play_frequency(en);
+}

+ 50 - 0
tamalib/hw.h

@@ -0,0 +1,50 @@
+/*
+ * 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 _HW_H_
+#define _HW_H_
+
+#include "hal.h"
+
+#define LCD_WIDTH 32
+#define LCD_HEIGHT 16
+
+#define ICON_NUM 8
+
+typedef enum {
+    BTN_STATE_RELEASED = 0,
+    BTN_STATE_PRESSED,
+} btn_state_t;
+
+typedef enum {
+    BTN_LEFT = 0,
+    BTN_MIDDLE,
+    BTN_RIGHT,
+} button_t;
+
+bool_t hw_init(void);
+void hw_release(void);
+
+void hw_set_lcd_pin(u8_t seg, u8_t com, u8_t val);
+void hw_set_button(button_t btn, btn_state_t state);
+
+void hw_set_buzzer_freq(u4_t freq);
+void hw_enable_buzzer(bool_t en);
+
+#endif /* _HW_H_ */

+ 128 - 0
tamalib/tamalib.c

@@ -0,0 +1,128 @@
+/*
+ * 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.
+ */
+#include "tamalib.h"
+#include "hw.h"
+#include "cpu.h"
+#include "hal.h"
+
+#define DEFAULT_FRAMERATE 30 // fps
+
+static exec_mode_t exec_mode = EXEC_MODE_RUN;
+
+static u32_t step_depth = 0;
+
+static timestamp_t screen_ts = 0;
+
+static u32_t ts_freq;
+
+static u8_t g_framerate = DEFAULT_FRAMERATE;
+
+hal_t* g_hal;
+
+bool_t tamalib_init(const u12_t* program, breakpoint_t* breakpoints, u32_t freq) {
+    bool_t res = 0;
+
+    res |= cpu_init(program, breakpoints, freq);
+    res |= hw_init();
+
+    ts_freq = freq;
+
+    return res;
+}
+
+void tamalib_release(void) {
+    hw_release();
+    cpu_release();
+}
+
+void tamalib_set_framerate(u8_t framerate) {
+    g_framerate = framerate;
+}
+
+u8_t tamalib_get_framerate(void) {
+    return g_framerate;
+}
+
+void tamalib_register_hal(hal_t* hal) {
+    g_hal = hal;
+}
+
+void tamalib_set_exec_mode(exec_mode_t mode) {
+    exec_mode = mode;
+    step_depth = cpu_get_depth();
+    cpu_sync_ref_timestamp();
+}
+
+void tamalib_step(void) {
+    if(exec_mode == EXEC_MODE_PAUSE) {
+        return;
+    }
+
+    if(cpu_step()) {
+        exec_mode = EXEC_MODE_PAUSE;
+        step_depth = cpu_get_depth();
+    } else {
+        switch(exec_mode) {
+        case EXEC_MODE_PAUSE:
+        case EXEC_MODE_RUN:
+            break;
+
+        case EXEC_MODE_STEP:
+            exec_mode = EXEC_MODE_PAUSE;
+            break;
+
+        case EXEC_MODE_NEXT:
+            if(cpu_get_depth() <= step_depth) {
+                exec_mode = EXEC_MODE_PAUSE;
+                step_depth = cpu_get_depth();
+            }
+            break;
+
+        case EXEC_MODE_TO_CALL:
+            if(cpu_get_depth() > step_depth) {
+                exec_mode = EXEC_MODE_PAUSE;
+                step_depth = cpu_get_depth();
+            }
+            break;
+
+        case EXEC_MODE_TO_RET:
+            if(cpu_get_depth() < step_depth) {
+                exec_mode = EXEC_MODE_PAUSE;
+                step_depth = cpu_get_depth();
+            }
+            break;
+        }
+    }
+}
+
+void tamalib_mainloop(void) {
+    timestamp_t ts;
+
+    while(!g_hal->handler()) {
+        tamalib_step();
+
+        /* Update the screen @ g_framerate fps */
+        ts = g_hal->get_timestamp();
+        if(ts - screen_ts >= ts_freq / g_framerate) {
+            screen_ts = ts;
+            g_hal->update_screen();
+        }
+    }
+}

+ 65 - 0
tamalib/tamalib.h

@@ -0,0 +1,65 @@
+/*
+ * 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 _TAMALIB_H_
+#define _TAMALIB_H_
+
+#include "cpu.h"
+#include "hw.h"
+#include "hal.h"
+
+#define tamalib_set_button(btn, state) hw_set_button(btn, state)
+
+#define tamalib_set_speed(speed) cpu_set_speed(speed)
+
+#define tamalib_get_state() cpu_get_state()
+#define tamalib_refresh_hw() cpu_refresh_hw()
+
+#define tamalib_reset() cpu_reset()
+
+#define tamalib_add_bp(list, addr) cpu_add_bp(list, addr)
+#define tamalib_free_bp(list) cpu_free_bp(list)
+
+typedef enum {
+    EXEC_MODE_PAUSE,
+    EXEC_MODE_RUN,
+    EXEC_MODE_STEP,
+    EXEC_MODE_NEXT,
+    EXEC_MODE_TO_CALL,
+    EXEC_MODE_TO_RET,
+} exec_mode_t;
+
+void tamalib_release(void);
+bool_t tamalib_init(const u12_t* program, breakpoint_t* breakpoints, u32_t freq);
+
+void tamalib_set_framerate(u8_t framerate);
+u8_t tamalib_get_framerate(void);
+
+void tamalib_register_hal(hal_t* hal);
+
+void tamalib_set_exec_mode(exec_mode_t mode);
+
+/* NOTE: Only one of these two functions must be used in the main application
+ * (tamalib_step() should be used only if tamalib_mainloop() does not fit the
+ * main application execution flow).
+ */
+void tamalib_step(void);
+void tamalib_mainloop(void);
+
+#endif /* _TAMALIB_H_ */