Skip to main content

rmk_q6_he_ansi/matrix/
analog_matrix.rs

1//! Hall-effect analog matrix scanner with EEPROM-backed per-key calibration
2//! and continuous auto-calibration.
3
4mod calibration;
5mod scan;
6mod types;
7
8pub(crate) use crate::matrix::analog_matrix::types::{
9    AdcSampleTime,
10    AutoCalib,
11    EEPROM_POWER_ON_DELAY,
12    KeyCalib,
13    KeyState,
14};
15use crate::{
16    eeprom::Ft24c64,
17    matrix::{
18        calib_store::{CALIB_BUF_LEN, CalibEntry, EEPROM_BASE_ADDR, try_deserialize},
19        hc164_cols::Hc164Cols,
20    },
21};
22use core::future::pending;
23use embassy_stm32::{
24    Peri,
25    adc::{Adc, AnyAdcChannel, BasicInstance, Instance, RxDma},
26    dma::InterruptHandler,
27    i2c::mode::MasterMode,
28    interrupt::typelevel::Binding,
29    pac::adc,
30};
31use embassy_time::Timer;
32use rmk::{
33    event::{KeyboardEvent, publish_event_async},
34    input_device::{InputDevice, Runnable},
35};
36pub use types::HallCfg;
37
38/// ADC-related peripherals grouped to allow split borrows when constructing
39/// a [`embassy_stm32::adc::ConfiguredSequence`].
40pub struct AdcPart<'peripherals, ADC, D, const ROW: usize>
41where
42    ADC: Instance<Regs = adc::Adc> + BasicInstance,
43    D: RxDma<ADC>,
44    AdcSampleTime<ADC>: Clone,
45{
46    /// ADC peripheral used for sampling hall sensors.
47    pub adc: Adc<'peripherals, ADC>,
48    /// DMA channel used for non-blocking ADC sequence reads.
49    pub dma: Peri<'peripherals, D>,
50    /// ADC channels corresponding to each matrix row.
51    pub row_adc: [AnyAdcChannel<'peripherals, ADC>; ROW],
52    /// ADC sample time applied to every channel in the sequence.
53    pub sample_time: AdcSampleTime<ADC>,
54}
55
56impl<'peripherals, ADC, D, const ROW: usize> AdcPart<'peripherals, ADC, D, ROW>
57where
58    ADC: Instance<Regs = adc::Adc> + BasicInstance,
59    D: RxDma<ADC>,
60    AdcSampleTime<ADC>: Clone,
61{
62    /// Create a [`embassy_stm32::adc::ConfiguredSequence`] over all row
63    /// channels.
64    ///
65    /// Programs the ADC sequence registers once; the returned reader can be
66    /// triggered repeatedly without reprogramming, saving ~20 cycles per
67    /// column per scan.
68    fn configured_sequence<'reader, IRQ2>(
69        &'reader mut self,
70        irq: IRQ2,
71    ) -> embassy_stm32::adc::ConfiguredSequence<'reader, adc::Adc>
72    where
73        IRQ2: Binding<D::Interrupt, InterruptHandler<D>> + 'reader + 'peripherals,
74    {
75        let st = self.sample_time;
76        self.adc.configured_sequence(self.dma.reborrow(), self.row_adc.iter_mut().map(|ch| (ch, st)), irq)
77    }
78
79    /// Create a new [`AdcPart`] from the given peripherals.
80    pub const fn new(
81        adc: Adc<'peripherals, ADC>,
82        row_adc: [AnyAdcChannel<'peripherals, ADC>; ROW],
83        dma: Peri<'peripherals, D>,
84        sample_time: AdcSampleTime<ADC>,
85    ) -> Self {
86        Self { adc, dma, row_adc, sample_time }
87    }
88}
89
90/// Hall-effect analog matrix scanner with EEPROM-backed per-key calibration
91/// and continuous auto-calibration.
92///
93/// On first boot (or after EEPROM corruption) the firmware performs a guided
94/// two-phase calibration:
95///
96/// 1. **Zero-travel pass** - all keys fully released; the firmware averages
97///    `HallCfg::calib_passes` reads per key. Backlight signals amber.
98/// 2. **Full-travel pass** - user presses every key to the bottom within
99///    `HallCfg::full_calib_duration`; each key must be held for
100///    [`crate::matrix::analog_matrix::types::CALIB_HOLD_DURATION_MS`] before it
101///    is accepted and its LED turns green.
102///
103/// After all keys are accepted a
104/// [`crate::matrix::analog_matrix::types::CALIB_SETTLE_AFTER_ALL_DONE`]
105/// window continues sampling to capture the true bottom-out ADC. Validated
106/// entries are written to the FT24C64 EEPROM and verified by read-back. On all
107/// subsequent boots only full-travel data is loaded from EEPROM; zero-travel
108/// is re-measured fresh to compensate for temperature drift.
109///
110/// During normal operation the auto-calibrator silently refines both zero and
111/// full-travel values on every press/release cycle, keeping the scanner
112/// accurate as the sensor drifts over time without requiring user interaction.
113pub struct AnalogHallMatrix<'peripherals, ADC, D, IRQ, IM, const ROW: usize, const COL: usize>
114where
115    ADC: Instance<Regs = adc::Adc> + BasicInstance,
116    D: RxDma<ADC>,
117    IRQ: Binding<D::Interrupt, InterruptHandler<D>> + Copy + 'peripherals,
118    IM: MasterMode,
119    AdcSampleTime<ADC>: Clone,
120{
121    /// ADC peripherals and channels grouped for split-borrow compatibility.
122    adc_part: AdcPart<'peripherals, ADC, D, ROW>,
123    /// Per-key auto-calibration state used to refine
124    /// [`self::AnalogHallMatrix::calib`] during normal operation.
125    auto_calib: [[AutoCalib; COL]; ROW],
126    /// Per-key calibration data applied during the scan loop.
127    calib: [[KeyCalib; COL]; ROW],
128    /// Sensing and scanning configuration.
129    cfg: HallCfg,
130    /// Column driver used to select the active column via the HC164.
131    cols: Hc164Cols<'peripherals>,
132    /// EEPROM driver for loading and persisting calibration data.
133    eeprom: Ft24c64<'peripherals, IM>,
134    /// DMA interrupt binding reused for every ADC sequence read.
135    irq: IRQ,
136    /// Dynamic per-key runtime state for the scan loop.
137    state: [[KeyState; COL]; ROW],
138}
139
140impl<'peripherals, ADC, D, IRQ, IM, const ROW: usize, const COL: usize>
141    AnalogHallMatrix<'peripherals, ADC, D, IRQ, IM, ROW, COL>
142where
143    ADC: Instance<Regs = adc::Adc> + BasicInstance,
144    D: RxDma<ADC>,
145    IRQ: Binding<D::Interrupt, InterruptHandler<D>> + Copy + 'peripherals,
146    IM: MasterMode,
147    AdcSampleTime<ADC>: Clone,
148{
149    /// Create a new matrix scanner.
150    ///
151    /// Calibration is deferred to [`Runnable::run`], which loads from EEPROM
152    /// on subsequent boots or runs a full first-boot calibration pass.
153    pub fn new(
154        adc_part: AdcPart<'peripherals, ADC, D, ROW>,
155        irq: IRQ,
156        cols: Hc164Cols<'peripherals>,
157        cfg: HallCfg,
158        eeprom: Ft24c64<'peripherals, IM>,
159    ) -> Self {
160        Self {
161            adc_part,
162            auto_calib: [[AutoCalib::new(); COL]; ROW],
163            calib: [[KeyCalib::uncalibrated(); COL]; ROW],
164            cfg,
165            cols,
166            eeprom,
167            irq,
168            state: [[KeyState::new(); COL]; ROW],
169        }
170    }
171
172    /// Apply a row-major array of [`CalibEntry`] values to
173    /// `self.calib`, pairing each stored full-travel reading with the
174    /// corresponding live zero-travel reading from `zero_raw`.
175    fn apply_calib(&mut self, entries: &[[CalibEntry; COL]; ROW], zero_raw: &[[u16; COL]; ROW]) {
176        for ((cal_row, entry_row), zero_row) in self.calib.iter_mut().zip(entries).zip(zero_raw) {
177            for ((cal, entry), &zero) in cal_row.iter_mut().zip(entry_row).zip(zero_row) {
178                *cal = KeyCalib::new(zero, entry.full);
179            }
180        }
181    }
182}
183
184impl<'peripherals, ADC, D, IRQ, IM, const ROW: usize, const COL: usize> InputDevice
185    for AnalogHallMatrix<'peripherals, ADC, D, IRQ, IM, ROW, COL>
186where
187    ADC: Instance<Regs = adc::Adc> + BasicInstance,
188    D: RxDma<ADC>,
189    IRQ: Binding<D::Interrupt, InterruptHandler<D>> + Copy + 'peripherals,
190    IM: MasterMode,
191    AdcSampleTime<ADC>: Clone,
192{
193    type Event = KeyboardEvent;
194
195    /// Not used - events are published directly via [`publish_event_async`] in
196    /// [`Runnable::run`]. Returns [`pending`] to satisfy the trait bound
197    /// without competing with the scan loop.
198    async fn read_event(&mut self) -> Self::Event { pending().await }
199}
200
201impl<'peripherals, ADC, D, IRQ, IM, const ROW: usize, const COL: usize> Runnable
202    for AnalogHallMatrix<'peripherals, ADC, D, IRQ, IM, ROW, COL>
203where
204    ADC: Instance<Regs = adc::Adc> + BasicInstance,
205    D: RxDma<ADC>,
206    IRQ: Binding<D::Interrupt, InterruptHandler<D>> + Copy + 'peripherals,
207    IM: MasterMode,
208    AdcSampleTime<ADC>: Clone,
209{
210    async fn run(&mut self) -> ! {
211        let mut eeprom_buf = [0_u8; CALIB_BUF_LEN];
212        let mut entries = Self::default_entries();
213
214        // Wait for the EEPROM to complete its power-on reset before issuing
215        // the first I²C transaction.
216        Timer::after(EEPROM_POWER_ON_DELAY).await;
217
218        let loaded = self.eeprom.read(EEPROM_BASE_ADDR, &mut eeprom_buf).await.is_ok()
219            && try_deserialize::<ROW, COL>(&eeprom_buf, &mut entries);
220
221        if loaded {
222            // Re-measure zero travel on every boot to compensate for
223            // temperature drift; full-travel data comes from EEPROM.
224            let zero_raw = self.calibrate_zero_raw().await;
225            self.apply_calib(&entries, &zero_raw);
226        } else {
227            self.run_first_boot_calib(&mut eeprom_buf, &mut entries).await;
228        }
229
230        let mut buf = [0_u16; ROW];
231        let mut seq = self.adc_part.configured_sequence(self.irq);
232        loop {
233            if let Some(ev) = Self::scan_for_next_change(
234                &mut self.cols,
235                &mut self.state,
236                &mut self.calib,
237                &mut self.auto_calib,
238                &mut seq,
239                &mut buf,
240                self.cfg,
241            )
242            .await
243            {
244                publish_event_async(ev).await;
245            }
246        }
247    }
248}
249
250/// Copy the element at `(row, col)` from a 2-D array slice, returning `None`
251/// if either index is out of bounds.
252#[inline]
253fn get2<T: Copy, const C: usize>(arr: &[[T; C]], row: usize, col: usize) -> Option<T> {
254    arr.get(row).and_then(|r| r.get(col)).copied()
255}
256
257/// Return a mutable reference to `(row, col)` in a 2-D array slice, returning
258/// `None` if either index is out of bounds.
259#[inline]
260fn get2_mut<T, const C: usize>(arr: &mut [[T; C]], row: usize, col: usize) -> Option<&mut T> {
261    arr.get_mut(row).and_then(|r| r.get_mut(col))
262}