Expose GPIO matrix to users
At the moment, the drivers in the HAL take GPIO pins for each peripheral signal, which works for most cases.
However the ESP32 can also:
- Route a GPIO pin to multiple peripheral inputs.
- Route a peripheral output to multiple GPIO pins.
- Route a fixed 1 or 0 to peripheral's input.
- Route a GPIO's input to another GPIO's output. i.e. loopback
A use case for number 3 is #1515. Users should have the option to set a signal to a constant value instead of a GPIO pin.
A use case for number 1 is to be able to route the vsync pin to the I2sCamera driver and also listen for interrupts on the pin with Input.
Also see https://github.com/bjoernQ/esp-hal/pull/1 .
See https://github.com/esp-rs/esp-hal/pull/1684 . Inversion shouldn't be implemented per peripheral but should be a general thing.
We also really should have "route a peripheral output and some number of inputs to the same pin" - currently I'm hacking around esp-hal when doing 1-wire with the rmt peripheral to get the right configuration and it'd be good to have a proper solution.
GPIO matrix will be a great addition, many great hacks around RMT/I2S etc etc can be enabled by the GPIO matrix.
Another peripheral specific example of GPIO matrix features. https://github.com/esp-rs/esp-hal/blob/d0cd890e51c16b61707f7cce16f21fa7478fbdcf/esp-hal/src/pcnt/channel.rs#L69-L98
Another use case is I2C/SPI on multiple pins. This significantly improves board routing and allows much better signal integrity by avoiding long buses.
Another one https://github.com/esp-rs/esp-hal/pull/1769 . Number 3 is needed for this.
Some notes on the topic after reading the TRMs of the ESP32-S2, ESP32-S3 and ESP32-C6. (I won't be apologizing for the wall of text this time :slightly_smiling_face: )
Hardware facts
Every GPIO pad has 4 settings/control signals.
- Input enable: If false, any reads from the pin/pad return false. If true, reads return the pin value.
- Output enable: If false, pin/pad is disconnected. If true, it is connected.
- (Weak) Pull up enable: If true, pin/pad is connected to the pull up.
- (Weak) Pull down enable: If true, pin/pad is connected to the pull down.
Assuming input is enabled, the value/signal from the the pin is sent to the IO_MUX, RTC_IO_MUX and GPIO Matrix. (This means a peripheral can read from a pin via IO_MUX whilst the others read via the GPIO Matrix) Assuming output is enabled, its value/signal must be configured to come from IO_MUX (or RTC_IO_MUX if main cpu if off) or GPIO Matrix.
Each peripheral input signal must be configured to come from either IO_MUX or GPIO Matrix. Each peripheral output signal is sent to both the IO_MUX and GPIO Matrix.
If a signal between a pad and peripheral is to be inverted, filtered or synced, it must go through the GPIO Matrix. If a pin is to be read/written directly it happens via the matrix.
Having laid out the hardware facts, how does this translate into Rust?
Software representation
If we were to take the purist approach we would have a separate object per gpio pin and peripheral input signal.
struct Io {
gpio0: GpioPin<0>,
gpio1: GpioPin<1>,
gpioN: GpioPin<N>,
signal0: PeripheralInput<0>,
signal1: PeripheralInput<1>,
signalN: PeripheralInput<N>,
}
GpioPin let's you configure the output settings and control signals.
PeripheralInput let's you configure the input settings for the input signal.
Each of these objects would be Send without needed any synchronization.
However this is rather cumbersome to deal with and we ideally want each driver to deal with the configuration of the pin and signal where possible, as this feels more natural/practical/convenient.
Unfortunately I wasn't able to come up with a sane/sensible model that allows you to change the pin/signal settings after a peripheral driver has been created. I sadly kept coming back to the purist model above (which I'm not happy with) so I will leave that as a future enhancement and settle for the configure once design below which is still strictly better than the current model.
Proposal (WIP)
With the restriction that we can only setup the pins/signals once at driver creation time, I propose this design (heavily inspired from @bjoernQ 's code/ideas) that solves the following goals.
- Each driver should be able to express that the signals they need can come from GPIO Matrix and/or IO_MUX.
- Each pin/pad should be able to express what functions it can perform.
- Any attempt to use a pin/pad must enable it's corresponding input/output bit.
- Use IO_MUX where possible.
The simple case
Giving full ownership of the pin the a driver. Ideally the driver will configure the pin to be in the perfect mode for operation, i.e. Use IO_MUX if possible.
let io = Io::new(....);
let driver = Driver::new(io.pins.gpio3);
The shared input case
Multiple peripherals want to read (not write) from a gpio pin.
let io = Io::new(....);
// Takes shared (not full) ownership of the pin and enables input mode.
let shared_pin = SharedPin::new(io.pins.gpio3);
let shared_pin2 = shared_pin.clone();
let shared_pin3 = shared_pin.clone();
let driver1 = Driver::new(shared_pin2);
let driver2 = Driver::new(shared_pin3);
let input = Input::new(shared_pin);
if input.is_high() {
// do things
}
The fixed input case
A peripheral wants to read a constant 1 or 0.
let driver = Driver::new(GPIO_MATRIX_ONE);
let driver2 = Driver::new(GPIO_MATRIX_ZERO);
Perhaps the Level enum could be used for this instead. :thinking:
The inverted input case
A peripheral is receiving input via the GPIO Matrix and the signal needs to be inverted.
let io = Io::new(....);
let inverted_pin = InvertedInput::new(io.pins.gpio3);
let driver = Driver::new(inverted_pin);
or even
let io = Io::new(....);
let shared_pin = SharedPin::new(io.pins.gpio3);
let shared_pin2 = shared_pin.clone();
let driver1 = Driver::new(shared_pin2);
let driver2 = Driver::new(InvertedInput::new(shared_pin3));
The interconnect case
User wants a pin to be connected to one output and multiple inputs. Useful for HIL and SPI 3Wire
let io = Io::new(....);
// Takes exclusive (not full) ownership of the pin and enables (undone in `Drop`) input and output mode.
let inter_pin = InterconnectPin::new(io.pins.gpio3);
let (output_pin, input_pin) = inter_pin.split();
let input_pin2 = input_pin.clone();
let driver1 = Driver::new(output_pin);
let driver2 = Driver::new(input_pin);
let driver3 = Driver::new(InvertedInput::new(input_pin2));
The multiple output case
User wants to send a peripheral's output to multiple pins.
TODO
The inverted output case
User wants to invert a peripheral's output to a specific pin.
TODO
The interconnect case but the output goes via IO_MUX
TODO
Required structs and traits
There will need to be a series of struct and traits that achieve the above and ensure that a user can't do the wrong/impossible thing and that there are no surprises.
WIP Pseudo code
// Existing structs and traits
pub enum Level { /* ... */ }
pub struct GpioPin<const GPIONUM: u8>;
pub trait Pin { /* ... */ };
pub trait InputPin: Pin { /* ... */ };
pub trait OutputPin: Pin { /* ... */ };
// New structs and traits
pub trait GpioMatrixInput<'a> {
/// Returns pin number, 0x38 or 0x39
fn source(&self) -> u8;
fn is_inverted(&self) -> bool;
fn can_use_io_mux(&self, signal: InputSignal) -> bool;
}
pub trait GpioMatrixOuput<'a> {
fn configure(&mut self, signal: OutputSignal);
}
impl GpioMatrixInput for Level;
pub struct SharedPin<'d, P: InputPin> {
source_pin: &'d P,
}
impl Clone for SharedPin;
impl GpioMatrixInput for SharedPin;
// impl InputPin for SharedPin;
pub struct InterconnectPin<'d, P: InputPin + OutputPin> {
source_pin: &'d mut P,
}
impl InterconnectPin {
pub fn split(self) -> (SharedPin<'d, P>, InterconnectedOutputPin<'d, P>);
}
pub struct InterconnectedOutputPin<'d, P: InputPin + OutputPin> {
source_pin: &'d mut P,
}
impl GpioMatrixOutput for InterconnectedOutputPin;
// impl OutputPin for InterconnectedOutputPin;
pub struct Inverted<S> {
s: S,
}
impl<S: GpioMatrixInput> GpioMatrixInput for Inverted<S>;
impl<S: GpioMatrixOutput> GpioMatrixOutput for Inverted<S>;