Last active
August 22, 2022 08:03
-
-
Save Lauszus/88a9052045346530a2ab to your computer and use it in GitHub Desktop.
Code for controlling a RC car using a SteelSeries SRW-S1 Steering Wheel
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Code for controlling a RC car using a SteelSeries SRW-S1 Steering Wheel - developed by Kristian Lauszus | |
The PPM signal is connected to the trainer port of a RC transmitter which then sends the signal to the RC car | |
The SRWS1 library is part of the USB Host Shield library: https://github.com/felis/USB_Host_Shield_2.0 | |
PPM code is based on: https://code.google.com/archive/p/generate-ppm-signal | |
For more information visit my blog: http://blog.tkjelectronics.dk/ or | |
send me an e-mail: kristianl@tkjelectronics.com | |
*/ | |
#include <EEPROM.h> // Include the official EEPROM library | |
#include <SRWS1.h> // Include SRW-S1 driver | |
#include <SPI.h> // Include the SPI library needed by the USB Host Shield library | |
#define N_CHANNELS 4 // Set the number of PPM channels here - I set it to four as this is the minimum my RC transmitter can decode | |
#define PPM_FRAME_LEN (N_CHANNELS * 2000 + 6500) // The PPM frame length in us, seems to be the standard equation for calculating it | |
#define PPM_PULSE_LEN 300 // The PPM positive pulse length in us | |
// ESC and Servo min and max pulse length in us | |
#define ESC_MIN 1000 | |
#define ESC_MAX 2000 | |
#define ESC_MID ((ESC_MAX - ESC_MIN)/2 + ESC_MIN) | |
#define SERVO_MIN 1000 | |
#define SERVO_MAX 2000 | |
#define SERVO_MID ((SERVO_MAX - SERVO_MIN)/2 + SERVO_MIN) | |
#define MAGIC_VALUE 0xAA // Can be any 8-bit value and just used to see if the EEPROM has been set yet | |
USB Usb; | |
SRWS1 srw1(&Usb); | |
static const uint8_t signalPin = 13; // Set PPM signal output pin | |
static int8_t trim; // Used to trim the steering wheel | |
static volatile uint16_t ppm[N_CHANNELS]; // This array holds the servo values for the PPM signal | |
static uint8_t counterToUsScaleFactor; // Used to convert us values into values used for the counter | |
// These are used to read and write to the port registers - see http://www.arduino.cc/en/Reference/PortManipulation | |
// I do this to save processing power - see this page for more information: http://www.billporter.info/ready-set-oscillate-the-fastest-way-to-change-arduino-pins/ | |
static volatile uint8_t pinBitMask, *pinOutPort; | |
void setup() { | |
initPPM(); | |
Serial.begin(57600); | |
if (Usb.Init() == -1) { | |
Serial.print(F("\r\nOSC did not start")); | |
while (1); // Halt | |
} | |
uint8_t magicValue; | |
EEPROM.get(0, magicValue); | |
if (magicValue == MAGIC_VALUE) | |
EEPROM.get(1, trim); // Get trim value from the EEPROM | |
else { // Reset values to their default value | |
magicValue = MAGIC_VALUE; | |
trim = 0; | |
EEPROM.put(0, magicValue); | |
EEPROM.put(1, trim); | |
} | |
Serial.println(F("\r\nSender started")); | |
} | |
void initPPM(void) { | |
for (uint8_t i = 0; i < N_CHANNELS; i++) | |
ppm[i] = SERVO_MID; // Initiallize default PPM values | |
pinMode(signalPin, OUTPUT); | |
pinBitMask = digitalPinToBitMask(signalPin); | |
pinOutPort = portOutputRegister(digitalPinToPort(signalPin)); | |
*pinOutPort &= ~pinBitMask; // Set the PPM signal pin to the default state (off) | |
// Please read: http://maxembedded.com/2011/07/avr-timers-ctc-mode | |
cli(); // Disable interrupts | |
TCCR1A = 0; // Set entire TCCR1A register to 0 | |
TCCR1B = (1 << WGM12) | (1 << CS11); // Turn on Clear Timer on Compare (CTC) mode with a prescaler equal to 8: 0.5 us at 16 MHz, 1 us at 8 MHz etc | |
counterToUsScaleFactor = (F_CPU / 8) / 1e6; // Used to convert us values into counter units | |
OCR1A = 0; // Timer compare value - will be set in the ISR routine | |
TIMSK1 |= (1 << OCIE1A); // Enable timer compare interrupt | |
sei(); // Enable interrupts | |
} | |
void updatePPM(uint16_t throttle, uint16_t steering) { | |
steering = constrain(steering + trim * (SERVO_MAX - SERVO_MIN) / 180, SERVO_MIN, SERVO_MAX); // Apply trim value | |
#if 1 | |
ppm[0] = throttle; | |
ppm[1] = steering; | |
#else | |
Serial.print(throttle); | |
Serial.write(','); | |
Serial.println(steering); | |
#endif | |
} | |
// Make lights go back and forth | |
void strobeLight(void) { | |
static uint32_t timer; | |
uint32_t now = millis(); | |
if (now - timer > 12) { | |
timer = now; // Reset timer | |
static uint16_t leds = 0; | |
/*D_PrintHex<uint16_t > (leds, 0x80); | |
Serial.println();*/ | |
srw1.setLeds(leds); // Update LEDs | |
static bool dirUp = true; | |
if (dirUp) { | |
leds <<= 1; | |
if (leds == 0x8000) // All are actually turned off, as there is only 15 LEDs | |
dirUp = false; // If we have reached the end i.e. all LEDs are off, then change direction | |
else if (!(leds & 0x8000)) // If last bit is not set, then set the lowest bit | |
leds |= 1; // Set lowest bit | |
} else { | |
leds >>= 1; | |
if (leds == 0) // Check if all LEDs are off | |
dirUp = true; // If all LEDs are off, then repeat the sequence | |
else if (!(leds & 0x1)) // If last bit is not set, then set the top bit | |
leds |= 1 << 15; // Set top bit | |
} | |
} | |
} | |
void loop() { | |
Usb.Task(); | |
if (srw1.connected()) { | |
static bool running = false; | |
if (srw1.buttonClickState.select) { | |
srw1.buttonClickState.select = 0; // Clear event | |
running = !running; | |
} | |
// Trim steering wheel | |
static uint8_t oldDpad; | |
if (srw1.srws1Data.btn.dpad == DPAD_RIGHT && srw1.srws1Data.btn.dpad != oldDpad && trim < 90) { | |
trim--; | |
EEPROM.put(1, trim); // Write value to EEPROM | |
} else if (srw1.srws1Data.btn.dpad == DPAD_LEFT && srw1.srws1Data.btn.dpad != oldDpad && trim > -90) { | |
trim++; | |
EEPROM.put(1, trim); // Write value to EEPROM | |
} else if (srw1.buttonClickState.lights) { | |
srw1.buttonClickState.lights = 0; // Clear event | |
trim = 0; // Reset trim value | |
EEPROM.put(1, trim); // Write value to EEPROM | |
} | |
oldDpad = srw1.srws1Data.btn.dpad; | |
uint32_t now = millis(); | |
if (running) { | |
srw1.setLeds(1 << map(srw1.srws1Data.tilt, -1800, 1800, 0, 14)); // Turn on a LED according to tilt value | |
static uint32_t timer; | |
if (now - timer > 100) { // Limit output data to 100 ms | |
timer = now; // Reset timer | |
static float sensitivity = 1.0f; // Used to adjust the sensitivity of the throttle | |
if (srw1.buttonClickState.rightGear && sensitivity < 1) { | |
srw1.buttonClickState.rightGear = 0; // Clear event | |
sensitivity += 0.1f; | |
sensitivity = constrain(sensitivity, 0.0f, 1.0f); // Due to the nature of floating point which might just be very close to 1 this small fix is needed to make sure that it does not exceed 1 | |
} else if (srw1.buttonClickState.leftGear && sensitivity > 0) { | |
srw1.buttonClickState.leftGear = 0; // Clear event | |
sensitivity -= 0.1f; | |
sensitivity = constrain(sensitivity, 0.0f, 1.0f); // Similar reason as above, but just make sure that is it not negative | |
} | |
int16_t tilt = constrain(srw1.srws1Data.tilt * (srw1.srws1Data.assistValues + 1), -1800, 1800); // Scale sterring sensitivity based on the assist values knob | |
uint16_t steering = map(tilt, -1800, 1800, SERVO_MIN, SERVO_MAX); // Convert tilt into servo values | |
uint16_t throttle = ESC_MID; | |
if (srw1.srws1Data.rightTrigger) | |
throttle = map(srw1.srws1Data.rightTrigger * sensitivity, 0, 1023, ESC_MID, ESC_MAX); | |
else if (srw1.srws1Data.leftTrigger) | |
throttle = map(srw1.srws1Data.leftTrigger * sensitivity, 0, 1023, ESC_MID, ESC_MIN); | |
updatePPM(throttle, steering); // Update throttle and steering commands | |
} | |
} else { | |
strobeLight(); // Turn on strobe light effect using the 15 LEDs | |
static uint32_t timer; | |
if (now - timer > 100) { // Limit output data to 100 ms | |
timer = now; // Reset timer | |
updatePPM(ESC_MID, SERVO_MID); // Stop motor and center steering servo | |
} | |
} | |
} | |
} | |
// This interrupt routine generates the PPM signal | |
ISR(TIMER1_COMPA_vect) { | |
static boolean startPulse = true; | |
if (startPulse) { | |
*pinOutPort |= pinBitMask; // Turn pin on | |
startPulse = false; | |
OCR1A = PPM_PULSE_LEN * counterToUsScaleFactor; // Call interrupt again after PPM_PULSE_LEN has passed | |
} | |
else { // End pulse and calculate when to start the next pulse | |
static uint8_t currentChannel; // Current channel number | |
static uint16_t remainingPulseLength; // This is time remaining to ensure that the total PPM frame has the right length | |
*pinOutPort &= ~pinBitMask; // Turn pin off | |
startPulse = true; | |
if (currentChannel >= N_CHANNELS) { | |
currentChannel = 0; | |
remainingPulseLength += PPM_PULSE_LEN; | |
OCR1A = (PPM_FRAME_LEN - remainingPulseLength) * counterToUsScaleFactor; // Call interrupt after the entire frame has been sent | |
remainingPulseLength = 0; | |
} else { | |
OCR1A = (ppm[currentChannel] - PPM_PULSE_LEN) * counterToUsScaleFactor; // Call interrupt again when it should send out the start pulse again | |
remainingPulseLength += ppm[currentChannel]; | |
currentChannel++; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment