Skip to content

Instantly share code, notes, and snippets.

@Lauszus
Last active August 22, 2022 08:03
Show Gist options
  • Save Lauszus/88a9052045346530a2ab to your computer and use it in GitHub Desktop.
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
/*
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