Advertisement
LandoRo

ESP32-s3 USB-HID JoyStick 4 Axis Pedals

Jul 6th, 2025
457
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C 16.47 KB | Gaming | 0 0
  1. #include <Joystick_ESP32S2.h>
  2. #include <SPI.h>
  3. #include <ADS1220_WE.h>
  4. #include <Wire.h>
  5. #include <Adafruit_GFX.h>
  6. #include <Adafruit_SSD1306.h>
  7. #include <EEPROM.h>
  8.  
  9. #define ADS1220_CS_PIN 10      // ADS1220 Chip Select
  10. #define ADS1220_DRDY_PIN 9     // ADS1220 Data Ready
  11. #define SPI_SCK 13             // SPI SCLK
  12. #define SPI_MISO 12            // SPI MISO
  13. #define SPI_MOSI 11            // SPI MOSI
  14. #define OLED_SDA 4             // OLED I2C SDA
  15. #define OLED_SCL 5             // OLED I2C SCL
  16. #define BUTTON_PREV 6          // Previous axis button
  17. #define BUTTON_NEXT_PAGE 7     // Next page button
  18. #define BUTTON_CALIBRATE 2     // Calibrate/filter button
  19. #define BUTTON_DEFAULT 3       // Restore default button
  20.  
  21. #define SCREEN_WIDTH 128       // OLED width
  22. #define SCREEN_HEIGHT 64       // OLED height
  23. #define OLED_ADDRESS 0x3C      // OLED I2C address
  24. #define CALIBRATION_TIME 4000  // 4 seconds per min/max
  25. #define DONE_TIME 1000         // 1 second for "DONE"
  26. #define DEBOUNCE_TIME 50       // 50 ms debounce
  27. #define OLED_REFRESH_MS 500    // OLED update every 500 ms (2 Hz)
  28. #define DEADBAND_COUNTS 3355   // 2 mV equivalent (2/5000 * 2^23)
  29. #define MAX_SAMPLES 20         // Max averaging samples
  30.  
  31. #define EEPROM_SIZE 64         // Allocate 64 bytes for EEPROM
  32. #define EEPROM_MAGIC 0x12345678 // Magic number for EEPROM validation
  33. #define EEPROM_MAGIC_ADDR 0
  34. #define EEPROM_MIN_CLUTCH_ADDR 4
  35. #define EEPROM_MAX_CLUTCH_ADDR 8
  36. #define EEPROM_MIN_BRAKE_ADDR 12
  37. #define EEPROM_MAX_BRAKE_ADDR 16
  38. #define EEPROM_MIN_GAS_ADDR 20
  39. #define EEPROM_MAX_GAS_ADDR 24
  40. #define EEPROM_MIN_HB_ADDR 28
  41. #define EEPROM_MAX_HB_ADDR 32
  42. #define EEPROM_SAMPLES_CLUTCH_ADDR 36
  43. #define EEPROM_SAMPLES_BRAKE_ADDR 40
  44. #define EEPROM_SAMPLES_GAS_ADDR 44
  45. #define EEPROM_SAMPLES_HB_ADDR 48
  46.  
  47. Joystick_ joystick(JOYSTICK_DEFAULT_REPORT_ID, JOYSTICK_TYPE_JOYSTICK,
  48.                   0, 0, true, true, true, true, false, false, false, false);
  49. ADS1220_WE ads(ADS1220_CS_PIN, ADS1220_DRDY_PIN);
  50. Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
  51.  
  52. // Calibration state
  53. enum CalibState { IDLE, MIN_CALIB, MAX_CALIB, MIN_DONE, MAX_DONE };
  54. volatile CalibState calibState = IDLE;
  55. volatile int selectedAxis = 0; // 0=C, 1=B, 2=G, 3=H
  56. volatile int calibAxis = -1;   // Axis being calibrated
  57. volatile unsigned long calibStartTime = 0;
  58. volatile int currentScreen = 0; // 0=Filter Adjust, 1=Min/Max Calibration
  59.  
  60. // Button states for debouncing
  61. volatile bool lastPrevState = HIGH;
  62. volatile bool lastNextPageState = HIGH;
  63. volatile bool lastCalibrateState = HIGH;
  64. volatile bool lastDefaultState = HIGH;
  65. volatile unsigned long lastButtonCheck = 0;
  66.  
  67. // Min/max values (defaults: 0 to 2^23-1)
  68. volatile int32_t minClutch = 0, maxClutch = 8388607;
  69. volatile int32_t minBrake = 0, maxBrake = 8388607;
  70. volatile int32_t minGas = 0, maxGas = 8388607;
  71. volatile int32_t minHB = 0, maxHB = 8388607;
  72. volatile int32_t currentMin, currentMax;
  73.  
  74. // Averaging samples (default: 2)
  75. volatile uint8_t samplesClutch = 2, samplesBrake = 2, samplesGas = 2, samplesHB = 2;
  76.  
  77. // ADC values (shared with Core 0)
  78. volatile int32_t valueClutch = 0, valueBrake = 0, valueGas = 0, valueHB = 0;
  79. // Last output values for deadband
  80. volatile int32_t lastValueClutch = 0, lastValueBrake = 0, lastValueGas = 0, lastValueHB = 0;
  81.  
  82. // Circular buffer for ADC samples
  83. volatile int32_t sampleBuffer[4][MAX_SAMPLES]; // Buffer for C, B, G, H
  84. volatile uint8_t sampleCount[4] = {0, 0, 0, 0}; // Sample counts per channel
  85. volatile uint8_t currentChannel = 0; // Current ADC channel (0=C, 1=B, 2=G, 3=H)
  86. volatile bool dataReady = false;
  87.  
  88. // DRDY interrupt
  89. void IRAM_ATTR drdyISR() {
  90.   if (dataReady) return; // Prevent re-entry
  91.   dataReady = true;
  92.   int32_t data = ads.getRawData();
  93.   sampleBuffer[currentChannel][sampleCount[currentChannel]] = data;
  94.   sampleCount[currentChannel]++;
  95.   if (sampleCount[currentChannel] >= (currentChannel == 0 ? samplesClutch : currentChannel == 1 ? samplesBrake : currentChannel == 2 ? samplesGas : samplesHB)) {
  96.     sampleCount[currentChannel] = 0; // Reset when buffer is full
  97.   }
  98.   currentChannel = (currentChannel + 1) % 4; // Cycle through channels
  99.   ads.setCompareChannels(currentChannel == 0 ? ADS1220_MUX_0_AVSS : currentChannel == 1 ? ADS1220_MUX_1_AVSS : currentChannel == 2 ? ADS1220_MUX_2_AVSS : ADS1220_MUX_3_AVSS);
  100. }
  101.  
  102. // Integer-based mapping
  103. long customMap(int32_t x, int32_t in_min, int32_t in_max, long out_min, long out_max) {
  104.   int64_t result = (int64_t)(x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
  105.   if (result < out_min) return out_min;
  106.   if (result > out_max) return out_max;
  107.   return (long)result;
  108. }
  109.  
  110. // PSI calculation for BRAKE (0.5V=0 PSI, 4.5V=100 PSI)
  111. float calculatePSIBrake(int32_t valueBrake) {
  112.   float psi = (float)(valueBrake - 839681) * 100.0 / (7951127 - 839681);
  113.   return constrain(psi, 0.0, 100.0);
  114. }
  115.  
  116. void saveCalibration() {
  117.   EEPROM.put(EEPROM_MIN_CLUTCH_ADDR, minClutch);
  118.   EEPROM.put(EEPROM_MAX_CLUTCH_ADDR, maxClutch);
  119.   EEPROM.put(EEPROM_MIN_BRAKE_ADDR, minBrake);
  120.   EEPROM.put(EEPROM_MAX_BRAKE_ADDR, maxBrake);
  121.   EEPROM.put(EEPROM_MIN_GAS_ADDR, minGas);
  122.   EEPROM.put(EEPROM_MAX_GAS_ADDR, maxGas);
  123.   EEPROM.put(EEPROM_MIN_HB_ADDR, minHB);
  124.   EEPROM.put(EEPROM_MAX_HB_ADDR, maxHB);
  125.   EEPROM.put(EEPROM_SAMPLES_CLUTCH_ADDR, samplesClutch);
  126.   EEPROM.put(EEPROM_SAMPLES_BRAKE_ADDR, samplesBrake);
  127.   EEPROM.put(EEPROM_SAMPLES_GAS_ADDR, samplesGas);
  128.   EEPROM.put(EEPROM_SAMPLES_HB_ADDR, samplesHB);
  129.   EEPROM.commit();
  130. }
  131.  
  132. void loadCalibration() {
  133.   uint32_t magic;
  134.   EEPROM.get(EEPROM_MAGIC_ADDR, magic);
  135.   if (magic != EEPROM_MAGIC) {
  136.     saveCalibration();
  137.     EEPROM.put(EEPROM_MAGIC_ADDR, EEPROM_MAGIC);
  138.     EEPROM.commit();
  139.   } else {
  140.     EEPROM.get(EEPROM_MIN_CLUTCH_ADDR, minClutch);
  141.     EEPROM.get(EEPROM_MAX_CLUTCH_ADDR, maxClutch);
  142.     EEPROM.get(EEPROM_MIN_BRAKE_ADDR, minBrake);
  143.     EEPROM.get(EEPROM_MAX_BRAKE_ADDR, maxBrake);
  144.     EEPROM.get(EEPROM_MIN_GAS_ADDR, minGas);
  145.     EEPROM.get(EEPROM_MAX_GAS_ADDR, maxGas);
  146.     EEPROM.get(EEPROM_MIN_HB_ADDR, minHB);
  147.     EEPROM.get(EEPROM_MAX_HB_ADDR, maxHB);
  148.     EEPROM.get(EEPROM_SAMPLES_CLUTCH_ADDR, samplesClutch);
  149.     EEPROM.get(EEPROM_SAMPLES_BRAKE_ADDR, samplesBrake);
  150.     EEPROM.get(EEPROM_SAMPLES_GAS_ADDR, samplesGas);
  151.     EEPROM.get(EEPROM_SAMPLES_HB_ADDR, samplesHB);
  152.     samplesClutch = constrain(samplesClutch, 1, MAX_SAMPLES);
  153.     samplesBrake = constrain(samplesBrake, 1, MAX_SAMPLES);
  154.     samplesGas = constrain(samplesGas, 1, MAX_SAMPLES);
  155.     samplesHB = constrain(samplesHB, 1, MAX_SAMPLES);
  156.   }
  157. }
  158.  
  159. void resetDefaultCalibration(int axis) {
  160.   if (axis == 0) { minClutch = 0; maxClutch = 8388607; samplesClutch = 2; }
  161.   else if (axis == 1) { minBrake = 0; maxBrake = 8388607; samplesBrake = 2; }
  162.   else if (axis == 2) { minGas = 0; maxGas = 8388607; samplesGas = 2; }
  163.   else { minHB = 0; maxHB = 8388607; samplesHB = 2; }
  164.   saveCalibration();
  165. }
  166.  
  167. // Core 0 task for buttons, calibration, and OLED
  168. void displayAndControlTask(void *pvParameters) {
  169.   unsigned long lastDisplayUpdate = 0;
  170.   while (1) {
  171.     unsigned long currentMillis = millis();
  172.  
  173.     // Read buttons with debouncing
  174.     if (currentMillis - lastButtonCheck >= DEBOUNCE_TIME && calibState == IDLE) {
  175.       bool prevState = digitalRead(BUTTON_PREV);
  176.       bool nextPageState = digitalRead(BUTTON_NEXT_PAGE);
  177.       bool calibrateState = digitalRead(BUTTON_CALIBRATE);
  178.       bool defaultState = digitalRead(BUTTON_DEFAULT);
  179.  
  180.       if (prevState == LOW && lastPrevState == HIGH) {
  181.         selectedAxis = (selectedAxis - 1 + 4) % 4;
  182.         while (digitalRead(BUTTON_PREV) == LOW && millis() - currentMillis < 1000);
  183.       }
  184.       lastPrevState = prevState;
  185.  
  186.       if (nextPageState == LOW && lastNextPageState == HIGH) {
  187.         currentScreen = (currentScreen + 1) % 2;
  188.         while (digitalRead(BUTTON_NEXT_PAGE) == LOW && millis() - currentMillis < 1000);
  189.       }
  190.       lastNextPageState = nextPageState;
  191.  
  192.       if (calibrateState == LOW && lastCalibrateState == HIGH) {
  193.         if (currentScreen == 0) {
  194.           if (selectedAxis == 0) samplesClutch = (samplesClutch % MAX_SAMPLES) + 1;
  195.           else if (selectedAxis == 1) samplesBrake = (samplesBrake % MAX_SAMPLES) + 1;
  196.           else if (selectedAxis == 2) samplesGas = (samplesGas % MAX_SAMPLES) + 1;
  197.           else samplesHB = (samplesHB % MAX_SAMPLES) + 1;
  198.           saveCalibration();
  199.         } else {
  200.           calibState = MIN_CALIB;
  201.           calibAxis = selectedAxis;
  202.           calibStartTime = currentMillis;
  203.           currentMin = 8388608;
  204.           currentMax = -8388608;
  205.         }
  206.       }
  207.       lastCalibrateState = calibrateState;
  208.  
  209.       if (defaultState == LOW && lastDefaultState == HIGH) {
  210.         resetDefaultCalibration(selectedAxis);
  211.       }
  212.       lastDefaultState = defaultState;
  213.  
  214.       lastButtonCheck = currentMillis;
  215.     }
  216.  
  217.     // Update calibration (Screen 2 only)
  218.     if (calibState != IDLE && calibAxis >= 0 && calibAxis < 4) {
  219.       int32_t value = (calibAxis == 0) ? valueClutch : (calibAxis == 1) ? valueBrake : (calibAxis == 2) ? valueGas : valueHB;
  220.       if (calibState == MIN_CALIB) {
  221.         if (value < currentMin) currentMin = value;
  222.         if (currentMillis - calibStartTime >= CALIBRATION_TIME) {
  223.           calibState = MIN_DONE;
  224.           calibStartTime = currentMillis;
  225.         }
  226.       } else if (calibState == MIN_DONE && currentMillis - calibStartTime >= DONE_TIME) {
  227.         if (calibAxis == 0) minClutch = currentMin;
  228.         else if (calibAxis == 1) minBrake = currentMin;
  229.         else if (calibAxis == 2) minGas = currentMin;
  230.         else minHB = currentMin;
  231.         calibState = MAX_CALIB;
  232.         calibStartTime = currentMillis;
  233.       } else if (calibState == MAX_CALIB) {
  234.         if (value > currentMax) currentMax = value;
  235.         if (currentMillis - calibStartTime >= CALIBRATION_TIME) {
  236.           calibState = MAX_DONE;
  237.           calibStartTime = currentMillis;
  238.         }
  239.       } else if (calibState == MAX_DONE && currentMillis - calibStartTime >= DONE_TIME) {
  240.         if (calibAxis == 0) maxClutch = currentMax;
  241.         else if (calibAxis == 1) maxBrake = currentMax;
  242.         else if (calibAxis == 2) maxGas = currentMax;
  243.         else maxHB = currentMax;
  244.         saveCalibration();
  245.         calibState = IDLE;
  246.         calibAxis = -1;
  247.       }
  248.     }
  249.  
  250.     // Update OLED
  251.     if (currentMillis - lastDisplayUpdate >= OLED_REFRESH_MS) {
  252.       display.clearDisplay();
  253.       display.setCursor(0, 0);
  254.       if (calibState == MIN_CALIB) {
  255.         display.print(calibAxis == 0 ? "C" : calibAxis == 1 ? "B" : calibAxis == 2 ? "G" : "H");
  256.         display.print(" MIN: ");
  257.         display.println((calibAxis == 0) ? valueClutch : (calibAxis == 1) ? valueBrake : (calibAxis == 2) ? valueGas : valueHB);
  258.         display.print("Time: ");
  259.         float remaining = (CALIBRATION_TIME - (currentMillis - calibStartTime)) / 1000.0;
  260.         display.print(remaining, 1);
  261.         display.println("s");
  262.       } else if (calibState == MIN_DONE) {
  263.         display.print(calibAxis == 0 ? "C" : calibAxis == 1 ? "B" : calibAxis == 2 ? "G" : "H");
  264.         display.print(" MIN: ");
  265.         display.print(currentMin);
  266.         display.println(" DONE");
  267.       } else if (calibState == MAX_CALIB) {
  268.         display.print(calibAxis == 0 ? "C" : calibAxis == 1 ? "B" : calibAxis == 2 ? "G" : "H");
  269.         display.print(" MAX: ");
  270.         display.println((calibAxis == 0) ? valueClutch : (calibAxis == 1) ? valueBrake : (calibAxis == 2) ? valueGas : valueHB);
  271.         display.print("Time: ");
  272.         float remaining = (CALIBRATION_TIME - (currentMillis - calibStartTime)) / 1000.0;
  273.         display.print(remaining, 1);
  274.         display.println("s");
  275.       } else if (calibState == MAX_DONE) {
  276.         display.print(calibAxis == 0 ? "C" : calibAxis == 1 ? "B" : calibAxis == 2 ? "G" : "H");
  277.         display.print(" MAX: ");
  278.         display.print(currentMax);
  279.         display.println(" DONE");
  280.       } else if (currentScreen == 0) {
  281.         display.println("Filter Settings");
  282.         display.print(selectedAxis == 0 ? ">C: " : " C: "); display.println(samplesClutch);
  283.         display.print(selectedAxis == 1 ? ">B: " : " B: "); display.println(samplesBrake);
  284.         display.print(selectedAxis == 2 ? ">G: " : " G: "); display.println(samplesGas);
  285.         display.print(selectedAxis == 3 ? ">H: " : " H: "); display.println(samplesHB);
  286.       } else {
  287.         display.println("Pedals Min/Max");
  288.         display.print(selectedAxis == 0 ? ">C: " : " C: "); display.print(minClutch); display.print(" "); display.println(maxClutch);
  289.         display.print(selectedAxis == 1 ? ">B: " : " B: "); display.print(minBrake); display.print(" "); display.println(maxBrake);
  290.         display.print(selectedAxis == 2 ? ">G: " : " G: "); display.print(minGas); display.print(" "); display.println(maxGas);
  291.         display.print(selectedAxis == 3 ? ">H: " : " H: "); display.print(minHB); display.print(" "); display.println(maxHB);
  292.         display.print("PSI B: "); display.println(calculatePSIBrake(valueBrake), 1);
  293.       }
  294.       display.display();
  295.       lastDisplayUpdate = currentMillis;
  296.     }
  297.  
  298.     vTaskDelay(20 / portTICK_PERIOD_MS); // Yield every 20 ms
  299.   }
  300. }
  301.  
  302. void setup() {
  303.   joystick.begin();
  304.   joystick.setXAxisRange(0, 65535);
  305.   joystick.setYAxisRange(0, 65535);
  306.   joystick.setZAxisRange(0, 65535);
  307.   joystick.setRxAxisRange(0, 65535);
  308.  
  309.   EEPROM.begin(EEPROM_SIZE);
  310.   loadCalibration();
  311.  
  312.   pinMode(BUTTON_PREV, INPUT_PULLUP);
  313.   pinMode(BUTTON_NEXT_PAGE, INPUT_PULLUP);
  314.   pinMode(BUTTON_CALIBRATE, INPUT_PULLUP);
  315.   pinMode(BUTTON_DEFAULT, INPUT_PULLUP);
  316.  
  317.   SPI.begin(SPI_SCK, SPI_MISO, SPI_MOSI, ADS1220_CS_PIN);
  318.   if (!ads.init()) {
  319.     while (1);
  320.   }
  321.   ads.setSPIClockSpeed(10000000);
  322.   ads.setVRefSource(ADS1220_VREF_REFP0_REFN0);
  323.   ads.setVRefValue_V(5.0);
  324.   ads.setGain(ADS1220_GAIN_1);
  325.   ads.bypassPGA(true);
  326.   ads.setDataRate(ADS1220_DR_LVL_6); // 1200 SPS
  327.   ads.setOperatingMode(ADS1220_TURBO_MODE);
  328.   ads.setConversionMode(ADS1220_CONTINUOUS);
  329.   ads.setFIRFilter(ADS1220_50HZ_60HZ);
  330.  
  331.   // Initialize first channel
  332.   ads.setCompareChannels(ADS1220_MUX_0_AVSS); // Start with CLUTCH
  333.   pinMode(ADS1220_DRDY_PIN, INPUT);
  334.   attachInterrupt(digitalPinToInterrupt(ADS1220_DRDY_PIN), drdyISR, FALLING);
  335.  
  336.   Wire.begin(OLED_SDA, OLED_SCL);
  337.   Wire.setClock(400000);
  338.   if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
  339.     while (1);
  340.   }
  341.   display.clearDisplay();
  342.   display.setTextSize(1);
  343.   display.setTextColor(SSD1306_WHITE);
  344.   display.setCursor(0, 0);
  345.   display.println("Pedals Calibration");
  346.   display.display();
  347.  
  348.   xTaskCreatePinnedToCore(
  349.     displayAndControlTask, "DisplayControlTask", 4096, NULL, 1, NULL, 0
  350.   );
  351. }
  352.  
  353. void loop() {
  354.   static bool toggle = false;
  355.   unsigned long currentMillis = millis();
  356.   if (!dataReady) return; // Process only when new data
  357.   dataReady = false;
  358.  
  359.   // Compute averages
  360.   int32_t sumClutch = 0, sumBrake = 0, sumGas = 0, sumHB = 0;
  361.   for (int i = 0; i < samplesClutch; i++) sumClutch += sampleBuffer[0][i];
  362.   for (int i = 0; i < samplesBrake; i++) sumBrake += sampleBuffer[1][i];
  363.   for (int i = 0; i < samplesGas; i++) sumGas += sampleBuffer[2][i];
  364.   for (int i = 0; i < samplesHB; i++) sumHB += sampleBuffer[3][i];
  365.  
  366.   int32_t newValueClutch = samplesClutch > 0 ? sumClutch / samplesClutch : sumClutch;
  367.   int32_t newValueBrake = samplesBrake > 0 ? sumBrake / samplesBrake : sumBrake;
  368.   int32_t newValueGas = samplesGas > 0 ? sumGas / samplesGas : sumGas;
  369.   int32_t newValueHB = samplesHB > 0 ? sumHB / samplesHB : sumHB;
  370.  
  371.   // Apply 2 mV deadband filter
  372.   if (abs(newValueClutch - lastValueClutch) >= DEADBAND_COUNTS) {
  373.     valueClutch = newValueClutch;
  374.     lastValueClutch = newValueClutch;
  375.   }
  376.   if (abs(newValueBrake - lastValueBrake) >= DEADBAND_COUNTS) {
  377.     valueBrake = newValueBrake;
  378.     lastValueBrake = newValueBrake;
  379.   }
  380.   if (abs(newValueGas - lastValueGas) >= DEADBAND_COUNTS) {
  381.     valueGas = newValueGas;
  382.     lastValueGas = newValueGas;
  383.   }
  384.   if (abs(newValueHB - lastValueHB) >= DEADBAND_COUNTS) {
  385.     valueHB = newValueHB;
  386.     lastValueHB = newValueHB;
  387.   }
  388.  
  389.   // Map to 0–65535
  390.   int mappedClutch = customMap(valueClutch, minClutch, maxClutch, 0, 65535);
  391.   int mappedBrake = customMap(valueBrake, minBrake, maxBrake, 0, 65535);
  392.   int mappedGas = customMap(valueGas, minGas, maxGas, 0, 65535);
  393.   int mappedHB = customMap(valueHB, minHB, maxHB, 0, 65535);
  394.  
  395.   // Send joystick data every 2nd loop
  396.   joystick.setXAxis(mappedClutch);
  397.   joystick.setYAxis(mappedBrake);
  398.   joystick.setZAxis(mappedGas);
  399.   joystick.setRxAxis(mappedHB);
  400.   if (toggle) joystick.sendState();
  401.   toggle = !toggle;
  402. }
Tags: DIY sim racing
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement