Help speed up my SPI

Thread Starter

portreathbeach

Joined Mar 7, 2010
143
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.

Rich (BB code):
void spi_bit_out(unsigned char data){
    SDAT = (data>>7);               // output the MSB
    SCLK = 1;                       // clock high
    short_delay();                 // 2uS delay
    SCLK = 0;                       // clock low
}


//....................................................................
// Writes a byte to the SPI bus
//....................................................................
void spi_wr(unsigned char data, unsigned char com_data)
{
    unsigned char i;                // loop counter
    DC = com_data;                  // set the data/command pin

    for (i = 0; i < 8; i++)         // loop through each bit
        {
        spi_bit_out(data);          // output bit
        data = data << 1;           // shift left for next bit
        }
}
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.
 

Thread Starter

portreathbeach

Joined Mar 7, 2010
143
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:

Rich (BB code):
void spi_wr(unsigned char data, unsigned char com_data)
{
    unsigned char i;                // loop counter

    DC = com_data;                  // set the data/command pin

    for (i = 0; i < 8; i++)         // loop through each bit
        {

    SDAT = (data & 0x80) ? 1 : 0 ;  // output the MSB
    SCLK = 1;                       // clock high
    SCLK = 0;                       // clock low

        data = data << 1;           // shift left for next bit
    }
}
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:

Markd77

Joined Sep 7, 2009
2,806
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 ;
 

Thread Starter

portreathbeach

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

Rich (BB code):
void spi_wr(unsigned char data, unsigned char com_data)
{
    DC = com_data;                  // set the data/command pin

    SDAT = (data & 0x80) ? 1 : 0 ;
    SCLK = 1;                       // clock high
    SCLK = 0;                       // clock low
    SDAT = (data & 0x40) ? 1 : 0 ;
    SCLK = 1;                       // clock high
    SCLK = 0;                       // clock low
    SDAT = (data & 0x20) ? 1 : 0 ;
    SCLK = 1;                       // clock high
    SCLK = 0;                       // clock low
    SDAT = (data & 0x10) ? 1 : 0 ;
    SCLK = 1;                       // clock high
    SCLK = 0;                       // clock low
    SDAT = (data & 0x08) ? 1 : 0 ;
    SCLK = 1;                       // clock high
    SCLK = 0;                       // clock low
    SDAT = (data & 0x04) ? 1 : 0 ;
    SCLK = 1;                       // clock high
    SCLK = 0;                       // clock low
    SDAT = (data & 0x02) ? 1 : 0 ;
    SCLK = 1;                       // clock high
    SCLK = 0;                       // clock low
    SDAT = (data & 0x01) ? 1 : 0 ;
    SCLK = 1;                       // clock high
    SCLK = 0;                       // clock low
}
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.
 

THE_RB

Joined Feb 11, 2008
5,438
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;
Rich (BB code):
btfss data,7
bcf portb,0    // SDAT
btfsc data,7
bsf portb,0
bsf portb,1    // SCLK
bcf portb,1
total 6 instructions, and takes 6 cycles (1.2uS per bit).

In MikroC this code compiles down to that;
Rich (BB code):
  if(data.F7) PORTB.F0 = 1;    // SDAT
  else        PORTB.F0 = 0;
  PORTB.F1 = 1;                // SCLK
  PORTB.F1 = 0;
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... ;)
 

Thread Starter

portreathbeach

Joined Mar 7, 2010
143
Cheers The_RB.

SDAT is defined as so:

Rich (BB code):
#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?
 

Thread Starter

portreathbeach

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

Rich (BB code):
#asm
_asm
    btfss data,7
    bcf PORTC,3    // SDAT
    btfsc data,7
    bsf PORTC,4
    bsf PORTC,4    // SCLK
    bcf PORTC,4
_endasm
#endasm
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?
 

Thread Starter

portreathbeach

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

Here is my SPI write function:

Rich (BB code):
void spi_wr(unsigned char data, unsigned char com_data)
{
    DC = com_data;                  // set the data/command pin
    unsigned char d = data;

#asm
_asm
    btfss spi_wr@d,7
    bcf SDAT_PORT,SDAT_PIN    // SDAT
    btfsc spi_wr@d,7
    bsf SDAT_PORT,SDAT_PIN
    bsf SCLK_PORT,SCLK_PIN    // SCLK
    bcf SCLK_PORT,SCLK_PIN

    btfss spi_wr@d,6
    bcf SDAT_PORT,SDAT_PIN    // SDAT
    btfsc spi_wr@d,6
    bsf SDAT_PORT,SDAT_PIN
    bsf SCLK_PORT,SCLK_PIN    // SCLK
    bcf SCLK_PORT,SCLK_PIN

etc.
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.

:)
 

THE_RB

Joined Feb 11, 2008
5,438
Congrats! Good to see you got it sorted.

You might want to try it in C code, this code;
Rich (BB code):
  if(data.F7) PORTB.F0 = 1;    // SDAT
  else        PORTB.F0 = 0;
  PORTB.F1 = 1;                // SCLK
  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. :)
 

Thread Starter

portreathbeach

Joined Mar 7, 2010
143
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.
 

ErnieM

Joined Apr 24, 2011
8,415
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.
 

Thread Starter

portreathbeach

Joined Mar 7, 2010
143
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.
 

John P

Joined Oct 14, 2008
2,068
How much faster is the hardware SPI on the PIC micros?
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?
 

Thread Starter

portreathbeach

Joined Mar 7, 2010
143
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.
 

John P

Joined Oct 14, 2008
2,068
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!
Rich (BB code):
	sspstat = 0b01000000;
	sspcon = 0b00100000;		// Master mode, Fosc/4 (1MHz)
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.

Rich (BB code):
write_to_7219(int addr_l, int data_l, int addr_h, int data_h, int to_local)
{
          
	bit_clear(sspcon, 7);			// Clear write collision detect

	sspbuf = addr_h;
	while (!bit_test(sspstat, 0)) ;
	sspbuf = data_h;
	while (!bit_test(sspstat, 0)) ;	     	// Every write involves 5 bytes
	sspbuf = addr_l;
	while (!bit_test(sspstat, 0)) ;
	sspbuf = data_l;
	while (!bit_test(sspstat, 0));
        sspbuf = to_local;			     // To 74HC595
	while (!bit_test(sspstat, 0)) ;

	bit_set(portc_shadow, 6);
	portc = portc_shadow;		     // Strobe high

	bit_clear(portc_shadow, 6);
	portc = portc_shadow;		     // 7219 strobe low
}
 
Last edited:

Thread Starter

portreathbeach

Joined Mar 7, 2010
143
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.
 

Thread Starter

portreathbeach

Joined Mar 7, 2010
143
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
 

Thread Starter

portreathbeach

Joined Mar 7, 2010
143
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.
 

Thread Starter

portreathbeach

Joined Mar 7, 2010
143
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:

Rich (BB code):
    SCE = 0;            // Chip enable

    PIR1bits.SSP1IF = 0;            // clear the SSP transmit finished interrupt flag
    SSP1CON1bits.WCOL = 0;          // clear write collision bit SSP1
    SSP1BUF = data;                 // Load data into buffer SSP1
    while(!PIR1bits.SSP1IF);        // wait for SSP interrupt flag to say transmit is finished

    SCE = 1;            // Chip enable
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.
 
Top