From ef2ca135d54c972e00b099e796f673fd952fc3de Mon Sep 17 00:00:00 2001 From: Keith Conger Date: Tue, 17 Feb 2026 22:44:10 -0700 Subject: [PATCH] Workboy (#278) * Workboy emulation * Update ReadMe.md --- Gameboy.sv | 24 +++- ReadMe.md | 1 + files.qip | 1 + rtl/workboy.sv | 311 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 333 insertions(+), 4 deletions(-) create mode 100644 rtl/workboy.sv diff --git a/Gameboy.sv b/Gameboy.sv index ef5dfd4..9ae2d34 100644 --- a/Gameboy.sv +++ b/Gameboy.sv @@ -62,7 +62,7 @@ module emu input [11:0] HDMI_WIDTH, input [11:0] HDMI_HEIGHT, output HDMI_FREEZE, - output HDMI_BLACKOUT, + output HDMI_BLACKOUT, output HDMI_BOB_DEINT, `ifdef MISTER_FB @@ -204,7 +204,7 @@ assign AUDIO_MIX = status[8:7]; // 0 1 2 3 4 5 6 // 01234567890123456789012345678901 23456789012345678901234567890123 // 0123456789ABCDEFGHIJKLMNOPQRSTUV 0123456789ABCDEFGHIJKLMNOPQRSTUV -// XXXXXXXXXXX XXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXX +// XXXXXXXXXXX XXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXX `include "build_id.v" localparam CONF_STR = { @@ -260,6 +260,7 @@ localparam CONF_STR = { "P3,Misc.;", "P3-;", "P3O6,Link Port,Disabled,Enabled;", + "P3O[45],WorkBoy Keyboard,Off,On;", "P3o6,Rumble,On,Off;", "P3-;", "P3OP,FastForward Sound,On,Off;", @@ -339,6 +340,7 @@ wire img_readonly; wire [63:0] img_size; wire [15:0] joy0_rumble; +wire [64:0] RTC_bcd; wire [32:0] RTC_time; wire sys_auto = (status[15:14] == 0); @@ -395,6 +397,7 @@ hps_io #(.CONF_STR(CONF_STR), .WIDE(1)) hps_io .info_req(ss_info_req), .info(ss_info), + .RTC(RTC_bcd), .TIMESTAMP(RTC_time) ); @@ -1052,9 +1055,22 @@ wire ser_data_in; wire ser_data_out; wire ser_clk_in; wire ser_clk_out; -wire serial_ena = status[6]; +wire workboy_ena = status[45]; +wire serial_ena = status[6] & ~workboy_ena; +wire workboy_data_out; -assign ser_data_in = serial_ena ? USER_IN[2] : 1'b1; +workboy workboy +( + .clk_sys(clk_sys), + .reset(reset | ~workboy_ena), + .ps2_key(ps2_key), + .rtc_bcd(RTC_bcd), + .serial_clk_in(ser_clk_out), + .serial_data_in(ser_data_out), + .serial_data_out(workboy_data_out) +); + +assign ser_data_in = workboy_ena ? workboy_data_out : (serial_ena ? USER_IN[2] : 1'b1); assign USER_OUT[1] = serial_ena ? ser_data_out : 1'b1; assign ser_clk_in = serial_ena ? USER_IN[0] : 1'b1; diff --git a/ReadMe.md b/ReadMe.md index 723ef6e..c70578e 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -17,6 +17,7 @@ _This is a port of [Gameboy for MiST](https://github.com/mist-devel/gameboy)_ * Custom Palette Loading * Real-Time Clock Support * Gameboy Link Port Support - Requires USERIO adapter +* Workboy * Cheats * Fast boot * GBA mode for GBC games diff --git a/files.qip b/files.qip index 1bcfbbf..7a88645 100644 --- a/files.qip +++ b/files.qip @@ -21,6 +21,7 @@ set_global_assignment -name SYSTEMVERILOG_FILE rtl/savestate_ui.sv set_global_assignment -name VERILOG_FILE rtl/gb.v set_global_assignment -name VERILOG_FILE rtl/hdma.v set_global_assignment -name VERILOG_FILE rtl/link.v +set_global_assignment -name SYSTEMVERILOG_FILE rtl/workboy.sv set_global_assignment -name VERILOG_FILE rtl/sgb.v set_global_assignment -name VERILOG_FILE rtl/cart.v set_global_assignment -name VERILOG_FILE rtl/mappers/mappers.v diff --git a/rtl/workboy.sv b/rtl/workboy.sv new file mode 100644 index 0000000..99566db --- /dev/null +++ b/rtl/workboy.sv @@ -0,0 +1,311 @@ +// Workboy +// Based on all published information, this is probably incomplete. + + +module workboy +( + input clk_sys, + input reset, + + input [10:0] ps2_key, + input [64:0] rtc_bcd, + + input serial_clk_in, + input serial_data_in, + output reg serial_data_out +); + +localparam [7:0] CMD_R = 8'h52; // 'R' +localparam [7:0] CMD_W = 8'h57; // 'W' +localparam [7:0] CMD_O = 8'h4F; // 'O' +localparam [7:0] RESP_D = 8'h44; // 'D' + +localparam [7:0] KEY_NONE = 8'hFF; +localparam [7:0] KEY_REQUIRE = 8'h40; +localparam [7:0] KEY_FORBID = 8'h80; +localparam [7:0] KEY_SHIFT_DOWN = 8'd39; // WorkBoy NUM mode key +localparam [7:0] KEY_SHIFT_UP = 8'd50; // WorkBoy CAPS mode key + +function automatic [8:0] map_ps2_to_workboy; + input [7:0] scancode; + input extended; + input shift_held; + begin + map_ps2_to_workboy = 9'h000; + + if (extended) begin + case (scancode) + 8'h6B: map_ps2_to_workboy = {1'b1, 8'd13}; // Left + 8'h70: map_ps2_to_workboy = {1'b1, 8'd12}; // Insert/Unknown + 8'h71: map_ps2_to_workboy = {1'b1, 8'd11}; // Delete/Backspace + 8'h72: map_ps2_to_workboy = {1'b1, 8'd55}; // Down + 8'h74: map_ps2_to_workboy = {1'b1, 8'd56}; // Right + 8'h75: map_ps2_to_workboy = {1'b1, 8'd54}; // Up + default: ; + endcase + end else begin + case (scancode) + 8'h01: map_ps2_to_workboy = {1'b1, 8'd9}; // F9 + 8'h03: map_ps2_to_workboy = {1'b1, 8'd5}; // F5 + 8'h04: map_ps2_to_workboy = {1'b1, 8'd3}; // F3 + 8'h05: map_ps2_to_workboy = {1'b1, 8'd1}; // F1 + 8'h06: map_ps2_to_workboy = {1'b1, 8'd2}; // F2 + 8'h09: map_ps2_to_workboy = {1'b1, 8'd12}; // F10 + 8'h0A: map_ps2_to_workboy = {1'b1, 8'd8}; // F8 + 8'h0B: map_ps2_to_workboy = {1'b1, 8'd6}; // F6 + 8'h0C: map_ps2_to_workboy = {1'b1, 8'd4}; // F4 + 8'h0D: map_ps2_to_workboy = {1'b1, KEY_SHIFT_DOWN}; // Tab -> NUM mode + 8'h0E: map_ps2_to_workboy = {1'b1, shift_held ? (8'd25 | KEY_REQUIRE) : 8'd51}; // ` or ~ + 8'h15: map_ps2_to_workboy = {1'b1, 8'd17}; // Q + 8'h16: map_ps2_to_workboy = {1'b1, shift_held ? (8'd24 | KEY_REQUIRE) : (8'd17 | KEY_REQUIRE)}; // 1 or ! + 8'h1A: map_ps2_to_workboy = {1'b1, 8'd40}; // Z + 8'h1B: map_ps2_to_workboy = {1'b1, 8'd29}; // S + 8'h1C: map_ps2_to_workboy = {1'b1, 8'd28}; // A + 8'h1D: map_ps2_to_workboy = {1'b1, 8'd18}; // W + 8'h1E: map_ps2_to_workboy = {1'b1, shift_held ? (8'd53 | KEY_REQUIRE) : (8'd18 | KEY_REQUIRE)}; // 2 or @ + 8'h21: map_ps2_to_workboy = {1'b1, 8'd42}; // C + 8'h22: map_ps2_to_workboy = {1'b1, 8'd41}; // X + 8'h23: map_ps2_to_workboy = {1'b1, 8'd30}; // D + 8'h24: map_ps2_to_workboy = {1'b1, 8'd19}; // E + 8'h25: map_ps2_to_workboy = {1'b1, shift_held ? (8'd27 | KEY_FORBID) : (8'd28 | KEY_REQUIRE)}; // 4 or $ + 8'h26: map_ps2_to_workboy = {1'b1, shift_held ? (8'd27 | KEY_REQUIRE) : (8'd19 | KEY_REQUIRE)}; // 3 or # + 8'h29: map_ps2_to_workboy = {1'b1, 8'd52}; // Space + 8'h2A: map_ps2_to_workboy = {1'b1, shift_held ? (8'd43 | KEY_REQUIRE) : 8'd43}; // V or . + 8'h2B: map_ps2_to_workboy = {1'b1, 8'd31}; // F + 8'h2C: map_ps2_to_workboy = {1'b1, shift_held ? (8'd21 | KEY_REQUIRE) : 8'd21}; // T or M- + 8'h2D: map_ps2_to_workboy = {1'b1, shift_held ? (8'd20 | KEY_REQUIRE) : 8'd20}; // R or M+ + 8'h2E: map_ps2_to_workboy = {1'b1, shift_held ? (8'd44 | KEY_REQUIRE) : (8'd29 | KEY_REQUIRE)}; // 5 or % + 8'h31: map_ps2_to_workboy = {1'b1, 8'd45}; // N + 8'h32: map_ps2_to_workboy = {1'b1, 8'd44}; // B + 8'h33: map_ps2_to_workboy = {1'b1, shift_held ? (8'd33 | KEY_REQUIRE) : 8'd33}; // H or x + 8'h34: map_ps2_to_workboy = {1'b1, 8'd32}; // G + 8'h35: map_ps2_to_workboy = {1'b1, shift_held ? (8'd22 | KEY_REQUIRE) : 8'd22}; // Y or MR + 8'h36: map_ps2_to_workboy = {1'b1, (8'd30 | KEY_REQUIRE)}; // 6 + 8'h3A: map_ps2_to_workboy = {1'b1, shift_held ? (8'd46 | KEY_REQUIRE) : 8'd46}; // M or C + 8'h3B: map_ps2_to_workboy = {1'b1, shift_held ? (8'd34 | KEY_REQUIRE) : 8'd34}; // J or divide + 8'h3C: map_ps2_to_workboy = {1'b1, shift_held ? (8'd23 | KEY_REQUIRE) : 8'd23}; // U or MC + 8'h3D: map_ps2_to_workboy = {1'b1, (8'd40 | KEY_REQUIRE)}; // 7 (Shift+7 '&' has no direct WorkBoy symbol) + 8'h3E: map_ps2_to_workboy = {1'b1, shift_held ? (8'd26 | KEY_REQUIRE) : (8'd41 | KEY_REQUIRE)}; // 8 or * + 8'h41: map_ps2_to_workboy = {1'b1, shift_held ? (8'd47 | KEY_REQUIRE) : (8'd47 | KEY_FORBID)}; // , or < + 8'h42: map_ps2_to_workboy = {1'b1, 8'd35}; // K + 8'h43: map_ps2_to_workboy = {1'b1, 8'd24}; // I + 8'h44: map_ps2_to_workboy = {1'b1, shift_held ? (8'd25 | KEY_REQUIRE) : 8'd25}; // O or pound + 8'h45: map_ps2_to_workboy = {1'b1, shift_held ? (8'd36 | KEY_REQUIRE) : (8'd51 | KEY_REQUIRE)}; // 0 or ) + 8'h46: map_ps2_to_workboy = {1'b1, shift_held ? (8'd35 | KEY_REQUIRE) : (8'd42 | KEY_REQUIRE)}; // 9 or ( + 8'h49: map_ps2_to_workboy = {1'b1, shift_held ? (8'd48 | KEY_REQUIRE) : (8'd48 | KEY_FORBID)}; // . or > + 8'h4A: map_ps2_to_workboy = {1'b1, shift_held ? (8'd49 | KEY_REQUIRE) : (8'd49 | KEY_FORBID)}; // / or ? + 8'h4B: map_ps2_to_workboy = {1'b1, 8'd36}; // L + 8'h4C: map_ps2_to_workboy = {1'b1, shift_held ? 8'd37 : (8'd37 | KEY_FORBID)}; // ; or : + 8'h4D: map_ps2_to_workboy = {1'b1, 8'd26}; // P + 8'h4E: map_ps2_to_workboy = {1'b1, (8'd32 | KEY_REQUIRE)}; // - + 8'h52: map_ps2_to_workboy = {1'b1, shift_held ? (8'd53 | KEY_REQUIRE) : (8'd53 | KEY_FORBID)}; // ' or @ + 8'h54: map_ps2_to_workboy = {1'b1, (8'd35 | KEY_REQUIRE)}; // [ -> ( + 8'h55: map_ps2_to_workboy = {1'b1, shift_held ? (8'd31 | KEY_REQUIRE) : (8'd45 | KEY_REQUIRE)}; // = or + + 8'h58: map_ps2_to_workboy = {1'b1, KEY_SHIFT_UP}; // Caps Lock -> CAPS mode + 8'h5A: map_ps2_to_workboy = {1'b1, 8'd38}; // Enter + 8'h5B: map_ps2_to_workboy = {1'b1, (8'd36 | KEY_REQUIRE)}; // ] -> ) + 8'h5D: map_ps2_to_workboy = {1'b1, shift_held ? (8'd27 | KEY_REQUIRE) : (8'd27 | KEY_FORBID)}; // \ or | + 8'h61: map_ps2_to_workboy = {1'b1, shift_held ? (8'd27 | KEY_REQUIRE) : (8'd27 | KEY_FORBID)}; // Non-US \ or | + 8'h66: map_ps2_to_workboy = {1'b1, 8'd11}; // Backspace + 8'h76: map_ps2_to_workboy = {1'b1, 8'd10}; // Escape + 8'h77: map_ps2_to_workboy = {1'b1, KEY_SHIFT_DOWN}; // Num Lock -> NUM mode + 8'h83: map_ps2_to_workboy = {1'b1, 8'd7}; // F7 + default: ; + endcase + end + end +endfunction + +function automatic [7:0] bcd_to_bin; + input [7:0] bcd; + begin + bcd_to_bin = (bcd[7:4] * 8'd10) + bcd[3:0]; + end +endfunction + +function automatic [7:0] nibble_to_ascii_hex; + input [3:0] nibble; + begin + nibble_to_ascii_hex = (nibble < 4'd10) ? (8'h30 + nibble) : (8'h41 + nibble - 4'd10); + end +endfunction + +reg ps2_last; +reg phys_shift_down; +reg shift_down; +reg user_shift_down; +wire ps2_event = (ps2_last != ps2_key[10]); +wire ps2_pressed = ps2_key[9]; +wire ps2_extended = ps2_key[8]; +wire [7:0] ps2_scancode = ps2_key[7:0]; + +reg [7:0] wb_key; +reg [7:0] mode; +reg [7:0] buffer[0:20]; +reg [5:0] buffer_index; + +reg clk_last; +reg clk_rise_armed; +wire clk_falling = clk_last & ~serial_clk_in; +wire clk_rising = ~clk_last & serial_clk_in; + +reg [2:0] bit_count; +reg [7:0] rx_shift; +wire [7:0] rx_byte = {rx_shift[6:0], serial_data_in}; +reg [7:0] tx_shift; + +reg [8:0] mapped_key; +reg [7:0] key_next; +reg [7:0] next_byte; +reg [7:0] year_tm; +integer i; + +always @(posedge clk_sys) begin + ps2_last <= ps2_key[10]; + clk_last <= serial_clk_in; + + if (reset) begin + serial_data_out <= 1'b1; + ps2_last <= 1'b0; + phys_shift_down <= 1'b0; + shift_down <= 1'b0; + user_shift_down <= 1'b0; + wb_key <= 8'h00; + mode <= 8'h00; + buffer_index <= 6'd0; + clk_last <= serial_clk_in; + clk_rise_armed <= 1'b0; + bit_count <= 3'd0; + rx_shift <= 8'h00; + tx_shift <= 8'h00; + for (i = 0; i < 21; i = i + 1) + buffer[i] <= 8'h00; + end else begin + if (ps2_event) begin + mapped_key = 9'h000; + + if (!ps2_extended && (ps2_scancode == 8'h12 || ps2_scancode == 8'h59)) begin + phys_shift_down <= ps2_pressed; + mapped_key = {1'b1, ps2_pressed ? KEY_SHIFT_DOWN : KEY_SHIFT_UP}; + end else if (ps2_pressed) begin + mapped_key = map_ps2_to_workboy(ps2_scancode, ps2_extended, phys_shift_down); + end else if (ps2_scancode != 8'hF0) begin + wb_key <= KEY_NONE; + end + + if (mapped_key[8]) begin + key_next = mapped_key[7:0]; + + if (((key_next & (KEY_REQUIRE | KEY_FORBID)) == 8'h00) && (user_shift_down != shift_down)) begin + if (user_shift_down) key_next = key_next | KEY_REQUIRE; + else key_next = key_next | KEY_FORBID; + end + + wb_key <= key_next; + end + end + + if (clk_falling) begin + clk_rise_armed <= 1'b1; + serial_data_out <= tx_shift[7]; + end + + if (clk_rising && clk_rise_armed) begin + clk_rise_armed <= 1'b0; + rx_shift <= rx_byte; + + if (bit_count == 3'd7) begin + bit_count <= 3'd0; + next_byte = 8'h00; + + if ((mode != CMD_W) && (rx_byte == CMD_R)) begin + next_byte = RESP_D; + wb_key <= KEY_NONE; + mode <= CMD_R; + buffer_index <= 6'd1; + year_tm = 8'd100 + bcd_to_bin(rtc_bcd[47:40]); + + for (i = 0; i < 21; i = i + 1) + buffer[i] <= 8'h00; + buffer[0] <= 8'h04; + buffer[2] <= rtc_bcd[7:0]; + buffer[3] <= rtc_bcd[15:8]; + buffer[4] <= rtc_bcd[23:16]; + buffer[5] <= rtc_bcd[31:24]; + buffer[6] <= rtc_bcd[39:32]; + buffer[15] <= year_tm; + end else if ((mode != CMD_W) && (rx_byte == CMD_W)) begin + next_byte = RESP_D; + mode <= CMD_W; + wb_key <= KEY_NONE; + buffer_index <= 6'd0; + end else if ((mode != CMD_W) && ((rx_byte == CMD_O) || (mode == CMD_O))) begin + mode <= CMD_O; + next_byte = wb_key; + + if (wb_key != KEY_NONE) begin + if (wb_key & KEY_REQUIRE) begin + key_next = wb_key & ~KEY_REQUIRE; + wb_key <= key_next; + if (shift_down) begin + next_byte = key_next; + wb_key <= KEY_NONE; + end else begin + next_byte = KEY_SHIFT_DOWN; + shift_down <= 1'b1; + end + end else if (wb_key & KEY_FORBID) begin + key_next = wb_key & ~KEY_FORBID; + wb_key <= key_next; + if (!shift_down) begin + next_byte = key_next; + wb_key <= KEY_NONE; + end else begin + next_byte = KEY_SHIFT_UP; + shift_down <= 1'b0; + end + end else begin + if (wb_key == KEY_SHIFT_DOWN) begin + shift_down <= 1'b1; + user_shift_down <= 1'b1; + end else if (wb_key == KEY_SHIFT_UP) begin + shift_down <= 1'b0; + user_shift_down <= 1'b0; + end + next_byte = wb_key; + wb_key <= KEY_NONE; + end + end + end else if (mode == CMD_R) begin + if (buffer_index >= 6'd42) begin + next_byte = 8'h00; + end else begin + if (buffer_index[0]) next_byte = nibble_to_ascii_hex(buffer[buffer_index[5:1]][3:0]); + else next_byte = nibble_to_ascii_hex(buffer[buffer_index[5:1]][7:4]); + buffer_index <= buffer_index + 1'd1; + end + end else if (mode == CMD_W) begin + next_byte = RESP_D; + if (buffer_index < 6'd2) begin + buffer_index <= buffer_index + 1'd1; + end else if ((buffer_index - 6'd2) < 6'd21) begin + buffer[buffer_index - 6'd2] <= rx_byte; + buffer_index <= buffer_index + 1'd1; + if ((buffer_index + 6'd1 - 6'd2) == 6'd21) + mode <= CMD_O; + end + end + + tx_shift <= next_byte; + end else begin + bit_count <= bit_count + 1'd1; + tx_shift <= {tx_shift[6:0], 1'b0}; + end + end else if (clk_rising) begin + clk_rise_armed <= 1'b0; + bit_count <= 3'd0; + end + end +end + +endmodule