Introduction: STM32-based Custom Gaming Keypad With RGB (Originally Made for Osu!)

About: I'm a 18 year old Fusion 360 and Tinkercad user. I am currently working on a big format CoreXY 3D printer.

What is this?

This is a custom gaming keypad with four hot-swappable mechanical keys and customizable RGB LEDs. It is powered by an STM32F103Cx microcontroller and is programmed through the Arduino IDE. Its keys can be programmed on the fly through the driver program provided or through composite serial communication.

Feature List:

  • Fast STM32F103Cx microcontroller allows for low input latency.
  • USB Full Speed with 1000hz polling rate.
  • Hot-swappable keys.
  • EEPROM setting storage.
  • On-the-fly Key Customization (with GUI for Windows users)
  • Three RGB Modes with capability to add more.
  • Fully open source.
  • Arduino-compatible.
  • Modular and easy to carry.

How are the keys arranged?

The keys are arranged in the pattern of the keys "A S Z X" that appears on most keyboards. This means that if two of the keys are used for gaming (such as Z and X tapping in osu) then the rest can be used for other applications.

What do I need to build this?

In addition to the materials listed, you will also need advanced soldering skills (for soldering fine-pitch LQFP package and the micro-USB port). The soldering technique used to solder the LQFP package is known as drag soldering, which may seem complicated but is not hard to do after some practice.

Supplies

Materials:

I base my materials off Digikey components, but if you can find them on other distributors, then that works too.

  • Custom PCB
  • 1x STM32F103CxT6 (x can be replaced with 4, 6, 8, or B, though keep in mind that you need to check the size of the compiled code to make sure they fit on the flash memory) (DigiKey) (clones may, but are not guaranteed to, work if genuine ones are out of stock)
  • 2x 7.3mm height tactile switches (DigiKey)
  • 4x Reverse mount 4-PLCC LEDs (DigiKey)
  • 6x 0805 10K ohm resistors (DigiKey)
  • 4x 0805 68 ohm resistors (DigiKey)
  • 5x 0805 1.5K ohm resistors (DigiKey)
  • 3x N-channel SOT-23-3 3.3V level MOSFETs (DigiKey)
  • 3x 0805 100nF capacitors (DigiKey)
  • 2x 0805 1uF capacitors (DigiKey)
  • 2x 0805 20pF capacitors (Digikey)
  • 1x 0805 4.7uF capacitor (DigiKey)
  • 1x MCP1703T-3302T/DB 3.3V voltage regulator (DigiKey)
  • 1x 1210 500mA PTC resettable fuse (DigiKey)
  • 1x USB3090 micro USB A/B port (DigiKey)
  • 1x Row of 90 degree headers (DigiKey)
  • 1x 8MHz HC-49 crystal (DigiKey)
  • 4x Round rubber bumpers (DigiKey)
  • 4x Kailh hot swap sockets (KBDFans)
  • 4x Cherry MX RGB or equivalent switches (KBDFans)

Tools:

  • Soldering iron (preferably with cone, chisel, and knife tips)
  • Flux paste
  • 3D printer
  • ST-Link V2 or compatible versions
  • (recommended) solder wick

Step 1: Design Concept - Hardware

This is an overview of the design concept, and it'll be broken down into several sections.

Microcontroller

The microcontroller is the main control chip of this project. It acts as a composite USB device (HID and CDC simultaneously) and also handles the scanning of keypresses. Normally an ATMega32u4 is enough for a regular keyboard project, but because this is made for gaming, we need a microcontroller that is able to deliver low latency.

The microcontroller used in this project is an STM32F103 series MCU. It runs at 72MHz and is very well-supported on the Arduino IDE.

Power

The power is fed through the power pins on the USB port, but the voltage on USB is 5v, while the STM32F103 microcontroller can only tolerate 3.3v. Thus, a voltage regulator is required. The MCP1703T is chosen for this task.

Switches

Cherry MX style mechanical switches are probably the best options for any keyboard project unless there is a specific switch requirement. In this case, hot swap sockets are used for easy change of switches.

The microcontroller uses a for loop to switch between reading the four individual keys. And since there are only four keys, a matrix is not required.

RGB LED Controls

The pushbuttons and the RGB LEDs are for that signature RGB on gaming peripherals. The pushbuttons control the LED mode, color, and brightness.

Step 2: Hardware Design - Schematic

I used EAGLE to design the PCB. Above is the schematic.

It is definitely not the cleanest schematic, but it gets the job done. :)

You may notice that there aren't as many decoupling capacitors as the ST design guide recommended. In my experience, the chip works fine without 100nF capacitors on ALL power pins (though I don't deny that adding additional capacitors is good design practice).

Step 3: Hardware Design - PCB

The PCB layout is roughly 58mm by 59mm. Most of its components are placed on the bottom layer, with the exception of the components surrounding the voltage regulator.

Most of the SMD passive components are 0805, which are relatively easy to solder. However, the STM32 MCU is no doubt the hardest to solder of all the components. The technique to solder that component will be discussed later on.

Step 4: Hardware Design - PCB - Ordering

Since Instructables does not allow the upload of zip files, I have included the PCB schematic, board, and gerber files on GitHub.

I recommend ordering PCBs through PCB services including but not limited to JLCPCB and PCBWay. Etching your own PCB is not recommended in this case.

Step 5: Hardware Assembly - PCB - Overview

To begin with the PCB assembly, you need several tools and materials. Also prepare yourself for some hardcore soldering if you've never tried to solder fine-pitched surface mount components.

Soldering iron

For the soldering iron, I strongly recommend having multiple tips for assembling this PCB. Regular chisel tip is great for through hole soldering, cone tip is great for drag soldering the LQFP packaged STM32, and knife tip is the best in my experience for soldering the extremely tricky USB port.

Flux

Flux is extremely important in SMD soldering, especially for fine-pitched components like the LQFP STM32 or the USB port. I use a knockoff no-clean flux paste from Aliexpress, but any decent quality flux paste should do the job. Please make sure that the flux paste is in a paste form, not solid rosin.

Solder

For solder, any type would work, but I'd recommend types with a high flux content so that the experience will be nicer.

Tweezers

Tweezers are almost a necessity in SMD soldering. Please ensure that you have tweezers or the passive components will be a nightmare to solder.

Microscope

Although not required, a soldering microscope is highly recommended and will make the job a lot easier. Don't worry if you don't have one though! Your phone's camera is probably powerful enough to act as a cheap microscope with the simple installation of a microscope app.

Step 6: Hardware Assembly - PCB - Soldering - STM32 Microcontroller

This is the hardest part to solder in hardware assembly, which is why you should start with soldering this before anything else is assembled.

Here are a few videos showing the technique I use in this tutorial (drag soldering):

Tip recommended for this component: cone tip (C type)

  1. To begin, first make sure you are grounded to avoid any ESD damage.
  2. Then use tweezers to place the chip onto the PCB.
  3. Dispense a little bit of flux to one corner of the chip, covering as little area as possible.
  4. Melt a little bit of solder on the tip of your soldering iron.
  5. Re-check the alignment of the chip to the pins and try to hold the chip down while staying aligned. This can be done by hand or with a tweezer (don't burn your finger)
  6. Place the tip of the soldering iron onto the place you applied flux to. The flux should burn off and solder should automatically flow onto the pad. If it does not flow, try adding a tiny bit of pressure.
  7. Hold the tip there and do not remove it until this step is over. Make fine adjustments to make sure that every single pin of the chip is as perfectly aligned as possible.
  8. Release the soldering iron from the chip while still holding the chip in place.
  9. Check the alignment once again. If only small changes need to be made, no solder reflow is required and you can simply use tweezers to gently apply some forces to one side. If there is a big misalignment, repeat steps 6-8.
  10. Once the alignment is perfected. Apply flux to all pins of the chip, hold the chip firmly in place with a tweezer, apply solder to the tip of the iron, and drag it across each row of pins slowly.
  11. If done correctly, solder should flow onto the pins and each pin should have a shiny joint. If not, then go back and drag it again. More flux can be added if needed.
  12. Do a visual inspection to make sure every pin is soldered.

Step 7: Hardware Assembly - PCB - Soldering - USB Port

Now for another challenging component, the USB port.

This is a micro-USB A/B port with fine pitched pins that are meant for reflow soldering. However, that doesn't mean that we can't easily hand solder this.

Tip recommended: knife tip (K type)

  1. First, place the USB port, and you may notice that the port actually self aligns.
  2. Flip the board over and solder in the two through-hole metal pins on the bottom of the USB port. Make sure that the port stays relatively aligned when you solder these pins.
  3. Flip the board back around, and now it's time to solder the power, data, and ground pins.
  4. Apply lots of flux around the pins.
  5. Add a small amount of solder to the end of the soldering tip. It is strongly recommended to use a knife tip or any tip that is flat and relatively thin.
  6. Place the end of the tip with solder onto the pads. Drag it along multiple times and add a bit of force if needed. You may notice that solder also flows onto the casing of the USB port. This is fine and can be removed later on.
  7. The solder should flow just like how it did on the STM32. You can test connections by plugging in a micro-USB wire and using a multimeter to check for continuity from one end of the wire to the pads. Also make sure that none of the pins are shorted.

Congrats! That's all the hard parts of the soldering done!

Step 8: Hardware Assembly - PCB - Soldering - Power Regulation

Finishing up the front of the PCB, solder the rest of the components in place. (I will be working on a diagram showing where components go, and it will be posted soon)

Tip recommended: chisel tip (D type)

The capacitor on the front of the PCB should be 1uF and the resistor should be 1.5K.

There should be a 500mA PTC fuse on the 1210 footprint, but it can be bypassed with a wire.

Step 9: Hardware Assembly - PCB - Soldering - Oscillator Circuit

Now we solder the oscillator circuit located below the microcontroller on the back side of the PCB.

Tip recommended: chisel tip (D type)

This is relatively easy. Simply solder the 8MHz crystal normally and then solder the 0805 capacitors in place.

Step 10: Hardware Assembly - PCB - Soldering - Hot Swap Sockets

Now we solder the hot swap sockets on the back of the board.

Tip recommended: chisel tip (D type)

Place the hot swap sockets in following the orientation shown on the silkscreen and then apply solder to the pads. This should be easy.

Step 11: Hardware Assembly - PCB - Soldering - Back of the PCB

The pictures shown are my first iteration. You may notice the "wrongly oriented" MOSFETs and the different capacitor setup. This is fixed in the released version of the PCB, which means that you should orient your MOSFETs the RIGHT WAY.

Finish up the back of the PCB. I will be making a diagram soon of where all the components go. Right now this is described in text.

There are two setups for the back of the PCB. One is with the RGB LEDs and one is without. The two setups are shown in the picture on the left and the picture on the right, with the picture on the left being without the LEDs. Notice how the MOSFETs LEDs, and LED resistors are missing.

Tip recommended: chisel tip (D type)

Setup 1 (without RGB LEDs):

  • Two 10K resistors on the top near the solder jumpers.
  • One 10K resistor nearest to the reset button and one 100nF capacitor next to the resistor.
  • One 1.5K resistor EACH next to the hot swap sockets, like shown in picture.
  • One 4.7uF capacitor placed anywhere on the spots reserved for capacitors on the power trace.
  • One 1uF capacitor placed anywhere on the spots reserved for capacitors on the power trace.
  • One 100nF capacitor EACH on the rest of the spots reserved for capacitors on the power trace and power pins.

Setup 2 (with RGB LEDs):

  • All of setup 1, in addition to:
  • MOSFETs along the three places for the MOSFETs, oriented the correct way, not the way shown.
  • One 10K resistor for EACH MOSFET.
  • Four RGB LEDs, with ONE per key, oriented like the way shown.
  • One 68 ohm resistor for EACH RGB LED in their spots.

Step 12: Hardware Assembly - PCB - Soldering - Button and Headers

Finally, solder the headers and the buttons in. This should be the easiest part of the soldering process.

Tip recommended: chisel tip (D type)

Step 13: Hardware Assembly - ST-Link Connection

Now it's time to program the STM32 on this board.

The first four pins of the headers are the programming pins. Connect them each to their appropriate ST-Link pins.

  • SWDIO - SWDIO
  • SWCLK - SWCLK
  • GND - GND
  • 3V3 - 3.3V

Step 14: Hardware Assembly - Connect!

It's finally time to see if you did everything correctly! Plug the ST-Link into the computer and hope that nothing blows up...everything seems fine? GREAT!

  1. Download the ST-Link Utility from STMicroelectronics.
  2. Once you've opened it up, click on Target - Connect.
  3. If everything works correctly, you should see a successful connect as indicated by the second picture.
  4. You may view additional information by playing around, but you can also just disconnect now.

Step 15: Design Concept - Software

Let's go back to the design concepts now and take a look at software before we install the firmware.

This is an overview of the software design concept behind this keypad.

Startup

  1. On startup, the microcontroller checks for any stored settings such as key definitions or RGB colors in its EEPROM. If there is, it will initialize the settings, and if not, it will give these settings default values.
  2. Then the microcontroller initializes the other libraries and performs a little self test on the RGB LEDs.

Loop

This part of the code will be looped for the duration that the keypad is on.

  1. The microcontroller first checks to see if any keys that were not previously pressed were now pressed. If it finds such event happening, a debounce timer will start on that key, in which no key releases will be registered within the debounce time. This is the best way I can think of to debounce the switch without sacrificing latency. This method basically registers a keypress from the moment it detects a HIGH instead of a low. It may not be the most sound way to debounce switches, but it works.
  2. Then the microcontroller checks for key releases for the keys that have been pressed and are not within the debounce period.
  3. After that, the microcontroller checks for any presses of the RGB mode button. It records the amount of time that the RGB mode button is pressed/held for and uses that information to determine whether the user is trying to set the light mode, the color, or the brightness.
  4. The MCU executing the code by making any changes to the RGB LED controls if required.
  5. The MCU checks for any available serial communication from its composite CDC port. The serial communication is my way of implementing on-the-fly key change controls. I have written a .NET C# program that uses the serial port that the keypad opens to communicate with the MCU and to map the keys without reprogramming the chip. The details of serial commands will be mentioned further on.
  6. Lastly, of course, the code loops back.

Step 16: Assembly - Software - Two Options

There are two options for uploading the code to the STM32 microcontroller.

Option 1: Precompiled bin file

This is the simplest way to upload the code. The precompiled bin file has the regular firmware loaded on it, which contains serial communication and RGB LED enabled.

Option 2: Manual Compilation through Arduino IDE.

This options gives much more flexibility and customizability and is not hard to do. It utilizes the STM32Duino core and the Arduino IDE to compile the code.

Step 17: Assembly - Software - Precompiled Bin File

If you choose to upload by using the precompiled bin file, follow this step.

The precompiled bin file can be found on GitHub.

To program the microcontroller with the bin file, follow the instructions below after downloading/extracting the bin file and opening up ST-Link Utility.

  1. Connect to the STM32 by clicking Target - Connect.
  2. Then in the Target menu, select Program and Verify.
  3. Select the bin file that you downloaded.
  4. The window shown in the second picture should pop up. Click on Start after verifying that the settings are correct.
  5. The program should be flashed successfully and the LEDs should light up and switch through their colors.

Step 18: Assembly - Software - Manual Compilation - STM32Duino Core

If you choose to upload by using manual compilation, follow this and the following steps.

  1. You first need to install the STM32Duino Core on the Arduino IDE because STM32 is not natively supported. To do this, open File - Preferences, and paste the following url into the Additional Board Manager URLs textbox: http://dan.drown.org/stm32duino/package_STM32duino_index.json
  2. Click on OK, and then navigate to Tools - Board - Board Manager.
  3. Search "stm32" in the search textbox after the board manager loads, and then install the STM32F1XX/GD32F1XX boards library by stm32duino, like shown in the second picture.
  4. The board should install correctly.

Step 19: Assembly - Software - Manual Compilation - Polling Interval Change

On default, the STM32Duino has a USB composite library built-in, and this library uses 10ms polling intervals for USB HID.

While 10ms polling interval is enough for many keyboard applications, it is not preferred for gaming, especially rhythm games. Thankfully though, we can change the polling interval by editing the library's code.

  1. To do change the polling interval, first navigate to this folder: C:\Users\{username}\AppData\Local\Arduino15\packages\stm32duino\hardware\STM32F1\{version name} \libraries\USBComposite (the local appdata folder can be found by pressing Windows + R and typing "%localappdata%". Then find the Arduino15 folder and continue from that point.)
  2. After this folder is reached, open usb_hid.c in a code editor such as notepad++ or visual studio and use the shortcut Ctrl + F to search for the term "bInterval".
  3. Change the value of bInterval to 0x01 like shown. This reduces the polling interval down to 1ms, which means that the polling rate is now 1000hz.

Step 20: Assembly - Software - Manual Compilation - Arduino Code

Now we may upload the actual code. Refer to the next step for code customization.

/*
  Osu Keypad Code by Xieshi Zhang aka CrazyBlackStone
  Uses STM32F103C8T6 on custom PCB and Arduino IDE
  Project published under CC-BY-NC-SA
  Code under GPL v3 https://www.gnu.org/licenses/gpl-3.0.en.html
*/

#include <USBComposite.h>
#include <EEPROM.h> //eeprom library is natively installed on stm32duino or stm32 core

const char ManufacturerName[] = "Xieshi Zhang";
const char DeviceName[] = "Custom Osu! Keypad";
const char DeviceSerial[] = "00000000000000000001";

//pin definitions
#define red PA1
#define green PA2
#define blue PA3
#define switch1 PA4
#define switch2 PA5
#define switch3 PA6
#define switch4 PA7
#define modeSw PB0

//key definitions
char keys[4] = {'a', 's', 'z', 'x'};

//settings
#define debounceTime 5 //This is the debounce time in milliseconds for the keyboard switches. Changing this will not affect the latency but will determine the shortest keypress possible. Cherry specifies a 5ms maximum bounce time. 
#define maxPWM 255 //This is the maximum PWM value. It should be 255 by default
#define RGBLED //This is the option for RGB LEDs that can be soldered on to the PCB. Comment this out if you're not using any LEDs. CAUTION: ALTHOUGH RGB LEDS DO NOT WASTE SIGNIFICANT CPU CYCLES, PLEASE DISABLE THIS OPTION IF NO LEDS ARE USED FOR MAXIMUM SPEED!
#define serialCommunication // This is the option for enabling/disabling composite serial, which lets you configure keys on the fly with the driver or with serial commands. WARNING: ALTHOUGH STM32F1 IS VERY FAST, LEAVING SERIAL INITIALIZED MAY WASTE CPU CYCLES!
#define ledDelay 2 //This is the LED color switching delay/speed.
#define colorIncrementDelay 5000 //This is the amount of time the color will stay before incrementing.

//variables
const int switchPins[4] = {switch1, switch2, switch3, switch4};
const int ledPins[3] = {red, green, blue};
bool oldSwitchState[4] = {LOW, LOW, LOW, LOW};
bool switchState[4] = {LOW, LOW, LOW, LOW};
bool oldModeState = HIGH;
unsigned long debounce[4] = {0, 0, 0, 0};
unsigned long modePressTime;
unsigned long ledTime;
unsigned long colorTime;
unsigned long currentMillis;
byte lightMode = 0;
byte rgbMode = 0;
byte colorCount;
int brightness = 50; //percent
float rgbBrightness[3] = {1, 1, 1};
float targetRGBBrightness[3] = {1, 1, 1};
byte pwmRGB[3] = {maxPWM * rgbBrightness[1] * brightness / 100, maxPWM * rgbBrightness[2] * brightness / 100, maxPWM * rgbBrightness[3] * brightness / 100};
String prefix = "i";

USBHID HID;
HIDKeyboard Keyboard(HID);
#ifdef serialCommunication
USBCompositeSerial CompositeSerial;
#endif

void setup() {
  pinMode(modeSw, INPUT_PULLUP);

  pinMode(switch1, INPUT);
  pinMode(switch2, INPUT);
  pinMode(switch3, INPUT);
  pinMode(switch4, INPUT);

  #ifdef RGBLED
  pinMode(red, OUTPUT);
  pinMode(green, OUTPUT);
  pinMode(blue, OUTPUT);

  if (EEPROM.read(7) != 1)
  {
    EEPROM.write(0, rgbMode);
    EEPROM.write(1, lightMode);
    EEPROM.write(2, brightness);
    EEPROM.write(7, 1);
  }
  
  rgbMode = EEPROM.read(0);
  lightMode = EEPROM.read(1);
  brightness = EEPROM.read(2);
  #endif
  
  USBComposite.setManufacturerString(ManufacturerName);
  USBComposite.setProductString(DeviceName);
  USBComposite.setSerialString(DeviceSerial);
  USBComposite.setVendorId(0x987);

  #ifdef serialCommunication
  HID.begin(CompositeSerial, HID_KEYBOARD);

  if (EEPROM.read(8) != 1)
  {
    EEPROM.write(3, keys[0]);
    EEPROM.write(4, keys[1]);
    EEPROM.write(5, keys[2]);
    EEPROM.write(6, keys[3]);
    EEPROM.write(8, 1);
  }
  
  keys[0] = EEPROM.read(3);
  keys[1] = EEPROM.read(4);
  keys[2] = EEPROM.read(5);
  keys[3] = EEPROM.read(6);
  #else
  HID.begin(HID_KEYBOARD);
  #endif
  
  Keyboard.begin();

  #ifdef RGBLED
  digitalWrite(red, HIGH);
  delay(400);
  digitalWrite(red, LOW);
  digitalWrite(green, HIGH);
  delay(400);
  digitalWrite(green, LOW);
  digitalWrite(blue, HIGH);
  delay(400);
  digitalWrite(blue, LOW);

  checkRGBMode();
  if (lightMode == 0)
    turnOnLED();
  else
    turnOffLED();
  #endif
}

void loop() {
  //key polling and debouncing
  for (int i = 0; i < 4; i++)
  {
    switchState[i] = digitalRead(switchPins[i]);
    if (switchState[i] && !oldSwitchState[i] && (millis() - debounce[i]) > debounceTime)
    {
      debounce[i] = millis();
      oldSwitchState[i] = switchState[i];
      Keyboard.press(keys[i]);

      #ifdef RGBLED
      if (lightMode == 2)
        turnOnLED();
      #endif
    }
    else if (!switchState[i] && oldSwitchState[i] && (millis() - debounce[i]) > debounceTime)
    {
      debounce[i] = millis();
      oldSwitchState[i] = switchState[i];
      Keyboard.release(keys[i]);

      #ifdef RGBLED
      if (lightMode == 2)
        turnOffLED();
      #endif
    }
  }

  #ifdef serialCommunication
  if (CompositeSerial.available())
  {
    String command = CompositeSerial.readString();
    if (command.startsWith("i"))
    {
      prefix = "i";
      printInfo();
    }
    else if (command.startsWith("s")) //s:1:k:
    {
      String setKey = getValue(command, ':', 1);
      String setString = getValue(command, ':', 2);
      int intSetKey = setKey.toInt();
      if (setString[0] != ':')
        keys[intSetKey - 1] = setString[0];
      else
        keys[intSetKey - 1] = NULL;
      int EPValue = intSetKey + 2;
      EEPROM.update(EPValue, setString[0]);
      prefix = "d";
      printInfo();
    }
  }
  #endif
  //rgb led control, will be disabled if not defined
  #ifdef RGBLED
  if (digitalRead(modeSw) != oldModeState)
  {
    delay(10);
    bool modeState = digitalRead(modeSw);
    if (modeState == LOW && modeState != oldModeState)
    {
      modePressTime = millis();
      oldModeState = modeState;
    }
    else if (modeState == HIGH && modeState != oldModeState)
    {
      oldModeState = modeState;
      if ((millis() - modePressTime) < 300)
      {
        if (lightMode < 2)
          lightMode++;
        else
          lightMode = 0;
        EEPROM.update(1, lightMode);
      }
      else if ((millis() - modePressTime) <= 4000 && lightMode != 1)
      {
        if (rgbMode < 7)
          rgbMode++;
        else
          rgbMode = 0;
        EEPROM.update(0, rgbMode);
        checkRGBMode();
      }

      if (lightMode == 0)
        turnOnLED();
      else
        turnOffLED();

      if (lightMode == 1)
      {
        rgbBrightness[0] = 0;
        rgbBrightness[1] = 0;
        rgbBrightness[2] = 0;
      }
      //turnOnLED();
    }
  }
  else if (!digitalRead(modeSw) && (millis() - modePressTime) > 4000 && modePressTime)
  {
    currentMillis = millis();
    if ((currentMillis - modePressTime) < 5000)
    {
      brightness = 100;
      turnOnLED();
    }
    else if ((currentMillis - modePressTime) < 5500)
    {
      brightness = 90;
      turnOnLED();
    }
    else if ((currentMillis - modePressTime) < 6000)
    {
      brightness = 80;
      turnOnLED();
    }
    else if ((currentMillis - modePressTime) < 6500)
    {
      brightness = 70;
      turnOnLED();
    }
    else if ((currentMillis - modePressTime) < 7000)
    {
      brightness = 60;
      turnOnLED();
    }
    else if ((currentMillis - modePressTime) < 7500)
    {
      brightness = 50;
      turnOnLED();
    }
    else if ((currentMillis - modePressTime) < 8000)
    {
      brightness = 40;
      turnOnLED();
    }
    else if ((currentMillis - modePressTime) < 8500)
    {
      brightness = 30;
      turnOnLED();
    }
    else if ((currentMillis - modePressTime) < 9000)
    {
      brightness = 20;
      turnOnLED();
    }
    else if ((currentMillis - modePressTime) < 9500)
    {
      brightness = 10;
      turnOnLED();
    }
    else if ((currentMillis - modePressTime) < 10000)
    {
      brightness = 5;
      turnOnLED();
    }
    else
    {
      brightness = 0;
      turnOnLED();
    }
    EEPROM.update(2, brightness);
  }

  if (lightMode == 1)
  {
    if ((millis() - ledTime) >= ledDelay)
    {
      ledTime = millis();
      for (int i = 0; i < 3; i++)
      {
        if (rgbBrightness[i] > targetRGBBrightness[i])
          rgbBrightness[i] = rgbBrightness[i] - 0.01;
        else if (rgbBrightness[i] < targetRGBBrightness[i])
          rgbBrightness[i] = rgbBrightness[i] + 0.01;
        if (rgbBrightness[i] > 1)
          rgbBrightness[i] = 1;
      }
      turnOnLED();
    }
    if ((millis() - colorTime) >= colorIncrementDelay)
    {
      colorTime = millis();
      incrementColor();
    }
  }
#endif
}

#ifdef RGBLED
void turnOnLED()
{
  for (int i = 0; i < 3; i++)
  {
    pwmRGB[i] = maxPWM * rgbBrightness[i] * brightness / 100;
    analogWrite(ledPins[i], pwmRGB[i]);
  }
}
void turnOffLED()
{
  for (int i = 0; i < 3; i++)
  {
    analogWrite(ledPins[i], 0);
  }
}
void checkRGBMode()
{
  if (rgbMode == 0)
  {
    rgbBrightness[0] = 1;
    rgbBrightness[1] = 0;
    rgbBrightness[2] = 0;
  }
  else if (rgbMode == 1)
  {
    rgbBrightness[0] = 0.7;
    rgbBrightness[1] = 0.4;
    rgbBrightness[2] = 0;
  }
  else if (rgbMode == 2)
  {
    rgbBrightness[0] = 0;
    rgbBrightness[1] = 1;
    rgbBrightness[2] = 0;
  }
  else if (rgbMode == 3)
  {
    rgbBrightness[0] = 0;
    rgbBrightness[1] = 0.5;
    rgbBrightness[2] = 0.5;
  }
  else if (rgbMode == 4)
  {
    rgbBrightness[0] = 0;
    rgbBrightness[1] = 0;
    rgbBrightness[2] = 1;
  }
  else if (rgbMode == 5)
  {
    rgbBrightness[0] = 0.5;
    rgbBrightness[1] = 0;
    rgbBrightness[2] = 0.5;
  }
  else if (rgbMode == 6)
  {
    rgbBrightness[0] = 0.36;
    rgbBrightness[1] = 0.14;
    rgbBrightness[2] = 0.12;
  }
  else if (rgbMode == 7) //ffffe0
  {
    rgbBrightness[0] = 0.8;
    rgbBrightness[1] = 0.2;
    rgbBrightness[2] = 0.1;
  }
}
void incrementColor()
{
  if (colorCount < 1)
    colorCount++;
  else
    colorCount = 0;

  if (colorCount == 0)
  {
    targetRGBBrightness[0] = 1;
    targetRGBBrightness[1] = 0;
    targetRGBBrightness[2] = 0;
  }
  else if (colorCount == 1)
  {
    targetRGBBrightness[0] = 0;
    targetRGBBrightness[1] = 0;
    targetRGBBrightness[2] = 1;
  }
}
#endif

#ifdef serialCommunication
String getValue(String data, char separator, int index)
{
    int found = 0;
    int strIndex[] = { 0, -1 };
    int maxIndex = data.length() - 1;

    for (int i = 0; i <= maxIndex && found <= index; i++) {
        if (data.charAt(i) == separator || i == maxIndex) {
            found++;
            strIndex[0] = strIndex[1] + 1;
            strIndex[1] = (i == maxIndex) ? i+1 : i;
        }
    }
    return found > index ? data.substring(strIndex[0], strIndex[1]) : "";
}
void printInfo()
{
  CompositeSerial.print(prefix);
  CompositeSerial.print(":");
  if (keys[0] != ':')
    CompositeSerial.print(keys[0]);
  CompositeSerial.print(":");
  if (keys[1] != ':')
    CompositeSerial.print(keys[1]);
  CompositeSerial.print(":");
  if (keys[2] != ':')
    CompositeSerial.print(keys[2]);
  CompositeSerial.print(":");
  if (keys[3] != ':')
    CompositeSerial.print(keys[3]);
  CompositeSerial.print(":");
  CompositeSerial.print("v1.0.0");
  #ifdef RGBLED
  CompositeSerial.print(":");
  CompositeSerial.print(lightMode);
  CompositeSerial.print(":");
  CompositeSerial.print(rgbMode);
  CompositeSerial.print(":");
  CompositeSerial.print(brightness);
  #endif
  CompositeSerial.print("\n");
}
#endif

Step 21: Assembly - Software - Manual Compilation - Customize

In the code, there are a few settings that you may customize.

Debounce Time

This is the amount of time that the keys will refuse to register key releases after a key press is detected. A debounce time is required because mechanical switches naturally "bounce" after they are pressed, which may result in the registering of multiple false keypresses within a short amount of time if a debounce timer is not used. In this case, the debounce timer is set up in a way that does not affect latency whatsoever, so changing this value will not make the keys delay more. Cherry specifies a 5ms bounce time, and this value should work for most mechanical switches.

RGB LED

Disabling this option will disable all the code relating to the control of the RGB LEDs. This may save a bit of CPU cycles, but not enough to effectively change the performance of the keypad.

Serial Communication

Disabling this option will disallow the keypad to be programmed through serial or the driver software, but it will save a few CPU cycles because the microcontroller no longer needs to constantly listen for serial commands. Although theoretically the microcontroller should run faster without serial enabled, the speed difference is negligible.

(Advanced) RGB LED Color and Pattern

To customize the RGB LED color, you must directly edit the code. The colors defined under checkRGBMode() store all the light mode 0 colors (static), and the colors under incrementColor() store the light mode 2 (cycling through colors) colors.

To change the color, simply change the rgbBrightness[] values. The values represent the brightness of that color in percentage, with 0 being 0% and 1 being 100%. The array of 1-3 in rgbBrightness[] represent the colors red, green, and blue respectively.

Light Mode 2 Customizations (defined in settings):

LED Delay: This represents the delay between the process of color switching in light mode 2, which means that if the delay is higher, then the colors will switch slowly, while if the delay is lower, the colors will switch faster.

Color increment delay: This represents the amount of time between switching colors in light mode 2.

Step 22: Assembly - Software - Manual Compilation

Let's upload the Arduino code to the STM32. To do this, make sure your ST-Link is still plugged in.

  1. Open up the Arduino IDE and the .ino file from Software Design - Arduino Code.
  2. Select the options shown in the picture and click upload, no ports or programmer need to be selected.
  3. If everything works properly, the code should upload successfully, you should get a success message down on the terminal, and your keypad should light up if it has RGB LEDs.

Step 23: Assembly - 3D Printed Case

Attached are STL files for the 3D printed case of the keypad.

To assemble, place the PCB in between the two pieces, make sure the cutouts match where the components are, and then close the assembly. You may need to use M3x8mm self tapping screws to secure this in place.

Step 24: Assembly - Video Animation

This is a video animation of how to assemble the keypad.

The steps below gets into the assembly in more detail.

Step 25: Assembly - Bottom Piece

To begin the assembly, align the bottom piece with the PCB.

There is a cutout on the bottom piece corresponding to the location of a HC49 crystal. Make sure the correct orientation is used so that the crystal is put inside of the cutout.

Step 26: Assembly - Top Plate

Align the switches with the cutouts on the top piece (which acts as a plate) and push the switches in. They should snap in cleanly if you provide a little bit of force.

Do not put the keycaps on right now to prevent damage.

Step 27: Assembly - Combine and Secure

Combine the two pieces together like shown, and insert M3x8mm screws on the bottom (self tapping preferably) to secure the two pieces together.

You may now place the keycaps on.

You may also place silicone bumpers under the keypad so that it does not slide.

Step 28: How to Use

Using this keypad is very simple. You plug it into your computer and it should work as a normal, low latency, 1000hz polling rate HID keyboard.

However, you can customize this keypad to your own liking by a big extent.

RGB LEDs (if available)

You can change the RGB LED colors and modes with the mode button. Pressing the mode button for less than 0.3s changes the light mode.

The light modes are:

  1. Static color
  2. Lights up when key is pressed
  3. Alternating (default firmware is between red and blue, but multiple colors are possible)

Pressing the button for longer than 0.3s but shorter than 5s changes the LED color, which can be customized through the code.

Holding the button for longer than 5s will start to change the LED brightness, and the brightness will go down in 1s increments as you keep the button held. The brightness will be set on button release.

All the settings are stored in EEPROM and are remembered unless the EEPROM is erased.

Key Settings (if available)

This keypad has an amazing feature which is composite serial communication. Enabling this in code may mean a theoretically slower execution of code, but this should not affect latency to a great extent. You can connect to this device's serial COM port and be able to communicate to it via serial.

The serial commands are not meant to be user friendly, meaning that unfortunately, the easiest way to set keys on-the-fly is to use the GUI shown on the next step.

However, if you're limited to a device that can't run .exe files, then to set a key, one can use the command: "s:{key number}:{character}:"

  • For example: "s:1:k:" sets key 1 to "k" (lowercase).

Step 29: Additional Driver

I made an additional driver in visual studio that allows the user to customize keys on the fly. This is achieved through the amazing composite USB library that the STM32Duino core contains.

The exe file is on GitHub and is not obfuscated, meaning that if you're concerned that it's malicious, you can view its source code directly through programs such as ILSpy or upload it to VirusTotal. (Windows may not recognize me as a verifiable developer, so it may still give a SmartScreen warning, which can be ignored)

Step 30: Additional Driver - How to Use

First to start up the program, Windows may warn you that the publisher of this app is unknown and give a SmartScreen error. To bypass this, click on "More Info" and "Run Anyway".

This software is not malicious, but if you suspect that it could be malicious, I recommend uploading the executable to VirusTotal or use a decompiler such as ILSpy to view the source code as this is not obfuscated in any way.

Connection

After the driver successfully starts, the driver will first try to seek a serial connection to the device. There are two ways to connect to the device, one is by manually selecting the port, and the other is by auto-detecting. Although auto-detection may sound promising, the truth is that it will connect to any device named "USB Serial Device", which is unreliable. Manual connection is always preferred unless auto-detection is known to work, and that can be done by unchecking the auto-detect box.

Keybinds

You may now set keybinds on the fly with the program after the device is connected. To do this, use the panel called "keybinds". You may select the key number through the dropdown menu (reference help - key map for the key map), and then the keybind value can be entered through the textbox. Only Ascii characters and numbers are accepted from my knowledge, and unfortunately, colon is not a viable option because it is used as a delimiter. If you wish to disable that key, simply put nothing in the textbox and press set.

After the two options are filled, simply press the set button and if all goes well, the key bind will be set. This new setting will be saved on the microcontroller even after power down.

Raw Serial and Status

These two parts are used for diagnostics purposes. The raw serial is a display of the serial data that the device sends back, and the status panel shows the different settings stored on the device.

If there are any bugs, please report them through the comments, private message, or GitHub.

Step 31: Troubleshooting

Here is a list of problems that you may encounter when making this keypad. If these tips do not solve the problem, then feel free to contact me through comments or private messages.

I assembled everything, but when I try to connect to the microcontroller, ST-Link Utility does not recognize the chip.

First, double check the soldering on the microcontroller. Sometimes the pins may appear soldered even when there is no soldered applied. Also check for potential bridges in the pins. I recommend using a microscope or your camera phone with a microscope app as a makeshift microscope to check the soldering. I find that poking the pins with a pair of sharp tweezers is a great way to check whether the pins are soldered or not.

If that does not work, try to connect to the chip under reset. To do this, go to Target - Settings in ST-Link Utility and ensure that the mode is "Connect Under Reset". Then hold the reset button and click connect. If done correctly, the program should freeze until you release reset, and the chip will be successfully connected.

The chip connects and programs properly through ST-Link, but connecting it to USB gives me an error.

This problem is most likely due to the USB port being not soldered correctly. The USB port is one of the trickiest to solder parts on this PCB, so if this problem happens, I recommend going over the USB port pins again with a knife tip, solder, and lots of flux. I recommend applying a slight pressure to the pins to make sure that they're wetted properly.

To check the USB pin connections, you can use a USB cable and use a multimeter to measure continuity between the pins on the USB male connector to the pads on the PCB. Also make sure that the D+ and D- pins are not shorted.

I ordered my PCB before 5/9/2021 using the Gerber files provided, why are the LEDs not lighting up?

This is due to my mistake on uploading the previous version of the PCB Gerber files to GitHub (I'm terribly sorry). You can fix this by rotating the MOSFETs like shown in previous pictures of the PCB. If you need any help, feel free to PM me.

This issue is now fixed with the new Gerber files, but please double check before ordering.

Step 32: Gameplay Video

Here's an Osu! gameplay video that my friend made while using my keypad (and uploaded on his channel, feel free to check it out if you like Osu content).

You may have to click on "Watch on Youtube" if the embed doesn't work.

Step 33: Conclusion

Congrats! You built your own custom keypad!

I will keep this project updated for a few months as I will be constantly trying to improve it.

I would also like to thank Maker Hub Club for providing funding for this project.

For now, thanks for reading or making and have a nice day!

Microcontroller Contest

Runner Up in the
Microcontroller Contest