summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Pavone <pavone@retrodev.com>2021-02-15 11:24:06 -0800
committerMichael Pavone <pavone@retrodev.com>2021-02-15 11:24:06 -0800
commitdcb57030108d8ed37fe5e28b06a830f7f4a20ccf (patch)
tree9bdcb0c26ec963433b7f78c2ebb9b266a8692885
parent59887b4ad62925212ff7fda6c8f6aa9feaf93bf9 (diff)
Implement Heartbeat Personal Trainer peripheral and add ROM DB entry for Outback Joey
-rw-r--r--blastem.c13
-rw-r--r--io.c298
-rw-r--r--io.h20
-rw-r--r--rom.db10
-rw-r--r--romdb.c15
-rw-r--r--romdb.h1
6 files changed, 354 insertions, 3 deletions
diff --git a/blastem.c b/blastem.c
index dff7f1c..66e4eb6 100644
--- a/blastem.c
+++ b/blastem.c
@@ -281,12 +281,23 @@ static char *get_save_dir(system_media *media)
return save_dir;
}
+const char *get_save_fname(uint8_t save_type)
+{
+ switch(save_type)
+ {
+ case SAVE_I2C: return "save.eeprom";
+ case SAVE_NOR: return "save.nor";
+ case SAVE_HBPT: return "save.hbpt";
+ default: return "save.sram";
+ }
+}
+
void setup_saves(system_media *media, system_header *context)
{
static uint8_t persist_save_registered;
rom_info *info = &context->info;
char *save_dir = get_save_dir(info->is_save_lock_on ? media->chain : media);
- char const *parts[] = {save_dir, PATH_SEP, info->save_type == SAVE_I2C ? "save.eeprom" : info->save_type == SAVE_NOR ? "save.nor" : "save.sram"};
+ char const *parts[] = {save_dir, PATH_SEP, get_save_fname(info->save_type)};
free(save_filename);
save_filename = alloc_concat_m(3, parts);
if (info->is_save_lock_on) {
diff --git a/io.c b/io.c
index f93eb38..fce774c 100644
--- a/io.c
+++ b/io.c
@@ -40,7 +40,8 @@ const char * device_type_names[] = {
"EA 4-way Play cable B",
"Sega Parallel Transfer Board",
"Generic Device",
- "Generic Serial"
+ "Generic Serial",
+ "Heartbeat Personal Trainer"
};
#define GAMEPAD_TH0 0
@@ -59,6 +60,13 @@ enum {
IO_READ
};
+enum {
+ HBPT_NEED_INIT,
+ HBPT_IDLE,
+ HBPT_CMD_PAYLOAD,
+ HBPT_REPLY
+};
+
typedef struct {
uint8_t states[2], value;
} gp_button_def;
@@ -87,6 +95,9 @@ static io_port *find_gamepad(sega_io *io, uint8_t gamepad_num)
if (port->device_type < IO_MOUSE && port->device.pad.gamepad_num == gamepad_num) {
return port;
}
+ if (port->device_type == IO_HEARTBEAT_TRAINER && port->device.heartbeat_trainer.device_num == gamepad_num) {
+ return port;
+ }
}
return NULL;
}
@@ -248,6 +259,10 @@ void process_device(char * device_type, io_port * port)
port->device_type = IO_GAMEPAD6;
}
port->device.pad.gamepad_num = device_type[gamepad_len+2] - '0';
+ } else if(startswith(device_type, "heartbeat_trainer.")) {
+ port->device_type = IO_HEARTBEAT_TRAINER;
+ port->device.heartbeat_trainer.nv_memory = NULL;
+ port->device.heartbeat_trainer.device_num = device_type[strlen("heartbeat_trainer.")] - '0';
} else if(startswith(device_type, "mouse")) {
if (port->device_type != IO_MOUSE) {
port->device_type = IO_MOUSE;
@@ -411,6 +426,30 @@ cleanup_sock:
#endif
if (ports[i].device_type == IO_GAMEPAD3 || ports[i].device_type == IO_GAMEPAD6 || ports[i].device_type == IO_GAMEPAD2) {
debug_message("IO port %s connected to gamepad #%d with type '%s'\n", io_name(i), ports[i].device.pad.gamepad_num, device_type_names[ports[i].device_type]);
+ } else if (ports[i].device_type == IO_HEARTBEAT_TRAINER) {
+ debug_message("IO port %s connected to Heartbeat Personal Trainer #%d\n", io_name(i), ports[i].device.heartbeat_trainer.device_num);
+ if (rom->save_type == SAVE_HBPT) {
+ ports[i].device.heartbeat_trainer.nv_memory = rom->save_buffer;
+ uint32_t page_size = 16;
+ for (; page_size < 128; page_size *= 2)
+ {
+ if (rom->save_size / page_size < 256) {
+ break;
+ }
+ }
+ ports[i].device.heartbeat_trainer.nv_page_size = page_size;
+ uint32_t num_pages = rom->save_size / page_size;
+ ports[i].device.heartbeat_trainer.nv_pages = num_pages < 256 ? num_pages : 255;
+ } else {
+ ports[i].device.heartbeat_trainer.nv_page_size = 16;
+ ports[i].device.heartbeat_trainer.nv_pages = 32;
+ size_t bufsize =
+ ports[i].device.heartbeat_trainer.nv_page_size * ports[i].device.heartbeat_trainer.nv_pages
+ + 5 + 8;
+ ports[i].device.heartbeat_trainer.nv_memory = malloc(bufsize);
+ memset(ports[i].device.heartbeat_trainer.nv_memory, 0xFF, bufsize);
+ }
+ ports[i].device.heartbeat_trainer.state = HBPT_NEED_INIT;
} else {
debug_message("IO port %s connected to device '%s'\n", io_name(i), device_type_names[ports[i].device_type]);
}
@@ -641,6 +680,257 @@ static void service_socket(io_port *port)
}
#endif
+enum {
+ HBPT_UNKNOWN1 = 1,
+ HBPT_POLL,
+ HBPT_READ_PAGE = 5,
+ HBPT_WRITE_PAGE,
+ HBPT_READ_RTC,
+ HBPT_SET_RTC,
+ HBPT_GET_STATUS,
+ HBPT_ERASE_NVMEM,
+ HBPT_NVMEM_PARAMS,
+ HBPT_INIT
+};
+
+static void start_reply(io_port *port, uint8_t bytes, const uint8_t *src)
+{
+ port->device.heartbeat_trainer.remaining_bytes = bytes;
+ port->device.heartbeat_trainer.state = HBPT_REPLY;
+ port->device.heartbeat_trainer.cur_buffer = (uint8_t *)src;
+}
+
+static void simple_reply(io_port *port, uint8_t value)
+{
+ port->device.heartbeat_trainer.param = value;
+ start_reply(port, 1, &port->device.heartbeat_trainer.param);
+}
+
+static void expect_payload(io_port *port, uint8_t bytes, uint8_t *dst)
+{
+ port->device.heartbeat_trainer.remaining_bytes = bytes;
+ port->device.heartbeat_trainer.state = HBPT_CMD_PAYLOAD;
+ port->device.heartbeat_trainer.cur_buffer = dst;
+}
+
+void hbpt_check_init(io_port *port)
+{
+ if (port->device.heartbeat_trainer.state == HBPT_NEED_INIT) {
+ port->device.heartbeat_trainer.rtc_base_timestamp = 0;
+ for (int i = 0; i < 8; i ++)
+ {
+ port->device.heartbeat_trainer.rtc_base_timestamp <<= 8;
+ port->device.heartbeat_trainer.rtc_base_timestamp |= port->device.heartbeat_trainer.nv_memory[i];
+ }
+ memcpy(port->device.heartbeat_trainer.rtc_base, port->device.heartbeat_trainer.nv_memory + 8, 5);
+ if (port->device.heartbeat_trainer.rtc_base_timestamp == UINT64_MAX) {
+ //uninitialized save, set the appropriate status bit
+ port->device.heartbeat_trainer.status |= 1;
+ }
+ port->device.heartbeat_trainer.bpm = 60;
+ port->device.heartbeat_trainer.state = HBPT_IDLE;
+ }
+}
+
+void hbpt_check_send_reply(io_port *port)
+{
+ if (port->device.heartbeat_trainer.state == HBPT_REPLY && !port->receive_end) {
+ port->serial_receiving = *(port->device.heartbeat_trainer.cur_buffer++);
+ port->receive_end = port->serial_cycle + 10 * port->serial_divider;
+ if (!--port->device.heartbeat_trainer.remaining_bytes) {
+ port->device.heartbeat_trainer.state = HBPT_IDLE;
+ }
+ }
+}
+
+uint8_t is_leap_year(uint16_t year)
+{
+ if (year & 3) {
+ return 0;
+ }
+ if (year % 100) {
+ return 1;
+ }
+ if (year % 400) {
+ return 0;
+ }
+ return 1;
+}
+
+uint8_t days_in_month(uint8_t month, uint16_t year)
+{
+ static uint8_t days_per_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
+ if (month == 2 && is_leap_year(year)) {
+ return 29;
+ }
+ if (month > 12 || !month) {
+ return 30;
+ }
+ return days_per_month[month-1];
+}
+
+void hbpt_write_byte(io_port *port)
+{
+ hbpt_check_init(port);
+ uint8_t reply;
+ switch (port->device.heartbeat_trainer.state)
+ {
+ case HBPT_IDLE:
+ port->device.heartbeat_trainer.cmd = port->serial_transmitting;
+ switch (port->device.heartbeat_trainer.cmd)
+ {
+ case HBPT_UNKNOWN1:
+ start_reply(port, 11, NULL);
+ break;
+ case HBPT_POLL:
+ start_reply(port, 3, &port->device.heartbeat_trainer.bpm);
+ if (port->serial_cycle - port->last_poll_cycle > MIN_POLL_INTERVAL) {
+ process_events();
+ port->last_poll_cycle = port->serial_cycle;
+ }
+ port->device.heartbeat_trainer.buttons = (port->input[GAMEPAD_TH0] << 2 & 0xC0) | (port->input[GAMEPAD_TH1] & 0x1F);
+ if (port->device.heartbeat_trainer.cadence && port->input[GAMEPAD_TH1] & 0x20) {
+ port->device.heartbeat_trainer.cadence--;
+ printf("Cadence: %d\n", port->device.heartbeat_trainer.cadence);
+ } else if (port->device.heartbeat_trainer.cadence < 255 && port->input[GAMEPAD_EXTRA] & 1) {
+ port->device.heartbeat_trainer.cadence++;
+ printf("Cadence: %d\n", port->device.heartbeat_trainer.cadence);
+ }
+ if (port->device.heartbeat_trainer.bpm && port->input[GAMEPAD_EXTRA] & 4) {
+ port->device.heartbeat_trainer.bpm--;
+ printf("Heart Rate: %d\n", port->device.heartbeat_trainer.bpm);
+ } else if (port->device.heartbeat_trainer.bpm < 255 && port->input[GAMEPAD_EXTRA] & 2) {
+ port->device.heartbeat_trainer.bpm++;
+ printf("Heart Rate: %d\n", port->device.heartbeat_trainer.bpm);
+ }
+
+ break;
+ case HBPT_READ_PAGE:
+ case HBPT_WRITE_PAGE:
+ //strictly speaking for the write case, we want 1 + page size here
+ //but the rest of the payload goes to a different destination
+ expect_payload(port, 1, &port->device.heartbeat_trainer.param);
+ break;
+ case HBPT_READ_RTC: {
+ uint8_t *rtc = port->device.heartbeat_trainer.rtc_base;
+ start_reply(port, 5, rtc);
+ uint64_t now = time(NULL);
+ uint64_t delta = (now - port->device.heartbeat_trainer.rtc_base_timestamp + 30) / 60;
+ rtc[4] += delta % 60;
+ if (rtc[4] > 59) {
+ rtc[4] -= 60;
+ rtc[3]++;
+ }
+ delta /= 60;
+ if (delta) {
+ rtc[3] += delta % 24;
+ delta /= 24;
+ if (rtc[3] > 23) {
+ rtc[3] -= 24;
+ delta++;
+ }
+ if (delta) {
+ uint16_t year = rtc[0] < 81 ? 2000 + rtc[0] : 1900 + rtc[0];
+ uint8_t days_cur_month = days_in_month(rtc[1], year);
+ while (delta + rtc[2] > days_cur_month) {
+ delta -= days_cur_month + 1 - rtc[2];
+ rtc[2] = 1;
+ if (++rtc[1] == 13) {
+ rtc[1] = 1;
+ year++;
+ }
+ days_cur_month = days_in_month(rtc[1], year);
+ }
+ rtc[1] += delta;
+ rtc[0] = year % 100;
+ }
+ }
+ printf("RTC %02d-%02d-%02d %02d:%02d\n", rtc[0], rtc[1], rtc[2], rtc[3], rtc[4]);
+ port->device.heartbeat_trainer.rtc_base_timestamp = now;
+ break;
+ }
+ case HBPT_SET_RTC:
+ port->device.heartbeat_trainer.rtc_base_timestamp = time(NULL);
+ expect_payload(port, 5, port->device.heartbeat_trainer.rtc_base);
+ break;
+ case HBPT_GET_STATUS:
+ simple_reply(port, port->device.heartbeat_trainer.status);
+ break;
+ case HBPT_ERASE_NVMEM:
+ expect_payload(port, 1, &port->device.heartbeat_trainer.param);
+ break;
+ case HBPT_NVMEM_PARAMS:
+ start_reply(port, 2, &port->device.heartbeat_trainer.nv_page_size);
+ break;
+ case HBPT_INIT:
+ expect_payload(port, 19, NULL);
+ break;
+ default:
+ // it's unclear what these commands do as they are unused by Outback Joey
+ // just return 0 to indicate failure
+ simple_reply(port, 0);
+ }
+ break;
+ case HBPT_CMD_PAYLOAD:
+ if (port->device.heartbeat_trainer.cur_buffer) {
+ *(port->device.heartbeat_trainer.cur_buffer++) = port->serial_transmitting;
+ }
+ if (!--port->device.heartbeat_trainer.remaining_bytes) {
+ switch (port->device.heartbeat_trainer.cmd)
+ {
+ case HBPT_READ_PAGE:
+ case HBPT_WRITE_PAGE:
+ if (
+ port->device.heartbeat_trainer.cmd == HBPT_WRITE_PAGE
+ && port->device.heartbeat_trainer.cur_buffer != &port->device.heartbeat_trainer.param + 1) {
+ simple_reply(port, 1);
+ break;
+ }
+ port->device.heartbeat_trainer.remaining_bytes = port->device.heartbeat_trainer.nv_page_size;
+ port->device.heartbeat_trainer.cur_buffer =
+ port->device.heartbeat_trainer.param < port->device.heartbeat_trainer.nv_pages
+ ? port->device.heartbeat_trainer.nv_memory + 5 + 8
+ + port->device.heartbeat_trainer.param * port->device.heartbeat_trainer.nv_page_size
+ : NULL;
+ if (port->device.heartbeat_trainer.cmd == HBPT_WRITE_PAGE) {
+ return;
+ }
+ port->device.heartbeat_trainer.state = HBPT_REPLY;
+ break;
+ case HBPT_SET_RTC:
+ //save RTC base values back to nv memory area so it's saved to disk on exit
+ for (int i = 0; i < 8; i++)
+ {
+ port->device.heartbeat_trainer.nv_memory[i] = port->device.heartbeat_trainer.rtc_base_timestamp >> (56 - i*8);
+ }
+ memcpy(port->device.heartbeat_trainer.nv_memory + 8, port->device.heartbeat_trainer.rtc_base, 5);
+ simple_reply(port, 1);
+ break;
+ case HBPT_ERASE_NVMEM:
+ memset(
+ port->device.heartbeat_trainer.nv_memory + 5 + 8,
+ port->device.heartbeat_trainer.param,
+ port->device.heartbeat_trainer.nv_pages * port->device.heartbeat_trainer.nv_page_size
+ );
+ simple_reply(port, 1);
+ break;
+ case HBPT_INIT: {
+ static const char reply[] = "(C) HEARTBEAT CORP";
+ start_reply(port, strlen(reply), reply);
+ break;
+ }
+ }
+ }
+ }
+ hbpt_check_send_reply(port);
+}
+
+void hbpt_read_byte(io_port *port)
+{
+ hbpt_check_init(port);
+ hbpt_check_send_reply(port);
+}
+
const int mouse_delays[] = {112*7, 120*7, 96*7, 132*7, 104*7, 96*7, 112*7, 96*7};
enum {
@@ -667,6 +957,9 @@ void io_run(io_port *port, uint32_t current_cycle)
if (port->serial_ctrl & SCTRL_BIT_TX_ENABLE) {
switch (port->device_type)
{
+ case IO_HEARTBEAT_TRAINER:
+ hbpt_write_byte(port);
+ break;
#ifndef _WIN32
case IO_GENERIC_SERIAL:
write_serial_byte(port);
@@ -693,6 +986,9 @@ void io_run(io_port *port, uint32_t current_cycle)
if (!port->receive_end) {
switch(port->device_type)
{
+ case IO_HEARTBEAT_TRAINER:
+ hbpt_read_byte(port);
+ break;
#ifndef _WIN32
case IO_GENERIC_SERIAL:
read_serial_byte(port);
diff --git a/io.h b/io.h
index 00f1528..a574be8 100644
--- a/io.h
+++ b/io.h
@@ -25,7 +25,8 @@ enum {
IO_EA_MULTI_B,
IO_SEGA_PARALLEL,
IO_GENERIC,
- IO_GENERIC_SERIAL
+ IO_GENERIC_SERIAL,
+ IO_HEARTBEAT_TRAINER
};
typedef struct {
@@ -58,6 +59,23 @@ typedef struct {
uint8_t mode;
uint8_t cmd;
} keyboard;
+ struct {
+ uint8_t *nv_memory;
+ uint8_t *cur_buffer;
+ uint64_t rtc_base_timestamp;
+ uint8_t rtc_base[5];
+ uint8_t bpm;
+ uint8_t cadence;
+ uint8_t buttons;
+ uint8_t nv_page_size;
+ uint8_t nv_pages;
+ uint8_t param;
+ uint8_t state;
+ uint8_t status;
+ uint8_t device_num;
+ uint8_t cmd;
+ uint8_t remaining_bytes;
+ } heartbeat_trainer;
} device;
uint8_t output;
uint8_t control;
diff --git a/rom.db b/rom.db
index 48e4ba7..48960bc 100644
--- a/rom.db
+++ b/rom.db
@@ -1425,3 +1425,13 @@ NETO-001 {
}
}
+T-122026 {
+ name Outback Joey
+ HeartbeatTrainer {
+ size 512
+ }
+ device_overrides {
+ 1 heartbeat_trainer.1
+ 2 gamepad3.2
+ }
+}
diff --git a/romdb.c b/romdb.c
index 473707b..13a0d0d 100644
--- a/romdb.c
+++ b/romdb.c
@@ -32,6 +32,8 @@ char const *save_type_name(uint8_t save_type)
return "EEPROM";
} else if(save_type == SAVE_NOR) {
return "NOR Flash";
+ } else if(save_type == SAVE_HBPT) {
+ return "Heartbeat Personal Trainer";
}
return "SRAM";
}
@@ -986,6 +988,19 @@ rom_info configure_rom(tern_node *rom_db, void *vrom, uint32_t rom_size, void *l
info.port1_override = tern_find_ptr(device_overrides, "1");
info.port2_override = tern_find_ptr(device_overrides, "2");
info.ext_override = tern_find_ptr(device_overrides, "ext");
+ if (
+ info.save_type == SAVE_NONE
+ && (
+ (info.port1_override && startswith(info.port1_override, "heartbeat_trainer."))
+ || (info.port2_override && startswith(info.port2_override, "heartbeat_trainer."))
+ || (info.ext_override && startswith(info.ext_override, "heartbeat_trainer."))
+ )
+ ) {
+ info.save_type = SAVE_HBPT;
+ info.save_size = atoi(tern_find_path_default(entry, "HeartbeatTrainer\0size\0", (tern_val){.ptrval="512"}, TVAL_PTR).ptrval);
+ info.save_buffer = calloc(info.save_size + 5 + 8, 1);
+ memset(info.save_buffer, 0xFF, info.save_size);
+ }
} else {
info.port1_override = info.port2_override = info.ext_override = NULL;
}
diff --git a/romdb.h b/romdb.h
index 00524db..0589bfb 100644
--- a/romdb.h
+++ b/romdb.h
@@ -11,6 +11,7 @@
#define RAM_FLAG_MASK RAM_FLAG_ODD
#define SAVE_I2C 0x01
#define SAVE_NOR 0x02
+#define SAVE_HBPT 0x03
#define SAVE_NONE 0xFF
#include "tern.h"