Help speed up my SPI

Discussion in 'Programmer's Corner' started by portreathbeach, Jun 15, 2014.

  1. portreathbeach

    Thread Starter Active Member

    Mar 7, 2010
    143
    5
    Hi,

    I am currently working with bit banging an SPI protocol using a PIC16F690 and a Nokia 5110 LCD module. I've been using some adapted code from Microchip's library, but it is pretty slow.

    Code ( (Unknown Language)):
    1.  
    2. void spi_bit_out(unsigned char data){
    3.     SDAT = (data>>7);               // output the MSB
    4.     SCLK = 1;                       // clock high
    5.     short_delay();                 // 2uS delay
    6.     SCLK = 0;                       // clock low
    7. }
    8.  
    9.  
    10. //....................................................................
    11. // Writes a byte to the SPI bus
    12. //....................................................................
    13. void spi_wr(unsigned char data, unsigned char com_data)
    14. {
    15.     unsigned char i;                // loop counter
    16.     DC = com_data;                  // set the data/command pin
    17.  
    18.     for (i = 0; i < 8; i++)         // loop through each bit
    19.         {
    20.         spi_bit_out(data);          // output bit
    21.         data = data << 1;           // shift left for next bit
    22.         }
    23. }
    24.  
    I know I can speed it up a little by not having spi_wr call the spi_bit_out function and put that function into spr_wr, but it's still pretty slow.

    Bassically, everytime spi_bit_out is called, the register is rotated 7 times to get the MSB, is there a quicker and less costly way of doing this.

    As 'data' in the bit_out function is an unsigned char, could I simply subtract 127 from it and leave myself with either 1 or 0 in the data register?

    It currently takes about 0.4s to clear the screen on my Nokia 5110 LCD module running at 4MHz. I need to get a quicker routine, as I want to play around with a 1.8" 128x160 colour TTF module. This needs 2 bytes sent to it for every pixel whereas the Nokia 5110 module uses a bit for each pixel.
     
  2. Papabravo

    Expert

    Feb 24, 2006
    10,148
    1,791
    How about:

    SDAT = (data & 0x80) ? 1 : 0 ;
     
    absf likes this.
  3. portreathbeach

    Thread Starter Active Member

    Mar 7, 2010
    143
    5
    Hi Papabravo, that has certainly sped things up. That instruction takes 6uS at 8Mhz to complete, rather than the 56uS the rotate one did, but still this seems slow. In assembly, a couple of simple bit test functions would take so much less time to complete.

    This function now takes 130uS (at 8MHz) to send 1 Byte out over SPI:

    Code ( (Unknown Language)):
    1. void spi_wr(unsigned char data, unsigned char com_data)
    2. {
    3.     unsigned char i;                // loop counter
    4.  
    5.     DC = com_data;                  // set the data/command pin
    6.  
    7.     for (i = 0; i < 8; i++)         // loop through each bit
    8.         {
    9.  
    10.     SDAT = (data & 0x80) ? 1 : 0 ;  // output the MSB
    11.     SCLK = 1;                       // clock high
    12.     SCLK = 0;                       // clock low
    13.  
    14.         data = data << 1;           // shift left for next bit
    15.     }
    16. }
    Even at 8MHz, just clearing the display of a 160x128 TTF display would take 160 * 128 * 2 bytes per pixel * 130uS = 5.32 seconds.

    Is there any quicker way to send SPI apart from a quicker processor and using the PICs hardware SPI port?


    Edit: Got a few figures wrong, I've changed them now!
     
    Last edited: Jun 15, 2014
  4. Markd77

    Senior Member

    Sep 7, 2009
    2,803
    594
    Unroll the loop and get rid of the shifts, should save a bit of time:
    SDAT = (data & 0x80) ? 1 : 0 ;
    SDAT = (data & 0x40) ? 1 : 0 ;
    SDAT = (data & 0x20) ? 1 : 0 ;
    SDAT = (data & 0x10) ? 1 : 0 ;
    SDAT = (data & 0x08) ? 1 : 0 ;
    SDAT = (data & 0x04) ? 1 : 0 ;
    SDAT = (data & 0x02) ? 1 : 0 ;
    SDAT = (data & 0x01) ? 1 : 0 ;
     
  5. portreathbeach

    Thread Starter Active Member

    Mar 7, 2010
    143
    5
    Cheers, Just gave it a go in the simulator and it takes 64uS to send a byte when using this (at 8MHz):

    Code ( (Unknown Language)):
    1. void spi_wr(unsigned char data, unsigned char com_data)
    2. {
    3.     DC = com_data;                  // set the data/command pin
    4.  
    5.     SDAT = (data & 0x80) ? 1 : 0 ;
    6.     SCLK = 1;                       // clock high
    7.     SCLK = 0;                       // clock low
    8.     SDAT = (data & 0x40) ? 1 : 0 ;
    9.     SCLK = 1;                       // clock high
    10.     SCLK = 0;                       // clock low
    11.     SDAT = (data & 0x20) ? 1 : 0 ;
    12.     SCLK = 1;                       // clock high
    13.     SCLK = 0;                       // clock low
    14.     SDAT = (data & 0x10) ? 1 : 0 ;
    15.     SCLK = 1;                       // clock high
    16.     SCLK = 0;                       // clock low
    17.     SDAT = (data & 0x08) ? 1 : 0 ;
    18.     SCLK = 1;                       // clock high
    19.     SCLK = 0;                       // clock low
    20.     SDAT = (data & 0x04) ? 1 : 0 ;
    21.     SCLK = 1;                       // clock high
    22.     SCLK = 0;                       // clock low
    23.     SDAT = (data & 0x02) ? 1 : 0 ;
    24.     SCLK = 1;                       // clock high
    25.     SCLK = 0;                       // clock low
    26.     SDAT = (data & 0x01) ? 1 : 0 ;
    27.     SCLK = 1;                       // clock high
    28.     SCLK = 0;                       // clock low
    29. }
    30.  
    Haven't tried it for real, but it should work.

    Still may have to look at the hardware SPI I think though, as this would still take 2.62 seconds to write to the 128x160 RGB screen.
     
  6. THE_RB

    AAC Fanatic!

    Feb 11, 2008
    5,435
    1,305
    It still sees very slow!

    How is SDAT defined? What speed is your PIC running at? Can you post the asm output from your compiler?

    You need to get something that compiles down to this;
    Code ( (Unknown Language)):
    1.  
    2. btfss data,7
    3. bcf portb,0    // SDAT
    4. btfsc data,7
    5. bsf portb,0
    6. bsf portb,1    // SCLK
    7. bcf portb,1
    8.  
    total 6 instructions, and takes 6 cycles (1.2uS per bit).

    In MikroC this code compiles down to that;
    Code ( (Unknown Language)):
    1.  
    2.   if(data.F7) PORTB.F0 = 1;    // SDAT
    3.   else        PORTB.F0 = 0;
    4.   PORTB.F1 = 1;                // SCLK
    5.   PORTB.F1 = 0;
    6.  
    If you make 8 sets of that it will take 1.2uS *8 = 9.6 uS to send the whole byte. :)

    Those times are based on your PIC at 20MHz, because you would be nuts to be running at 8MHz and complaining that things are taking too long... ;)
     
    absf likes this.
  7. portreathbeach

    Thread Starter Active Member

    Mar 7, 2010
    143
    5
    Cheers The_RB.

    SDAT is defined as so:

    Code ( (Unknown Language)):
    1. #define SDAT        PORTCbits.RC3

    As I am using the free Microchip XC8 compiler, it may be adding extra lines of code in there to bulk it out. I could code the byte_out function in assembly, this would speed things up.

    How much faster is the hardware SPI on the PIC micros?
     
  8. portreathbeach

    Thread Starter Active Member

    Mar 7, 2010
    143
    5
    OK, I just tried this (obviously only outputs 1 bit, but it's just a test):

    Code ( (Unknown Language)):
    1. #asm
    2. _asm
    3.     btfss data,7
    4.     bcf PORTC,3    // SDAT
    5.     btfsc data,7
    6.     bsf PORTC,4
    7.     bsf PORTC,4    // SCLK
    8.     bcf PORTC,4
    9. _endasm
    10. #endasm
    11.  
    Not sure why I need to use #asm, then _asm, but it's the only thing that worked and others on the internet suggested it.

    Only problem is, an error saying 'data' is not defined. Obviously is assembly I need to give it an address not a variable from my C code. Is there a sneaky way to get the address of where 'data' is stored to be able to use it in the asm part of the code?
     
  9. portreathbeach

    Thread Starter Active Member

    Mar 7, 2010
    143
    5
    Right, sorted it and it is now flying....

    Here is my SPI write function:

    Code ( (Unknown Language)):
    1. void spi_wr(unsigned char data, unsigned char com_data)
    2. {
    3.     DC = com_data;                  // set the data/command pin
    4.     unsigned char d = data;
    5.  
    6. #asm
    7. _asm
    8.     btfss spi_wr@d,7
    9.     bcf SDAT_PORT,SDAT_PIN    // SDAT
    10.     btfsc spi_wr@d,7
    11.     bsf SDAT_PORT,SDAT_PIN
    12.     bsf SCLK_PORT,SCLK_PIN    // SCLK
    13.     bcf SCLK_PORT,SCLK_PIN
    14.  
    15.     btfss spi_wr@d,6
    16.     bcf SDAT_PORT,SDAT_PIN    // SDAT
    17.     btfsc spi_wr@d,6
    18.     bsf SDAT_PORT,SDAT_PIN
    19.     bsf SCLK_PORT,SCLK_PIN    // SCLK
    20.     bcf SCLK_PORT,SCLK_PIN
    21.  
    22. etc.
    23.  
    At 8MHz clock, the function takes 29uS to send a byte, way better than before. It takes 34uS in total to complete a byte send, as I have to get the byte I want to send from an array, then call the spi_out function.

    So, to display an image on my Nokia 5110 LCD module, it only takes 28mS. Way quicker than before.

    So, for a 160x128 colour TTF screen, even at 8MHz, that would 160 * 128 * 2 * 34uS = 1.39 seconds. Not too bad.


    Thanks for all the help, all I need to do now is use a quicker clock.

    :)
     
  10. THE_RB

    AAC Fanatic!

    Feb 11, 2008
    5,435
    1,305
    Congrats! Good to see you got it sorted.

    You might want to try it in C code, this code;
    Code ( (Unknown Language)):
    1.  
    2.   if(data.F7) PORTB.F0 = 1;    // SDAT
    3.   else        PORTB.F0 = 0;
    4.   PORTB.F1 = 1;                // SCLK
    5.   PORTB.F1 = 0;
    Should compile down to the same assembler you are using. It's good practice with really speed ciritcal C code to compile it and check the compiler output which should be in an autogenerated .asm or .lst file.

    Then you get to know how badly the compiler mangles your nice C code. ;)

    But seriously with the right C code your compiler will probably make the same asm output that you have above, without the need to use inline asm in your code which can have its own problems. :)
     
  11. portreathbeach

    Thread Starter Active Member

    Mar 7, 2010
    143
    5
    I'll have a bit more of a play with the compiler to test what it outputs, but am pretty happy to use assembly within my C code.

    Do you know much about using fonts on GLCDs. I have the basic system font 5x7 which I use on my Nokia 5110 module, but when I get my 1.8" 160x128 I will need larger fonts. I've downloaded a few from Adafruit, but I don't understand how they work. Any help would be great.
     
  12. ErnieM

    AAC Fanatic!

    Apr 24, 2011
    7,394
    1,606
    A font is just a small bitmap: you pick a corner and start drawing it from there. I store them as an array of bitmaps with the ASCII code being the index into the array.

    Complications arise when the corners of the bitmap do not align with the natural byte layout of the screen. In fact, except for very tall (or wide) characters they never do!

    Back when I did this I would read out a byte of the data on the screen, punch a hole for the letter, shift a portion of my bitmap to align it, and write the byte back.

    I remember it took me an intense morning to code& debug for that, and as soon as that afternoon I could not read my own code.

    I will post if anyone is interested.
     
  13. portreathbeach

    Thread Starter Active Member

    Mar 7, 2010
    143
    5
    Cheers ErnieM. I just downloaded LCD Font Creator which I'm running on a virtual machine on the Mac. I can sort of see how it's working. Pretty similar to my basic LCD Creator I wrote.

    At present I am using the standard 5x7 font array which works well with the Nokia 5110 module as the characters are not proportionally spaced. They each take 5 columns with a blank column added at the start.

    I'll just keep playing about with the font creator until I get what I need.
     
  14. John P

    AAC Fanatic!

    Oct 14, 2008
    1,634
    224
    A lot. And it's faster both in terms of the data being sent out, and the time used by the processor--because, of course, it's an automatic process.

    In fact, not using the built-in SPI when it's available is a case of "buying a dog and then barking yourself". Why wouldn't you use it?
     
  15. portreathbeach

    Thread Starter Active Member

    Mar 7, 2010
    143
    5
    Hi John. I see what you are saying.

    I started using software because I was programming in assembly and it looked very complicated using the built in hardware. But now I'm using C it may be easier for me to set it up.

    You say about processing time. But surely most of the time, even with hardware SPI, you are still sitting there waiting for it to send a byte and then loading another byte etc. until everything you want to send is sent.
     
  16. John P

    AAC Fanatic!

    Oct 14, 2008
    1,634
    224
    Well, it can look intimidating, but it's actually very easy. Here is the setup procedure from a project I did using the PIC16F690 and the CCS compiler, though this part is probably the same for any other compiler. CCS likes to give you a function to do everything, and I usually write things out explicitly--that's my way of buying a dog and barking myself!
    Code ( (Unknown Language)):
    1.  
    2.     sspstat = 0b01000000;
    3.     sspcon = 0b00100000;        // Master mode, Fosc/4 (1MHz)
    4.  
    Then here is the way I drive the SPI port; in this case I'm only sending data, not receiving anything. What I'm operating is two MAX7219 display drivers, which each take 2 bytes of data in series, and there's one of them on the output of the other. Then in addition, there's a 74HC595 shift register between the processor and the display drivers, so it gets the last data byte. It's a peculiar feature of the CCS compiler that they call a single byte an "int", so where you see "int" just pretend it's "unsigned char". Finally, note the use of portc_shadow, which is there to ensure that I don't get tripped up by Microchip's absurd read-modify-write nonsense with port pins. Maybe I don't need that, but I just wasn't in the mood to figure it out.

    What I recall about this was that I looked at the time taken to send each byte, and at 1MHz it would be 8 instruction times or a little longer, and I thought, will waiting for each byte delay the program enough to cause problems? And for 5 bytes, I just decided it was OK to wait, for the sake of having a simple routine. Probably if I'd done some kind of polling routine to let the processor do some other work and then send another byte, it would have taken at least as long to decide what to do next, but it's the kind of decision you have to make all the time with these processors.

    Code ( (Unknown Language)):
    1.  
    2.  
    3. write_to_7219(int addr_l, int data_l, int addr_h, int data_h, int to_local)
    4. {
    5.          
    6.     bit_clear(sspcon, 7);           // Clear write collision detect
    7.  
    8.     sspbuf = addr_h;
    9.     while (!bit_test(sspstat, 0)) ;
    10.     sspbuf = data_h;
    11.     while (!bit_test(sspstat, 0)) ;         // Every write involves 5 bytes
    12.     sspbuf = addr_l;
    13.     while (!bit_test(sspstat, 0)) ;
    14.     sspbuf = data_l;
    15.     while (!bit_test(sspstat, 0));
    16.         sspbuf = to_local;               // To 74HC595
    17.     while (!bit_test(sspstat, 0)) ;
    18.  
    19.     bit_set(portc_shadow, 6);
    20.     portc = portc_shadow;            // Strobe high
    21.  
    22.     bit_clear(portc_shadow, 6);
    23.     portc = portc_shadow;            // 7219 strobe low
    24. }
    25.  
     
    Last edited: Jun 17, 2014
  17. portreathbeach

    Thread Starter Active Member

    Mar 7, 2010
    143
    5
    Thanks John P,

    Maybe I'll have a look into the hardware side of SPI.

    I don't mind using software to do it, I've now managed to use some of the stuff I've learnt here to speed up my i2c routines.
     
  18. portreathbeach

    Thread Starter Active Member

    Mar 7, 2010
    143
    5
    Hi again John P.

    Was going to try hardware SPI later tonight and just wanted to ask a couple of questions.

    Do I need to set up the TRIS registers for the port pins when using the SPI, or does it automatically set them for you.


    After SSPBUF has been loaded with the data to send, you have the command line:

    while (!bit_test(sspstat, 0)) ;

    How long does that instruction take to run? Would using:

    while (!SSPSTAT & 0x01);

    be quicker?


    Thanks in advance

    Craig
     
  19. portreathbeach

    Thread Starter Active Member

    Mar 7, 2010
    143
    5
    Hi. Got the hardware SPI working.

    I used 'while (!BF)' for the wait till buffer is clear.

    I have to say though, it is only slightly lquicker than the assembly I used. But obviously looks a lot better than all those assembly codes in the program.
     
  20. portreathbeach

    Thread Starter Active Member

    Mar 7, 2010
    143
    5
    Just had a few hours trying to get hardware SPI working on a PIC18LF26K22. If anyone is interested, here is how you write to the SPI bus:

    Code ( (Unknown Language)):
    1.  
    2.     SCE = 0;            // Chip enable
    3.  
    4.     PIR1bits.SSP1IF = 0;            // clear the SSP transmit finished interrupt flag
    5.     SSP1CON1bits.WCOL = 0;          // clear write collision bit SSP1
    6.     SSP1BUF = data;                 // Load data into buffer SSP1
    7.     while(!PIR1bits.SSP1IF);        // wait for SSP interrupt flag to say transmit is finished
    8.  
    9.     SCE = 1;            // Chip enable
    10.  
    The while (!BF) doesn't seem to work on the 18F PIC I'm using, so here I clear the 'transmit finished' interrupt flag, load the buffer, then wait for the flag to set before continuing. Then clear it again.
     
Loading...