diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index c770f5f21..d044e9650 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -54,6 +54,8 @@ add_library(core
     memory_card.h
     namco_guncon.cpp
     namco_guncon.h
+    negcon.cpp
+    negcon.h
     pad.cpp
     pad.h
     playstation_mouse.cpp
diff --git a/src/core/controller.cpp b/src/core/controller.cpp
index 12e147fcd..9c38cc864 100644
--- a/src/core/controller.cpp
+++ b/src/core/controller.cpp
@@ -4,6 +4,7 @@
 #include "digital_controller.h"
 #include "namco_guncon.h"
 #include "playstation_mouse.h"
+#include "negcon.h"
 
 Controller::Controller() = default;
 
@@ -54,6 +55,9 @@ std::unique_ptr<Controller> Controller::Create(System* system, ControllerType ty
     case ControllerType::PlayStationMouse:
       return PlayStationMouse::Create(system);
 
+    case ControllerType::NeGcon:
+      return NeGcon::Create();
+
     case ControllerType::None:
     default:
       return {};
@@ -86,6 +90,9 @@ Controller::AxisList Controller::GetAxisNames(ControllerType type)
     case ControllerType::PlayStationMouse:
       return PlayStationMouse::StaticGetAxisNames();
 
+    case ControllerType::NeGcon:
+      return NeGcon::StaticGetAxisNames();
+
     case ControllerType::None:
     default:
       return {};
@@ -108,6 +115,9 @@ Controller::ButtonList Controller::GetButtonNames(ControllerType type)
     case ControllerType::PlayStationMouse:
       return PlayStationMouse::StaticGetButtonNames();
 
+    case ControllerType::NeGcon:
+      return NeGcon::StaticGetButtonNames();
+
     case ControllerType::None:
     default:
       return {};
@@ -130,6 +140,9 @@ u32 Controller::GetVibrationMotorCount(ControllerType type)
     case ControllerType::PlayStationMouse:
       return PlayStationMouse::StaticGetVibrationMotorCount();
 
+    case ControllerType::NeGcon:
+      return NeGcon::StaticGetVibrationMotorCount();
+
     case ControllerType::None:
     default:
       return 0;
@@ -152,6 +165,9 @@ std::optional<s32> Controller::GetAxisCodeByName(ControllerType type, std::strin
     case ControllerType::PlayStationMouse:
       return PlayStationMouse::StaticGetAxisCodeByName(axis_name);
 
+    case ControllerType::NeGcon:
+      return NeGcon::StaticGetAxisCodeByName(axis_name);
+
     case ControllerType::None:
     default:
       return std::nullopt;
@@ -174,6 +190,9 @@ std::optional<s32> Controller::GetButtonCodeByName(ControllerType type, std::str
     case ControllerType::PlayStationMouse:
       return PlayStationMouse::StaticGetButtonCodeByName(button_name);
 
+    case ControllerType::NeGcon:
+      return NeGcon::StaticGetButtonCodeByName(button_name);
+
     case ControllerType::None:
     default:
       return std::nullopt;
diff --git a/src/core/negcon.cpp b/src/core/negcon.cpp
new file mode 100644
index 000000000..85d235c41
--- /dev/null
+++ b/src/core/negcon.cpp
@@ -0,0 +1,246 @@
+#include "negcon.h"
+#include "common/assert.h"
+#include "common/log.h"
+#include "common/state_wrapper.h"
+#include <array>
+#include <cmath>
+
+NeGcon::NeGcon()
+{
+  m_axis_state.fill(0x00);
+  m_axis_state[static_cast<u8>(Axis::Steering)] = 0x80;
+}
+
+NeGcon::~NeGcon() = default;
+
+ControllerType NeGcon::GetType() const
+{
+  return ControllerType::NeGcon;
+}
+
+std::optional<s32> NeGcon::GetAxisCodeByName(std::string_view axis_name) const
+{
+  return StaticGetAxisCodeByName(axis_name);
+}
+
+std::optional<s32> NeGcon::GetButtonCodeByName(std::string_view button_name) const
+{
+  return StaticGetButtonCodeByName(button_name);
+}
+
+void NeGcon::Reset()
+{
+  m_transfer_state = TransferState::Idle;
+}
+
+bool NeGcon::DoState(StateWrapper& sw)
+{
+  if (!Controller::DoState(sw))
+    return false;
+
+  sw.Do(&m_button_state);
+  sw.Do(&m_transfer_state);
+  return true;
+}
+
+void NeGcon::SetAxisState(s32 axis_code, float value)
+{
+  if (axis_code < 0 || axis_code >= static_cast<s32>(Axis::Count))
+    return;
+
+  // Steering Axis: -1..1 -> 0..255
+  if (axis_code == static_cast<s32>(Axis::Steering))
+  {
+    const u8 u8_value = static_cast<u8>(std::clamp(((value + 1.0f) / 2.0f) * 255.0f, 0.0f, 255.0f));
+
+    SetAxisState(static_cast<Axis>(axis_code), u8_value);
+
+    return;
+  }
+
+  // I, II, L: 0..1 -> 0..255 or -1..0 -> 0..255 to support negative axis ranges,
+  // e.g. if bound to analog stick instead of trigger
+  const u8 u8_value = static_cast<u8>(std::clamp(std::abs(value) * 255.0f, 0.0f, 255.0f));
+
+  SetAxisState(static_cast<Axis>(axis_code), u8_value);
+}
+
+void NeGcon::SetAxisState(Axis axis, u8 value)
+{
+  m_axis_state[static_cast<u8>(axis)] = value;
+}
+
+void NeGcon::SetButtonState(s32 button_code, bool pressed)
+{
+  if (button_code < 0 || button_code >= static_cast<s32>(Button::Count))
+    return;
+
+  SetButtonState(static_cast<Button>(button_code), pressed);
+}
+
+void NeGcon::SetButtonState(Button button, bool pressed)
+{
+  // Mapping of Button to index of corresponding bit in m_button_state
+  static constexpr std::array<u8, static_cast<size_t>(Button::Count)> indices = {3, 4, 5, 6, 7, 11, 12, 13};
+
+  if (pressed)
+    m_button_state &= ~(u16(1) << indices[static_cast<u8>(button)]);
+  else
+    m_button_state |= u16(1) << indices[static_cast<u8>(button)];
+}
+
+void NeGcon::ResetTransferState()
+{
+  m_transfer_state = TransferState::Idle;
+}
+
+bool NeGcon::Transfer(const u8 data_in, u8* data_out)
+{
+  static constexpr u16 ID = 0x5A23;
+
+  switch (m_transfer_state)
+  {
+    case TransferState::Idle:
+    {
+      if (data_in == 0x42)
+      {
+        *data_out = Truncate8(ID);
+        m_transfer_state = TransferState::IDMSB;
+        return true;
+      }
+      else
+      {
+        *data_out = 0xFF;
+        return (data_in == 0x01);
+      }
+    }
+
+    case TransferState::IDMSB:
+    {
+      *data_out = Truncate8(ID >> 8);
+      m_transfer_state = TransferState::ButtonsLSB;
+      return true;
+    }
+
+    case TransferState::ButtonsLSB:
+    {
+      *data_out = Truncate8(m_button_state);
+      m_transfer_state = TransferState::ButtonsMSB;
+      return true;
+    }
+
+    case TransferState::ButtonsMSB:
+    {
+      *data_out = Truncate8(m_button_state >> 8);
+      m_transfer_state = TransferState::AnalogSteering;
+      return true;
+    }
+
+    case TransferState::AnalogSteering:
+    {
+      *data_out = Truncate8(m_axis_state[static_cast<u8>(Axis::Steering)]);
+      m_transfer_state = TransferState::AnalogI;
+      return true;
+    }
+
+    case TransferState::AnalogI:
+    {
+      *data_out = Truncate8(m_axis_state[static_cast<u8>(Axis::I)]);
+      m_transfer_state = TransferState::AnalogII;
+      return true;
+    }
+
+    case TransferState::AnalogII:
+    {
+      *data_out = Truncate8(m_axis_state[static_cast<u8>(Axis::II)]);
+      m_transfer_state = TransferState::AnalogL;
+      return true;
+    }
+
+    case TransferState::AnalogL:
+    {
+      *data_out = Truncate8(m_axis_state[static_cast<u8>(Axis::L)]);
+      m_transfer_state = TransferState::Idle;
+      return false;
+    }
+
+    default:
+    {
+      UnreachableCode();
+      return false;
+    }
+  }
+}
+
+std::unique_ptr<NeGcon> NeGcon::Create()
+{
+  return std::make_unique<NeGcon>();
+}
+
+std::optional<s32> NeGcon::StaticGetAxisCodeByName(std::string_view axis_name)
+{
+#define AXIS(name)                                                                                                     \
+  if (axis_name == #name)                                                                                              \
+  {                                                                                                                    \
+    return static_cast<s32>(ZeroExtend32(static_cast<u8>(Axis::name)));                                                \
+  }
+
+  AXIS(Steering);
+  AXIS(I);
+  AXIS(II);
+  AXIS(L);
+
+  return std::nullopt;
+
+#undef AXIS
+}
+
+std::optional<s32> NeGcon::StaticGetButtonCodeByName(std::string_view button_name)
+{
+#define BUTTON(name)                                                                                                   \
+  if (button_name == #name)                                                                                            \
+  {                                                                                                                    \
+    return static_cast<s32>(ZeroExtend32(static_cast<u8>(Button::name)));                                              \
+  }
+
+  BUTTON(Up);
+  BUTTON(Down);
+  BUTTON(Left);
+  BUTTON(Right);
+  BUTTON(A);
+  BUTTON(B);
+  BUTTON(R);
+  BUTTON(Start);
+
+  return std::nullopt;
+
+#undef BUTTON
+}
+
+Controller::AxisList NeGcon::StaticGetAxisNames()
+{
+#define A(n)                                                                                                           \
+  {                                                                                                                    \
+    #n, static_cast <s32>(Axis::n)                                                                                     \
+  }
+
+  return {A(Steering), A(I), A(II), A(L)};
+
+#undef A
+}
+
+Controller::ButtonList NeGcon::StaticGetButtonNames()
+{
+#define B(n)                                                                                                           \
+  {                                                                                                                    \
+    #n, static_cast <s32>(Button::n)                                                                                   \
+  }
+
+  return {B(Up), B(Down), B(Left), B(Right), B(A), B(B), B(R), B(Start)};
+#undef B
+}
+
+u32 NeGcon::StaticGetVibrationMotorCount()
+{
+  return 0;
+}
diff --git a/src/core/negcon.h b/src/core/negcon.h
new file mode 100644
index 000000000..0ff3388ef
--- /dev/null
+++ b/src/core/negcon.h
@@ -0,0 +1,78 @@
+#pragma once
+#include "controller.h"
+#include <array>
+#include <memory>
+#include <optional>
+#include <string_view>
+
+class NeGcon final : public Controller
+{
+public:
+  enum class Axis : u8
+  {
+    Steering = 0,
+    I = 1,
+    II = 2,
+    L = 3,
+    Count
+  };
+
+  enum class Button : u8
+  {
+    Start = 0,
+    Up = 1,
+    Right = 2,
+    Down = 3,
+    Left = 4,
+    R = 5,
+    B = 6,
+    A = 7,
+    Count
+  };
+
+  NeGcon();
+  ~NeGcon() override;
+
+  static std::unique_ptr<NeGcon> Create();
+  static std::optional<s32> StaticGetAxisCodeByName(std::string_view axis_name);
+  static std::optional<s32> StaticGetButtonCodeByName(std::string_view button_name);
+  static AxisList StaticGetAxisNames();
+  static ButtonList StaticGetButtonNames();
+  static u32 StaticGetVibrationMotorCount();
+
+  ControllerType GetType() const override;
+  std::optional<s32> GetAxisCodeByName(std::string_view axis_name) const override;
+  std::optional<s32> GetButtonCodeByName(std::string_view button_name) const override;
+
+  void Reset() override;
+  bool DoState(StateWrapper& sw) override;
+
+  void SetAxisState(s32 axis_code, float value) override;
+  void SetButtonState(s32 button_code, bool pressed) override;
+
+  void ResetTransferState() override;
+  bool Transfer(const u8 data_in, u8* data_out) override;
+
+  void SetAxisState(Axis axis, u8 value);
+  void SetButtonState(Button button, bool pressed);
+
+private:
+  enum class TransferState : u8
+  {
+    Idle,
+    IDMSB,
+    ButtonsLSB,
+    ButtonsMSB,
+    AnalogSteering,
+    AnalogI,
+    AnalogII,
+    AnalogL
+  };
+
+  std::array<u8, static_cast<u8>(Axis::Count)> m_axis_state {};
+
+  // buttons are active low; bits 0-2, 8-10, 14-15 are not used and are always high
+  u16 m_button_state = UINT16_C(0xFFFF);
+
+  TransferState m_transfer_state = TransferState::Idle;
+};
diff --git a/src/core/settings.cpp b/src/core/settings.cpp
index 5a433e353..153f7bf81 100644
--- a/src/core/settings.cpp
+++ b/src/core/settings.cpp
@@ -419,10 +419,10 @@ const char* Settings::GetAudioBackendDisplayName(AudioBackend backend)
   return s_audio_backend_display_names[static_cast<int>(backend)];
 }
 
-static std::array<const char*, 5> s_controller_type_names = {
-  {"None", "DigitalController", "AnalogController", "NamcoGunCon", "PlayStationMouse"}};
-static std::array<const char*, 5> s_controller_display_names = {
-  {"None", "Digital Controller", "Analog Controller (DualShock)", "Namco GunCon", "PlayStation Mouse"}};
+static std::array<const char*, 6> s_controller_type_names = {
+  {"None", "DigitalController", "AnalogController", "NamcoGunCon", "PlayStationMouse", "NeGcon"}};
+static std::array<const char*, 6> s_controller_display_names = {
+  {"None", "Digital Controller", "Analog Controller (DualShock)", "Namco GunCon", "PlayStation Mouse", "NeGcon"}};
 
 std::optional<ControllerType> Settings::ParseControllerTypeName(const char* str)
 {
diff --git a/src/core/types.h b/src/core/types.h
index 8eea33f13..ca78133d5 100644
--- a/src/core/types.h
+++ b/src/core/types.h
@@ -89,6 +89,7 @@ enum class ControllerType
   AnalogController,
   NamcoGunCon,
   PlayStationMouse,
+  NeGcon,
   Count
 };