This thread has been locked.

If you have a related question, please click the "Ask a related question" button in the top right corner. The newly created question will be automatically linked to this question.

MSP432P401R: Catching the end of SPI byte transmission

Part Number: MSP432P401R

Hello!

I am having a bit of a problem setting up an SPI transmission with an MSP432 microcontroller.
I am quite new to this controller, so maybe this question is obvious.

Basically I am trying to drive an EA DOGL-128 graphical LCD display with SPI interface.

This is how the spi interface is initialised:

#include <driverlib.h>
#include <spi_drv.h>

/* SPI Master Configuration Parameter */
const eUSCI_SPI_MasterConfig spiMasterConfig =
{
        EUSCI_B_SPI_CLOCKSOURCE_SMCLK,             // SMCLK Clock Source
        12000000,                                   // SMCLK = DCO = 12MHZ
        10000,                                    // SPICLK = 10khz
        EUSCI_B_SPI_MSB_FIRST,                     // MSB First
        EUSCI_B_SPI_PHASE_DATA_CHANGED_ONFIRST_CAPTURED_ON_NEXT,    // Phase
        EUSCI_B_SPI_CLOCKPOLARITY_INACTIVITY_HIGH, // High polarity
        EUSCI_B_SPI_3PIN                           // 3Wire SPI Mode
};


Public void spi_init(void)
{
    /* Selecting P1.5 and P1.6 in SPI mode */
    GPIO_setAsPeripheralModuleFunctionInputPin(GPIO_PORT_P1,
            GPIO_PIN5 | GPIO_PIN6 , GPIO_PRIMARY_MODULE_FUNCTION);

    // Set pin 4.3 as CS pin.
    GPIO_setAsOutputPin(GPIO_PORT_P4, GPIO_PIN3);

    /* We should give a falling edge to CS pin, not entirely sure if this is necessary. */
    /* Possibly we might have to catch the transmit interrupt and then control CS??? Not yet sure how this works. */
    GPIO_setOutputHighOnPin(GPIO_PORT_P4, GPIO_PIN3);

    /* Configuring SPI in 3wire master mode */
    SPI_initMaster(EUSCI_B0_BASE, &spiMasterConfig);

    /* Enable SPI module */
    SPI_enableModule(EUSCI_B0_BASE);
}

And this is what the transmit function looks like:

Public void spi_transmit_byte(U8 byte)
{
    /* Polling to see if the TX buffer is ready */
    while (!(SPI_getInterruptStatus(EUSCI_B0_BASE,EUSCI_B_SPI_TRANSMIT_INTERRUPT)));
    /* Transmitting data to slave */
    SPI_transmitData(EUSCI_B0_BASE, byte);
}

Now the SPI transmission itself works OK, checked with a logic analyser and it seems there is no problem with it.
The problem is that the LCD has an A0 pin that basically determines if the command I am sending should be interpreted 
as writing data or as a command. The A0 pin needs to be set low/high at the end of the SPI byte transmission.

I tried controlling it like this at first:

Private void disp_command(U8 cmd, Boolean reg_select)
{
    if (reg_select)
    {
        GPIO_setOutputHighOnPin(GPIO_PORT_P1, GPIO_PIN7);
    }
    else
    {
        GPIO_setOutputLowOnPin(GPIO_PORT_P1, GPIO_PIN7);
    }
    spi_transmit_byte(cmd);
}

But this did not work, because the A0 pin got set before the previous byte was finished sending.

So I tried to set A0 in the spi_transmit_byte function. 

Public void spi_transmit_byte(U8 byte, Boolean reg_select)
{
    /* Polling to see if the TX buffer is ready */
    while (!(SPI_getInterruptStatus(EUSCI_B0_BASE,EUSCI_B_SPI_TRANSMIT_INTERRUPT)));
    /* Transmitting data to slave */
	if (reg_select)
    {
        GPIO_setOutputHighOnPin(GPIO_PORT_P1, GPIO_PIN7);
    }
    else
    {
        GPIO_setOutputLowOnPin(GPIO_PORT_P1, GPIO_PIN7);
    }
	
    SPI_transmitData(EUSCI_B0_BASE, byte);
}

However in this case A0 is also set at the wrong time. More exactly it is set during the PREVIOUS
byte, so the transmission isn't correct. Basically the way I understand it, is that the EUSCI_B_SPI_TRANSMIT_INTERRUPT is raised

when the SPI interface is ready for a new byte, but the old one hasn't finished sending yet. What I need to do is somehow catch the moment
when the MCU starts to actually send a byte and then control the A0 pin. Right now I don't see a good way of doing this. maybe somebody has experience with a similar problem.

PS! In the code there are the following defines:

#define Public
#define Private static

  • Just a thought:

    Maybe I could create an interrupt handler for EUSCI_B_SPI_TRANSMIT_INTERRUPT.

    When spi_transmit_byte is called then we store the desired A0 pin state in a global variable. When EUSCI_B_SPI_TRANSMIT_INTERRUPT
    happens then we know that the interface is ready for new TX data, so it should be in the process of sending out the previous byte, so we read
    the global variable and based on that drive the A0 pin in the interrupt handler.

    Still, that seems like a bit of a sketchy solution to me....
  • You can poll STATW:UCBUSY after sending the final byte to know when the SPI goes idle.

    UCBUSY is flaky in early versions of the MSP432 chip (read: "black Launchpad"). If you have a "red Launchpad" you should be fine.
  • Working with a black launchpad unfortunately :( - Still will give it a try, let you know how it goes.
  • Are you working with the DMA controller? The display receives a lot of data, so the DMA might be handy here. Anyway - if you do not use the DMA and if you can afford the little gap between the bytes, you can also trigger on the RX interrupt instead of the TX interrupt. In this case you definitely know when a byte has been shifted out completely.

    Dennis

  • Hmm.. well the LCD does not transmit anything back, so it is transmit only. But I guess that it does not matter, since I could
    just receive 0x00 every time and discard the data. Thanks for the idea, I will try it as well.
  • The LCD does not have to transmit anything back. There is no need to read the data at all, simply clear the RX interrupt flag - you do not even have to configure the MISO pin in order to trigger on the RX interrupt.

  • Dennis is right that DMA is very useful for the bulk Tx that graphical displays (typically) want. Unfortunately, it doesn't get you out of the UCBUSY business, since the DMA is just writing to the (buffered) TXBUF, so the DMA completes before the SPI does.

    You could do DMA (to a bit-bucket) on the Rx side as well, which would keep things in synch, but is kinda overkill.

    When I was doing this with the black Launchpad, I finally resorted ("don't try this at home, kids") to counting clocks. One of the joys of SPI is that its timing is deterministic -- based on the MCLK/SMCLK ratio and the USCI clock divider, you know that the transaction must complete within some number of clocks after the DMA completes, so I just used __delay_cycles to wait until I knew it must have finished. It's a bit clunky, but it was only applied for the last (of perhaps a few hundred) bytes.
  • Yes, at this point I am just trying to get the LCD to start up and show "anything", so I think that DMA is a bit overkill. I might just try and get it to run with really stupid code, like making a 50msec delay before every command, so that I KNOW for sure that the pins are in the right configuration. I can try more advanced solutions later, when I know that at least something is working :)
  • Bruce McKenney47378 said:
    Dennis is right that DMA is very useful for the bulk Tx that graphical displays (typically) want. Unfortunately, it doesn't get you out of the UCBUSY business, since the DMA is just writing to the (buffered) TXBUF, so the DMA completes before the SPI does.

    Fully agree to that. And I saw myself in the same situation with a graphical display long time ago. I used an EA DOGS102 part. I ended up with a combination of the DMA for sending the data and polling the BUSY flag after the DMA signaled completion in order to recognize the finished transmission of the USCI module. Also with the black LaunchPad - I never experienced any problems with UCBUSY.

    Dennis

  • Joonatan Renel said:
    Yes, at this point I am just trying to get the LCD to start up and show "anything"

    Then the way with triggering on the RX interrupt is 100% safe, although this method is not the best one since you have small gaps between the bytes (which isn't a problem for the LCD) and you also have an interrupt after every byte (but this would also be the case case when triggering on the TX interrupt).

    Dennis

  • On a side note - I haven't had time to look into the A0 pin solution, but I did figure out why the display did not do anything, (e.g. even accept commands like show all points, which does not use A0) - I plugged the ˇRESET wire to the ˇRST pin on the MSP432, just to try it. Before I was using a general purpose IO pin, but I think that maybe setting it low in the beginning is the wrong way to go. Maybe DOGL 128 needs to do a full reset cycle, meaning it has to go high, then stay there for a small amount of time and then get pulled low again.

    Anyway, the display is now active and showing garbage (which is to be expected until I get the A0 pin problem sorted out) :)
  • Joonatan Renel said:
    Anyway, the display is now active and showing garbage (which is to be expected until I get the A0 pin problem sorted out)

    You can give the RX interrupt solution a try - then you always know the transmission state of every byte and you can set or clear your A0 pin when necessary.

  • What Dennis said. In pseudocode-ish:

    while (!TXIFG) /*EMPTY*/;
    TXBUF = byte;
    while (!RXIFG) /*EMPTY*/;
    (void)RXBUF; // clear RXIFG

    Since there's never more than one byte in the pipeline, the RXIFG tells you that the byte has been sent (and you can change A0).
  • > Also with the black LaunchPad - I never experienced any problems with UCBUSY.

    It didn't happen all the time. But it only had to happen once.
  • But I would use interrupts instead of polling the RX flag. Those are quite a lot bytes and the controller would stay in a loop for a very long time.
  • Ok, looks like I got the A0 pin working, the solution is not ideal, because it creates gaps in the bytes that are sent, but it is a good starting place. I think if I spend some time on this, I can probably find a better solution, but for testing this is good enough.

    Here is the code:

    /*
     * spi.c
     *
     *  Created on: Aug 13, 2017
     *      Author: Joonatan
     */
    
    #include <driverlib.h>
    #include <spi_drv.h>
    #include "register.h"
    
    /* SPI Master Configuration Parameter */
    const eUSCI_SPI_MasterConfig spiMasterConfig =
    {
            EUSCI_B_SPI_CLOCKSOURCE_SMCLK,             // SMCLK Clock Source
            12000000,                                  // SMCLK = DCO = 12MHZ
            100000,                                    // SPICLK = 100khz
            EUSCI_B_SPI_MSB_FIRST,                     // MSB First
            EUSCI_B_SPI_PHASE_DATA_CHANGED_ONFIRST_CAPTURED_ON_NEXT,    // Phase
            EUSCI_B_SPI_CLOCKPOLARITY_INACTIVITY_HIGH, // High polarity
            EUSCI_B_SPI_3PIN                           // 3Wire SPI Mode
    };
    
    
    Public void spi_init(void)
    {
        /* Selecting P1.5 and P1.6 in SPI mode */
        GPIO_setAsPeripheralModuleFunctionInputPin(GPIO_PORT_P1,
                GPIO_PIN5 | GPIO_PIN6 , GPIO_PRIMARY_MODULE_FUNCTION);
    
        /* Configuring SPI in 3wire master mode */
        SPI_initMaster(EUSCI_B0_BASE, &spiMasterConfig);
    
        /* Enable SPI module */
        SPI_enableModule(EUSCI_B0_BASE);
    
        /* Enabling interrupts */
        SPI_enableInterrupt(EUSCI_B0_BASE, EUSCI_B_SPI_RECEIVE_INTERRUPT);
        Interrupt_enableInterrupt(INT_EUSCIB0);
    }
    
    Public void spi_transmit(U8 * data, U16 data_len)
    {
        U8 * data_ptr = data;
    
        while (data_len > 0)
        {
            spi_transmit_byte(*data_ptr, FALSE);
            data_ptr++;
            data_len--;
        }
    }
    
    volatile Boolean isReadyToTransmit = TRUE;
    
    
    //******************************************************************************
    //
    //This is the EUSCI_B0 interrupt vector service routine.
    //
    //******************************************************************************
    void EUSCIB0_IRQHandler(void)
    {
        uint32_t status = SPI_getEnabledInterruptStatus(EUSCI_B0_BASE);
    
        SPI_clearInterruptFlag(EUSCI_B0_BASE, status);
    
        if (status & EUSCI_B_SPI_RECEIVE_INTERRUPT)
        {
            isReadyToTransmit = TRUE;
        }
    }
    
    Public void spi_transmit_byte(U8 byte, Boolean reg_select)
    {
        //Set A0 pin to proper state.
        while(!isReadyToTransmit);
    
        if (reg_select)
        {
            set_reg_select(1u);
        }
        else
        {
            set_reg_select(0u);
        }
    
        isReadyToTransmit = FALSE;
        //Transmit data to slave.
        SPI_transmitData(EUSCI_B0_BASE, byte);
    }
    

  • And just for reference, here is the file that actually writes commands to the display, maybe somebody is interested in the future.
    I created functions for pin controls elsewhere, but this should not be too difficult.

    This code basically initializes the EA DOGL128 LCD display, clears it and then writes some lines onto one of the segments of the display.
    It needs a lot of work, to be turned into anything useful, but I think it is a good starting point.

    /*
     * display_drv.c
     *
     *  Created on: Aug 13, 2017
     *      Author: Joonatan
     */
    
    #include "display_drv.h"
    #include <driverlib.h>
    #include "spi_drv.h"
    #include "register.h"
    
    #define NUMBER_OF_PAGES 8u
    #define NUMBER_OF_COLUMNS 128u
    
    /* TODO : Some of these could be replaced with macros, or inline functions. */
    
    Private void disp_command(U8 cmd, Boolean reg_select);
    Private void set_page_address(U8 page);
    Private void set_column(U8 column);
    Private void clear_display(void);
    Private void write_data(U8 data);
    Private void display_reset(void);
    
    Private U8 display_buffer[NUMBER_OF_COLUMNS][NUMBER_OF_PAGES];
    
    
    /* Set up A0 pin for display. */
    Public void display_init(void)
    {
        //Set the chip select pin as high initially.
        memset(display_buffer, 0x00u, sizeof(display_buffer));
        display_reset();
        set_cs_pin(1u);
    }
    
    Public void display_test_sequence(void)
    {
        U8 ix;
    
        //Low CS, means that it is active.
        set_cs_pin(0u);
    
        //Display start line set.
        disp_command(0x40u, FALSE);
    
        //Set ADC in reverse
        disp_command(0xA1u, FALSE);
        //Common output mode select
        disp_command(0xC0u, FALSE);
    
        //display normal
        disp_command(0xA6u, FALSE);
    
        //LCD bias set
        disp_command(0xA2u, FALSE);
    
        //Power control set
        disp_command(0x2Fu, FALSE);
    
        //Booster ratio set.
        disp_command(0xF8u, FALSE);
        disp_command(0x00u, FALSE);
    
        //Contrast set.
        disp_command(0x27u, FALSE);
        disp_command(0x81u, FALSE);
        disp_command(0x10u, FALSE);
    
        //Static indicator set.
        disp_command(0xACu, FALSE);
    
        //Turn on display.
        disp_command(0x00u, FALSE);
        disp_command(0xAFu, FALSE);
    
        delay_msec(200);
        //Turn on all points.
        disp_command((0xA4u | 0x01u), FALSE);
        clear_display();
        delay_msec(2000);
        //Turn off all points.
        disp_command(0xA4u, FALSE);
    
        //Lets try writing something.
    
        //Set to page address 2.
        set_page_address(4u);
        set_column(0u);
    
        for (ix = 0u; ix < 128u; ix++)
        {
            //Write data.
            //disp_command(0xAAu, TRUE);
            write_data(0xAAu);
        }
    }
    
    
    Private void disp_command(U8 cmd, Boolean reg_select)
    {
        spi_transmit_byte(cmd, reg_select);
    }
    
    
    Private void set_page_address(U8 page)
    {
        disp_command(0xB0u | (page & 0x0fu), FALSE);
    }
    
    
    Private void write_data(U8 data)
    {
        disp_command(data, TRUE);
    }
    
    
    Private void set_column(U8 column)
    {
        //Set column MSB.
        disp_command(0x10u | (column >> 4u),  FALSE);
        //Set column LSB.
        disp_command(column & 0x0fu, FALSE);
    }
    
    
    Private void clear_display(void)
    {
        U8 ix;
        U8 yx;
    
        for (ix = 0u; ix < NUMBER_OF_PAGES; ix++)
        {
            set_page_address(ix);
            set_column(0);
            for (yx = 0u; yx < NUMBER_OF_COLUMNS; yx++)
            {
                write_data(0x00u);
            }
        }
    }
    
    
    Private void display_reset(void)
    {
        set_disp_reset_pin(0u);
        delay_msec(100);
        set_disp_reset_pin(1u);
        delay_msec(100);
    }

  • To know that it generally works is often the most important thing - from this point the optimization can be started ;) Good to hear it works!
  • /*
     * typedefs.h
     *
     *  Created on: 21.02.2016
     *      Author: Joonatan
     */
    
    #ifndef TYPEDEFS_H_
    #define TYPEDEFS_H_
    
    #include <string.h>
    #include <stdio.h>
    
    typedef unsigned char   U8;
    typedef unsigned short  U16;
    typedef unsigned int    U32;
    
    typedef enum { FALSE, TRUE } Boolean;
    
    //////// masking
    #define ISBIT(p,b)    (p & b)
    #define SETBIT(p,b)   p |= b
    #define CLRBIT(p,b)   p &= ~b
    
    #define BIT_0  0x01u
    #define BIT_1  0x02u
    #define BIT_2  0x04u
    #define BIT_3  0x08u
    #define BIT_4  0x10u
    #define BIT_5  0x20u
    #define BIT_6  0x40u
    #define BIT_7  0x80u
    
    #define Private static
    #define Public
    
    #endif /* TYPEDEFS_H_ */

    Added some type definitions as well -> Might make it easier to make sense of the code.

**Attention** This is a public forum