Re-configuring peripheral as GPIO at runtime

Rust newbie question.

I'm working on a board using the stm32h7xx hal and need to solve a particularly hairy problem.

The board comprises of several components that are controlled via an SPI peripheral. However, there is also another device that needs to be driven by a GPIO connected to the MOSI line of the SPI peripheral.

I've done the basic skeleton of the driver in question. The sections marked TODO are the bits I'm struggling with.

use cortex_m::prelude::_embedded_hal_blocking_spi_Write;
use stm32h7xx_hal::{spi::Enabled, gpio::{Output, Pin, PushPull}, pac::SPI3, spi::Spi};

pub struct ShiftRegSelector {
    up_ciod_sh_reg_sel0: Pin<'D', 10, Output<PushPull>>,
    up_ciod_sh_reg_sel1: Pin<'D', 11, Output<PushPull>>,
    up_ciod_sh_reg_n_latch: Pin<'E', 10, Output<PushPull>>,
    up_ciod_sh_reg_n_rst: Pin<'E', 15, Output<PushPull>>,
    spi: Spi<SPI3, Enabled>,
}

impl ShiftRegSelector {
    pub fn new(
        up_ciod_sh_reg_sel0: Pin<'D', 10, Output<PushPull>>,
        up_ciod_sh_reg_sel1: Pin<'D', 11, Output<PushPull>>,
        up_ciod_sh_reg_n_latch: Pin<'E', 10, Output<PushPull>>,
        up_ciod_sh_reg_n_rst: Pin<'E', 15, Output<PushPull>>,
        spi: Spi<SPI3, Enabled>,
    ) -> Self {
        Self {
            up_ciod_sh_reg_sel0,
            up_ciod_sh_reg_sel1,
            up_ciod_sh_reg_n_latch,
            up_ciod_sh_reg_n_rst,
            spi,
        }
    }

    pub fn psu_13v8_ctrl(&mut self, _enable: bool) {

        // TODO: Reconfigure spi.mosi as GPIO output
        self.reset_mux();

        if _enable {
            // TODO: Drive GPIO line high"
        } else {
            // TODO: Drive GPIO line low
        }
        
        self.latch_mux();
        self.reset_mux();

        // TODO: Restore SPI configuration
    }

    fn latch_mux(&mut self) {
        self.up_ciod_sh_reg_sel0.set_high();
        self.up_ciod_sh_reg_sel1.set_high();
        self.up_ciod_sh_reg_n_latch.set_low();
    }
    
    fn reset_mux(&mut self) {
        self.up_ciod_sh_reg_n_rst.set_high();
        self.up_ciod_sh_reg_n_latch.set_high();
    }
    
    pub fn something(&mut self) {
        self.spi.write(&[0x11u8, 0x22, 0x33]).unwrap();
    }
}

How should the IO/peripheral reconfigration be done in psu_13v8_ctrl?

you need to reconfigure the gpio pin (that is managed by the Spi driver), which is not supported by the current hal API (not without unsafe hacks, that is. see below).

when you construct the Spi driver:

let dp = ...;                   // Device peripherals
let (sck, miso, mosi) = ...;    // GPIO pins

let spi = dp.SPI3.spi((sck, miso, mosi), ...);

the problem is, although you can disable the Spi driver, and then release SPI3 controller related resources (e.g. the register blocks) with the free() method, the resouces for the gpio pins are consumed, and there's no way to get them back.

so unfortunately, only way to get the required pin resources is to steal() them, which is EXTREMELY unsafe, as the safety precondition of steal() is (I assume intentionally,) not clearly specified.

so the "obvious" workaround is just "steal" the gpio pin:

    // TODO: Reconfigure spi.mosi as GPIO output
    // SAFETY:
    // these pins are logicially "owned" by the `Spi`
    // but they are "forgotten" during construction of `Spi`
    // don't touch any anything other than these pins
    let io = unsafe {
        // note: the pin numbers are illustrative only, I don't know the actual pin configurations
        let Peripherals {
            GPIOA, ..
        } = Peripherals::steal();
        let gpioa::Parts {
            pa1, ..
        } = GPIOA.split_without_reset();
        pa1.into_push_pull_output()
    };

and re-configure it as spi alternate function when finished:

     // TODO: Restore SPI configuration
     io.into_alternate::<>();

however, I don't know whether this is sound or not. logically, the hardware resources of the pins are "owned" by the Spi driver.

it'd be nice if the free() API can be extended to also give back the gpio pins. it can be done easily: the Spi type just needs to store the Pins, which are zero sized. but it'll be a breaking change. I think a better alternative is to add a new API but keeping the original, let's call it free_with_pins().

so, with the proposed new API. here's a sketch how you can do it "the right way".

when you need to operate between spi and gpio modes, you can use an enum. for this example though, since the gpio mode is transient, only during the psu_13v8_ctrl() method, you can just wrap the spi into an Option:

pub struct ShiftRegSelector {
    // ...
    spi: Option<Spi<SPI3, Enabled>>,
}
// reconfiguring the spi driver needs access to `CoreClocks`
pub fn psu_13v8_ctrl(&mut self, _enable: bool, clocks: &CoreClocks) {

    // TODO: Reconfigure spi.mosi as GPIO output

    // save the spi related hardware resources
    let (SPI3, rec, (sck, miso, mosi)) = self.spi.take().free_with_pins();
    // re-configure the mosi pin as push-pull output (or whatever you need)
    let mut io = mosi.into_push_pull_output();

    self.reset_mux();

    if _enable {
        // TODO: Drive GPIO line high"
        io.set_high();
    } else {
        // TODO: Drive GPIO line low
        io.set_low();
    }
    
    self.latch_mux();
    self.reset_mux();

    // TODO: Restore SPI configuration

    // re-configure the gpio pin as mosi
    let mosi = io.into_alternate::<...>();
    // use the same config and freq
    self.spi.replace(SPI3::spi((sck, miso, mosi), config, freq, rec, clocks);
}

note, using Option (or a custom enum) is safe, but the downside is you must do the runtime check everytime you use the spi which is inconvenient. there's ways to get rid of the Option, but unsafe (e.g. using raw pointers).

and here's a mock up of the promosed new API implemented in current version. although it also uses steal(), it feels less "wrong" because we only steal the pins after the Spi driver (who "owns" the pins") is destructed and resources are released:

// this a mockup for the proposed new API, but implemented with `steal()`
fn free_with_pins(self) -> (SPI3, rec::Spi3, Pins) {
    let spi = spi.disable();
    let (SPI3, rec) = spi.free();
    let pins = unsafe {
        // the pin numbers are illustrative
        let Peripherals {
            GPIOA, GPIOB, ..
        } = Peripherals::steal();
        let gpioa::Parts {
            pa1, ..
        } = GPIOA.split_without_reset();
        let gpiob::Parts {
            pb2, pb3, ..
        } = GPIOB.split_without_reset();
        (pa1.into_alternate(...), pb2.into_alternate(), pb3.into_alternate())
    };
    (SPI3, rec, pins)
} 

Thanks for the response.

Ok, I think you're suggesting that I modify the hal crate. is there a standard workflow for that? I'm still learning rust so I don' have to confidence to do that yet :slight_smile:.

yeah, the "proper" way to do what you want needs extended/new API of the hal crate, however, you can try the hacky workaround first. I feel it "wrong" in some sense, but would probably work in practice.

I'll repeat it here, the first TODO:

    // TODO: Reconfigure spi.mosi as GPIO output

    // SAFETY:
    // these pins are logicially "owned" by the `Spi`
    // but they are "forgotten" during construction of `Spi`
    // don't touch any anything other than these pins
    let io = unsafe {
        // note: here the pin `PA1` is illustrative only, I don't know the actual pin configurations
        // change this according to your circuit
        let Peripherals {
            GPIOA, ..
        } = Peripherals::steal();
        let gpioa::Parts {
            pa1, ..
        } = GPIOA.split_without_reset();
        pa1.into_push_pull_output()
    };

and the last TODO:

    // TODO: Restore SPI configuration

    // refer to the datasheet for the correct alternate function number
     io.into_alternate::<SPI_MOSI_PIN>();

Thanks again!