diff --git a/src/common/settings.h b/src/common/settings.h
index 86e0fa1402..3b7be63b3f 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -590,6 +590,9 @@ struct Values {
     BasicSetting<int> touch_from_button_map_index{0, "touch_from_button_map"};
     std::vector<TouchFromButtonMap> touch_from_button_maps;
 
+    BasicSetting<bool> enable_ring_controller{true, "enable_ring_controller"};
+    RingconRaw ringcon_analogs;
+
     // Data Storage
     BasicSetting<bool> use_virtual_sd{true, "use_virtual_sd"};
     BasicSetting<bool> gamecard_inserted{false, "gamecard_inserted"};
diff --git a/src/common/settings_input.h b/src/common/settings_input.h
index 4ff37e1863..6f42346bc6 100644
--- a/src/common/settings_input.h
+++ b/src/common/settings_input.h
@@ -357,6 +357,7 @@ constexpr int NUM_KEYBOARD_MODS_HID = NumKeyboardMods;
 using AnalogsRaw = std::array<std::string, NativeAnalog::NumAnalogs>;
 using ButtonsRaw = std::array<std::string, NativeButton::NumButtons>;
 using MotionsRaw = std::array<std::string, NativeMotion::NumMotions>;
+using RingconRaw = std::string;
 
 constexpr u32 JOYCON_BODY_NEON_RED = 0xFF3C28;
 constexpr u32 JOYCON_BUTTONS_NEON_RED = 0x1E0A0A;
diff --git a/src/core/hid/emulated_devices.cpp b/src/core/hid/emulated_devices.cpp
index cc0dcd931e..2f84d2b528 100644
--- a/src/core/hid/emulated_devices.cpp
+++ b/src/core/hid/emulated_devices.cpp
@@ -15,6 +15,7 @@ EmulatedDevices::EmulatedDevices() = default;
 EmulatedDevices::~EmulatedDevices() = default;
 
 void EmulatedDevices::ReloadFromSettings() {
+    ring_params = Common::ParamPackage(Settings::values.ringcon_analogs);
     ReloadInput();
 }
 
@@ -66,6 +67,8 @@ void EmulatedDevices::ReloadInput() {
         key_index++;
     }
 
+    ring_analog_device = Common::Input::CreateDevice<Common::Input::InputDevice>(ring_params);
+
     for (std::size_t index = 0; index < mouse_button_devices.size(); ++index) {
         if (!mouse_button_devices[index]) {
             continue;
@@ -120,6 +123,13 @@ void EmulatedDevices::ReloadInput() {
                 },
         });
     }
+
+    if (ring_analog_device) {
+        ring_analog_device->SetCallback({
+            .on_change =
+                [this](const Common::Input::CallbackStatus& callback) { SetRingAnalog(callback); },
+        });
+    }
 }
 
 void EmulatedDevices::UnloadInput() {
@@ -155,6 +165,7 @@ void EmulatedDevices::SaveCurrentConfig() {
     if (!is_configuring) {
         return;
     }
+    Settings::values.ringcon_analogs = ring_params.Serialize();
 }
 
 void EmulatedDevices::RestoreConfig() {
@@ -164,6 +175,15 @@ void EmulatedDevices::RestoreConfig() {
     ReloadFromSettings();
 }
 
+Common::ParamPackage EmulatedDevices::GetRingParam() const {
+    return ring_params;
+}
+
+void EmulatedDevices::SetRingParam(Common::ParamPackage param) {
+    ring_params = std::move(param);
+    ReloadInput();
+}
+
 void EmulatedDevices::SetKeyboardButton(const Common::Input::CallbackStatus& callback,
                                         std::size_t index) {
     if (index >= device_status.keyboard_values.size()) {
@@ -410,6 +430,23 @@ void EmulatedDevices::SetMouseStick(const Common::Input::CallbackStatus& callbac
     TriggerOnChange(DeviceTriggerType::Mouse);
 }
 
+void EmulatedDevices::SetRingAnalog(const Common::Input::CallbackStatus& callback) {
+    std::lock_guard lock{mutex};
+    const auto force_value = TransformToStick(callback);
+
+    device_status.ring_analog_value = force_value.x;
+
+    if (is_configuring) {
+        device_status.ring_analog_value = {};
+        TriggerOnChange(DeviceTriggerType::RingController);
+        return;
+    }
+
+    device_status.ring_analog_state.force = force_value.x.value;
+
+    TriggerOnChange(DeviceTriggerType::RingController);
+}
+
 KeyboardValues EmulatedDevices::GetKeyboardValues() const {
     std::scoped_lock lock{mutex};
     return device_status.keyboard_values;
@@ -425,6 +462,10 @@ MouseButtonValues EmulatedDevices::GetMouseButtonsValues() const {
     return device_status.mouse_button_values;
 }
 
+RingAnalogValue EmulatedDevices::GetRingSensorValues() const {
+    return device_status.ring_analog_value;
+}
+
 KeyboardKey EmulatedDevices::GetKeyboard() const {
     std::scoped_lock lock{mutex};
     return device_status.keyboard_state;
@@ -450,6 +491,10 @@ AnalogStickState EmulatedDevices::GetMouseWheel() const {
     return device_status.mouse_wheel_state;
 }
 
+RingSensorForce EmulatedDevices::GetRingSensorForce() const {
+    return device_status.ring_analog_state;
+}
+
 void EmulatedDevices::TriggerOnChange(DeviceTriggerType type) {
     std::scoped_lock lock{callback_mutex};
     for (const auto& poller_pair : callback_list) {
diff --git a/src/core/hid/emulated_devices.h b/src/core/hid/emulated_devices.h
index 73e9f02933..fb6451e7a7 100644
--- a/src/core/hid/emulated_devices.h
+++ b/src/core/hid/emulated_devices.h
@@ -26,9 +26,11 @@ using MouseButtonDevices = std::array<std::unique_ptr<Common::Input::InputDevice
 using MouseAnalogDevices = std::array<std::unique_ptr<Common::Input::InputDevice>,
                                       Settings::NativeMouseWheel::NumMouseWheels>;
 using MouseStickDevice = std::unique_ptr<Common::Input::InputDevice>;
+using RingAnalogDevice = std::unique_ptr<Common::Input::InputDevice>;
 
 using MouseButtonParams =
     std::array<Common::ParamPackage, Settings::NativeMouseButton::NumMouseButtons>;
+using RingAnalogParams = Common::ParamPackage;
 
 using KeyboardValues =
     std::array<Common::Input::ButtonStatus, Settings::NativeKeyboard::NumKeyboardKeys>;
@@ -39,12 +41,17 @@ using MouseButtonValues =
 using MouseAnalogValues =
     std::array<Common::Input::AnalogStatus, Settings::NativeMouseWheel::NumMouseWheels>;
 using MouseStickValue = Common::Input::TouchStatus;
+using RingAnalogValue = Common::Input::AnalogStatus;
 
 struct MousePosition {
     f32 x;
     f32 y;
 };
 
+struct RingSensorForce {
+    f32 force;
+};
+
 struct DeviceStatus {
     // Data from input_common
     KeyboardValues keyboard_values{};
@@ -52,6 +59,7 @@ struct DeviceStatus {
     MouseButtonValues mouse_button_values{};
     MouseAnalogValues mouse_analog_values{};
     MouseStickValue mouse_stick_value{};
+    RingAnalogValue ring_analog_value{};
 
     // Data for HID serices
     KeyboardKey keyboard_state{};
@@ -59,12 +67,14 @@ struct DeviceStatus {
     MouseButton mouse_button_state{};
     MousePosition mouse_position_state{};
     AnalogStickState mouse_wheel_state{};
+    RingSensorForce ring_analog_state{};
 };
 
 enum class DeviceTriggerType {
     Keyboard,
     KeyboardModdifier,
     Mouse,
+    RingController,
 };
 
 struct InterfaceUpdateCallback {
@@ -110,6 +120,15 @@ public:
     /// Reverts any mapped changes made that weren't saved
     void RestoreConfig();
 
+    // Returns the current mapped ring device
+    Common::ParamPackage GetRingParam() const;
+
+    /**
+     * Updates the current mapped ring device
+     * @param param ParamPackage with ring sensor data to be mapped
+     */
+    void SetRingParam(Common::ParamPackage param);
+
     /// Returns the latest status of button input from the keyboard with parameters
     KeyboardValues GetKeyboardValues() const;
 
@@ -119,6 +138,9 @@ public:
     /// Returns the latest status of button input from the mouse with parameters
     MouseButtonValues GetMouseButtonsValues() const;
 
+    /// Returns the latest status of analog input from the ring sensor with parameters
+    RingAnalogValue GetRingSensorValues() const;
+
     /// Returns the latest status of button input from the keyboard
     KeyboardKey GetKeyboard() const;
 
@@ -134,6 +156,9 @@ public:
     /// Returns the latest mouse wheel change
     AnalogStickState GetMouseWheel() const;
 
+    /// Returns the latest ringcon force sensor value
+    RingSensorForce GetRingSensorForce() const;
+
     /**
      * Adds a callback to the list of events
      * @param update_callback InterfaceUpdateCallback that will be triggered
@@ -185,6 +210,12 @@ private:
      */
     void SetMouseStick(const Common::Input::CallbackStatus& callback);
 
+    /**
+     * Updates the ring analog sensor status of the ring controller
+     * @param callback A CallbackStatus containing the force status
+     */
+    void SetRingAnalog(const Common::Input::CallbackStatus& callback);
+
     /**
      * Triggers a callback that something has changed on the device status
      * @param type Input type of the event to trigger
@@ -193,11 +224,14 @@ private:
 
     bool is_configuring{false};
 
+    RingAnalogParams ring_params;
+
     KeyboardDevices keyboard_devices;
     KeyboardModifierDevices keyboard_modifier_devices;
     MouseButtonDevices mouse_button_devices;
     MouseAnalogDevices mouse_analog_devices;
     MouseStickDevice mouse_stick_device;
+    RingAnalogDevice ring_analog_device;
 
     mutable std::mutex mutex;
     mutable std::mutex callback_mutex;
diff --git a/src/core/hle/service/hid/hidbus.cpp b/src/core/hle/service/hid/hidbus.cpp
index db2864277d..af7662a158 100644
--- a/src/core/hle/service/hid/hidbus.cpp
+++ b/src/core/hle/service/hid/hidbus.cpp
@@ -3,6 +3,7 @@
 // Refer to the license.txt file included.
 
 #include "common/logging/log.h"
+#include "common/settings.h"
 #include "core/core.h"
 #include "core/core_timing.h"
 #include "core/core_timing_util.h"
@@ -190,6 +191,7 @@ void HidBus::IsExternalDeviceConnected(Kernel::HLERequestContext& ctx) {
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(ResultSuccess);
         rb.Push(is_attached);
+        return;
     }
 
     LOG_ERROR(Service_HID, "Invalid handle");
@@ -217,7 +219,7 @@ void HidBus::Initialize(Kernel::HLERequestContext& ctx) {
         const auto entry_index = devices[device_index.value()].handle.internal_index;
         auto& cur_entry = hidbus_status.entries[entry_index];
 
-        if (bus_handle_.internal_index == 0) {
+        if (bus_handle_.internal_index == 0 && Settings::values.enable_ring_controller) {
             MakeDevice<RingController>(bus_handle_);
             devices[device_index.value()].is_device_initializated = true;
             devices[device_index.value()].device->ActivateDevice();
diff --git a/src/core/hle/service/hid/hidbus/hidbus_base.cpp b/src/core/hle/service/hid/hidbus/hidbus_base.cpp
index 9cac0be80d..09bff10e5a 100644
--- a/src/core/hle/service/hid/hidbus/hidbus_base.cpp
+++ b/src/core/hle/service/hid/hidbus/hidbus_base.cpp
@@ -12,7 +12,7 @@ namespace Service::HID {
 
 HidbusBase::HidbusBase(KernelHelpers::ServiceContext& service_context_)
     : service_context(service_context_) {
-    send_command_asyc_event = service_context.CreateEvent("hidbus:SendCommandAsycEvent");
+    send_command_async_event = service_context.CreateEvent("hidbus:SendCommandAsyncEvent");
 }
 HidbusBase::~HidbusBase() = default;
 
@@ -66,7 +66,7 @@ void HidbusBase::SetTransferMemoryPointer(u8* t_mem) {
 }
 
 Kernel::KReadableEvent& HidbusBase::GetSendCommandAsycEvent() const {
-    return send_command_asyc_event->GetReadableEvent();
+    return send_command_async_event->GetReadableEvent();
 }
 
 } // namespace Service::HID
diff --git a/src/core/hle/service/hid/hidbus/hidbus_base.h b/src/core/hle/service/hid/hidbus/hidbus_base.h
index 41e5719985..13d073a3d8 100644
--- a/src/core/hle/service/hid/hidbus/hidbus_base.h
+++ b/src/core/hle/service/hid/hidbus/hidbus_base.h
@@ -165,6 +165,7 @@ protected:
     bool device_enabled{};
     bool polling_mode_enabled{};
     JoyPollingMode polling_mode = {};
+    // TODO(German77): All data accessors need to be replaced with a ring lifo object
     JoyDisableSixAxisDataAccessor disable_sixaxis_data{};
     JoyEnableSixAxisDataAccessor enable_sixaxis_data{};
     ButtonOnlyPollingDataAccessor button_only_data{};
@@ -172,7 +173,7 @@ protected:
     u8* transfer_memory{nullptr};
     bool is_transfer_memory_set{};
 
-    Kernel::KEvent* send_command_asyc_event;
+    Kernel::KEvent* send_command_async_event;
     KernelHelpers::ServiceContext& service_context;
 };
 } // namespace Service::HID
diff --git a/src/core/hle/service/hid/hidbus/ringcon.cpp b/src/core/hle/service/hid/hidbus/ringcon.cpp
index 6fe68081f7..5ec3cc83c4 100644
--- a/src/core/hle/service/hid/hidbus/ringcon.cpp
+++ b/src/core/hle/service/hid/hidbus/ringcon.cpp
@@ -2,7 +2,7 @@
 // Licensed under GPLv2 or any later version
 // Refer to the license.txt file included.
 
-#include "core/hid/emulated_controller.h"
+#include "core/hid/emulated_devices.h"
 #include "core/hid/hid_core.h"
 #include "core/hle/kernel/k_event.h"
 #include "core/hle/kernel/k_readable_event.h"
@@ -13,9 +13,7 @@ namespace Service::HID {
 RingController::RingController(Core::HID::HIDCore& hid_core_,
                                KernelHelpers::ServiceContext& service_context_)
     : HidbusBase(service_context_) {
-    // Use the horizontal axis of left stick for emulating input
-    // There is no point on adding a frontend implementation since Ring Fit Adventure doesn't work
-    input = hid_core_.GetEmulatedController(Core::HID::NpadIdType::Player1);
+    input = hid_core_.GetEmulatedDevices();
 }
 
 RingController::~RingController() = default;
@@ -41,6 +39,8 @@ void RingController::OnUpdate() {
         return;
     }
 
+    // TODO: Increment multitasking counters from motion and sensor data
+
     switch (polling_mode) {
     case JoyPollingMode::SixAxisSensorEnable: {
         enable_sixaxis_data.header.total_entries = 10;
@@ -74,9 +74,8 @@ RingController::RingConData RingController::GetSensorValue() const {
         .data = 0,
     };
 
-    const f32 stick_value = static_cast<f32>(input->GetSticks().left.x) / 32767.0f;
-
-    ringcon_sensor_value.data = static_cast<s16>(stick_value * range) + idle_value;
+    const f32 force_value = input->GetRingSensorForce().force * range;
+    ringcon_sensor_value.data = static_cast<s16>(force_value) + idle_value;
 
     return ringcon_sensor_value;
 }
@@ -105,6 +104,8 @@ std::vector<u8> RingController::GetReply() const {
         return GetReadRepCountReply();
     case RingConCommands::ReadTotalPushCount:
         return GetReadTotalPushCountReply();
+    case RingConCommands::ResetRepCount:
+        return GetResetRepCountReply();
     case RingConCommands::SaveCalData:
         return GetSaveDataReply();
     default:
@@ -119,36 +120,9 @@ bool RingController::SetCommand(const std::vector<u8>& data) {
         return false;
     }
 
-    // There must be a better way to do this
-    const u32 command_id =
-        u32{data[0]} + (u32{data[1]} << 8) + (u32{data[2]} << 16) + (u32{data[3]} << 24);
-    static constexpr std::array supported_commands = {
-        RingConCommands::GetFirmwareVersion,
-        RingConCommands::ReadId,
-        RingConCommands::c20105,
-        RingConCommands::ReadUnkCal,
-        RingConCommands::ReadFactoryCal,
-        RingConCommands::ReadUserCal,
-        RingConCommands::ReadRepCount,
-        RingConCommands::ReadTotalPushCount,
-        RingConCommands::SaveCalData,
-    };
+    std::memcpy(&command, data.data(), sizeof(RingConCommands));
 
-    for (RingConCommands cmd : supported_commands) {
-        if (command_id == static_cast<u32>(cmd)) {
-            return ExcecuteCommand(cmd, data);
-        }
-    }
-
-    LOG_ERROR(Service_HID, "Command not implemented {}", command_id);
-    command = RingConCommands::Error;
-    // Signal a reply to avoid softlocking
-    send_command_asyc_event->GetWritableEvent().Signal();
-    return false;
-}
-
-bool RingController::ExcecuteCommand(RingConCommands cmd, const std::vector<u8>& data) {
-    switch (cmd) {
+    switch (command) {
     case RingConCommands::GetFirmwareVersion:
     case RingConCommands::ReadId:
     case RingConCommands::c20105:
@@ -158,23 +132,27 @@ bool RingController::ExcecuteCommand(RingConCommands cmd, const std::vector<u8>&
     case RingConCommands::ReadRepCount:
     case RingConCommands::ReadTotalPushCount:
         ASSERT_MSG(data.size() == 0x4, "data.size is not 0x4 bytes");
-        command = cmd;
-        send_command_asyc_event->GetWritableEvent().Signal();
+        send_command_async_event->GetWritableEvent().Signal();
+        return true;
+    case RingConCommands::ResetRepCount:
+        ASSERT_MSG(data.size() == 0x4, "data.size is not 0x4 bytes");
+        total_rep_count = 0;
+        send_command_async_event->GetWritableEvent().Signal();
         return true;
     case RingConCommands::SaveCalData: {
         ASSERT_MSG(data.size() == 0x14, "data.size is not 0x14 bytes");
 
         SaveCalData save_info{};
-        std::memcpy(&save_info, &data, sizeof(SaveCalData));
+        std::memcpy(&save_info, data.data(), sizeof(SaveCalData));
         user_calibration = save_info.calibration;
-
-        command = cmd;
-        send_command_asyc_event->GetWritableEvent().Signal();
+        send_command_async_event->GetWritableEvent().Signal();
         return true;
     }
     default:
-        LOG_ERROR(Service_HID, "Command not implemented {}", cmd);
+        LOG_ERROR(Service_HID, "Command not implemented {}", command);
         command = RingConCommands::Error;
+        // Signal a reply to avoid softlocking the game
+        send_command_async_event->GetWritableEvent().Signal();
         return false;
     }
 }
@@ -240,27 +218,29 @@ std::vector<u8> RingController::GetReadUserCalReply() const {
 }
 
 std::vector<u8> RingController::GetReadRepCountReply() const {
-    // The values are hardcoded from a real joycon
     const GetThreeByteReply reply{
         .status = DataValid::Valid,
-        .data = {30, 0, 0},
-        .crc = GetCrcValue({30, 0, 0, 0}),
+        .data = {total_rep_count, 0, 0},
+        .crc = GetCrcValue({total_rep_count, 0, 0, 0}),
     };
 
     return GetDataVector(reply);
 }
 
 std::vector<u8> RingController::GetReadTotalPushCountReply() const {
-    // The values are hardcoded from a real joycon
     const GetThreeByteReply reply{
         .status = DataValid::Valid,
-        .data = {30, 0, 0},
-        .crc = GetCrcValue({30, 0, 0, 0}),
+        .data = {total_push_count, 0, 0},
+        .crc = GetCrcValue({total_push_count, 0, 0, 0}),
     };
 
     return GetDataVector(reply);
 }
 
+std::vector<u8> RingController::GetResetRepCountReply() const {
+    return GetReadRepCountReply();
+}
+
 std::vector<u8> RingController::GetSaveDataReply() const {
     const StatusReply reply{
         .status = DataValid::Valid,
diff --git a/src/core/hle/service/hid/hidbus/ringcon.h b/src/core/hle/service/hid/hidbus/ringcon.h
index e8b3d82546..2dbc6150e7 100644
--- a/src/core/hle/service/hid/hidbus/ringcon.h
+++ b/src/core/hle/service/hid/hidbus/ringcon.h
@@ -10,7 +10,7 @@
 #include "core/hle/service/hid/hidbus/hidbus_base.h"
 
 namespace Core::HID {
-class EmulatedController;
+class EmulatedDevices;
 } // namespace Core::HID
 
 namespace Service::HID {
@@ -43,6 +43,7 @@ private:
     static constexpr s16 idle_deadzone = 120;
     static constexpr s16 range = 2500;
 
+    // Most missing command names are leftovers from other firmware versions
     enum class RingConCommands : u32 {
         GetFirmwareVersion = 0x00020000,
         ReadId = 0x00020100,
@@ -60,10 +61,10 @@ private:
         ReadUserCal = 0x00021A04,
         ReadRepCount = 0x00023104,
         ReadTotalPushCount = 0x00023204,
-        Unknown9 = 0x04013104,
-        Unknown10 = 0x04011104,
-        Unknown11 = 0x04011204,
-        Unknown12 = 0x04011304,
+        ResetRepCount = 0x04013104,
+        Unknown8 = 0x04011104,
+        Unknown9 = 0x04011204,
+        Unknown10 = 0x04011304,
         SaveCalData = 0x10011A04,
         Error = 0xFFFFFFFF,
     };
@@ -180,9 +181,6 @@ private:
     };
     static_assert(sizeof(RingConData) == 0x8, "RingConData is an invalid size");
 
-    // Executes the command requested
-    bool ExcecuteCommand(RingConCommands cmd, const std::vector<u8>& data);
-
     // Returns RingConData struct with pressure sensor values
     RingConData GetSensorValue() const;
 
@@ -204,12 +202,15 @@ private:
     // Returns 20 byte reply with user calibration values
     std::vector<u8> GetReadUserCalReply() const;
 
-    // (STUBBED) Returns 8 byte reply
+    // Returns 8 byte reply
     std::vector<u8> GetReadRepCountReply() const;
 
-    // (STUBBED) Returns 8 byte reply
+    // Returns 8 byte reply
     std::vector<u8> GetReadTotalPushCountReply() const;
 
+    // Returns 8 byte reply
+    std::vector<u8> GetResetRepCountReply() const;
+
     // Returns 4 byte save data reply
     std::vector<u8> GetSaveDataReply() const;
 
@@ -225,6 +226,12 @@ private:
 
     RingConCommands command{RingConCommands::Error};
 
+    // These counters are used in multitasking mode while the switch is sleeping
+    // Total steps taken
+    u8 total_rep_count = 0;
+    // Total times the ring was pushed
+    u8 total_push_count = 0;
+
     const u8 device_id = 0x20;
     const FirmwareVersion version = {
         .sub = 0x0,
@@ -242,6 +249,6 @@ private:
         .zero = {.value = idle_value, .crc = 225},
     };
 
-    Core::HID::EmulatedController* input;
+    Core::HID::EmulatedDevices* input;
 };
 } // namespace Service::HID
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index b1467d0166..2ee21f7518 100644
--- a/src/yuzu/CMakeLists.txt
+++ b/src/yuzu/CMakeLists.txt
@@ -99,6 +99,9 @@ add_executable(yuzu
     configuration/configure_profile_manager.cpp
     configuration/configure_profile_manager.h
     configuration/configure_profile_manager.ui
+    configuration/configure_ringcon.cpp
+    configuration/configure_ringcon.h
+    configuration/configure_ringcon.ui
     configuration/configure_network.cpp
     configuration/configure_network.h
     configuration/configure_network.ui
diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp
index d2e735f480..ac26b885b4 100644
--- a/src/yuzu/configuration/config.cpp
+++ b/src/yuzu/configuration/config.cpp
@@ -60,6 +60,11 @@ const std::array<int, 2> Config::default_stick_mod = {
     0,
 };
 
+const std::array<int, 2> Config::default_ringcon_analogs{{
+    Qt::Key_A,
+    Qt::Key_D,
+}};
+
 // This shouldn't have anything except static initializers (no functions). So
 // QKeySequence(...).toString() is NOT ALLOWED HERE.
 // This must be in alphabetical order according to action name as it must have the same order as
@@ -346,6 +351,23 @@ void Config::ReadTouchscreenValues() {
         ReadSetting(QStringLiteral("touchscreen_diameter_y"), 15).toUInt();
 }
 
+void Config::ReadHidbusValues() {
+    Settings::values.enable_ring_controller =
+        ReadSetting(QStringLiteral("enable_ring_controller"), true).toBool();
+
+    const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
+        0, 0, default_ringcon_analogs[0], default_ringcon_analogs[1], 0, 0.05f);
+    auto& ringcon_analogs = Settings::values.ringcon_analogs;
+
+    ringcon_analogs =
+        qt_config->value(QStringLiteral("ring_controller"), QString::fromStdString(default_param))
+            .toString()
+            .toStdString();
+    if (ringcon_analogs.empty()) {
+        ringcon_analogs = default_param;
+    }
+}
+
 void Config::ReadAudioValues() {
     qt_config->beginGroup(QStringLiteral("Audio"));
 
@@ -369,6 +391,7 @@ void Config::ReadControlValues() {
     ReadMouseValues();
     ReadTouchscreenValues();
     ReadMotionTouchValues();
+    ReadHidbusValues();
 
 #ifdef _WIN32
     ReadBasicSetting(Settings::values.enable_raw_input);
@@ -962,6 +985,16 @@ void Config::SaveMotionTouchValues() {
     qt_config->endArray();
 }
 
+void Config::SaveHidbusValues() {
+    WriteBasicSetting(Settings::values.enable_ring_controller);
+
+    const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
+        0, 0, default_ringcon_analogs[0], default_ringcon_analogs[1], 0, 0.05f);
+    WriteSetting(QStringLiteral("ring_controller"),
+                 QString::fromStdString(Settings::values.ringcon_analogs),
+                 QString::fromStdString(default_param));
+}
+
 void Config::SaveValues() {
     if (global) {
         SaveControlValues();
@@ -1002,6 +1035,7 @@ void Config::SaveControlValues() {
     SaveMouseValues();
     SaveTouchscreenValues();
     SaveMotionTouchValues();
+    SaveHidbusValues();
 
     WriteGlobalSetting(Settings::values.use_docked_mode);
     WriteGlobalSetting(Settings::values.vibration_enabled);
diff --git a/src/yuzu/configuration/config.h b/src/yuzu/configuration/config.h
index ae3e36a116..f0ab6bdaab 100644
--- a/src/yuzu/configuration/config.h
+++ b/src/yuzu/configuration/config.h
@@ -42,6 +42,7 @@ public:
     static const std::array<int, Settings::NativeMotion::NumMotions> default_motions;
     static const std::array<std::array<int, 4>, Settings::NativeAnalog::NumAnalogs> default_analogs;
     static const std::array<int, 2> default_stick_mod;
+    static const std::array<int, 2> default_ringcon_analogs;
     static const std::array<int, Settings::NativeMouseButton::NumMouseButtons>
         default_mouse_buttons;
     static const std::array<int, Settings::NativeKeyboard::NumKeyboardKeys> default_keyboard_keys;
@@ -66,6 +67,7 @@ private:
     void ReadMouseValues();
     void ReadTouchscreenValues();
     void ReadMotionTouchValues();
+    void ReadHidbusValues();
 
     // Read functions bases off the respective config section names.
     void ReadAudioValues();
@@ -93,6 +95,7 @@ private:
     void SaveMouseValues();
     void SaveTouchscreenValues();
     void SaveMotionTouchValues();
+    void SaveHidbusValues();
 
     // Save functions based off the respective config section names.
     void SaveAudioValues();
diff --git a/src/yuzu/configuration/configure_input.cpp b/src/yuzu/configuration/configure_input.cpp
index 4ca74a5f7c..73d7ba24ba 100644
--- a/src/yuzu/configuration/configure_input.cpp
+++ b/src/yuzu/configuration/configure_input.cpp
@@ -20,6 +20,7 @@
 #include "yuzu/configuration/configure_input_advanced.h"
 #include "yuzu/configuration/configure_input_player.h"
 #include "yuzu/configuration/configure_motion_touch.h"
+#include "yuzu/configuration/configure_ringcon.h"
 #include "yuzu/configuration/configure_touchscreen_advanced.h"
 #include "yuzu/configuration/configure_vibration.h"
 #include "yuzu/configuration/input_profiles.h"
@@ -158,6 +159,10 @@ void ConfigureInput::Initialize(InputCommon::InputSubsystem* input_subsystem,
             [this, input_subsystem] {
                 CallConfigureDialog<ConfigureMotionTouch>(*this, input_subsystem);
             });
+    connect(advanced, &ConfigureInputAdvanced::CallRingControllerDialog,
+            [this, input_subsystem, &hid_core] {
+                CallConfigureDialog<ConfigureRingController>(*this, input_subsystem, hid_core);
+            });
 
     connect(ui->vibrationButton, &QPushButton::clicked,
             [this, &hid_core] { CallConfigureDialog<ConfigureVibration>(*this, hid_core); });
diff --git a/src/yuzu/configuration/configure_input_advanced.cpp b/src/yuzu/configuration/configure_input_advanced.cpp
index 20fc2599d5..8fd1f4a389 100644
--- a/src/yuzu/configuration/configure_input_advanced.cpp
+++ b/src/yuzu/configuration/configure_input_advanced.cpp
@@ -79,13 +79,17 @@ ConfigureInputAdvanced::ConfigureInputAdvanced(QWidget* parent)
             &ConfigureInputAdvanced::UpdateUIEnabled);
     connect(ui->touchscreen_enabled, &QCheckBox::stateChanged, this,
             &ConfigureInputAdvanced::UpdateUIEnabled);
+    connect(ui->enable_ring_controller, &QCheckBox::stateChanged, this,
+            &ConfigureInputAdvanced::UpdateUIEnabled);
 
     connect(ui->debug_configure, &QPushButton::clicked, this,
             [this] { CallDebugControllerDialog(); });
     connect(ui->touchscreen_advanced, &QPushButton::clicked, this,
             [this] { CallTouchscreenConfigDialog(); });
     connect(ui->buttonMotionTouch, &QPushButton::clicked, this,
-            &ConfigureInputAdvanced::CallMotionTouchConfigDialog);
+            [this] { CallMotionTouchConfigDialog(); });
+    connect(ui->ring_controller_configure, &QPushButton::clicked, this,
+            [this] { CallRingControllerDialog(); });
 
 #ifndef _WIN32
     ui->enable_raw_input->setVisible(false);
@@ -132,6 +136,7 @@ void ConfigureInputAdvanced::ApplyConfiguration() {
     Settings::values.enable_raw_input = ui->enable_raw_input->isChecked();
     Settings::values.enable_udp_controller = ui->enable_udp_controller->isChecked();
     Settings::values.controller_navigation = ui->controller_navigation->isChecked();
+    Settings::values.enable_ring_controller = ui->enable_ring_controller->isChecked();
 }
 
 void ConfigureInputAdvanced::LoadConfiguration() {
@@ -164,6 +169,7 @@ void ConfigureInputAdvanced::LoadConfiguration() {
     ui->enable_raw_input->setChecked(Settings::values.enable_raw_input.GetValue());
     ui->enable_udp_controller->setChecked(Settings::values.enable_udp_controller.GetValue());
     ui->controller_navigation->setChecked(Settings::values.controller_navigation.GetValue());
+    ui->enable_ring_controller->setChecked(Settings::values.enable_ring_controller.GetValue());
 
     UpdateUIEnabled();
 }
@@ -185,4 +191,5 @@ void ConfigureInputAdvanced::UpdateUIEnabled() {
     ui->touchscreen_advanced->setEnabled(ui->touchscreen_enabled->isChecked());
     ui->mouse_panning->setEnabled(!ui->mouse_enabled->isChecked());
     ui->mouse_panning_sensitivity->setEnabled(!ui->mouse_enabled->isChecked());
+    ui->ring_controller_configure->setEnabled(ui->enable_ring_controller->isChecked());
 }
diff --git a/src/yuzu/configuration/configure_input_advanced.h b/src/yuzu/configuration/configure_input_advanced.h
index 3083d55c14..4472cb8469 100644
--- a/src/yuzu/configuration/configure_input_advanced.h
+++ b/src/yuzu/configuration/configure_input_advanced.h
@@ -29,6 +29,7 @@ signals:
     void CallMouseConfigDialog();
     void CallTouchscreenConfigDialog();
     void CallMotionTouchConfigDialog();
+    void CallRingControllerDialog();
 
 private:
     void changeEvent(QEvent* event) override;
diff --git a/src/yuzu/configuration/configure_input_advanced.ui b/src/yuzu/configuration/configure_input_advanced.ui
index 66f2075f2a..14403cb102 100644
--- a/src/yuzu/configuration/configure_input_advanced.ui
+++ b/src/yuzu/configuration/configure_input_advanced.ui
@@ -2603,6 +2603,20 @@
                  </property>
                 </widget>
                </item>
+               <item row="4" column="0">
+                <widget class="QCheckBox" name="enable_ring_controller">
+                 <property name="text">
+                  <string>Ring Controller</string>
+                 </property>
+                </widget>
+               </item>
+               <item row="4" column="2">
+                <widget class="QPushButton" name="ring_controller_configure">
+                 <property name="text">
+                  <string>Configure</string>
+                 </property>
+                </widget>
+               </item>
               </layout>
              </widget>
             </item>
diff --git a/src/yuzu/configuration/configure_ringcon.cpp b/src/yuzu/configuration/configure_ringcon.cpp
new file mode 100644
index 0000000000..144e2b83f5
--- /dev/null
+++ b/src/yuzu/configuration/configure_ringcon.cpp
@@ -0,0 +1,424 @@
+// Copyright 2022 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <memory>
+#include <QKeyEvent>
+#include <QMenu>
+#include <QTimer>
+
+#include "core/hid/emulated_devices.h"
+#include "core/hid/hid_core.h"
+#include "input_common/drivers/keyboard.h"
+#include "input_common/drivers/mouse.h"
+#include "input_common/main.h"
+#include "ui_configure_ringcon.h"
+#include "yuzu/bootmanager.h"
+#include "yuzu/configuration/config.h"
+#include "yuzu/configuration/configure_ringcon.h"
+
+const std::array<std::string, ConfigureRingController::ANALOG_SUB_BUTTONS_NUM>
+    ConfigureRingController::analog_sub_buttons{{
+        "left",
+        "right",
+    }};
+
+namespace {
+
+QString GetKeyName(int key_code) {
+    switch (key_code) {
+    case Qt::Key_Shift:
+        return QObject::tr("Shift");
+    case Qt::Key_Control:
+        return QObject::tr("Ctrl");
+    case Qt::Key_Alt:
+        return QObject::tr("Alt");
+    case Qt::Key_Meta:
+        return {};
+    default:
+        return QKeySequence(key_code).toString();
+    }
+}
+
+QString GetButtonName(Common::Input::ButtonNames button_name) {
+    switch (button_name) {
+    case Common::Input::ButtonNames::ButtonLeft:
+        return QObject::tr("Left");
+    case Common::Input::ButtonNames::ButtonRight:
+        return QObject::tr("Right");
+    case Common::Input::ButtonNames::ButtonDown:
+        return QObject::tr("Down");
+    case Common::Input::ButtonNames::ButtonUp:
+        return QObject::tr("Up");
+    case Common::Input::ButtonNames::TriggerZ:
+        return QObject::tr("Z");
+    case Common::Input::ButtonNames::TriggerR:
+        return QObject::tr("R");
+    case Common::Input::ButtonNames::TriggerL:
+        return QObject::tr("L");
+    case Common::Input::ButtonNames::ButtonA:
+        return QObject::tr("A");
+    case Common::Input::ButtonNames::ButtonB:
+        return QObject::tr("B");
+    case Common::Input::ButtonNames::ButtonX:
+        return QObject::tr("X");
+    case Common::Input::ButtonNames::ButtonY:
+        return QObject::tr("Y");
+    case Common::Input::ButtonNames::ButtonStart:
+        return QObject::tr("Start");
+    case Common::Input::ButtonNames::L1:
+        return QObject::tr("L1");
+    case Common::Input::ButtonNames::L2:
+        return QObject::tr("L2");
+    case Common::Input::ButtonNames::L3:
+        return QObject::tr("L3");
+    case Common::Input::ButtonNames::R1:
+        return QObject::tr("R1");
+    case Common::Input::ButtonNames::R2:
+        return QObject::tr("R2");
+    case Common::Input::ButtonNames::R3:
+        return QObject::tr("R3");
+    case Common::Input::ButtonNames::Circle:
+        return QObject::tr("Circle");
+    case Common::Input::ButtonNames::Cross:
+        return QObject::tr("Cross");
+    case Common::Input::ButtonNames::Square:
+        return QObject::tr("Square");
+    case Common::Input::ButtonNames::Triangle:
+        return QObject::tr("Triangle");
+    case Common::Input::ButtonNames::Share:
+        return QObject::tr("Share");
+    case Common::Input::ButtonNames::Options:
+        return QObject::tr("Options");
+    default:
+        return QObject::tr("[undefined]");
+    }
+}
+
+void SetAnalogParam(const Common::ParamPackage& input_param, Common::ParamPackage& analog_param,
+                    const std::string& button_name) {
+    // The poller returned a complete axis, so set all the buttons
+    if (input_param.Has("axis_x") && input_param.Has("axis_y")) {
+        analog_param = input_param;
+        return;
+    }
+    // Check if the current configuration has either no engine or an axis binding.
+    // Clears out the old binding and adds one with analog_from_button.
+    if (!analog_param.Has("engine") || analog_param.Has("axis_x") || analog_param.Has("axis_y")) {
+        analog_param = {
+            {"engine", "analog_from_button"},
+        };
+    }
+    analog_param.Set(button_name, input_param.Serialize());
+}
+} // namespace
+
+ConfigureRingController::ConfigureRingController(QWidget* parent,
+                                                 InputCommon::InputSubsystem* input_subsystem_,
+                                                 Core::HID::HIDCore& hid_core_)
+    : QDialog(parent), timeout_timer(std::make_unique<QTimer>()),
+      poll_timer(std::make_unique<QTimer>()), input_subsystem{input_subsystem_},
+
+      ui(std::make_unique<Ui::ConfigureRingController>()) {
+    ui->setupUi(this);
+
+    analog_map_buttons = {
+        ui->buttonRingAnalogPull,
+        ui->buttonRingAnalogPush,
+    };
+
+    emulated_device = hid_core_.GetEmulatedDevices();
+    emulated_device->SaveCurrentConfig();
+    emulated_device->EnableConfiguration();
+
+    LoadConfiguration();
+
+    for (int sub_button_id = 0; sub_button_id < ANALOG_SUB_BUTTONS_NUM; ++sub_button_id) {
+        auto* const analog_button = analog_map_buttons[sub_button_id];
+
+        if (analog_button == nullptr) {
+            continue;
+        }
+
+        connect(analog_button, &QPushButton::clicked, [=, this] {
+            HandleClick(
+                analog_map_buttons[sub_button_id],
+                [=, this](const Common::ParamPackage& params) {
+                    Common::ParamPackage param = emulated_device->GetRingParam();
+                    SetAnalogParam(params, param, analog_sub_buttons[sub_button_id]);
+                    emulated_device->SetRingParam(param);
+                },
+                InputCommon::Polling::InputType::Stick);
+        });
+
+        analog_button->setContextMenuPolicy(Qt::CustomContextMenu);
+
+        connect(analog_button, &QPushButton::customContextMenuRequested,
+                [=, this](const QPoint& menu_location) {
+                    QMenu context_menu;
+                    Common::ParamPackage param = emulated_device->GetRingParam();
+                    context_menu.addAction(tr("Clear"), [&] {
+                        emulated_device->SetRingParam({});
+                        analog_map_buttons[sub_button_id]->setText(tr("[not set]"));
+                    });
+                    context_menu.addAction(tr("Invert axis"), [&] {
+                        const bool invert_value = param.Get("invert_x", "+") == "-";
+                        const std::string invert_str = invert_value ? "+" : "-";
+                        param.Set("invert_x", invert_str);
+                        emulated_device->SetRingParam(param);
+                        for (int sub_button_id = 0; sub_button_id < ANALOG_SUB_BUTTONS_NUM;
+                             ++sub_button_id) {
+                            analog_map_buttons[sub_button_id]->setText(
+                                AnalogToText(param, analog_sub_buttons[sub_button_id]));
+                        }
+                    });
+                    context_menu.exec(
+                        analog_map_buttons[sub_button_id]->mapToGlobal(menu_location));
+                });
+    }
+
+    connect(ui->sliderRingAnalogDeadzone, &QSlider::valueChanged, [=, this] {
+        Common::ParamPackage param = emulated_device->GetRingParam();
+        const auto slider_value = ui->sliderRingAnalogDeadzone->value();
+        ui->labelRingAnalogDeadzone->setText(tr("Deadzone: %1%").arg(slider_value));
+        param.Set("deadzone", slider_value / 100.0f);
+        emulated_device->SetRingParam(param);
+    });
+
+    connect(ui->restore_defaults_button, &QPushButton::clicked, this,
+            &ConfigureRingController::RestoreDefaults);
+
+    timeout_timer->setSingleShot(true);
+    connect(timeout_timer.get(), &QTimer::timeout, [this] { SetPollingResult({}, true); });
+
+    connect(poll_timer.get(), &QTimer::timeout, [this] {
+        const auto& params = input_subsystem->GetNextInput();
+        if (params.Has("engine") && IsInputAcceptable(params)) {
+            SetPollingResult(params, false);
+            return;
+        }
+    });
+
+    resize(0, 0);
+}
+
+ConfigureRingController::~ConfigureRingController() {
+    emulated_device->DisableConfiguration();
+};
+
+void ConfigureRingController::changeEvent(QEvent* event) {
+    if (event->type() == QEvent::LanguageChange) {
+        RetranslateUI();
+    }
+
+    QDialog::changeEvent(event);
+}
+
+void ConfigureRingController::RetranslateUI() {
+    ui->retranslateUi(this);
+}
+
+void ConfigureRingController::UpdateUI() {
+    RetranslateUI();
+    const Common::ParamPackage param = emulated_device->GetRingParam();
+
+    for (int sub_button_id = 0; sub_button_id < ANALOG_SUB_BUTTONS_NUM; ++sub_button_id) {
+        auto* const analog_button = analog_map_buttons[sub_button_id];
+
+        if (analog_button == nullptr) {
+            continue;
+        }
+
+        analog_button->setText(AnalogToText(param, analog_sub_buttons[sub_button_id]));
+    }
+
+    const auto deadzone_label = ui->labelRingAnalogDeadzone;
+    const auto deadzone_slider = ui->sliderRingAnalogDeadzone;
+
+    int slider_value = static_cast<int>(param.Get("deadzone", 0.15f) * 100);
+    deadzone_label->setText(tr("Deadzone: %1%").arg(slider_value));
+    deadzone_slider->setValue(slider_value);
+}
+
+void ConfigureRingController::ApplyConfiguration() {
+    emulated_device->DisableConfiguration();
+    emulated_device->SaveCurrentConfig();
+    emulated_device->EnableConfiguration();
+}
+
+void ConfigureRingController::LoadConfiguration() {
+    UpdateUI();
+}
+
+void ConfigureRingController::RestoreDefaults() {
+    const std::string default_ring_string = InputCommon::GenerateAnalogParamFromKeys(
+        0, 0, Config::default_ringcon_analogs[0], Config::default_ringcon_analogs[1], 0, 0.05f);
+    emulated_device->SetRingParam(Common::ParamPackage(default_ring_string));
+    UpdateUI();
+}
+
+void ConfigureRingController::HandleClick(
+    QPushButton* button, std::function<void(const Common::ParamPackage&)> new_input_setter,
+    InputCommon::Polling::InputType type) {
+    button->setText(tr("[waiting]"));
+    button->setFocus();
+
+    input_setter = new_input_setter;
+
+    input_subsystem->BeginMapping(type);
+
+    QWidget::grabMouse();
+    QWidget::grabKeyboard();
+
+    timeout_timer->start(2500); // Cancel after 2.5 seconds
+    poll_timer->start(25);      // Check for new inputs every 25ms
+}
+
+void ConfigureRingController::SetPollingResult(const Common::ParamPackage& params, bool abort) {
+    timeout_timer->stop();
+    poll_timer->stop();
+    input_subsystem->StopMapping();
+
+    QWidget::releaseMouse();
+    QWidget::releaseKeyboard();
+
+    if (!abort) {
+        (*input_setter)(params);
+    }
+
+    UpdateUI();
+
+    input_setter = std::nullopt;
+}
+
+bool ConfigureRingController::IsInputAcceptable(const Common::ParamPackage& params) const {
+    return true;
+}
+
+void ConfigureRingController::mousePressEvent(QMouseEvent* event) {
+    if (!input_setter || !event) {
+        return;
+    }
+
+    const auto button = GRenderWindow::QtButtonToMouseButton(event->button());
+    input_subsystem->GetMouse()->PressButton(0, 0, 0, 0, button);
+}
+
+void ConfigureRingController::keyPressEvent(QKeyEvent* event) {
+    event->ignore();
+    if (!input_setter || !event) {
+        return;
+    }
+    if (event->key() != Qt::Key_Escape) {
+        input_subsystem->GetKeyboard()->PressKey(event->key());
+    }
+}
+
+QString ConfigureRingController::ButtonToText(const Common::ParamPackage& param) {
+    if (!param.Has("engine")) {
+        return QObject::tr("[not set]");
+    }
+
+    const QString toggle = QString::fromStdString(param.Get("toggle", false) ? "~" : "");
+    const QString inverted = QString::fromStdString(param.Get("inverted", false) ? "!" : "");
+    const auto common_button_name = input_subsystem->GetButtonName(param);
+
+    // Retrieve the names from Qt
+    if (param.Get("engine", "") == "keyboard") {
+        const QString button_str = GetKeyName(param.Get("code", 0));
+        return QObject::tr("%1%2").arg(toggle, button_str);
+    }
+
+    if (common_button_name == Common::Input::ButtonNames::Invalid) {
+        return QObject::tr("[invalid]");
+    }
+
+    if (common_button_name == Common::Input::ButtonNames::Engine) {
+        return QString::fromStdString(param.Get("engine", ""));
+    }
+
+    if (common_button_name == Common::Input::ButtonNames::Value) {
+        if (param.Has("hat")) {
+            const QString hat = QString::fromStdString(param.Get("direction", ""));
+            return QObject::tr("%1%2Hat %3").arg(toggle, inverted, hat);
+        }
+        if (param.Has("axis")) {
+            const QString axis = QString::fromStdString(param.Get("axis", ""));
+            return QObject::tr("%1%2Axis %3").arg(toggle, inverted, axis);
+        }
+        if (param.Has("axis_x") && param.Has("axis_y") && param.Has("axis_z")) {
+            const QString axis_x = QString::fromStdString(param.Get("axis_x", ""));
+            const QString axis_y = QString::fromStdString(param.Get("axis_y", ""));
+            const QString axis_z = QString::fromStdString(param.Get("axis_z", ""));
+            return QObject::tr("%1%2Axis %3,%4,%5").arg(toggle, inverted, axis_x, axis_y, axis_z);
+        }
+        if (param.Has("motion")) {
+            const QString motion = QString::fromStdString(param.Get("motion", ""));
+            return QObject::tr("%1%2Motion %3").arg(toggle, inverted, motion);
+        }
+        if (param.Has("button")) {
+            const QString button = QString::fromStdString(param.Get("button", ""));
+            return QObject::tr("%1%2Button %3").arg(toggle, inverted, button);
+        }
+    }
+
+    QString button_name = GetButtonName(common_button_name);
+    if (param.Has("hat")) {
+        return QObject::tr("%1%2Hat %3").arg(toggle, inverted, button_name);
+    }
+    if (param.Has("axis")) {
+        return QObject::tr("%1%2Axis %3").arg(toggle, inverted, button_name);
+    }
+    if (param.Has("motion")) {
+        return QObject::tr("%1%2Axis %3").arg(toggle, inverted, button_name);
+    }
+    if (param.Has("button")) {
+        return QObject::tr("%1%2Button %3").arg(toggle, inverted, button_name);
+    }
+
+    return QObject::tr("[unknown]");
+}
+
+QString ConfigureRingController::AnalogToText(const Common::ParamPackage& param,
+                                              const std::string& dir) {
+    if (!param.Has("engine")) {
+        return QObject::tr("[not set]");
+    }
+
+    if (param.Get("engine", "") == "analog_from_button") {
+        return ButtonToText(Common::ParamPackage{param.Get(dir, "")});
+    }
+
+    if (!param.Has("axis_x") || !param.Has("axis_y")) {
+        return QObject::tr("[unknown]");
+    }
+
+    const auto engine_str = param.Get("engine", "");
+    const QString axis_x_str = QString::fromStdString(param.Get("axis_x", ""));
+    const QString axis_y_str = QString::fromStdString(param.Get("axis_y", ""));
+    const bool invert_x = param.Get("invert_x", "+") == "-";
+    const bool invert_y = param.Get("invert_y", "+") == "-";
+
+    if (dir == "modifier") {
+        return QObject::tr("[unused]");
+    }
+
+    if (dir == "left") {
+        const QString invert_x_str = QString::fromStdString(invert_x ? "+" : "-");
+        return QObject::tr("Axis %1%2").arg(axis_x_str, invert_x_str);
+    }
+    if (dir == "right") {
+        const QString invert_x_str = QString::fromStdString(invert_x ? "-" : "+");
+        return QObject::tr("Axis %1%2").arg(axis_x_str, invert_x_str);
+    }
+    if (dir == "up") {
+        const QString invert_y_str = QString::fromStdString(invert_y ? "-" : "+");
+        return QObject::tr("Axis %1%2").arg(axis_y_str, invert_y_str);
+    }
+    if (dir == "down") {
+        const QString invert_y_str = QString::fromStdString(invert_y ? "+" : "-");
+        return QObject::tr("Axis %1%2").arg(axis_y_str, invert_y_str);
+    }
+
+    return QObject::tr("[unknown]");
+}
\ No newline at end of file
diff --git a/src/yuzu/configuration/configure_ringcon.h b/src/yuzu/configuration/configure_ringcon.h
new file mode 100644
index 0000000000..cf9e54f09f
--- /dev/null
+++ b/src/yuzu/configuration/configure_ringcon.h
@@ -0,0 +1,85 @@
+// Copyright 2022 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <functional>
+#include <memory>
+#include <QDialog>
+
+namespace InputCommon {
+class InputSubsystem;
+} // namespace InputCommon
+
+namespace Core::HID {
+class HIDCore;
+class EmulatedDevices;
+} // namespace Core::HID
+
+namespace Ui {
+class ConfigureRingController;
+} // namespace Ui
+
+class ConfigureRingController : public QDialog {
+    Q_OBJECT
+
+public:
+    explicit ConfigureRingController(QWidget* parent, InputCommon::InputSubsystem* input_subsystem_,
+                                     Core::HID::HIDCore& hid_core_);
+    ~ConfigureRingController() override;
+
+    void ApplyConfiguration();
+
+private:
+    void changeEvent(QEvent* event) override;
+    void RetranslateUI();
+
+    void UpdateUI();
+
+    /// Load configuration settings.
+    void LoadConfiguration();
+
+    /// Restore all buttons to their default values.
+    void RestoreDefaults();
+
+    /// Called when the button was pressed.
+    void HandleClick(QPushButton* button,
+                     std::function<void(const Common::ParamPackage&)> new_input_setter,
+                     InputCommon::Polling::InputType type);
+
+    /// Finish polling and configure input using the input_setter.
+    void SetPollingResult(const Common::ParamPackage& params, bool abort);
+
+    /// Checks whether a given input can be accepted.
+    bool IsInputAcceptable(const Common::ParamPackage& params) const;
+
+    /// Handle mouse button press events.
+    void mousePressEvent(QMouseEvent* event) override;
+
+    /// Handle key press events.
+    void keyPressEvent(QKeyEvent* event) override;
+
+    QString ButtonToText(const Common::ParamPackage& param);
+
+    QString AnalogToText(const Common::ParamPackage& param, const std::string& dir);
+
+    static constexpr int ANALOG_SUB_BUTTONS_NUM = 2;
+
+    // A group of four QPushButtons represent one analog input. The buttons each represent left,
+    // right, respectively.
+    std::array<QPushButton*, ANALOG_SUB_BUTTONS_NUM> analog_map_buttons;
+
+    static const std::array<std::string, ANALOG_SUB_BUTTONS_NUM> analog_sub_buttons;
+
+    std::unique_ptr<QTimer> timeout_timer;
+    std::unique_ptr<QTimer> poll_timer;
+
+    /// This will be the the setting function when an input is awaiting configuration.
+    std::optional<std::function<void(const Common::ParamPackage&)>> input_setter;
+
+    InputCommon::InputSubsystem* input_subsystem;
+    Core::HID::EmulatedDevices* emulated_device;
+
+    std::unique_ptr<Ui::ConfigureRingController> ui;
+};
diff --git a/src/yuzu/configuration/configure_ringcon.ui b/src/yuzu/configuration/configure_ringcon.ui
new file mode 100644
index 0000000000..9ec634dd49
--- /dev/null
+++ b/src/yuzu/configuration/configure_ringcon.ui
@@ -0,0 +1,278 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ConfigureRingController</class>
+ <widget class="QDialog" name="ConfigureRingController">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>298</width>
+    <height>339</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Configure Ring Controller</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QLabel" name="label_2">
+     <property name="minimumSize">
+      <size>
+       <width>280</width>
+       <height>0</height>
+      </size>
+     </property>
+     <property name="text">
+      <string>If you want to use this controller configure player 1 as right controller and player 2 as dual joycon before starting the game to allow this controller to be detected properly.</string>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <spacer name="verticalSpacer_2">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeType">
+      <enum>QSizePolicy::Fixed</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>10</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  <item>
+  <widget class="QGroupBox" name="RingAnalog">
+    <property name="title">
+    <string>Ring Sensor Parameters</string>
+    </property>
+    <property name="alignment">
+    <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+    </property>
+    <layout class="QVBoxLayout" name="verticalLayout_3">
+    <property name="spacing">
+      <number>0</number>
+    </property>
+    <property name="sizeConstraint">
+      <enum>QLayout::SetDefaultConstraint</enum>
+    </property>
+    <property name="leftMargin">
+      <number>3</number>
+    </property>
+    <property name="topMargin">
+      <number>6</number>
+    </property>
+    <property name="rightMargin">
+      <number>3</number>
+    </property>
+    <property name="bottomMargin">
+      <number>0</number>
+    </property>
+    <item>
+      <layout class="QHBoxLayout" name="buttonRingAnalogPullHorizontaLayout">
+      <property name="spacing">
+        <number>3</number>
+      </property>
+      <item alignment="Qt::AlignHCenter">
+        <widget class="QGroupBox" name="buttonRingAnalogPullGroup">
+        <property name="title">
+          <string>Pull</string>
+        </property>
+        <property name="alignment">
+          <set>Qt::AlignCenter</set>
+        </property>
+        <layout class="QVBoxLayout" name="buttonRingAnalogPullVerticalLayout">
+          <property name="spacing">
+          <number>3</number>
+          </property>
+          <property name="leftMargin">
+          <number>3</number>
+          </property>
+          <property name="topMargin">
+          <number>3</number>
+          </property>
+          <property name="rightMargin">
+          <number>3</number>
+          </property>
+          <property name="bottomMargin">
+          <number>3</number>
+          </property>
+          <item>
+          <widget class="QPushButton" name="buttonRingAnalogPull">
+            <property name="minimumSize">
+            <size>
+              <width>68</width>
+              <height>0</height>
+            </size>
+            </property>
+            <property name="maximumSize">
+            <size>
+              <width>68</width>
+              <height>16777215</height>
+            </size>
+            </property>
+            <property name="styleSheet">
+            <string notr="true">min-width: 68px;</string>
+            </property>
+            <property name="text">
+            <string>Pull</string>
+            </property>
+          </widget>
+          </item>
+        </layout>
+        </widget>
+      </item>
+      <item alignment="Qt::AlignHCenter">
+        <widget class="QGroupBox" name="buttonRingAnalogPushGroup">
+        <property name="title">
+          <string>Push</string>
+        </property>
+        <property name="alignment">
+          <set>Qt::AlignCenter</set>
+        </property>
+        <layout class="QVBoxLayout" name="buttonRingAnalogPushVerticalLayout">
+          <property name="spacing">
+          <number>3</number>
+          </property>
+          <property name="leftMargin">
+          <number>3</number>
+          </property>
+          <property name="topMargin">
+          <number>3</number>
+          </property>
+          <property name="rightMargin">
+          <number>3</number>
+          </property>
+          <property name="bottomMargin">
+          <number>3</number>
+          </property>
+          <item>
+          <widget class="QPushButton" name="buttonRingAnalogPush">
+            <property name="minimumSize">
+            <size>
+              <width>68</width>
+              <height>0</height>
+            </size>
+            </property>
+            <property name="maximumSize">
+            <size>
+              <width>68</width>
+              <height>16777215</height>
+            </size>
+            </property>
+            <property name="styleSheet">
+            <string notr="true">min-width: 68px;</string>
+            </property>
+            <property name="text">
+            <string>Push</string>
+            </property>
+          </widget>
+          </item>
+        </layout>
+        </widget>
+      </item>
+      </layout>
+    </item>
+    <item>
+      <layout class="QVBoxLayout" name="sliderRingAnalogDeadzoneVerticalLayout">
+      <property name="spacing">
+        <number>3</number>
+      </property>
+      <property name="sizeConstraint">
+        <enum>QLayout::SetDefaultConstraint</enum>
+      </property>
+      <property name="leftMargin">
+        <number>0</number>
+      </property>
+      <property name="topMargin">
+        <number>10</number>
+      </property>
+      <property name="rightMargin">
+        <number>0</number>
+      </property>
+      <property name="bottomMargin">
+        <number>3</number>
+      </property>
+      <item>
+        <layout class="QHBoxLayout" name="sliderRingAnalogDeadzoneHorizontalLayout">
+        <item>
+          <widget class="QLabel" name="labelRingAnalogDeadzone">
+          <property name="text">
+            <string>Deadzone: 0%</string>
+          </property>
+          <property name="alignment">
+            <set>Qt::AlignHCenter</set>
+          </property>
+          </widget>
+        </item>
+        </layout>
+      </item>
+      <item>
+        <widget class="QSlider" name="sliderRingAnalogDeadzone">
+        <property name="maximum">
+          <number>100</number>
+        </property>
+        <property name="orientation">
+          <enum>Qt::Horizontal</enum>
+        </property>
+        </widget>
+      </item>
+      </layout>
+    </item>
+    </layout>
+  </widget>
+  </item>
+   <item>
+    <spacer name="verticalSpacer">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>40</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QPushButton" name="restore_defaults_button">
+       <property name="text">
+        <string>Restore Defaults</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QDialogButtonBox" name="buttonBox">
+       <property name="standardButtons">
+        <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>ConfigureRingController</receiver>
+   <slot>accept()</slot>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>ConfigureRingController</receiver>
+   <slot>reject()</slot>
+   </connection>
+ </connections>
+</ui>