tp-compact-keyboard
tp-compact-keyboard copied to clipboard
Customise USB Compact Keyboard Firmware
(I am only half joking here)
The keyboard has an upgradable firmware and a documented microcontroller: https://support.lenovo.com/us/en/documents/pd026745 http://www.sonix.com.tw/article-en-1002-3048
It would be great to have a keyboard with a working middle mouse button. A completely clean room firmware would allow this.
As a bonus, it would be great to implement proper negative inertia, which has disappeared from newer TrackPoint devices: http://blogs.epfl.ch/icenet/documents/Ykt3Eext.pdf
Interesting. Did you try upgrading your own keyboard? Notice any different behaviour other than the listed key combination not working?
Yes, I did update it. No visible changes. I tried sending it some new parameters from Linux but it still has the annoying middle-button behaviors.
So I have been doing a little reverse engineering (I'm getting this keyboard tomorrow and just wanted to tinker a little bit) and these are my findings so far:
- Lenovo's firmware upgrade tool
tp_compact_usb_kb_with_trackpoint_fw.exefile is a modified version of Sonix'sSONiX_USB_MCU_ISP_Tool_V2.3.0.6.exe(BinDiff shows a big percentage of exactly matching functions), you can see it in a resource editor and when comparing side by side:
- Inside Lenovo's tool is a
BINARYresource which is an executable originally namedLenovoTest.exe(Sonix's tool has no such thing) - This executable does absolutely nothing, but appended to the end is a 24892 bytes file that appears to be an SN8 file (albeit encypted with a simple XOR key 0x5A)
- When using Sonix's SN8 C Studio with a template for the
SN8F2288chip combined with theC2AsmDemosource code you can get it to produce an SN8 file of similar size with similar contents (most of the header appears to match). - The function that decodes (part of) the SN8 file is located at 0x40E080 in Lenovo's tool for people that might be interested in looking further.
- A disassembler is available for the SN8F2288 instruction set called dissn8.
My plan is to figure out how the SN8 file format works (probably it just directly flashes certain regions to the ROM) and create disassembler cores for IDA/Binary Ninja to be able to reverse engineer the Lenovo firmware.
@mrexodia Did you have time to do some more work on this?
@dal00 I did do some work on this but recently I didn’t have much time. I came in contact with @vpelletier from https://github.com/vpelletier/dissn8 and he in fact reversed most of the firmware.
At the moment I’m working on an emulator for the whole chip to do debugging on changes to the firmware without risking to brick my board. This is coming along nicely and at the moment I’m implementing USB support inside qemu.
It’s mostly a project I work on during flights though so don’t expect too much.
Also, doing a clean room firmware could be possible but is a significant effort without benefits other than having an open source firmware. My plan is to just hook in the real firmware and replace the keyboard routine with something more to my liking...
@mrexodia Cool, thanks for the update! What kind of changes do you have in mind?
Hi,
Mostly I want to change mappings for Fn+something to arbitrary keycodes (F13 and certain other keys that no keyboard has for a personal project) and also allow swapping the Fn/Ctrl keys (although I got used to that by now).
Probably I could have made my changes by now but I’m just doing the emulator stuff to learn 😀
Here is some progress on an interactive disassembler/emulator for the platform: https://github.com/mrexodia/SN8F2288_gui
I figured out how to disable the automatic hardware "scrolling". If you start with version 3.30 of the firmware (SHA-256 7116a3819ee094857d21e4671cb6cf953d582372126f0f6728f6b2421eda7bd4) and write 0x5A5A to locations 476076, 475710, 475718 and 481870 (in decimal), you should get a file with SHA-256 7fd326d15862211932ce73965c2ba2b86d87b918a54f25d7af37eef1b29d27ba.
If you use that to flash the firmware, it functions as a normal three button mouse would (obviously, one can still do software fake mouse wheel shenanigans if one wants to). I think it still intersperses some scrolling reports depending on the mode, but all the actual scrolling calculations should be disabled.
This seems to work fine with Windows (with, and presumably without, the Lenovo driver), but Linux (the lenovo_hid driver, I think) seems to ignore the middle mouse button events. I mostly intend to use the keyboard with Windows (all my Linux machines are actual Thinkpads :)), so I'm not going to debug this further.
Some obvious NB:
- I cannot, and will not, guarantee this won't brick your keyboard. Use at your own risk.
- I cannot, and will not, provide patched binaries (for reasons of copyright).
Can you elaborate on the process to come to this conclusion for the patch?
I used dissn8 and looked for code that writes to the USB endpoint FIFO and from there figured out which memory locations are used to hold button state, and from there which places check for middle mouse button. (I'm not really sure to what extent I can legally share specifics about this...)
I really want to swap the Fn and Ctrl keys. It's crazy that this isn't possible with the default firmware, since their laptops have the feature to swap. It's increcibly hard to press Ctrl+Shift with the current (ISO) layout.
Do you know if this would even be possible with just a FW mod, or if the key matrix is designed so that some combinations wouldn't register? I just want to know if there's any point in looking into this, or if i should just do the HW mod.
From what I looked at when disassembling the keyboard, it is just a laptop keyboard glued to a plastic body (you can remove the body without removing any screws if you want to look inside yourself). This tells me that there is no problem with the design of the key matrix.
It is possible to modify the firmware to switch the Ctrl and Fn keys, but I'm hesitant of trying because I don't want to brick the keyboard. Recently I implemented interrupts and timers is my emulator and I can now observe pins being read/written, but no more time 😄
Did you guys make any progress with this?
I really want to be able to swap the Fn and Ctrl keys as well.
This is the closest to progress I got: https://github.com/mrexodia/SN8F2288_gui
Personally I just got used to it and had to drop the project for time reasons...
I improved dissn8 some more, the biggest addition being the ability to generate global function call graph and per-function branch graphs (in graphviz "dot" format), closely second to per-address read/write/set/clear access tracking.
I also continued my firmware analysis a bit further, but still haven't made up my mind about releasing the dissn8 .cfg file I write firmware-specific findings to: there is no code in it but symbolic names I came up with for ram & rom addresses, and some comments. IANAL, but this does not look "clean room" enough to me. In any case, the firmware can be fully disassembled with the chip declaration file only, all the (few) jump types the CPU has are supported. The only things missing in dissn8 are ram bank selection (which this firmware doesn't use, and given how it uses B0 and non-B0 instruction variants for the same variables it looks like the compiler used does not support them itself) and possible direct program counter assignments as opposed to increments (which this firmware also doesn't use).
Just a cautionary word: firmware flashing is purely handled by the firmware itself, the chip does not on its own assist in getting the device on the USB bus. So the next step after a bad flash will very likely be to poke at whatever JTAG capabilities the chip may have (I have not looked into this myself). Given how obscure the code is in some places (I think I found a periodic event occurring every 14.5 hours... that can't be right), combined with the at places obscure documentation of the chip itself (timer source can be external or internal, internal being "fcpu or fclk" but without any indication of which it really is - and there is a factor of 2 to 8 between these two...), this means I would be very surprised if someone achieves extended firmware modifications without bricking a few.
I did not get very deep into it, but the risk of bricking was why I started writing an emulator. Most things appear to be working and the next step would be to actually support USB with qemu or similar...
With regards to bricking though, perhaps it is possible to always run the firmware in the “kernel” component that is responsible for handling flashing (I went through many Chinese sites looking for example source code and it looks like there is some kind of support for detecting failed flashes and then falling back). To jump to the normal operation it could be made so that it only happens if a specific hid packet is received, meaning that if you mess up nothing would persist making it less likely to brick.
You might like my gui by the way, I only did a minimal implementation and it is already very comfortable to comment/label (compatible with your cfg file format). It’s no IDA, but gets the job done better than manually reading in a text editor and is very hackable :)
Not sure if I implemented cross reference support yet, but it should also be very easy...
On Tue, 7 May 2019 at 00:48, Vincent Pelletier [email protected] wrote:
I improved dissn8 some more, the biggest addition being the ability to generate global function call graph and per-function branch graphs (in graphviz "dot" format), closely second to per-address read/write/set/clear access tracking.
I also continued my firmware analysis a bit further, but still haven't made up my mind about releasing the dissn8 .cfg file I write firmware-specific findings to: there is no code in it but symbolic names I came up with for ram & rom addresses, and some comments. IANAL, but this does not look "clean room" enough to me. In any case, the firmware can be fully disassembled with the chip declaration file only, all the (few) jump types the CPU has are supported. The only things missing in dissn8 are ram bank selection (which this firmware doesn't use, and given how it uses B0 and non-B0 instruction variants for the same variables it looks like the compiler used does not support them itself) and possible direct program counter assignments as opposed to increments (which this firmware also doesn't use).
Just a cautionary word: firmware flashing is purely handled by the firmware itself, the chip does not on its own assist in getting the device on the USB bus. So the next step after a bad flash will very likely be to poke at whatever JTAG capabilities the chip may have (I have not looked into this myself). Given how obscure the code is in some places (I think I found a periodic event occurring every 14.5 hours... that can't be right), combined with the at places obscure documentation of the chip itself (timer source can be external or internal, internal being "fcpu or fclk" but without any indication of which it really is - and there is a factor of 2 to 8 between these two...), this means I would be very surprised if someone achieves extended firmware modifications without bricking a few.
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/lentinj/tp-compact-keyboard/issues/32#issuecomment-489815142, or mute the thread https://github.com/notifications/unsubscribe-auth/AASYFGNANNMK2LT5CWN47HDPUCYVVANCNFSM4BX7FQBA .
I am making enough progress on the analysis that I think I can prepare a description for a clean-room reimplementation:
- general microcontroler settings (clock source frequency, clock ratios, ...) as these cannot be modified by reflashing and need to be known to respect timing constraints
- port settings & what is connected on which (4 pins are still resisting my attempts at the moment)
- keyboard matrix (scancodes for each cell)
- structure of i²c messages exchanged with the mouse controler
- flash general layout (where is built-in flasher, where is persistent storage and what is stored)
- flashing protocol (I may be able to provide one implemented in python) (maybe a bit more ?)
The rest should be available already: CPU doc, USB HID doc. A libre assembler tool should be easy to implement. A libre C compiler should be a lot more effort, and I doubt it would be worth it (unless we have an sdcc dev in the audience maybe ? But then it will be another arch costing in maintenance for very few known device types...). So the implementation will likely have to be done in pure assembler.
To give a very general idea of the amount of effort needed, the proprietary firmware has around 6500 assembler instructions (flasher included). This chip's assembly dialect is very simple (a bit simpler than the 8051, to give a more widely known example). I believe this would require a few weeks of work, ~~maybe a few days in collaborative sprint mode~~ [EDIT: "days" is very optimistic, let's leave it at "weeks"].
Is anyone motivated to take this on, and non-tainted by the proprietary implementation ?
Fun discovery of the day: if you press "return" while plugging the keyboard, it boots in flash mode. It's still under firmware control, but that means that if we do not touch the early initialisation code & flashing program, we can have a quite fail-safe setup for experimenting.
Fun discovery of the day: if you press "return" while plugging the keyboard, it boots in flash mode. It's still under firmware control, but that means that if we do not touch the early initialisation code & flashing program, we can have a quite fail-safe setup for experimenting.
Sadly, that's not possible... Early initialisation code calls functions well past this offset, before checking key state. Also, erase pages are huge compared to how fast the code progresses in initialising everything, so custom firmware would get an already initialised state with interrupts enabled, and would have to carefuly place stub functions to satisfy early init, and tread around the memory offsets it uses...
Such frankenware seems much more dangerous than reimplementing the key polling ourselves early in the init.
I have most of the clean-room spec written. I still feel there is something fishy with i²c, and the chip at the other end seems famously undocumented (custom ASIC ?), so I have to check some more before they are ready.
I still would like to hear that someone is motivated to work on the implementation part of the firmware.
In addition, I would also like to hear from anyone willing to review the clearoom spec without being candidate for implementation, in order to advise on what is missing and what shouldn't be there (ex: I feel the keyboard matrix may be too much, as it can be deduced during implementation by using the keyboard - although some cells will not be found until enough layouts have been tested).
I have written a flashing tool, and have verified with actual hardware all except the dangerous commands. I ordered a second keyboard to not brick the one I use (non-local layout). I do not intend to release it until I have used it myself, so it's not in my repository yet.
Curiosity killed the... mouse.
Below, a cautionary tale for the curious:
<my life> I was snooping around the i²c bus, which has no probe pad. So I soldered 2 wires to the pull-up resistors on SCL and SDA. Which are surface-mount components. One successful measure later, and I was tearing down my setup.
Then a brainfart hit, and I burned myself with the soldering iron. The recoil caused SCL's pull-up to lift. No way to get solder back under it. I removed it completely, resoldered the pads, and the resistor suddenly decided it wanted to travel at high speed to somewhere.
So I discovered a new feature of the firmware: if it cannot initialise the mouse chip (via i²c), it reboots the keyboard. So it won't work as a mouseless keyboard until I get my hands on what seems to be a 3.3k 1005 and somehow succeed in my first intentional SMC solder. </my life>
The good thing is that now I'm less scared of trying my flashing tool, as boot-time flashing mode is still available (RETURN press on plug is checked before mouse init).
The good thing is that now I'm less scared of trying my flashing tool, as boot-time flashing mode is still available (RETURN press on plug is checked before mouse init).
Done. flashsn8 pushed to my repository. Works-for-me state. No warranty, use at your own risk. Tested on linux with python-libusb1 installed (as of Debian sid).
A libre assembler tool should be easy to implement.
Done, pushed to my repository.
It is very crude, error reporting is abysmal (what's a line number ?), but it's able to assemble the source produced by dissn8, and end up with the same binary image as dissn8's input and should be able to cope with some style variations.
So the libre toolchain is now bootstrapped.
So it won't work as a mouseless keyboard until I get my hands on what seems to be a 3.3k 1005 and somehow succeed in my first intentional SMC solder.
Success !
It was hard, and I had to wipe the board clean of burned flux twice, but I eventually succeeded. I suspect I lifted the pad along with the original resistor, and had to make a solder bridge to a nearby via.
Now that board, which sustained a few erase/program cycles with my tool (with the original firmware), is back in working order. And I now have a spare keyboard in another layout - and could confirm I could swap the key matrices between both.
I'm currently busy writing asm libraries for the chip. I've written flasher interfacing, a bit-banging I²C and am progressing on USB implementation - but I did not test much yet. I²C is a pain to check even in the original toolchain's simulator, as it does not have an accurate port emulation. I expanded the features of the assembler enough to be able to work on modular sources (ram allocation, symbol exports/imports, anonymous jump targets).
EDIT: Just pushed the - still very crude and 95% untested - asm libraries in my repository.
Hey Vincent!
Just wanted to chip in and say I'm following attentively your progress, as I'm writing this from my very own compact USB keyboard.
I really appreciate – and judging by the reactions on your comments, I'm not alone – that you continue to report your progress regularly, so cheers!
Thanks a lot for the support, it really helps keeping my motivation !
I will try to push this as much as I can, although I'm still worried that I cannot on my own implement the final firmware: I am tainted by my analysis of the proprietary implementation. Even though keyboard reading is not rocket science, copyright is copyright, and I do not want to do all this work (as fun as it may be), then push a firmware just to have it taken down by a DMCA claim. So I'm implementing from specs as much as I can when specs exist (and am fairly confident I am not reproducing idiosyncrasies of the proprietary firmware, although there are only so many ways to implement a USB SETUP packet dispatcher), but will need some external help to tie it all together with the board- & device-specific stuff.
On a related motivational note, I realized that one feature I miss from old thinkpad keyboards is the Fn+up/down/left/right key combo for stop/play|pause/previous/next multimedia keys. A custom firmware would bring these back.
Now that I have a bunch of untested ASM code around, my next goal is to test it automatically, and for this I need a scriptable emulator (ex: run until this address is reached in at most this many cycles, then flip that port bit, then run until that address is reached...). I'm not yet sure how to go: there is @mrexodia 's emulator but I am not familiar enough with C++ to add scripting to it. Or I could implement an emulator in python, but this will also take quite some time as I have to start from scratch.
With regards to the emulator, in the end it didn't take me very long to implement emulation of the instructions. The main issue for me is implementing the I2C/USB/timers/interrupts because I'm not at all familiar with stuff at a level this low...
I like the progress a lot though!
With regards to the emulator, in the end it didn't take me very long to implement emulation of the instructions.
I started implementing one and I confirm instructions is not the hardest part.
The main issue for me is implementing the I2C/USB/timers/interrupts
This is indeed the hardest part.
Ports are also harder than they seem on the surface: is the port driving the line (high/low), does it have an internal pull-up, is there an external pull-up, is the line pulled low externally... I find the SN8F2288 pin schema in the spec underwhelming:
- internal pull-up is not described as controlled by pin direction. This would make sense if all pins were open-drain (so the chip could either be driving high or pulling low through the pull-up), but only 4 pins are documented as having an extra open-drain configuration. Having totem-pole outputs and pull-ups makes no sense to me.
- it is not specified whether reading a port bit reads the drive bit from register or actually samples pin state: on an open-drain the pin could be set to let the line float (1) but reading from that same pin could produce a 0 if anything else is pulling the line low.
So far I could implement pins by following my own assumptions (read samples from line, pull-up disabled on output pins), but I have not verified on the actual hardware (...because I do not have any firmware code nearly ready for a first flash). Each pin is basically a voltage divider circuit, and I compare computed voltage to logic thresholds. I'm sure it can be simplified (binary voltage & binary impedance ?) but I'm happy that this model is enough to get a bit-banging I²C implementation able to communicate.
Simulator progress:
- instructions work (I'm sure there are a few bugs left - like interrupts firing while a certain instruction is running). I have implemented them at instruction granularity level (and not cpu cycle), as I do not know what each cycle does in multi-cycle instructions and it is easier to implement interrupts this way.
- non-memory registers are implemented (ex: writing to PECMD triggers the right action, writing to WATCHDOG resets the watchdog counter, ...)
@YZimplemented, raises an exception if YZ points at@YZ(I suspect that on original chip this would be either a constant value or a "hidden" normal ram cell). I also assume pointing YZ to a register and accessing@YZdoes the same as accessing that register.- call stack is implemented, but I do not know which address the µC pushes (return address or call address). I assume return address, as it makes interrupts easier to implement.
- interrupts & reset are implemented
- clocks are implemented (Fhosc & Flosc), with all power modes supported (but untested)
- watchdog is implemented (12-bits counter on Flosc)
- timers are implemented and work, but no buzzer/external counter support for TCx.
- dummy ADC, MSP, UART, PWM, SIO placeholders. I realized that SIO is really I²C without the name. It is even on the same port as original firmware uses for bit-banging implementation, just not on the same pins. No idea why they didn't use it.
- incomplete USB implementation (no way for script to do anything with the bus yet)
- flash erase & program works (only tested on the config save page managed by original firmware)
- ports are implemented, without interrupt support
On scriptability level, it is terse:
- one-instruction step function
- write & read watchers
- current cycle number
- current run time
- overhead is huge (around 1 second to simulate 10ms of cpu time). Improving this is very low on the priority list (feature completeness comes before, as does correctness on details). I will try pypy before trying to optimize the code.
I also implemented (separately) board-specific IOs: key matrix, minimal I²C slave understanding the same protocol as the real one (...all constant bytes considered magic, I have no idea on their meaning). With that board I am able currently to at least reach a successful mouse init (about 200ms of run-time in the original firmware, including 50ms of flash page erase). I still have to test the key matrix. USB is not implemented at all (need in-CPU-emulator support for this). I²C slave implementation is ugly, with code duplication - I failed a lot on this part.
On to the next: USB support. Once working and I could get a few HID reports from the original firmware I will probably push the simulator.
EDIT: Woops, I pinged "yz" user when talking about the register. Sorry.
Once working and I could get a few HID reports from the original firmware I will probably push the simulator.
I did not go all the way to HID reports yet, but USB EP0 is working. I pushed the simulators for the CPU and the board. Next, HID reports.
On my machine (i5-5200U @ 2.20GHz) the very basic test scenario I committed (which simulates around 152ms of µC time) takes 6.7s with python2.7 and 1.9s with pypy 7.0.0 .
And a sneak peek at the output of current test scenario, which simulates USB enumeration & configuration and ends as soon as mouse init over I²C is over:
$ pypy ./ku1255_sim.py board_fw.bin
./simsn8.py:1105: UserWarning: Ignoring write to 0x00a5: 0x00
value,
./simsn8.py:1114: UserWarning: Ignoring write to 0x00c3: 0x00
value,
./simsn8.py:1114: UserWarning: Ignoring write to 0x00d3: 0xff
value,
./simsn8.py:1114: UserWarning: Ignoring write to 0x00e3: 0xff
value,
pre-address device desc: 12 01 00 02 00 00 00 08
post-address device desc: 12 01 00 02 00 00 00 08
full device desc: 12 01 00 02 00 00 00 08 ef 17 47 60 30 03 01 02 00 01
config desc head: 09 02 3b 00 02 01 00 a0
len 59
config desc: 09 02 3b 00 02 01 00 a0 32 09 04 00 00 01 03 01 01 00 09 21 00 01 00 01 22 51 00 07 05 81 03 3f 00 0a 09 04 01 00 01 03 01 02 00 09 21 00 01 00 01 22 d3 00 07 05 82 03 3f 00 0a
1
0
103.834ms start condition
Received write address, asserting SDA
Received data byte 0xfc
104.042ms stop condition
150.874ms start condition
Received read address, asserting SDA
Sending 0x80
CPU ACK
Sending 0x00
CPU ACK
Sending 0x00
CPU ACK
Sending 0x00
CPU ACK
Sending 0x00
CPU NACK
151.498ms stop condition
152.124ms start condition
Received write address, asserting SDA
Received data byte 0xc4
A very short progress report this time: I can get an HID report from mouse (but I did not test very intensely either), but key matrix is full of bugs:
- I can get some keycodes but not all, and it seems to depend on what key presses happened before, or what delay between presses. As each keypress needs thousands of instructions to run, I did not debug step-by-step so far. I suspect a bug in
Portclass and/or related impedance-retrieval functions. - USB endpoints are behaving differently from the real keyboard: on real keyboard transfers are only ACK'ed by device when there is something to send: either a key press, or a key release. In my test code, there seems to be a pump-priming effect, where the first key press causes a report to be sent but after that they never stop. Which means reports and keypresses get out of sync, and I can't be sure what key state the report corresponds to. Adding super-long sleeps (to let µC run and hopefully stabilise) does not seem to help.
I think I need to step back for a while to stop running in circles.