Skip to main content

rmk_q6_he_ansi/matrix/analog_matrix/
calibration.rs

1//! First-boot guided calibration and EEPROM persistence.
2//!
3//! Contains the two-phase calibration sequence (zero-travel pass followed by
4//! full-travel press pass) and the EEPROM read/write/verify logic.  The
5//! continuous background auto-calibration that runs during normal scanning
6//! lives in [`super::scan`] alongside the hot scan loop that calls it.
7
8use super::{AdcSampleTime, AnalogHallMatrix, get2};
9use crate::{
10    backlight::{
11        led_processor::{BACKLIGHT_CH, BacklightCmd, CalibPhase},
12        mapping::MATRIX_TO_LED,
13    },
14    matrix::{
15        analog_matrix::types::{
16            BOTTOM_JITTER,
17            CALIB_HOLD_DURATION_MS,
18            CALIB_PRESS_THRESHOLD,
19            CALIB_SETTLE_AFTER_ALL_DONE,
20            CALIB_ZERO_TOLERANCE,
21            DEFAULT_FULL_RANGE,
22            KeyCalibState,
23            MIN_USEFUL_FULL_RANGE,
24            REF_ZERO_TRAVEL,
25            UNCALIBRATED_ZERO,
26            VALID_RAW_MIN,
27            ZERO_TRAVEL_DEAD_ZONE,
28        },
29        calib_store,
30        calib_store::{CALIB_BUF_LEN, CalibEntry, EEPROM_BASE_ADDR},
31        sensor_mapping::SENSOR_POSITIONS,
32    },
33};
34use calib_store::try_deserialize;
35use embassy_stm32::{
36    adc::{BasicInstance, Instance, RxDma},
37    dma::InterruptHandler,
38    i2c::mode::MasterMode,
39    interrupt::typelevel::Binding,
40    pac::adc,
41};
42use embassy_time::{Duration, Instant, Timer};
43
44impl<'peripherals, ADC, D, IRQ, IM, const ROW: usize, const COL: usize>
45    AnalogHallMatrix<'peripherals, ADC, D, IRQ, IM, ROW, COL>
46where
47    ADC: Instance<Regs = adc::Adc> + BasicInstance,
48    D: RxDma<ADC>,
49    IRQ: Binding<D::Interrupt, InterruptHandler<D>> + Copy + 'peripherals,
50    IM: MasterMode,
51    AdcSampleTime<ADC>: Clone,
52{
53    /// Build the default calibration entries used when EEPROM is blank or
54    /// invalid, giving a reasonable approximation of travel until real
55    /// calibration runs.
56    pub(super) const fn default_entries() -> [[CalibEntry; COL]; ROW] {
57        [[CalibEntry { full: UNCALIBRATED_ZERO.saturating_sub(DEFAULT_FULL_RANGE) }; COL]; ROW]
58    }
59
60    /// Average `cfg.calib_passes` full-matrix scans to establish per-key
61    /// zero-travel (resting) ADC values.
62    ///
63    /// All keys must be fully released during this pass. Returns a
64    /// `ROW × COL` array of raw ADC averages, each reduced by
65    /// [`ZERO_TRAVEL_DEAD_ZONE`] so that the resting position sits cleanly
66    /// below the measured average, preventing ADC noise from producing
67    /// spurious non-zero travel readings. Does not modify `self.calib`.
68    pub(super) async fn calibrate_zero_raw(&mut self) -> [[u16; COL]; ROW] {
69        let mut acc = [[0_u32; COL]; ROW];
70        let mut seq = self.adc_part.configured_sequence(self.irq);
71        let mut buf = [0_u16; ROW];
72
73        for _ in 0..self.cfg.calib_passes {
74            for col in 0..COL {
75                self.cols.select(col);
76                Timer::after(self.cfg.col_settle_us).await;
77                seq.read(&mut buf).await;
78                for (acc_row, &raw) in acc.iter_mut().zip(buf.iter()) {
79                    if let Some(cell) = acc_row.get_mut(col) {
80                        *cell = cell.saturating_add(u32::from(raw));
81                    }
82                }
83            }
84        }
85
86        let mut result = [[UNCALIBRATED_ZERO; COL]; ROW];
87        for (res_row, acc_row) in result.iter_mut().zip(acc.iter()) {
88            for (res, &total) in res_row.iter_mut().zip(acc_row.iter()) {
89                // Leave the UNCALIBRATED_ZERO initializer in place on any
90                // arithmetic failure (e.g. calib_passes == 0).
91                if let Some(avg) = total.checked_div(self.cfg.calib_passes) {
92                    *res = u16::try_from(avg).unwrap_or(UNCALIBRATED_ZERO).saturating_sub(ZERO_TRAVEL_DEAD_ZONE);
93                }
94            }
95        }
96        result
97    }
98
99    /// Sample the matrix for `duration` (or until all real keys are accepted),
100    /// recording the minimum ADC reading seen per key.
101    ///
102    /// Lower ADC = more magnet travel, so the minimum reading over the window
103    /// is the deepest press seen. A key is accepted only after it has stayed
104    /// continuously below [`CALIB_PRESS_THRESHOLD`] for
105    /// [`CALIB_HOLD_DURATION_MS`]; releasing and re-pressing resets the timer.
106    /// The LED turns green only at acceptance, not at first crossing.
107    ///
108    /// After all keys are accepted a [`CALIB_SETTLE_AFTER_ALL_DONE`]
109    /// continuation window keeps updating `min_raw` so the stored value
110    /// reflects the true bottom-out ADC, not merely the acceptance instant.
111    pub(super) async fn sample_full_raw(
112        &mut self,
113        duration: Duration,
114        zero_raw: &[[u16; COL]; ROW],
115    ) -> [[u16; COL]; ROW] {
116        let deadline = Instant::now() + duration;
117        let mut min_raw = [[u16::MAX; COL]; ROW];
118
119        // Closure to update the running minimum for a single position.
120        let mut update_min = |row: usize, col: usize, raw: u16| {
121            if let Some(cell) = min_raw.get_mut(row).and_then(|r| r.get_mut(col)) {
122                *cell = (*cell).min(raw);
123            }
124        };
125
126        let mut calib_state: [[KeyCalibState; COL]; ROW] = [[KeyCalibState::Waiting; COL]; ROW];
127        let mut calibrated_count: usize = 0;
128        let hold_duration = Duration::from_millis(CALIB_HOLD_DURATION_MS);
129
130        // Count only positions that have a physical sensor and a plausible
131        // zero-travel reading so the denominator matches the detection loop.
132        let total_keys = (0..ROW)
133            .flat_map(|r| (0..COL).map(move |c| (r, c)))
134            .filter(|&(r, c)| {
135                SENSOR_POSITIONS.get(r).and_then(|row| row.get(c)).copied().unwrap_or(false)
136                    && get2(zero_raw, r, c).is_some_and(|z| z.abs_diff(REF_ZERO_TRAVEL) <= CALIB_ZERO_TOLERANCE)
137            })
138            .count()
139            .max(1);
140
141        let mut seq = self.adc_part.configured_sequence(self.irq);
142        let mut buf = [0_u16; ROW];
143        let mut last_pct: u8 = 0;
144
145        // Phase A: sample until all real keys are accepted or the deadline expires.
146        while Instant::now() < deadline && calibrated_count < total_keys {
147            for col in 0..COL {
148                self.cols.select(col);
149                Timer::after(self.cfg.col_settle_us).await;
150                seq.read(&mut buf).await;
151
152                for (row, &raw) in buf.iter().enumerate() {
153                    // Always track the deepest reading seen, regardless of
154                    // whether the key has been accepted yet.
155                    update_min(row, col, raw);
156
157                    if !SENSOR_POSITIONS.get(row).and_then(|r| r.get(col)).copied().unwrap_or(false) {
158                        continue;
159                    }
160
161                    let Some(key_state) = calib_state.get_mut(row).and_then(|r| r.get_mut(col)) else {
162                        continue;
163                    };
164
165                    // Skip keys already accepted.
166                    if matches!(*key_state, KeyCalibState::Accepted) {
167                        continue;
168                    }
169
170                    let zero = get2(zero_raw, row, col).unwrap_or(UNCALIBRATED_ZERO);
171                    let pressed = zero.saturating_sub(raw) >= CALIB_PRESS_THRESHOLD;
172
173                    match *key_state {
174                        KeyCalibState::Waiting => {
175                            if pressed {
176                                // Start hold timer on first threshold crossing.
177                                *key_state = KeyCalibState::Holding(Instant::now());
178                            }
179                        }
180                        KeyCalibState::Holding(first_seen) => {
181                            if pressed {
182                                if first_seen.elapsed() >= hold_duration {
183                                    // Hold duration satisfied; accept this key.
184                                    *key_state = KeyCalibState::Accepted;
185                                    calibrated_count = calibrated_count.saturating_add(1);
186
187                                    if let Some(Some(led_idx)) =
188                                        MATRIX_TO_LED.get(row).and_then(|r| r.get(col)).copied()
189                                    {
190                                        BACKLIGHT_CH.sender().try_send(BacklightCmd::CalibKeyDone(led_idx)).ok();
191                                    }
192
193                                    let pct = u8::try_from(
194                                        calibrated_count.saturating_mul(100).checked_div(total_keys).unwrap_or(0),
195                                    )
196                                    .unwrap_or(100)
197                                    .min(100);
198
199                                    if pct != last_pct {
200                                        last_pct = pct;
201                                        BACKLIGHT_CH.sender().try_send(BacklightCmd::CalibProgress(pct)).ok();
202                                    }
203                                }
204                                // else: still within hold window; keep waiting.
205                            } else {
206                                // Released before hold duration; reset so the
207                                // user must press it all the way down again.
208                                *key_state = KeyCalibState::Waiting;
209                            }
210                        }
211                        KeyCalibState::Accepted => {}
212                    }
213                }
214            }
215        }
216
217        // If all keys were accepted (not just a deadline timeout), signal the
218        // backlight to blink green so the user knows to release their keys.
219        // Best-effort: missing this signal only skips the animation.
220        if calibrated_count >= total_keys {
221            BACKLIGHT_CH.sender().try_send(BacklightCmd::CalibPhase(CalibPhase::AllAccepted)).ok();
222        }
223
224        // Phase B: continue sampling for CALIB_SETTLE_AFTER_ALL_DONE_MS
225        // regardless of whether Phase A ended by full acceptance or timeout.
226        // This gives every accepted key time to reach its true bottom-out ADC
227        // rather than storing the value at the moment of acceptance, and still
228        // captures the deepest reading seen for any keys that ran out of time.
229        let settle_deadline = Instant::now().saturating_add(CALIB_SETTLE_AFTER_ALL_DONE);
230        while Instant::now() < settle_deadline {
231            for col in 0..COL {
232                self.cols.select(col);
233                Timer::after(self.cfg.col_settle_us).await;
234                seq.read(&mut buf).await;
235                for (row, &raw) in buf.iter().enumerate() {
236                    update_min(row, col, raw);
237                }
238            }
239        }
240
241        min_raw
242    }
243
244    /// Run the guided first-boot two-phase calibration, persist the result to
245    /// EEPROM, and apply it to `self.calib`.
246    ///
247    /// Backlight signals during the process:
248    /// - **Amber** - zero-travel pass, all keys must be released.
249    /// - **Blue → green per key** - full-travel press window.
250    /// - **Green blink ×3** - all keys accepted; keys may be released.
251    /// - **Green for 2 s** - calibration stored successfully.
252    /// - **Amber** - EEPROM write-back verification failed; keyboard will
253    ///   re-calibrate on the next boot.
254    ///
255    /// Keys not pressed during the full-travel window fall back to
256    /// `zero − DEFAULT_FULL_RANGE` so the keyboard remains functional.
257    pub(super) async fn run_first_boot_calib(
258        &mut self,
259        eeprom_buf: &mut [u8; CALIB_BUF_LEN],
260        entries: &mut [[CalibEntry; COL]; ROW],
261    ) {
262        BACKLIGHT_CH.sender().try_send(BacklightCmd::CalibPhase(CalibPhase::Zero)).ok();
263        let zero_raw = self.calibrate_zero_raw().await;
264
265        BACKLIGHT_CH.sender().try_send(BacklightCmd::CalibPhase(CalibPhase::Full)).ok();
266        let full_raw = self.sample_full_raw(self.cfg.full_calib_duration, &zero_raw).await;
267
268        for ((entry_row, zero_row), full_row) in entries.iter_mut().zip(zero_raw.iter()).zip(full_raw.iter()) {
269            for ((entry, &zero), &seen_min) in entry_row.iter_mut().zip(zero_row.iter()).zip(full_row.iter()) {
270                let full = if zero > seen_min && zero.saturating_sub(seen_min) >= MIN_USEFUL_FULL_RANGE {
271                    seen_min.saturating_add(BOTTOM_JITTER).min(zero.saturating_sub(MIN_USEFUL_FULL_RANGE))
272                } else {
273                    zero.saturating_sub(DEFAULT_FULL_RANGE).max(VALID_RAW_MIN)
274                };
275                *entry = CalibEntry { full };
276            }
277        }
278
279        self.apply_calib(entries, &zero_raw);
280
281        // Allow the backlight channel to drain before starting the I²C write.
282        Timer::after_micros(200).await;
283        calib_store::serialize(entries, eeprom_buf);
284        let erase_ok = self.eeprom.zero_out().await.is_ok();
285        let write_ok = self.eeprom.write(EEPROM_BASE_ADDR, eeprom_buf).await.is_ok();
286
287        // Verify by reading back into the same buffer and re-deserializing.
288        // Reusing the buffer avoids a second large stack allocation.
289        let verified = erase_ok
290            && write_ok
291            && self.eeprom.read(EEPROM_BASE_ADDR, eeprom_buf).await.is_ok()
292            && try_deserialize::<ROW, COL>(eeprom_buf, entries);
293
294        if !verified {
295            // Signal amber so the user knows calibration will repeat on the
296            // next boot.
297            BACKLIGHT_CH.sender().try_send(BacklightCmd::CalibPhase(CalibPhase::Zero)).ok();
298            return;
299        }
300
301        BACKLIGHT_CH.sender().try_send(BacklightCmd::CalibPhase(CalibPhase::Done)).ok();
302    }
303}