Pelco KBD300A to Kerbal space program controller (part 3, code to exercise the hardware)

In the first two entries we talked about the hardware to be interfaced to and how I decided to accomplish that. Now we get to deal with the consequences of making those choices easy to interface with. My code is in the same github repository as the board designs. Let’s jump right in with the 7-segment displays. 

// called this way, it uses the default address 0x40

Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();

I’m using the PCA9685 which I originally started using on breakout boards from adafruit marketed for servo control. It’s a reasonable use case as they also need PWM signals to set the position and this chip lets you ‘set and forget’ the pwm state of each pin without having to update it every cycle. 

  pwm.begin();

pwm.setOscillatorFrequency(27000000);
pwm.setPWMFreq(1600); // This is the maximum PWM frequency

// if you want to really speed stuff up, you can go into 'fast 400khz I2C' mode
// some i2c devices dont like this so much so if you're sharing the bus, watch
// out for this!
Wire.setClock(400000);

The initialization is no big deal, for LEDs the high pwm rate is perfect and the oscillator frequency doesn’t need to be tuned to match anything in particular. I also set the i2c to run pretty fast as everything can handle it. 

void displayChar(int index, char charachter, uint16_t brightness = 4095) {

byte firstSsegmentPins[] = { 7, 8, 9, 10, 11, 13, 12 }; //a,b,c,d,e,f,g
byte secondSsegmentPins[] = { 0, 1, 2, 3, 4, 6, 5 }; //a,b,c,d,e,f,g
uint8_t toPrint = SevenSegmentASCII[charachter - 0x20];
for (int i = 0; i < 7; i++) {
if (index == 0) {
if ((toPrint & (1 << i)) > 0) {
pwm.setPWM(firstSsegmentPins[i], 0, brightness);

} else {
pwm.setPWM(firstSsegmentPins[i], 0, 0);
}
} else if (index == 1) {
if ((toPrint & (1 << i)) > 0) {
pwm.setPWM(secondSsegmentPins[i], 0, brightness);

} else {
pwm.setPWM(secondSsegmentPins[i], 0, 0);
}
} else {
Serial.println("error");
}
}
}

OK, this is the meat of getting digits displayed. With this function I can manually set which digit I want to change (remember, these are persistent as they are managed by the standalone i2c LED driver) and its brightness. For the data to send to the display I pull from a table of ascii character to 7-segment conversions. Where did I get that table? From this super useful set of fonts available for these types of displays. It even has approximations for some special characters and shows how much fidelity you lose as you have fewer segments to display with. This is the function I use for writing to the digits (which do not have decimal places mapped BTW) and I know I’ve given up the ability to set different parts of the digit at different brightnesses, but would that rally make this a better design?

void displayNum(int number, uint16_t brightness = 4095) {

displayChar(1, (number % 10) + 0x30, brightness);
displayChar(0, (number / 10 % 10) + 0x30, brightness);
}

This is how I wrap that so I can insert any number and have it displayed on the screen. I only have two digits and I have to shift into the ascii table a bit to start the number section, but this works just fine for numbers between 0 and 99 which is all I get anyway. That’s all I do to interact with the 7-segments, but I do have two of those bits going out to separate LEDs as well. 

//0-4095 range

void LedBrightness(indicatorLED index, uint16_t pwmValue) {
switch (index) {
case shift:
analogWrite(shiftLedPin, (pwmValue / 16));
break;
case ack:
pwm.setPWM(ackLedPin, 0, (4095 - pwmValue));
break;
case prev:
pwm.setPWM(prevLedPin, 0, (4095 - pwmValue));
break;
default:
// statements
break;
}
}

This is how I’m handling the three status indicator LEDs. The range is a little limited for the onboard analog output so I re-scaled the output but you can’t really tell the difference. I like using enums for stuff like this to make the code a lot more readable. 

void modeSet(indicatorLED index, bool mode) {

LedBrightness(index, mode * 4095);
switch (index) {
case shift:
shiftMode = mode;
break;
case ack:
ackMode = mode;
if (mode) {
mySimpit.printToKSP("Activate RCS!");
mySimpit.activateAction(RCS_ACTION);
} else {
mySimpit.printToKSP("Desactivate RCS!");
mySimpit.deactivateAction(RCS_ACTION);
}
break;
case prev:
prevMode = mode;
if (mode) {
mySimpit.printToKSP("Activate SAS!");
mySimpit.activateAction(SAS_ACTION);
} else {
mySimpit.printToKSP("Desactivate SAS!");
mySimpit.deactivateAction(SAS_ACTION);
}
break;
default:
//Serial.println("error");
break;
}
}

For each LED I decided to implement mode functions that can be checked from other functions and can trigger actions on change. This means that I don’t individually set the LED state, I set the mode state and everything else follows. I’m also always setting the LED to the max brightness so that 4096 levels of dimming is being wasted. Shame. 

void modeToggle(indicatorLED index) {

switch (index) {
case shift:
modeSet(shift, !shiftMode);
break;
case ack:
modeSet(ack, !ackMode);
break;
case prev:
modeSet(prev, !prevMode);
break;
default:
//Serial.println("error");
break;
}
}

Here’s the ting though, I usually just want to switch the mode to whatever it’s not so I again wrapped that in this which just inverts the current mode and all the things that come with it. These now act like numlock and capslock LEDs on your keyboard except they control a spaceship so it’s a lot cooler. 

#include <ADS1115_WE.h>

#define I2C_ADDRESS 0x48

ADS1115_WE adc = ADS1115_WE(I2C_ADDRESS);

...

if (!adc.init()) {
Serial.println("ADS1115 not connected!");
}
adc.setVoltageRange_mV(ADS1115_RANGE_6144);
adc.setCompareChannels(ADS1115_COMP_0_GND);
adc.setConvRate(ADS1115_860_SPS);
adc.setMeasureMode(ADS1115_CONTINUOUS);

Here’s the declarations for the analog to digital converter. I set up the range, the fact these are all single-ended and not differential readings, how fast to go, and that I just want it to keep running. 

float readChannel(ADS1115_MUX channel) {

float voltage = 0.0;
adc.setCompareChannels(channel);
voltage = adc.getResult_V(); // alternative: getResult_mV for Millivolt
return voltage;
}

Per usual here we have stacked complexity. The lowest level interface returns a given floating point voltage for whichever channel we need. 

int16_t readAxis(stickAxis toRead) {

float xDeadH = 2.65;
float xDeadL = 2.55;
float yDeadH = 2.64;
float yDeadL = 2.54;
float rDeadH = 2.50;
float rDeadL = 2.34;

float xMaxH = 3.37;
float xMaxL = 1.88;
float yMaxH = 3.32;
float yMaxL = 1.86;
float rMaxH = 3.62;
float rMaxL = 1.50;

float voltage = 0.0;
int16_t tempV = 0;
switch (toRead) {
case X:
voltage = readChannel(ADS1115_COMP_0_GND);
if (voltage > xMaxH) {
voltage = xMaxH;
}
if (voltage < xMaxL) {
voltage = xMaxL;
}
if (voltage > xDeadH) {
tempV = 1 + 32767 * ((voltage - xDeadH) / (xMaxH - xDeadH));
} else if (voltage < xDeadL) {
tempV = -1 - 32767 * ((voltage - xDeadL) / (xMaxL - xDeadL));
}
break;
case Y:
voltage = readChannel(ADS1115_COMP_1_GND);
if (voltage > yMaxH) {
voltage = yMaxH;
}
if (voltage < yMaxL) {
voltage = yMaxL;
}
if (voltage > yDeadH) {
tempV = 1 + 32767 * ((voltage - yDeadH) / (yMaxH - yDeadH));
} else if (voltage < yDeadL) {
tempV = -1 - 32767 * ((voltage - yDeadL) / (yMaxL - yDeadL));
}
tempV = -tempV; //reverse the polarity
break;
case R:
voltage = readChannel(ADS1115_COMP_2_GND);
if (voltage > rMaxH) {
voltage = rMaxH;
}
if (voltage < rMaxL) {
voltage = rMaxL;
}
if (voltage > rDeadH) {
tempV = 1 + 32767 * ((voltage - rDeadH) / (rMaxH - rDeadH));
} else if (voltage < rDeadL) {
tempV = -1 - 32767 * ((voltage - rDeadL) / (rMaxL - rDeadL));
}
return tempV;
break;
default:
break;
}
return tempV;
}

This is where all the complication for reading the axes comes in. I spent a while with small programs that did ‘peak and hold’ on each channel so I could determine the dead zones where the joystick values might stay even after being left to return to home and the maximum travel voltages in both directions based on the mechanical stops. As you can see I don’t get the full 0-5v swing so it’s a good thing I drove them at that voltage otherwise I’d have even less resolution to play with. From my readings I don’t even go above 4v but I left the range for the ADC to over 5v just to be safe. The logic for each axis is fairly simple and I can update all the aspects from a table of values. 

  • if the voltage is greater than the theoretical max, cap it to the max
  • if the voltage is less than the theoretical min, cap it to the min
  • if the voltage is greater than the top of the dead zone, scale it and output a positive value
  • if the voltage is less than the bottom of the dead zone, scale it and output a negative value
void readAxes() {


Serial.print("Xaxis:");
Serial.print(readAxis(X));

Serial.print(",Yaxis:");
Serial.print(readAxis(Y));

Serial.print(",Raxis:");
Serial.print(readAxis(R));
Serial.println();

delay(20);
}

This function is unused and was part of the initial debugging and proof of concept.


#include <Keypad.h>

const byte ROWS = 8; //eight rows
const byte COLS = 5; //five columns
//define the cymbols on the buttons of the keypads
char hexaKeys[ROWS][COLS] = {
{ 'N', '4', 'P', '!', 'F' },
{ '^', '3', 'E', 'M', '%' },
{ '&', '2', '9', 'C', 'A' },
{ 'S', '1', '8', 'L', 'O' },
{ ')', '0', '+', '_', 'Z' },
{ '$', '7', '[', ']', '*' },
{ '(', '6', 'T', '#', 'G' },
{ '?', '5', 'H', '@', '=' }
};
byte rowPins[ROWS] = { 23, 19, 18, 5, 17, 16, 4, 12 }; //connect to the row pinouts of the keypad
byte colPins[COLS] = { 15, 13, 14, 26, 27 }; //connect to the column pinouts of the keypad

//initialize an instance of class NewKeypad
Keypad customKeypad = Keypad(makeKeymap(hexaKeys), rowPins, colPins, ROWS, COLS);

This is the declaration of the key matrix reading. I have all the pins defined, the character map setup, and now when we query the key map we can see what’s going on. 

  char customKey = customKeypad.getKey();


if (customKey) {
switch (customKey) {
case 'S':
modeToggle(shift);
break;
case 'A':
modeToggle(ack);
break;
case 'P':
modeToggle(prev);
break;
case 'N':
//Serial.println("near");
throttleState = 99;
throttleUp();
break;
case 'F':
//Serial.println("far");
throttleUp();
break;
case '=':
//Serial.println("open");
throttleState = 0;
throttleDown();
break;
case '?':
//Serial.println("close");
throttleDown();
break;
case '!':
//Serial.println("F1");
break;
case '@':
//Serial.println("F2");
break;
case '#':
//Serial.println("F3");
break;
case ']':
//Serial.println("aux on");
break;
case '[':
//Serial.println("aux off");
break;
case 'O':
//Serial.println("mon");
break;
case 'E':
//Serial.println("next");
mySimpit.activateAction(STAGE_ACTION);
break;
case 'H':
//Serial.println("hold");
break;
case 'T':
//Serial.println("pattern");
break;
case 'Z':
//Serial.println("preset");
break;
case 'M':
//Serial.println("macro");
break;
case 'G':
//Serial.println("pgm");
break;
case 'C':
//Serial.println("cam");
break;
case 'L':
//Serial.println("clear");
break;
default:
//Serial.print("number key: ");
//Serial.println(customKey);
break;
}
}
//readAxes();
rotationUpdate();
throttleRepeat();

Inside the main loop we have this. It checks the state of the keys and if they go down it triggers an action. As you can see there’s not that many set up right now. Maximum and minimum throttle, throttle up and down, RCS and SAS toggle, and fire next stage. The interesting aspect comes next for how I implemented the throttle up and down key repeat. 

void throttleRepeat() {

if (customKeypad.findInList('F') > -1) //up
{
if (customKeypad.key[customKeypad.findInList('F')].kstate == HOLD) {
throttleUp();
delay(10);
}
}
if (customKeypad.findInList('?') > -1) //down
{
if (customKeypad.key[customKeypad.findInList('?')].kstate == HOLD) {
throttleDown();
delay(10);
}
}
}

To implement a key repeat I can query if the library has determined if a key is being held down. If that’s the case I just call the right function to move one increment, block for some time, and then fall through for it to be checked the next loop. If I don’t do this then the single button press only triggers the throttle to move one unit (in my case 1%) and not move any further until it goes back up again. I don’t usually like blocking code but making this non-blocking and implementing background timers would make this so messy…

// Private : Hardware scan hacked to other polarity

void Keypad::scanKeys() {
// Re-intialize the row pins. Allows sharing these pins with other hardware.
for (byte r=0; r<sizeKpd.rows; r++) {
pin_mode(rowPins[r],INPUT); //was INPUT_PULLUP
}

// bitMap stores ALL the keys that are being pressed.
for (byte c=0; c<sizeKpd.columns; c++) {
pin_mode(columnPins[c],OUTPUT);
pin_write(columnPins[c], HIGH); //column pulse output was LOW.
for (byte r=0; r<sizeKpd.rows; r++) {
bitWrite(bitMap[r], c, pin_read(rowPins[r])); // do not invert keypress.
}
// Set pin to high impedance input. Effectively ends column pulse.
pin_write(columnPins[c],LOW); //set back LOW
pin_mode(columnPins[c],INPUT);
}
}

This is the big issue I had with reading the key matrix on this board was the polarity. With the placement of the diodes and resistors on the board I’m not modifying it limits to scanning from one direction and with one polarity. If this were a dumb matrix that was just a series of buttons then you could do whatever, but I had to hack the arduino default keypad library to scan with an inverted polarity otherwise the signal wouldn’t pass through the diode. I’m not afraid to hack up libraries if they don’t work just right, but this one was a little annoying to figure out the issues. I also had intermittent problems that I tracked down to those resistors on the JTAG connector so they’re gone on my populated version and marked as ‘do not populate on the BOM. 

That marks the third entry in this series getting all the code talking to the hardware, last we have the issue of getting this code talking to kerbal space program. As promised I have the project up on PCBWay so you can order one of these boards if you either want to convert a crazy expensive camera controller into an esp32 controller, or if you just want an esp32 breakout board with some extra goodies. 

3 Responses to “Pelco KBD300A to Kerbal space program controller (part 3, code to exercise the hardware)”

  1. Pelco KBD300A to Kerbal space program controller (part 2, hardware design) | Evan's Techie-Blog Says:

    […] next will be the building and testing of the actual hardware functions and by then I think I’ll have a pcbway project set up so you can order this board populated […]

  2. Pelco KBD300A to Kerbal space program controller (part 1, reverse engineering) | Evan's Techie-Blog Says:

    […] come in 4 parts. This one is the reverse engineering, the next will be the hardware design, then software and troubleshooting for interacting with all the board segments, then the software to integrate it into KSP. Waiting […]

  3. Pelco KBD300A to Kerbal space program controller (part 4, interface to KSP) | Evan's Techie-Blog Says:

    […] Hacks, repairs, arcade games, sci-fi, and some very bad ideas with possibly humorous consequences « Pelco KBD300A to Kerbal space program controller (part 3, code to exercise the hardware) […]

Leave a comment