Shop OBEX P1 Docs P2 Docs Learn Events
Need a fast I2C object and having some trouble grasping the PASM_I2C_Driver — Parallax Forums

Need a fast I2C object and having some trouble grasping the PASM_I2C_Driver

photomankcphotomankc Posts: 943
edited 2011-10-25 20:47 in Propeller 1
I'm goinig to need some fast(ish) I2C communication as I get started writing up a stepper drive for my robot project. I downloaded the PASM and Spin versions of the I2C driver but I see they are heavily biased to accessing EEPROMs and have built in assumptions that I will be using register addresses. Before I go cross-eyed trying to figure out how to alter the PASM and not bust it peices I have a question or two.

One, has anyone modified this already for simple reading and writing with just (I2C ADDR) + (DATA) + (DATA) ...... ???

Failing that. I'm lost on the whole device select thing. As hard as I look I don't get what is being done there. All I'll want to do is take a normal 7 bit I2C address and shift it left then set the R/W bit and transmit. I can't figure out what's going on though between addrReg and devSelect in the driver so I'm not sure how to alter this to suit me.

My protocol will be simple and both devices will agree on this. A single byte command ID and then a specific number of bytes to follow with the data for what to do. Several will be single byte commands to take instant action like soft stop, hard stop, and reset. No registers needed. So am I correct to assume I can take this:
mov     t1, addrreg1
shr     t1, #15
and     t1, #%1110
or      devsel1, t1
call    #startfunc

and make it something like this instead:
mov     t1, addr
shl      t1, #1
or        t1, #RECV
call    #startfunc

So now I pass in a full address, it shifts left one to set or clear the R/W bit and transmit that using the read or write functions. I'd also cut out all the writing of the register data from those functions as well I think. So Strike all this:
'   ackbit := (ackbit << 1) | Write(SCL, addrReg >> 8 & $FF)
                        mov     parm1, addrreg1
                        shr     parm1, #8
                        and     parm1, #$ff
                        call    #writebytefunc
                        shl     ackbit2, #1
                        or      ackbit2, parm1
'   ackbit := (ackbit << 1) | Write(SCL, addrReg & $FF)          
                        mov     parm1, addrreg1
                        and     parm1, #$ff
                        call    #writebytefunc
                        shl     ackbit2, #1
                        or      ackbit2, parm1

Comments

  • Peter JakackiPeter Jakacki Posts: 10,193
    edited 2011-10-18 21:47
    Not sure which actual object you are talking about. I just use the primitive for writing a byte whether it is an address or data and frame the transaction with start and stop conditions. No need to worry about the special EEPROM definitions.
  • photomankcphotomankc Posts: 943
    edited 2011-10-18 23:06
    It's PASM_i2C_driver.spin I'm looking at. Good point though. I got wrapped up in trying to understand the convenient word and long functions which were just calling the xxxxPage functions. Got wrapped around the axle on that and lost track of the fact they are all just using read and write. Thanks for the smack upside the head! I think I can alter those others to allow for easier sending of words and longs for my use but at least I can get started this way.
  • Dave HeinDave Hein Posts: 6,347
    edited 2011-10-19 15:34
    photomankc,

    pasm_i2c_driver is basically a PASM version of Mike Green's basic_i2c_driver. It was based on an earlier version of basic_i2c_driver that generated a device select and 2 address bytes. Since then, Mike Green added a feature that enabled sending 0 or 1 address bytes by oring the address with either $200000 or $300000. This could be added to pasm_i2c_driver, or you could just remove the code that sends the extra address bytes like you suggested in your first post.

    You can adjust the value of DELAY_CYCLES to set the bus speed that you want. The current value yields a bus speed of about 400 KHz on an 80 MHz processor.

    Dave
  • photomankcphotomankc Posts: 943
    edited 2011-10-20 07:54
    Dave / All, I tried going the way of modifying the code and ran into a flurry of difficulty. The drive controller is being developed on an Arduino and I have it talking to another Arduino at 400KHz with no problem. When I interface the Prop to it and I use the primative 'Write' functions then the code works just fine but is too slow due to the overhead of calling each function in spin with long pauses between bytes. 170uS but it did read and display what I wrote to it. Then I tested modified multi-byte code against an empty bus and I seemed to get the expected results; The address and data both look good but no ACK and the time to transmit address and one byte was down to ~60uS, much better for what I need.

    When I added in the Arduino after that then it went to h@#$. It would alternate between SDA starting high and being left low till the next start, or starting low and then being left high till the next start back and forth like that. Also was seeing some weird 'half' clocks where they were short by several volts but the correct duration. That's got to be electrical somehow but I can't see how right off the bat. I did disable the Arduinos built-in pull-ups to 5V and used my own to 3.3V, but the Arduino is a 5V device. Now, If I added delays between the bytes as well as some to the start and stop it gets better but still goes crazy now and then but most of the data makes it to the other side. Every now and then however the stop condition gets missed no matter how many delays I add and then the next few bytes go back to the flip-flopping data line and midget clock pulses only to settle down again and behave a few bytes later.

    I'm going to try to test against some simple 3.3V native device like an EEPROM or temp sensor and simply see if I can get just a simple ACKs to all my writes to that. I was a little lost in what was going on with the 'ackbit2' variable. Looked like perhaps it's just accumulating the ACKs but I wasn't sure if it's doing something more subtle elswhere that I'm missing. Maybe you can look at the 'writepagefunc' as I have altered it and see if I'm doing anything blatently stupid there? writepagefunc is the only one I have been really working on thus far. As far as I know the Arduino's built in I2C hardware doesn't try to drive the clock but I sure can't figure out if not... then how do those midget clock pulses happen?

    Ultimately I'd like to alter this to be open-drain and not drive SCL or SDA directly at all. Attached are the original driver and then my modified driver and the simple test code
  • Dave HeinDave Hein Posts: 6,347
    edited 2011-10-20 08:40
    One thing that is different is that you call i2c.WriteLong, which writes 4 data bytes instead of the 1 data byte that you send in your commented code. Have you tried calling i2c.WriteByte?

    You may want to try a longer delay until you get things working. Set DELAY_CYCLES to 200 to see if that helps. You can speed it up later once you get it working. Also, pasm_i2c_driver generates an asymetrical clock that is high for one-third of the cycle, and low for two-thirds. This is consistent with the timing used by most EEPROMS. The high clock pulse may be too short for the arduino, so you may need to add an extra call to the delay routine when pulling the clock high. You can then reduce DELAY_CYCLES to get 400 KHz.
  • photomankcphotomankc Posts: 943
    edited 2011-10-20 09:03
    Thanks! I'll give that a look. I threw the WriteLong in there at the end just make sure the loop was working as expected. The tests were done with writeByte. It's definately got some speed sensitivity because any delay below 55 means there is no hope of it working at all. I went up to 80 and it seemed better so that does make sense reguarding the clock high time because I had two Arduino's talking all the way up to 500KHz. That gives me somewhere to start looking at this. Anything under 75 to 80uSec is probably fast enough for it to squeeze in without tying up the other end for too long when a command is running but since the Aurdino side needs to attend to it's step signal up to once every ~300uSecs the faster I can cram a byte into it the better.

    I also need to make some proper level shifters for the two of them and eliminate weirdness from everyone trying to pull the bus to different voltage levels on each side.


    BTW: Thanks a ton for taking the time to answer! My last work in PASM was 2-1/2 years ago and it was nowhere near the complexity of this.
  • Mike GreenMike Green Posts: 23,101
    edited 2011-10-20 09:04
    If you haven't looked at it already, download FemtoBasic from the ObEx and look at sdspifemto.spin. This is the PASM I2C driver that Basic_I2C_Driver is based on and it can be used by itself. The interface routines are written in Spin, but they just communicate through a small control block in the hub RAM. There's no reason why it can't be used by a PASM program running in another cog and it was written so I/O can be overlapped and double buffering could be used. Have a look at the comments at the beginning.

    There is a WAITCNT delay before picking up a new command. This was put in to cut down on power consumption by not having the cog sit in a tight loop waiting for a byte to become non-zero, but it could be removed without affecting anything.
  • Rick_HRick_H Posts: 116
    edited 2011-10-20 17:35
    rh_I2C_asm.spin
    I think the most educational PASM code I have written was an I2C driver, but mine is really striped down, no interface and such. I have it outputting to an 8 channel DAC 8 bit signals and the address and channel data every 360us. and its all based on blinking LED tutorial in the WebPM.pdf.
  • photomankcphotomankc Posts: 943
    edited 2011-10-20 23:30
    It's not the clock high time. Adding more does nothing. It does seem to be related to the time after the ACKs and the next event. Adding in 11uSecs of delay between start, bytes, and stop has helped greatly. So far 300KHz is the fastest clock rate I can get about 99% reliabilty out of. Still, once every so often it blows the stop conditon and double reads the bus like below:

    Here we are looking at 8 bytes recieved where byte one is the command and the rest is data. The second byte is incremented by one each time the write occurs. This was written using the WritePage function.

    Caught Command - F1
    Cmd Buffer - F1, 78, F1, F0, D3, C2, B1, A0, 0, 0, 0, 0, 0, 0, 0, 0,
    Command = CMD_HARD_STOP

    Caught Command - F1
    Cmd Buffer - F1, 79, F1, F0, D3, C2, B1, A0, 0, 0, 0, 0, 0, 0, 0, 0,
    Command = CMD_HARD_STOP

    Caught Command - F1
    Cmd Buffer - F1, 7A, F1, F0, D3, C2, B1, 41, 8, F1, 7B, F1, F0, D3, C2, B1,
    Command = CMD_HARD_STOP

    Caught Command - F1
    Cmd Buffer - F1, 7C, F1, F0, D3, C2, B1, A0, 8, F1, 7B, F1, F0, D3, C2, B1,
    Command = CMD_HARD_STOP

    The frustrating thing is it can take 20 minutes to happen or it can happen twice in a row in seconds. Slowin down and delaying more just doesn't seem to help much. Uggghhhh. On the positive note I think I understand the code well enough to alter it and see if it works with other I2C stuff. I might not have the skill to write that from scratch in PASM but I think I can hack this one into what I want.

    I'm tired now and this is becoming 'unfun'.
  • Peter JakackiPeter Jakacki Posts: 10,193
    edited 2011-10-20 23:34
    What value pull-ups are you using. Depending upon the bus capacitance etc you may need a lower value resistor, especially for the SDA line.
  • photomankcphotomankc Posts: 943
    edited 2011-10-21 22:49
    It's the delay between bytes it appears. Looks for all the world like it implements clock stretching because the clock line will sometimes get held low visably when I do a stop bit and I have recoded the write/stop/start to drive the line as open collector. Looks like 12 or 13uSecs between the bytes is required as well as bracketing the data at stop and start. I don't remeber the Arduino to Arduino run looking like that but I'll run a test later and see. So far I'm going on 45 minutes without a lost command nor a missed stop. Looking more hopeful if a little speed disappointing. It is operating though at 400KHz now at least.

    The resistors are 4.7K and signals look good, just the modest rise humps but not like shark fins or anything.
  • Ray0665Ray0665 Posts: 231
    edited 2011-10-22 08:13
    I2C in Spin. Maybe you can use it for learning purposes, don't know if it is fast enough for what you want.
  • ChrisGaddChrisGadd Posts: 310
    edited 2011-10-22 11:11
    I'm not sure I understand your setup; is the propeller the master, supplying clocks, sending and requesting data, producing all of the starts and stops, and it's a slave device that is missing an occasional stop?

    Here's something I wrote just to experiment with the EEPROM. It just reads the first 500 bytes and displays them on a serial terminal. It works up to about 450KHz, though the EEPROM is the only device I've tried it with.
    I2C PASM 01.spin

    After getting that one figured out, I rewrote it to see just how far I could push the tolerances. Turns out they can be pushed quite a bit. This one is completely reliable up to 500KHz, which I suspect is the limit of the EEPROM.
    I2C PASM.spin
  • photomankcphotomankc Posts: 943
    edited 2011-10-22 20:10
    Correct. Prop is master and at present is only writing data to the bus and will be in control of the bus completely. The Arduino is a slave using it's Two-Wire hardware and is reading data from the bus. When viewed on the scope you could see the midget pulses every so often and when they appeared then at times the clock would stay low for too long even though a stop condition did appear on the data line. Back when I had the bus running 5V with the prop behind some protection resistors you'd see the midget pulses as a complete clock pulse but only rising to under 1V. The Prop was driving it but something else was holding it down. Now with open-collector 3V bus the clocks are just gone when it happens of course. Looking at the data-sheet for the AVR I think it's starting to make some sense. The AVR has a single byte buffer for the two-wire comms so after it recieves or sends it's going to need some time to move the byte in or out of that. I assume the Library on the Arduino handles that but I'm betting it's stretching the clock until it's ready for the next byte which is why adding in delay between the bytes and delay before the stop has got it working.

    What finally made me see it was that when the Arduino was held in reset and no ACKs were coming in the last clock low just before the stop condition was followed by an immediate rise just as it was coded. When the Arduino comes out of reset and starts ACKing the bytes then the last clock low is dragging on for several microseconds longer. If that got long enough to just go past the point where the prop released the SDA line then that stop was missed. The ACKs can't affect my time-keeping so it has to be the slave holding the clock low. I think I have the code figured out well enough and so far, as long as I keep the delay between bytes at 12uSecs then it works without any errors. So I think I'll look at implementing clock-stretching awareness in the code. That would be a better solution then trying to add enough delay for the worst-case and really I shouold have thought of it earlier given this is an MController not a dedicated hardware system. The way the two-wire library worked on the Arduino had me thinking it had a multi-byte buffer but it dont.
  • ChrisGaddChrisGadd Posts: 310
    edited 2011-10-23 06:24
    Hmm, after the prop finishes sending a byte, try floating the clock high one final time for the acknowledge, and leaving the clock high until the next byte is ready and send a repeated start. If the prop is the only master device on the bus, there shouldn't be any reason it has to relinquish control. If the arduino doesn't acknowledge the following control byte, that should indicate that it's still busy and you need to resend the start and control until it does acknowledge.
  • localrogerlocalroger Posts: 3,452
    edited 2011-10-23 14:37
    Last year I did a high speed IIC EEPROM driver and while I started with the Femtobasic drivers I ended up rewriting them from scratch because while those drivers work, and well, the division of labor between functional elements is really unnatural for IIC and modifying them is a pain. Unfortunately, the code I wrote is entirely PASM based and not very suitable for your general purpose usage. I feel your pain.

    Earlier this year I wrote an IIC slave suite which had to be in PASM because the master was running at 90 KHz, and I was quite glad I had done the earlier master conversion because I would never have been able to debug the damn thing otherwise.
  • photomankcphotomankc Posts: 943
    edited 2011-10-23 20:18
    That's a bingo folks. I'm quite happy tonight! :) I added a clock-stretching check to each high clock transition in write_byte and in stop and that's the ticket. Scope shows that this working quite well and makes it obvious why I was tearing my hair out earlier. This would have never worked right until it was open-collector and a serious amount of delay would have been needed to ignore clock-stretching. The fantastic news is that I now get my first byte out on the line in under 60uSecs, down from 78uSecs with delays everywhere. No errors or corrupt data on the other side after hours of operation. It averages about 7uSecs of clock-stretch after each byte but every now and again it can double and that was likely where errors were happening when a pulse or most of a pulse got wiped out. So I'm on track to create a PASM, open-collector, clock-stretching I2C driver I guess. Didn't see myself doinig that a week ago.


    Here's the snipets that are relevent:
    writebytefunc     mov     data1, parm1      ' Get the data byte
                            mov     count1, #8        ' Set loop count for 8 bits
    
                            andn    outa, sda_bit     ' Set I2C lines to logic low - KVC
                            andn    outa, scl_bit     ' Set I2C lines to logic low - KVC
                            
    :loop                 shl     data1, #1         ' Shift left one bit
                            test    data1, #$100    wz ' Check MSB
            if_z           or      dira, sda_bit     ' Drive SDA if low
            if_nz         andn    dira, sda_bit     ' Float SDA if High
                            call    #delay
                            andn    dira, scl_bit     ' Float SCL High KVC
                            call    #check_stretch    ' Check for clock stretching
                            call    #delay            
                            call    #delay            ' Longer clock HIGH time 
                            call    #delay
                            or      dira, scl_bit     ' Drive SCL LOW KVC
                            call    #delay
                            djnz    count1, #:loop
                            
    
                            andn    dira, sda_bit     ' Float SDA
                            call    #delay
                            andn    dira, scl_bit     ' Float SCL High KVC
                            call    #delay
                            call    #delay            ' Longer clock HIGH time 
                            call    #delay
                            call    #check_stretch 
                            test    sda_bit, ina    wz ' Check SDA input
            if_z           mov     ackbit1, #0       ' Set to zero if LOW
            if_nz         mov     ackbit1, #1       ' Set to one if HIGH
                            or      dira, scl_bit     ' Drive SCL LOW KVC 
                            call    #delay      
                            or      dira, sda_bit     ' Set SDA as output
                            mov     parm1, ackbit1    ' Return the ack bit
                            call    #delay    
                            'or      outa, scl_bit     ' Set SCL HIGH
    writebytefunc_ret       ret
    

    check_stretch           test    scl_bit, ina    wz    ' Check SCL state
                  if_z            jmp     #check_stretch     ' if LOW keep checking
    check_stretch_ret     ret                                  ' Otherwise proceed
    

    I made the clock highs longer to be more realistic with the waveforms I'm seeing much faster and I get some nasty shark fins. Possible that some of that could be removed if the resistors were lower than 4.7K and a not dealing with breadboard capacitance. I also made up a little level shifter board from some BSS138 mosfets to allow each side to have the bus running at it's native voltage. First forey into surface mount components and it's working nicely.
  • Dave HeinDave Hein Posts: 6,347
    edited 2011-10-24 06:12
    Good work on implementing clock stretching. I wonder why the Arduino needs to stretch the clock. Maybe it's due to interrupt latency, or it requires some time to process the data as it receives it. Was there any documentation on the Arduino app that mentioned clock stretching?
  • photomankcphotomankc Posts: 943
    edited 2011-10-24 08:21
    Thanks! On the why....It's a bear to read through the AVR datasheet on it since they made it an alphabet soup for two-wire, but it does mention that there is a flag raised ("Two Wire Interrupt Flag") when the two-wire hardware expects the MCU to take action and that SCL is stretched low while the flag is raised. The software must then clear the flag when it's done. Just took a little digging to find that. So there is interrupt and processing overhead involved but seems kind of long just to fetch a byte and get out which is what I would think the goal would be but then it's only a 16MHz MCU too with library overhead so I'll not get too riled over it. I may dive into the library later to get a better picture. For now I'm happy with the fact that the new code will slow down where it needs to but run flat out where it can. However all this came with a cost, 400KHz looks to be close to the ceiling for the clock given the longer high time and added instructions. That's fine though. Without getting into the protocol for switching to high-speed mode that's as fast as an I2C bus ought to run anyway and the timing for my 'quick commands' will be pretty darn good really.
  • photomankcphotomankc Posts: 943
    edited 2011-10-25 20:47
    Well,
    Thanks to all that chimed in with help and suggestions. All major functions seem to be working as expected and the timing all suggests that it's meeting specs for I2C even on the big capacitor known as my breadboard. I now have two way coms going between the devices so now it's just tweaks and getting into the guts of making the steppers move how I tell them.

    Got a new toy tonight! USBee Logic Analyzer. This thing is just wicked cool. I don't have to count bits on the O-Scope anymore!

    Drive I2C Status Command.jpg
    1024 x 454 - 66K
Sign In or Register to comment.