Skip to content

Commit

Permalink
Analog matrix support:
Browse files Browse the repository at this point in the history
commit d91fe0f
Merge: 240da6f a5b5ff2
Author: Univa <41708691+Univa@users.noreply.github.com>
Date:   Sat Mar 23 14:36:11 2024 -0400

    Merge branch 'main' into analog-matrix

commit 240da6f
Author: Univa <41708691+Univa@users.noreply.github.com>
Date:   Sat Mar 23 14:10:47 2024 -0400

    basic support for analog matrices

commit d1d7095
Author: Univa <41708691+Univa@users.noreply.github.com>
Date:   Thu Mar 21 00:05:21 2024 -0400

    fix broken backlight macros
  • Loading branch information
Univa committed Mar 23, 2024
1 parent a5b5ff2 commit 4b7ac44
Show file tree
Hide file tree
Showing 15 changed files with 1,705 additions and 91 deletions.
75 changes: 75 additions & 0 deletions docs/src/content/docs/getting-started/matrix-and-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,81 @@ Each row must have the same number of columns. If there are matrix positions tha
In this example, the switch connected to `PB10` maps to row 0, column 1. Based on the implementation of `KeyboardLayout`, this
switch will correspond to the `Q`/`F1` key.

## Analog matrix

:::caution
Analog matrices are a work in progress, and may not be fully stable.
:::

If your switch is powered by an analog-to-digital conversion peripheral (which is usually the case with hall-effect switches, for example),
then you can use the `build_analog_matrix!` macro. In addition, you will need to specify an ADC sampler configuration, using the `setup_adc_sampler!`
macro.

```rust ins={4-15,17-29}
// rest of your config...

// Create an ADC sampler, where the pins of the MCU are either connected to a multiplexer, or directly to the analog source
setup_adc_sampler! {
// (interrupt, ADC peripheral) => { ...
(ADC1_2, ADC2) => {
Multiplexer {
pin: PA2 // MCU analog pin connected to a multiplexer
select_pins: { PA3 No PA4 } // Pins connected to the selection pins on the multiplexer
},
Direct {
pin: PA5 // MCU analog pin connected directly to an analog source
},
}
}

use rumcake::keyboard::{build_analog_matrix, KeyboardMatrix};
impl KeyboardMatrix for MyKeyboard {
build_analog_matrix! {
{
[ (1,0) (0,1) (0,4) (0,5) ]
[ (0,0) No No No ]
}
{
[ 3040..4080 3040..4080 3040..4080 3040..4080 ]
[ 3040..4080 No No No ]
}
}
}
```

Firstly, an ADC sampler definition is provided. In this example, the `ADC2` peripheral (controlled by the `ADC1_2` interrupt), is connected to two pins.
Pin `PA2` is connected to a multiplexer, and pin `PA5` is connected directly to the analog source (a switch in this case).

For `PA2`, the multiplexer output selection is controlled by `PA3` and `PA4`. The second select pin is unused, so that is denoted with `No`.
Pins are ordered least-significant bit first. So, if `PA4` is high and `PA2` is low, multiplexer output `4` is selected.

:::note
All multiplexer definitions in `setup_adc_sampler!` must have the same number of select pins. If you have multiplexers with varying numbers of
select pins, you can pad the smaller multiplexers with `No`s until the definitions have the same number of select pins.
:::

:::note
Note that the arguments of the `setup_adc_sampler!` macro will depend on the platform that you're building for.
Check the API reference for specific arguments that you need to call `setup_adc_sampler!`
:::

The matrix provided by `build_analog_matrix!` serves two purposes:

- Define a mapping from matrix position (row, col) to analog pin index and multiplexer output (if applicable).
- Define the possible ranges of values that the analog source can generate from the ADC process.

When we take a look at row 0, col 0 on the matrix we find:

- It corresponds to ADC pin `0` (which is connected to the multiplexer, `PA2`), and multiplexer output `0` (when the select pins `PA3` and `PA4` are set low).
- It is expected to yield values ranging from `3040` to `4080` from the ADC.

For row 1, col 0 on the matrix, we find:

- It corresponds to ADC pin `1` (which is connected directly to the analog source via `PA5`). The `0` in `(1,0)` is ignored, since it is not connected to a multiplexer.
- It is expected to yield values ranging from `3040` to `4080` from the ADC.

Note that unused matrix positions are denoted by `No`.

# Revisualizing a matrix (e.g. duplex matrix)

Sometimes, your keyboard might have a complicated matrix scheme that could make it
Expand Down
231 changes: 231 additions & 0 deletions keyberon/src/analog.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
//! Actuator definition.
//!
//! The actuator provides different mechanisms to determine when a key is considered actuated. Due
//! to the unstable signals generated by an analog source, the actuator uses thresholds to
//! determine whether the user intends to release or press a given key. The actuator can be
//! customized to change the acutation points for each switch, along with the mode that determines
//! when it actuates.

use crate::layout::Event;

/// Errors when working with the [`AnalogActuator`]
pub enum AnalogActuatorError {
/// The provided row and column pair does not exist in the analog matrix.
InvalidLocation,
}

/// Different modes to determine when a switch is actuated
#[derive(Default, Clone, Copy)]
pub enum AnalogAcutationMode {
/// Key is considered actuated when the key is past the actuation point, and released when it
/// is above the actuation point.
#[default]
Static,
/// Actuations are registered any time the user represses a key below the actuation point.
/// Releases are registered as soon as the user starts to release.
Rapid,
/// Similar to dynamic, but once the user goes past the actuation point, subsequent actuations
/// ignore the actuation point. So, a user can repress a key to register an actuation regardless
/// of whether they are above or below the actuation point. Once the user fully releases the
/// key, the user will need to go past the actuation point again.
ContinuousRapid,
}

/// Analog matrix actuator
pub struct AnalogActuator<const CS: usize, const RS: usize> {
press_threshold: u8,
release_threshold: u8,
modes: [[AnalogAcutationMode; CS]; RS],
actuation_points: [[u8; CS]; RS],
cur_state: [[u8; CS]; RS],
new_state: [[u8; CS]; RS],
cur_actuated: [[bool; CS]; RS],
}

impl<const CS: usize, const RS: usize> AnalogActuator<CS, RS> {
/// Create a new actuator.
///
/// You must provide the default actuation mode for each key position, and their actuation points.
/// Note that 255 represents a fully pressed switch, while 0 represents an unpressed switch.
pub const fn new(
modes: [[AnalogAcutationMode; CS]; RS],
actuation_points: [[u8; CS]; RS],
) -> Self {
Self {
modes,
press_threshold: 5,
release_threshold: 5,
cur_state: [[0; CS]; RS],
new_state: [[0; CS]; RS],
cur_actuated: [[false; CS]; RS],
actuation_points,
}
}

/// Update the actuation mode for a given key.
pub fn set_mode(
&mut self,
row: usize,
col: usize,
mode: AnalogAcutationMode,
) -> Result<(), AnalogActuatorError> {
self.modes
.get_mut(row)
.and_then(|row| {
row.get_mut(col).map(|key| {
*key = mode;
})
})
.ok_or(AnalogActuatorError::InvalidLocation)
}

/// Set the actuation point for a given key. A value of 0 represents an unpressed state,
/// and a value of 255 should represent a fully pressed switch.
pub fn set_actuation_point(
&mut self,
row: usize,
col: usize,
value: u8,
) -> Result<(), AnalogActuatorError> {
self.actuation_points
.get_mut(row)
.and_then(|row| {
row.get_mut(col).map(|key| {
*key = value;
})
})
.ok_or(AnalogActuatorError::InvalidLocation)
}

/// Update the threshold to register a key press
pub fn set_press_threshold(&mut self, press_threshold: u8) {
self.press_threshold = press_threshold;
}

/// Update the threshold to register a key release
pub fn set_release_threshold(&mut self, release_threshold: u8) {
self.release_threshold = release_threshold;
}

/// Iterates on the `Event`s generated by the update.
///
/// `T` must be some kind of array of array of u8.
///
/// # Example
///
/// ```
/// use keyberon::analog::{AnalogAcutationMode, AnalogActuator};
/// use keyberon::layout::Event;
/// let mut actuator = AnalogActuator::new(
/// [[AnalogAcutationMode::Static; 2]; 2],
/// [[127; 2]; 2],
/// );
///
/// // no changes
/// assert_eq!(0, actuator.events([[0, 0], [0, 0]]).count());
///
/// // `(0, 1)` is pressed.
/// assert_eq!(
/// vec![Event::Press(0, 1)],
/// actuator.events([[0, 255], [0, 0]]).collect::<Vec<_>>(),
/// );
/// ```
pub fn events(&mut self, new: [[u8; CS]; RS]) -> impl Iterator<Item = Event> + '_ {
self.new_state = new;

let press_threshold = self.press_threshold;
let release_threshold = self.release_threshold;

self.cur_state
.iter_mut()
.zip(self.cur_actuated.iter_mut())
.zip(self.actuation_points.iter())
.zip(self.modes.iter())
.zip(self.new_state.iter())
.enumerate()
.flat_map(move |(row, ((((o, a), p), m), n))| {
o.iter_mut()
.zip(a.iter_mut())
.zip(p.iter())
.zip(m.iter())
.zip(n.iter())
.enumerate()
.filter_map(
move |(col, ((((cur, actuated), actuation_point), mode), new))| {
let mut event = None;

match mode {
AnalogAcutationMode::Static => {
if *actuated
&& *new < actuation_point.saturating_sub(release_threshold)
{
*actuated = false;
event = Some(Event::Release(row as u8, col as u8));
} else if !*actuated
&& *new >= actuation_point.saturating_add(press_threshold)
{
*actuated = true;
event = Some(Event::Press(row as u8, col as u8));
};
*cur = *new;
}
AnalogAcutationMode::Rapid => {
if *actuated {
if *new < cur.saturating_sub(release_threshold) {
// Check for releases
*actuated = false;
event = Some(Event::Release(row as u8, col as u8));
*cur = *new;
} else if *new > *cur {
// If the user presses the key further, update cur
*cur = *new;
}
} else if *new > cur.saturating_add(press_threshold)
&& *new >= *actuation_point
{
// Check for presses
*actuated = true;
event = Some(Event::Press(row as u8, col as u8));
*cur = *new;
} else if *new < *cur {
// If the user releases the key further, update cur
*cur = *new
};
}
AnalogAcutationMode::ContinuousRapid => {
if *actuated {
if *new < cur.saturating_sub(release_threshold) {
// Check for releases
*actuated = false;
event = Some(Event::Release(row as u8, col as u8));
*cur = *new;
} else if *new > *cur {
// If the user presses the key further, update cur
*cur = *new;
}
} else if *cur == 0 {
// If the key was fully released, only register an actuation if
// we go past the actuation point.
if *new >= *actuation_point {
*actuated = true;
event = Some(Event::Press(row as u8, col as u8));
*cur = *new;
};
} else if *new > cur.saturating_add(press_threshold) {
// Check for presses
*actuated = true;
event = Some(Event::Press(row as u8, col as u8));
*cur = *new;
} else if *new < *cur {
// If the user releases the key further, update cur
*cur = *new;
}
}
}

event
},
)
})
}
}
1 change: 1 addition & 0 deletions keyberon/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use usb_device::bus::UsbBusAllocator;
use usb_device::prelude::*;

pub mod action;
pub mod analog;
pub mod chording;
pub mod debounce;
pub mod hid;
Expand Down
52 changes: 52 additions & 0 deletions keyberon/src/matrix.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
//! Hardware pin switch matrix handling.

use core::ops::Range;

use embedded_hal::digital::v2::{InputPin, OutputPin};
use num_traits::{clamp, SaturatingSub};

/// Describes the hardware-level matrix of switches.
///
Expand Down Expand Up @@ -135,3 +138,52 @@ where
Ok(keys)
}
}

/// Matrix where switches generate an analog signal (e.g. hall-effect switches or
/// electrocapacitive). When a key in an analog matrix is pressed, the sampled value returned by
/// the ADC may fall within different ranges. Yielded values can depend on the HAL, and hardware
/// used for the analog-to-digital conversion process. Thus, these values can vary between
/// keyboards and even individual keys. Ranges of values for each key will need to be provided to
/// normalize the analog signal into an 8-bit integer, where 0 represents an unpressed key, and 255
/// represents a fully-pressed key.
///
/// Generic parameters are in order: Raw type returned when sampling your ADC, the number of
/// columns and rows.
pub struct AnalogMatrix<T, const CS: usize, const RS: usize> {
ranges: [[Range<T>; CS]; RS],
}

impl<T, const CS: usize, const RS: usize> AnalogMatrix<T, CS, RS> {
/// Create a new AnalogMatrix
pub fn new(ranges: [[Range<T>; CS]; RS]) -> Self {
Self { ranges }
}
}

impl<T: SaturatingSub + PartialOrd, const CS: usize, const RS: usize> AnalogMatrix<T, CS, RS>
where
u32: From<T>,
{
/// Scan the matrix, and obtain the analog signal generated by each switch. The
/// `get_press_value` function should return the raw ADC sample for the given key.
pub fn get<E>(
&mut self,
get_press_value: impl Fn(usize, usize) -> Result<T, E>,
) -> Result<[[u8; CS]; RS], E> {
let mut keys = [[0; CS]; RS];

keys.iter_mut().enumerate().try_for_each(|(row, cols)| {
cols.iter_mut().enumerate().try_for_each(|(col, key)| {
let value = get_press_value(row, col)?;
let Range { start, end } = &self.ranges[row][col];
*key = ((u32::from(clamp(&value, start, end).saturating_sub(start)))
.saturating_mul(255)
/ u32::from(end.saturating_sub(start))) as u8;
Ok(())
})?;
Ok(())
})?;

Ok(keys)
}
}
Loading

0 comments on commit 4b7ac44

Please sign in to comment.