diff --git a/docs/src/content/docs/getting-started/matrix-and-layout.md b/docs/src/content/docs/getting-started/matrix-and-layout.md index 18bb445..c19aeeb 100644 --- a/docs/src/content/docs/getting-started/matrix-and-layout.md +++ b/docs/src/content/docs/getting-started/matrix-and-layout.md @@ -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 diff --git a/keyberon/src/analog.rs b/keyberon/src/analog.rs new file mode 100644 index 0000000..68f3adb --- /dev/null +++ b/keyberon/src/analog.rs @@ -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 { + 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 AnalogActuator { + /// 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::>(), + /// ); + /// ``` + pub fn events(&mut self, new: [[u8; CS]; RS]) -> impl Iterator + '_ { + 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 + }, + ) + }) + } +} diff --git a/keyberon/src/lib.rs b/keyberon/src/lib.rs index d04e8b2..c99f3ea 100644 --- a/keyberon/src/lib.rs +++ b/keyberon/src/lib.rs @@ -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; diff --git a/keyberon/src/matrix.rs b/keyberon/src/matrix.rs index 1f4e0ad..eb7ae6d 100644 --- a/keyberon/src/matrix.rs +++ b/keyberon/src/matrix.rs @@ -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. /// @@ -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 { + ranges: [[Range; CS]; RS], +} + +impl AnalogMatrix { + /// Create a new AnalogMatrix + pub fn new(ranges: [[Range; CS]; RS]) -> Self { + Self { ranges } + } +} + +impl AnalogMatrix +where + u32: From, +{ + /// 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( + &mut self, + get_press_value: impl Fn(usize, usize) -> Result, + ) -> 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) + } +} diff --git a/rumcake-macros/src/backlight.rs b/rumcake-macros/src/backlight.rs index 29e2b90..9cfd54e 100644 --- a/rumcake-macros/src/backlight.rs +++ b/rumcake-macros/src/backlight.rs @@ -4,21 +4,15 @@ use quote::{quote, ToTokens}; use syn::parse::Parse; use syn::punctuated::Punctuated; use syn::spanned::Spanned; -use syn::{braced, ExprTuple, Ident, Token}; +use syn::{braced, Ident, Token}; use crate::keyboard::{MatrixLike, OptionalItem}; +use crate::TuplePair; -pub fn led_layout(input: MatrixLike>) -> TokenStream { +pub fn led_layout(input: MatrixLike>) -> TokenStream { let coordinates = input.rows.iter().map(|row| { let items = &row.cols; - if let Some(item) = items.iter().find(|item| match item { - OptionalItem::None => false, - OptionalItem::Some(tuple) => tuple.elems.len() != 2, - }) { - abort!(item.span(), "Item is not a coordinate.") - }; - quote! { #(#items),* } }); @@ -37,16 +31,16 @@ pub struct LEDFlags { impl Parse for LEDFlags { fn parse(input: syn::parse::ParseStream) -> syn::Result { Ok(LEDFlags { - flags: input.parse_terminated(Ident::parse, Token![|])?, + flags: input.call(Punctuated::parse_separated_nonempty)?, }) } } impl ToTokens for LEDFlags { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - for flag in self.flags.iter() { - quote! { ::rumcake::backlight::LEDFlags::#flag }.to_tokens(tokens) - } + let flags = self.flags.iter(); + + quote! { #(::rumcake::backlight::LEDFlags::#flags)|* }.to_tokens(tokens) } } @@ -74,7 +68,7 @@ pub fn led_flags(input: MatrixLike>) -> TokenStream { #[derive(Debug)] pub struct BacklightMatrixMacroInput { pub led_layout_brace: syn::token::Brace, - pub led_layout: MatrixLike>, + pub led_layout: MatrixLike>, pub led_flags_brace: syn::token::Brace, pub led_flags: MatrixLike>, } diff --git a/rumcake-macros/src/hw/nrf.rs b/rumcake-macros/src/hw/nrf.rs index 6370c2c..edb0fb2 100644 --- a/rumcake-macros/src/hw/nrf.rs +++ b/rumcake-macros/src/hw/nrf.rs @@ -1,8 +1,11 @@ use proc_macro2::{Ident, TokenStream}; use proc_macro_error::{abort, OptionExt}; use quote::quote; +use syn::parse::Parse; use syn::punctuated::Punctuated; -use syn::Token; +use syn::{braced, parenthesized, Token}; + +use crate::common::{AnalogPinType, DirectPinDefinition, MultiplexerDefinition}; pub const HAL_CRATE: &'static str = "embassy_nrf"; @@ -152,3 +155,115 @@ pub fn setup_buffered_uarte(args: Punctuated) -> TokenStream { } } } +pub struct NrfAdcSamplerDefinition { + parenthesis_token: syn::token::Paren, + adc_instance_args: Punctuated, + colon_token: Token![=>], + brace_token: syn::token::Brace, + channels: Punctuated, +} + +impl Parse for NrfAdcSamplerDefinition { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let adc_type_content; + let channels_content; + Ok(Self { + parenthesis_token: parenthesized!(adc_type_content in input), + adc_instance_args: Punctuated::parse_terminated(&adc_type_content)?, + colon_token: input.parse()?, + brace_token: braced!(channels_content in input), + channels: Punctuated::parse_terminated(&channels_content)?, + }) + } +} + +pub fn setup_adc_sampler( + NrfAdcSamplerDefinition { + adc_instance_args, + channels, + .. + }: NrfAdcSamplerDefinition, +) -> TokenStream { + let mut args = adc_instance_args.iter(); + let timer = args.next().expect_or_abort("Missing timer argument."); + let ppi_ch0 = args.next().expect_or_abort("Missing PPI CH0 argument."); + let ppi_ch1 = args.next().expect_or_abort("Missing PPI CH1 argument."); + + let channel_count = channels.len(); + let select_pin_count = channels.iter().fold(0, |acc, ch| { + if let AnalogPinType::Multiplexed(MultiplexerDefinition { select_pins, .. }) = ch { + acc.max(select_pins.len()) + } else { + acc + } + }); + let buf_size = 2usize.pow(select_pin_count as u32); + + let (pins, channels): (Vec, Vec) = channels + .iter() + .map(|ch| match ch { + AnalogPinType::Multiplexed(MultiplexerDefinition { + pin, select_pins, .. + }) => { + let select_pins = select_pins.iter().map(|select_pin| match select_pin { + crate::keyboard::OptionalItem::None => quote! { None }, + crate::keyboard::OptionalItem::Some(pin_ident) => { + quote! { Some(::rumcake::hw::mcu::output_pin!(#pin_ident)) } + } + }); + + ( + quote! { + ::rumcake::hw::mcu::AnalogPinType::Multiplexed( + [0; #buf_size], + ::rumcake::hw::Multiplexer::new( + [ #(#select_pins),* ], + None + ) + ) + }, + quote! { + ::rumcake::hw::mcu::embassy_nrf::saadc::ChannelConfig::single_ended( + unsafe { ::rumcake::hw::mcu::embassy_nrf::peripherals::#pin::steal() } + ) + }, + ) + } + AnalogPinType::Direct(DirectPinDefinition { pin, .. }) => ( + quote! { + ::rumcake::hw::mcu::AnalogPinType::Direct([0]) + }, + quote! { + ::rumcake::hw::mcu::embassy_nrf::saadc::ChannelConfig::single_ended( + unsafe { ::rumcake::hw::mcu::embassy_nrf::peripherals::#pin::steal() } + ) + }, + ), + }) + .unzip(); + + quote! { + type AdcSamplerType = ::rumcake::hw::mcu::AdcSampler< + 'static, + ::rumcake::hw::mcu::embassy_nrf::peripherals::#timer, + ::rumcake::hw::mcu::embassy_nrf::peripherals::#ppi_ch0, + ::rumcake::hw::mcu::embassy_nrf::peripherals::#ppi_ch1, + #select_pin_count, + #channel_count, + >; + + fn setup_adc_sampler() -> &'static AdcSamplerType { + static SAMPLER: ::rumcake::once_cell::sync::OnceCell = ::rumcake::once_cell::sync::OnceCell::new(); + + SAMPLER.get_or_init(|| unsafe { + ::rumcake::hw::mcu::AdcSampler::new( + [ #(#pins),* ], + [ #(#channels),* ], + ::rumcake::hw::mcu::embassy_nrf::peripherals::#timer::steal(), + ::rumcake::hw::mcu::embassy_nrf::peripherals::#ppi_ch0::steal(), + ::rumcake::hw::mcu::embassy_nrf::peripherals::#ppi_ch1::steal(), + ) + }) + } + } +} diff --git a/rumcake-macros/src/hw/rp.rs b/rumcake-macros/src/hw/rp.rs index b19f44f..2464e4d 100644 --- a/rumcake-macros/src/hw/rp.rs +++ b/rumcake-macros/src/hw/rp.rs @@ -4,6 +4,8 @@ use quote::quote; use syn::punctuated::Punctuated; use syn::Token; +use crate::common::{AnalogPinType, DirectPinDefinition, MultiplexerDefinition}; + pub const HAL_CRATE: &'static str = "embassy_rp"; pub fn input_pin(ident: Ident) -> TokenStream { @@ -137,3 +139,73 @@ pub fn setup_buffered_uart(args: Punctuated) -> TokenStream { } } } + +pub fn setup_adc_sampler(channels: Punctuated) -> TokenStream { + let channel_count = channels.len(); + let select_pin_count = channels.iter().fold(0, |acc, ch| { + if let AnalogPinType::Multiplexed(MultiplexerDefinition { select_pins, .. }) = ch { + acc.max(select_pins.len()) + } else { + acc + } + }); + + let (pins, channels): (Vec, Vec) = channels + .iter() + .map(|ch| match ch { + AnalogPinType::Multiplexed(MultiplexerDefinition { + pin, select_pins, .. + }) => { + let select_pins = select_pins.iter().map(|select_pin| match select_pin { + crate::keyboard::OptionalItem::None => quote! { None }, + crate::keyboard::OptionalItem::Some(pin_ident) => { + quote! { Some(::rumcake::hw::mcu::output_pin!(#pin_ident)) } + } + }); + + ( + quote! { + ::rumcake::hw::mcu::AnalogPinType::Multiplexed( + ::rumcake::hw::Multiplexer::new( + [ #(#select_pins),* ], + None + ) + ) + }, + quote! { + ::rumcake::hw::mcu::embassy_rp::adc::Channel::new_pin( + unsafe { ::rumcake::hw::mcu::embassy_rp::peripherals::#pin::steal() }, + ::rumcake::hw::mcu::embassy_rp::gpio::Pull::None + ) + }, + ) + } + AnalogPinType::Direct(DirectPinDefinition { pin, .. }) => ( + quote! { + ::rumcake::hw::mcu::AnalogPinType::Direct + }, + quote! { + ::rumcake::hw::mcu::embassy_rp::adc::Channel::new_pin( + unsafe { ::rumcake::hw::mcu::embassy_rp::peripherals::#pin::steal() }, + ::rumcake::hw::mcu::embassy_rp::gpio::Pull::None + ) + }, + ), + }) + .unzip(); + + quote! { + type AdcSamplerType = ::rumcake::hw::mcu::AdcSampler<'static, #select_pin_count, #channel_count>; + + static SAMPLER: ::rumcake::once_cell::sync::OnceCell = ::rumcake::once_cell::sync::OnceCell::new(); + + fn setup_adc_sampler() -> &'static AdcSamplerType { + SAMPLER.get_or_init(|| + ::rumcake::hw::mcu::AdcSampler::new( + [ #(#pins),* ], + [ #(#channels),* ] + ) + ) + } + } +} diff --git a/rumcake-macros/src/hw/stm32.rs b/rumcake-macros/src/hw/stm32.rs index d765d99..6b00394 100644 --- a/rumcake-macros/src/hw/stm32.rs +++ b/rumcake-macros/src/hw/stm32.rs @@ -1,8 +1,11 @@ use proc_macro2::{Ident, TokenStream}; use proc_macro_error::{abort, OptionExt}; use quote::quote; +use syn::parse::Parse; use syn::punctuated::Punctuated; -use syn::Token; +use syn::{braced, parenthesized, Token}; + +use crate::common::{AnalogPinType, DirectPinDefinition, MultiplexerDefinition}; pub const HAL_CRATE: &'static str = "embassy_stm32"; @@ -145,3 +148,144 @@ pub fn setup_buffered_uart(args: Punctuated) -> TokenStream { } } } + +pub struct STM32AdcSamplerDefinition { + parenthesis_token: syn::token::Paren, + adc_instance_args: Punctuated, + colon_token: Token![=>], + brace_token: syn::token::Brace, + channels: Punctuated, +} + +impl Parse for STM32AdcSamplerDefinition { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let adc_type_content; + let channels_content; + Ok(Self { + parenthesis_token: parenthesized!(adc_type_content in input), + adc_instance_args: Punctuated::parse_terminated(&adc_type_content)?, + colon_token: input.parse()?, + brace_token: braced!(channels_content in input), + channels: Punctuated::parse_terminated(&channels_content)?, + }) + } +} + +fn setup_adc_inner(adc_definition: &STM32AdcSamplerDefinition) -> (TokenStream, TokenStream) { + let STM32AdcSamplerDefinition { + adc_instance_args, + channels, + .. + } = adc_definition; + + let mut args = adc_instance_args.iter(); + let interrupt = args.next().expect_or_abort("Missing interrupt argument."); + let adc = args + .next() + .expect_or_abort("Missing ADC peripheral argument."); + + let channel_count = channels.len(); + let select_pin_count = channels.iter().fold(0, |acc, ch| { + if let AnalogPinType::Multiplexed(MultiplexerDefinition { select_pins, .. }) = ch { + acc.max(select_pins.len()) + } else { + acc + } + }); + + let (pins, channels): (Vec, Vec) = channels + .iter() + .map(|ch| match ch { + AnalogPinType::Multiplexed(MultiplexerDefinition { + pin, select_pins, .. + }) => { + let select_pins = select_pins.iter().map(|select_pin| match select_pin { + crate::keyboard::OptionalItem::None => quote! { None }, + crate::keyboard::OptionalItem::Some(pin_ident) => { + quote! { Some(::rumcake::hw::mcu::output_pin!(#pin_ident)) } + } + }); + + ( + quote! { + ::rumcake::hw::mcu::AnalogPinType::Multiplexed( + ::rumcake::hw::Multiplexer::new( + [ #(#select_pins),* ], + None + ) + ) + }, + quote! { + ::rumcake::hw::mcu::Channel::new( + unsafe { ::rumcake::hw::mcu::embassy_stm32::peripherals::#pin::steal() } + ) + }, + ) + } + AnalogPinType::Direct(DirectPinDefinition { pin, .. }) => ( + quote! { + ::rumcake::hw::mcu::AnalogPinType::Direct + }, + quote! { + ::rumcake::hw::mcu::Channel::new( + unsafe { ::rumcake::hw::mcu::embassy_stm32::peripherals::#pin::steal() } + ) + }, + ), + }) + .unzip(); + + ( + quote! { + ::rumcake::hw::mcu::AdcSampler<'static, ::rumcake::hw::mcu::embassy_stm32::peripherals::#adc, #select_pin_count, #channel_count> + }, + quote! { + ::rumcake::hw::mcu::AdcSampler::new( + unsafe { ::rumcake::hw::mcu::embassy_stm32::peripherals::#adc::steal() }, + { + ::rumcake::hw::mcu::embassy_stm32::bind_interrupts! { + struct Irqs { + #interrupt => ::rumcake::hw::mcu::embassy_stm32::adc::InterruptHandler<::rumcake::hw::mcu::embassy_stm32::peripherals::#adc>; + } + }; + Irqs + }, + [ #(#pins),* ], + [ #(#channels),* ] + ) + }, + ) +} + +pub fn setup_adc_sampler( + samplers: Punctuated, +) -> TokenStream { + let (types, instances): (Vec, Vec) = + samplers.iter().map(setup_adc_inner).unzip(); + + let final_type = if types.len() == 1 { + quote! { #(#types)* } + } else { + quote! { (#(#types),*) } + }; + + let final_instance = if instances.len() == 1 { + quote! { #(#instances)* } + } else { + quote! { (#(#instances),*) } + }; + + quote! { + type AdcSamplerType = #final_type; + + static SAMPLER: ::rumcake::once_cell::sync::OnceCell< + AdcSamplerType, + > = ::rumcake::once_cell::sync::OnceCell::new(); + + fn setup_adc_sampler() -> &'static AdcSamplerType { + SAMPLER.get_or_init(|| + #final_instance + ) + } + } +} diff --git a/rumcake-macros/src/keyboard.rs b/rumcake-macros/src/keyboard.rs index 763c50d..2f1c10d 100644 --- a/rumcake-macros/src/keyboard.rs +++ b/rumcake-macros/src/keyboard.rs @@ -5,9 +5,11 @@ use darling::FromMeta; use proc_macro2::{Ident, TokenStream, TokenTree}; use proc_macro_error::OptionExt; use quote::{quote, quote_spanned, ToTokens}; -use syn::parse::{Parse, Parser}; +use syn::parse::Parse; use syn::spanned::Spanned; -use syn::{braced, bracketed, ItemStruct, PathSegment}; +use syn::{braced, bracketed, custom_keyword, ExprRange, ItemStruct, PathSegment}; + +use crate::TuplePair; #[derive(Debug, FromMeta, Default)] #[darling(default)] @@ -346,10 +348,10 @@ pub(crate) fn keyboard_main( ::rumcake::hw::mcu::initialize_rcc(); }); - #[cfg(feature = "nrf")] - { + if cfg!(feature = "nrf") { spawning.extend(quote! { - spawner.spawn(::rumcake::adc_task!()).unwrap(); + let sampler = setup_adc_sampler(); + spawner.spawn(::rumcake::adc_task!(sampler)).unwrap(); }); if uses_bluetooth { @@ -712,23 +714,16 @@ pub(crate) enum OptionalItem { Some(T), } +custom_keyword!(No); + impl Parse for OptionalItem { fn parse(input: syn::parse::ParseStream) -> syn::Result { - input.step(|cursor| { - if let Some((tt, next)) = cursor.token_tree() { - if tt.to_string() == "No" { - return Ok((OptionalItem::None, next)); - } - - return if let Ok(t) = T::parse.parse2(tt.into_token_stream()) { - Ok((OptionalItem::Some(t), next)) - } else { - Err(cursor.error("Invalid item.")) - }; - }; - - Err(cursor.error("No item found.")) - }) + let lookahead = input.lookahead1(); + if lookahead.peek(No) { + input.parse::().map(|_| OptionalItem::None) + } else { + input.parse().map(OptionalItem::Some) + } } } @@ -853,6 +848,7 @@ pub fn build_direct_pin_matrix(input: MatrixLike>) -> TokenS quote! { const MATRIX_ROWS: usize = #row_count; const MATRIX_COLS: usize = #col_count; + fn get_matrix() -> &'static ::rumcake::keyboard::PollableMatrix { static MATRIX: ::rumcake::once_cell::sync::OnceCell< ::rumcake::keyboard::PollableMatrix< @@ -877,6 +873,87 @@ pub fn build_direct_pin_matrix(input: MatrixLike>) -> TokenS } } +#[derive(Debug)] +pub struct AnalogMatrixDefinition { + pub pos_to_ch_brace: syn::token::Brace, + pub pos_to_ch: MatrixLike>, + pub ranges_brace: syn::token::Brace, + pub ranges: MatrixLike>, +} + +impl syn::parse::Parse for AnalogMatrixDefinition { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let pos_to_ch_content; + let pos_to_ch_brace = braced!(pos_to_ch_content in input); + let ranges_content; + let ranges_brace = braced!(ranges_content in input); + + Ok(Self { + pos_to_ch_brace, + pos_to_ch: pos_to_ch_content.parse()?, + ranges_brace, + ranges: ranges_content.parse()?, + }) + } +} + +pub fn build_analog_matrix(input: AnalogMatrixDefinition) -> TokenStream { + let pos_to_ch = input.pos_to_ch.rows.iter().map(|row| { + let items = row.cols.iter().map(|item| match item { + OptionalItem::None => quote! { (0, 0) }, + OptionalItem::Some(tuple) => quote! { #tuple }, + }); + quote! { #(#items),* } + }); + + let ranges = input.ranges.rows.iter().map(|row| { + let items = row.cols.iter().map(|item| match item { + OptionalItem::None => quote! { 0..0 }, + OptionalItem::Some(range) => quote! { #range }, + }); + quote! { #(#items),* } + }); + + let row_count = pos_to_ch.len(); + let col_count = input + .pos_to_ch + .rows + .first() + .expect_or_abort("At least one row must be specified") + .cols + .len(); + + quote! { + const MATRIX_ROWS: usize = #row_count; + const MATRIX_COLS: usize = #col_count; + + fn get_matrix() -> &'static ::rumcake::keyboard::PollableMatrix { + static MATRIX: ::rumcake::once_cell::sync::OnceCell< + ::rumcake::keyboard::PollableMatrix< + ::rumcake::keyboard::PollableAnalogMatrix< + AdcSamplerType, + #col_count, + #row_count + > + > + > = ::rumcake::once_cell::sync::OnceCell::new(); + MATRIX.get_or_init(|| { + ::rumcake::keyboard::PollableMatrix::new( + ::rumcake::keyboard::setup_analog_keyboard_matrix( + setup_adc_sampler(), + [ + #([ #pos_to_ch ]),* + ], + [ + #([ #ranges ]),* + ], + ) + ) + }) + } + } +} + #[derive(Debug)] pub struct LayoutLike { pub layers: Vec>, diff --git a/rumcake-macros/src/lib.rs b/rumcake-macros/src/lib.rs index b3293c7..131b99e 100644 --- a/rumcake-macros/src/lib.rs +++ b/rumcake-macros/src/lib.rs @@ -2,13 +2,13 @@ use darling::FromMeta; use heck::{ToShoutySnakeCase, ToSnakeCase}; use proc_macro2::{Ident, Literal, TokenStream, TokenTree}; use proc_macro_error::proc_macro_error; -use quote::{format_ident, quote, quote_spanned}; +use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::parse::Parse; use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::{ - parse_macro_input, parse_quote, parse_str, DeriveInput, ExprTuple, ItemEnum, ItemFn, - ItemStruct, LitStr, Meta, Pat, Token, + parenthesized, parse_macro_input, parse_quote, parse_str, DeriveInput, ItemEnum, ItemFn, + ItemStruct, LitInt, LitStr, Meta, Pat, Token, }; struct Templates(Punctuated); @@ -98,6 +98,35 @@ pub fn generate_items_from_enum_variants( .into() } +#[derive(Debug)] +struct TuplePair { + parenthesis_token: syn::token::Paren, + left: LitInt, + comma_token: Token![,], + right: LitInt, +} + +impl Parse for TuplePair { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let content; + let parenthesis_token = parenthesized!(content in input); + Ok(Self { + parenthesis_token, + left: content.parse()?, + comma_token: content.parse()?, + right: content.parse()?, + }) + } +} + +impl ToTokens for TuplePair { + fn to_tokens(&self, tokens: &mut TokenStream) { + let left = &self.left; + let right = &self.right; + quote! { (#left,#right) }.to_tokens(tokens) + } +} + mod derive; #[proc_macro_derive(LEDEffect, attributes(animated, reactive))] @@ -143,6 +172,12 @@ pub fn build_direct_pin_matrix(input: proc_macro::TokenStream) -> proc_macro::To keyboard::build_direct_pin_matrix(matrix).into() } +#[proc_macro] +pub fn build_analog_matrix(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let matrix = parse_macro_input!(input as keyboard::AnalogMatrixDefinition); + keyboard::build_analog_matrix(matrix).into() +} + #[proc_macro] pub fn build_layout(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let raw = input.clone(); @@ -169,7 +204,7 @@ pub fn setup_backlight_matrix(input: proc_macro::TokenStream) -> proc_macro::Tok #[proc_macro_error] pub fn led_layout(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let matrix = - parse_macro_input!(input as keyboard::MatrixLike>); + parse_macro_input!(input as keyboard::MatrixLike>); backlight::led_layout(matrix).into() } @@ -219,6 +254,100 @@ pub fn is31fl3731_get_led_from_rgb_matrix_coordinates( #[cfg_attr(feature = "rp", path = "hw/rp.rs")] mod hw; +pub(crate) mod common { + use proc_macro2::Ident; + use syn::parse::Parse; + use syn::{braced, custom_keyword, Token}; + + custom_keyword!(Multiplexer); + custom_keyword!(Direct); + custom_keyword!(pin); + custom_keyword!(select_pins); + + #[allow(dead_code)] + pub struct MultiplexerDefinition { + pub multiplexer_field_name: Multiplexer, + pub pin_brace_token: syn::token::Brace, + pub pin_field_name: pin, + pub pin_field_colon_token: Token![:], + pub pin: Ident, + pub select_pins_field_name: select_pins, + pub select_pins_field_colon_token: Token![:], + pub select_pins_brace_token: syn::token::Brace, + pub select_pins: Vec>, + } + + impl Parse for MultiplexerDefinition { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let content; + let select_pins_content; + Ok(Self { + multiplexer_field_name: input.parse()?, + pin_brace_token: braced!(content in input), + pin_field_name: content.parse()?, + pin_field_colon_token: content.parse()?, + pin: content.parse()?, + select_pins_field_name: content.parse()?, + select_pins_field_colon_token: content.parse()?, + select_pins_brace_token: braced!(select_pins_content in content), + select_pins: { + let mut pins = Vec::new(); + while let Ok(t) = select_pins_content.parse() { + pins.push(t) + } + if !select_pins_content.is_empty() { + return Err(syn::Error::new( + select_pins_content.span(), + "Encountered an invalid token.", + )); + } + pins + }, + }) + } + } + + #[allow(dead_code)] + pub struct DirectPinDefinition { + pub direct_field_name: Direct, + pub brace_token: syn::token::Brace, + pub pin_field_name: pin, + pub colon_token: Token![:], + pub pin: Ident, + } + + impl Parse for DirectPinDefinition { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let content; + Ok(Self { + direct_field_name: input.parse()?, + brace_token: braced!(content in input), + pin_field_name: content.parse()?, + colon_token: content.parse()?, + pin: content.parse()?, + }) + } + } + + pub enum AnalogPinType { + Multiplexed(MultiplexerDefinition), + Direct(DirectPinDefinition), + } + + impl Parse for AnalogPinType { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let lookahead = input.lookahead1(); + if lookahead.peek(Direct) { + input.parse().map(AnalogPinType::Direct) + } else if lookahead.peek(Multiplexer) { + input.parse().map(AnalogPinType::Multiplexed) + } else { + Err(lookahead.error()) + } + } + } +} + #[proc_macro] pub fn input_pin(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let ident = parse_macro_input!(input as Ident); @@ -265,6 +394,21 @@ pub fn setup_dma_channel(input: proc_macro::TokenStream) -> proc_macro::TokenStr hw::setup_dma_channel(args).into() } +#[proc_macro] +#[proc_macro_error] +pub fn setup_adc_sampler(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + #[cfg(feature = "stm32")] + let channels = parse_macro_input!(input with Punctuated::parse_terminated); + + #[cfg(feature = "nrf")] + let channels = parse_macro_input!(input as hw::NrfAdcSamplerDefinition); + + #[cfg(feature = "rp")] + let channels = parse_macro_input!(input with Punctuated::parse_terminated); + + hw::setup_adc_sampler(channels).into() +} + mod via; #[proc_macro] diff --git a/rumcake/src/hw/mcu/nrf.rs b/rumcake/src/hw/mcu/nrf.rs index cc4661d..9332121 100644 --- a/rumcake/src/hw/mcu/nrf.rs +++ b/rumcake/src/hw/mcu/nrf.rs @@ -4,21 +4,33 @@ //! of other versions of the `mcu` module. This is the case so that parts of `rumcake` can remain //! hardware-agnostic. +use core::cell::RefCell; +use core::mem::MaybeUninit; +use core::ops::DerefMut; + +use defmt::error; +use embassy_futures::select::select; use embassy_nrf::bind_interrupts; +use embassy_nrf::gpio::Output; use embassy_nrf::interrupt::{InterruptExt, Priority}; use embassy_nrf::nvmc::Nvmc; use embassy_nrf::peripherals::SAADC; +use embassy_nrf::ppi::ConfigurableChannel; use embassy_nrf::saadc::{ChannelConfig, Input, Saadc, VddhDiv5Input}; +use embassy_nrf::timer::Instance; use embassy_nrf::usb::Driver; use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex; +use embassy_sync::blocking_mutex::ThreadModeMutex; use embassy_sync::mutex::Mutex; +use embassy_sync::signal::Signal; use embassy_time::{Duration, Timer}; use static_cell::StaticCell; use crate::hw::BATTERY_LEVEL_STATE; +use crate::keyboard::MatrixSampler; pub use rumcake_macros::{ - input_pin, output_pin, setup_buffered_uarte, setup_i2c, setup_i2c_blocking, + input_pin, output_pin, setup_adc_sampler, setup_buffered_uarte, setup_i2c, setup_i2c_blocking, }; pub use embassy_nrf; @@ -26,10 +38,13 @@ pub use embassy_nrf; #[cfg(feature = "nrf-ble")] pub use nrf_softdevice; +use super::Multiplexer; + #[cfg(feature = "nrf52840")] pub const SYSCLK: u32 = 48_000_000; pub type RawMutex = ThreadModeRawMutex; +pub type BlockingMutex = ThreadModeMutex; pub fn jump_to_bootloader() { // TODO @@ -157,43 +172,242 @@ pub fn setup_internal_softdevice_flash(sd: &nrf_softdevice::Softdevice) -> nrf_s nrf_softdevice::Flash::take(sd) } -#[rumcake_macros::task] -pub async fn adc_task() { - let mut adc = unsafe { - bind_interrupts! { - struct Irqs { - SAADC => embassy_nrf::saadc::InterruptHandler; - } - } - embassy_nrf::interrupt::SAADC.set_priority(embassy_nrf::interrupt::Priority::P2); - let vddh = VddhDiv5Input; - let channel = ChannelConfig::single_ended(vddh.degrade_saadc()); - Saadc::new(SAADC::steal(), Irqs, Default::default(), [channel]) - }; +pub type AdcSampleType = i16; + +/// Different types of analog pins. +pub enum AnalogPinType<'a, const MP: usize> +where + [(); 2_usize.pow(MP as u32)]:, +{ + /// A pin that is connected to an analog multiplexer. Must contain a buffer to store the + /// samples obtained by the ADC, and a [`Multiplexer`] definition. + Multiplexed( + [AdcSampleType; 2_usize.pow(MP as u32)], + Multiplexer, MP>, + ), + /// A pin that is directly connected to the analog source. Must contain a buffer to store the + /// sample obtained by the ADC. + Direct([AdcSampleType; 1]), +} - adc.calibrate().await; +/// A sampler for the analog pins on an nRF MCU. This sampler can handle analog pins that may be +/// multiplexed, or directly wired to the analog source. This can also be used to power an analog +/// keyboard matrix. +pub struct AdcSampler<'a, TIM, PPI0, PPI1, const MP: usize, const C: usize> +where + [(); C + 1]:, + [(); 2_usize.pow(MP as u32)]:, +{ + idx_to_pin_type: BlockingMutex; C]>>, + adc_sampler: Mutex>, +} - loop { - let mut buf: [i16; 1] = [0; 1]; - adc.sample(&mut buf).await; +struct RawAdcSampler<'a, TIM, PPI0, PPI1, const C: usize> +where + [(); C + 1]:, +{ + adc: Saadc<'a, { C + 1 }>, + timer: TIM, + ppi_ch0: PPI0, + ppi_ch1: PPI1, +} - let sample = &buf[0]; - let mv = sample * 5; +impl< + 'a, + TIM: Instance, + PPI0: ConfigurableChannel, + PPI1: ConfigurableChannel, + const MP: usize, + const C: usize, + > AdcSampler<'a, TIM, PPI0, PPI1, MP, C> +where + [(); C + 1]:, + [(); 2_usize.pow(MP as u32)]:, +{ + /// Create a new instance of the ADC sampler. + pub fn new( + idx_to_pin_type: [AnalogPinType<'a, MP>; C], + configs: [ChannelConfig<'_>; C], + timer: TIM, + ppi_ch0: PPI0, + ppi_ch1: PPI1, + ) -> Self { + Self { + idx_to_pin_type: BlockingMutex::new(RefCell::new(idx_to_pin_type)), + adc_sampler: unsafe { + bind_interrupts! { + struct Irqs { + SAADC => embassy_nrf::saadc::InterruptHandler; + } + } + embassy_nrf::interrupt::SAADC.set_priority(embassy_nrf::interrupt::Priority::P2); + let channels = { + let mut uninit_arr: [MaybeUninit>; C + 1] = + MaybeUninit::uninit().assume_init(); + + let mut bat_ch_config = + ChannelConfig::single_ended(VddhDiv5Input.degrade_saadc()); + bat_ch_config.time = embassy_nrf::saadc::Time::_3US; + uninit_arr[0] = MaybeUninit::new(bat_ch_config); + + for (i, mut config) in configs.into_iter().enumerate() { + config.time = embassy_nrf::saadc::Time::_3US; + uninit_arr[i + 1] = MaybeUninit::new(config) + } + + uninit_arr.map(|config| config.assume_init()) + }; + + Mutex::new(RawAdcSampler { + adc: Saadc::new(SAADC::steal(), Irqs, Default::default(), channels), + timer, + ppi_ch0, + ppi_ch1, + }) + }, + } + } - let pct = if mv >= 4200 { - 100 - } else if mv <= 3450 { - 0 - } else { - (mv * 2 / 15 - 459) as u8 - }; + /// Run the sampler. This can only be used by running `adc_task`. + #[allow(clippy::await_holding_refcell_ref)] + async fn run_sampler(&self) { + let mut adc_sampler = self.adc_sampler.lock().await; + let RawAdcSampler { + adc, + timer, + ppi_ch0, + ppi_ch1, + .. + } = adc_sampler.deref_mut(); + + adc.calibrate().await; + + let mut bufs = [[[0; C + 1]; 1]; 2]; + + // sample acquisition time: 3 microseconds (based on default saadc::Config) + // sample conversion time: 2 microseconds (worst case, based on datasheet) + // 1/(tacq + tconv): 200kHz + // fsample = 33.333kHz (with sample threshold = 30 and 1MHz timer, buffer starts to get filled every 30 microseconds) + // NOTE: depending on the number of keys and multiplexers, we need to go through multiple cycles of sampling to obtain all the key samples + // example: for 80 keys evenly divided among 5 multiplexers (6 channels total when you include VddhDiv5Input), you need to sample 16 times. + // sampling 6 channels 1 time will take ((3 + 2) microseconds * 6) = 30us. + // sampling 6 channels 16 times will take 480us, enough for the default matrix polling rate of 500us. + adc.run_task_sampler( + timer, + ppi_ch0, + ppi_ch1, + embassy_nrf::timer::Frequency::F1MHz, + 30, + &mut bufs, + move |buf| { + let buf = buf[0]; + self.idx_to_pin_type.lock(|pin_types| { + let mut pin_types = pin_types.borrow_mut(); + BAT_SAMPLE_CHANNEL.signal(buf[0]); + for (i, value) in buf.iter().skip(1).enumerate() { + match &mut pin_types[i] { + AnalogPinType::Multiplexed(values, multiplexer) => { + values[multiplexer.cur_channel as usize] = *value; + multiplexer + .select_channel( + ((multiplexer.cur_channel as usize + 1) + % 2_usize.pow(MP as u32)) + as u8, + ) + .unwrap(); + } + AnalogPinType::Direct(values) => { + values[0] = *value; + } + }; + } + }); + + embassy_nrf::saadc::CallbackResult::Continue + }, + ) + .await; + } - BATTERY_LEVEL_STATE.set(pct).await; + /// Obtain a sample from the ADC. The `ch` argument corresponds to the index of the analog pin + /// you want to sample (which you provided in the [`Self::new()`] method). If the pin is + /// multiplexed, the `sub_ch` argument is used to determine which multiplexer channel to sample + /// from. Otherwise, the `sub_ch` argument is ignored. + pub fn get_sample(&self, channel: usize, sub_channel: usize) -> Option { + self.idx_to_pin_type.lock(|pin_types| { + pin_types + .borrow() + .get(channel) + .and_then(|ch| match ch { + AnalogPinType::Multiplexed(values, _) => values.get(sub_channel).copied(), + AnalogPinType::Direct([result]) => Some(*result), + }) + .map(|value| (value - i16::MIN) as u16) + }) + } +} - Timer::after(Duration::from_secs(10)).await; +impl< + 'a, + TIM: Instance, + PPI0: ConfigurableChannel, + PPI1: ConfigurableChannel, + const MP: usize, + const C: usize, + > MatrixSampler for AdcSampler<'a, TIM, PPI0, PPI1, MP, C> +where + [(); C + 1]:, + [(); 2_usize.pow(MP as u32)]:, +{ + type SampleType = u16; + + fn get_sample(&self, ch: usize, sub_ch: usize) -> Option { + self.get_sample(ch, sub_ch) } } +static BAT_SAMPLE_CHANNEL: Signal = Signal::new(); + +#[rumcake_macros::task] +pub async fn adc_task<'a, const MP: usize, const N: usize>( + sampler: &AdcSampler< + 'a, + impl Instance, + impl ConfigurableChannel, + impl ConfigurableChannel, + MP, + N, + >, +) where + [(); N + 1]:, + [(); 2_usize.pow(MP as u32)]:, +{ + let adc_fut = sampler.run_sampler(); + + let bat_fut = async { + loop { + let sample = BAT_SAMPLE_CHANNEL.wait().await; + let mv = sample * 5; + + let pct = if mv >= 4200 { + 100 + } else if mv <= 3450 { + 0 + } else { + (mv * 2 / 15 - 459) as u8 + }; + + BATTERY_LEVEL_STATE.set(pct).await; + + Timer::after(Duration::from_secs(10)).await; + } + }; + + select(adc_fut, bat_fut).await; + + error!("[NRF_ADC] ADC sampler has stopped. This should not happen."); +} + #[cfg(feature = "nrf-ble")] /// A mutex that is locked when the softdevice is advertising. This is mainly to prevent /// [`nrf_softdevice::ble::peripheral::ADV_PORTAL`] from being opened by more than one task at the diff --git a/rumcake/src/hw/mcu/rp.rs b/rumcake/src/hw/mcu/rp.rs index d67215a..83935a5 100644 --- a/rumcake/src/hw/mcu/rp.rs +++ b/rumcake/src/hw/mcu/rp.rs @@ -4,25 +4,36 @@ //! of other versions of the `mcu` module. This is the case so that parts of `rumcake` can remain //! hardware-agnostic. +use core::cell::RefCell; +use core::ops::DerefMut; + use defmt::assert; +use embassy_rp::adc::{Adc, Async as AdcAsync, Channel}; use embassy_rp::bind_interrupts; use embassy_rp::config::Config; use embassy_rp::flash::Async; use embassy_rp::flash::Flash as HALFlash; -use embassy_rp::peripherals::{FLASH, USB}; +use embassy_rp::gpio::Output; +use embassy_rp::peripherals::{ADC, FLASH, USB}; use embassy_rp::rom_data::reset_to_usb_boot; use embassy_rp::usb::Driver; use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex; +use embassy_sync::blocking_mutex::ThreadModeMutex; pub use rumcake_macros::{ - input_pin, output_pin, setup_buffered_uart, setup_dma_channel, setup_i2c, + input_pin, output_pin, setup_adc_sampler, setup_buffered_uart, setup_dma_channel, setup_i2c, }; pub use embassy_rp; +use crate::keyboard::MatrixSampler; + +use super::Multiplexer; + pub const SYSCLK: u32 = 125_000_000; pub type RawMutex = ThreadModeRawMutex; +pub type BlockingMutex = ThreadModeMutex; /// A function that allows you to jump to the bootloader, usually for re-flashing the firmware. pub fn jump_to_bootloader() { @@ -87,6 +98,85 @@ pub fn setup_usb_driver( } } +/// Different types of analog pins. +pub enum AnalogPinType<'a, const MP: usize> { + /// A pin that is connected to an analog multiplexer. Must contain a [`Multiplexer`] + /// definition. + Multiplexed(Multiplexer, MP>), + /// A pin that is directly connected to the analog source. + Direct, +} + +pub type AdcSampleType = u16; + +// TODO: use a different mutex if using multiple cores on the MCU, thread mode mutex is not safe for multicore. +/// A sampler for the analog pins on an RP MCU. This sampler can handle analog pins that may be +/// multiplexed, or directly wired to the analog source. This can also be used to power an analog +/// keyboard matrix. +pub struct AdcSampler<'a, const MP: usize, const C: usize> { + adc_sampler: BlockingMutex>>, +} + +struct RawAdcSampler<'a, const MP: usize, const C: usize> { + idx_to_pin_type: [AnalogPinType<'a, MP>; C], + channels: [Channel<'a>; C], + adc: Adc<'a, AdcAsync>, +} + +impl<'a, const MP: usize, const C: usize> AdcSampler<'a, MP, C> { + /// Create a new instance of the ADC sampler. + pub fn new(idx_to_pin_type: [AnalogPinType<'a, MP>; C], analog_pins: [Channel<'a>; C]) -> Self { + let adc = unsafe { + bind_interrupts! { + struct Irqs { + ADC_IRQ_FIFO => embassy_rp::adc::InterruptHandler; + } + } + + Adc::new(ADC::steal(), Irqs, Default::default()) + }; + + Self { + adc_sampler: BlockingMutex::new(RefCell::new(RawAdcSampler { + idx_to_pin_type, + channels: analog_pins, + adc, + })), + } + } + + /// Obtain a sample from the ADC. The `ch` argument corresponds to the index of the analog pin + /// you want to sample (which you provided in the [`Self::new()`] method). If the pin is + /// multiplexed, the `sub_ch` argument is used to determine which multiplexer channel to sample + /// from. Otherwise, the `sub_ch` argument is ignored. + pub fn get_sample(&self, ch: usize, sub_ch: usize) -> Option { + self.adc_sampler.lock(|adc_sampler| { + let mut adc_sampler = adc_sampler.borrow_mut(); + let RawAdcSampler { + idx_to_pin_type, + channels, + adc, + } = adc_sampler.deref_mut(); + + idx_to_pin_type.get_mut(ch).map(|channel| match channel { + AnalogPinType::Multiplexed(ref mut multiplexer) => { + multiplexer.select_channel(sub_ch as u8).unwrap(); + adc.blocking_read(&mut channels[ch]).unwrap() + } + AnalogPinType::Direct => adc.blocking_read(&mut channels[ch]).unwrap(), + }) + }) + } +} + +impl<'a, const MP: usize, const C: usize> MatrixSampler for AdcSampler<'a, MP, C> { + type SampleType = AdcSampleType; + + fn get_sample(&self, ch: usize, sub_ch: usize) -> Option { + self.get_sample(ch, sub_ch) + } +} + pub type Flash<'a, const FLASH_SIZE: usize> = HALFlash<'a, FLASH, Async, FLASH_SIZE>; /// Construct an instance of [`Flash`]. This usually needs to be passed to diff --git a/rumcake/src/hw/mcu/stm32.rs b/rumcake/src/hw/mcu/stm32.rs index c7ce833..2c611d8 100644 --- a/rumcake/src/hw/mcu/stm32.rs +++ b/rumcake/src/hw/mcu/stm32.rs @@ -4,18 +4,35 @@ //! of other versions of the `mcu` module. This is the case so that parts of `rumcake` can remain //! hardware-agnostic. -use embassy_stm32::bind_interrupts; +use core::cell::RefCell; +use core::future::poll_fn; +use core::marker::PhantomData; +use core::ops::DerefMut; +use core::task::Poll; + +use embassy_futures::block_on; +use embassy_stm32::adc::{Adc, AdcPin, Instance, InterruptHandler, SampleTime}; use embassy_stm32::flash::{Blocking, Flash as HALFlash}; +use embassy_stm32::gpio::Output; +use embassy_stm32::interrupt::typelevel::Binding; use embassy_stm32::peripherals::{FLASH, PA11, PA12, USB}; -use embassy_stm32::rcc::{APBPrescaler, Hse, Pll, PllMul, PllPreDiv, PllSource, Sysclk, HSI_FREQ}; +use embassy_stm32::rcc::{Pll, PllMul, PllPreDiv, PllSource, Sysclk}; use embassy_stm32::usb::Driver; +use embassy_stm32::{bind_interrupts, Peripheral}; use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex; +use embassy_sync::blocking_mutex::ThreadModeMutex; use static_cell::StaticCell; -pub use rumcake_macros::{input_pin, output_pin, setup_buffered_uart, setup_i2c}; +pub use rumcake_macros::{ + input_pin, output_pin, setup_adc_sampler, setup_buffered_uart, setup_i2c, +}; pub use embassy_stm32; +use crate::keyboard::MatrixSampler; + +use super::Multiplexer; + #[cfg(feature = "stm32f072cb")] pub const SYSCLK: u32 = 48_000_000; @@ -23,6 +40,7 @@ pub const SYSCLK: u32 = 48_000_000; pub const SYSCLK: u32 = 72_000_000; pub type RawMutex = ThreadModeRawMutex; +pub type BlockingMutex = ThreadModeMutex; /// A function that allows you to jump to the bootloader, usually for re-flashing the firmware. pub fn jump_to_bootloader() { @@ -52,6 +70,8 @@ pub fn initialize_rcc() { #[cfg(feature = "stm32f072cb")] { + use embassy_stm32::rcc::HSI_FREQ; + rcc_conf.pll = Some(Pll { src: PllSource::HSI, prediv: PllPreDiv::DIV2, @@ -62,6 +82,8 @@ pub fn initialize_rcc() { #[cfg(feature = "stm32f303cb")] { + use embassy_stm32::rcc::{APBPrescaler, AdcClockSource, AdcPllPrescaler, Hse}; + let hse = embassy_stm32::time::mhz(8); let div = gcd(SYSCLK, hse.0); @@ -76,6 +98,7 @@ pub fn initialize_rcc() { }); rcc_conf.apb1_pre = APBPrescaler::DIV2; rcc_conf.apb2_pre = APBPrescaler::DIV2; + rcc_conf.adc = AdcClockSource::Pll(AdcPllPrescaler::DIV1); rcc_conf.sys = Sysclk::PLL1_P; } @@ -139,6 +162,275 @@ pub fn setup_usb_driver( } } +pub struct Channel { + channel: u8, + _phantom: PhantomData, +} + +impl Channel { + pub fn new(mut pin: impl AdcPin) -> Self { + #[cfg(feature = "stm32f072cb")] + pin.set_as_analog(); + + Self { + channel: pin.channel(), + _phantom: PhantomData, + } + } +} + +/// Different types of analog pins. +pub enum AnalogPinType<'a, const MP: usize> { + /// A pin that is connected to an analog multiplexer. Must contain a [`Multiplexer`] + /// definition. + Multiplexed(Multiplexer, MP>), + /// A pin that is directly connected to the analog source. + Direct, +} + +pub type AdcSampleType = u16; + +/// A sampler for the analog pins on an STM32 MCU. This sampler can handle analog pins that may be +/// multiplexed, or directly wired to the analog source. This can also be used to power an analog +/// keyboard matrix. +pub struct AdcSampler<'a, ADC: Instance, const MP: usize, const C: usize> { + adc_sampler: BlockingMutex>>, +} + +struct RawAdcSampler<'a, ADC: Instance, const MP: usize, const C: usize> { + _adc: Adc<'a, ADC>, + idx_to_pin_type: [AnalogPinType<'a, MP>; C], + analog_pins: [Channel; C], +} + +impl<'a, ADC: Instance, const MP: usize, const C: usize> AdcSampler<'a, ADC, MP, C> { + /// Create a new instance of the ADC sampler. + pub fn new( + adc: impl Peripheral

+ 'a, + irq: impl Binding> + 'a, + idx_to_pin_type: [AnalogPinType<'a, MP>; C], + analog_pins: [Channel; C], + ) -> Self { + let _adc = Adc::new(adc, irq, &mut embassy_time::Delay); + + #[cfg(feature = "stm32f303cb")] + for Channel { channel: ch, .. } in analog_pins.iter() { + let sample_time = SampleTime::CYCLES1_5; + if *ch <= 9 { + ADC::regs() + .smpr1() + .modify(|reg| reg.set_smp(*ch as _, sample_time)); + } else { + ADC::regs() + .smpr2() + .modify(|reg| reg.set_smp((ch - 10) as _, sample_time)); + } + } + + #[cfg(feature = "stm32f072cb")] + { + let sample_time = SampleTime::CYCLES1_5; + ADC::regs().smpr().modify(|reg| reg.set_smp(sample_time)) + } + + Self { + adc_sampler: BlockingMutex::new(RefCell::new(RawAdcSampler { + _adc, + idx_to_pin_type, + analog_pins, + })), + } + } + + async fn read(&self, ch: &mut Channel) -> AdcSampleType { + // This is just Adc::read and Adc::convert inlined. + // Sample time setup is done in `Self::new()`. + + #[cfg(feature = "stm32f303cb")] + { + ADC::regs().sqr1().write(|w| w.set_sq(0, ch.channel)); + + ADC::regs().isr().write(|_| {}); + + ADC::regs().ier().modify(|w| w.set_eocie(true)); + ADC::regs().cr().modify(|w| w.set_adstart(true)); + + poll_fn(|cx| { + ADC::state().waker.register(cx.waker()); + + if ADC::regs().isr().read().eoc() { + Poll::Ready(()) + } else { + Poll::Pending + } + }) + .await; + + ADC::regs().isr().write(|_| {}); + + ADC::regs().dr().read().rdata() + } + + #[cfg(feature = "stm32f072cb")] + { + ADC::regs() + .chselr() + .write(|reg| reg.set_chselx(ch.channel as usize, true)); + + ADC::regs().isr().modify(|reg| { + reg.set_eoc(true); + reg.set_eosmp(true); + }); + + ADC::regs().ier().modify(|w| w.set_eocie(true)); + ADC::regs().cr().modify(|reg| reg.set_adstart(true)); + + poll_fn(|cx| { + ADC::state().waker.register(cx.waker()); + + if ADC::regs().isr().read().eoc() { + Poll::Ready(()) + } else { + Poll::Pending + } + }) + .await; + + ADC::regs().dr().read().data() + } + } + + /// Obtain a sample from the ADC. The `ch` argument corresponds to the index of the analog pin + /// you want to sample (which you provided in the [`Self::new`] method). If the pin is + /// multiplexed, the `sub_ch` argument is used to determine which multiplexer channel to sample + /// from. Otherwise, the `sub_ch` argument is ignored. + pub fn get_sample(&self, ch: usize, sub_ch: usize) -> Option { + self.adc_sampler.lock(|adc_sampler| { + let mut adc_sampler = adc_sampler.borrow_mut(); + let RawAdcSampler { + idx_to_pin_type, + analog_pins, + .. + } = adc_sampler.deref_mut(); + + idx_to_pin_type.get_mut(ch).map(|channel| match channel { + AnalogPinType::Multiplexed(ref mut multiplexer) => { + multiplexer.select_channel(sub_ch as u8).unwrap(); + block_on(self.read(&mut analog_pins[ch])) + } + AnalogPinType::Direct => block_on(self.read(&mut analog_pins[ch])), + }) + }) + } +} + +// ok now this is epic + +impl<'a, ADC: Instance, const MP: usize, const C: usize> MatrixSampler + for AdcSampler<'a, ADC, MP, C> +{ + type SampleType = AdcSampleType; + + fn get_sample(&self, ch: usize, sub_ch: usize) -> Option { + self.get_sample(ch, sub_ch) + } +} + +impl< + 'a, + ADC: Instance, + const MP: usize, + const C: usize, + ADC2: Instance, + const MP2: usize, + const C2: usize, + > MatrixSampler for (AdcSampler<'a, ADC, MP, C>, AdcSampler<'a, ADC2, MP2, C2>) +{ + type SampleType = AdcSampleType; + + fn get_sample(&self, ch: usize, sub_ch: usize) -> Option { + if ch < C { + return self.0.get_sample(ch, sub_ch); + } + + self.1.get_sample(ch, sub_ch) + } +} + +impl< + 'a, + ADC: Instance, + const MP: usize, + const C: usize, + ADC2: Instance, + const MP2: usize, + const C2: usize, + ADC3: Instance, + const MP3: usize, + const C3: usize, + > MatrixSampler + for ( + AdcSampler<'a, ADC, MP, C>, + AdcSampler<'a, ADC2, MP2, C2>, + AdcSampler<'a, ADC3, MP3, C3>, + ) +{ + type SampleType = AdcSampleType; + + fn get_sample(&self, ch: usize, sub_ch: usize) -> Option { + if ch < C { + return self.0.get_sample(ch, sub_ch); + } + + if ch < C2 { + return self.1.get_sample(ch, sub_ch); + } + + self.2.get_sample(ch, sub_ch) + } +} + +impl< + 'a, + ADC: Instance, + const MP: usize, + const C: usize, + ADC2: Instance, + const MP2: usize, + const C2: usize, + ADC3: Instance, + const MP3: usize, + const C3: usize, + ADC4: Instance, + const MP4: usize, + const C4: usize, + > MatrixSampler + for ( + AdcSampler<'a, ADC, MP, C>, + AdcSampler<'a, ADC2, MP2, C2>, + AdcSampler<'a, ADC3, MP3, C3>, + AdcSampler<'a, ADC4, MP4, C4>, + ) +{ + type SampleType = AdcSampleType; + + fn get_sample(&self, ch: usize, sub_ch: usize) -> Option { + if ch < C { + return self.0.get_sample(ch, sub_ch); + } + + if ch < C2 { + return self.1.get_sample(ch, sub_ch); + } + + if ch < C3 { + return self.2.get_sample(ch, sub_ch); + } + + self.3.get_sample(ch, sub_ch) + } +} + /// A wrapper around the [`embassy_stm32::Flash`] struct. This implements /// [`embedded_storage_async`] traits so that it can work with the [`crate::storage`] system. pub struct Flash { diff --git a/rumcake/src/hw/mod.rs b/rumcake/src/hw/mod.rs index 9a46778..61bdc81 100644 --- a/rumcake/src/hw/mod.rs +++ b/rumcake/src/hw/mod.rs @@ -15,15 +15,16 @@ compile_error!("Please enable only one chip feature flag."); #[cfg_attr(feature = "rp", path = "mcu/rp.rs")] pub mod mcu; -use core::ptr::read_volatile; -use core::ptr::write_volatile; -use core::mem::MaybeUninit; -use core::cell::UnsafeCell; use crate::hw::mcu::jump_to_bootloader; use crate::State; +use core::cell::UnsafeCell; +use core::mem::MaybeUninit; +use core::ptr::read_volatile; +use core::ptr::write_volatile; use embassy_futures::select; use embassy_sync::signal::Signal; use embassy_time::Timer; +use embedded_hal::digital::v2::OutputPin; use mcu::RawMutex; @@ -161,3 +162,50 @@ extern "C" { /// [`__config_start`], and add the size of your config section, in bytes. pub static __config_end: u32; } + +pub struct Multiplexer { + cur_channel: u8, + pins: [Option; P], + en: Option, +} + +impl, const P: usize> Multiplexer { + pub fn new(pins: [Option; P], en: Option) -> Self { + let mut multiplexer = Self { + pins, + en, + cur_channel: 0, + }; + let _ = multiplexer.select_channel(0); + multiplexer + } + + pub fn select_channel(&mut self, mut channel: u8) -> Result<(), E> { + for i in 0..P { + if let Some(ref mut pin) = self.pins[i] { + if channel & 0x01 == 0x01 { + pin.set_high()?; + } else { + pin.set_low()?; + } + } + channel >>= 1; + } + self.cur_channel = channel; + Ok(()) + } + + pub fn enable(&mut self) -> Result<(), E> { + if let Some(ref mut en) = self.en { + en.set_low()?; + } + Ok(()) + } + + pub fn disable(&mut self) -> Result<(), E> { + if let Some(ref mut en) = self.en { + en.set_high()?; + } + Ok(()) + } +} diff --git a/rumcake/src/keyboard.rs b/rumcake/src/keyboard.rs index e207b23..60709fd 100644 --- a/rumcake/src/keyboard.rs +++ b/rumcake/src/keyboard.rs @@ -4,6 +4,8 @@ //! Keyboard layouts and matrices are implemented with the help of [TeXitoi's `keyberon` crate](`keyberon`). use core::convert::Infallible; +use core::ops::Range; + use defmt::{debug, info, warn, Debug2Format}; use embassy_sync::channel::Channel; use embassy_sync::mutex::{Mutex, MutexGuard}; @@ -11,9 +13,11 @@ use embassy_sync::pubsub::{PubSubBehavior, PubSubChannel}; use embassy_time::{Duration, Ticker, Timer}; use embedded_hal::digital::v2::{InputPin, OutputPin}; use heapless::Vec; +use keyberon::analog::{AnalogActuator, AnalogAcutationMode}; use keyberon::debounce::Debouncer; use keyberon::layout::{CustomEvent, Event, Layers, Layout as KeyberonLayout}; -use keyberon::matrix::{DirectPinMatrix, Matrix}; +use keyberon::matrix::{AnalogMatrix, DirectPinMatrix, Matrix}; +use num_traits::SaturatingSub; use usbd_human_interface_device::device::consumer::MultipleConsumerReport; use usbd_human_interface_device::{ device::keyboard::NKROBootKeyboardReport, page::Keyboard as KeyboardKeycode, @@ -26,7 +30,7 @@ use crate::hw::mcu::RawMutex; use crate::hw::CURRENT_OUTPUT_STATE; pub use rumcake_macros::{ - build_direct_pin_matrix, build_layout, build_standard_matrix, remap_matrix, + build_analog_matrix, build_direct_pin_matrix, build_layout, build_standard_matrix, remap_matrix, }; /// Basic keyboard trait that must be implemented to use rumcake. Defines basic keyboard information. @@ -174,6 +178,19 @@ pub fn setup_direct_pin_keyboard_matrix< Ok((matrix, debouncer)) } +/// Setup an analog keyboard matrix. The output of this function can be passed to the matrix +/// polling task directly. +pub fn setup_analog_keyboard_matrix( + sampler: &S, + pos_to_ch: [[(u8, u8); CS]; RS], + ranges: [[Range; CS]; RS], +) -> PollableAnalogMatrix { + let sampler = AnalogMatrixSampler { pos_to_ch, sampler }; + let matrix = AnalogMatrix::new(ranges); + let actuator = AnalogActuator::new([[AnalogAcutationMode::default(); CS]; RS], [[127; CS]; RS]); + (sampler, matrix, actuator) +} + /// Custom keycodes used to interact with other rumcake features. /// /// These can be used in your keyboard layout, defined in [`KeyboardLayout::get_layout`] @@ -232,12 +249,8 @@ pub trait Pollable { fn events(&mut self) -> impl Iterator; } -pub type PollableStandardMatrix< - I: InputPin, - O: OutputPin, - const CS: usize, - const RS: usize, -> = (Matrix, Debouncer<[[bool; CS]; RS]>); +pub type PollableStandardMatrix = + (Matrix, Debouncer<[[bool; CS]; RS]>); impl< I: InputPin, @@ -257,11 +270,8 @@ impl< } } -pub type PollableDirectPinMatrix< - I: InputPin, - const CS: usize, - const RS: usize, -> = (DirectPinMatrix, Debouncer<[[bool; CS]; RS]>); +pub type PollableDirectPinMatrix = + (DirectPinMatrix, Debouncer<[[bool; CS]; RS]>); impl, const CS: usize, const RS: usize> Pollable for PollableDirectPinMatrix @@ -271,6 +281,61 @@ impl, const CS: usize, const RS: usize> Pollable } } +/// Trait that allows you to use ADC hardware to pull samples for an analog matrix. +pub trait MatrixSampler { + /// Type of samples generated by the ADC. + type SampleType: SaturatingSub + PartialOrd; + + /// Get the sample for the given analog pin. `sub_ch` is used only if the pin is multiplexed. + fn get_sample(&self, ch: usize, sub_ch: usize) -> Option; +} + +pub struct AnalogMatrixSampler<'a, S, const CS: usize, const RS: usize> { + pos_to_ch: [[(u8, u8); CS]; RS], + sampler: &'a S, +} + +impl<'a, S: MatrixSampler, const CS: usize, const RS: usize> AnalogMatrixSampler<'a, S, CS, RS> { + fn get_key_state(&self, row: usize, col: usize) -> Option { + self.pos_to_ch + .get(row) + .and_then(|row| row.get(col)) + .and_then(|(ch, sub_ch)| { + MatrixSampler::get_sample(self.sampler, *ch as usize, *sub_ch as usize) + }) + } +} + +#[derive(Debug)] +enum SampleError { + NoSampleForKeyPosition(usize, usize), +} + +pub type PollableAnalogMatrix<'a, S, const CS: usize, const RS: usize> = ( + AnalogMatrixSampler<'a, S, CS, RS>, + AnalogMatrix<::SampleType, CS, RS>, + AnalogActuator, +); + +impl Pollable + for PollableAnalogMatrix<'_, S, CS, RS> +where + u32: From, +{ + fn events(&mut self) -> impl Iterator { + let matrix_state = self + .1 + .get(|row, col| { + self.0 + .get_key_state(row, col) + .ok_or(SampleError::NoSampleForKeyPosition(row, col)) + }) + .unwrap(); + + self.2.events(matrix_state) + } +} + /// Channel with keyboard events polled from the swtich matrix /// /// The coordinates received will be remapped according to the implementation of