Temporarily (and quickly) disabling SPI for bitbanging on Teensy 4.0

Status
Not open for further replies.

wootie11

Member
Hi all,

I'm working on a project that's interfacing a Teensy 4.0 with a NRF24L01+ and TLC5948 LED Driver. Because I need to bitbang at some point while communicating with the TLC5948, I'm looking for a way to disable/reenable SPI as quickly as possible, so that I can also communicate with the NRF24. I wrote my own library for the TLC5948, where you can see my current attempts at getting this to work.


Background:
Basically, the TLC5948 takes an input buffer of 257 bits (1 'type' bit that specifies whether the data controls brightness or modifies the device configuration + 32 bytes of data), which makes it really ugly for chaining when using SPI (as far as I can tell I can only send bytes, not bits with the SPI library). To get around this, I first tried to disable SPI, bit bang the first bit, then enable SPI and use an SPI.transfer() to do the majority of the work. For an Arduino, this was as simple as:

Code:
inline void bitBangSpi1() {
    noInterrupts();

    SPCR &= ~_BV(SPE); // disable hw SPI (SPI.begin() stops us from writing to MOSI)

    // Bit bang a '1'
    PORTB |= 0b00001000; // set PB3/D11/MOSI high
    PORTB |= 0b00100000; // set PB5/D13/SCLK high
    PORTB &= 0b11011111; // set PB5/D13/SCLK low

    SPCR |= _BV(SPE); // restore hw SPI

    interrupts();
}

Unfortunately, doing something similar with the Teensy 4 did not work:
Code:
inline void bitBangSpi1() {

    LPSPI4.CR &= ~LPSPI_CR_MEN;

    digitalWriteFast(SIN,HIGH);
    digitalWriteFast(SCLK,HIGH);
    digitalWriteFast(SCLK,LOW);
    digitalWriteFast(SIN,LOW);

    LPSPI4.CR |= ~LPSPI_CR_MEN;

}

I ended up just writing:
Code:
inline void bitBangSpi1() {

    SPI.end();
    pinMode(SIN,OUTPUT);
    pinMode(SCLK,OUTPUT);

    digitalWriteFast(SIN,HIGH);
    digitalWriteFast(SCLK,HIGH);
    digitalWriteFast(SCLK,LOW);
    digitalWriteFast(SIN,LOW);

    SPI.begin(); // re-enable SPI

}
Circuit Design
pcb-design.jpg

Which was terrible in performance compared to my other attempt (a write to two TLC5948s took 500us+ vs 75us for a shiftOut version) and at 600Mhz the TLC5948s were a bit glitchy (sometimes not turning on, sometimes turning on the wrong LEDs, etc.). Researching the glitchiness problem, I was inspired by another post on this forum (sadly I couldn't find it again) where someone noted that shiftOut could potentially be better than SPI due to the raw speed of the fastio pin-mode used by SPI (if I understood correctly) and the signal integrity effects the fast rise time has. I tried it and ended up using a shiftOut-based method that seems a lot more solid in terms of reliability (@600Mhz it's no longer glitching) and was faster (75us vs 500+ from the SPI disabling version). The problem now is that I can't talk with the NRF24 if I'm using the LED drivers since SPI disables bit-banging on those pins and I'd like to avoid repeatedly calling SPI.begin() and SPI.end(), which seems to do a lot more than I need it to (just disable the SPI module so I can write to the pins). Is this even possible? I took a look at the SPI implementation for the Teensy 4 and to be honest I'm not sure what's essential or not. It looks like a lot of mux/pin configuration is required, but something like restoring the SPISettings() (on line 1314 of SPI.cpp) might be superfluous, right? My thought is the entire SPI configuration might not have to be restored if I'm just doing some bit-banging and then starting to use SPI again, but perhaps I'm wrong. I guess the other way to do it would be to re-implement the NRF24 code as a bit-bang or re-implement the TLC5948 library to avoid bit-banging at all, but I was hoping to avoid that. I'd love to hear some wiser peoples' thoughts on this, as I'm relatively new to Teensy and don't want to re-invent the wheel.

Thanks for your time,
Will :)
 
Otherwise, this reminds me that the I2C implementation does a similar sort of bit banging to rescue a stuck bus. Maybe this can give you some inspiration: https://github.com/PaulStoffregen/W...0176d8a5fc610fc564427282ca0/WireIMXRT.cpp#L87

@damiend I never thought to look at any I2C implementations - thank you very much for the tip! I'll see if I can't copy that portConfigRegister magic and see if I can quickly disable/re-enable SPI :):)

I also figured out that the RF24 library has a software SPI setting... however configuring it to use the hardware SPI pins and running it with my code is still not working quite right :(. I'm testing with a simple 16 led blink and it's able to write something it seems - however it looks like the whole buffer isn't getting sent (only the first 4 of 16 leds light up, however disabling the radio fixes the problem completely).
 
So, a quick update on things. I was not able to find a quick way to enable/disable SPI. I was able to actually use pure SPI.transfer() calls by computing the number of leading bits required for a given number of TLC5948 chips and shifting the data before writing it. My code now looks something like:
Code:
void Tlc5948::writeGsBufferSPI16(uint16_t* buf, uint16_t numVals, uint8_t numTlcs) { // buffer with correct endianness for 16bit values
    SPI.beginTransaction(SPISettings(SPI_SPEED,TLC5948_BIT_ORDER,TLC5948_SPI_MODE));
    if (numTlcs == 0 && numVals % 32 != 0) // don't accept weird input
        return;
    if (numTlcs == 0) numTlcs = numVals / 32; // have to calculate number of Tlcs
    uint16_t valToWrite = 0x0;
    uint16_t currVal = 0;
    for (uint8_t i = numTlcs; i > 1; i--) {
        uint8_t shiftOffset = i % 8; // how much to shift by to add next bits
        for (uint8_t j = 0; j < 16; j++) {
            valToWrite |= buf[currVal] >> (17 - shiftOffset);
            SPI.transfer16(valToWrite);
            valToWrite = buf[currVal] << (shiftOffset - 1);
            currVal = (currVal + 1) % numVals;
        }
    }
    SPI.transfer16(valToWrite); // write out last byte
    for (uint8_t j = 0; j < 16; j++) { // last 16 values will be aligned
        SPI.transfer16(buf[currVal]);
        currVal = (currVal + 1) % numVals;
    }
    SPI.endTransaction();
    pulseLatch();
}
Testing this code I get 52us per write (with a SPI clock of 20Mhz, 33Mhz gives 38us but with glitchy results), so overall a win on that front :)

Now I'm having other issues :confused: The NRF24 is able to be read/written to properly even after several writes to the TLC5948s, however the writeGsBufferSPI16 function above produces really weird results. It successfully writes color data out, but it "lags" about halfway through so that each half of the LEDs is being lit correctly while the other half is off before filling the TLCs up with the correct data. Running the code without the RF24 resolves the weird display issue completely. Now my question is - is this because of SPI and DMA? It seems like there's still some sort of conflict between the TLC5948s and the NRF24.... Has anyone come across an issue like this before?
 
Each pin has a mux which allows 1 of 8 peripherals to control the pin. You could leave SPI enabled and just write to the mux register to give the pin control back to GPIO. Then write to it again to give the pin to SPI.
 
Are the TLC5948s and the NRF24 on separate SPI busses already? If not, and while this would be a fascinating problem to debug, you could potentially make your life a lot easier by patching your board so that the NRF24 connects to SPI1 or SPI2. This will require soldering wires to the accessory pads underside the Teensy4.0, though.

If they're on the same bus -- If I read the datasheet right, it looks like the TLC5948 doesn't have a CS pin that disables the interface, just a LATCH pin. The input shift register is always active and will fill up with any data you send to the NRF24. You'd have to make sure it gets flushed out somehow.

Does the NRF24 trigger interrupts on the Teensy? That could be another cause. One solution would be to disable these interrupts while writing to the LED drivers, or use the atomic blocks from "util/atomic.h".
 
Last edited:
Each pin has a mux which allows 1 of 8 peripherals to control the pin. You could leave SPI enabled and just write to the mux register to give the pin control back to GPIO. Then write to it again to give the pin to SPI.

@PaulStoffregen, thank you - that's exactly what I was hoping for, but wasn't sure if it was possible! I took a closer look at the SPI implementation and couldn't find out exactly what registers were being written to, but I think I managed to find the corresponding mux register names and bits from other posts and got something like:

Code:
// add SPI enable/disable
inline void disableSPI() {
    // SIN/MOSI -> pin 11 -> GPIO7_02 -> (from Teensy 4.0 Hypothetical assignment) -> B0_02
    // SOUT/MISO -> pin 12  -> GPIO7_01 -> ... -> B0_01
    // SCLK/SCKs -> pin 13 -> GPIO7_03 -> ... -> B0_03

    // MOSI control, pin 11
    IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_02 &= 0xFFF0; // zero out last bits
    IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_02 |= 0x0005; // ALT5 - GPIO2_IOO2 - GPIO2 page 510

    // MISO control, pin 12
    IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_01 &= 0xFFF0; // zero out last bits
    IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_01 |= 0x0005; // ALT5 - GPIO2_IOO2 - GPIO2 page 509

    // SCLK control, pin 13
    IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_03 &= 0xFFF0; // zero out last bits
    IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_03 |= 0x0005; // ALT5 - GPIO2_IOO2 - GPIO2 page 511
}

inline void enableSPI() {
    // SIN/MOSI -> pin 11 -> GPIO7_02 -> (from Teensy 4.0 Hypothetical assignment) -> B0_02
    // SOUT/MISO -> pin 12  -> GPIO7_01 -> ... -> B0_01
    // SCLK/SCKs -> pin 13 -> GPIO7_03 -> ... -> B0_03

    // MOSI control, pin 11
    IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_02 &= 0xFFF0; // zero out last bits
    IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_02 |= 0x0003; // ALT3 - GPIO2_IOO2 - GPIO2 page 510

    // MISO control, pin 12
    IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_01 &= 0xFFF0; // zero out last bits
    IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_01 |= 0x0003; // ALT3 - GPIO2_IOO2 - GPIO2 page 509

    // SCLK control, pin 13
    IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_03 &= 0xFFF0; // zero out last bits
    IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_03 |= 0x0003; // ALT3 - GPIO2_IOO2 - GPIO2 page 511
}

It seems to be working right now, at least for a simple test case where I alternate between SPI and non-SPI writes to the drivers :) thanks for the help!




Are the TLC5948s and the NRF24 on separate SPI busses already? If not, and while this would be a fascinating problem to debug, you could potentially make your life a lot easier by patching your board so that the NRF24 connects to SPI1 or SPI2. This will require soldering wires to the accessory pads underside the Teensy4.0, though.

If they're on the same bus -- If I read the datasheet right, it looks like the TLC5948 doesn't have a CS pin that disables the interface, just a LATCH pin. The input shift register is always active and will fill up with any data you send to the NRF24. You'd have to make sure it gets flushed out somehow.

Does the NRF24 trigger interrupts on the Teensy? That could be another cause. One solution would be to disable these interrupts while writing to the LED drivers, or use the atomic blocks from "util/atomic.h".

@damiend First, thanks for all the thoughtful responses - I appreciate you taking so much time to help me figure this out. I actually tried to use separate SPI ports but I couldn't quite figure out how to connect the bottom pins on the PCB I made. I will definitely keep that idea in mind for the future, though! It turns out the problem was only partly SPI (really the problem was/is me :)). Using @PaulStoffregens suggestion fixed the shiftOut function I was using, but it was actually working fine with the modified SPI16 function. In case you're interested in the problem: you might have seen the TLC5948 also has a GSCLK signal that controls the internal PWM counter - for my project I occasionally change the frequency of GSCLK (each TLC has its own) and I guess the frequencies were too slow (I forgot to multiply somewhere), making it seem like it was a slowly updating when it was really just the chips pwm'ing slowly.. oof. Thanks again for your help!!
 
Status
Not open for further replies.
Back
Top