Skip to content

Commit f22ad32

Browse files
committed
Add Logitech G27 shifter support
Extends the LogitechShifter class, and adds support for reading the shift registers connected to the shifter's additional buttons.
1 parent ea86a3b commit f22ad32

2 files changed

Lines changed: 386 additions & 0 deletions

File tree

src/SimRacing.cpp

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,6 +1060,231 @@ LogitechShifter::LogitechShifter(PinNum pinX, PinNum pinY, PinNum pinRev, PinNum
10601060
this->setCalibration({ 490, 440 }, { 253, 799 }, { 262, 86 }, { 460, 826 }, { 470, 76 }, { 664, 841 }, { 677, 77 });
10611061
}
10621062

1063+
1064+
LogitechShifterG27::LogitechShifterG27(
1065+
PinNum pinX, PinNum pinY,
1066+
PinNum pinLatch, PinNum pinClock, PinNum pinData,
1067+
PinNum pinDetect,
1068+
PinNum pinLed
1069+
) :
1070+
LogitechShifter(pinX, pinY, UnusedPin, pinDetect),
1071+
1072+
pinLatch(sanitizePin(pinLatch)), pinClock(sanitizePin(pinClock)), pinData(sanitizePin(pinData)),
1073+
pinLed(sanitizePin(pinLed))
1074+
{
1075+
this->pinModesSet = false;
1076+
this->buttonStates = this->previousButtons = 0x0000; // zero all button data
1077+
}
1078+
1079+
void LogitechShifterG27::cacheButtons(uint16_t newStates) {
1080+
this->previousButtons = this->buttonStates; // save current to previous
1081+
this->buttonStates = newStates; // replace current with new value
1082+
}
1083+
1084+
void LogitechShifterG27::setPinModes(bool enabled) {
1085+
// check if pins are valid. if one or more pins is unused,
1086+
// this isn't going to work and we shouldn't bother setting
1087+
// any of the pin states
1088+
if (
1089+
this->pinData == UnusedPin ||
1090+
this->pinLatch == UnusedPin ||
1091+
this->pinClock == UnusedPin)
1092+
{
1093+
return;
1094+
}
1095+
1096+
// set up data pin to read from regardless
1097+
pinMode(this->pinData, INPUT);
1098+
1099+
// enabled = drive the output pins
1100+
if (enabled) {
1101+
// note: writing the output before setting the
1102+
// pin mode so that we don't accidentally drive
1103+
// the wrong direction momentarily
1104+
1105+
// set latch pin as output, HIGH on idle
1106+
digitalWrite(this->pinLatch, HIGH);
1107+
pinMode(this->pinLatch, OUTPUT);
1108+
1109+
// set clock pin as output, LOW on idle
1110+
digitalWrite(this->pinClock, LOW);
1111+
pinMode(this->pinClock, OUTPUT);
1112+
1113+
// if we have an LED pin, set it to output and turn
1114+
// the LED on (active low)
1115+
if (this->pinLed != UnusedPin) {
1116+
digitalWrite(this->pinLed, LOW);
1117+
pinMode(this->pinLed, OUTPUT);
1118+
}
1119+
}
1120+
1121+
// disabled = leave output pins as high-z
1122+
else {
1123+
// note: setting the mode before writing the
1124+
// output for the same reason; changing in
1125+
// high-z mode is safer
1126+
1127+
// set latch pin as high impedance, with pull-up
1128+
pinMode(this->pinLatch, INPUT);
1129+
digitalWrite(this->pinLatch, HIGH);
1130+
1131+
// set clock pin as high impedance, no pull-up
1132+
pinMode(this->pinClock, INPUT);
1133+
digitalWrite(this->pinClock, LOW);
1134+
1135+
// if we have an LED pin, set it to input, LOW on idle
1136+
if (this->pinLed != UnusedPin) {
1137+
pinMode(this->pinLed, INPUT);
1138+
digitalWrite(this->pinLed, LOW);
1139+
}
1140+
}
1141+
1142+
this->pinModesSet = enabled;
1143+
}
1144+
1145+
uint16_t LogitechShifterG27::readShiftRegisters() {
1146+
// if the pin outputs are not set, quit (none pressed)
1147+
if (!this->pinModesSet) return 0x0000;
1148+
1149+
uint16_t data = 0x0000;
1150+
1151+
// pulse shift register latch from high to low to high, 12 us
1152+
// (this timing is *completely* arbitrary, but it's nice to have
1153+
// *some* delay so that much faster MCUs don't blow through it)
1154+
digitalWrite(this->pinLatch, LOW);
1155+
delayMicroseconds(12);
1156+
digitalWrite(this->pinLatch, HIGH);
1157+
delayMicroseconds(12);
1158+
1159+
// clock is pullsed from LOW to HIGH on every bit,
1160+
// and then left to idle low
1161+
for (int i = 0; i < 16; ++i) {
1162+
digitalWrite(this->pinClock, LOW);
1163+
const bool state = digitalRead(this->pinData);
1164+
if (state) data |= 1 << (15 - i); // store data in word, MSB-first
1165+
digitalWrite(this->pinClock, HIGH);
1166+
delayMicroseconds(6);
1167+
}
1168+
digitalWrite(this->pinClock, LOW);
1169+
1170+
return data;
1171+
}
1172+
1173+
void LogitechShifterG27::begin() {
1174+
// disable pin outputs. this sets the initial
1175+
// 'safe' state. the outputs will be enabled
1176+
// by the 'updateState(bool)' function when needed.
1177+
this->setPinModes(0);
1178+
1179+
// call the begin() class of the base, which will also
1180+
// poll 'update()' on our behalf
1181+
this->AnalogShifter::begin();
1182+
}
1183+
1184+
bool LogitechShifterG27::updateState(bool connected) {
1185+
bool changed = false;
1186+
1187+
// if we're connected, set the pin modes, read the
1188+
// shift registers, and cache the data
1189+
if (connected) {
1190+
if (!this->pinModesSet) {
1191+
this->setPinModes(1);
1192+
}
1193+
1194+
const uint16_t data = this->readShiftRegisters();
1195+
this->cacheButtons(data);
1196+
changed |= this->buttonsChanged();
1197+
}
1198+
1199+
// if we're *not* connected, reset the pin modes and
1200+
// set no buttons pressed
1201+
else {
1202+
if (this->pinModesSet) {
1203+
this->setPinModes(0);
1204+
}
1205+
1206+
this->cacheButtons(0x0000);
1207+
changed |= this->buttonsChanged();
1208+
}
1209+
1210+
// we also need to update the data for the analog shifter
1211+
changed |= AnalogShifter::updateState(connected);
1212+
1213+
return changed;
1214+
}
1215+
1216+
bool LogitechShifterG27::buttonsChanged() const {
1217+
return this->buttonStates != this->previousButtons;
1218+
}
1219+
1220+
bool LogitechShifterG27::getButton(Button button) const {
1221+
return this->extractButton(button, this->buttonStates);
1222+
}
1223+
1224+
bool LogitechShifterG27::getButtonChanged(Button button) const {
1225+
return this->getButton(button) != this->extractButton(button, this->previousButtons);
1226+
}
1227+
1228+
int LogitechShifterG27::getDpadAngle() const {
1229+
const Button pads[4] = {
1230+
DPAD_UP,
1231+
DPAD_RIGHT,
1232+
DPAD_DOWN,
1233+
DPAD_LEFT,
1234+
};
1235+
1236+
// combine pads to a bitfield (nybble)
1237+
uint8_t dpad = 0x00;
1238+
for (uint8_t i = 0; i < 4; ++i) {
1239+
dpad |= (this->getButton(pads[i]) << i);
1240+
}
1241+
1242+
// The hatswitch value is from 0-7 proceeding clockwise
1243+
// from top (0 is 'up', 1 is 'up + right', etc.). I don't
1244+
// know of a great way to do this, so have this naive
1245+
// lookup table with a built-in SOCD cleaner
1246+
1247+
// For this, simultaneous opposing cardinal directions
1248+
// are neutral (because this is presumably used for
1249+
// navigation only, and not fighting games. Probably).
1250+
1251+
// bitfield to hatswitch lookup table
1252+
const uint8_t hat_table[16] = {
1253+
8, // 0b0000, Unpressed
1254+
0, // 0b0001, Up
1255+
2, // 0b0010, Right
1256+
1, // 0b0011, Right + Up
1257+
4, // 0b0100, Down
1258+
8, // 0b0101, Down + Up (SOCD None)
1259+
3, // 0b0110, Down + Right
1260+
2, // 0b0111, Down + Right + Up (SOCD Right)
1261+
6, // 0b1000, Left
1262+
7, // 0b1001, Left + Up
1263+
8, // 0b1010, Left + Right (SOCD None)
1264+
0, // 0b1011, Left + Right + Up (SOCD Up)
1265+
5, // 0b1100, Left + Down
1266+
6, // 0b1101, Left + Down + Up (SOCD Left)
1267+
4, // 0b1110, Left + Down + Right (SOCD Down)
1268+
8, // 0b1111, Left + Down + Right + Up (SOCD None)
1269+
};
1270+
1271+
// multiply the 0-8 value by 45 to get it in degrees
1272+
int16_t angle = hat_table[dpad & 0x0F] * 45;
1273+
1274+
// edge case: if no buttons are pressed, the angle is '-1'
1275+
if (angle == 360) angle = -1;
1276+
1277+
return angle;
1278+
}
1279+
1280+
bool LogitechShifterG27::readReverseButton() {
1281+
// this virtual function is provided for the sake of the AnalogShifter base
1282+
// class, which can use this to get the button state from the shift register
1283+
// without needing to interface with the shift registers themselves
1284+
return this->getButton(BUTTON_REVERSE);
1285+
}
1286+
1287+
10631288
//#########################################################
10641289
// Handbrake #
10651290
//#########################################################

src/SimRacing.h

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,167 @@ namespace SimRacing {
915915
DeviceConnection detectObj; ///< detector instance for checking if the shifter is connected
916916
};
917917

918+
/**
919+
* @brief Interface with the Logitech G27 shifter
920+
* @ingroup Shifters
921+
*
922+
* The G27 shifter includes the same analog shifter as the Logitech Driving
923+
* Force shifter (implemented in the LogitechShifter class), as well as
924+
* a directional pad and eight buttons.
925+
*
926+
* @see https://en.wikipedia.org/wiki/Logitech_G27
927+
*/
928+
class LogitechShifterG27 : public LogitechShifter {
929+
public:
930+
/**
931+
* @brief Enumeration of button values
932+
*
933+
* Buttons 1-4 are the red buttons, from left to right. The directional
934+
* pad is read as four separate buttons. The black buttons use cardinal
935+
* directions.
936+
*
937+
* These values represent the bit offset from LSB. Data is read in
938+
* MSB first.
939+
*/
940+
enum Button : uint8_t {
941+
BUTTON_UNUSED1 = 15, ///< Unused shift register pin
942+
BUTTON_REVERSE = 14, ///< Reverse button (press down on the shifter)
943+
BUTTON_UNUSED2 = 13, ///< Unused shift register pin
944+
BUTTON_SEQUENTIAL = 12, ///< Sequential mode button (turn the dial counter-clockwise)
945+
BUTTON_3 = 11, ///< 3rd red button (mid right)
946+
BUTTON_2 = 10, ///< 2nd red button (mid left)
947+
BUTTON_4 = 9, ///< 4th red button (far right)
948+
BUTTON_1 = 8, ///< 1st red button (far left)
949+
BUTTON_NORTH = 7, ///< The top black button
950+
BUTTON_EAST = 6, ///< The right black button
951+
BUTTON_WEST = 5, ///< The left black button
952+
BUTTON_SOUTH = 4, ///< The bottom black button
953+
DPAD_RIGHT = 3, ///< Right button of the directional pad
954+
DPAD_LEFT = 2, ///< Left button of the directional pad
955+
DPAD_DOWN = 1, ///< Down button of the directional pad
956+
DPAD_UP = 0, ///< Top button of the directional pad
957+
};
958+
959+
/**
960+
* Class constructor
961+
*
962+
* @param pinX analog input pin for the X axis, DE-9 pin 4
963+
* @param pinY analog input pin for the Y axis, DE-9 pin 8
964+
* @param pinLatch digital output pin to pulse to latch data, DE-9 pin 3
965+
* @param pinClock digital output pin to pulse as a clock, DE-9 pin 7
966+
* @param pinData digital input pin to use for reading data, DE-9 pin 2
967+
* @param pinDetect the digital input pin for device detection, DE-9 pin 1.
968+
* Requires a pull-down resistor.
969+
* @param pinLed digital output pin to light the power LED on connection,
970+
* DE-9 pin 5. Requires a 100 Ohm series resistor.
971+
*/
972+
LogitechShifterG27(
973+
PinNum pinX, PinNum pinY,
974+
PinNum pinLatch, PinNum pinClock, PinNum pinData,
975+
PinNum pinDetect = UnusedPin,
976+
PinNum pinLed = UnusedPin
977+
);
978+
979+
/**
980+
* Initializes the hardware pins for reading the gear states and
981+
* the buttons.
982+
*/
983+
virtual void begin();
984+
985+
/**
986+
* Retrieve the state of a single button
987+
*
988+
* @param button The button to retrieve
989+
* @returns The state of the button
990+
*/
991+
bool getButton(Button button) const;
992+
993+
/**
994+
* Checks whether a button has changed between updates
995+
*
996+
* @param button The button to check
997+
* @returns 'true' if the button's state has changed, 'false' otherwise
998+
*/
999+
bool getButtonChanged(Button button) const;
1000+
1001+
/**
1002+
* Get the directional pad angle in degrees
1003+
*
1004+
* This is useful for using the directional pad as a "hatswitch", in USB
1005+
*
1006+
* @returns the directional pad value in degrees (0-360), or '-1' if no
1007+
* directional pad buttons are pressed
1008+
*/
1009+
int getDpadAngle() const;
1010+
1011+
/**
1012+
* Checks if any of the buttons have changed since the last update
1013+
*
1014+
* @returns 'true' if any buttons have changed state, 'false' otherwise
1015+
*/
1016+
bool buttonsChanged() const;
1017+
1018+
protected:
1019+
/** @copydoc Peripheral::updateState(bool) */
1020+
virtual bool updateState(bool connected);
1021+
1022+
private:
1023+
/**
1024+
* Extracts a button value from a given data word
1025+
*
1026+
* @param button The button to extract state for
1027+
* @param data Packed data word containing button states
1028+
*
1029+
* @returns The state of the button
1030+
*/
1031+
static bool extractButton(Button button, uint16_t data) {
1032+
// convert button to single bit with offset, and perform
1033+
// a bitwise 'AND' to get the bit value
1034+
return data & (1 << (uint8_t) button);
1035+
}
1036+
1037+
/**
1038+
* Store the current button data for reference and replace it with
1039+
* a new value
1040+
*
1041+
* @param newStates The new button states to store
1042+
*/
1043+
void cacheButtons(uint16_t newStates);
1044+
1045+
/**
1046+
* Set the pin modes for all pins
1047+
*
1048+
* @param enabled 'true' to set the pins to their active configuration,
1049+
* 'false' to set them to idle / safe
1050+
*/
1051+
void setPinModes(bool enabled);
1052+
1053+
/**
1054+
* Shift the button data out from the shift register
1055+
*
1056+
* @returns the 16-bit data from the shift registers
1057+
*/
1058+
uint16_t readShiftRegisters();
1059+
1060+
/** @copydoc AnalogShifter::readReverseButton() */
1061+
virtual bool readReverseButton();
1062+
1063+
// Pins for the shift register interface
1064+
PinNum pinLatch; ///< Pin to pulse to latch data, DE-9 pin 3
1065+
PinNum pinClock; ///< Pin to pulse as a clock, DE-9 pin 7
1066+
PinNum pinData; ///< Pin to use for reading data, DE-9 pin 2
1067+
1068+
// Generic I/O pins
1069+
PinNum pinLed; ///< Pin to light the power LED, DE-9 pin 5
1070+
1071+
// I/O state
1072+
bool pinModesSet; ///< Flag for whether the output pins are enabled / driven
1073+
1074+
// Button states
1075+
uint16_t buttonStates; ///< the state of the buttons, as a packed word (where 0 = unpressed and 1 = pressed)
1076+
uint16_t previousButtons; ///< the previous state of the buttons, for comparison
1077+
};
1078+
9181079

9191080
#if defined(__AVR_ATmega32U4__) || defined(SIM_RACING_DOXYGEN)
9201081
/**

0 commit comments

Comments
 (0)