hekate/bdk/input/joycon.c
2023-10-12 06:59:15 +03:00

1271 lines
30 KiB
C

/*
* Joy-Con UART driver for Nintendo Switch
*
* Copyright (c) 2019-2023 CTCaer
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope 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, see <http://www.gnu.org/licenses/>.
*/
#include <string.h>
#include "joycon.h"
#include <gfx_utils.h>
#include <power/max17050.h>
#include <power/regulator_5v.h>
#include <soc/bpmp.h>
#include <soc/clock.h>
#include <soc/fuse.h>
#include <soc/gpio.h>
#include <soc/pinmux.h>
#include <soc/timer.h>
#include <soc/uart.h>
#include <soc/t210.h>
// For disabling driver when logging is enabled.
#include <libs/lv_conf.h>
#define JC_WIRED_CMD 0x91
#define JC_WIRED_HID 0x92
#define JC_WIRED_INIT_REPLY 0x94
#define JC_INIT_HANDSHAKE 0xA5
#define JC_HORI_INPUT_RPT_CMD 0x9A
#define JC_HORI_INPUT_RPT 0x00
#define JC_WIRED_CMD_GET_INFO 0x01
#define JC_WIRED_CMD_SET_CHARGER 0x02
#define JC_WIRED_CMD_GET_CHARGER 0x03
#define JC_WIRED_CMD_BATT_VOLT 0x06
#define JC_WIRED_CMD_WAKE_REASON 0x07
#define JC_WIRED_CMD_HID_CONN 0x10
#define JC_WIRED_CMD_HID_DISC 0x11
#define JC_WIRED_CMD_SET_HIDRATE 0x12 // Output report rate.
#define JC_WIRED_CMD_SET_BRATE 0x20
#define JC_HID_OUTPUT_RPT 0x01
#define JC_HID_RUMBLE_RPT 0x10
#define JC_HID_INPUT_RPT 0x30
#define JC_HID_SUBMCD_RPT 0x21
#define JC_HID_SUBCMD_HCI_STATE 0x06
#define HCI_STATE_SLEEP 0x00
#define HCI_STATE_RECONNECT 0x01
#define HCI_STATE_PAIR 0x02
#define HCI_STATE_HOME 0x04
#define JC_HID_SUBCMD_SPI_READ 0x10
#define SPI_READ_OFFSET 0x20
#define JC_HID_SUBCMD_RUMBLE_CTL 0x48
#define JC_HID_SUBCMD_SND_RUMBLE 0xFF
#define JC_SIO_OUTPUT_RPT 0x91
#define JC_SIO_INPUT_RPT 0x92
#define JC_SIO_CMD_ACK 0x80
#define JC_SIO_CMD_INIT 0x01
#define JC_SIO_CMD_UNK02 0x02
#define JC_SIO_CMD_VER_RPT 0x03
#define JC_SIO_CMD_UNK20 0x20 // JC_WIRED_CMD_SET_BRATE
#define JC_SIO_CMD_UNK21 0x21
#define JC_SIO_CMD_UNK22 0x22
#define JC_SIO_CMD_UNK40 0x40
#define JC_SIO_CMD_STATUS 0x41
#define JC_SIO_CMD_IAP_VER 0x42
#define JC_BTN_MASK_L 0xFF2900 // 0xFFE900: with charge status.
#define JC_BTN_MASK_R 0x0056FF
#define JC_ID_L 0x01 // Joycon (L). Mask for Hori (L).
#define JC_ID_R 0x02 // Joycon (R). Mask for Hori (R).
#define JC_ID_HORI 0x20 // Mask for Hori. Actual ids: 0x21, 0x22.
#define JC_CRC8_POLY 0x8D
enum
{
JC_STATE_START = 0,
JC_STATE_HANDSHAKED = 1,
JC_STATE_BRATE_CHANGED = 2,
JC_STATE_BRATE_OK = 3,
JC_STATE_INIT_DONE = 4
};
enum
{
JC_BATT_EMTPY = 0,
JC_BATT_CRIT = 2,
JC_BATT_LOW = 4,
JC_BATT_MID = 6,
JC_BATT_FULL = 8
};
static const u8 sio_init[] = {
JC_SIO_OUTPUT_RPT, JC_SIO_CMD_INIT,
0x00, 0x00, 0x00, 0x00, 0x00, 0x95
};
static const u8 sio_set_rpt_version[] = {
JC_SIO_OUTPUT_RPT, JC_SIO_CMD_VER_RPT,
// old fw: 0x00, 0x0D (0.13). New 3.4.
// force_update_en: 0x01
0x00, 0x00, 0x03, 0x04, 0x00, 0xDA
};
// Every 8ms.
static const u8 sio_pad_status[] = {
JC_SIO_OUTPUT_RPT, JC_SIO_CMD_STATUS,
0x00, 0x00, 0x00, 0x00, 0x00, 0xB0
};
static const u8 init_wake[] = {
0xA1, 0xA2, 0xA3, 0xA4
};
static const u8 init_handshake[] = {
0x19, 0x01, 0x03, 0x07, 0x00, // Uart header.
JC_INIT_HANDSHAKE, 0x02, // Wired cmd and wired subcmd.
0x01, 0x7E, 0x00, 0x00, 0x00 // Wired subcmd data and crc.
};
static const u8 init_get_info[] = {
0x19, 0x01, 0x03, 0x07, 0x00, // Uart header.
JC_WIRED_CMD, JC_WIRED_CMD_GET_INFO, // Wired cmd and subcmd.
0x00, 0x00, 0x00, 0x00, 0x24 // Wired subcmd data and crc.
};
static const u8 init_switch_brate[] = {
0x19, 0x01, 0x03, 0x0F, 0x00, // Uart header.
JC_WIRED_CMD, JC_WIRED_CMD_SET_BRATE, // Wired cmd and subcmd.
0x08, 0x00, 0x00, 0xBD, 0xB1, // Wired subcmd data, data crc and crc.
// Baudrate 3 megabaud.
0xC0, 0xC6, 0x2D, 0x00, 0x00, 0x00, 0x00, 0x00
};
static const u8 init_hid_disconnect[] = {
0x19, 0x01, 0x03, 0x07, 0x00, // Uart header.
JC_WIRED_CMD, JC_WIRED_CMD_HID_DISC, // Wired cmd and subcmd.
0x00, 0x00, 0x00, 0x00, 0x0E // Wired subcmd data and crc.
};
static const u8 init_set_hid_rate[] = {
0x19, 0x01, 0x03, 0x0B, 0x00, // Uart header.
JC_WIRED_CMD, JC_WIRED_CMD_SET_HIDRATE, // Wired cmd and subcmd.
0x04, 0x00, 0x00, 0x12, 0xA6, // Wired subcmd data, data crc and crc.
// Output report rate 15 ms.
0x0F, 0x00, 0x00, 0x00
// 5 ms.
// 0x04, 0x00, 0x00, 0x0E, 0xD5,
// 0x05, 0x00, 0x00, 0x00
};
static const u8 init_hid_connect[] = {
0x19, 0x01, 0x03, 0x07, 0x00, // Uart header.
JC_WIRED_CMD, JC_WIRED_CMD_HID_CONN, // Wired cmd and subcmd.
0x00, 0x00, 0x00, 0x00, 0x3D // Wired subcmd data and crc.
};
static const u8 nx_pad_status[] = {
0x19, 0x01, 0x03, 0x08, 0x00, // Uart header.
JC_WIRED_HID, 0x00, // Wired cmd and hid cmd.
0x01, 0x00, 0x00, 0x69, 0x2D, 0x1F // hid data, data crc and crc.
};
static const u8 hori_pad_status[] = {
0x19, 0x01, 0x03, 0x07, 0x00, // Uart header.
JC_HORI_INPUT_RPT_CMD, 0x01, // Hori cmd and hori subcmd.
0x00, 0x00, 0x00, 0x00, 0x48 // Hori cmd data and crc.
};
typedef struct _jc_uart_hdr_t
{
u8 magic[3];
u8 total_size_lsb;
u8 total_size_msb;
} jc_uart_hdr_t;
typedef struct _jc_wired_hdr_t
{
jc_uart_hdr_t uart_hdr;
u8 cmd;
u8 data[5];
u8 crc;
u8 payload[];
} jc_wired_hdr_t;
typedef struct _jc_hid_out_rpt_t
{
u8 cmd;
u8 pkt_id;
u8 rumble[8];
u8 subcmd;
u8 subcmd_data[];
} jc_hid_out_rpt_t;
typedef struct _jc_hid_out_spi_read_t
{
u32 addr;
u8 size;
} jc_hid_out_spi_read_t;
typedef struct _jc_hid_in_rpt_t
{
u8 cmd;
u8 pkt_id;
u8 conn_info:4;
u8 batt_info:4;
u8 btn_right;
u8 btn_shared;
u8 btn_left;
u8 stick_h_left;
u8 stick_m_left;
u8 stick_v_left;
u8 stick_h_right;
u8 stick_m_right;
u8 stick_v_right;
u8 vib_decider; // right:8, left:8. (bit3 en, bit2-0 buffer avail).
u8 submcd_ack;
u8 subcmd;
u8 subcmd_data[];
} jc_hid_in_rpt_t;
typedef struct _jc_hid_in_spi_read_t
{
u32 addr;
u8 size;
u8 data[];
} jc_hid_in_spi_read_t;
typedef struct _jc_hid_in_pair_data_t
{
u8 magic;
u8 size;
u16 checksum;
u8 mac[6];
u8 ltk[16];
u8 pad0[10];
u8 bt_caps; // bit3: Secure conn supported host, bit5: Paired to TBFC supported host, bit6: iTBFC page supported
u8 pad1;
} jc_hid_in_pair_data_t;
typedef struct _jc_sio_out_rpt_t
{
u8 cmd;
u8 subcmd;
u16 payload_size;
u8 data[2];
u8 crc_payload;
u8 crc_hdr;
u8 payload[];
} jc_sio_out_rpt_t;
typedef struct _jc_sio_in_rpt_t
{
u8 cmd;
u8 ack;
u16 payload_size;
u8 status;
u8 unk;
u8 crc_payload;
u8 crc_hdr;
u8 payload[];
} jc_sio_in_rpt_t;
typedef struct _jc_hid_in_sixaxis_rpt_t
{
s16 acc_x;
s16 acc_y;
s16 acc_z;
s16 gyr_x;
s16 gyr_y;
s16 gyr_z;
} __attribute__((packed)) jc_hid_in_sixaxis_rpt_t;
typedef struct _jc_sio_hid_in_rpt_t
{
u8 type;
u8 pkt_id;
u8 unk;
u8 btn_right;
u8 btn_shared;
u8 btn_left;
u8 stick_h_left;
u8 stick_m_left;
u8 stick_v_left;
u8 stick_h_right;
u8 stick_m_right;
u8 stick_v_right;
u8 siaxis_rpt; // bit0-3: report num. bit4-7: imu type.
// Each report is 800 us?
jc_hid_in_sixaxis_rpt_t sixaxis[15];
} jc_sio_hid_in_rpt_t;
typedef struct _joycon_ctxt_t
{
u8 buf[0x100]; //FIXME: If heap is used, dumping breaks.
u8 uart;
u8 type;
u8 state;
u8 mac[6];
u32 last_received_time;
u32 last_status_req_time;
u8 rumble_sent;
u8 connected;
u8 detected;
u8 sio_mode;
} joycon_ctxt_t;
static joycon_ctxt_t jc_l = {0};
static joycon_ctxt_t jc_r = {0};
static bool jc_init_done = false;
static u32 hid_pkt_inc = 0;
static jc_gamepad_rpt_t jc_gamepad;
static u8 _jc_crc(u8 *data, u16 len, u8 init)
{
u8 crc = init;
for (u16 i = 0; i < len; i++)
{
crc ^= data[i];
for (u16 j = 0; j < 8; j++)
{
if ((crc & 0x80) != 0)
crc = (u8)((crc << 1) ^ JC_CRC8_POLY);
else
crc <<= 1;
}
}
return crc;
}
static void _jc_power_supply(u8 uart, bool enable)
{
if (enable)
{
if (regulator_5v_get_dev_enabled(1 << uart))
return;
regulator_5v_enable(1 << uart);
if (jc_gamepad.sio_mode)
return;
if (jc_init_done)
{
if (uart == UART_C)
gpio_write(GPIO_PORT_CC, GPIO_PIN_3, GPIO_HIGH);
else
gpio_write(GPIO_PORT_K, GPIO_PIN_3, GPIO_HIGH);
return;
}
if (uart == UART_C)
{
// Joy-Con(L) Charge Enable.
PINMUX_AUX(PINMUX_AUX_SPDIF_IN) = PINMUX_PULL_DOWN | 1;
gpio_direction_output(GPIO_PORT_CC, GPIO_PIN_3, GPIO_HIGH);
}
else
{
// Joy-Con(R) Charge Enable.
PINMUX_AUX(PINMUX_AUX_GPIO_PK3) = PINMUX_DRIVE_4X | PINMUX_PULL_DOWN | 2;
gpio_direction_output(GPIO_PORT_K, GPIO_PIN_3, GPIO_HIGH);
}
}
else
{
if (!regulator_5v_get_dev_enabled(1 << uart))
return;
regulator_5v_disable(1 << uart);
if (jc_gamepad.sio_mode)
return;
if (uart == UART_C)
gpio_write(GPIO_PORT_CC, GPIO_PIN_3, GPIO_LOW);
else
gpio_write(GPIO_PORT_K, GPIO_PIN_3, GPIO_LOW);
}
}
static void _jc_detect()
{
if (!jc_gamepad.sio_mode)
{
// Turn on Joy-Con detect. (UARTB/C TX). UART CTS also if HW flow control and irq is enabled.
PINMUX_AUX(PINMUX_AUX_UART2_TX) = PINMUX_INPUT_ENABLE | PINMUX_TRISTATE;
PINMUX_AUX(PINMUX_AUX_UART3_TX) = PINMUX_INPUT_ENABLE | PINMUX_TRISTATE;
gpio_direction_input(GPIO_PORT_G, GPIO_PIN_0);
gpio_direction_input(GPIO_PORT_D, GPIO_PIN_1);
usleep(20);
// Read H6/E6 which are shared with UART TX pins.
jc_r.detected = !gpio_read(GPIO_PORT_H, GPIO_PIN_6);
jc_l.detected = !gpio_read(GPIO_PORT_E, GPIO_PIN_6);
// Turn off Joy-Con detect. (UARTB/C TX).
PINMUX_AUX(PINMUX_AUX_UART2_TX) = 0;
PINMUX_AUX(PINMUX_AUX_UART3_TX) = 0;
gpio_config(GPIO_PORT_G, GPIO_PIN_0, GPIO_MODE_SPIO);
gpio_config(GPIO_PORT_D, GPIO_PIN_1, GPIO_MODE_SPIO);
usleep(20);
}
else
{
//! TODO: Is there a way to detect a broken Sio?
jc_l.detected = true;
}
}
static void _jc_conn_check()
{
_jc_detect();
if (jc_gamepad.sio_mode)
return;
// Check if a Joy-Con was disconnected.
if (!jc_l.detected)
{
if (jc_l.connected)
_jc_power_supply(UART_C, false);
hid_pkt_inc = 0;
jc_l.connected = false;
jc_l.rumble_sent = false;
jc_gamepad.conn_l = false;
jc_gamepad.batt_info_l = 0;
jc_gamepad.bt_conn_l.type = 0;
jc_gamepad.buttons &= ~JC_BTN_MASK_L;
}
if (!jc_r.detected)
{
if (jc_r.connected)
_jc_power_supply(UART_B, false);
hid_pkt_inc = 0;
jc_r.connected = false;
jc_r.rumble_sent = false;
jc_gamepad.conn_r = false;
jc_gamepad.batt_info_r = 0;
jc_gamepad.bt_conn_r.type = 0;
jc_gamepad.buttons &= ~JC_BTN_MASK_R;
}
}
static void _joycon_send_raw(u8 uart_port, const u8 *buf, u16 size)
{
uart_send(uart_port, buf, size);
uart_wait_xfer(uart_port, UART_TX_IDLE);
}
static u16 _jc_packet_add_uart_hdr(jc_wired_hdr_t *out, u8 wired_cmd, u8 *data, u16 size, bool crc)
{
out->uart_hdr.magic[0] = 0x19;
out->uart_hdr.magic[1] = 0x01;
out->uart_hdr.magic[2] = 0x3;
out->uart_hdr.total_size_lsb = sizeof(jc_wired_hdr_t) - sizeof(jc_uart_hdr_t);
out->uart_hdr.total_size_msb = 0;
out->cmd = wired_cmd;
if (data)
memcpy(out->data, data, size);
out->crc = crc ? _jc_crc(&out->uart_hdr.total_size_msb,
sizeof(out->uart_hdr.total_size_msb) +
sizeof(out->cmd) + sizeof(out->data), 0) : 0;
return sizeof(jc_wired_hdr_t);
}
static u16 _jc_hid_output_rpt_craft(jc_wired_hdr_t *rpt, u8 *payload, u16 size, bool crc)
{
u16 pkt_size = _jc_packet_add_uart_hdr(rpt, JC_WIRED_HID, NULL, 0, crc);
pkt_size += size;
rpt->uart_hdr.total_size_lsb += size;
rpt->data[0] = size >> 8;
rpt->data[1] = size & 0xFF;
if (payload)
memcpy(rpt->payload, payload, size);
return pkt_size;
}
static void _jc_send_hid_output_rpt(u8 uart, u8 *payload, u16 size, bool crc)
{
u8 rpt[0x50];
memset(rpt, 0, sizeof(rpt));
u32 rpt_size = _jc_hid_output_rpt_craft((jc_wired_hdr_t *)rpt, payload, size, crc);
_joycon_send_raw(uart, rpt, rpt_size);
}
static u8 _jc_hid_pkt_id_incr()
{
return (hid_pkt_inc++ & 0xF);
}
static void _jc_send_hid_cmd(u8 uart, u8 subcmd, u8 *data, u16 size)
{
const u8 rumble_neutral[8] = { 0x00, 0x01, 0x40, 0x40, 0x00, 0x01, 0x40, 0x40 };
const u8 rumble_init[8] = { 0xc2, 0xc8, 0x03, 0x72, 0xc2, 0xc8, 0x03, 0x72 };
u8 temp[0x30] = {0};
jc_hid_out_rpt_t *hid_pkt = (jc_hid_out_rpt_t *)temp;
memcpy(hid_pkt->rumble, rumble_neutral, sizeof(rumble_neutral));
if (subcmd == JC_HID_SUBCMD_SND_RUMBLE)
{
bool send_r_rumble = jc_r.connected && !jc_r.rumble_sent;
bool send_l_rumble = jc_l.connected && !jc_l.rumble_sent;
// Enable rumble.
hid_pkt->cmd = JC_HID_OUTPUT_RPT;
hid_pkt->pkt_id = _jc_hid_pkt_id_incr();
hid_pkt->subcmd = JC_HID_SUBCMD_RUMBLE_CTL;
hid_pkt->subcmd_data[0] = 1;
if (send_r_rumble)
_jc_send_hid_output_rpt(UART_B, (u8 *)hid_pkt, 0x10, false);
if (send_l_rumble)
_jc_send_hid_output_rpt(UART_C, (u8 *)hid_pkt, 0x10, false);
// Send rumble.
hid_pkt->cmd = JC_HID_RUMBLE_RPT;
hid_pkt->pkt_id = _jc_hid_pkt_id_incr();
memcpy(hid_pkt->rumble, rumble_init, sizeof(rumble_init));
if (send_r_rumble)
_jc_send_hid_output_rpt(UART_B, (u8 *)hid_pkt, 10, false);
if (send_l_rumble)
_jc_send_hid_output_rpt(UART_C, (u8 *)hid_pkt, 10, false);
msleep(15);
// Disable rumble.
hid_pkt->cmd = JC_HID_OUTPUT_RPT;
hid_pkt->pkt_id = _jc_hid_pkt_id_incr();
hid_pkt->subcmd = JC_HID_SUBCMD_RUMBLE_CTL;
hid_pkt->subcmd_data[0] = 0;
memcpy(hid_pkt->rumble, rumble_neutral, sizeof(rumble_neutral));
if (send_r_rumble)
_jc_send_hid_output_rpt(UART_B, (u8 *)hid_pkt, 0x10, false);
if (send_l_rumble)
_jc_send_hid_output_rpt(UART_C, (u8 *)hid_pkt, 0x10, false);
}
else
{
bool crc_needed = (jc_l.uart == uart) ? (jc_l.type & JC_ID_HORI) : (jc_r.type & JC_ID_HORI);
hid_pkt->cmd = JC_HID_OUTPUT_RPT;
hid_pkt->pkt_id = _jc_hid_pkt_id_incr();
hid_pkt->subcmd = subcmd;
if (data)
memcpy(hid_pkt->subcmd_data, data, size);
_jc_send_hid_output_rpt(uart, (u8 *)hid_pkt, sizeof(jc_hid_out_rpt_t) + size, crc_needed);
}
}
static void _jc_charging_decider(u8 batt, u8 uart)
{
u32 system_batt_enough = max17050_get_cached_batt_volt() > 4000;
// Power supply control based on battery levels and charging.
if ((batt >> 1 << 1) < JC_BATT_LOW) // Level without checking charging.
_jc_power_supply(uart, true);
else if (batt > (system_batt_enough ? JC_BATT_FULL : JC_BATT_MID)) // Addresses the charging bit.
_jc_power_supply(uart, false);
}
static void _jc_parse_wired_hid(joycon_ctxt_t *jc, const u8* packet, u32 size)
{
u32 btn_tmp;
jc_hid_in_rpt_t *hid_pkt = (jc_hid_in_rpt_t *)packet;
switch (hid_pkt->cmd)
{
case JC_HORI_INPUT_RPT:
case JC_HID_INPUT_RPT:
btn_tmp = hid_pkt->btn_right | hid_pkt->btn_shared << 8 | hid_pkt->btn_left << 16;
if (jc->type & JC_ID_L)
{
jc_gamepad.buttons &= ~JC_BTN_MASK_L;
jc_gamepad.buttons |= (btn_tmp & JC_BTN_MASK_L);
jc_gamepad.lstick_x = hid_pkt->stick_h_left | ((hid_pkt->stick_m_left & 0xF) << 8);
jc_gamepad.lstick_y = (hid_pkt->stick_m_left >> 4) | (hid_pkt->stick_v_left << 4);
jc_gamepad.batt_info_l = hid_pkt->batt_info;
}
else if (jc->type & JC_ID_R)
{
jc_gamepad.buttons &= ~JC_BTN_MASK_R;
jc_gamepad.buttons |= (btn_tmp & JC_BTN_MASK_R);
jc_gamepad.rstick_x = hid_pkt->stick_h_right | ((hid_pkt->stick_m_right & 0xF) << 8);
jc_gamepad.rstick_y = (hid_pkt->stick_m_right >> 4) | (hid_pkt->stick_v_right << 4);
jc_gamepad.batt_info_r = hid_pkt->batt_info;
}
jc_gamepad.conn_l = jc_l.connected;
jc_gamepad.conn_r = jc_r.connected;
if (hid_pkt->cmd == JC_HID_INPUT_RPT)
_jc_charging_decider(hid_pkt->batt_info, jc->uart);
break;
case JC_HID_SUBMCD_RPT:
if (hid_pkt->subcmd == JC_HID_SUBCMD_SPI_READ)
{
jc_bt_conn_t *bt_conn;
if (jc->type & JC_ID_L)
bt_conn = &jc_gamepad.bt_conn_l;
else
bt_conn = &jc_gamepad.bt_conn_r;
jc_hid_in_spi_read_t *spi_info = (jc_hid_in_spi_read_t *)hid_pkt->subcmd_data;
jc_hid_in_pair_data_t *pair_data = (jc_hid_in_pair_data_t *)spi_info->data;
// Check if the reply is pairing info.
if (spi_info->size == 0x1A && pair_data->magic == 0x95 && pair_data->size == 0x22)
{
bt_conn->type = jc->type;
memcpy(bt_conn->mac, jc->mac, 6);
memcpy(bt_conn->host_mac, pair_data->mac, 6);
for (u32 i = 16; i > 0; i--)
bt_conn->ltk[16 - i] = pair_data->ltk[i - 1];
}
}
break;
default:
break;
}
}
static void _jc_parse_wired_init(joycon_ctxt_t *jc, const u8* data, u32 size)
{
switch (data[0])
{
case JC_WIRED_CMD_GET_INFO:
for (int i = 12; i > 6; i--)
jc->mac[12 - i] = data[i];
jc->type = data[6];
jc->connected = true;
break;
case JC_WIRED_CMD_SET_BRATE:
jc->state = JC_STATE_BRATE_CHANGED;
break;
case JC_WIRED_CMD_HID_DISC:
jc->state = JC_STATE_BRATE_OK;
break;
case JC_WIRED_CMD_HID_CONN:
case JC_WIRED_CMD_SET_HIDRATE:
// done.
default:
break;
}
}
static void _jc_uart_pkt_parse(joycon_ctxt_t *jc, const jc_wired_hdr_t *pkt, size_t size)
{
switch (pkt->cmd)
{
case JC_HORI_INPUT_RPT_CMD:
case JC_WIRED_HID:
_jc_parse_wired_hid(jc, pkt->payload, (pkt->data[0] << 8) | pkt->data[1]);
break;
case JC_WIRED_INIT_REPLY:
_jc_parse_wired_init(jc, pkt->data, size - sizeof(jc_uart_hdr_t) - 1);
break;
case JC_INIT_HANDSHAKE:
jc->state = JC_STATE_HANDSHAKED;
break;
default:
break;
}
jc->last_received_time = get_tmr_ms();
}
static void _jc_sio_parse_payload(joycon_ctxt_t *jc, u8 cmd, const u8* payload, u32 size)
{
switch (cmd)
{
case JC_SIO_CMD_STATUS:
jc_sio_hid_in_rpt_t *hid_pkt = (jc_sio_hid_in_rpt_t *)payload;
jc_gamepad.buttons = hid_pkt->btn_right | hid_pkt->btn_shared << 8 | hid_pkt->btn_left << 16;
jc_gamepad.home = !gpio_read(GPIO_PORT_V, GPIO_PIN_3);
jc_gamepad.lstick_x = hid_pkt->stick_h_left | ((hid_pkt->stick_m_left & 0xF) << 8);
jc_gamepad.lstick_y = (hid_pkt->stick_m_left >> 4) | (hid_pkt->stick_v_left << 4);
jc_gamepad.rstick_x = hid_pkt->stick_h_right | ((hid_pkt->stick_m_right & 0xF) << 8);
jc_gamepad.rstick_y = (hid_pkt->stick_m_right >> 4) | (hid_pkt->stick_v_right << 4);
jc_gamepad.batt_info_l = jc_l.connected;
jc_gamepad.batt_info_r = gpio_read(GPIO_PORT_E, GPIO_PIN_7); // Set IRQ status.
jc_gamepad.conn_l = jc_l.connected;
jc_gamepad.conn_r = jc_l.connected;
break;
default:
break;
}
}
static void _jc_sio_uart_pkt_parse(joycon_ctxt_t *jc, const jc_sio_in_rpt_t *pkt, u32 size)
{
if (pkt->crc_hdr != _jc_crc((u8 *)pkt, sizeof(jc_sio_in_rpt_t) - 1, 0))
return;
u8 cmd = pkt->ack & (~JC_SIO_CMD_ACK);
switch (cmd)
{
case JC_SIO_CMD_INIT:
jc->connected = pkt->status == 0;
break;
case JC_SIO_CMD_VER_RPT:
if (jc->connected)
jc->connected = pkt->status == 0;
break;
case JC_SIO_CMD_IAP_VER:
case JC_SIO_CMD_STATUS:
_jc_sio_parse_payload(jc, cmd, pkt->payload, pkt->payload_size);
break;
case JC_SIO_CMD_UNK02:
case JC_SIO_CMD_UNK20:
case JC_SIO_CMD_UNK21:
case JC_SIO_CMD_UNK22:
case JC_SIO_CMD_UNK40:
default:
break;
}
jc->last_received_time = get_tmr_ms();
}
static void _jc_rcv_pkt(joycon_ctxt_t *jc)
{
if (!jc->detected)
return;
u32 len = uart_recv(jc->uart, (u8 *)jc->buf, 0x100);
if (len < 8)
return;
// For Joycon, check uart reply magic.
jc_wired_hdr_t *jc_pkt = (jc_wired_hdr_t *)jc->buf;
if (!jc->sio_mode && !memcmp(jc_pkt->uart_hdr.magic, "\x19\x81\x03", 3))
{
_jc_uart_pkt_parse(jc, jc_pkt, jc_pkt->uart_hdr.total_size_lsb + sizeof(jc_uart_hdr_t));
return;
}
// For Sio, check uart output report and command ack.
jc_sio_in_rpt_t *sio_pkt = (jc_sio_in_rpt_t *)(jc->buf);
if (jc->sio_mode && sio_pkt->cmd == JC_SIO_INPUT_RPT && (sio_pkt->ack & JC_SIO_CMD_ACK) == JC_SIO_CMD_ACK)
{
_jc_sio_uart_pkt_parse(jc, sio_pkt, sio_pkt->payload_size + sizeof(jc_sio_in_rpt_t));
return;
}
}
static bool _jc_send_init_rumble(joycon_ctxt_t *jc)
{
// Send init rumble or request nx pad status report.
if ((jc_r.connected && !jc_r.rumble_sent) || (jc_l.connected && !jc_l.rumble_sent))
{
_jc_send_hid_cmd(jc->uart, JC_HID_SUBCMD_SND_RUMBLE, NULL, 0);
if (jc_l.connected)
jc_l.rumble_sent = true;
if (jc_r.connected)
jc_r.rumble_sent = true;
return 1;
}
return 0;
}
static void _jc_req_nx_pad_status(joycon_ctxt_t *jc)
{
if (!jc->detected)
return;
bool is_nxpad = !(jc->type & JC_ID_HORI) && !jc->sio_mode;
if (jc->last_status_req_time > get_tmr_ms() || !jc->connected)
return;
if (is_nxpad)
{
bool sent_rumble = _jc_send_init_rumble(jc);
if (sent_rumble)
return;
}
if (is_nxpad)
_joycon_send_raw(jc->uart, nx_pad_status, sizeof(nx_pad_status));
else if (jc->sio_mode)
_joycon_send_raw(jc->uart, sio_pad_status, sizeof(sio_pad_status));
else
_joycon_send_raw(jc->uart, hori_pad_status, sizeof(hori_pad_status));
jc->last_status_req_time = get_tmr_ms() + 15;
}
static bool _jc_validate_pairing_info(u8 *buf, bool *is_hos)
{
u8 crc = 0;
for (u32 i = 0; i < 0x22; i++)
crc += buf[4 + i];
crc += 0x68; // Host is Switch.
if ((crc ^ 0x55) == buf[2])
*is_hos = true;
crc -= 0x68;
crc += 0x08; // Host is PC.
if (*is_hos || (crc ^ 0x55) == buf[2])
return true;
return false;
}
jc_gamepad_rpt_t *jc_get_bt_pairing_info(bool *is_l_hos, bool *is_r_hos)
{
u8 retries;
jc_bt_conn_t *bt_conn;
if (!jc_init_done || jc_gamepad.sio_mode)
return NULL;
bt_conn = &jc_gamepad.bt_conn_l;
memset(bt_conn->host_mac, 0, 6);
memset(bt_conn->ltk, 0, 16);
bt_conn = &jc_gamepad.bt_conn_r;
memset(bt_conn->host_mac, 0, 6);
memset(bt_conn->ltk, 0, 16);
_jc_conn_check();
while (jc_l.last_status_req_time > get_tmr_ms())
{
_jc_rcv_pkt(&jc_r);
_jc_rcv_pkt(&jc_l);
}
jc_hid_in_spi_read_t subcmd_data_l;
subcmd_data_l.addr = 0x2000;
subcmd_data_l.size = 0x1A;
jc_hid_in_spi_read_t subcmd_data_r;
subcmd_data_r.addr = 0x2000;
subcmd_data_r.size = 0x1A;
bool jc_r_found = jc_r.connected ? false : true;
bool jc_l_found = jc_l.connected ? false : true;
// Set mode to HW controlled RTS.
uart_set_mode(jc_l.uart, UART_AO_TX_HW_RX);
uart_set_mode(jc_r.uart, UART_AO_TX_HW_RX);
u32 total_retries = 10;
retry:
retries = 10;
while (retries)
{
u32 time_now = get_tmr_ms();
if ((!jc_l_found && jc_l.last_status_req_time < time_now) || (!jc_r_found && jc_r.last_status_req_time < time_now))
{
if (!jc_l_found)
{
_jc_send_hid_cmd(jc_l.uart, JC_HID_SUBCMD_SPI_READ, (u8 *)&subcmd_data_l, 5);
jc_l.last_status_req_time = get_tmr_ms() + 15;
}
if (!jc_r_found)
{
_jc_send_hid_cmd(jc_r.uart, JC_HID_SUBCMD_SPI_READ, (u8 *)&subcmd_data_r, 5);
jc_r.last_status_req_time = get_tmr_ms() + 15;
}
retries--;
}
// Wait for the first 36 bytes to arrive.
msleep(5);
if (!jc_l_found)
{
memset(jc_l.buf, 0, 0x100);
_jc_rcv_pkt(&jc_l);
bool is_hos = false;
if (_jc_validate_pairing_info(&jc_l.buf[SPI_READ_OFFSET], &is_hos))
{
bool is_active = jc_l.buf[SPI_READ_OFFSET] == 0x95;
if (!is_active)
subcmd_data_l.addr += 0x26; // Get next slot.
else
jc_l_found = true; // Entry is active.
if (jc_l_found && is_hos)
*is_l_hos = true;
}
}
if (!jc_r_found)
{
memset(jc_r.buf, 0, 0x100);
_jc_rcv_pkt(&jc_r);
bool is_hos = false;
if (_jc_validate_pairing_info(&jc_r.buf[SPI_READ_OFFSET], &is_hos))
{
bool is_active = jc_r.buf[SPI_READ_OFFSET] == 0x95;
if (!is_active)
subcmd_data_r.addr += 0x26; // Get next slot.
else
jc_r_found = true; // Entry is active.
if (jc_r_found && is_hos)
*is_r_hos = true;
}
}
if (jc_l_found && jc_r_found)
break;
}
if (!jc_l_found || !jc_r_found)
{
if (total_retries)
{
total_retries--;
goto retry;
}
if (!jc_l_found)
{
bt_conn = &jc_gamepad.bt_conn_l;
memset(bt_conn->host_mac, 0, 6);
memset(bt_conn->ltk, 0, 16);
}
if (!jc_r_found)
{
bt_conn = &jc_gamepad.bt_conn_r;
memset(bt_conn->host_mac, 0, 6);
memset(bt_conn->ltk, 0, 16);
}
}
// Restore mode to manual RTS.
uart_set_mode(jc_l.uart, UART_AO_TX_MN_RX);
uart_set_mode(jc_r.uart, UART_AO_TX_MN_RX);
return &jc_gamepad;
}
static void _jc_init_conn(joycon_ctxt_t *jc)
{
if (!jc->detected)
return;
if (((u32)get_tmr_ms() - jc->last_received_time) > 1000)
{
_jc_power_supply(jc->uart, true);
// Mask out buttons and set connected to false.
if (jc->uart == UART_B)
{
jc_gamepad.buttons &= ~JC_BTN_MASK_R;
jc_gamepad.conn_r = false;
}
else
{
jc_gamepad.buttons &= ~JC_BTN_MASK_L;
jc_gamepad.conn_l = false;
}
// Initialize uart to 1 megabaud and manual RTS.
uart_init(jc->uart, 1000000, UART_AO_TX_MN_RX);
if (!jc->sio_mode)
{
jc->state = JC_STATE_START;
// Set TX and RTS inversion for Joycon.
uart_invert(jc->uart, true, UART_INVERT_TXD | UART_INVERT_RTS);
// Wake up the controller.
_joycon_send_raw(jc->uart, init_wake, sizeof(init_wake));
_jc_rcv_pkt(jc); // Clear RX FIFO.
// Do a handshake.
u32 retries = 10;
while (retries && jc->state != JC_STATE_HANDSHAKED)
{
_joycon_send_raw(jc->uart, init_handshake, sizeof(init_handshake));
msleep(5);
_jc_rcv_pkt(jc);
retries--;
}
if (jc->state != JC_STATE_HANDSHAKED)
goto out;
// Get info about the controller.
_joycon_send_raw(jc->uart, init_get_info, sizeof(init_get_info));
msleep(2);
_jc_rcv_pkt(jc);
if (!(jc->type & JC_ID_HORI))
{
// Request 3 megabaud change.
_joycon_send_raw(jc->uart, init_switch_brate, sizeof(init_switch_brate));
msleep(2);
_jc_rcv_pkt(jc);
if (jc->state == JC_STATE_BRATE_CHANGED)
{
// Reinitialize uart to 3 megabaud and manual RTS.
uart_init(jc->uart, 3000000, UART_AO_TX_MN_RX);
uart_invert(jc->uart, true, UART_INVERT_TXD | UART_INVERT_RTS);
// Disconnect HID.
retries = 10;
while (retries && jc->state != JC_STATE_BRATE_OK)
{
_joycon_send_raw(jc->uart, init_hid_disconnect, sizeof(init_hid_disconnect));
msleep(5);
_jc_rcv_pkt(jc);
retries--;
}
if (jc->state != JC_STATE_BRATE_OK)
goto out;
}
// Create HID connection with the new rate.
_joycon_send_raw(jc->uart, init_hid_connect, sizeof(init_hid_connect));
msleep(2);
_jc_rcv_pkt(jc);
// Set hid packet rate.
_joycon_send_raw(jc->uart, init_set_hid_rate, sizeof(init_set_hid_rate));
msleep(2);
_jc_rcv_pkt(jc);
}
else // Hori. Unset RTS inversion.
uart_invert(jc->uart, false, UART_INVERT_RTS);
}
else
{
// Set Sio NPOR low to configure BOOT0 mode.
gpio_write(GPIO_PORT_CC, GPIO_PIN_5, GPIO_LOW);
usleep(300);
gpio_write(GPIO_PORT_T, GPIO_PIN_0, GPIO_LOW);
gpio_write(GPIO_PORT_CC, GPIO_PIN_5, GPIO_HIGH);
msleep(100);
// Clear RX FIFO.
_jc_rcv_pkt(jc);
// Initialize the controller.
u32 retries = 10;
while (!jc->connected)
{
_joycon_send_raw(jc->uart, sio_init, sizeof(sio_init));
msleep(5);
_jc_rcv_pkt(jc);
retries--;
}
if (!jc->connected)
goto out;
// Set output report version.
_joycon_send_raw(jc->uart, sio_set_rpt_version, sizeof(sio_set_rpt_version));
msleep(5);
_jc_rcv_pkt(jc);
}
// Initialization done.
jc->state = JC_STATE_INIT_DONE;
out:
jc->last_received_time = get_tmr_ms();
if (!jc->sio_mode && jc->connected && !(jc->type & JC_ID_HORI))
_jc_power_supply(jc->uart, false);
}
}
void jc_init_hw()
{
jc_l.uart = UART_C;
jc_r.uart = UART_B;
jc_l.sio_mode = fuse_read_hw_type() == FUSE_NX_HW_TYPE_HOAG;
jc_gamepad.sio_mode = jc_l.sio_mode;
#if !defined(DEBUG_UART_PORT) || !(DEBUG_UART_PORT)
_jc_power_supply(UART_C, true);
_jc_power_supply(UART_B, true);
// Sio Initialization.
if (jc_gamepad.sio_mode)
{
// Enable 4 MHz clock to Sio.
clock_enable_extperiph2();
PINMUX_AUX(PINMUX_AUX_TOUCH_CLK) = PINMUX_PULL_DOWN;
// Configure Sio HOME BUTTON.
PINMUX_AUX(PINMUX_AUX_LCD_GPIO1) = PINMUX_INPUT_ENABLE | PINMUX_TRISTATE | PINMUX_PULL_UP | 1;
gpio_direction_input(GPIO_PORT_V, GPIO_PIN_3);
// Configure Sio IRQ
PINMUX_AUX(PINMUX_AUX_GPIO_PE7) = PINMUX_INPUT_ENABLE | PINMUX_TRISTATE | PINMUX_PULL_UP;
gpio_direction_input(GPIO_PORT_E, GPIO_PIN_7);
// Configure Sio NRST and BOOT0.
PINMUX_AUX(PINMUX_AUX_CAM1_STROBE) = PINMUX_PULL_DOWN | 1;
PINMUX_AUX(PINMUX_AUX_CAM2_PWDN) = PINMUX_PULL_DOWN | 1;
// Set BOOT0 to flash mode. (output high is sram mode).
gpio_direction_output(GPIO_PORT_T, GPIO_PIN_0, GPIO_LOW);
// NRST to pull down.
gpio_direction_input(GPIO_PORT_T, GPIO_PIN_1);
// Configure Sio NPOR.
PINMUX_AUX(PINMUX_AUX_USB_VBUS_EN1) = PINMUX_IO_HV | PINMUX_LPDR | 1;
gpio_direction_output(GPIO_PORT_CC, GPIO_PIN_5, GPIO_LOW);
}
#if 0 // Already set by hw init.
// Set Joy-Con IsAttached pinmux. Shared with UARTB/UARTC TX.
PINMUX_AUX(PINMUX_AUX_GPIO_PE6) = PINMUX_INPUT_ENABLE | PINMUX_TRISTATE;
PINMUX_AUX(PINMUX_AUX_GPIO_PH6) = PINMUX_INPUT_ENABLE | PINMUX_TRISTATE;
// Set Joy-Con IsAttached mode. Shared with UARTB/UARTC TX.
gpio_config(GPIO_PORT_E, GPIO_PIN_6, GPIO_MODE_GPIO);
gpio_config(GPIO_PORT_H, GPIO_PIN_6, GPIO_MODE_GPIO);
#endif
// Configure pinmuxing for UART B and C.
if (!jc_gamepad.sio_mode)
pinmux_config_uart(UART_B);
pinmux_config_uart(UART_C);
// Ease the stress to APB.
bpmp_freq_t prev_fid = bpmp_clk_rate_set(BPMP_CLK_NORMAL);
// Enable UART B and C clocks.
if (!jc_gamepad.sio_mode)
clock_enable_uart(UART_B);
clock_enable_uart(UART_C);
// Restore OC.
bpmp_clk_rate_set(prev_fid);
jc_init_done = true;
#endif
}
void jc_deinit()
{
if (!jc_init_done)
return;
// Disable power.
_jc_power_supply(UART_B, false);
_jc_power_supply(UART_C, false);
if (!jc_gamepad.sio_mode)
{
// Send sleep command.
u8 data = HCI_STATE_SLEEP;
if (jc_r.connected && !(jc_r.type & JC_ID_HORI))
{
_jc_send_hid_cmd(UART_B, JC_HID_SUBCMD_HCI_STATE, &data, 1);
_jc_rcv_pkt(&jc_r);
}
if (jc_l.connected && !(jc_l.type & JC_ID_HORI))
{
_jc_send_hid_cmd(UART_C, JC_HID_SUBCMD_HCI_STATE, &data, 1);
_jc_rcv_pkt(&jc_l);
}
}
else
{
// Disable Sio NPOR.
gpio_write(GPIO_PORT_CC, GPIO_PIN_5, GPIO_LOW);
// Disable 4 MHz clock to Sio.
clock_disable_extperiph2();
}
// Disable UART B and C clocks.
if (!jc_gamepad.sio_mode)
clock_disable_uart(UART_B);
clock_disable_uart(UART_C);
}
jc_gamepad_rpt_t *joycon_poll()
{
if (!jc_init_done)
return NULL;
_jc_conn_check();
_jc_init_conn(&jc_r);
_jc_init_conn(&jc_l);
_jc_req_nx_pad_status(&jc_r);
_jc_req_nx_pad_status(&jc_l);
_jc_rcv_pkt(&jc_r);
_jc_rcv_pkt(&jc_l);
return &jc_gamepad;
}