Shop OBEX P1 Docs P2 Docs Learn Events
SPI blues - need a guru's eye — Parallax Forums

SPI blues - need a guru's eye

Some time back i wrote my own SPI driver because the only one I could find on OBEX was providing SHIFTIN and SHIFTOUT routines, but nothing in the fashion of: rxData:= Shift(txData. How I understand the principle of SPI, data is read in as it is written out. Therefore I wrote a SPI library to provide that ( rx:= Transfer(tx)). This excersise was a stretch for me at my skill level, so I was quite proud too.
Now I supect there is a bug hidden in there. I am at present working at getting up and running with a powerSTEP01 stepper motor controller, and the first or last '1' bit goes missing when writing to the chip, depending on SPI mode.
Needless to say I would want that bug in my SPI code to be killed, but to get on with talking to the chip I would like if there is some other SPI object with the same functionality that I can use.

Thanks,

Erlend

Comments

  • Peter JakackiPeter Jakacki Posts: 10,193
    edited 2016-09-30 12:32
    If you have a close look at the SPI timings for the actual commands rather than the "general SPI" timing in the datasheet you will find that they are effectively half-duplex, no need to read while you are writing. This is indeed the case I have found as I have used other ST stepper chips with the same SPI structure.

    I know some of the timing diagrams seem to indicate read/write which only leads to this confusion. Figure 28 is fairly typical, send a command byte then with the MOSI low just read the 3 data bytes etc.
  • It looks like iIdleCLK is incorrect. As it is, it looks like you are always using POL=0. Try changing it to:
    iIdleCLK:=   iMode >> 1
    
  • Seairth wrote: »
    It looks like iIdleCLK is incorrect. As it is, it looks like you are always using POL=0. Try changing it to:
    iIdleCLK:=   iMode >> 1
    

    Thanks, but isn't iMode & %10 having the same effect? I tried your suggestion, but the bug is still there.

    Erlend
  • If you have a close look at the SPI timings for the actual commands rather than the "general SPI" timing in the datasheet you will find that they are effectively half-duplex, no need to read while you are writing. This is indeed the case I have found as I have used other ST stepper chips with the same SPI structure.

    I know some of the timing diagrams seem to indicate read/write which only leads to this confusion. Figure 28 is fairly typical, send a command byte then with the MOSI low just read the 3 data bytes etc.

    Agree, but some chips do behave as per SPI (duplex), ( e.g. the one that got me started on SPI, the nRF24L01+).
    I would prefer to do the SPI comms similar across different chips.

    Erlend
  • I find that when we try to generalize everything, we sometimes overburden code with unnecessary complications -- but that's me. Several years ago I wrote a simple Spin driver for the MCP2515 CAN interface chip that includes this (working) routine.
    pri spi_readwrite(datain) | dataout
    
    '' Shifts datain into MCP2515, receives and returns dataout (from MCP2515)
    
      datain <<= constant(32-8)                                     ' prep for MSB output
      repeat 8
        outa[mosi] := (datain <-= 1) & 1                            ' datain msb --> MOSI  
        dataout := (dataout << 1) | ina[miso]                       ' MISO <-- dataout msb 
        outa[sck] := 1                                              ' clock high
        outa[sck] := 0                                              ' clock low
       
      return dataout & $FF
    
  • Erlend wrote: »
    Seairth wrote: »
    It looks like iIdleCLK is incorrect. As it is, it looks like you are always using POL=0. Try changing it to:
    iIdleCLK:=   iMode >> 1
    

    Thanks, but isn't iMode & %10 having the same effect? I tried your suggestion, but the bug is still there.

    iMode & %10 just clears the LSB. For mode 0/1, iIdleCLK will be %00. For mode 2/3, iIdleCLK will be %10. When you assign OUTA[PINclk], it is only to iIdleCLK[0], which is always zero.
  • Which mode are you using? Based on the datasheet, it looks like you should be using mode 3 (%11):

    * CLK is Idle High.
    * Write on the falling edge
    * Read on the rising edge

    I think the problem you are running into is that you are performing the write too early (as if PHA=0). Try the following modification:
       REPEAT bitpos FROM iFirstPos TO iLastPos                          'follow the direction set by choice of MSB first or LSB first 
         IF iPhaseCLK== 0                                                
           OUTA[PINmosi]:= FRAMEtx >>bitpos                              'if the bit at at that postion is one, output 1
                                                                          
         WAITCNT(500 + cnt)                                              'allow the slave time to react                                                                  
         OUTA[PINclk]:= iActiveCLK                                       'leading edge of clock (whether Active is 1 or 0 is defined by SPI mode)
         
         IF iPhaseCLK== 0                                                'if acting on leading edge read the input now:
           IF INA[PINmiso]== 1                                           'if incoming data bit is 1                                                                      
             Framerx |= 1<<bitpos                                         'set bit in the buffer at respective position to 1, otherwise leavi it at 0
         ELSE
           OUTA[PINmosi]:= FRAMEtx >>bitpos                              'if the bit at at that postion is one, output 1
             
         WAITCNT(500 + cnt)                                              'allow the slave time to react    
         OUTA[PINclk]:= iIdleCLK                                         'trailing edge of clock (whether Idle is 1 or 0 is defined by SPI mode)
         
         IF iPhaseCLK== 1                                                'if acting on trailing edge read the input now:
           IF INA[PINmiso]== 1                                           'if incoming data bit is 1                                                                      
             Framerx |= 1<<bitpos                                        'set bit in the buffer at respective position to 1, otherwise leave it at 0     
    
  • JonnyMac wrote: »
    I find that when we try to generalize everything, we sometimes overburden code with unnecessary complications -- but that's me. Several years ago I wrote a simple Spin driver for the MCP2515 CAN interface chip that includes this (working) routine.
    pri spi_readwrite(datain) | dataout
    
    '' Shifts datain into MCP2515, receives and returns dataout (from MCP2515)
    
      datain <<= constant(32-8)                                     ' prep for MSB output
      repeat 8
        outa[mosi] := (datain <-= 1) & 1                            ' datain msb --> MOSI  
        dataout := (dataout << 1) | ina[miso]                       ' MISO <-- dataout msb 
        outa[sck] := 1                                              ' clock high
        outa[sck] := 0                                              ' clock low
       
      return dataout & $FF
    

    Do you remember what command(s) in the MCP2515 use full-duplex SPI?
  • I don't, and looking through the client's code it seems that I was mostly writing 0 when reading the device. Still this is the only routine that communicates with the device.

    This isn't about the MCP2515, it's about code and my belief that sometimes it's not worth having a "this object does everything in every possible circumstance" object when few lines of code will do the trick.
  • Seairth wrote: »
    iMode & %10 just clears the LSB. For mode 0/1, iIdleCLK will be %00. For mode 2/3, iIdleCLK will be %10. When you assign OUTA[PINclk], it is only to iIdleCLK[0], which is always zero.

    Ah, now I see. Thanks!

    Erlend

  • JonnyMac wrote: »
    I find that when we try to generalize everything, we sometimes overburden code with unnecessary complications -- but that's me. Several years ago I wrote a simple Spin driver for the MCP2515 CAN interface chip that includes this (working) routine.

    Yes, that is a perspective I sometimes need - each time I get hung up in developing 'the ideal code' instead of just something that gets the job done. Thanks for that PRI, I will put it into my toolbox, for sure.

    Erlend
  • I used the TLC2543 11-channel ADC extensively with the BASIC Stamp, and it is one of those that can use full duplex. Which of course the Stamp cannot do. The value that is sent by one SHIFTOUT command conveys the channel number for the ADC result that is returned in the following SHIFTIN command. If you can do full duplex, then you can acquire data twice as fast, but for most purposes given a Stamp, the half-duplex did just fine. There was one exception on the Stamp that occasionally came in handy. I could get full duplex on channel zero by leaving the mosi pin low and then just repeatedly clocking the chip to acquire that one channel faster than any other.
  • Seairth wrote: »
    Which mode are you using? Based on the datasheet, it looks like you should be using mode 3 (%11):

    * CLK is Idle High.
    * Write on the falling edge
    * Read on the rising edge

    I think the problem you are running into is that you are performing the write too early (as if PHA=0). Try the following modification:

    Seairth,

    I am very grateful for that you have taken the time to review my code and suggest how to fix the bugs! With the modifications you have suggested, it works nicely. I will have to wait for a moment when my head is clear (at least clearer...) to write up the SPI mode 3 timing diagram and step through the old and new code - to fully understand what your changes do.

    Erlend
  • I'm glad it worked for you. Timing for these sorts of things can be mind bending. I had to go over the diagrams and my suggested code changes multiple times just to make sure it did what I thought it was going to do. And, even then, I wasn't sure.

    I think that's why @JonnyMac's comment is worth paying attention to. In SPI, there are only four timings (the four modes). Any one of them is easy to understand on their own. It's when we try to come up with an algorithm that covers all four timings that things get difficult. However! We are interacting with hardware that will only ever make use of one of those timings. It will never change. All we need is enough code to handle that one timing scenario. This leads us toward the idea that it can be easier (at least, in this case) to have four separate code snippets, each of which are simple and understandable, that we choose from. And, as an added benefit, it will be faster (if only marginally), which is often an important design factor for this sort of work.
  • This leads us toward the idea that it can be easier (at least, in this case) to have four separate code snippets, each of which are simple and understandable, that we choose from.
    I like that idea and will do something like this for myself (just roughing out framework). It should be easy to port the the PRI methods to PASM if very high speed io is required.
    pub spi_io(outVal, p_inVal, mode, bits)
    
      case mode
        0     : result := spi_mode0(outVal, bits)
        1     : result := spi_mode1(outVal, bits)   
        2     : result := spi_mode2(outVal, bits)   
        3     : result := spi_mode3(outVal, bits)
        other : return
    
      if (p_inVal > 0)                                              ' do we care about read?
        case bits
           1..8  : byte[p_inVal] := result
           9..16 : word[p_inVal] := result  
          24..32 : long[p_inVal] := result
    
    
    pri spi_mode0
    
    
    pri spi_mode1
    
    
    pri spi_mode2
    
    
    pri spi_mode3                                                           
                                                                     
    
Sign In or Register to comment.