Configuring alternate SPI pins on a custom Teensy 4

Rezo

Well-known member
I'm building a project using DevBoard v5 (based on a Teensy Micromod schematic, with SDRAM) , and I need to access SPI instance, but the standard pins 9,10,11, 12 (B0_00, B0_01, B0_02, B0_03) are being used by the eLCDIF instance.
There is a set of alternate pins I can use (B1_03, B1_05, B1_06, B1_07), but they are not defined Teensy pins on any 4.x variant.

Looking at the hardware object for the MM/T4.0 is seems to define the pin, port and some other things:

Code:
const SPIClass::SPI_Hardware_t  SPIClass::spiclass_lpspi4_hardware = {
    CCM_CCGR1, CCM_CCGR1_LPSPI4(CCM_CCGR_ON),
    DMAMUX_SOURCE_LPSPI4_TX, DMAMUX_SOURCE_LPSPI4_RX, _spi_dma_rxISR0,
    12,
    3 | 0x10,
    0,
    IOMUXC_LPSPI4_SDI_SELECT_INPUT,
    11,
    3 | 0x10,
    0,
    IOMUXC_LPSPI4_SDO_SELECT_INPUT,
    13,
    3 | 0x10,
    0,
    IOMUXC_LPSPI4_SCK_SELECT_INPUT,
    10,
    3 | 0x10,
    1,
    0,
    &IOMUXC_LPSPI4_PCS0_SELECT_INPUT
};

Which is used in the SPI begin() function


Code:
void SPIClass::begin()
{

    // CBCMR[LPSPI_CLK_SEL] - PLL2 = 528 MHz
    // CBCMR[LPSPI_PODF] - div4 = 132 MHz


    hardware().clock_gate_register &= ~hardware().clock_gate_mask;

    CCM_CBCMR = (CCM_CBCMR & ~(CCM_CBCMR_LPSPI_PODF_MASK | CCM_CBCMR_LPSPI_CLK_SEL_MASK)) |
        CCM_CBCMR_LPSPI_PODF(2) | CCM_CBCMR_LPSPI_CLK_SEL(1); // pg 714
//        CCM_CBCMR_LPSPI_PODF(6) | CCM_CBCMR_LPSPI_CLK_SEL(2); // pg 714

    uint32_t fastio = IOMUXC_PAD_DSE(7) | IOMUXC_PAD_SPEED(2);
    //uint32_t fastio = IOMUXC_PAD_DSE(6) | IOMUXC_PAD_SPEED(1);
    //uint32_t fastio = IOMUXC_PAD_DSE(3) | IOMUXC_PAD_SPEED(3);
    //Serial.printf("SPI MISO: %d MOSI: %d, SCK: %d\n", hardware().miso_pin[miso_pin_index], hardware().mosi_pin[mosi_pin_index], hardware().sck_pin[sck_pin_index]);
    *(portControlRegister(hardware().miso_pin[miso_pin_index])) = fastio;
    *(portControlRegister(hardware().mosi_pin[mosi_pin_index])) = fastio;
    *(portControlRegister(hardware().sck_pin[sck_pin_index])) = fastio;

    //printf("CBCMR = %08lX\n", CCM_CBCMR);
    hardware().clock_gate_register |= hardware().clock_gate_mask;
    *(portConfigRegister(hardware().miso_pin[miso_pin_index])) = hardware().miso_mux[miso_pin_index];
    *(portConfigRegister(hardware().mosi_pin [mosi_pin_index])) = hardware().mosi_mux[mosi_pin_index];
    *(portConfigRegister(hardware().sck_pin [sck_pin_index])) = hardware().sck_mux[sck_pin_index];

    // Set the Mux pins
    //Serial.println("SPI: Set Input select registers");
    hardware().sck_select_input_register = hardware().sck_select_val[sck_pin_index];
    hardware().miso_select_input_register = hardware().miso_select_val[miso_pin_index];
    hardware().mosi_select_input_register = hardware().mosi_select_val[mosi_pin_index];

    //digitalWriteFast(10, HIGH);
    //pinMode(10, OUTPUT);
    //digitalWriteFast(10, HIGH);
    port().CR = LPSPI_CR_RST;

    // Lets initialize the Transmit FIFO watermark to FIFO size - 1...
    // BUGBUG:: I assume queue of 16 for now...
    port().FCR = LPSPI_FCR_TXWATER(15);

    // We should initialize the SPI to be in a known default state.
    beginTransaction(SPISettings());
    endTransaction();
}


I don't mind modifying the SPI library to support this, but I am having a hard time understanding what changes to make to the hardware object to allow me to use these non teensy alternate pins.
Any help would be appriciated!
 
The SPI.h has definition of the hardware section:
C++:
class SPIClass { // Teensy 4
public:
    #if defined(ARDUINO_TEENSY41)
    // T4.1 has SPI2 pins on memory connectors as well as SDCard
    static const uint8_t CNT_MISO_PINS = 2;
    static const uint8_t CNT_MOSI_PINS = 2;
    static const uint8_t CNT_SCK_PINS = 2;
    static const uint8_t CNT_CS_PINS = 3;
    #else
    static const uint8_t CNT_MISO_PINS = 1;
    static const uint8_t CNT_MOSI_PINS = 1;
    static const uint8_t CNT_SCK_PINS = 1;
    static const uint8_t CNT_CS_PINS = 1;
#endif
    typedef struct {
        volatile uint32_t &clock_gate_register;
        const uint32_t clock_gate_mask;
        uint8_t  tx_dma_channel;
        uint8_t  rx_dma_channel;
        void     (*dma_rxisr)();
        // MISO pins
        const uint8_t          miso_pin[CNT_MISO_PINS];
        const uint32_t      miso_mux[CNT_MISO_PINS];
        const uint8_t         miso_select_val[CNT_MISO_PINS];
        volatile uint32_t     &miso_select_input_register;

        // MOSI pins
        const uint8_t          mosi_pin[CNT_MOSI_PINS];
        const uint32_t      mosi_mux[CNT_MOSI_PINS];
        const uint8_t         mosi_select_val[CNT_MOSI_PINS];
        volatile uint32_t     &mosi_select_input_register;

        // SCK pins
        const uint8_t          sck_pin[CNT_SCK_PINS];
        const uint32_t      sck_mux[CNT_SCK_PINS];
        const uint8_t         sck_select_val[CNT_SCK_PINS];
        volatile uint32_t     &sck_select_input_register;

        // CS Pins
        const uint8_t          cs_pin[CNT_CS_PINS];
        const uint32_t      cs_mux[CNT_CS_PINS];
        const uint8_t          cs_mask[CNT_CS_PINS];
        const uint8_t         pcs_select_val[CNT_CS_PINS];
        volatile uint32_t     *pcs_select_input_register[CNT_CS_PINS];

    } SPI_Hardware_t;

So MMOD which this is derived from has limits of 1 pin defined for each of the MISO/MOSI... might up that to two...
Might also put in #else defined( ARDUINO_TEENSY_DEVBRD5_
1738676563878.png

Assuming you are using the variant... those B1 pins have Arduino pins assigned to them.
So you could muck up a copy of the table... like the T4.1 version:
C++:
#if defined(ARDUINO_TEENSY41)
const SPIClass::SPI_Hardware_t  SPIClass::spiclass_lpspi4_hardware = {
    CCM_CCGR1, CCM_CCGR1_LPSPI4(CCM_CCGR_ON),
    DMAMUX_SOURCE_LPSPI4_TX, DMAMUX_SOURCE_LPSPI4_RX, _spi_dma_rxISR0,
    12, 53,  // MISO
    3 | 0x10, 1 | 0x10,
    0, 1,
    IOMUXC_LPSPI4_SDI_SELECT_INPUT,
    11, 54, // MOSI
    3 | 0x10, 1 | 0x10,
    0, 1,
    IOMUXC_LPSPI4_SDO_SELECT_INPUT,
    13, 55, // SCK
    3 | 0x10, 1 | 0x10,
    0, 1,
    IOMUXC_LPSPI4_SCK_SELECT_INPUT,
    10, 37, 36, // CS
    3 | 0x10, 2 | 0x10, 2 | 0x10,
    1, 2, 3,
    0, 0, 0,
    &IOMUXC_LPSPI4_PCS0_SELECT_INPUT, 0, 0
};
Note: If you change the number of max entities for the things like MISOs, you need to update it for all SPI objects. SPI1, SPI2...

I only showed the updates for MISO, MOSI, SCK... You could also do the CS pins 50-52, if you use them for hardware CS...
I updated this with combination of my Excel document and the IMXRT RM. For example:
MISO
1738677617394.png

Shows the B1_05 uses ALT1 setting and it's input select value is 1.
To make it more variant friendly, if it looked like variant stuff was mainlined. Would be that each variant would define what
it's default MISO, MOSI, SCK, are as arduino pin numbers. And then These tables would show all of the possible IMXRT pins
that could be used. If you called setMISO(53), the code would instead of looking for 53 in the table, it would map the pin to
lets say its IMXRT PADS address, and then the table would contain the pad addresses of the valid options...

Tables would not grow much as for example in the case mentioned there are only 2 valid pins for MISO, MOSI, SCK. CS
there are maybe 4...

Hope that helps
 
@PaulStoffregen that's not a bad idea, assuming async transfers are not needed
@KurtE I assume your suggestions would only work with the variant code? No way to do this without it?
 
@KurtE I assume your suggestions would only work with the variant code? No way to do this without it?
Of course you can do it without variant code... After all, it's just code...
Note: Jump to bottom for quick answer

Life can be made easier, by using the pin name code, that I have in the variant stuff...
It starts off with:
Code:
#pragma once

// PinName values: 32 bit top 16 - offset in EMC table, low 8 bits 0-4 GPIO PIN 5-7 On Port #
typedef enum {
    AD_B0_00 = 0x0BC0000, AD_B0_01 = 0x0C00001, AD_B0_02 = 0x0C40002, AD_B0_03 = 0x0C80003, AD_B0_04 = 0x0CC0004, AD_B0_05 = 0x0D00005, AD_B0_06 = 0x0D40006, AD_B0_07 = 0x0D80007,
    AD_B0_08 = 0x0DC0008, AD_B0_09 = 0x0E00009, AD_B0_10 = 0x0E4000A, AD_B0_11 = 0x0E8000B, AD_B0_12 = 0x0EC000C, AD_B0_13 = 0x0F0000D, AD_B0_14 = 0x0F4000E, AD_B0_15 = 0x0F8000F,
    AD_B1_00 = 0x0FC0010, AD_B1_01 = 0x1000011, AD_B1_02 = 0x1040012, AD_B1_03 = 0x1080013, AD_B1_04 = 0x10C0014, AD_B1_05 = 0x1100015, AD_B1_06 = 0x1140016, AD_B1_07 = 0x1180017,
    AD_B1_08 = 0x11C0018, AD_B1_09 = 0x1200019, AD_B1_10 = 0x124001A, AD_B1_11 = 0x128001B, AD_B1_12 = 0x12C001C, AD_B1_13 = 0x130001D, AD_B1_14 = 0x134001D, AD_B1_15 = 0x138001F,
    B0_00 = 0x13C0020, B0_01 = 0x1400021, B0_02 = 0x1440022, B0_03 = 0x1480023, B0_04 = 0x14C0024, B0_05 = 0x1500025, B0_06 = 0x1540026, B0_07 = 0x1580027,
    B0_08 = 0x15C0028, B0_09 = 0x1600029, B0_10 = 0x164002A, B0_11 = 0x168002B, B0_12 = 0x16C002C, B0_13 = 0x170002D, B0_14 = 0x174002E, B0_15 = 0x178002F,
    B1_00 = 0x17C0030, B1_01 = 0x1800031, B1_02 = 0x1840032, B1_03 = 0x1880033, B1_04 = 0x18C0034, B1_05 = 0x1900035, B1_06 = 0x1940036, B1_07 = 0x1980037,
    B1_08 = 0x19C0038, B1_09 = 0x1A00039, B1_10 = 0x1A4003A, B1_11 = 0x1A8003B, B1_12 = 0x1AC003C, B1_13 = 0x1B0003D, B1_14 = 0x1B4003D, B1_15 = 0x1B8003F,
    EMC_00 = 0x0140060, EMC_01 = 0x0180061, EMC_02 = 0x01C0062, EMC_03 = 0x0200063, EMC_04 = 0x0240064, EMC_05 = 0x0280065, EMC_06 = 0x02C0066, EMC_07 = 0x0300067,
    EMC_08 = 0x0340068, EMC_09 = 0x0380069, EMC_10 = 0x03C006A, EMC_11 = 0x040006B, EMC_12 = 0x044006C, EMC_13 = 0x048006D, EMC_14 = 0x04C006E, EMC_15 = 0x050006F,
    EMC_16 = 0x0540070, EMC_17 = 0x0580071, EMC_18 = 0x05C0072, EMC_19 = 0x0600073, EMC_20 = 0x0640074, EMC_21 = 0x0680075, EMC_22 = 0x06C0076, EMC_23 = 0x0700077,
    EMC_24 = 0x0740078, EMC_25 = 0x0780079, EMC_26 = 0x07C007A, EMC_27 = 0x080007B, EMC_28 = 0x084007C, EMC_29 = 0x088007D, EMC_30 = 0x08C007D, EMC_31 = 0x090007F,
    EMC_32 = 0x0940052, EMC_33 = 0x0980053, EMC_34 = 0x09C0054, EMC_35 = 0x0A00055, EMC_36 = 0x0A40056, EMC_37 = 0x0A80057, EMC_38 = 0x0AC0058, EMC_39 = 0x0B00059,
    EMC_40 = 0x0B4005A, EMC_41 = 0x0B8005B, SD_B0_00 = 0x1BC004C, SD_B0_01 = 0x1C0004D, SD_B0_02 = 0x1C4004E, SD_B0_03 = 0x1C8004F, SD_B0_04 = 0x1CC0050, SD_B0_05 = 0x1D00051,
    SD_B1_00 = 0x1D40040, SD_B1_01 = 0x1D80041, SD_B1_02 = 0x1DC0042, SD_B1_03 = 0x1E00043, SD_B1_04 = 0x1E40044, SD_B1_05 = 0x1E80045, SD_B1_06 = 0x1EC0046, SD_B1_07 = 0x1F00047,
    SD_B1_08 = 0x1F40048, SD_B1_09 = 0x1F80049, SD_B1_10 = 0x1FC004A, SD_B1_11 = 0x200004B
} IMXRT_PIN_t;

//#define digitalPinToPort(pin)    (pin)
inline uint32_t digitalPinNameToBitMask(IMXRT_PIN_t pin_name)                 {return  1 << (pin_name & 0x1f);}
inline uint32_t digitalPinNameToBit(IMXRT_PIN_t pin_name)                     {return  (pin_name & 0x1f); }
//inline volatile uint32_t * pinNamePortOutputRegister(IMXRT_PIN_t pin_name) { return }
inline volatile uint32_t * pinNamePortSetRegister(IMXRT_PIN_t pin_name)     { return (volatile uint32_t*)((uint8_t *)&GPIO6_DR_SET + 0x4000 * ((pin_name >> 5) & 0x7));}
inline volatile uint32_t * pinNamePortClearRegister(IMXRT_PIN_t pin_name)     { return (volatile uint32_t*)((uint8_t *)&GPIO6_DR_CLEAR + 0x4000 * ((pin_name >> 5) & 0x7));}
inline volatile uint32_t * pinNamePortToggleRegister(IMXRT_PIN_t pin_name)     { return (volatile uint32_t*)((uint8_t *)&GPIO6_DR_TOGGLE + 0x4000 * ((pin_name >> 5) & 0x7));}
inline volatile uint32_t * pinNamePortInputRegister(IMXRT_PIN_t pin_name) { return (volatile uint32_t*)((uint8_t *)&GPIO6_PSR + 0x4000 * ((pin_name >> 5) & 0x7));}
inline volatile uint32_t * pinNamePortModeRegister(IMXRT_PIN_t pin_name) { return (volatile uint32_t *)(IMXRT_IOMUXC_ADDRESS + (pin_name >> 16));}
inline volatile uint32_t * pinNamePortConfigRegister(IMXRT_PIN_t pin_name) { return (volatile uint32_t *)(IMXRT_IOMUXC_ADDRESS + (pin_name >> 16) + 0x1f0);}
//inline volatile uint32_t * pinNamePortControlRegister(IMXRT_PIN_t pin_name) { return }
//#define digitalPinToPortReg(pin) (portOutputRegister(pin))

inline void digitalWrite(IMXRT_PIN_t pin_name, uint8_t val) {
    uint32_t mask = 1 << (pin_name & 0x1f);
    if (val) {
        volatile uint32_t *reg = (volatile uint32_t*)((uint8_t *)&GPIO6_DR_SET + 0x4000 * ((pin_name >> 5) & 0x7));
        *reg = mask;
    } else {
        volatile uint32_t *reg = (volatile uint32_t*)((uint8_t *)&GPIO6_DR_CLEAR + 0x4000 * ((pin_name >> 5) & 0x7));
        *reg = mask;
    }
}
inline void digitalToggle(IMXRT_PIN_t pin_name) {
    uint32_t mask = 1 << (pin_name & 0x1f);
    volatile uint32_t *reg = (volatile uint32_t*)((uint8_t *)&GPIO6_DR_TOGGLE + 0x4000 * ((pin_name >> 5) & 0x7));
    //Serial.printf("Toggle %x Reg:%x mask:%x\n", pin_name, reg, mask);
    *reg = mask;
}
With it you can do things like:
pinMode(B1_07, OUTPUT);
digitalWrite(B1_07, HIGH);

For example if you look at the begin code, you will see mostly:
Code:
    uint32_t fastio = IOMUXC_PAD_DSE(7) | IOMUXC_PAD_SPEED(2);

    *(portControlRegister(hardware().miso_pin[miso_pin_index])) = fastio;
    *(portControlRegister(hardware().mosi_pin[mosi_pin_index])) = fastio;
    *(portControlRegister(hardware().sck_pin[sck_pin_index])) = fastio;

    //printf("CBCMR = %08lX\n", CCM_CBCMR);
    hardware().clock_gate_register |= hardware().clock_gate_mask;
    *(portConfigRegister(hardware().miso_pin[miso_pin_index])) = hardware().miso_mux[miso_pin_index];
    *(portConfigRegister(hardware().mosi_pin [mosi_pin_index])) = hardware().mosi_mux[mosi_pin_index];
    *(portConfigRegister(hardware().sck_pin [sck_pin_index])) = hardware().sck_mux[sck_pin_index];

    // Set the Mux pins 
    //Serial.println("SPI: Set Input select registers");
    hardware().sck_select_input_register = hardware().sck_select_val[sck_pin_index];
    hardware().miso_select_input_register = hardware().miso_select_val[miso_pin_index];
    hardware().mosi_select_input_register = hardware().mosi_select_val[mosi_pin_index];
So you could simply call the begin and them replicate it or create a version that takes pin names...
And there might be a few holes in some of my wrapper there but if you look at the pinMode, you will see:
Code:
void pinMode(IMXRT_PIN_t pin_name, uint8_t mode)
{
    // generate PAD and MUX register from name...
    volatile uint32_t *mux = (volatile uint32_t *)(IMXRT_IOMUXC_ADDRESS + (pin_name >> 16));
    volatile uint32_t *pad = (volatile uint32_t *)(IMXRT_IOMUXC_ADDRESS + (pin_name >> 16) + 0x1f0);
    volatile uint32_t *gdir = (volatile uint32_t*)((uint8_t *)&GPIO6_GDIR + 0x4000 * ((pin_name >> 5) & 0x7));
    uint32_t mask = 1 << (pin_name & 0x1f);
...
So you can set the pad for each of the pins to the fastio from above.
You can set the mux = to the values I mentioned in previous post (1 | 0x10)

You need to set the Input mux registers to 1...
Code:
IOMUXC_LPSPI4_SDI_SELECT_INPUT = 1;
IOMUXC_LPSPI4_SDO_SELECT_INPUT = 1;
IOMUXC_LPSPI4_SCK_SELECT_INPUT = 1;

The above parts you can probably boil it down farther.
Code:
IOMUXC_SW_PAD_CTL_PAD_GPIO_B1_05 = fastio;
IOMUXC_SW_PAD_CTL_PAD_GPIO_B1_06 = fastio;
IOMUXC_SW_PAD_CTL_PAD_GPIO_B1_07 = fastio;
IOMUXC_SW_MUX_CTL_PAD_GPIO_B1_05 = 1 | 0x10; 
IOMUXC_SW_MUX_CTL_PAD_GPIO_B1_06 = 1 | 0x10;
IOMUXC_SW_MUX_CTL_PAD_GPIO_B1_07 = 1 | 0x10;
IOMUXC_LPSPI4_SDI_SELECT_INPUT = 1;
IOMUXC_LPSPI4_SDO_SELECT_INPUT = 1;
IOMUXC_LPSPI4_SCK_SELECT_INPUT = 1;
 
Back
Top