mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
runtime: PS1 main-RAM read wait states — fix MMX6 memcard "invalid"
Model the R3000A having no usable D-cache (its data cache is repurposed as the scratchpad): every CPU data read from main DRAM costs +6 wait-state cycles on top of the 1-cycle opcode (DuckStation RAM_READ_TICKS = 6). The recompiler previously charged 1 cycle/instruction flat, so BIOS/game busy- wait poll loops ran ~6x too fast in guest time. Concretely this fixes Mega Man X6's "This data is invalid" memory-card error: the game's fixed-iteration TestEvent poll (A0(0xAC) directory load) exhausted at ~43 frames while the VBlank-paced card directory op needs ~72 frames, so the poll timed out mid-operation; the retry re-called A0(0xAC), A0(0x4F) returned card-busy, cleared A[port]=0, and orphaned the in-flight sector completion -> permanent stall. With realistic per-read latency the poll now outlasts the op (op runs once to completion, broken-sector index reaches 20, terminal event fires), and the card screen shows the correct "no save data found" message. Verified: empty card + a populated card both load correctly; gameplay stable. - memory.c: psx_guest_read_word/half/byte wrappers add +6 cyc for reads whose effective physical address is main RAM (keyed on runtime address, so relocated kernel code is timed correctly; scratchpad/MMIO/ROM excluded). - main.cpp: wire cpu.read_word/half/byte to the guest wrappers (covers static-recompiled and dirty-RAM-interpreted loads, incl. LWC2). - debug_server.c: trace_read_word/half use psx_read_* directly so debug reads (IO thread) never charge guest cycles or race the emu thread. Also includes prior uncommitted DualShock SIO config state machine (sio.c) + dual keyboard/pad P1 input (main.cpp) from the MMX6 bring-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -546,24 +546,30 @@ void debug_server_log_restore_event(uint32_t kind, uint32_t target_pc, uint32_t
|
||||
e->in_exception = (uint8_t)psx_get_in_exception();
|
||||
}
|
||||
|
||||
extern uint16_t psx_read_half(uint32_t addr);
|
||||
static uint32_t trace_read_word(CPUState *cpu, uint32_t addr)
|
||||
{
|
||||
(void)cpu;
|
||||
uint32_t phys = addr & 0x1FFFFFFFu;
|
||||
if (phys > 0x001FFFFCu &&
|
||||
(phys < 0x1F800000u || phys > 0x1F8003FCu)) {
|
||||
return 0;
|
||||
}
|
||||
return cpu ? cpu->read_word(addr) : 0;
|
||||
/* Use psx_read_word directly (NOT cpu->read_word): debug instrumentation
|
||||
* must not charge guest main-RAM read wait states, and runs on the IO
|
||||
* thread where psx_advance_cycles would race the emulation thread. */
|
||||
return psx_read_word(addr);
|
||||
}
|
||||
|
||||
static uint32_t trace_read_half(CPUState *cpu, uint32_t addr)
|
||||
{
|
||||
(void)cpu;
|
||||
uint32_t phys = addr & 0x1FFFFFFFu;
|
||||
if (phys > 0x001FFFFEu &&
|
||||
(phys < 0x1F800000u || phys > 0x1F8003FEu)) {
|
||||
return 0;
|
||||
}
|
||||
return cpu ? cpu->read_half(addr) : 0;
|
||||
return psx_read_half(addr);
|
||||
}
|
||||
|
||||
static int sreg_trace_focus_func(uint32_t func)
|
||||
|
||||
+58
-11
@@ -96,6 +96,12 @@ extern "C" uint16_t psx_read_half(uint32_t addr);
|
||||
extern "C" void psx_write_half(uint32_t addr, uint16_t val);
|
||||
extern "C" uint8_t psx_read_byte(uint32_t addr);
|
||||
extern "C" void psx_write_byte(uint32_t addr, uint8_t val);
|
||||
/* Guest-side data-read wrappers: same as psx_read_* but charge PS1 main-RAM
|
||||
* read wait states (R3000A has no D-cache). Wired to cpu->read_* below so the
|
||||
* timing applies to recompiled + interpreted guest loads, not debug/device reads. */
|
||||
extern "C" uint32_t psx_guest_read_word(uint32_t addr);
|
||||
extern "C" uint16_t psx_guest_read_half(uint32_t addr);
|
||||
extern "C" uint8_t psx_guest_read_byte(uint32_t addr);
|
||||
|
||||
/* ---- SDL state ---- */
|
||||
static SDL_Window* sdl_window;
|
||||
@@ -1197,6 +1203,18 @@ static bool hybrid_dpad_active(const PlayerInput& p, bool kb_always) {
|
||||
* (stick -> analog, D-pad -> digital) so the game runs its own analog or
|
||||
* digital input path exactly as on hardware (mirrors the inline block this
|
||||
* helper was extracted from for the low-latency re-sample). */
|
||||
/* Dev builds (PSX_DEBUG_TOOLS — the dev/debug configuration) ALWAYS feed the
|
||||
* keyboard into Player 1 alongside whatever device the launcher routed to port 1,
|
||||
* so a tester can drive the game from EITHER the keyboard OR the selected
|
||||
* controller (e.g. a DualShock) at the same time, with no reconfiguration. The
|
||||
* keyboard map and the controller are merged (active-low AND) onto the P1 pad
|
||||
* word. Release builds keep the single launcher-selected device per port. */
|
||||
#if defined(PSX_DEBUG_TOOLS)
|
||||
static const bool g_dev_kb_p1 = true;
|
||||
#else
|
||||
static const bool g_dev_kb_p1 = false;
|
||||
#endif
|
||||
|
||||
static void sample_pad_into_sio(int override) {
|
||||
if (override >= 0) {
|
||||
sio_set_pad_state_slot(0, (uint16_t)override);
|
||||
@@ -1204,23 +1222,40 @@ static void sample_pad_into_sio(int override) {
|
||||
}
|
||||
for (int s = 0; s < 2; s++) {
|
||||
PlayerInput& p = g_players[s];
|
||||
if (p.kind == 0) continue; /* no device in this port */
|
||||
sio_set_pad_state_slot(s, pad_buttons_for(p));
|
||||
const bool kb_here = (g_dev_kb_p1 && s == 0);
|
||||
if (p.kind == 0 && !kb_here) continue; /* no device in this port */
|
||||
|
||||
/* Resolve the pad type this frame from the player's mode. */
|
||||
/* Buttons: merge the assigned device with the keyboard (PSX pad word is
|
||||
* active-low, so AND combines "pressed on either source"). */
|
||||
uint16_t btn = (p.kind != 0) ? pad_buttons_for(p) : (uint16_t)0xFFFF;
|
||||
if (kb_here) btn &= pad_from_keyboard();
|
||||
sio_set_pad_state_slot(s, btn);
|
||||
|
||||
/* Resolve the pad type this frame from the player's mode (a port driven
|
||||
* by the keyboard alone presents as a digital pad). */
|
||||
const int mode = (p.kind != 0) ? p.mode : (int)PSXRecompV4::PAD_MODE_DIGITAL;
|
||||
int eff_analog;
|
||||
uint8_t st[4] = { 0x80, 0x80, 0x80, 0x80 };
|
||||
if (p.mode == PSXRecompV4::PAD_MODE_DIGITAL) {
|
||||
if (mode == PSXRecompV4::PAD_MODE_DIGITAL) {
|
||||
eff_analog = 0;
|
||||
} else if (p.mode == PSXRecompV4::PAD_MODE_ANALOG) {
|
||||
} else if (mode == PSXRecompV4::PAD_MODE_ANALOG) {
|
||||
eff_analog = 1;
|
||||
pad_sticks_for(p, st, /*fold_dpad=*/true);
|
||||
} else { /* HYBRID */
|
||||
if (hybrid_stick_active(p)) p.hybrid_analog = true;
|
||||
else if (hybrid_dpad_active(p, false)) p.hybrid_analog = false;
|
||||
if (hybrid_stick_active(p)) p.hybrid_analog = true;
|
||||
else if (hybrid_dpad_active(p, kb_here)) p.hybrid_analog = false;
|
||||
eff_analog = p.hybrid_analog ? 1 : 0;
|
||||
if (eff_analog) pad_sticks_for(p, st, /*fold_dpad=*/false);
|
||||
}
|
||||
/* In dev builds also fold the keyboard arrows onto the analog stick, so an
|
||||
* analog-mode P1 still steers from the keyboard. */
|
||||
if (kb_here && eff_analog) {
|
||||
const Uint8* keys = SDL_GetKeyboardState(NULL);
|
||||
if (keys[SDL_SCANCODE_LEFT]) st[0] = 0x00;
|
||||
if (keys[SDL_SCANCODE_RIGHT]) st[0] = 0xFF;
|
||||
if (keys[SDL_SCANCODE_UP]) st[1] = 0x00;
|
||||
if (keys[SDL_SCANCODE_DOWN]) st[1] = 0xFF;
|
||||
}
|
||||
sio_set_pad_analog(s, eff_analog, st[0], st[1], st[2], st[3]);
|
||||
}
|
||||
}
|
||||
@@ -1796,7 +1831,16 @@ int main(int argc, char** argv) {
|
||||
bool memcard1_enabled = true;
|
||||
bool memcard2_enabled = true;
|
||||
/* [controller] device routing (defaults: P1 keyboard/digital, P2 none). */
|
||||
/* Dev builds default Player 1 to the first connected controller ("auto"):
|
||||
* combined with the always-on keyboard merge (g_dev_kb_p1) this means the
|
||||
* selected controller AND the keyboard both drive P1 with no launcher setup.
|
||||
* If no controller is present, "auto" opens nothing and the keyboard merge
|
||||
* still drives P1. Release keeps "keyboard" (the launcher assigns devices). */
|
||||
#if defined(PSX_DEBUG_TOOLS)
|
||||
std::string p1_device = "auto";
|
||||
#else
|
||||
std::string p1_device = "keyboard";
|
||||
#endif
|
||||
std::string p2_device = "none";
|
||||
int p1_mode = PSXRecompV4::PAD_MODE_HYBRID;
|
||||
int p2_mode = PSXRecompV4::PAD_MODE_HYBRID;
|
||||
@@ -2212,7 +2256,10 @@ int main(int argc, char** argv) {
|
||||
set_player_device(g_players[0], p1_device, p1_mode);
|
||||
set_player_device(g_players[1], p2_device, p2_mode);
|
||||
for (int s = 0; s < 2; s++) {
|
||||
sio_set_pad_connected(s, g_players[s].kind != 0 ? 1 : 0);
|
||||
/* Dev builds keep P1 connected even with no controller so the always-on
|
||||
* keyboard (g_dev_kb_p1) can drive port 1 standalone. */
|
||||
const bool kb_p1 = (g_dev_kb_p1 && s == 0);
|
||||
sio_set_pad_connected(s, (g_players[s].kind != 0 || kb_p1) ? 1 : 0);
|
||||
sio_set_pad_analog(s, pad_mode_boot_analog(g_players[s].mode), 0x80, 0x80, 0x80, 0x80);
|
||||
}
|
||||
/* SPU float-shadow gate must be set before spu_init() (which runs
|
||||
@@ -2435,11 +2482,11 @@ int main(int argc, char** argv) {
|
||||
std::memset(&cpu, 0, sizeof(cpu));
|
||||
|
||||
/* Wire memory function pointers. */
|
||||
cpu.read_word = psx_read_word;
|
||||
cpu.read_word = psx_guest_read_word; /* +6cyc main-RAM read wait states */
|
||||
cpu.write_word = psx_write_word;
|
||||
cpu.read_half = psx_read_half;
|
||||
cpu.read_half = psx_guest_read_half;
|
||||
cpu.write_half = psx_write_half;
|
||||
cpu.read_byte = psx_read_byte;
|
||||
cpu.read_byte = psx_guest_read_byte;
|
||||
cpu.write_byte = psx_write_byte;
|
||||
/* Wire the sljit JIT host-helper table (cpu-relative => position-independent
|
||||
* shards; prerequisite for the persisted sljit shard cache). Harmless when
|
||||
|
||||
@@ -744,6 +744,31 @@ uint8_t psx_read_byte(uint32_t addr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- CPU guest-side data reads: model PS1 main-RAM read latency ----
|
||||
* The R3000A has no usable D-cache (its data cache is repurposed as the 1 KB
|
||||
* scratchpad), so every CPU data read from main DRAM costs 1 opcode cycle plus
|
||||
* 6 wait-state cycles (DuckStation RAM_READ_TICKS = 6; "1 opcode + 6 waitstates
|
||||
* = 7 cycles"). The base opcode cycle is already charged by the per-block
|
||||
* psx_advance_cycles(instruction_count); here we add only the +6 wait states,
|
||||
* and only for main-RAM targets — scratchpad, MMIO, BIOS ROM and open-bus are
|
||||
* excluded (they have their own access timing). The penalty is keyed on the
|
||||
* runtime effective physical address (all of KUSEG/KSEG0/KSEG1 alias the same
|
||||
* DRAM), NOT on the code region, so relocated kernel code is timed correctly.
|
||||
*
|
||||
* These wrappers are wired to cpu->read_* (see main.cpp), so the penalty
|
||||
* applies to BOTH statically-recompiled and dirty-RAM-interpreted guest loads,
|
||||
* but NOT to debug-server / device reads that call psx_read_* directly. */
|
||||
#define PSX_RAM_READ_WAIT_CYCLES 6u
|
||||
extern void psx_advance_cycles(uint32_t cycles);
|
||||
|
||||
static inline void charge_main_ram_read(uint32_t addr) {
|
||||
if ((addr & 0x1FFFFFFFu) < RAM_SIZE) psx_advance_cycles(PSX_RAM_READ_WAIT_CYCLES);
|
||||
}
|
||||
|
||||
uint32_t psx_guest_read_word(uint32_t addr) { charge_main_ram_read(addr); return psx_read_word(addr); }
|
||||
uint16_t psx_guest_read_half(uint32_t addr) { charge_main_ram_read(addr); return psx_read_half(addr); }
|
||||
uint8_t psx_guest_read_byte(uint32_t addr) { charge_main_ram_read(addr); return psx_read_byte(addr); }
|
||||
|
||||
void psx_write_byte(uint32_t addr, uint8_t val) {
|
||||
if (sr_ptr && (*sr_ptr & 0x10000u)) return;
|
||||
|
||||
|
||||
+59
-58
@@ -64,6 +64,15 @@ static uint8_t pad_response[8];
|
||||
static uint8_t pad_response_len = 0;
|
||||
static uint8_t pad_response_idx = 0;
|
||||
static uint8_t pad_current_cmd = 0;
|
||||
/* DualShock config-mode latch, per slot. A real controller only answers the
|
||||
* config commands (0x44/0x45/0x46/0x47/0x4C/0x4D/0x4F) and reports the config
|
||||
* ID 0xF3 while it is IN config mode; outside config it reports its normal ID
|
||||
* (0x41 digital / 0x73 analog) and ignores config commands. Config is entered/
|
||||
* exited by command 0x43 with the data byte 0x01(enter)/0x00(exit). Faking
|
||||
* "always in config" (constant 0xF3) wedges games that probe the pad type via
|
||||
* 0x43 before polling — e.g. Mega Man X6 loops 01 43 00 00 forever and never
|
||||
* reaches 0x42. (MMX6 ISSUES.md #2.) */
|
||||
static uint8_t pad_in_config[2] = { 0, 0 };
|
||||
|
||||
/* Memory card SIO state machine */
|
||||
typedef enum {
|
||||
@@ -591,94 +600,86 @@ static void pad_process_byte(uint8_t tx_byte) {
|
||||
case PAD_WAIT_ACCESS:
|
||||
pad_current_cmd = tx_byte;
|
||||
pad_response_idx = 1;
|
||||
/* Controller ID reported as the first response byte. Real hardware
|
||||
* reports the config ID (0xF3) ONLY while in config mode; otherwise the
|
||||
* normal mode ID (0x41 digital / 0x73 analog). */
|
||||
{
|
||||
const uint8_t cur_id = pad_in_config[selected_slot] ? 0xF3
|
||||
: (pad_analog[selected_slot] ? 0x73 : 0x41);
|
||||
if (tx_byte == 0x42) {
|
||||
/* Read poll. Analog (or in-config) uses the 8-byte format with the
|
||||
* four stick axes; a plain digital pad uses the 4-byte format. */
|
||||
const uint16_t btn = pad_buttons[selected_slot];
|
||||
if (pad_analog[selected_slot]) {
|
||||
/* DualShock analog poll: id 0x73, then buttons, then the four
|
||||
* stick axes (right X/Y, left X/Y) per the SIO pad protocol. */
|
||||
pad_response[0] = 0x73;
|
||||
pad_response[1] = 0x5A;
|
||||
pad_response[2] = (uint8_t)(btn & 0xFF);
|
||||
pad_response[3] = (uint8_t)(btn >> 8);
|
||||
pad_response[0] = cur_id;
|
||||
pad_response[1] = 0x5A;
|
||||
pad_response[2] = (uint8_t)(btn & 0xFF);
|
||||
pad_response[3] = (uint8_t)(btn >> 8);
|
||||
if (pad_analog[selected_slot] || pad_in_config[selected_slot]) {
|
||||
pad_response[4] = pad_stick[selected_slot][2]; /* right X */
|
||||
pad_response[5] = pad_stick[selected_slot][3]; /* right Y */
|
||||
pad_response[6] = pad_stick[selected_slot][0]; /* left X */
|
||||
pad_response[7] = pad_stick[selected_slot][1]; /* left Y */
|
||||
pad_response_len = 8;
|
||||
} else {
|
||||
pad_response[0] = 0x41; /* Digital pad poll */
|
||||
pad_response[1] = 0x5A;
|
||||
pad_response[2] = (uint8_t)(btn & 0xFF);
|
||||
pad_response[3] = (uint8_t)(btn >> 8);
|
||||
pad_response_len = 4;
|
||||
}
|
||||
pad_state = PAD_SEND_RESPONSE;
|
||||
sio_rx_data = pad_response[0];
|
||||
sio_stat |= SIO_STAT_ACK;
|
||||
} else if (tx_byte == 0x43) {
|
||||
static const uint8_t resp_43[8] = {
|
||||
0xF3, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
memcpy(pad_response, resp_43, sizeof(resp_43));
|
||||
pad_response_len = (uint8_t)sizeof(resp_43);
|
||||
/* Enter/exit config mode. The ID byte reflects the CURRENT mode; the
|
||||
* enter(0x01)/exit(0x00) flag is the second data byte, latched in
|
||||
* PAD_SEND_RESPONSE so it takes effect after this transaction. */
|
||||
pad_response[0] = cur_id;
|
||||
pad_response[1] = 0x5A;
|
||||
pad_response[2] = 0x00; pad_response[3] = 0x00;
|
||||
pad_response[4] = 0x00; pad_response[5] = 0x00;
|
||||
pad_response[6] = 0x00; pad_response[7] = 0x00;
|
||||
pad_response_len = 8;
|
||||
pad_state = PAD_SEND_RESPONSE;
|
||||
sio_rx_data = pad_response[0];
|
||||
sio_stat |= SIO_STAT_ACK;
|
||||
} else if (tx_byte == 0x45) {
|
||||
static const uint8_t resp_45[8] = {
|
||||
0xF3, 0x5A, 0x03, 0x02, 0x01, 0x02, 0x01, 0x00
|
||||
};
|
||||
memcpy(pad_response, resp_45, sizeof(resp_45));
|
||||
pad_response_len = (uint8_t)sizeof(resp_45);
|
||||
pad_state = PAD_SEND_RESPONSE;
|
||||
sio_rx_data = pad_response[0];
|
||||
sio_stat |= SIO_STAT_ACK;
|
||||
} else if (tx_byte == 0x46) {
|
||||
static const uint8_t resp_46[8] = {
|
||||
0xF3, 0x5A, 0x00, 0x00, 0x01, 0x02, 0x00, 0x0A
|
||||
};
|
||||
memcpy(pad_response, resp_46, sizeof(resp_46));
|
||||
pad_response_len = (uint8_t)sizeof(resp_46);
|
||||
pad_state = PAD_SEND_RESPONSE;
|
||||
sio_rx_data = pad_response[0];
|
||||
sio_stat |= SIO_STAT_ACK;
|
||||
} else if (tx_byte == 0x47) {
|
||||
static const uint8_t resp_47[8] = {
|
||||
0xF3, 0x5A, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00
|
||||
};
|
||||
memcpy(pad_response, resp_47, sizeof(resp_47));
|
||||
pad_response_len = (uint8_t)sizeof(resp_47);
|
||||
pad_state = PAD_SEND_RESPONSE;
|
||||
sio_rx_data = pad_response[0];
|
||||
sio_stat |= SIO_STAT_ACK;
|
||||
} else if (tx_byte == 0x4C) {
|
||||
static const uint8_t resp_4c[8] = {
|
||||
0xF3, 0x5A, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00
|
||||
};
|
||||
memcpy(pad_response, resp_4c, sizeof(resp_4c));
|
||||
pad_response_len = (uint8_t)sizeof(resp_4c);
|
||||
pad_state = PAD_SEND_RESPONSE;
|
||||
sio_rx_data = pad_response[0];
|
||||
sio_stat |= SIO_STAT_ACK;
|
||||
} else if (tx_byte == 0x4D) {
|
||||
static const uint8_t resp_4d[8] = {
|
||||
0xF3, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
memcpy(pad_response, resp_4d, sizeof(resp_4d));
|
||||
pad_response_len = (uint8_t)sizeof(resp_4d);
|
||||
} else if (pad_in_config[selected_slot] &&
|
||||
(tx_byte == 0x44 || tx_byte == 0x45 || tx_byte == 0x46 ||
|
||||
tx_byte == 0x47 || tx_byte == 0x4C || tx_byte == 0x4D ||
|
||||
tx_byte == 0x4F)) {
|
||||
/* Config-only commands (analog/rumble init handshake). Canned
|
||||
* responses; only valid while in config mode (a digital pad ignores
|
||||
* them, handled by the else branch below). */
|
||||
static const uint8_t r_def[8] = { 0xF3,0x5A,0x00,0x00,0x00,0x00,0x00,0x00 };
|
||||
static const uint8_t r_45[8] = { 0xF3,0x5A,0x03,0x02,0x01,0x02,0x01,0x00 };
|
||||
static const uint8_t r_46[8] = { 0xF3,0x5A,0x00,0x00,0x01,0x02,0x00,0x0A };
|
||||
static const uint8_t r_47[8] = { 0xF3,0x5A,0x00,0x00,0x02,0x00,0x01,0x00 };
|
||||
static const uint8_t r_4c[8] = { 0xF3,0x5A,0x00,0x00,0x00,0x04,0x00,0x00 };
|
||||
const uint8_t *r = r_def;
|
||||
if (tx_byte == 0x45) r = r_45;
|
||||
else if (tx_byte == 0x46) r = r_46;
|
||||
else if (tx_byte == 0x47) r = r_47;
|
||||
else if (tx_byte == 0x4C) r = r_4c;
|
||||
memcpy(pad_response, r, 8);
|
||||
pad_response_len = 8;
|
||||
pad_state = PAD_SEND_RESPONSE;
|
||||
sio_rx_data = pad_response[0];
|
||||
sio_stat |= SIO_STAT_ACK;
|
||||
} else {
|
||||
/* Unknown command, or a config command issued OUTSIDE config mode:
|
||||
* a real pad does not respond — return hi-z and end the transaction. */
|
||||
pad_state = PAD_IDLE;
|
||||
pad_response_len = 0;
|
||||
pad_response_idx = 0;
|
||||
pad_current_cmd = 0;
|
||||
sio_rx_data = 0xFF;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PAD_SEND_RESPONSE:
|
||||
/* For 0x43 (enter/exit config), the data byte selecting enter(0x01)/
|
||||
* exit(0x00) arrives paired with response index 2. Latch the new config
|
||||
* state; it takes effect from the next transaction (the ID byte already
|
||||
* reported the mode that was current at the start of this one). */
|
||||
if (pad_current_cmd == 0x43 && pad_response_idx == 2)
|
||||
pad_in_config[selected_slot] = (tx_byte == 0x01) ? 1 : 0;
|
||||
if (pad_response_idx < pad_response_len) {
|
||||
sio_rx_data = pad_response[pad_response_idx++];
|
||||
if (pad_response_idx < pad_response_len) {
|
||||
|
||||
Reference in New Issue
Block a user