├── images ├── micro-psx-wiring.png └── psx-cable-colors.png ├── library.properties ├── keywords.txt ├── examples └── psx-basic-example │ └── psx-basic-example.ino ├── LICENSE ├── src ├── arduino_psx.h └── arduino_psx.cpp └── README.md /images/micro-psx-wiring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veroxzik/arduino-psx-controller/HEAD/images/micro-psx-wiring.png -------------------------------------------------------------------------------- /images/psx-cable-colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veroxzik/arduino-psx-controller/HEAD/images/psx-cable-colors.png -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=Arduino-PSX 2 | version=1.0 3 | author=Verox Zik 4 | maintainer=Verox Zik <43590004+veroxzik@users.noreply.github.com> 5 | sentence=Library to act as a Playstation 1/2 controller. 6 | paragraph=This library allows an Arduino Leonardo, Micro, or Pro Micro to act as a Playstation 1/2 digital controller. 7 | category=Device Control 8 | url=https://github.com/veroxzik/arduino-psx-controller/ 9 | architectures=avr 10 | includes=arduino_psx.h -------------------------------------------------------------------------------- /keywords.txt: -------------------------------------------------------------------------------- 1 | # Syntax Coloring Map for Arduino-PSX 2 | 3 | # Datatypes (KEYWORD1) 4 | PSX KEYWORD1 5 | 6 | # Methods and Functions (KEYWORD2) 7 | init KEYWORD2 8 | setAlwaysHeld KEYWORD2 9 | setButton KEYWORD2 10 | send KEYWORD2 11 | isr KEYWORD2 12 | 13 | # Enums (KEYWORD2) 14 | PS_INPUT KEYWORD2 15 | 16 | # Enum Variables (KEYWORD3) 17 | PS_NONE KEYWORD3 18 | PS_SELECT KEYWORD3 19 | PS_L3 KEYWORD3 20 | PS_R3 KEYWORD3 21 | PS_START KEYWORD3 22 | PS_UP KEYWORD3 23 | PS_RIGHT KEYWORD3 24 | PS_DOWN KEYWORD3 25 | PS_LEFT KEYWORD3 26 | PS_L2 KEYWORD3 27 | PS_R2 KEYWORD3 28 | PS_L1 KEYWORD3 29 | PS_R1 KEYWORD3 30 | PS_TRIANGLE KEYWORD3 31 | PS_CIRCLE KEYWORD3 32 | PS_CROSS KEYWORD3 33 | PS_SQUARE KEYWORD3 -------------------------------------------------------------------------------- /examples/psx-basic-example/psx-basic-example.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void setup() { 4 | // Setup PSX 5 | PSX.init(8); 6 | 7 | // Setup buttons 8 | pinMode(A0, INPUT_PULLUP); 9 | pinMode(A1, INPUT_PULLUP); 10 | pinMode(A2, INPUT_PULLUP); 11 | pinMode(A3, INPUT_PULLUP); 12 | pinMode(A4, INPUT_PULLUP); 13 | pinMode(A5, INPUT_PULLUP); 14 | } 15 | 16 | void loop() { 17 | 18 | // Set buttons 19 | PSX.setButton(PS_INPUT::PS_LEFT, digitalRead(A0) == LOW); 20 | PSX.setButton(PS_INPUT::PS_DOWN, digitalRead(A1) == LOW); 21 | PSX.setButton(PS_INPUT::PS_UP, digitalRead(A2) == LOW); 22 | PSX.setButton(PS_INPUT::PS_RIGHT, digitalRead(A3) == LOW); 23 | PSX.setButton(PS_INPUT::PS_CIRCLE, digitalRead(A4) == LOW); 24 | PSX.setButton(PS_INPUT::PS_CROSS, digitalRead(A5) == LOW); 25 | 26 | // Send info 27 | PSX.send(); 28 | 29 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 veroxzik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/arduino_psx.h: -------------------------------------------------------------------------------- 1 | #ifndef PSX_h 2 | #define PSX_h 3 | 4 | #if defined(ARDUINO) && ARDUINO >= 100 5 | #include "Arduino.h" 6 | #else 7 | #error "Arduino versions older than v1.0 are not supported." 8 | #endif 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | /* 15 | Following section from Arduino-Libs (digitalWriteFast) 16 | 17 | Copyright (c) 2011-2020 Watterott electronic (www.watterott.com) 18 | All rights reserved. 19 | */ 20 | 21 | #if (defined(ARDUINO_AVR_LEONARDO) || \ 22 | defined(__AVR_ATmega16U4__) || \ 23 | defined(__AVR_ATmega32U4__)) 24 | 25 | #define UART_RX_PIN (0) //PD2 26 | #define UART_TX_PIN (1) //PD3 27 | 28 | #define I2C_SDA_PIN (2) //PD1 29 | #define I2C_SCL_PIN (3) //PD0 30 | 31 | #define SPI_HW_SS_PIN (17) //PB0 32 | #define SPI_HW_MOSI_PIN (16) //PB2 33 | #define SPI_HW_MISO_PIN (14) //PB3 34 | #define SPI_HW_SCK_PIN (15) //PB1 35 | 36 | #define __digitalPinToPortReg(P) \ 37 | ((((P) >= 0 && (P) <= 4) || (P) == 6 || (P) == 12 || (P) == 24 || (P) == 25 || (P) == 29) ? &PORTD : (((P) == 5 || (P) == 13) ? &PORTC : (((P) >= 18 && (P) <= 23)) ? &PORTF : (((P) == 7) ? &PORTE : &PORTB))) 38 | #define __digitalPinToDDRReg(P) \ 39 | ((((P) >= 0 && (P) <= 4) || (P) == 6 || (P) == 12 || (P) == 24 || (P) == 25 || (P) == 29) ? &DDRD : (((P) == 5 || (P) == 13) ? &DDRC : (((P) >= 18 && (P) <= 23)) ? &DDRF : (((P) == 7) ? &DDRE : &DDRB))) 40 | #define __digitalPinToPINReg(P) \ 41 | ((((P) >= 0 && (P) <= 4) || (P) == 6 || (P) == 12 || (P) == 24 || (P) == 25 || (P) == 29) ? &PIND : (((P) == 5 || (P) == 13) ? &PINC : (((P) >= 18 && (P) <= 23)) ? &PINF : (((P) == 7) ? &PINE : &PINB))) 42 | #define __digitalPinToBit(P) \ 43 | (((P) >= 8 && (P) <= 11) ? (P) - 4 : (((P) >= 18 && (P) <= 21) ? 25 - (P) : (((P) == 0) ? 2 : (((P) == 1) ? 3 : (((P) == 2) ? 1 : (((P) == 3) ? 0 : (((P) == 4) ? 4 : (((P) == 6) ? 7 : (((P) == 13) ? 7 : (((P) == 14) ? 3 : (((P) == 15) ? 1 : (((P) == 16) ? 2 : (((P) == 17) ? 0 : (((P) == 22) ? 1 : (((P) == 23) ? 0 : (((P) == 24) ? 4 : (((P) == 25) ? 7 : (((P) == 26) ? 4 : (((P) == 27) ? 5 : 6 ))))))))))))))))))) 44 | 45 | #else 46 | #error "This library does not support any processor besides the ATmega32U4 or ATmega16U4" 47 | #endif 48 | 49 | /* End Arduino-Libs Section */ 50 | 51 | typedef enum { 52 | PS_NONE = 0, 53 | PS_SELECT = (1 << 0), 54 | PS_L3 = (1 << 1), 55 | PS_R3 = (1 << 2), 56 | PS_START = (1 << 3), 57 | PS_UP = (1 << 4), 58 | PS_RIGHT = (1 << 5), 59 | PS_DOWN = (1 << 6), 60 | PS_LEFT = (1 << 7), 61 | PS_L2 = (1 << 8), 62 | PS_R2 = (1 << 9), 63 | PS_L1 = (1 << 10), 64 | PS_R1 = (1 << 11), 65 | PS_TRIANGLE = (1 << 12), 66 | PS_CIRCLE = (1 << 13), 67 | PS_CROSS = (1 << 14), 68 | PS_SQUARE = (1 << 15), 69 | } PS_INPUT; 70 | 71 | class PSX_ { 72 | 73 | private: 74 | uint16_t buttonState = 0; 75 | uint16_t defaultState = 0; 76 | bool invertCIPO = true; 77 | bool invertACK = true; 78 | volatile uint8_t *ackDDR; 79 | volatile uint8_t *ackPORT; 80 | volatile uint8_t ackPinMask; 81 | 82 | uint8_t dataBuf[5] = {0x41, 0x5A, 0xFF, 0xFF, 0xFF}; 83 | uint8_t commandBuf[5] = {0x01, 0x42, 0x00, 0x00, 0x00}; 84 | uint8_t commandMemcard = 0x81; 85 | volatile bool memcardActive = false; 86 | volatile uint8_t currentByte = 0; 87 | 88 | public: 89 | void init(int ackPin, bool invACK = false, bool invCIPO = true); 90 | void setAlwaysHeld(uint16_t buttons) { 91 | defaultState = buttons; 92 | buttonState = defaultState; 93 | }; 94 | 95 | void setButton(uint16_t button, bool state); 96 | void send(); 97 | 98 | void isr(); 99 | 100 | }; 101 | 102 | extern PSX_ PSX; 103 | 104 | #endif // PSX_h -------------------------------------------------------------------------------- /src/arduino_psx.cpp: -------------------------------------------------------------------------------- 1 | #include "arduino_psx.h" 2 | 3 | ISR(SPI_STC_vect) { 4 | PSX.isr(); 5 | } 6 | 7 | void PSX_::init(int ackPin = 8, bool invACK = false, bool invCIPO = true) { 8 | ackDDR = __digitalPinToDDRReg(ackPin); 9 | ackPORT = __digitalPinToPortReg(ackPin); 10 | ackPinMask = __digitalPinToBit(ackPin); 11 | invertACK = invACK; 12 | invertCIPO = invCIPO; 13 | 14 | if (invertACK) { 15 | (*ackDDR) |= (1 << ackPinMask); // Set as output 16 | (*ackPORT) &= ~(1 << ackPinMask); // Set LOW (open drain -- pullup on console) 17 | } else { 18 | (*ackDDR) &= ~(1 << ackPinMask); // Set as input 19 | (*ackPORT) &= ~(1 << ackPinMask); // Ensure pullup is off 20 | } 21 | 22 | if (invertCIPO) { 23 | DDRB |= (1 << 3); // Set as output 24 | PORTB &= ~(1 << 3); // Set LOW (open drain -- pullup on console) 25 | } else { 26 | DDRB &= ~(1 << 3); // Set as input 27 | PORTB &= ~(1 << 3); // Ensure pullup is off 28 | } 29 | 30 | #if (F_CPU == 16000000L) 31 | SPCR |= (1 << SPR1); // Fosc/64 @16MHz==250KHz 32 | #elif (F_CPU == 8000000L) 33 | SPCR |= (1 << SPR1); // SPR1=1, SPR0=0 for Fosc/64 base 34 | SPSR |= (1 << SPI2X); // SPI2X=1 for double speed -> Fosc/32 @8MHz==250KHz 35 | #endif 36 | SPCR |= (1 << CPHA); // Setup @ leading edge, sample @ falling edge 37 | SPCR |= (1 << CPOL); // Leading edge is falling edge, trailing edge is rising edge 38 | SPCR &= ~(1 << MSTR); // MSTR bit is zero, SPI is slave 39 | SPCR |= (1 << DORD); // Byte is transmitted LSB first, MSB last 40 | SPCR |= (1 << SPE); // Enable SPI 41 | SPCR |= (1 << SPIE); // Enable Serial Transfer Complete (STC) interrupt 42 | SPDR = 0xFF; 43 | 44 | sei(); 45 | } 46 | 47 | void PSX_::setButton(uint16_t button, bool state) { 48 | if (state) { 49 | buttonState |= button; 50 | } else { 51 | buttonState &= ~button; 52 | } 53 | buttonState |= defaultState; // Ensure the previously set Always Held buttons are indeed held 54 | } 55 | 56 | void PSX_::send() { 57 | // Fill data array with button info 58 | dataBuf[2] = ~(buttonState & 0xFF); 59 | dataBuf[3] = ~((buttonState >> 8) & 0xFF); 60 | 61 | // Check SS and clear memcard flag if necessary 62 | if ((PINB & (1 << 0)) > 0) { 63 | if (memcardActive) { 64 | memcardActive = false; 65 | } 66 | currentByte = 0; 67 | if (!invertCIPO) { 68 | DDRB &= ~(1 << 3); // Disable output 69 | } 70 | } 71 | } 72 | 73 | void PSX_::isr() { 74 | uint8_t recvByte = SPDR; 75 | 76 | if (recvByte == commandBuf[currentByte] && memcardActive == false) { 77 | DDRB |= (1 << 3); // Enable output 78 | if (invertCIPO) { 79 | SPDR = ~dataBuf[currentByte]; 80 | } else { 81 | SPDR = dataBuf[currentByte]; 82 | } 83 | currentByte++; 84 | if (currentByte < 5) { 85 | _delay_us(8); // Necessary delay for PS1 (not needed for PS2) 86 | // Set ACK low 87 | if (invertACK) { 88 | (*ackPORT) |= (1 << ackPinMask); 89 | } else { 90 | (*ackDDR) |= (1 << ackPinMask); // Enable output (default LOW) 91 | } 92 | _delay_us(1); 93 | // Set ACK high 94 | if (invertACK) { 95 | (*ackPORT) &= ~(1 << ackPinMask); 96 | } else { 97 | (*ackDDR) &= ~(1 << ackPinMask); 98 | } 99 | } else { 100 | DDRB &= ~(1 << 3); // Disable output 101 | SPDR = 0xFF; 102 | currentByte = 0; 103 | } 104 | } else { 105 | if (recvByte == commandMemcard && currentByte == 0) { 106 | memcardActive = true; 107 | } 108 | DDRB &= ~(1 << 3); // Disable output 109 | SPDR = 0xFF; 110 | currentByte = 0; 111 | } 112 | } 113 | 114 | PSX_ PSX; 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arduino PSX Controller 2 | 3 | This is an Arduino library used to emulate a Playstation 1/2 controller, exclusively for the Leonardo / Micro / Pro Micro. 4 | 5 | ### Caveats 6 | 7 | 1. This library will only function in digital mode. That means no joysticks and no pressure sensitive triggers or buttons. 8 | 9 | ## Materials Required 10 | 11 | It is **highly** recommended that you use an [Arduino Micro](https://store.arduino.cc/products/arduino-micro) or [comparable clone](https://www.aliexpress.com/item/1893729784.html). This is due to the fact that all necessary pins are easily broken out. Using a Leonardo or Pro Micro will require you to solder onto pre-existing LEDs, which can be difficult. 12 | 13 | You will also need qty 1 (one) NPN signal transistor. I used 2N2222A, but many others will work. A similar N-Channel MOSFET, such as the 2N7000 will also work. 14 | 15 | Lastly, you will need a cable to plug into your Playstation. Options include: 16 | 1. Cutting the cord from an existing controller 17 | 2. Buying an extension cable and using the male end 18 | 3. A pre-terminated cable [such as this one](https://www.aliexpress.com/item/32826516802.html) 19 | 20 | ## Playstation Wiring 21 | 22 | Let's get acquainted with the Playstation cable and color codes. It is always a good idea to verify that these colors match the cable you own. Some third party or cheap cables may vary. 23 | 24 | ![Playstation Cable](/images/psx-cable-colors.png) 25 | 26 | * **CIPO:** (Controller-In / Peripheral-Out) This is data from the controller to the console. It is held HIGH via a pull-up resistor inside the console. We will use the transistor to pull this line to GND. Also known as MISO. 27 | * **COPI:** (Controller-Out / Peripheral-In) This is the data from the console to the controller. Also known as MOSI. 28 | * **7.6V:** This provides power for the rumble motors. See [Powering the Arduino](#powering-the-arduino). 29 | * **GND:** The common ground for the console, also known as 0V. 30 | * **3.3V:** This is power coming from the console, which typically powers the controller processor. See [Powering the Arduino](#powering-the-arduino). 31 | * **CS:** (Chip Select) This line goes LOW when the console is requesting data from a controller. This is how the console selects which player's controller to read from. Also known as SS. 32 | * **SCK:** (Serial Clock) This is the clock signal coming from the console. 33 | * **N/C:** This wire is not used (not connected). 34 | * **ACK:** This is how the controller tells the console it has finished sending data. It is held HIGH via a pull-up resistor inside the console. 35 | 36 | ## Arduino Wiring 37 | 38 | The example program follows the wiring below. Please refer to your specific transistor datasheet for its pinout. 39 | 40 | ![Arduino Micro Wiring](/images/micro-psx-wiring.png) 41 | 42 | ### Pins that cannot be changed 43 | 44 | * CIPO 45 | * COPI 46 | * CS 47 | * SCK 48 | 49 | ### Pins that can be changed 50 | 51 | * ACK 52 | 53 | ### Power Pins 54 | 55 | * VIN 56 | * 5V 57 | * GND 58 | 59 | ## Powering the Arduino 60 | 61 | There's a few options you have for powering the arduino. 62 | 63 | ### Option 1: (Recommended!) 7.6V to VIN 64 | 65 | Connect 7.6V to VIN on the Arduino. This will power the Arduino off the rumble line and at 5V. It is safe to plug in the USB connector at the same time using this method. 66 | 67 | ### Option 2: USB Only 68 | 69 | Disconnect both 7.6V and 3.3V from the Arduino and only use the USB connection. USB will need to be plugged in any time you wish to use the controller. 70 | 71 | ### Option 3: Playstation to 3.3V 72 | 73 | Disconnect 7.6V and connect 3.3V from the Playstation to the 5V pin on your Arduino. This will force the chip to run at 3.3V. 74 | 75 | **DO NOT PLUG IN USB WHEN CONNECTED LIKE THIS** as it can damage the Playstation or your PC. 76 | 77 | ## Installation Instructions: 78 | 79 | 1. Download: https://github.com/veroxzik/arduino-psx-controller/archive/refs/heads/main.zip 80 | 2. In the Arduino IDE, select `Sketch` > `Include Library` > `Add .ZIP Library...`. Browse to where the downloaded ZIP file is located and click Open. The example will now appear under `File` > `Examples` > `Arduino-PSX`. 81 | 82 | ## Basic Use 83 | 84 | ### Initialization 85 | 86 | Include the header at the top of your code. This will automatically create an instance of the `PSX` object. 87 | 88 | ``` 89 | #include 90 | ``` 91 | 92 | Next, run the initialization function inside `setup`: 93 | 94 | ``` 95 | void setup() { 96 | PSX.init(8); 97 | } 98 | ``` 99 | 100 | The `init` function takes up to 3 arguments. 101 | 102 | 1. `ackPin`: This is the pin that the ACK line is attached to. Use the digital pin number, shown in the diagram above as the `D` numbers. 103 | * The default is `8`. 104 | 2. `invertACK`: This allows you to invert the logic for the ACK pin. Set this to `false` when connected directly to the Arduino and use `true` when connected via a transistor. 105 | * The default is `false`. 106 | 3. `invertCIPO`: This allows you to invert the logic for the CIPO pin. Set this to `false` when connected directly to the Arduino and use `true` when connected via a transistor. Please see [Bypassing the Transistor](#bypassing-the-transistor) 107 | * The default is `true`. 108 | 109 | #### Always Held Buttons 110 | 111 | For certain types of controllers (Pop'n Music and Guitar Hero come to mind), the game expects certain buttons to *always* be pressed. 112 | 113 | To do so, run the method `setAlwaysHeld` using the button enums `BITWISE OR`'d together. 114 | 115 | ``` 116 | void setup() { 117 | PSX.init(8); 118 | PSX.setAlwaysHeld(PS_INPUT::PS_LEFT | PS_INPUT::PS_DOWN | PS_INPUT::PS_RIGHT); 119 | } 120 | ``` 121 | 122 | ### Setting Buttons 123 | 124 | Buttons can be set easily using the `setButton` method. 125 | 126 | Set a button to `true` to press it and `false` to release it. 127 | 128 | ``` 129 | // Press left button 130 | PSX.setButton(PS_INPUT::PS_LEFT, true); 131 | 132 | // Release left button 133 | PSX.setButton(PS_INPUT::PS_LEFT, false); 134 | ``` 135 | 136 | Valid options are as follows: 137 | ``` 138 | PS_INPUT::PS_SELECT 139 | PS_INPUT::PS_L3 140 | PS_INPUT::PS_R3 141 | PS_INPUT::PS_START 142 | PS_INPUT::PS_UP 143 | PS_INPUT::PS_RIGHT 144 | PS_INPUT::PS_DOWN 145 | PS_INPUT::PS_LEFT 146 | PS_INPUT::PS_L2 147 | PS_INPUT::PS_R2 148 | PS_INPUT::PS_L1 149 | PS_INPUT::PS_R1 150 | PS_INPUT::PS_TRIANGLE 151 | PS_INPUT::PS_CIRCLE 152 | PS_INPUT::PS_CROSS 153 | PS_INPUT::PS_SQUARE 154 | ``` 155 | 156 | ## Example 157 | 158 | A basic example is included. Navigate to the Example sketches in the Arduino IDE to run it. 159 | 160 | The example includes D-Pad controls, as well as O and X. Connect the following pins to GND to press the corresponding button: 161 | |Pin|Button| 162 | |:---:|:---:| 163 | |A0|LEFT| 164 | |A1|DOWN| 165 | |A2|UP| 166 | |A3|RIGHT| 167 | |A4|O| 168 | |A5|X| 169 | 170 | ## Additional Notes 171 | 172 | ### Bypassing the Transistor 173 | 174 | For safety and performance reasons, I suggest using the transistor as shown above to prevent backfeeding voltage into your console. This has the potential to cause harm. 175 | 176 | ***It has been noted by myself and others that bypassing the use of a transistor on the CIPO line may result in poor performance or corrupted data. It is not a software issue, but a result of electrical characteristics related to the Arduino, the length and impedance of your wires, and other factors. I will not be able to troubleshoot your situation if you go this route.*** 177 | 178 | ## Credits 179 | 180 | Some code used from [watterott's Arduino-Libs](https://github.com/watterott/Arduino-Libs). Specific lines are noted. 181 | 182 | ``` 183 | Copyright (c) 2011-2020 Watterott electronic (www.watterott.com) 184 | All rights reserved. 185 | ``` 186 | 187 | Wiring and protocol information from [Curious Inventor's page on the Playstation Controller](https://store.curiousinventor.com/guides/PS2) 188 | 189 | Code base adapted from [CrazyRedMachine's Ultimate Pop'n Controller](https://github.com/CrazyRedMachine/UltimatePopnController/tree/PSX), which itself is based on [busslave's PSX_RECEIVER.cpp](https://nfggames.com/forum2/index.php?topic=5001.0). 190 | 191 | Discord User GoroKaneda for getting me to work on and document this, as well as additional testing. 192 | 193 | Additional suggestions and feedback from [progmem](https://github.com/progmem). 194 | 195 | ## Donate 196 | 197 | If you appreciate this library or the other projects I do, you can make a monetary donation below. Thanks! 198 | 199 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/vxzk/tip) 200 | --------------------------------------------------------------------------------