Firmware, TOC
parent
36594d6257
commit
a45b40ebf1
283
README.md
283
README.md
|
|
@ -1,9 +1,41 @@
|
|||
|
||||
# Guide for designing a custom Topre keyboard
|
||||
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](https://deskthority.net/workshop-f7/designing-a-custom-topre-board-t11734.html),
|
||||
[Split HHKB - Realforce/TypeHeaven mod](https://deskthority.net/workshop-f7/another-custom-split-hand-topre-board-need-your-input-t14769.html).
|
||||
|
||||
## Circuitry
|
||||
If anything is unclear or needs adding, let me know.
|
||||
|
||||
### Overview
|
||||
Thanks to [hasu's research](https://github.com/tmk/tmk_keyboard/tree/master/keyboard/hhkb/doc)
|
||||
for starting this all off.
|
||||
|
||||
**Table of contents**
|
||||
|
||||
1. [Circuitry](#circuitry)
|
||||
1. [Basic schematic](#basic-schematic)
|
||||
2. [Drain pin](#drain-pin)
|
||||
3. [Practical considerations](#practical-considerations)
|
||||
1. [Parasitic capacitance](#parasitic-capacitance)
|
||||
2. [Other notes](#other-notes)
|
||||
4. KiCad files
|
||||
2. [Hardware (case/plate)](#hardware-caseplate)
|
||||
3. [Firmware](#firmware)
|
||||
1. [Basic read procedure](#basic-read-procedure)
|
||||
2. [Normalisation](#normalisation)
|
||||
3. [Calibration](#calibration)
|
||||
1. [Overview](#overview)
|
||||
2. [Calibration procedure](#calibration-procedure)
|
||||
3. [A warning](#a-warning)
|
||||
4. [Handling the depths](#handling-the-depths)
|
||||
1. [Digital conversion](#digital-conversion)
|
||||
2. [Analog](#analog)
|
||||
5. [Example](#example)
|
||||
|
||||
|
||||
# Circuitry
|
||||
|
||||
## Basic schematic
|
||||
|
||||
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
|
||||
|
|
@ -20,9 +52,7 @@ following schematic:
|
|||

|
||||
|
||||
Each read line is pulled to ground with an individual 22k resistor, and fed
|
||||
into an analog multiplexer. Any unused inputs of the multiplexer should be
|
||||
grounded to prevent additional sources of noise: this goes for any unused op
|
||||
amp pins too. After selecting a read line on the multiplexer, the
|
||||
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.
|
||||
|
||||
|
|
@ -49,12 +79,10 @@ 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
|
||||
by `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. I found it important to use a very fast amplifier here,
|
||||
opting for the OPA350A. Cheaper options proved to be too slow, turning the
|
||||
voltage spike into more of a voltage mound. The output of the amplifier should
|
||||
connect to an ADC pin of the microcontroller.
|
||||
line returns to ground. The output of the amplifier should connect to an ADC
|
||||
pin of the microcontroller.
|
||||
|
||||
### Drain pin
|
||||
## Drain pin
|
||||
|
||||
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
|
||||
|
|
@ -76,12 +104,239 @@ resistor formula:
|
|||
```
|
||||
|
||||
which gives `R ~ 950 Ohms` for our chosen values. Recalculating the relax time
|
||||
now gives `5 * 950 Ohms * 1 nF ~ 5 us` - much faster.
|
||||
now gives `5 * 950 Ohms * 1 nF ~ 5 us`. This would allow 1000 Hz polling for
|
||||
even 100 key keyboards.
|
||||
|
||||
## Practical considerations
|
||||
|
||||
## Hardware (case/plate)
|
||||
## Parasitic capacitance
|
||||
|
||||
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.
|
||||
|
||||
## Firmware
|
||||
## Other notes
|
||||
|
||||
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.
|
||||
|
||||
# Hardware (case/plate)
|
||||
|
||||
The stackup is fairly simple, and is determined by the housing dimensions:
|
||||
|
||||
```
|
||||
1.2 mm thick steel plate
|
||||
3/16 inch spacers
|
||||
PCB
|
||||
```
|
||||
TODO: hole sizes drawings, warn about Realforce control key rotation, dealing
|
||||
with clones, dome cutting.
|
||||
|
||||
# Firmware
|
||||
|
||||
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).
|
||||
|
||||
## Basic read procedure
|
||||
|
||||
Global variables
|
||||
```
|
||||
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)
|
||||
```
|
||||
|
||||
Matrix scan
|
||||
```
|
||||
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](#calibration).
|
||||
|
||||
## Normalisation
|
||||
|
||||
See [this wikipedia article](https://en.wikipedia.org/wiki/Feature_scaling#Rescaling).
|
||||
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);
|
||||
```
|
||||
|
||||
## Calibration
|
||||
|
||||
### Overview
|
||||
|
||||
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](#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.
|
||||
|
||||
### Calibration procedure
|
||||
|
||||
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.
|
||||
|
||||
### A warning
|
||||
|
||||
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 `screen`), you
|
||||
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.
|
||||
|
||||
### Digital conversion
|
||||
|
||||
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.
|
||||
|
||||
### Analog
|
||||
|
||||
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.
|
||||
|
||||
## Example
|
||||
|
||||
See [my fork of Kiibohd](https://github.com/tomsmalley/controller),
|
||||
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.
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
Loading…
Reference in New Issue