kiibohd_keyscanning/lib.rs
1// Copyright 2021 Zion Koyl
2// Copyright 2021-2023 Jacob Alexander
3//
4// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
5// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
6// http://opensource.org/licenses/MIT>, at your option. This file may not be
7// copied, modified, or distributed except according to those terms.
8
9#![no_std]
10
11pub mod state;
12
13pub use self::state::{KeyState, State};
14use embedded_hal::digital::v2::{InputPin, IoPin, OutputPin, PinState};
15
16#[cfg(feature = "kll-core")]
17pub trait KeyScanning<const MAX_EVENTS: usize> {
18 fn generate_events(&self, index: usize) -> kll_core::layout::TriggerEventIterator<MAX_EVENTS>;
19}
20
21/// Records momentary push button events
22///
23/// Cycles can be converted to time by multiplying by the scan period (Matrix::period())
24#[derive(Copy, Clone, Debug, PartialEq, Eq)]
25#[cfg_attr(feature = "defmt", derive(defmt::Format))]
26pub enum KeyEvent {
27 On {
28 /// Cycles since the last state change
29 cycles_since_state_change: u32,
30 },
31 Off {
32 /// Key is idle (a key can only be idle in the off state)
33 idle: bool,
34 /// Cycles since the last state change
35 cycles_since_state_change: u32,
36 },
37}
38
39/// This struct handles scanning and strobing of the key matrix.
40///
41/// It also handles the debouncing of key input to ensure acurate keypresses are being read.
42/// OutputPin's are passed as columns (cols) which are strobed.
43/// IoPins are functionally InputPins (rows) which are read. Rows are IoPins in order to drain the
44/// row/sense between strobes to prevent stray capacitance.
45///
46/// ```rust,ignore
47/// const CSIZE: usize = 18; // Number of columns
48/// const RSIZE: usize = 6; // Number of rows
49/// const MSIZE: usize = RSIZE * CSIZE; // Total matrix size
50/// // Period of time it takes to re-scan a column (everything must be constant time!)
51/// const SCAN_PERIOD_US = 40;
52/// // Debounce timer in us. Can only be as precise as a multiple of SCAN_PERIOD_US.
53/// // Per-key timer is reset if the raw gpio reading changes for any reason.
54/// const DEBOUNCE_US = 5000; // 5 ms
55/// // Idle timer in ms. Only valid if the switch is in the off state.
56/// const IDLE_MS = 600_0000; // 600 seconds or 10 minutes
57///
58/// let cols = [
59/// pins.strobe1.downgrade(),
60/// pins.strobe2.downgrade(),
61/// pins.strobe3.downgrade(),
62/// pins.strobe4.downgrade(),
63/// pins.strobe5.downgrade(),
64/// pins.strobe6.downgrade(),
65/// pins.strobe7.downgrade(),
66/// pins.strobe8.downgrade(),
67/// pins.strobe9.downgrade(),
68/// pins.strobe10.downgrade(),
69/// pins.strobe11.downgrade(),
70/// pins.strobe12.downgrade(),
71/// pins.strobe13.downgrade(),
72/// pins.strobe14.downgrade(),
73/// pins.strobe15.downgrade(),
74/// pins.strobe16.downgrade(),
75/// pins.strobe17.downgrade(),
76/// pins.strobe18.downgrade(),
77/// ];
78///
79/// let rows = [
80/// pins.sense1.downgrade(),
81/// pins.sense2.downgrade(),
82/// pins.sense3.downgrade(),
83/// pins.sense4.downgrade(),
84/// pins.sense5.downgrade(),
85/// pins.sense6.downgrade(),
86/// ];
87///
88/// let mut matrix = Matrix::<OutputPin, InputPin, CSIZE, RSIZE, MSIZE, SCAN_PERIOD_US, DEBOUNCE_US,
89/// IDLE_MS>::new(cols, rows);
90///
91/// // Prepare first strobe
92/// matrix.next_strobe().unwrap();
93///
94/// // --> This next part must be done in constant time (SCAN_PERIOD_US) <--
95/// let state = matrix.sense().unwrap();
96/// matrix.next_strobe().unwrap();
97/// ```
98pub struct Matrix<
99 C: OutputPin,
100 R: InputPin,
101 const CSIZE: usize,
102 const RSIZE: usize,
103 const MSIZE: usize,
104 const SCAN_PERIOD_US: u32,
105 const DEBOUNCE_US: u32,
106 const IDLE_MS: u32,
107> {
108 /// Strobe GPIOs (columns)
109 cols: [C; CSIZE],
110 /// Sense GPIOs (rows)
111 rows: [R; RSIZE],
112 /// Current GPIO column being strobed
113 cur_strobe: usize,
114 /// Recorded state of the entire matrix
115 state_matrix: [KeyState<CSIZE, SCAN_PERIOD_US, DEBOUNCE_US, IDLE_MS>; MSIZE],
116}
117
118impl<
119 C: OutputPin,
120 R: InputPin,
121 const CSIZE: usize,
122 const RSIZE: usize,
123 const MSIZE: usize,
124 const SCAN_PERIOD_US: u32,
125 const DEBOUNCE_US: u32,
126 const IDLE_MS: u32,
127 > Matrix<C, R, CSIZE, RSIZE, MSIZE, SCAN_PERIOD_US, DEBOUNCE_US, IDLE_MS>
128{
129 pub fn new<'a, E: 'a>(cols: [C; CSIZE], rows: [R; RSIZE]) -> Result<Self, E>
130 where
131 C: OutputPin<Error = E>,
132 E: core::convert::From<<C as OutputPin>::Error>,
133 {
134 let state_matrix = [KeyState::<CSIZE, SCAN_PERIOD_US, DEBOUNCE_US, IDLE_MS>::new(); MSIZE];
135 let mut res = Self {
136 cols,
137 rows,
138 cur_strobe: CSIZE - 1,
139 state_matrix,
140 };
141
142 // Reset strobe position and make sure all strobes are off
143 res.clear()?;
144 Ok(res)
145 }
146
147 /// Clears strobes
148 /// Resets strobe counter to the last element (so next_strobe starts at 0)
149 pub fn clear<'a, E: 'a>(&'a mut self) -> Result<(), E>
150 where
151 C: OutputPin<Error = E>,
152 {
153 // Clear all strobes
154 for c in self.cols.iter_mut() {
155 c.set_low()?;
156 }
157
158 // Reset strobe position
159 self.cur_strobe = CSIZE - 1;
160 Ok(())
161 }
162
163 /// Next strobe
164 pub fn next_strobe<'a, E: 'a>(&'a mut self) -> Result<usize, E>
165 where
166 C: OutputPin<Error = E> + IoPin<R, C>,
167 R: InputPin<Error = E> + IoPin<R, C>,
168 E: core::convert::From<<R as IoPin<R, C>>::Error>
169 + core::convert::From<<C as IoPin<R, C>>::Error>,
170 {
171 // Unset current strobe
172 self.cols[self.cur_strobe].set_low()?;
173
174 // Drain stray potential from sense lines
175 // NOTE: This is unsafe because the gpio are stored in an array and (likely) do not implement
176 // copy or clone. Since they are in an array, we can't move them either.
177 // Since we're just temporarily sinking the pin and putting it back, this is safe to
178 // do.
179 for s in self.rows.iter_mut() {
180 let ptr = s as *const R;
181 unsafe {
182 let row = core::ptr::read(ptr);
183 // Temporarily sink sense gpios and reset to sense/read gpio
184 row.into_output_pin(PinState::Low)?.into_input_pin()?;
185 }
186 }
187
188 // Check for roll-over condition
189 if self.cur_strobe >= CSIZE - 1 {
190 self.cur_strobe = 0;
191 } else {
192 self.cur_strobe += 1;
193 }
194
195 // Set new strobe
196 self.cols[self.cur_strobe].set_high()?;
197
198 Ok(self.cur_strobe)
199 }
200
201 /// Current strobe
202 pub fn strobe(&self) -> usize {
203 self.cur_strobe
204 }
205
206 /// Sense a column of switches
207 ///
208 /// Returns the results of each row for the currently strobed column and the measured strobe
209 pub fn sense<'a, E: 'a>(&'a mut self) -> Result<([KeyEvent; RSIZE], usize), E>
210 where
211 E: core::convert::From<<R as InputPin>::Error>,
212 {
213 let mut res = [KeyEvent::Off {
214 idle: false,
215 cycles_since_state_change: 0,
216 }; RSIZE];
217
218 for (i, r) in self.rows.iter().enumerate() {
219 // Read GPIO
220 let on = r.is_high()?;
221 // Determine matrix index
222 let index = self.cur_strobe * RSIZE + i;
223 // Record GPIO event and determine current status after debouncing algorithm
224 let (keystate, idle, cycles_since_state_change) = self.state_matrix[index].record(on);
225
226 // Assign KeyEvent using the output keystate
227 res[i] = if keystate == State::On {
228 KeyEvent::On {
229 cycles_since_state_change,
230 }
231 } else {
232 KeyEvent::Off {
233 idle,
234 cycles_since_state_change,
235 }
236 };
237 }
238
239 Ok((res, self.cur_strobe))
240 }
241
242 /// Return the KeyState for a given index
243 pub fn state(
244 &self,
245 index: usize,
246 ) -> Option<KeyState<CSIZE, SCAN_PERIOD_US, DEBOUNCE_US, IDLE_MS>> {
247 if index >= self.state_matrix.len() {
248 None
249 } else {
250 Some(self.state_matrix[index])
251 }
252 }
253
254 /// Generate event from KeyState
255 /// Useful when trying to determine if a key has not been pressed
256 pub fn generate_key_event(&self, index: usize) -> Option<KeyEvent> {
257 let state = self.state(index);
258
259 state.map(|state| match state.state().0 {
260 State::On => KeyEvent::On {
261 cycles_since_state_change: state.cycles_since_state_change(),
262 },
263 State::Off => KeyEvent::Off {
264 idle: state.idle(),
265 cycles_since_state_change: state.cycles_since_state_change(),
266 },
267 })
268 }
269}
270
271#[cfg(feature = "kll-core")]
272mod converters {
273 #[cfg(feature = "defmt")]
274 use defmt::*;
275 #[cfg(not(feature = "defmt"))]
276 use log::*;
277
278 use crate::*;
279 use heapless::Vec;
280 use kll_core::layout::TriggerEventIterator;
281
282 impl<
283 C: OutputPin,
284 R: InputPin,
285 const CSIZE: usize,
286 const RSIZE: usize,
287 const MSIZE: usize,
288 const SCAN_PERIOD_US: u32,
289 const DEBOUNCE_US: u32,
290 const IDLE_MS: u32,
291 const MAX_EVENTS: usize,
292 > KeyScanning<MAX_EVENTS> for Matrix<C, R, CSIZE, RSIZE, MSIZE, SCAN_PERIOD_US, DEBOUNCE_US, IDLE_MS>
293 {
294 /// Convert matrix state into a TriggerEvent
295 fn generate_events(&self, index: usize) -> TriggerEventIterator<MAX_EVENTS> {
296 self.generate_key_event(index)
297 .unwrap()
298 .trigger_events(index, false)
299 }
300 }
301
302 impl KeyEvent {
303 pub fn trigger_events<const MAX_EVENTS: usize>(
304 &self,
305 index: usize,
306 ignore_off: bool,
307 ) -> TriggerEventIterator<MAX_EVENTS> {
308 let mut events = Vec::new();
309
310 // Handle on/off events
311 match self {
312 KeyEvent::On {
313 cycles_since_state_change,
314 } => {
315 if *cycles_since_state_change == 0 {
316 trace!("Reading: {} {}", index, self);
317 events
318 .push(kll_core::TriggerEvent::Switch {
319 state: kll_core::trigger::Phro::Press,
320 index: index as u16,
321 last_state: 0,
322 })
323 .unwrap();
324 } else {
325 events
326 .push(kll_core::TriggerEvent::Switch {
327 state: kll_core::trigger::Phro::Hold,
328 index: index as u16,
329 last_state: *cycles_since_state_change,
330 })
331 .unwrap();
332 }
333 }
334 KeyEvent::Off {
335 cycles_since_state_change,
336 ..
337 } => {
338 if *cycles_since_state_change == 0 {
339 trace!("Reading: {} {}", index, self);
340 events
341 .push(kll_core::TriggerEvent::Switch {
342 state: kll_core::trigger::Phro::Release,
343 index: index as u16,
344 last_state: 0,
345 })
346 .unwrap();
347 // Ignore off events unless ignore_off is set
348 } else if !ignore_off {
349 events
350 .push(kll_core::TriggerEvent::Switch {
351 state: kll_core::trigger::Phro::Off,
352 index: index as u16,
353 last_state: *cycles_since_state_change,
354 })
355 .unwrap();
356 }
357 }
358 }
359 TriggerEventIterator::new(events)
360 }
361 }
362}