Compare commits

...

3 Commits

Author SHA1 Message Date
35e97a92e3 perf: don't read/write pin mode, write only
vendored mcp23017 library too
2024-11-10 21:10:21 -05:00
65fbaa323b fix: linear velocity profile broken 2024-11-10 19:41:32 -05:00
66d04dacc2 feat: velocity profiles 2024-11-10 18:56:08 -05:00
16 changed files with 806 additions and 30 deletions

6
Cargo.lock generated
View File

@ -772,7 +772,7 @@ dependencies = [
[[package]]
name = "geode_piano"
version = "0.2.1"
version = "0.3.0"
dependencies = [
"byte-slice-cast 1.2.2",
"cortex-m",
@ -989,9 +989,7 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "mcp23017"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c32fd6627e73f1cfa95c00ddcdcb5a6a6ddbd10b308d08588a502c018b6e12c"
version = "1.1.0"
dependencies = [
"embedded-hal 0.2.7",
]

View File

@ -1,6 +1,6 @@
[package]
name = "geode_piano"
version = "0.2.1"
version = "0.3.0"
edition = "2021"
license = "GPL-3.0-only"
@ -42,7 +42,8 @@ static_cell = "2"
portable-atomic = { version = "1.5", features = ["critical-section"] }
log = "0.4"
mcp23017 = { version = "1.0.0" }
# vendored for performance reasons
mcp23017 = { version = "1.1.0", path = "vendor/mcp23017" }
shared-bus = "0.3.1"
[profile.release]

View File

@ -44,7 +44,7 @@ but is harder to set up.
### with debug probe
- Install `probe-rs`.
- Install `probe-rs-tools` (`cargo install probe-rs-tools --locked`).
- Follow the wiring instructions in the [Pico Getting Started Guide](https://datasheets.raspberrypi.com/pico/getting-started-with-pico.pdf), at _Appendix A: Using Picoprobe_ in the Picoprobe Wiring section.
You only need to wire GND, SWCLK and SWDIO.
- If you are using a second Pico as a debug probe,

View File

@ -28,7 +28,7 @@ use embassy_rp::i2c;
use embassy_rp::peripherals::USB;
use embassy_rp::usb::{Driver, InterruptHandler};
use geode_piano::matrix;
use geode_piano::matrix::KeyMatrix;
use geode_piano::matrix::{KeyMatrix, VelocityProfile};
use geode_piano::midi;
use geode_piano::usb::usb_task;
use geode_piano::{blinky, pin_array, pins, unwrap};
@ -445,7 +445,13 @@ async fn piano_task(pin_driver: pins::TransparentPins) {
];
let mut mat = KeyMatrix::new(col_pins, row_pins, keymap);
mat.scan(pin_driver).await;
mat.scan(
pin_driver,
matrix::Config {
velocity_prof: VelocityProfile::Linear,
},
)
.await;
}
bind_interrupts!(struct Irqs {

View File

@ -3,9 +3,9 @@
use crate::midi;
use crate::pins;
use crate::unwrap;
use core::cmp::min;
use core::cmp::{max, min};
use embassy_rp::gpio;
use embassy_time::{Duration, Instant, Ticker};
use embassy_time::{Duration, Instant, Timer};
pub enum NormalState {
/// Normal open
@ -14,6 +14,38 @@ pub enum NormalState {
NC,
}
/// Profile to map from key press duration to MIDI velocity.
/// https://www.desmos.com/calculator/mynk7thhzp
pub enum VelocityProfile {
Linear,
Heavy,
Light,
}
fn velocity_light(us: u64) -> u8 {
if us <= 60000 {
min(127, (135000 - us * 6 / 5) / 1000) as u8
} else {
(127 - min(us, 240000) / 4000 - 60) as u8
}
}
fn velocity_heavy(us: u64) -> u8 {
if us <= 17000 {
((113000 - us) / 1000) as u8
} else {
((127000 - min(us, 190000) / 2 - 22000) / 1000) as u8
}
}
fn velocity_linear(us: u64) -> u8 {
(max(118000 - (us as i32), 5000) / 1000) as u8
}
pub struct Config {
pub velocity_prof: VelocityProfile,
}
/// Task to handle pedals in MIDI
///
/// `norm_open` represents a normally open switch
@ -63,16 +95,12 @@ impl<const N_ROWS: usize, const N_COLS: usize> KeyMatrix<N_ROWS, N_COLS> {
}
}
pub async fn scan(&mut self, mut pin_driver: pins::TransparentPins) {
pub async fn scan(&mut self, mut pin_driver: pins::TransparentPins, config: Config) {
for i in pin_driver.pins {
unwrap(pin_driver.set_input(i)).await;
unwrap(pin_driver.set_pull(i, gpio::Pull::Up)).await;
}
// scan frequency
// this might(?) panic if the scan takes longer than the tick
let mut ticker = Ticker::every(Duration::from_micros(4000));
let chan = midi::MidiChannel::new(0);
const MAX_NOTES: usize = 128;
@ -89,7 +117,7 @@ impl<const N_ROWS: usize, const N_COLS: usize> KeyMatrix<N_ROWS, N_COLS> {
loop {
let profile: bool = counter == 0;
counter += 1;
counter %= 500;
counter %= 5000;
let prof_start = Instant::now();
let mut prof_time_last_col = prof_start;
let mut prof_dur_col = Duration::from_ticks(0);
@ -121,16 +149,16 @@ impl<const N_ROWS: usize, const N_COLS: usize> KeyMatrix<N_ROWS, N_COLS> {
midi::KeyAction::N2(note) => {
if key_active {
if note_first[note as usize].is_some() && !note_on[note as usize] {
// millisecond duration of keypress
// microsecond duration of keypress
let dur =
note_first[note as usize].unwrap().elapsed().as_millis();
let velocity: u8 = if dur <= 60 {
(127 - dur * 6 / 5) as u8
} else {
(127 - min(dur, 240) / 4 - 60) as u8
note_first[note as usize].unwrap().elapsed().as_micros();
let velocity = match config.velocity_prof {
VelocityProfile::Heavy => velocity_heavy(dur),
VelocityProfile::Linear => velocity_linear(dur),
VelocityProfile::Light => velocity_light(dur),
};
defmt::debug!(
"{} velocity {} from dur {}ms",
"{} velocity {} from dur {}us",
note,
velocity,
dur
@ -171,7 +199,8 @@ impl<const N_ROWS: usize, const N_COLS: usize> KeyMatrix<N_ROWS, N_COLS> {
);
}
ticker.next().await;
// relinquish to other tasks for a moment
Timer::after_micros(50).await;
}
}
}

View File

@ -213,7 +213,7 @@ impl From<EndpointError> for Disconnected {
}
}
static MIDI_QUEUE: Channel<ThreadModeRawMutex, MidiMsg, 3> = Channel::new();
static MIDI_QUEUE: Channel<ThreadModeRawMutex, MidiMsg, 10> = Channel::new();
/// Handle sending MIDI until connection breaks
pub async fn midi_session<'d, T: Instance + 'd>(

View File

@ -108,6 +108,8 @@ impl IntoIterator for PinCollection {
pub struct TransparentPins {
addrs: [u8; N_PIN_EXTENDERS],
onboard_pins: [Flex<'static, AnyPin>; N_REGULAR_PINS],
/// Input/output state of each pin. 1 bit is input, 0 bit is output.
io_state: u64,
i2c_bus: I2cBus,
disable_unsafe_pins: bool,
/// Usable pins per extender. Depends on `disable_unsafe_pins`.
@ -191,6 +193,7 @@ impl TransparentPins {
) -> Result<Self, Error> {
let mut ret = TransparentPins {
addrs,
io_state: (1 << (N_REGULAR_PINS + N_EXTENDED_PINS)) - 1,
onboard_pins: pins.map(Flex::new),
i2c_bus: shared_bus::BusManagerSimple::new(i2c),
disable_unsafe_pins: false,
@ -221,7 +224,12 @@ impl TransparentPins {
// ports are flipped from what it should be
let port_a = (val & (0xff00)) >> 8;
let port_b = val & (0x00ff);
defmt::trace!("raw_to_usable: raw {:016b} a {:08b} b {:08b}", val, port_a, port_b);
defmt::trace!(
"raw_to_usable: raw {:016b} a {:08b} b {:08b}",
val,
port_a,
port_b
);
(port_a & 0x7f) | ((port_b & 0x7f) << 7)
} else {
val
@ -300,10 +308,13 @@ impl TransparentPins {
pub fn set_input(&mut self, addr: u8) -> Result<(), Error> {
let pin_n = self.addr_to_pin(addr);
let pin = self.get_pin(pin_n)?;
self.io_state |= 1 << pin_n;
match pin {
TransparentPin::Onboard(p) => self.onboard_pins[p].set_as_input(),
TransparentPin::Extended(p) => {
extender!(self, p.ext_id)?.pin_mode(p.loc_pin, mcp23017::PinMode::INPUT)?
let ext_io_word = (self.io_state >> (p.ext_id * PINS_PER_EXTENDER))
& ((1 << PINS_PER_EXTENDER) - 1);
extender!(self, p.ext_id)?.overwrite_pin_mode(p.loc_pin, ext_io_word as u16)?;
}
}
Ok(())
@ -313,10 +324,13 @@ impl TransparentPins {
pub fn set_output(&mut self, addr: u8) -> Result<(), Error> {
let pin_n = self.addr_to_pin(addr);
let pin = self.get_pin(pin_n)?;
self.io_state &= !(1 << pin_n);
match pin {
TransparentPin::Onboard(p) => self.onboard_pins[p].set_as_output(),
TransparentPin::Extended(p) => {
extender!(self, p.ext_id)?.pin_mode(p.loc_pin, mcp23017::PinMode::OUTPUT)?
let ext_io_word = (self.io_state >> (p.ext_id * PINS_PER_EXTENDER))
& ((1 << PINS_PER_EXTENDER) - 1);
extender!(self, p.ext_id)?.overwrite_pin_mode(p.loc_pin, ext_io_word as u16)?;
}
}
Ok(())

View File

@ -50,7 +50,7 @@ pub async fn usb_task(
let mut config = Config::new(0xdead, 0xbeef);
config.manufacturer = Some("dogeystamp");
config.product = Some("Geode-Piano MIDI keyboard");
config.serial_number = Some("0.2.1");
config.serial_number = Some("0.3.0");
config.max_power = 100;
config.max_packet_size_0 = 64;

View File

@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: cargo
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10

View File

@ -0,0 +1,144 @@
---
name: mcp23017-ci
on:
push:
branches:
- '**'
tags:
- "*.*.*"
env:
RUSTFLAGS: '--deny warnings'
jobs:
build:
name: Build
runs-on: ubuntu-latest
strategy:
matrix:
rust: [ stable, beta, nightly, 1.52.1 ]
TARGET:
- x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl
- arm-unknown-linux-gnueabi # Raspberry Pi 1
- armv7-unknown-linux-gnueabihf # Raspberry Pi 2, 3, etc
# Bare metal
- thumbv6m-none-eabi
- thumbv7em-none-eabi
- thumbv7em-none-eabihf
- thumbv7m-none-eabi
include:
- rust: nightly
experimental: true
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
target: ${{ matrix.TARGET }}
override: true
- name: Checkout CI scripts
uses: actions/checkout@v2
with:
repository: 'eldruin/rust-driver-ci-scripts'
ref: 'master'
path: 'ci'
- run: ./ci/patch-no-std.sh
if: ${{ ! contains(matrix.TARGET, 'x86_64') }}
checks:
name: Checks
runs-on: ubuntu-latest
strategy:
matrix:
rust: [ stable, beta ]
TARGET:
- x86_64-unknown-linux-gnu
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
target: ${{ matrix.TARGET }}
override: true
components: clippy, rustfmt
- name: Doc
uses: actions-rs/cargo@v1
with:
command: doc
- name: Formatting
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-latest
strategy:
matrix:
rust: [ stable ]
TARGET:
- x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl
- arm-unknown-linux-gnueabi # Raspberry Pi 1
- armv7-unknown-linux-gnueabihf # Raspberry Pi 2, 3, etc
# Bare metal
- thumbv6m-none-eabi
- thumbv7em-none-eabi
- thumbv7em-none-eabihf
- thumbv7m-none-eabi
include:
- experimental: true
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
target: ${{ matrix.TARGET }}
override: true
- run: cargo clippy --all-features -- --deny=warnings
test:
name: Tests
runs-on: ubuntu-latest
strategy:
matrix:
rust: [ stable, beta, nightly ]
TARGET: [ x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl ]
include:
- rust: nightly
experimental: true
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
target: ${{ matrix.TARGET }}
override: true
- name: Test
uses: actions-rs/cargo@v1
with:
command: test
args: --target=${{ matrix.TARGET }}
- name: Build examples
uses: actions-rs/cargo@v1
if: contains(matrix.TARGET, 'x86_64')
with:
command: build
args: --target=${{ matrix.TARGET }} --examples

10
vendor/mcp23017/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk

18
vendor/mcp23017/Cargo.toml vendored Normal file
View File

@ -0,0 +1,18 @@
[package]
name = "mcp23017"
version = "1.1.0"
description = "A rust driver for the MCP23017 (16-Bit I2C I/O Expander with Serial Interface)"
authors = ["Luca Zulian <lucagiuggia@gmail.com>"]
categories = ["embedded", "hardware-support", "no-std"]
keywords = ["hal", "IO"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/circuitry-maker/mcp23017"
edition = "2018"
exclude = [
"docs/",
"docs/*",
]
[dependencies]
embedded-hal = "0.2.3"

21
vendor/mcp23017/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Luca Zulian
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

62
vendor/mcp23017/README.md vendored Normal file
View File

@ -0,0 +1,62 @@
# `mcp23017`
> no_std driver for [MCP23017](http://ww1.microchip.com/downloads/en/DeviceDoc/20001952C.pdf) (16-Bit I2C I/O Expander with Serial Interface module)
[![Build Status](https://github.com/circuitry-maker/mcp23017/workflows/mcp23017-ci/badge.svg)](https://github.com/circuitry-maker/mcp23017/actions?query=workflow%3Amcp23017-ci)
[![crates.io](https://img.shields.io/crates/v/mcp23017.svg)](https://crates.io/crates/mcp23017)
[![Docs](https://docs.rs/mcp23017/badge.svg)](https://docs.rs/mcp23017)
## Basic usage
Include this [library](https://crates.io/crates/mcp23017) as a dependency in your `Cargo.toml`:
```rust
[dependencies.mcp23017]
version = "<version>"
```
Use [embedded-hal](https://github.com/rust-embedded/embedded-hal) implementation to get I2C handle and then create mcp23017 handle:
```rust
extern crate mcp23017;
match mcp23017::MCP23017::default(i2c) {
Ok(mut u) => {
u.init_hardware();
u.pin_mode(1, mcp23017::PinMode::OUTPUT); // for the first pin
u.all_pin_mode(mcp23017::PinMode::OUTPUT); // or for all pins
let status = u.read_gpioab().unwrap();
println!("all {:#?}", status).unwrap();
let read_a = u.read_gpio(mcp23017::Port::GPIOA).unwrap();
println!("port a {:#?}", read_a).unwrap();
match u.write_gpioab(65503){
Ok(_) => {
println!("ok").unwrap();
}
_ => {
println!("something wrong").unwrap();
}
}
}
Err(mcp23017::MCP23017::Error::BusError(error)) => {
println!("{:#?}", error).unwrap();;
panic!();
}
_ => {
panic!();
}
};
```
### Hardware address pins
![](docs/address-pins.jpg)
## Documentation
API Docs available on [docs.rs](https://docs.rs/mcp23017/0.1.0/mcp23017/)
## License
[MIT license](http://opensource.org/licenses/MIT)

BIN
vendor/mcp23017/docs/address-pins.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

466
vendor/mcp23017/src/lib.rs vendored Normal file
View File

@ -0,0 +1,466 @@
#![no_std]
//! Manages an MCP23017, a 16-Bit I2C I/O Expander with Serial Interface module.
//!
//! This operates the chip in `IOCON.BANK=0` mode, i.e. the registers are mapped sequentially.
//! This driver does not set `IOCON.BANK`, but the factory default is `0` and this driver does
//! not change that value.
//!
//! See [the datasheet](http://ww1.microchip.com/downloads/en/DeviceDoc/20001952C.pdf) for more
//! information on the device.
#![deny(
missing_docs,
missing_debug_implementations,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unstable_features,
unused_import_braces,
unused_qualifications,
warnings
)]
#![allow(dead_code, non_camel_case_types)]
#![allow(clippy::uninit_assumed_init, clippy::upper_case_acronyms)]
extern crate embedded_hal as ehal;
use ehal::blocking::i2c::{Write, WriteRead};
/// The default I2C address of the MCP23017.
const DEFAULT_ADDRESS: u8 = 0x20;
/// Binary constants.
const HIGH: bool = true;
const LOW: bool = false;
/// Struct for an MCP23017.
/// See the crate-level documentation for general info on the device and the operation of this
/// driver.
#[derive(Clone, Copy, Debug)]
pub struct MCP23017<I2C: Write + WriteRead> {
com: I2C,
/// The I2C slave address of this device.
pub address: u8,
}
/// Defines errors
#[derive(Debug, Copy, Clone)]
pub enum Error<E> {
/// Underlying bus error
BusError(E),
/// Interrupt pin not found
InterruptPinError,
}
impl<E> From<E> for Error<E> {
fn from(error: E) -> Self {
Error::BusError(error)
}
}
impl<I2C, E> MCP23017<I2C>
where
I2C: WriteRead<Error = E> + Write<Error = E>,
{
/// Creates an expander with the default configuration.
pub fn default(i2c: I2C) -> Result<MCP23017<I2C>, Error<E>>
where
I2C: Write<Error = E> + WriteRead<Error = E>,
{
MCP23017::new(i2c, DEFAULT_ADDRESS)
}
/// Creates an expander with specific address.
pub fn new(i2c: I2C, address: u8) -> Result<MCP23017<I2C>, Error<E>>
where
I2C: Write<Error = E> + WriteRead<Error = E>,
{
let chip = MCP23017 { com: i2c, address };
Ok(chip)
}
/// Initiates hardware with basic setup.
pub fn init_hardware(&mut self) -> Result<(), Error<E>> {
// set all inputs to defaults on port A and B
self.write_register(Register::IODIRA, 0xff)?;
self.write_register(Register::IODIRB, 0xff)?;
Ok(())
}
fn read_register(&mut self, reg: Register) -> Result<u8, E> {
let mut data: [u8; 1] = [0];
self.com.write_read(self.address, &[reg as u8], &mut data)?;
Ok(data[0])
}
fn read_double_register(&mut self, reg: Register) -> Result<[u8; 2], E> {
let mut buffer: [u8; 2] = [0; 2];
self.com
.write_read(self.address, &[reg as u8], &mut buffer)?;
Ok(buffer)
}
fn write_register(&mut self, reg: Register, byte: u8) -> Result<(), E> {
self.com.write(self.address, &[reg as u8, byte])
}
fn write_double_register(&mut self, reg: Register, word: u16) -> Result<(), E> {
let msb = (word >> 8) as u8;
self.com.write(self.address, &[reg as u8, word as u8, msb])
}
/// Updates a single bit in the register associated with the given pin.
/// This will read the register (`port_a_reg` for pins 0-7, `port_b_reg` for the other eight),
/// set the bit (as specified by the pin position within the register), and write the register
/// back to the device.
fn update_register_bit(
&mut self,
pin: u8,
pin_value: bool,
port_a_reg: Register,
port_b_reg: Register,
) -> Result<(), E> {
let reg = register_for_pin(pin, port_a_reg, port_b_reg);
let bit = bit_for_pin(pin);
let reg_value = self.read_register(reg)?;
let reg_value_mod = write_bit(reg_value, bit, pin_value);
self.write_register(reg, reg_value_mod)
}
/// Sets the mode for a single pin to either `Mode::INPUT` or `Mode::OUTPUT`.
pub fn pin_mode(&mut self, pin: u8, pin_mode: PinMode) -> Result<(), E> {
self.update_register_bit(
pin,
pin_mode.bit_value(),
Register::IODIRA,
Register::IODIRB,
)
}
/// Updates the entire register associated with the given pin.
fn overwrite_register_bit(
&mut self,
pin: u8,
byte: u8,
port_a_reg: Register,
port_b_reg: Register,
) -> Result<(), E> {
let reg = register_for_pin(pin, port_a_reg, port_b_reg);
self.write_register(reg, byte)
}
/// Write pin mode state for a pin without first reading from it.
///
/// Input pin is a 1 bit, output pin is a 0 bit.
/// Note that only one register (byte) containing the pin is written to.
pub fn overwrite_pin_mode(&mut self, pin: u8, word: u16) -> Result<(), E> {
let byte = if pin < 8 {
(word & 0xff) as u8
} else {
((word & 0xff00) >> 8) as u8
};
self.overwrite_register_bit(
pin,
byte,
Register::IODIRA,
Register::IODIRB,
)
}
/// Sets all pins' modes to either `Mode::INPUT` or `Mode::OUTPUT`.
pub fn all_pin_mode(&mut self, pin_mode: PinMode) -> Result<(), E> {
self.write_register(Register::IODIRA, pin_mode.register_value())?;
self.write_register(Register::IODIRB, pin_mode.register_value())
}
/// Reads all 16 pins (port A and B) into a single 16 bit variable.
pub fn read_gpioab(&mut self) -> Result<u16, E> {
let buffer = self.read_double_register(Register::GPIOA)?;
Ok((buffer[0] as u16) << 8 | (buffer[1] as u16))
}
/// Reads a single port, A or B, and returns its current 8 bit value.
pub fn read_gpio(&mut self, port: Port) -> Result<u8, E> {
let reg = match port {
Port::GPIOA => Register::GPIOA,
Port::GPIOB => Register::GPIOB,
};
self.read_register(reg)
}
/// Writes all the pins with the value at the same time.
pub fn write_gpioab(&mut self, value: u16) -> Result<(), E> {
self.write_double_register(Register::GPIOA, value)
}
/// Writes all the pins of one port with the value at the same time.
pub fn write_gpio(&mut self, port: Port, value: u8) -> Result<(), E> {
let reg = match port {
Port::GPIOA => Register::GPIOA,
Port::GPIOB => Register::GPIOB,
};
self.write_register(reg, value)
}
/// Writes a single bit to a single pin.
/// This function internally reads from the output latch register (`OLATA`/`OLATB`) and writes
/// to the GPIO register.
pub fn digital_write(&mut self, pin: u8, value: bool) -> Result<(), E> {
let bit = bit_for_pin(pin);
// Read the current GPIO output latches.
let ol_register = register_for_pin(pin, Register::OLATA, Register::OLATB);
let gpio = self.read_register(ol_register)?;
// Set the pin.
let gpio_mod = write_bit(gpio, bit, value);
// Write the modified register.
let reg_gp = register_for_pin(pin, Register::GPIOA, Register::GPIOB);
self.write_register(reg_gp, gpio_mod)
}
/// Reads a single pin.
pub fn digital_read(&mut self, pin: u8) -> Result<bool, E> {
let bit = bit_for_pin(pin);
let reg = register_for_pin(pin, Register::GPIOA, Register::GPIOB);
let value = self.read_register(reg)?;
Ok(read_bit(value, bit))
}
/// Enables or disables the internal pull-up resistor for a single pin.
pub fn pull_up(&mut self, pin: u8, value: bool) -> Result<(), E> {
self.update_register_bit(pin, value, Register::GPPUA, Register::GPPUB)
}
/// Inverts the input polarity for a single pin.
/// This uses the `IPOLA` or `IPOLB` registers, see the datasheet for more information.
pub fn invert_input_polarity(&mut self, pin: u8, value: bool) -> Result<(), E> {
self.update_register_bit(pin, value, Register::IPOLA, Register::IPOLB)
}
/// Configures the interrupt system. both port A and B are assigned the same configuration.
/// mirroring will OR both INTA and INTB pins.
/// open_drain will set the INT pin to value or open drain.
/// polarity will set LOW or HIGH on interrupt.
/// Default values after Power On Reset are: (false, false, LOW)
pub fn setup_interrupts(
&mut self,
mirroring: bool,
open_drain: bool,
polarity: Polarity,
) -> Result<(), E> {
// configure port A
self.setup_interrupt_port(Register::IOCONA, mirroring, open_drain, polarity)?;
// configure port B
self.setup_interrupt_port(Register::IOCONB, mirroring, open_drain, polarity)
}
fn setup_interrupt_port(
&mut self,
register: Register,
mirroring: bool,
open_drain: bool,
polarity: Polarity,
) -> Result<(), E> {
let mut io_conf_value = self.read_register(register)?;
io_conf_value = write_bit(io_conf_value, 6, mirroring);
io_conf_value = write_bit(io_conf_value, 2, open_drain);
io_conf_value = write_bit(io_conf_value, 1, polarity.bit_value());
self.write_register(register, io_conf_value)
}
/// Sets up a pin for interrupt.
/// Note that the interrupt condition finishes when you read the information about
/// the port / value that caused the interrupt or you read the port itself.
pub fn setup_interrupt_pin(&mut self, pin: u8, int_mode: InterruptMode) -> Result<(), E> {
// set the pin interrupt control (0 means change, 1 means compare against given value)
self.update_register_bit(
pin,
int_mode != InterruptMode::CHANGE,
Register::INTCONA,
Register::INTCONB,
)?;
// in a RISING interrupt the default value is 0, interrupt is triggered when the pin goes to 1
// in a FALLING interrupt the default value is 1, interrupt is triggered when pin goes to 0
self.update_register_bit(
pin,
int_mode == InterruptMode::FALLING,
Register::DEFVALA,
Register::DEFVALB,
)?;
// enable the pin for interrupt
self.update_register_bit(pin, HIGH, Register::GPINTENA, Register::GPINTENB)
}
/// Get last interrupt pin
pub fn get_last_interrupt_pin(&mut self) -> Result<u8, Error<E>> {
// try port A
let intf_a = self.read_register(Register::INTFA)?;
for x in 0..8 {
if read_bit(intf_a, x) {
return Ok(x);
}
}
// try port B
let intf_b = self.read_register(Register::INTFB)?;
for x in 0..8 {
if read_bit(intf_b, x) {
return Ok(x + 8);
}
}
Err(Error::InterruptPinError)
}
/// Gets last interrupt value
pub fn get_last_interrupt_value(&mut self) -> Result<u8, Error<E>> {
match self.get_last_interrupt_pin() {
Ok(pin) => {
let int_reg = register_for_pin(pin, Register::INTCAPA, Register::INTCAPB);
let bit = bit_for_pin(pin);
let val = self.read_register(int_reg)?;
Ok((val >> bit) & 0x01)
}
Err(e) => Err(e),
}
}
/// Get the complete value captured at the last interrupt of the specified port
pub fn get_captured_value(&mut self, port: Port) -> Result<u8, E> {
let reg = match port {
Port::GPIOA => Register::INTCAPA,
Port::GPIOB => Register::INTCAPB,
};
self.read_register(reg)
}
}
/// Changes the bit at position `bit` within `reg` to `val`.
fn write_bit(reg: u8, bit: u8, val: bool) -> u8 {
let mut res = reg;
if val {
res |= 1 << bit;
} else {
res &= !(1 << bit);
}
res
}
/// Returns whether the bit at position `bit` within `reg` is set.
fn read_bit(reg: u8, bit: u8) -> bool {
reg & (1 << bit) != 0
}
/// Returns the bit index associated with a given pin.
fn bit_for_pin(pin: u8) -> u8 {
pin % 8
}
/// Returns the register address, port dependent, for a given pin.
fn register_for_pin(pin: u8, port_a_addr: Register, port_b_addr: Register) -> Register {
if pin < 8 {
port_a_addr
} else {
port_b_addr
}
}
/// Pin modes.
#[derive(Debug, Copy, Clone)]
pub enum PinMode {
/// Represents input mode.
INPUT = 1,
/// Represents output mode.
OUTPUT = 0,
}
impl PinMode {
/// Returns the binary value of the `PinMode`, as used in IODIR.
fn bit_value(&self) -> bool {
match *self {
PinMode::INPUT => true,
PinMode::OUTPUT => false,
}
}
/// Returns a whole register full of the binary value of the `PinMode`.
fn register_value(&self) -> u8 {
match *self {
PinMode::INPUT => 0xff,
PinMode::OUTPUT => 0x00,
}
}
}
/// Interrupt modes.
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum InterruptMode {
/// Represents change mode.
CHANGE = 0,
/// Represents falling mode.
FALLING = 1,
/// Represents rising mode.
RISING = 2,
}
/// Polarity modes.
#[derive(Debug, Copy, Clone)]
pub enum Polarity {
/// Represents active-low mode.
LOW = 0,
/// Represents active-high mode.
HIGH = 1,
}
impl Polarity {
/// Returns the binary value of the Polarity, as used in IOCON.
fn bit_value(&self) -> bool {
match self {
Polarity::LOW => false,
Polarity::HIGH => true,
}
}
}
/// Generic port definitions.
#[derive(Debug, Copy, Clone)]
pub enum Port {
/// Represent port A.
GPIOA,
/// Represent port B.
GPIOB,
}
#[derive(Debug, Copy, Clone)]
enum Register {
IODIRA = 0x00,
IPOLA = 0x02,
GPINTENA = 0x04,
DEFVALA = 0x06,
INTCONA = 0x08,
IOCONA = 0x0A,
GPPUA = 0x0C,
INTFA = 0x0E,
INTCAPA = 0x10,
GPIOA = 0x12,
OLATA = 0x14,
IODIRB = 0x01,
IPOLB = 0x03,
GPINTENB = 0x05,
DEFVALB = 0x07,
INTCONB = 0x09,
IOCONB = 0x0B,
GPPUB = 0x0D,
INTFB = 0x0F,
INTCAPB = 0x11,
GPIOB = 0x13,
OLATB = 0x15,
}