Add XInput Analog mode

This mode will map the readouts of the drum to the left (for player 1)
or right (for player 2) analogstick axis.
This commit is contained in:
Frederik Walk 2024-10-12 18:38:42 +02:00
parent 1644e1e611
commit 2bcbf1e458
10 changed files with 191 additions and 65 deletions

View File

@ -17,7 +17,8 @@ The firmware is pretty much tailored to this specific use case, if you are looki
- Dualshock 3 - Dualshock 3
- Switch Pro Controller - Switch Pro Controller
- XInput - XInput
- Keyboard - XInput Analog (Compatible with [TaikoArcadeLoader](https://github.com/esuo1198/TaikoArcadeLoader) analog input)
- Keyboard (Mapping: 'DFJK' / 'CBN,')
- MIDI - MIDI
- Debug mode (will output current state via USB serial) - Debug mode (will output current state via USB serial)
- Additional buttons via external i2c GPIO expander - Additional buttons via external i2c GPIO expander

View File

@ -51,7 +51,6 @@ class Drum {
private: private:
enum class Id { enum class Id {
NONE,
DON_LEFT, DON_LEFT,
KA_LEFT, KA_LEFT,
DON_RIGHT, DON_RIGHT,
@ -74,6 +73,7 @@ class Drum {
class AdcInterface { class AdcInterface {
public: public:
// This is expected to return a 12bit value.
virtual uint16_t read(uint8_t channel) = 0; virtual uint16_t read(uint8_t channel) = 0;
}; };
@ -98,6 +98,8 @@ class Drum {
private: private:
void updateRollCounter(Utils::InputState &input_state); void updateRollCounter(Utils::InputState &input_state);
void updateDigitalInputState(Utils::InputState &input_state, const std::map<Id, uint16_t> &raw_values);
void updateAnalogInputState(Utils::InputState &input_state, const std::map<Id, uint16_t> &raw_values);
std::map<Id, uint16_t> sampleInputs(); std::map<Id, uint16_t> sampleInputs();
public: public:

View File

@ -23,6 +23,8 @@ typedef enum {
USB_MODE_KEYBOARD_P1, USB_MODE_KEYBOARD_P1,
USB_MODE_KEYBOARD_P2, USB_MODE_KEYBOARD_P2,
USB_MODE_XBOX360, USB_MODE_XBOX360,
USB_MODE_XBOX360_ANALOG_P1,
USB_MODE_XBOX360_ANALOG_P2,
USB_MODE_MIDI, USB_MODE_MIDI,
USB_MODE_DEBUG, USB_MODE_DEBUG,
} usb_mode_t; } usb_mode_t;

View File

@ -16,7 +16,7 @@ struct InputState {
struct Drum { struct Drum {
struct Pad { struct Pad {
bool triggered; bool triggered;
uint16_t raw; uint16_t analog;
}; };
Pad don_left, ka_left, don_right, ka_right; Pad don_left, ka_left, don_right, ka_right;
@ -62,7 +62,9 @@ struct InputState {
usb_report_t getPS3InputReport(); usb_report_t getPS3InputReport();
usb_report_t getPS4InputReport(); usb_report_t getPS4InputReport();
usb_report_t getKeyboardReport(Player player); usb_report_t getKeyboardReport(Player player);
usb_report_t getXinputReport(); usb_report_t getXinputBaseReport();
usb_report_t getXinputDigitalReport();
usb_report_t getXinputAnalogReport(Player player);
usb_report_t getMidiReport(); usb_report_t getMidiReport();
usb_report_t getDebugReport(); usb_report_t getDebugReport();

View File

@ -66,6 +66,8 @@ class Menu {
ChangeUsbModeKeyboardP1, ChangeUsbModeKeyboardP1,
ChangeUsbModeKeyboardP2, ChangeUsbModeKeyboardP2,
ChangeUsbModeXbox360, ChangeUsbModeXbox360,
ChangeUsbModeXbox360AnalogP1,
ChangeUsbModeXbox360AnalogP2,
ChangeUsbModeMidi, ChangeUsbModeMidi,
ChangeUsbModeDebug, ChangeUsbModeDebug,

View File

@ -45,6 +45,10 @@ static std::string modeToString(usb_mode_t mode) {
return "Keyboard P2"; return "Keyboard P2";
case USB_MODE_XBOX360: case USB_MODE_XBOX360:
return "Xbox 360"; return "Xbox 360";
case USB_MODE_XBOX360_ANALOG_P1:
return "Analog P1";
case USB_MODE_XBOX360_ANALOG_P2:
return "Analog P2";
case USB_MODE_MIDI: case USB_MODE_MIDI:
return "MIDI"; return "MIDI";
case USB_MODE_DEBUG: case USB_MODE_DEBUG:

View File

@ -4,14 +4,17 @@
#include "pico/time.h" #include "pico/time.h"
#include <algorithm> #include <algorithm>
#include <deque>
namespace Doncon::Peripherals { namespace Doncon::Peripherals {
Drum::InternalAdc::InternalAdc(const Drum::Config::AdcInputs &adc_inputs) { Drum::InternalAdc::InternalAdc(const Drum::Config::AdcInputs &adc_inputs) {
adc_gpio_init(adc_inputs.don_left + 26); static const uint adc_base_pin = 26;
adc_gpio_init(adc_inputs.don_right + 26);
adc_gpio_init(adc_inputs.ka_left + 26); adc_gpio_init(adc_base_pin + adc_inputs.don_left);
adc_gpio_init(adc_inputs.ka_right + 26); adc_gpio_init(adc_base_pin + adc_inputs.don_right);
adc_gpio_init(adc_base_pin + adc_inputs.ka_left);
adc_gpio_init(adc_base_pin + adc_inputs.ka_right);
adc_init(); adc_init();
} }
@ -80,6 +83,7 @@ Drum::Drum(const Config &config) : m_config(config) {
std::map<Drum::Id, uint16_t> Drum::sampleInputs() { std::map<Drum::Id, uint16_t> Drum::sampleInputs() {
std::map<Id, uint32_t> values; std::map<Id, uint32_t> values;
// Oversample ADC inputs to get rid of ADC noise
for (uint8_t sample_number = 0; sample_number < m_config.sample_count; ++sample_number) { for (uint8_t sample_number = 0; sample_number < m_config.sample_count; ++sample_number) {
for (const auto &pad : m_pads) { for (const auto &pad : m_pads) {
values[pad.first] += m_adc->read(pad.second.getChannel()); values[pad.first] += m_adc->read(pad.second.getChannel());
@ -138,10 +142,7 @@ void Drum::updateRollCounter(Utils::InputState &input_state) {
input_state.drum.previous_roll = previous_roll; input_state.drum.previous_roll = previous_roll;
} }
void Drum::updateInputState(Utils::InputState &input_state) { void Drum::updateDigitalInputState(Utils::InputState &input_state, const std::map<Drum::Id, uint16_t> &raw_values) {
// Oversample ADC inputs to get rid of ADC noise
const auto raw_values = sampleInputs();
auto get_threshold = [&](const Id target) { auto get_threshold = [&](const Id target) {
switch (target) { switch (target) {
case Id::DON_LEFT: case Id::DON_LEFT:
@ -152,8 +153,6 @@ void Drum::updateInputState(Utils::InputState &input_state) {
return m_config.trigger_thresholds.ka_left; return m_config.trigger_thresholds.ka_left;
case Id::KA_RIGHT: case Id::KA_RIGHT:
return m_config.trigger_thresholds.ka_right; return m_config.trigger_thresholds.ka_right;
case Id::NONE:
return (uint16_t)0;
} }
return (uint16_t)0; return (uint16_t)0;
}; };
@ -180,11 +179,6 @@ void Drum::updateInputState(Utils::InputState &input_state) {
set_max(Id::DON_LEFT, Id::KA_LEFT); set_max(Id::DON_LEFT, Id::KA_LEFT);
set_max(Id::DON_RIGHT, Id::KA_RIGHT); set_max(Id::DON_RIGHT, Id::KA_RIGHT);
input_state.drum.don_left.raw = raw_values.at(Id::DON_LEFT);
input_state.drum.ka_left.raw = raw_values.at(Id::KA_LEFT);
input_state.drum.don_right.raw = raw_values.at(Id::DON_RIGHT);
input_state.drum.ka_right.raw = raw_values.at(Id::KA_RIGHT);
input_state.drum.don_left.triggered = m_pads.at(Id::DON_LEFT).getState(); input_state.drum.don_left.triggered = m_pads.at(Id::DON_LEFT).getState();
input_state.drum.ka_left.triggered = m_pads.at(Id::KA_LEFT).getState(); input_state.drum.ka_left.triggered = m_pads.at(Id::KA_LEFT).getState();
input_state.drum.don_right.triggered = m_pads.at(Id::DON_RIGHT).getState(); input_state.drum.don_right.triggered = m_pads.at(Id::DON_RIGHT).getState();
@ -193,6 +187,63 @@ void Drum::updateInputState(Utils::InputState &input_state) {
updateRollCounter(input_state); updateRollCounter(input_state);
} }
void Drum::updateAnalogInputState(Utils::InputState &input_state, const std::map<Drum::Id, uint16_t> &raw_values) {
struct buffer_entry {
uint16_t value;
uint32_t timestamp;
};
static std::map<Id, std::deque<buffer_entry>> buffer;
uint32_t now = to_ms_since_boot(get_absolute_time());
std::for_each(raw_values.cbegin(), raw_values.cend(), [&](const auto &entry) {
const auto &id = entry.first;
const auto &raw = entry.second;
auto &buf = buffer[id];
// Clear outdated values, i.e. anything older than debounce_delay to allow for convenient configuration.
while (!buf.empty() && (buf.front().timestamp + m_config.debounce_delay_ms) <= now) {
buf.pop_front();
}
// Insert current value.
buf.push_back({raw, now});
// Set maximum value for each pads buffer window.
const auto get_max = [](const auto &input) {
return std::max_element(input.cbegin(), input.cend(),
[](const auto &a, const auto &b) { return a.value < b.value; })
->value;
};
// Map 12bit raw value to 16bit
const auto raw_to_uint16 = [](uint16_t raw) { return ((raw << 4) & 0xFFF0) | ((raw >> 8) & 0x000F); };
switch (id) {
case Id::DON_LEFT:
input_state.drum.don_left.analog = raw_to_uint16(get_max(buf));
break;
case Id::DON_RIGHT:
input_state.drum.don_right.analog = raw_to_uint16(get_max(buf));
break;
case Id::KA_LEFT:
input_state.drum.ka_left.analog = raw_to_uint16(get_max(buf));
break;
case Id::KA_RIGHT:
input_state.drum.ka_right.analog = raw_to_uint16(get_max(buf));
break;
}
});
}
void Drum::updateInputState(Utils::InputState &input_state) {
const auto raw_values = sampleInputs();
updateDigitalInputState(input_state, raw_values);
updateAnalogInputState(input_state, raw_values);
}
void Drum::setDebounceDelay(const uint16_t delay) { m_config.debounce_delay_ms = delay; } void Drum::setDebounceDelay(const uint16_t delay) { m_config.debounce_delay_ms = delay; }
void Drum::setThresholds(const Config::Thresholds &thresholds) { m_config.trigger_thresholds = thresholds; } void Drum::setThresholds(const Config::Thresholds &thresholds) { m_config.trigger_thresholds = thresholds; }

View File

@ -86,6 +86,8 @@ void usb_driver_init(usb_mode_t mode) {
usbd_send_report = send_hid_keyboard_report; usbd_send_report = send_hid_keyboard_report;
usbd_receive_report = NULL; usbd_receive_report = NULL;
break; break;
case USB_MODE_XBOX360_ANALOG_P1:
case USB_MODE_XBOX360_ANALOG_P2:
case USB_MODE_XBOX360: case USB_MODE_XBOX360:
usbd_desc_device = &xinput_desc_device; usbd_desc_device = &xinput_desc_device;
usbd_desc_cfg = xinput_desc_cfg; usbd_desc_cfg = xinput_desc_cfg;

View File

@ -28,7 +28,11 @@ usb_report_t InputState::getReport(usb_mode_t mode) {
case USB_MODE_KEYBOARD_P2: case USB_MODE_KEYBOARD_P2:
return getKeyboardReport(Player::Two); return getKeyboardReport(Player::Two);
case USB_MODE_XBOX360: case USB_MODE_XBOX360:
return getXinputReport(); return getXinputDigitalReport();
case USB_MODE_XBOX360_ANALOG_P1:
return getXinputAnalogReport(Player::One);
case USB_MODE_XBOX360_ANALOG_P2:
return getXinputAnalogReport(Player::Two);
case USB_MODE_MIDI: case USB_MODE_MIDI:
return getMidiReport(); return getMidiReport();
case USB_MODE_DEBUG: case USB_MODE_DEBUG:
@ -234,11 +238,11 @@ usb_report_t InputState::getKeyboardReport(InputState::Player player) {
return {(uint8_t *)&m_keyboard_report, sizeof(hid_nkro_keyboard_report_t)}; return {(uint8_t *)&m_keyboard_report, sizeof(hid_nkro_keyboard_report_t)};
} }
usb_report_t InputState::getXinputReport() { usb_report_t InputState::getXinputBaseReport() {
m_xinput_report.buttons1 = 0 // m_xinput_report.buttons1 = 0 //
| (controller.dpad.up ? (1 << 0) : 0) // Dpad Up | (controller.dpad.up ? (1 << 0) : 0) // Dpad Up
| ((controller.dpad.down || drum.don_left.triggered) ? (1 << 1) : 0) // Dpad Down | (controller.dpad.down ? (1 << 1) : 0) // Dpad Down
| ((controller.dpad.left || drum.ka_left.triggered) ? (1 << 2) : 0) // Dpad Left | (controller.dpad.left ? (1 << 2) : 0) // Dpad Left
| (controller.dpad.right ? (1 << 3) : 0) // Dpad Right | (controller.dpad.right ? (1 << 3) : 0) // Dpad Right
| (controller.buttons.start ? (1 << 4) : 0) // Start | (controller.buttons.start ? (1 << 4) : 0) // Start
| (controller.buttons.select ? (1 << 5) : 0) // Select | (controller.buttons.select ? (1 << 5) : 0) // Select
@ -249,8 +253,8 @@ usb_report_t InputState::getXinputReport() {
| (controller.buttons.l ? (1 << 0) : 0) // L1 | (controller.buttons.l ? (1 << 0) : 0) // L1
| (controller.buttons.r ? (1 << 1) : 0) // R1 | (controller.buttons.r ? (1 << 1) : 0) // R1
| (controller.buttons.home ? (1 << 2) : 0) // Guide | (controller.buttons.home ? (1 << 2) : 0) // Guide
| ((controller.buttons.south || drum.don_right.triggered) ? (1 << 4) : 0) // A | (controller.buttons.south ? (1 << 4) : 0) // A
| ((controller.buttons.east || drum.ka_right.triggered) ? (1 << 5) : 0) // B | (controller.buttons.east ? (1 << 5) : 0) // B
| (controller.buttons.west ? (1 << 6) : 0) // X | (controller.buttons.west ? (1 << 6) : 0) // X
| (controller.buttons.north ? (1 << 7) : 0); // Y | (controller.buttons.north ? (1 << 7) : 0); // Y
@ -265,6 +269,52 @@ usb_report_t InputState::getXinputReport() {
return {(uint8_t *)&m_xinput_report, sizeof(xinput_report_t)}; return {(uint8_t *)&m_xinput_report, sizeof(xinput_report_t)};
} }
usb_report_t InputState::getXinputDigitalReport() {
getXinputBaseReport();
m_xinput_report.buttons1 |= (drum.don_left.triggered ? (1 << 1) : 0) // Dpad Down
| (drum.ka_left.triggered ? (1 << 2) : 0); // Dpad Left
m_xinput_report.buttons2 |= (drum.don_right.triggered ? (1 << 4) : 0) // A
| (drum.ka_right.triggered ? (1 << 5) : 0); // B
return {(uint8_t *)&m_xinput_report, sizeof(xinput_report_t)};
}
usb_report_t InputState::getXinputAnalogReport(InputState::Player player) {
getXinputBaseReport();
int16_t x = 0;
int16_t y = 0;
auto map_to_axis = [](uint16_t raw) -> uint16_t { return raw >> 1; };
if (drum.ka_left.analog > drum.don_left.analog) {
x = -map_to_axis(drum.ka_left.analog);
} else {
x = map_to_axis(drum.don_left.analog);
}
if (drum.ka_right.analog > drum.don_right.analog) {
y = map_to_axis(drum.ka_right.analog);
} else {
y = -map_to_axis(drum.don_right.analog);
}
switch (player) {
case Player::One:
m_xinput_report.lx = x;
m_xinput_report.ly = y;
break;
case Player::Two:
m_xinput_report.rx = x;
m_xinput_report.ry = y;
break;
}
return {(uint8_t *)&m_xinput_report, sizeof(xinput_report_t)};
}
usb_report_t InputState::getMidiReport() { usb_report_t InputState::getMidiReport() {
struct state { struct state {
bool last_triggered; bool last_triggered;
@ -287,8 +337,8 @@ usb_report_t InputState::getMidiReport() {
target.on = false; target.on = false;
} }
if (new_state.triggered && new_state.raw > target.velocity) { if (new_state.triggered && new_state.analog > target.velocity) {
target.velocity = new_state.raw; target.velocity = new_state.analog;
} }
target.last_triggered = new_state.triggered; target.last_triggered = new_state.triggered;
@ -305,7 +355,7 @@ usb_report_t InputState::getMidiReport() {
m_midi_report.status.side_stick = side_stick.on; m_midi_report.status.side_stick = side_stick.on;
auto convert_range = [](uint16_t in) { auto convert_range = [](uint16_t in) {
uint16_t out = in / 16; uint16_t out = in / 256;
return uint8_t(out > 127 ? 127 : out); return uint8_t(out > 127 ? 127 : out);
}; };
@ -320,17 +370,17 @@ usb_report_t InputState::getMidiReport() {
usb_report_t InputState::getDebugReport() { usb_report_t InputState::getDebugReport() {
std::stringstream out; std::stringstream out;
auto bar = [](uint16_t val) { return std::string(val / 512, '#'); }; auto bar = [](uint16_t val) { return std::string(val / 8191, '#'); };
if (drum.don_left.triggered || drum.ka_left.triggered || drum.don_right.triggered || drum.ka_right.triggered) { if (drum.don_left.triggered || drum.ka_left.triggered || drum.don_right.triggered || drum.ka_right.triggered) {
out << "(" << (drum.ka_left.triggered ? "*" : " ") << "( " // out << "(" << (drum.ka_left.triggered ? "*" : " ") << "( " //
<< std::setw(4) << drum.ka_left.raw << "[" << std::setw(8) << bar(drum.ka_left.raw) << "]" // << std::setw(5) << drum.ka_left.analog << "[" << std::setw(8) << bar(drum.ka_left.analog) << "]" //
<< "(" << (drum.don_left.triggered ? "*" : " ") << "| " // << "(" << (drum.don_left.triggered ? "*" : " ") << "| " //
<< std::setw(4) << drum.don_left.raw << "[" << std::setw(8) << bar(drum.don_left.raw) << "]" // << std::setw(5) << drum.don_left.analog << "[" << std::setw(8) << bar(drum.don_left.analog) << "]" //
<< "|" << (drum.don_right.triggered ? "*" : " ") << ") " // << "|" << (drum.don_right.triggered ? "*" : " ") << ") " //
<< std::setw(4) << drum.don_right.raw << "[" << std::setw(8) << bar(drum.don_right.raw) << "]" // << std::setw(5) << drum.don_right.analog << "[" << std::setw(8) << bar(drum.don_right.analog) << "]" //
<< ")" << (drum.ka_right.triggered ? "*" : " ") << ") " << std::setw(4) << drum.ka_right.raw << "[" // << ")" << (drum.ka_right.triggered ? "*" : " ") << ") " //
<< std::setw(8) << bar(drum.ka_right.raw) << "]" // << std::setw(5) << drum.ka_right.analog << "[" << std::setw(8) << bar(drum.ka_right.analog) << "]" //
<< "\n"; << "\n";
} }

View File

@ -25,6 +25,8 @@ const std::map<Menu::Page, const Menu::Descriptor> Menu::descriptors = {
{"Keybrd P1", Menu::Descriptor::Action::ChangeUsbModeKeyboardP1}, // {"Keybrd P1", Menu::Descriptor::Action::ChangeUsbModeKeyboardP1}, //
{"Keybrd P2", Menu::Descriptor::Action::ChangeUsbModeKeyboardP2}, // {"Keybrd P2", Menu::Descriptor::Action::ChangeUsbModeKeyboardP2}, //
{"Xbox 360", Menu::Descriptor::Action::ChangeUsbModeXbox360}, // {"Xbox 360", Menu::Descriptor::Action::ChangeUsbModeXbox360}, //
{"Analog P1", Menu::Descriptor::Action::ChangeUsbModeXbox360AnalogP1}, //
{"Analog P2", Menu::Descriptor::Action::ChangeUsbModeXbox360AnalogP2}, //
{"MIDI", Menu::Descriptor::Action::ChangeUsbModeMidi}, // {"MIDI", Menu::Descriptor::Action::ChangeUsbModeMidi}, //
{"Debug", Menu::Descriptor::Action::ChangeUsbModeDebug}}, // {"Debug", Menu::Descriptor::Action::ChangeUsbModeDebug}}, //
0}}, // 0}}, //
@ -287,6 +289,14 @@ void Menu::performSelectionAction(Menu::Descriptor::Action action) {
m_store->setUsbMode(USB_MODE_XBOX360); m_store->setUsbMode(USB_MODE_XBOX360);
gotoParent(); gotoParent();
break; break;
case Descriptor::Action::ChangeUsbModeXbox360AnalogP1:
m_store->setUsbMode(USB_MODE_XBOX360_ANALOG_P1);
gotoParent();
break;
case Descriptor::Action::ChangeUsbModeXbox360AnalogP2:
m_store->setUsbMode(USB_MODE_XBOX360_ANALOG_P2);
gotoParent();
break;
case Descriptor::Action::ChangeUsbModeMidi: case Descriptor::Action::ChangeUsbModeMidi:
m_store->setUsbMode(USB_MODE_MIDI); m_store->setUsbMode(USB_MODE_MIDI);
gotoParent(); gotoParent();