News:

Let's find out together what makes a PIC Tick!

Main Menu

Software i2c in Positron

Started by TimB, Mar 07, 2023, 10:51 AM

Previous topic - Next topic

Frizie

Les, I see you use a procedure for the delay routine I2C_hDelay() which contains only DelayUs 1
Couldn't you write "better":

Symbol I2C_hDelay = 1

and at the various places in the program itself:

DelayUs I2C_hDelay

or, what is the advantage of using a procedure in this case?
Ohm sweet Ohm | www.picbasic.nl

top204

#21
If larger delays are used, the value for the delay is placed in compiler system variables then the compiler's library subroutine will be called to perform the delay itself, using more flash memory every time it is used for a single delay time.

For microcontrollers operating at faster speeds, the DelayUs command will use a block of assembler code that implements a small delay inline so that it is accurate, but for the accuracy, it takes up more flash memory whenever it is used in the program. However, with the procedure, it is a single call that is being made from the program, regardless of the delay required within it.

For example... On an 18F device operating at 64MHz, a DelayUs 1 command will produce the assembler code:

    movlw 5
_pblb__2
    decfsz WREG,F,0
    bra _pblb__2
    nop

Whenever it is used in the program's listing.

But if a procedure is created to delay for 1uS, the assembler code to call it is:

call DelayUS_1

or

rcall DelayUS_1, if the procedure is close to the memory address that called it, saving flash memory and operating faster, thanks to the optimiser pass.

So... Saving quite a bit of flash memory to do the same task if used multiple times in a program.

The DelayUs_1() procedure is:

Proc DelayUS_1()
    DelayUs 1
EndProc

For, not absolutely accurate, delays, a procedure call will save a lot of flash memory if it is used a lot in the program listing, and do the same job. However, it must be remembered that the enhanced 14-bit core devices do not have a large call stack, so always be aware of the call depth from procedures to procedures to procedures. The 18F devices have a larger call stack and the new 18F devices have a larger stack than the original 18F devices, so it is rarely a problem calling procedures from procedures from procedures with them, but it is important to be aware of the call stack size on the device.

JonW

This is excellent information and should be cited in the manual Les.

TimB


I just want to say thanks to Les for the i2c code. After playing with it and replacing the I2C_hDelay() with a NOP and specifying the port directly I have managed to get a read of 1 byte to 24us.  That is talking to a 32mhz pic 16f device acting as a slave. Clock stretching is working well and has made all the difference.

Talking to this slave is important as I do it at 100hz and that means main CPU overhead is reduced. 999.999% All I need is one byte. I needed 2-3 bits of that byte and was seriously thinking I would drop i2c and have it just +5, bit0, bit1, 0v and some other system to force it to talk i2c for a few other bytes I may need to check once in a while.

I have made my system as modules so if one breaks its a plug and play replacement.

As an aside this 16F module has water level sensors and even just reading the ADC channels it uses would take > 100us so well worth the effort to move it to standalone and talk i2c to it.



Its been an adventure for me this i2c stuff but looking at Les's code it now looks simple and understand what is happening.

 

top204

#24
Many thanks Jon and Tim.

Below is the "Software_I2C.inc" procedures made to use fixed pins for the SDA and SCL lines, and I named it "Software_I2C_Fixed_Pins.inc" in the includes directory:

$ifndef _Software_I2C_Fixed_Pins_inc_
$define _Software_I2C_Fixed_Pins_inc_
'
'   /\\\\\\\\\
'  /\\\///////\\\
'  \/\\\     \/\\\                                                 /\\\          /\\\
'   \/\\\\\\\\\\\/        /\\\\\     /\\\\\\\\\\     /\\\\\\\\   /\\\\\\\\\\\  /\\\\\\\\\\\  /\\\\\\\\\
'    \/\\\//////\\\      /\\\///\\\  \/\\\//////    /\\\/////\\\ \////\\\////  \////\\\////  \////////\\\
'     \/\\\    \//\\\    /\\\  \//\\\ \/\\\\\\\\\\  /\\\\\\\\\\\     \/\\\         \/\\\        /\\\\\\\\\\
'      \/\\\     \//\\\  \//\\\  /\\\  \////////\\\ \//\\///////      \/\\\ /\\     \/\\\ /\\   /\\\/////\\\
'       \/\\\      \//\\\  \///\\\\\/    /\\\\\\\\\\  \//\\\\\\\\\\    \//\\\\\      \//\\\\\   \//\\\\\\\\/\\
'        \///        \///     \/////     \//////////    \//////////      \/////        \/////     \////////\//
'                                  Let's find out together what makes a PIC Tick!
'
' Software I2C writing and reading routines using fixed pins for the SDA and SCL lines.
'
' Written for the Positron8 and Positron16 BASIC compilers by Les Johnson
'
' Set the pins used for the I2C SDA and SCL lines
'
    Symbol I2C_SDA_Pin = PORTC.3                ' Holds the pin used for the SDA line
    Symbol I2C_SCL_Pin = PORTC.4                ' Holds the pin used for the SCL line
'
' Create some global variables for the routines
'
    Dim I2C_bBitIndex As Byte Access            ' Holds the index for the bits to send/receive on the I2C line
    Dim I2C_bData     As Byte Access            ' Holds the byte sent or received

'----------------------------------------------------------------------------------------------
' Create a very small delay to slow down the I2C interface (if required)
' Input     : None
' Output    : None
' Notes     : None
'
$define I2C_hDelay() DelayCs 1

'----------------------------------------------------------------------------------------------
' Write a byte to the I2C bus
' Input     : pData holds the byte to write
' Output    : None
' Notes     : MSB first
'
Proc I2C_WriteByte(pData As I2C_bData)
    For I2C_bBitIndex = 7 DownTo 0              ' Create a loop for the 8-bits to write
        If pData.7 = 1 Then                     ' Is bit-7 of pData set?
            PinInput I2C_SDA_Pin                ' Yes. So make the SDA pin an input (made high by the pull-up resistor)
        Else                                    ' Otherwise...
            PinLow I2C_SDA_Pin                  ' Make the SDA pin an output low
        EndIf                                   '
        I2C_hDelay()                            ' Create a small delay
        PinInput I2C_SCL_Pin                    ' Make the SCL pin an input (made high by the pull-up resistor)
        I2C_hDelay()                            ' Create a small delay
        PinLow I2C_SCL_Pin                      ' Make the SCL pin an output low
        I2C_hDelay()                            ' Create a small delay
        Rol pData                               ' Rotate pData left by 1 bit
    Next
'
' Close up the interface
'
    PinInput I2C_SDA_Pin                        ' Make the SDA pin an input (made high by the pull-up resistor)
    PinInput I2C_SCL_Pin                        ' Make the SCL pin an input (made high by the pull-up resistor)
    I2C_hDelay()                                ' Create a small delay
    PinLow I2C_SCL_Pin                          ' Make the SCL pin an output low
    I2C_hDelay()                                ' Create a small delay
EndProc

'----------------------------------------------------------------------------------------------
' Read a byte from the I2C bus
' Input     : None
' Output    : Returns the byte received
' Notes     : MSB first
'
Proc I2C_ReadByte(), I2C_bData
    Dim wTimeout As Word

    Result = 0                                  ' Clear the result of the procedure before entering the loop
    PinInput I2C_SDA_Pin                        ' Make the SDA pin an input (made high by the pull-up resistor)
    For I2C_bBitIndex = 7 DownTo 0              ' Create a loop for the 8-bits to read
        Result = Result << 1                    ' Shift the result value left by 1 bit
        '
        ' Wait for any clock stretching required, with a small timeout
        '
        PinInput I2C_SCL_Pin                    ' Make the SCL pin an input (made high by the pull-up resistor)
        wTimeout = $01FF                        ' \
        Repeat                                  ' / Create a loop for the clock stretching
            If I2C_SCL_Pin = 1 Then Break       ' Exit the loop when the SCL pin is high
            Dec wTimeout                        ' \ Until the timeout value reaches 0
        Until wTimeout = 0                      ' /
        '
        ' Read the data
        '
        I2C_hDelay()                            ' Create a small delay
        Result.0 = I2C_SDA_Pin                  ' Read the state of the SDA pin into bit-0 of the result
        PinLow I2C_SCL_Pin                      ' Make the SCL pin an output low
        I2C_hDelay()                            ' Create a small delay
    Next
EndProc

'----------------------------------------------------------------------------------------------
' Send an I2C bus Start condition
' Input     : None
' Output    : None
' Notes     : None
'
Proc I2C_Start()
    PinInput I2C_SDA_Pin                        ' Make the SDA pin an input (made high by the pull-up resistor)
    I2C_hDelay()                                ' Create a small delay
    PinInput I2C_SCL_Pin                        ' Make the SCL pin an input (made high by the pull-up resistor)
    I2C_hDelay()                                ' Create a small delay
    PinLow I2C_SDA_Pin                          ' Make the SDA pin an output low
    I2C_hDelay()                                ' Create a small delay
    PinLow I2C_SCL_Pin                          ' Make the SCL pin an output low
    I2C_hDelay()
EndProc

'----------------------------------------------------------------------------------------------
' Send an I2C bus ReStart condition
' Input     : None
' Output    : None
' Notes     : None
'
Proc I2C_ReStart()
    PinInput I2C_SDA_Pin                        ' Make the SDA pin an input (made high by the pull-up resistor)
    I2C_hDelay()                                ' Create a small delay
    PinInput I2C_SCL_Pin                        ' Make the SCL pin an input (made high by the pull-up resistor)
    I2C_hDelay()                                ' Create a small delay
    PinLow I2C_SDA_Pin                          ' Make the SDA pin an output low
    I2C_hDelay()                                ' Create a small delay
    PinLow I2C_SCL_Pin                          ' Make the SCL pin an output low
    I2C_hDelay()
EndProc

'----------------------------------------------------------------------------------------------
' Send an I2C bus Stop condition
' Input     : None
' Output    : None
' Notes     : None
'
Proc I2C_Stop()
    PinLow I2C_SDA_Pin                          ' Make the SDA pin an output low
    I2C_hDelay()                                ' Create a small delay
    PinInput I2C_SCL_Pin                        ' Make the SCL pin an input (made high by the pull-up resistor)
    I2C_hDelay()                                ' Create a small delay
    PinInput I2C_SDA_Pin                        ' Make the SDA pin an input (made high by the pull-up resistor)
    I2C_hDelay()
EndProc

'----------------------------------------------------------------------------------------------
' Initiate an I2C Acknowledge
' Input     : None
' Output    : None
' Notes     : None
'
Proc I2C_Ack()
    I2C_hDelay()
    PinLow I2C_SDA_Pin                          ' Make the SDA pin an output low
    I2C_hDelay()                                ' Create a small delay
    PinInput I2C_SCL_Pin                        ' Make the SCL pin an input (made high by the pull-up resistor)
    I2C_hDelay()                                ' Create a small delay
    PinLow I2C_SCL_Pin                          ' Make the SCL pin an output low
    I2C_hDelay()                                ' Create a small delay
    PinInput I2C_SDA_Pin                        ' Make the SDA pin an input (made high by the pull-up resistor)
EndProc

'----------------------------------------------------------------------------------------------
' Initiate an I2C Not Acknowledge
' Input     : None
' Output    : None
' Notes     : None
'
Proc I2C_NAck()
    PinInput I2C_SDA_Pin                        ' Make the SDA pin an input (made high by the pull-up resistor)
    I2C_hDelay()                                ' Create a small delay
    PinInput I2C_SCL_Pin                        ' Make the SCL pin an input (made high by the pull-up resistor)
    I2C_hDelay()                                ' Create a small delay
    PinLow I2C_SCL_Pin                          ' Make the SCL pin an output low
    I2C_hDelay()                                ' Create a small delay
    PinInput I2C_SDA_Pin                        ' Make the SDA pin an input (made high by the pull-up resistor)
EndProc

$endif      ' _Software_I2C_Fixed_Pins_inc_

It can be made faster still, but will need testing as the code is being altered, in case it operates too fast.

For safety sake, the I2C interface was slowed down in the library, but can be made faster by removing some of the I2C_hDelay() meta-macros when the pins are changing their state. i.e. In the I2C_Start, I2C_ReStart, I2C_Ack, I2C_NAck, I2C_Stop and I2C_ReadByte and I2C_WriteByte procedures.

Notice I have changed the I2C_hDelay to a meta-macro instead of a precedure, because the DelayCs command uses very few mnemonics when the cycles are very low. And DelayCs 1 places a single nop or clrwdt in the assembler listing, and DelayCs 2 will place a bra $+1 mnemonic etc...

Also, note that the clock stretching loop has been made into a Repeat-Until loop for longer time stretches, and the GetPin functions have been changed to standard Port . Pin reads, because they are no longer required.

tumbleweed

It would be a nice addition for writebyte to support stretching as well. You could speed things up by only looking for it during the 9th bit (ACK), since that's when most slaves will use it.

Very few will do it during the 8-bit data phase.