Frank Adams
Published © Apache-2.0

USB Laptop Keyboard Controller Solder Party RP2350 Stamp XL

Convert a laptop keyboard into a USB keyboard with the Raspberry Pi microcontroller on the Solder Party RP2350 Stamp XL.

IntermediateFull instructions provided16 hours37
USB Laptop Keyboard Controller Solder Party RP2350 Stamp XL

Things used in this project

Hardware components

Solder Party RP2350 Stamp XL
Sold at lectronz.com and at pimoroni.com
×1
SparkFun USB C Breakout - Horizontal
Can also be wired to USB Type A, Mini-B, or microB breakout connectors
×1
FPC Breakout Board
Sold at Aliexpress, Amazon, EBay, and others
×1
2mm Pitch Single Row Header Pins
×1
2.54mm Pitch Dual Row Header Pins
×1
Carrier Board for Stamp XL, FPC, & USB
Send the zipped Gerber PCB file to JLCPCB or PCBWay or send the KiCad PCB file to OSHPark or Eurocircuits (among others)
×1

Software apps and online services

Thonny Python IDE
Adafruit Circuit Python
KMK

Story

Read more

Schematics

Carrier Board - KiCad PCB File

Send this file to board fabrication companies like OSHPark.com and Eurocircuits.com that accept KiCad files

Carrier Board - Zipped Gerber PCB File

Send this zipped Gerber PCB File to board fabrication companies like JLCPCB.com and PCBWay.com (among others).

Code

Matrix_Decoder_RP2350B.py

Python
This Circuit Python program is used to decode the key matrix of a laptop keyboard.
Load this routine in the Stamp XL drive with the name "code.py"
#   Copyright 2025 Frank Adams
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#       http://www.apache.org/licenses/LICENSE-2.0
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
# This program is used to decode the key matrix of a laptop keyboard. Use an FPC connector to connect all the 
# laptop keyboard pins to the GPIO pins of a RP2350 Stamp XL which uses a RP2350B chip and has 48 GPIO.
# The program cycles thru all the possible pin 
# combinations looking for a connection when a key is pressed. Open an editor with a text file that lists all 
# the keyboard keys. The program will send over USB, the two GPIO pin numbers that are connected when a key is pressed.
# Once all keys have been tested, the editor will contain a complete listing of the keyboard connections. 
# 
import time
import board
import digitalio
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
from adafruit_hid.keycode import Keycode

kbd = Keyboard(usb_hid.devices)
layout = KeyboardLayoutUS(kbd)

# List the Pi Pico GP I/O pins in the array that are connected to the keyboard FPC cable.
# If the program finds that 2 pins are always connected (probably grounds or LEDs), remove them from the I_O list.

I_O = [board.GP0, board.GP1, board.GP2, board.GP3, board.GP4, board.GP5, board.GP6, board.GP7, board.GP8,  
board.GP9, board.GP10, board.GP11, board.GP12, board.GP13, board.GP14, board.GP15, board.GP16, board.GP17, 
board.GP18, board.GP19, board.GP20, board.GP21, board.GP22, board.GP23, board.GP24, board.GP25, board.GP26,
board.GP27, board.GP28, board.GP29, board.GP30, board.GP31, board.GP32, board.GP33]


# This function converts the board.GP number from the array into a text number and sends it over USB.
# It will work with up to 48 GPIO pins (for the RP2350B)
def Send_GP(GP):
    if GP == board.GP0:
        layout.write('0')
    elif GP == board.GP1:
        layout.write('1')
    elif GP == board.GP2:
        layout.write('2')
    elif GP == board.GP3:
        layout.write('3')
    elif GP == board.GP4:
        layout.write('4')
    elif GP == board.GP5:
        layout.write('5')
    elif GP == board.GP6:
        layout.write('6')
    elif GP == board.GP7:
        layout.write('7')
    elif GP == board.GP8:
        layout.write('8')
    elif GP == board.GP9:
        layout.write('9')
    elif GP == board.GP10:
        layout.write('10')
    elif GP == board.GP11:
        layout.write('11')
    elif GP == board.GP12:
        layout.write('12')
    elif GP == board.GP13:
        layout.write('13')
    elif GP == board.GP14:
        layout.write('14')
    elif GP == board.GP15:
        layout.write('15')
    elif GP == board.GP16:
        layout.write('16')
    elif GP == board.GP17:
        layout.write('17')
    elif GP == board.GP18:
        layout.write('18')
    elif GP == board.GP19:
        layout.write('19')
    elif GP == board.GP20:
        layout.write('20')
    elif GP == board.GP21:
        layout.write('21')
    elif GP == board.GP22:
        layout.write('22')
    elif GP == board.GP23:
        layout.write('23')
    elif GP == board.GP24:
        layout.write('24')
    elif GP == board.GP25:
        layout.write('25')
    elif GP == board.GP26:
        layout.write('26')
    elif GP == board.GP27:
        layout.write('27')
    elif GP == board.GP28:
        layout.write('28')
    elif GP == board.GP29:
        layout.write('29')
    elif GP == board.GP30:
        layout.write('30')
    elif GP == board.GP31:
        layout.write('31')
    elif GP == board.GP32:
        layout.write('32')
    elif GP == board.GP33:
        layout.write('33')
    elif GP == board.GP34:
        layout.write('34')
    elif GP == board.GP35:
        layout.write('35')
    elif GP == board.GP36:
        layout.write('36')
    elif GP == board.GP37:
        layout.write('37')
    elif GP == board.GP38:
        layout.write('38')
    elif GP == board.GP39:
        layout.write('39')
    elif GP == board.GP40:
        layout.write('40')
    elif GP == board.GP41:
        layout.write('41')
    elif GP == board.GP42:
        layout.write('42')
    elif GP == board.GP43:
        layout.write('43')
    elif GP == board.GP44:
        layout.write('44')
    elif GP == board.GP45:
        layout.write('45')
    elif GP == board.GP46:
        layout.write('46')
    elif GP == board.GP47:
        layout.write('47')
    else: 
        layout.write('not defined')    

while True:
    # outer loop drives a gpio pin low
    for i in range(0, len(I_O)-1): # drive each gpio pin low up to the second from the last
        row = digitalio.DigitalInOut(I_O[i])
        row.direction = digitalio.Direction.OUTPUT
        row.value = False
        # inner loop reads all gpio pins that are greater than the outer loop index number
        for j in range(i+1, len(I_O)): # read up to the last gpio pin in the array
            column = digitalio.DigitalInOut(I_O[j])
            column.direction = digitalio.Direction.INPUT
            column.pull = digitalio.Pull.UP
            if column.value == False: # key is pushed if low
                Send_GP(I_O[j])  # display inner loop gpio pin number 
                kbd.send(Keycode.TAB, Keycode.TAB)  # move over 2 tabs for next number
                Send_GP(I_O[i])  # display outer loop gpio pin number
                kbd.send(Keycode.DOWN_ARROW)  # move down to next line
                while column.value == False:  # loop until key is released to proceed
                    pass # Do nothing
            column.deinit()	 # release the column pin as an input w/ pullup
        # 
        row.deinit() # release the row pin as an output
     
    time.sleep(0.001) # small delay before repeating main loop

code_LenovoE550.py

Python
KMK code for a Lenovo E550 laptop keyboard.
Load this routine in the Stamp XL drive with the name "code.py"
#   Copyright 2025 Frank Adams
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#       http://www.apache.org/licenses/LICENSE-2.0
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
#   The following GPIO connections and key matrix are for a Lenovo E550 laptop keyboard.
#   Use this KMK code as your starting point if using a different keyboard.
#   Change the name to code.py when copying this file to the Stamp XL.
#   See step 16 of my Instructable for more information.
#
import board

from kmk.kmk_keyboard import KMKKeyboard
from kmk.keys import KC
from kmk.scanners import DiodeOrientation
from kmk.extensions.media_keys import MediaKeys
from kmk.modules.layers import Layers

keyboard = KMKKeyboard()

keyboard.col_pins = (board.GP0, board.GP1, board.GP2, board.GP4, board.GP5, board.GP7, board.GP8, board.GP11, board.GP29)
keyboard.row_pins = (board.GP3, board.GP6, board.GP9, board.GP10, board.GP12, board.GP13, board.GP14, board.GP15, 
board.GP16, board.GP17, board.GP18, board.GP19, board.GP20, board.GP21, board.GP22, board.GP23, board.GP28, board.GP30, board.GP31)
keyboard.diode_orientation = DiodeOrientation.COL2ROW # Most laptop keyboards have no diodes so ROW2COL also works

keyboard.modules.append(Layers())
keyboard.extensions.append(MediaKeys())

FN = KC.TG(1)

# Add any other keys that your keyboard has using the KMK keycodes from https://docs.qmk.fm/keycodes_basic
# and the media keys from https://github.com/KMKfw/kmk_firmware/blob/main/docs/en/media_keys.md
# All key names are preceded with KC. except the FN key. KC.NO is used when there is no key at that matrix location.
# This keyboard has a keypad and those keys start with KC.P followed by the name or number from the keycodes_basic list.

keyboard.keymap = [
    [#layer 0: Base Layer
    KC.LSHIFT,		KC.NO,		KC.RSHIFT,	KC.NO,		KC.NO,		KC.NO,		KC.NO,		KC.NO,		KC.NO,
    KC.TAB,			KC.NO,		KC.Z,		KC.A,		KC.N1,		KC.Q,		KC.GRAVE,	KC.ESC,		KC.NO,
    KC.Y,			KC.N,		KC.M,		KC.J,		KC.N7,		KC.U,		KC.N6,		KC.H,		KC.NO,
    KC.F3,			KC.NO,		KC.C,		KC.D,		KC.N3,		KC.E,		KC.F2,		KC.F4,		KC.NO,
    KC.CAPS,		KC.NO,		KC.X,		KC.S,		KC.N2,		KC.W,		KC.F1,		KC.NO,		KC.NO,
    KC.T,			KC.B,		KC.V,		KC.F,		KC.N4,		KC.R,		KC.N5,		KC.G,		KC.NO,
    KC.F7,			KC.NO,		KC.DOT,		KC.L,		KC.N9,		KC.O,		KC.F8,		KC.NO,		KC.NO,
    KC.LBRC,		KC.SLASH,	KC.NO,		KC.SCOLON,	KC.N0,		KC.P,		KC.MINUS,	KC.QUOTE,	KC.NO,
    KC.RBRC,		KC.NO,		KC.COMMA,	KC.K,		KC.N8,		KC.I,		KC.EQUAL,	KC.F6,		KC.NO,
    KC.NO,			KC.NO,		KC.RCTRL,	KC.NO,		KC.NO,		KC.NO,		KC.LCTRL,	KC.NO,		KC.NO,
    KC.NO,			KC.RALT,	KC.NO,		KC.NO,		KC.NO,		KC.NO,		KC.NO,		KC.LALT,	KC.NO,
    KC.LGUI,		KC.RIGHT,	KC.NO,		KC.NO,		KC.F12,		KC.NO,		KC.NO,		KC.NO,		KC.NO,
    KC.NO,			KC.LEFT,	KC.PDOT,	KC.NO,		KC.END,		KC.NO,		KC.NO,		KC.UP,		KC.NO,
    KC.NO,			KC.DOWN,	KC.NO,		KC.NO,		KC.F11,		KC.NO,		KC.HOME,	KC.NO,		KC.NO,
    KC.BSPACE,		KC.SPACE,	KC.ENTER,	KC.BSLASH,	KC.F10,		KC.NO,		KC.F9,		KC.F5,		KC.NO,
    KC.NO,			KC.PGDOWN,	KC.PGUP,	KC.PSCREEN,	KC.INSERT,	KC.NO,		KC.DELETE,	KC.NO,		KC.NO,
    KC.NO,			KC.NO,		KC.NO,		KC.NO,		KC.NO,		KC.NO,		KC.NO,		KC.NO,		FN,
    KC.PSLS,		KC.PPLS,	KC.P9,		KC.P7,		KC.P8,		KC.PAST,	KC.PMNS,	KC.NUM_LOCK, KC.NO,
    KC.P5,			KC.P0,		KC.PENT,	KC.P2,		KC.P3,		KC.P6,		KC.P1,		KC.P4,		KC.NO,
    ],

# The Fn media layer is a copy of the base layer except where a media key exists (like mute at Fn-F1)  
	[#layer 1: Fn Media Layer
    KC.LSHIFT,		KC.NO,		KC.RSHIFT,	KC.NO,		KC.NO,		KC.NO,		KC.NO,		KC.NO,		KC.NO,
    KC.TAB,			KC.NO,		KC.Z,		KC.A,		KC.N1,		KC.Q,		KC.GRAVE,	KC.ESC,		KC.NO,
    KC.Y,			KC.N,		KC.M,		KC.J,		KC.N7,		KC.U,		KC.N6,		KC.H,		KC.NO,
    KC.VOLU,		KC.NO,		KC.C,		KC.D,		KC.N3,		KC.E,		KC.VOLD,	KC.F4,		KC.NO,
    KC.CAPS,		KC.NO,		KC.X,		KC.S,		KC.N2,		KC.W,		KC.MUTE,	KC.NO,		KC.NO,
    KC.T,			KC.B,		KC.V,		KC.F,		KC.N4,		KC.R,		KC.N5,		KC.G,		KC.NO,
    KC.F7,			KC.NO,		KC.DOT,		KC.L,		KC.N9,		KC.O,		KC.F8,		KC.NO,		KC.NO,
    KC.LBRC,		KC.SLASH,	KC.NO,		KC.SCOLON,	KC.N0,		KC.P,		KC.MINUS,	KC.QUOTE,	KC.NO,
    KC.RBRC,		KC.NO,		KC.COMMA,	KC.K,		KC.N8,		KC.I,		KC.EQUAL,	KC.BRIU,	KC.NO,
    KC.NO,			KC.NO,		KC.RCTRL,	KC.NO,		KC.NO,		KC.NO,		KC.LCTRL,	KC.NO,		KC.NO,
    KC.NO,			KC.RALT,	KC.NO,		KC.NO,		KC.NO,		KC.NO,		KC.NO,		KC.LALT,	KC.NO,
    KC.LGUI,		KC.RIGHT,	KC.NO,		KC.NO,		KC.F12,		KC.NO,		KC.NO,		KC.NO,		KC.NO,
    KC.NO,			KC.LEFT,	KC.PDOT,	KC.NO,		KC.END,		KC.NO,		KC.NO,		KC.UP,		KC.NO,
    KC.NO,			KC.DOWN,	KC.NO,		KC.NO,		KC.F11,		KC.NO,		KC.HOME,	KC.NO,		KC.NO,
    KC.BSPACE,		KC.SPACE,	KC.ENTER,	KC.BSLASH,	KC.F10,		KC.NO,		KC.F9,		KC.BRID,	KC.NO,
    KC.NO,			KC.PGDOWN,	KC.PGUP,	KC.PSCREEN,	KC.INSERT,	KC.NO,		KC.DELETE,	KC.NO,		KC.NO,
    KC.NO,			KC.NO,		KC.NO,		KC.NO,		KC.NO,		KC.NO,		KC.NO,		KC.NO,		FN,
    KC.PSLS,		KC.PPLS,	KC.P9,		KC.P7,		KC.P8,		KC.PAST,	KC.PMNS,	KC.NUM_LOCK, KC.NO,
    KC.P5,			KC.P0,		KC.PENT,	KC.P2,		KC.P3,		KC.P6,		KC.P1,		KC.P4,		KC.NO,
	],
	
]

if __name__ == '__main__':
    keyboard.go()

Keyboard_pin_list

Plain text
Lists all possible keys on the keyboard
LCTRL				
RCTRL				
LSHIFT				
RSHIFT				
LALT				
RALT				
LGUI				
FN					
A					
B					
C					
D					
E					
F					
G					
H					
I					
J					
K					
L					
M					
N					
O					
P					
Q					
R					
S					
T					
U					
V					
W					
X					
Y					
Z					
GRAVE `				
N1	(number 1)		
N2	(number 2)		
N3	(number 3)		
N4	(number 4)		
N5	(number 5)		
N6	(number 6)		
N7	(number 7)		
N8	(number 8)		
N9	(number 9)		
N0	(number 0)		
MINUS				
EQUAL				
BSPACE				
ESC					
F1					
F2					
F3					
F4					
F5					
F6					
F7					
F8					
F9					
F10					
F11					
F12					
INSERT				
DELETE				
RIGHT				
LEFT				
UP					
DOWN				
APP (MENU)			
SLASH	/			
DOT 	.			
COMMA	,			
SCOLON	;			
QUOTE	'			
ENTER				
LBRC	[			
RBRC	]			
BSLASH	\			
CAPS				
TAB					
SPACE				
HOME				
END					
PGUP				
PGDOWN				
PSCREEN				
EJCT (EJECT)		
MPRV (PREVIOUS)		
MNXT (NEXT)			
VOLD (VOL DOWN)		
VOLU (VOL UP)		
MPLY (PLAYPAUSE)	
MUTE (VOL ZERO)				
BRIU (BRIGHT UP)	
BRID (BRIGHT DOWN)	

SCRL
NUM					
KP_SLASH			
KP_ASTERISK			
KP_MINUS			
KP_PLUS				
KP_ENTER			
KP_DOT				
KP_0				
KP_1				
KP_2				
KP_3				
KP_4				
KP_5				
KP_6				
KP_7				
KP_8				
KP_9				

CALCULATOR			
Globe				
Folder				
Lock				

Add any other keys that your keyboard has using the KMK keycodes from https://docs.qmk.fm/keycodes_basic
and the media keys from https://github.com/KMKfw/kmk_firmware/blob/main/docs/en/media_keys.md
All key names are preceded with KC. except the FN key. KC.NO is used when there is no key at the matrix location.
Keyboards with a keypad precede the key with KC.P followed by the name or number from the keycodes_basic list.

USB_Laptop_Keyboard_Controller

Repository for all project files

Credits

Frank Adams
5 projects • 12 followers
I am a retired Boeing engineer that enjoys experimenting with Pi, Arduino, and Teensy projects.

Comments