Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/docs/main/docs/configuration/input_device/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All input devices are defined in the `[input_device]` table. Currently supported
- [Joystick (joystick)](./joystick.md)
- [PMW3610 Optical Mouse Sensor (pmw3610)](./pmw3610.md)
- [Azoteq IQS5xx Trackpad (iqs5xx)](./iqs5xx.md)
- [Azoteq IQS9151 Trackpad (iqs9151)](./iqs9151.md)

Please refer to the corresponding documentation for detailed configuration settings.

Expand Down
89 changes: 89 additions & 0 deletions docs/docs/main/docs/configuration/input_device/iqs9151.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Azoteq IQS9151 Trackpad

The Azoteq IQS9151 is an I²C capacitive trackpad controller used by some
keyboard trackpad modules.

::: note

- Currently only relative cursor movement is reported. Gestures, scrolling,
virtual buttons, dynamic scaling, and persistent IC configuration are not
supported.
- The driver performs a product-number check (`0x09bc`) and a minimal runtime
setup. It does not ship a panel-specific Azoteq configuration image.
- An active-low `RDY` pin is strongly recommended. Without it, the driver falls
back to timed polling and may stall the I²C bus through clock-stretching if
it polls mid-cycle. Event mode is only enabled when `RDY` is configured.
- Each `[[input_device.iqs9151]]` claims its own I²C peripheral. Sharing a bus
with another I²C device (e.g. an OLED) isn't supported yet.

:::

## Hardware

- `SDA` / `SCL` — I²C bus, 7-bit address `0x56`.
- `RDY` — active-low digital output used to identify the I²C communication
window. Connect to a GPIO that supports async level waits
(`embedded_hal_async::digital::Wait`).

## `toml` configuration

```toml
[[input_device.iqs9151]]
name = "trackpad0"
id = 0 # optional 0-255. Used for debug prints. Defaults to 0.

i2c.instance = "I2C0" # RP2040: I2C0 / I2C1. nRF52: TWISPI0 / TWISPI1 / TWISPI2.
i2c.sda = "PIN_4"
i2c.scl = "PIN_5"

# Optional: active-low RDY pin. Strongly recommended.
rdy = "PIN_15"

# Axis tweaks applied in PointingProcessor.
# proc_invert_x = true
# proc_invert_y = true
# proc_swap_xy = true
```

### Split

To add the trackpad to the central or a peripheral:

```toml
[[split.central.input_device.iqs9151]]
name = ...

# resp.
[[split.peripheral.input_device.iqs9151]]
name = ...
```

For split keyboards the device runs on whichever side it's wired to; the
matching `PointingProcessor` is generated on the central automatically.

## Rust configuration

Construct the device directly. For a split keyboard, add the device to whichever
side (`central.rs` or `peripheral.rs`) the trackpad is physically wired to.

```rust
use embassy_rp::gpio::{Input, Pull};
use embassy_rp::i2c::{Config, I2c};
use rmk::input_device::iqs9151::Iqs9151;
use rmk::input_device::pointing::{PointingProcessor, PointingProcessorConfig};

let mut i2c_cfg = Config::default();
i2c_cfg.frequency = 400_000;
let i2c = I2c::new_async(p.I2C0, p.PIN_5, p.PIN_4, Irqs, i2c_cfg);
let rdy = Some(Input::new(p.PIN_15, Pull::None));

const POINTING_DEV_ID: u8 = 0;
let mut trackpad = Iqs9151::new(POINTING_DEV_ID, i2c, rdy);

let mut trackpad_proc = PointingProcessor::new(
&keymap,
PointingProcessorConfig::default(),
);

run_all!(trackpad, trackpad_proc, /* matrix, ... */);
```
71 changes: 71 additions & 0 deletions rmk-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,7 @@ pub struct InputDeviceConfig {
pub pmw3610: Option<Vec<Pmw3610Config>>,
pub pmw33xx: Option<Vec<Pmw33xxConfig>>,
pub iqs5xx: Option<Vec<Iqs5xxConfig>>,
pub iqs9151: Option<Vec<Iqs9151Config>>,
}

#[derive(Clone, Debug, Default, Deserialize)]
Expand Down Expand Up @@ -997,6 +998,43 @@ pub struct Iqs5xxI2cConfig {
pub scl: String,
}

/// Azoteq IQS9151 trackpad configuration.
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Iqs9151Config {
/// Name of the trackpad (used for variable naming).
pub name: String,
/// RMK pointing-device id (0-255). Defaults to 0.
pub id: Option<u8>,
/// I²C bus the trackpad is connected to. The bus is dedicated to this
/// device — sharing with other I²C peripherals (e.g. an OLED) is not yet
/// supported via TOML.
pub i2c: Iqs9151I2cConfig,
/// Optional active-low `RDY` pin. Strongly recommended; without it the
/// driver falls back to timed polling and may stall the bus through
/// clock-stretching.
pub rdy: Option<String>,
/// Invert X in the PointingProcessor.
#[serde(default)]
pub proc_invert_x: bool,
/// Invert Y in the PointingProcessor.
#[serde(default)]
pub proc_invert_y: bool,
/// Swap X and Y in the PointingProcessor.
#[serde(default)]
pub proc_swap_xy: bool,
}

/// I²C bus configuration for the IQS9151. Distinct from the generic
/// `I2cConfig` because the default IQS9151 address is fixed at `0x56`.
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Iqs9151I2cConfig {
pub instance: String,
pub sda: String,
pub scl: String,
}

#[derive(Clone, Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EncoderConfig {
Expand Down Expand Up @@ -1221,4 +1259,37 @@ subs = 2
assert_eq!(config.event.layer_change.pubs, 2);
assert_eq!(config.event.layer_change.subs, 2);
}

#[test]
fn test_iqs9151_input_device_config() {
let user_toml = r#"
[[input_device.iqs9151]]
name = "trackpad"
id = 7
rdy = "P1_11"
i2c.instance = "TWISPI0"
i2c.sda = "P0_04"
i2c.scl = "P0_05"
proc_invert_x = true
"#;
let config: KeyboardTomlConfig = Config::builder()
.add_source(File::from_str(user_toml, FileFormat::Toml))
.build()
.unwrap()
.try_deserialize()
.unwrap();

let iqs9151 = config.input_device.unwrap().iqs9151.unwrap();

assert_eq!(iqs9151.len(), 1);
assert_eq!(iqs9151[0].name, "trackpad");
assert_eq!(iqs9151[0].id, Some(7));
assert_eq!(iqs9151[0].rdy.as_deref(), Some("P1_11"));
assert_eq!(iqs9151[0].i2c.instance, "TWISPI0");
assert_eq!(iqs9151[0].i2c.sda, "P0_04");
assert_eq!(iqs9151[0].i2c.scl, "P0_05");
assert!(iqs9151[0].proc_invert_x);
assert!(!iqs9151[0].proc_invert_y);
assert!(!iqs9151[0].proc_swap_xy);
}
}
6 changes: 3 additions & 3 deletions rmk-config/src/resolved/hardware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ pub use crate::chip::{ChipModel, ChipSeries};
pub use crate::communication::{CommunicationConfig, UsbInfo};
pub use crate::{
BleConfig, ChipConfig, CommunicationProtocol, DependencyConfig, DisplayConfig, DisplayDriver, EncoderConfig,
EncoderResolution, I2cConfig, InputDeviceConfig, Iqs5xxConfig, Iqs5xxI2cConfig, JoystickConfig, KeyInfo,
LightConfig, MatrixConfig, MatrixType, OutputConfig, PinConfig, Pmw33xxConfig, Pmw33xxType, Pmw3610Config,
PointingDeviceConfig, SerialConfig, SpiConfig, SplitBoardConfig, SplitConfig,
EncoderResolution, I2cConfig, InputDeviceConfig, Iqs5xxConfig, Iqs5xxI2cConfig, Iqs9151Config, Iqs9151I2cConfig,
JoystickConfig, KeyInfo, LightConfig, MatrixConfig, MatrixType, OutputConfig, PinConfig, Pmw33xxConfig,
Pmw33xxType, Pmw3610Config, PointingDeviceConfig, SerialConfig, SpiConfig, SplitBoardConfig, SplitConfig,
};

/// Resolved storage hardware config
Expand Down
16 changes: 16 additions & 0 deletions rmk-macro/src/codegen/chip/bind_interrupt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use syn::ItemMod;
use crate::codegen::display::expand_display_interrupt;
use crate::codegen::feature::{get_rmk_features, is_feature_enabled};
use crate::codegen::input_device::iqs5xx::expand_iqs5xx_interrupts;
use crate::codegen::input_device::iqs9151::expand_iqs9151_interrupts;

/// Expand `bind_interrupt!` stuffs, and other code before `main` function
pub(crate) fn expand_bind_interrupt(hardware: &Hardware, item_mod: &ItemMod) -> TokenStream2 {
Expand Down Expand Up @@ -92,6 +93,19 @@ pub(crate) fn bind_interrupt_default(hardware: &Hardware, item_mod: &ItemMod) ->
.unwrap_or(Vec::new()),
};
let iqs5xx_interrupt = expand_iqs5xx_interrupts(&chip.series, &iqs5xx_config);
let iqs9151_config = match board {
BoardConfig::UniBody(UniBodyConfig { input_device, .. }) => {
input_device.clone().iqs9151.unwrap_or(Vec::new())
}
BoardConfig::Split(split_config) => split_config
.central
.input_device
.clone()
.unwrap_or(InputDeviceConfig::default())
.iqs9151
.unwrap_or(Vec::new()),
};
let iqs9151_interrupt = expand_iqs9151_interrupts(&chip.series, &iqs9151_config);

match chip.series {
rmk_config::resolved::hardware::ChipSeries::Stm32 => {
Expand Down Expand Up @@ -246,6 +260,7 @@ pub(crate) fn bind_interrupt_default(hardware: &Hardware, item_mod: &ItemMod) ->
RTC0 => ::nrf_sdc::mpsl::HighPrioInterruptHandler;
#pmw33xx_spi_interrupts
#iqs5xx_interrupt
#iqs9151_interrupt
#display_interrupt
#extern_irqs
});
Expand Down Expand Up @@ -304,6 +319,7 @@ pub(crate) fn bind_interrupt_default(hardware: &Hardware, item_mod: &ItemMod) ->
#dma_irq_0
#pio0_irq_0
#iqs5xx_interrupt
#iqs9151_interrupt
#display_interrupt
});
#ble_task
Expand Down
8 changes: 5 additions & 3 deletions rmk-macro/src/codegen/input_device/iqs5xx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ pub(crate) fn expand_iqs5xx_device(

let device_ident = format_ident!("{}_device", sensor_name);
let i2c_ident = format_ident!("{}_i2c", sensor_name);
let i2c_buf_ident = format_ident!("{}_i2c_buf", sensor_name);
let i2c_buf_ref_ident = format_ident!("{}_i2c_buf_ref", sensor_name);
let rdy_ident = format_ident!("{}_rdy", sensor_name);
let processor_ident = format_ident!("{}_processor", sensor_name);
let processor_ident_config = format_ident!("{}_config", processor_ident);
Expand Down Expand Up @@ -77,15 +79,15 @@ pub(crate) fn expand_iqs5xx_device(
let device_init = match chip.series {
ChipSeries::Nrf52 => quote! {
#rdy_init
static #i2c_ident: ::static_cell::StaticCell<[u8; 16]> = ::static_cell::StaticCell::new();
let #i2c_ident = #i2c_ident.init([0u8; 16]);
static #i2c_buf_ident: ::static_cell::StaticCell<[u8; 16]> = ::static_cell::StaticCell::new();
let #i2c_buf_ref_ident = #i2c_buf_ident.init([0u8; 16]);
let #i2c_ident = ::embassy_nrf::twim::Twim::new(
p.#instance_ident,
Irqs,
p.#sda_ident,
p.#scl_ident,
::embassy_nrf::twim::Config::default(),
#i2c_ident,
#i2c_buf_ref_ident,
);
let mut #device_ident = ::rmk::input_device::iqs5xx::Iqs5xx::new(
#sensor_id,
Expand Down
Loading