Mercurial > repos > blastem
comparison io.c @ 2027:0f54a898db03
Implement Heartbeat Personal Trainer peripheral and add ROM DB entry for Outback Joey
author | Michael Pavone <pavone@retrodev.com> |
---|---|
date | Mon, 15 Feb 2021 11:24:06 -0800 |
parents | e7a516f08cec |
children | e597572f45ce |
comparison
equal
deleted
inserted
replaced
2026:aa338eb0eded | 2027:0f54a898db03 |
---|---|
38 "Sega multi-tap", | 38 "Sega multi-tap", |
39 "EA 4-way Play cable A", | 39 "EA 4-way Play cable A", |
40 "EA 4-way Play cable B", | 40 "EA 4-way Play cable B", |
41 "Sega Parallel Transfer Board", | 41 "Sega Parallel Transfer Board", |
42 "Generic Device", | 42 "Generic Device", |
43 "Generic Serial" | 43 "Generic Serial", |
44 "Heartbeat Personal Trainer" | |
44 }; | 45 }; |
45 | 46 |
46 #define GAMEPAD_TH0 0 | 47 #define GAMEPAD_TH0 0 |
47 #define GAMEPAD_TH1 1 | 48 #define GAMEPAD_TH1 1 |
48 #define GAMEPAD_EXTRA 2 | 49 #define GAMEPAD_EXTRA 2 |
55 enum { | 56 enum { |
56 IO_WRITE_PENDING, | 57 IO_WRITE_PENDING, |
57 IO_WRITTEN, | 58 IO_WRITTEN, |
58 IO_READ_PENDING, | 59 IO_READ_PENDING, |
59 IO_READ | 60 IO_READ |
61 }; | |
62 | |
63 enum { | |
64 HBPT_NEED_INIT, | |
65 HBPT_IDLE, | |
66 HBPT_CMD_PAYLOAD, | |
67 HBPT_REPLY | |
60 }; | 68 }; |
61 | 69 |
62 typedef struct { | 70 typedef struct { |
63 uint8_t states[2], value; | 71 uint8_t states[2], value; |
64 } gp_button_def; | 72 } gp_button_def; |
85 { | 93 { |
86 io_port *port = io->ports + i; | 94 io_port *port = io->ports + i; |
87 if (port->device_type < IO_MOUSE && port->device.pad.gamepad_num == gamepad_num) { | 95 if (port->device_type < IO_MOUSE && port->device.pad.gamepad_num == gamepad_num) { |
88 return port; | 96 return port; |
89 } | 97 } |
98 if (port->device_type == IO_HEARTBEAT_TRAINER && port->device.heartbeat_trainer.device_num == gamepad_num) { | |
99 return port; | |
100 } | |
90 } | 101 } |
91 return NULL; | 102 return NULL; |
92 } | 103 } |
93 | 104 |
94 static io_port *find_mouse(sega_io *io, uint8_t mouse_num) | 105 static io_port *find_mouse(sega_io *io, uint8_t mouse_num) |
246 port->device_type = IO_GAMEPAD2; | 257 port->device_type = IO_GAMEPAD2; |
247 } else { | 258 } else { |
248 port->device_type = IO_GAMEPAD6; | 259 port->device_type = IO_GAMEPAD6; |
249 } | 260 } |
250 port->device.pad.gamepad_num = device_type[gamepad_len+2] - '0'; | 261 port->device.pad.gamepad_num = device_type[gamepad_len+2] - '0'; |
262 } else if(startswith(device_type, "heartbeat_trainer.")) { | |
263 port->device_type = IO_HEARTBEAT_TRAINER; | |
264 port->device.heartbeat_trainer.nv_memory = NULL; | |
265 port->device.heartbeat_trainer.device_num = device_type[strlen("heartbeat_trainer.")] - '0'; | |
251 } else if(startswith(device_type, "mouse")) { | 266 } else if(startswith(device_type, "mouse")) { |
252 if (port->device_type != IO_MOUSE) { | 267 if (port->device_type != IO_MOUSE) { |
253 port->device_type = IO_MOUSE; | 268 port->device_type = IO_MOUSE; |
254 port->device.mouse.mouse_num = device_type[strlen("mouse")+1] - '0'; | 269 port->device.mouse.mouse_num = device_type[strlen("mouse")+1] - '0'; |
255 port->device.mouse.last_read_x = 0; | 270 port->device.mouse.last_read_x = 0; |
409 } | 424 } |
410 } else | 425 } else |
411 #endif | 426 #endif |
412 if (ports[i].device_type == IO_GAMEPAD3 || ports[i].device_type == IO_GAMEPAD6 || ports[i].device_type == IO_GAMEPAD2) { | 427 if (ports[i].device_type == IO_GAMEPAD3 || ports[i].device_type == IO_GAMEPAD6 || ports[i].device_type == IO_GAMEPAD2) { |
413 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]); | 428 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]); |
429 } else if (ports[i].device_type == IO_HEARTBEAT_TRAINER) { | |
430 debug_message("IO port %s connected to Heartbeat Personal Trainer #%d\n", io_name(i), ports[i].device.heartbeat_trainer.device_num); | |
431 if (rom->save_type == SAVE_HBPT) { | |
432 ports[i].device.heartbeat_trainer.nv_memory = rom->save_buffer; | |
433 uint32_t page_size = 16; | |
434 for (; page_size < 128; page_size *= 2) | |
435 { | |
436 if (rom->save_size / page_size < 256) { | |
437 break; | |
438 } | |
439 } | |
440 ports[i].device.heartbeat_trainer.nv_page_size = page_size; | |
441 uint32_t num_pages = rom->save_size / page_size; | |
442 ports[i].device.heartbeat_trainer.nv_pages = num_pages < 256 ? num_pages : 255; | |
443 } else { | |
444 ports[i].device.heartbeat_trainer.nv_page_size = 16; | |
445 ports[i].device.heartbeat_trainer.nv_pages = 32; | |
446 size_t bufsize = | |
447 ports[i].device.heartbeat_trainer.nv_page_size * ports[i].device.heartbeat_trainer.nv_pages | |
448 + 5 + 8; | |
449 ports[i].device.heartbeat_trainer.nv_memory = malloc(bufsize); | |
450 memset(ports[i].device.heartbeat_trainer.nv_memory, 0xFF, bufsize); | |
451 } | |
452 ports[i].device.heartbeat_trainer.state = HBPT_NEED_INIT; | |
414 } else { | 453 } else { |
415 debug_message("IO port %s connected to device '%s'\n", io_name(i), device_type_names[ports[i].device_type]); | 454 debug_message("IO port %s connected to device '%s'\n", io_name(i), device_type_names[ports[i].device_type]); |
416 } | 455 } |
417 } | 456 } |
418 } | 457 } |
639 } | 678 } |
640 } | 679 } |
641 } | 680 } |
642 #endif | 681 #endif |
643 | 682 |
683 enum { | |
684 HBPT_UNKNOWN1 = 1, | |
685 HBPT_POLL, | |
686 HBPT_READ_PAGE = 5, | |
687 HBPT_WRITE_PAGE, | |
688 HBPT_READ_RTC, | |
689 HBPT_SET_RTC, | |
690 HBPT_GET_STATUS, | |
691 HBPT_ERASE_NVMEM, | |
692 HBPT_NVMEM_PARAMS, | |
693 HBPT_INIT | |
694 }; | |
695 | |
696 static void start_reply(io_port *port, uint8_t bytes, const uint8_t *src) | |
697 { | |
698 port->device.heartbeat_trainer.remaining_bytes = bytes; | |
699 port->device.heartbeat_trainer.state = HBPT_REPLY; | |
700 port->device.heartbeat_trainer.cur_buffer = (uint8_t *)src; | |
701 } | |
702 | |
703 static void simple_reply(io_port *port, uint8_t value) | |
704 { | |
705 port->device.heartbeat_trainer.param = value; | |
706 start_reply(port, 1, &port->device.heartbeat_trainer.param); | |
707 } | |
708 | |
709 static void expect_payload(io_port *port, uint8_t bytes, uint8_t *dst) | |
710 { | |
711 port->device.heartbeat_trainer.remaining_bytes = bytes; | |
712 port->device.heartbeat_trainer.state = HBPT_CMD_PAYLOAD; | |
713 port->device.heartbeat_trainer.cur_buffer = dst; | |
714 } | |
715 | |
716 void hbpt_check_init(io_port *port) | |
717 { | |
718 if (port->device.heartbeat_trainer.state == HBPT_NEED_INIT) { | |
719 port->device.heartbeat_trainer.rtc_base_timestamp = 0; | |
720 for (int i = 0; i < 8; i ++) | |
721 { | |
722 port->device.heartbeat_trainer.rtc_base_timestamp <<= 8; | |
723 port->device.heartbeat_trainer.rtc_base_timestamp |= port->device.heartbeat_trainer.nv_memory[i]; | |
724 } | |
725 memcpy(port->device.heartbeat_trainer.rtc_base, port->device.heartbeat_trainer.nv_memory + 8, 5); | |
726 if (port->device.heartbeat_trainer.rtc_base_timestamp == UINT64_MAX) { | |
727 //uninitialized save, set the appropriate status bit | |
728 port->device.heartbeat_trainer.status |= 1; | |
729 } | |
730 port->device.heartbeat_trainer.bpm = 60; | |
731 port->device.heartbeat_trainer.state = HBPT_IDLE; | |
732 } | |
733 } | |
734 | |
735 void hbpt_check_send_reply(io_port *port) | |
736 { | |
737 if (port->device.heartbeat_trainer.state == HBPT_REPLY && !port->receive_end) { | |
738 port->serial_receiving = *(port->device.heartbeat_trainer.cur_buffer++); | |
739 port->receive_end = port->serial_cycle + 10 * port->serial_divider; | |
740 if (!--port->device.heartbeat_trainer.remaining_bytes) { | |
741 port->device.heartbeat_trainer.state = HBPT_IDLE; | |
742 } | |
743 } | |
744 } | |
745 | |
746 uint8_t is_leap_year(uint16_t year) | |
747 { | |
748 if (year & 3) { | |
749 return 0; | |
750 } | |
751 if (year % 100) { | |
752 return 1; | |
753 } | |
754 if (year % 400) { | |
755 return 0; | |
756 } | |
757 return 1; | |
758 } | |
759 | |
760 uint8_t days_in_month(uint8_t month, uint16_t year) | |
761 { | |
762 static uint8_t days_per_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; | |
763 if (month == 2 && is_leap_year(year)) { | |
764 return 29; | |
765 } | |
766 if (month > 12 || !month) { | |
767 return 30; | |
768 } | |
769 return days_per_month[month-1]; | |
770 } | |
771 | |
772 void hbpt_write_byte(io_port *port) | |
773 { | |
774 hbpt_check_init(port); | |
775 uint8_t reply; | |
776 switch (port->device.heartbeat_trainer.state) | |
777 { | |
778 case HBPT_IDLE: | |
779 port->device.heartbeat_trainer.cmd = port->serial_transmitting; | |
780 switch (port->device.heartbeat_trainer.cmd) | |
781 { | |
782 case HBPT_UNKNOWN1: | |
783 start_reply(port, 11, NULL); | |
784 break; | |
785 case HBPT_POLL: | |
786 start_reply(port, 3, &port->device.heartbeat_trainer.bpm); | |
787 if (port->serial_cycle - port->last_poll_cycle > MIN_POLL_INTERVAL) { | |
788 process_events(); | |
789 port->last_poll_cycle = port->serial_cycle; | |
790 } | |
791 port->device.heartbeat_trainer.buttons = (port->input[GAMEPAD_TH0] << 2 & 0xC0) | (port->input[GAMEPAD_TH1] & 0x1F); | |
792 if (port->device.heartbeat_trainer.cadence && port->input[GAMEPAD_TH1] & 0x20) { | |
793 port->device.heartbeat_trainer.cadence--; | |
794 printf("Cadence: %d\n", port->device.heartbeat_trainer.cadence); | |
795 } else if (port->device.heartbeat_trainer.cadence < 255 && port->input[GAMEPAD_EXTRA] & 1) { | |
796 port->device.heartbeat_trainer.cadence++; | |
797 printf("Cadence: %d\n", port->device.heartbeat_trainer.cadence); | |
798 } | |
799 if (port->device.heartbeat_trainer.bpm && port->input[GAMEPAD_EXTRA] & 4) { | |
800 port->device.heartbeat_trainer.bpm--; | |
801 printf("Heart Rate: %d\n", port->device.heartbeat_trainer.bpm); | |
802 } else if (port->device.heartbeat_trainer.bpm < 255 && port->input[GAMEPAD_EXTRA] & 2) { | |
803 port->device.heartbeat_trainer.bpm++; | |
804 printf("Heart Rate: %d\n", port->device.heartbeat_trainer.bpm); | |
805 } | |
806 | |
807 break; | |
808 case HBPT_READ_PAGE: | |
809 case HBPT_WRITE_PAGE: | |
810 //strictly speaking for the write case, we want 1 + page size here | |
811 //but the rest of the payload goes to a different destination | |
812 expect_payload(port, 1, &port->device.heartbeat_trainer.param); | |
813 break; | |
814 case HBPT_READ_RTC: { | |
815 uint8_t *rtc = port->device.heartbeat_trainer.rtc_base; | |
816 start_reply(port, 5, rtc); | |
817 uint64_t now = time(NULL); | |
818 uint64_t delta = (now - port->device.heartbeat_trainer.rtc_base_timestamp + 30) / 60; | |
819 rtc[4] += delta % 60; | |
820 if (rtc[4] > 59) { | |
821 rtc[4] -= 60; | |
822 rtc[3]++; | |
823 } | |
824 delta /= 60; | |
825 if (delta) { | |
826 rtc[3] += delta % 24; | |
827 delta /= 24; | |
828 if (rtc[3] > 23) { | |
829 rtc[3] -= 24; | |
830 delta++; | |
831 } | |
832 if (delta) { | |
833 uint16_t year = rtc[0] < 81 ? 2000 + rtc[0] : 1900 + rtc[0]; | |
834 uint8_t days_cur_month = days_in_month(rtc[1], year); | |
835 while (delta + rtc[2] > days_cur_month) { | |
836 delta -= days_cur_month + 1 - rtc[2]; | |
837 rtc[2] = 1; | |
838 if (++rtc[1] == 13) { | |
839 rtc[1] = 1; | |
840 year++; | |
841 } | |
842 days_cur_month = days_in_month(rtc[1], year); | |
843 } | |
844 rtc[1] += delta; | |
845 rtc[0] = year % 100; | |
846 } | |
847 } | |
848 printf("RTC %02d-%02d-%02d %02d:%02d\n", rtc[0], rtc[1], rtc[2], rtc[3], rtc[4]); | |
849 port->device.heartbeat_trainer.rtc_base_timestamp = now; | |
850 break; | |
851 } | |
852 case HBPT_SET_RTC: | |
853 port->device.heartbeat_trainer.rtc_base_timestamp = time(NULL); | |
854 expect_payload(port, 5, port->device.heartbeat_trainer.rtc_base); | |
855 break; | |
856 case HBPT_GET_STATUS: | |
857 simple_reply(port, port->device.heartbeat_trainer.status); | |
858 break; | |
859 case HBPT_ERASE_NVMEM: | |
860 expect_payload(port, 1, &port->device.heartbeat_trainer.param); | |
861 break; | |
862 case HBPT_NVMEM_PARAMS: | |
863 start_reply(port, 2, &port->device.heartbeat_trainer.nv_page_size); | |
864 break; | |
865 case HBPT_INIT: | |
866 expect_payload(port, 19, NULL); | |
867 break; | |
868 default: | |
869 // it's unclear what these commands do as they are unused by Outback Joey | |
870 // just return 0 to indicate failure | |
871 simple_reply(port, 0); | |
872 } | |
873 break; | |
874 case HBPT_CMD_PAYLOAD: | |
875 if (port->device.heartbeat_trainer.cur_buffer) { | |
876 *(port->device.heartbeat_trainer.cur_buffer++) = port->serial_transmitting; | |
877 } | |
878 if (!--port->device.heartbeat_trainer.remaining_bytes) { | |
879 switch (port->device.heartbeat_trainer.cmd) | |
880 { | |
881 case HBPT_READ_PAGE: | |
882 case HBPT_WRITE_PAGE: | |
883 if ( | |
884 port->device.heartbeat_trainer.cmd == HBPT_WRITE_PAGE | |
885 && port->device.heartbeat_trainer.cur_buffer != &port->device.heartbeat_trainer.param + 1) { | |
886 simple_reply(port, 1); | |
887 break; | |
888 } | |
889 port->device.heartbeat_trainer.remaining_bytes = port->device.heartbeat_trainer.nv_page_size; | |
890 port->device.heartbeat_trainer.cur_buffer = | |
891 port->device.heartbeat_trainer.param < port->device.heartbeat_trainer.nv_pages | |
892 ? port->device.heartbeat_trainer.nv_memory + 5 + 8 | |
893 + port->device.heartbeat_trainer.param * port->device.heartbeat_trainer.nv_page_size | |
894 : NULL; | |
895 if (port->device.heartbeat_trainer.cmd == HBPT_WRITE_PAGE) { | |
896 return; | |
897 } | |
898 port->device.heartbeat_trainer.state = HBPT_REPLY; | |
899 break; | |
900 case HBPT_SET_RTC: | |
901 //save RTC base values back to nv memory area so it's saved to disk on exit | |
902 for (int i = 0; i < 8; i++) | |
903 { | |
904 port->device.heartbeat_trainer.nv_memory[i] = port->device.heartbeat_trainer.rtc_base_timestamp >> (56 - i*8); | |
905 } | |
906 memcpy(port->device.heartbeat_trainer.nv_memory + 8, port->device.heartbeat_trainer.rtc_base, 5); | |
907 simple_reply(port, 1); | |
908 break; | |
909 case HBPT_ERASE_NVMEM: | |
910 memset( | |
911 port->device.heartbeat_trainer.nv_memory + 5 + 8, | |
912 port->device.heartbeat_trainer.param, | |
913 port->device.heartbeat_trainer.nv_pages * port->device.heartbeat_trainer.nv_page_size | |
914 ); | |
915 simple_reply(port, 1); | |
916 break; | |
917 case HBPT_INIT: { | |
918 static const char reply[] = "(C) HEARTBEAT CORP"; | |
919 start_reply(port, strlen(reply), reply); | |
920 break; | |
921 } | |
922 } | |
923 } | |
924 } | |
925 hbpt_check_send_reply(port); | |
926 } | |
927 | |
928 void hbpt_read_byte(io_port *port) | |
929 { | |
930 hbpt_check_init(port); | |
931 hbpt_check_send_reply(port); | |
932 } | |
933 | |
644 const int mouse_delays[] = {112*7, 120*7, 96*7, 132*7, 104*7, 96*7, 112*7, 96*7}; | 934 const int mouse_delays[] = {112*7, 120*7, 96*7, 132*7, 104*7, 96*7, 112*7, 96*7}; |
645 | 935 |
646 enum { | 936 enum { |
647 KB_SETUP, | 937 KB_SETUP, |
648 KB_READ, | 938 KB_READ, |
665 port->transmit_end = 0; | 955 port->transmit_end = 0; |
666 | 956 |
667 if (port->serial_ctrl & SCTRL_BIT_TX_ENABLE) { | 957 if (port->serial_ctrl & SCTRL_BIT_TX_ENABLE) { |
668 switch (port->device_type) | 958 switch (port->device_type) |
669 { | 959 { |
960 case IO_HEARTBEAT_TRAINER: | |
961 hbpt_write_byte(port); | |
962 break; | |
670 #ifndef _WIN32 | 963 #ifndef _WIN32 |
671 case IO_GENERIC_SERIAL: | 964 case IO_GENERIC_SERIAL: |
672 write_serial_byte(port); | 965 write_serial_byte(port); |
673 break; | 966 break; |
674 #endif | 967 #endif |
691 port->receive_end = 0; | 984 port->receive_end = 0; |
692 } | 985 } |
693 if (!port->receive_end) { | 986 if (!port->receive_end) { |
694 switch(port->device_type) | 987 switch(port->device_type) |
695 { | 988 { |
989 case IO_HEARTBEAT_TRAINER: | |
990 hbpt_read_byte(port); | |
991 break; | |
696 #ifndef _WIN32 | 992 #ifndef _WIN32 |
697 case IO_GENERIC_SERIAL: | 993 case IO_GENERIC_SERIAL: |
698 read_serial_byte(port); | 994 read_serial_byte(port); |
699 break; | 995 break; |
700 #endif | 996 #endif |