This is an incomplete guide for building a custom Topre keyboard: specifically the PCB, plate, and firmware. It is an accumulation of information gained through these projects: designing a custom Topre board, Split HHKB - Realforce/TypeHeaven mod.
If anything is unclear or needs adding, let me know.
Thanks to hasu's research for starting this all off.
Table of contents
- Hardware (case/plate)
The method I use to sense key depression is rather simple. In tests that I have done it works well provided some calibration is performed in the firmware to normalise the readings.
The matrix crossing points of a Topre keyboard are essentially variable capacitors which connect a "strobe" line to a "read" line. The strobe and the read lines form the electrodes directly on the PCB, and the conical spring under the dome couples them together, creating a variable capacitor with the range 0 ~ 6 pF (roughly). The strobe lines are just digital signals from a digital out pin of the microcontroller. The read lines are dealt with in the following schematic:
Each read line is pulled to ground with an individual 22k resistor, and fed into an analog multiplexer. After selecting a read line on the multiplexer, the microcontroller strobes a column and a small voltage pulse can be seen on the selected read line, larger pulses correspond to greater key depression.
The selected read line is connected to the capacitor C1, which causes the read line to behave like a simple RC decay circuit. The value can be chosen given the following formula:
Capacitance of key we are sensing Peak output voltage = Input voltage * ----------------------------------- Total row capacitance
Here the input voltage is
Vdd. The total row capacitance (to ground) consists
of C1 plus the capacitance of all the keys in the row. As such it is clear that
choosing a large value for C1 (compared to key capacitance) is important so
that our reading is not significantly altered due to other keys on the row
being depressed. We can't just make C1 enormous though, because it drops the
peak output voltage which ultimately contributes to a higher noise level. I
found 1 nF to be a good value.
Ignoring the "drain pin" for now, the read line passes through a current
limiting resistor into a non inverting amplifier. The purpose of this is to
provide a clean signal boost back into the range of 0 - 3.3V. The gain is given
1 + R2 / R4 which in this case is around 200. It also serves to protect
the microcontroller from negative voltages which can happen when the strobe
line returns to ground. The output of the amplifier should connect to an ADC
pin of the microcontroller.
With the selected read line forming an RC circuit we can see that the time for
it to relax to ground is simply governed by
5 * RC time constant. The time
constant is just
R * C, which in our case gives a total relax time of
5 * 22k Ohm * 1 nF ~ 100 us. Bearing in mind that we must wait for the matrix
to relax to ground before reading the next key, this translates to taking 100
us per key of the keyboard - giving us a polling rate less than 1000 Hz for
keyboards with more than 10 keys. In order to fix this, we just connect the
read line to the pin of the microcontroller through a current limiting 1k
resistor R1. This pin should be floating during the strobe and read process,
but after we have captured the reading in the ADC (takes around 5 us)
it can be grounded, reducing the resistance R according to the parallel
1 1 1 --- = ---- + ----- R R1 22k
R ~ 950 Ohms for our chosen values. Recalculating the relax time
5 * 950 Ohms * 1 nF ~ 5 us. This would allow 1000 Hz polling for
even 100 key keyboards.
When routing any analog lines (and to some extent the digital strobe lines) care must be taken to reduce parasitic capacitance due to the low signal level. The lines must be surrounded by a ground pour and have a ground plane on surrounding layers. Crossing tracks should ideally occur with a ground plane between them, but this requires a 4 layer PCB. 2 layers is perfectly fine, as long as you ensure traces cross at right angles and do so as little as possible. Basically just don't run the strobe lines (or other digital lines) close to the analog lines where you can avoid it! Study the Topre PCBs to see the careful routing of the matrix.
Any unused inputs of the multiplexer should be grounded to prevent additional sources of noise: this goes for any unused op amp pins too. If they are not connected, ground them.
I found it important to use a very fast amplifier, opting for the OPA350A. Cheaper options proved to be too slow, turning the voltage spike into more of a voltage mound, making reading unpredictable.
kicad folder, it contains an example switch footprint and schematic
The stackup is fairly simple, the spacer size is determined by the housing dimensions:
1.2 mm thick steel plate 3/16 inch (length) spacers PCB
The PCB must be securely fastened to the plate - the force from keypresses (and the force of pressing the keycaps onto the sliders) is transferred to the PCB, not the plate. Making sure there are enough fastening points is also important to keep the housings firmly sitting on the PCB and holding the domes down, or you may end up with a "squishing" sound when keys are at the bottom of the stroke. Put some fasteners in the central sections of the keyboard to keep it rigid - copy Topre if in doubt.
The sizes shown in this section are for Topre keyboards - not clones! Clones seem to be 14 x 14 mm square for 1U keys. They also require more consideration of fastener positions since they don't have the semicircular cutouts for bolts to pass through.
Single unit wide key housings require a hole of size 14.6 mm x 14 mm, they are wider horizontally. The corners can be chamfered 1 mm along each edge, as Topre does.
Double unit key housings (backspace, shift keys, ANSI enter key) should be 32.4 mm x 14 mm. I made mine 32.6 mm but they are slightly too large. The bottom edge can have chamfers in the corners, like Topre does: around 3 mm along the bottom edge with a 30 degree angle.
See example-split-hhkb.dxf for example sizes (The double unit keys are too wide though, as I mention above).
Warning about Realforce
Realforce keyboards (I don't know if this is true of all of them, but certainly the ANSI 55g 87u that I have) have a strange bottom left control key. The stem is rotated by 90 degrees, and so a housing which can hold this keycap must be rotated as well. Since they are not square this is a problem! If you want to support moving the control key around (swapping with alt, for example) both should be made 14.6 x 14.6 mm. The PCB will hold the housing when the keyboard is built.
You will need to modify a firmware quite substantially to get your keyboard working. Your initial choice depends on your chosen microcontroller and preferences in firmware. You need access to an ADC and some EEPROM memory in order to store calibration values: at least 2 bytes per key, if the stored values take 1 byte each (uint8_t). Using something like a Teensy is perfectly fine, the initial work I did was using a Teensy 3.1/3.2.
Basic read procedure
relaxTime (5 * R * C, include drain pin if using) state[num reads, num strobes] a structure containing: depth (current depth of key) pressed (whether the key is currently "pressed", see digital conversion)
for each read: select read line on multiplexer delay for relaxTime for each strobe: value <- strobeRead(strobe) depth <- normalise(strobe, read, value) state->depth[read, strobe] <- depth
Read value on ADC
strobeRead(strobe): static lastTime wait until lastTime + relaxTime < current time float drain pin disable interrupts (could throw timing off) set strobe high value <- read on ADC set strobe low enable interrupts ground drain pin lastTime <- current time return value
The values which come out of the strobeRead function are assumed to be 8 bit unsigned integers, that is, 0 to 255. This directly correlates to the voltage of the read line, and also the capacitance of the key. Thanks to Topre's method of using a conical spring, the capacitance of the key is linear with key depression. So the output of strobeRead can be taken as the key depth!
Of course it isn't quite so simple, because each key will have an offset value from ground and a different peak value when the key is fully depressed. As such, we need to be able to rescale each key to find the true depth. For example, if a key is reading on average 34 when unpressed and 200 when pressed, we must rescale this into the range [0, 255]. These values should be stored in EEPROM for each key, and can be determined by the calibration procedure.
See this wikipedia article. In the example procedure, we are rescaling to the range [0, 255].
Example procedure for uint8_t values, with no floating point operations
uint8_t normalise(strobe, read, uint8_t value): (calLow, calHigh) <- calibrationValues(strobe, read) // clamp to minimum and maximum values if (value < calLow) value <- calLow else if (value > calHigh) value <- calHigh uint16_t numerator <- 0xFF * (value - calLow) uint8_t denominator <- calHigh - calLow return (uint8_t) (numerator / denominator);
Repeated readings from the ADC for a single key might look something like this:
This shows a full key press and release as the ADC reading crosses above the actuation depth, and later below the release depth. We are interested in finding the average value when a key is unpressed, and the average value when a key is pressed, so that we can normalise values easily. The values HighMax, LowMax, and LowMin which are marked on the plot are easily measurable (see calibration procedure) and will allow us to calculate the averages we need. We take the noise to be
noise = lowMax - lowMin
and the signal to noise ratio to be
highMax - lowMax SNR = ------------------ noise
This can be a convenient read on how good a key is. Generally we are looking for at least SNR > 10, I've seen most values come in between 20 and 30 for the above design.
The values lowAvg and highAvg are just
lowMax + lowMin lowAvg = ----------------- 2
noise highAvg = highMax - ------- 2
These should be stored in EEPROM for the normalisation routine.
First we determine the values lowMax and lowMin. This is simple: just leave the keyboard untouched while scanning the matrix for a short period of time (a few seconds should do). For each key, keep a log on the highest and lowest measurements, this will give you lowMax and lowMin respectively.
Finding highMax can be done by scanning the matrix while the user presses each key in turn, keeping track of the highest measurement.
That's enough for a basic calibration routine, and I have found that is all that is required for stable measurements. To improve upon this, you might introduce continual recalibration via a more advanced signal processing method.
It is useful to have some sort of escape route from the main key detection
routine (for example a digital button which interrupts sending keypresses to
the host). When we are scanning the matrix at 1000Hz and you mess something up,
for example your calibration values are wrong and you are normalising sane
input values to 0 or 255 randomly, you could end up sending hundreds of
keypresses per second to the host mistakenly, and if your only method of
interrupting is via your serial connection (for example through
will have a bad time.
Handling the depths
Now we know how to collect the normalised depth of each key, we must do something with it.
The most basic method of converting a normalised key depth to a digital value is to check if a key is deeper than the desired actuation point. This won't work very well, and will spam the host with keypresses thanks to noisy readings. We must introduce hysteresis in the measurement of the key depth, we do this by storing key press state as well as depth.
A simple procedure for a single key is as follows:
if (not pressed and depth > actuation depth): pressed <- true send key press signal else if (pressed and depth < release depth): pressed <- false send key release signal
The actuation depth should be chosen somewhere around the middle. Don't set it too close to 0 or 255 or you risk missing presses or having false presses thanks to noisy readings. The calibration procedure should determine the noise levels, and you can set a buffer value accordingly (use a few times the measured noise value for safety). As a fallback, I find that giving a buffer of around 30 works nicely for the range 0-255. The release depth should be the actuation depth minus this buffer value.
Theoretically you could pipe the depth directly to an axis, be it a controller axis or something more interesting like analog mouse keys. Noisy readings mean you probably need to introduce a deadzone.
With split keyboards it is important to share the depth information over the connection, instead of just sending keypresses digitally. This is so the master can handle analog commands on the slave.
See my fork of Kiibohd, specifically Scan/SplitHHKB. Warning: it is not complete and basically is broken, it crashes after a short time. The Topre part works fine though, it's my hacked together interconnect solution that is the problem.