'------------------------------------------------------------------
' _I2CLib.bas
'------------------------------------------------------------------
' I2C Library for PIC18 devices using the MSSP module
' Author: COS, dodgflu66@yahoo.es
' Compiler: PIC18 Oshonsoft v6.10
' Date: 2026/05/02 - 2024/10

' Description:
' This library provides basic I2C master functions using the MSSP
' module. It includes routines to initialize the bus, generate START,
' RESTART and STOP conditions, write bytes, read bytes, and optionally
' allow longer read waiting times for devices that use clock stretching.

' This library is suitable for I2C slave devices such as EEPROMs,
' RTCs, temperature sensors, humidity sensors, ADCs, DACs, etc.

' Register naming note:
' Generic register names such as SSPCON1, SSPCON2, SSPSTAT, SSPBUF
' and SSPADD are used to maintain compatibility with PIC devices that
' only have one MSSP module.

' In PIC devices with two MSSP modules, Oshonsoft maps these generic
' names to MSSP1. To use MSSP2, the registers SSP2CON1, SSP2CON2,
' SSP2STAT, SSP2BUF and SSP2ADD must be used explicitly.

' Clock stretching note:
' Some I2C slave devices, such as the HTU21D in Hold Master mode,
' may hold the SCL line low while they complete an internal operation.
' This is known as clock stretching.

' The I2C master does not enable Hold Master mode itself. Hold Master
' mode is a feature of the slave device. This library only provides
' an optional longer timeout when waiting for received data.

' Revision notes:
' - ACK/NACK handling in Read_I2C() verified.
' - A maximum internal timeout has been added to each function
'   to ensure reliable communication with slow I2C slave devices.
' - Optional clock stretching wait added for sensors such as HTU21D.
'------------------------------------------------------------------

'------------------------------------------------------------------
' I2C pins
'------------------------------------------------------------------
Symbol I2C_IO_SCL = TRISC.3
Symbol I2C_IO_SDA = TRISC.4

'------------------------------------------------------------------
' Constants
'------------------------------------------------------------------
Const ACK = 0				    ' Send ACK after reading a byte
Const NACK = 1				    ' Send NACK after reading the last byte
'Const DELAY_END_WRITE_I2C = 25 ' Optional delay after writing, in microseconds
Const Delay_Timeout_I2C = 200   ' General timeout counter for MSSP operations
'------------------------------------------------------------------
' Global variables
'------------------------------------------------------------------
' Slave: Hold Master
Dim CLOCK_STRETCH_TIMEOUT_MS_I2C As Byte ' > 0, Enables extended wait for devices that use clock stretching.
										 ' The timeout defines the maximum wait time in milliseconds.
'------------------------------------------------------------------
' Function: Init_I2C

' Description:
' Initializes the MSSP module in I2C master mode.

' Arguments:
' - i2c_Khz   : desired I2C clock frequency in kHz
' - clock_Mhz : system oscillator frequency in MHz

' Notes:
' SSPADD is calculated using:

'     SSPADD = (Fosc / (4 * F_I2C)) - 1

' Example:
' For Fosc = 40 MHz and I2C = 100 kHz:

'     SSPADD = (40000000 / (4 * 100000)) - 1 = 99

' Returns:
' - 0: initialization completed
'------------------------------------------------------------------
Function Init_I2C(i2c_Khz As Long, clock_Mhz As Long) As Byte

    SSPSTAT = 0x80                    ' Slew rate disabled, suitable for standard speed
    SSPCON1 = 0x28                    ' Enable MSSP module in I2C master mode
    SSPCON2 = 0x00                    ' Clear MSSP control register

    SSPADD = ((clock_Mhz * 1000000) / (4 * (i2c_Khz * 1000))) - 1

    High I2C_IO_SCL                   ' Release SCL line
    High I2C_IO_SDA                   ' Release SDA line

    CLOCK_STRETCH_TIMEOUT_MS_I2C = 0  ' Extended read wait 0 by default

    WaitMs 1                          ' Short stabilization delay

    ReturnValue 0
End Function

'------------------------------------------------------------------
' Function: CLOCK_STRETCH_I2C(ms)

' Description:
' Enables or disables an extended read timeout.

' Arguments:
' - Timeout_ms_I2C : timeout in milliseconds

' Notes:
' This function does not force Hold Master mode in the I2C bus.
' Hold Master mode is selected by the slave device command.

' For example, the HTU21D uses:
' - 0xE3: temperature measurement, Hold Master mode
' - 0xE5: humidity measurement, Hold Master mode

' In that mode, the sensor may hold SCL low until the conversion is
' complete. This function simply allows Read_I2C() to wait longer
' for the received byte.

' If Timeout_ms_I2C = 0:
' - extended read wait is disabled.

' If Timeout_ms_I2C > 0:
' - extended reading waits up to Timeout_ms_I2C.

' Returns:
' - 0
'------------------------------------------------------------------
Function Clock_Stretch_I2C(Timeout_ms_I2C As Byte) As Byte
	CLOCK_STRETCH_TIMEOUT_MS_I2C = Timeout_ms_I2C
	ReturnValue 0
End Function

'------------------------------------------------------------------
' Function: START_I2C

' Description:
' Generates a START condition on the I2C bus.

' Notes:
' A START condition is generated when SDA goes low while SCL is high.

' Returns:
' - 0: START condition completed
' - 1: timeout error
'------------------------------------------------------------------
Function START_I2C() As Byte

    Dim count_timeout As Byte

    count_timeout = Delay_Timeout_I2C

    SSPCON2.SEN = 1                   ' Initiate START condition

    While SSPCON2.SEN = 1 And count_timeout > 0
        --count_timeout
    Wend

    If count_timeout = 0 Then
        ReturnValue 1
    Else
        ReturnValue 0
    Endif

End Function

'------------------------------------------------------------------
' Function: STOP_I2C

' Description:
' Generates a STOP condition on the I2C bus.

' Notes:
' A STOP condition is generated when SDA goes high while SCL is high.

' Returns:
' - 0: STOP condition completed
' - 1: timeout error
'------------------------------------------------------------------
Function STOP_I2C() As Byte

    Dim count_timeout As Byte

    count_timeout = Delay_Timeout_I2C

    SSPCON2.PEN = 1                   ' Initiate STOP condition

    While SSPCON2.PEN = 1 And count_timeout > 0
        --count_timeout
    Wend

    If count_timeout = 0 Then
        ReturnValue 1
    Else
        ReturnValue 0
    Endif

End Function

'------------------------------------------------------------------
' Function: ReStart_I2C

' Description:
' Generates a repeated START condition on the I2C bus.

' Notes:
' A repeated START allows the master to change from write mode to
' read mode without releasing the bus.

' This is commonly used with I2C sensors:

'     START
'     slave address + write
'     command/register address
'     RESTART
'     slave address + read
'     read data
'     STOP

' Returns:
' - 0: repeated START condition completed
' - 1: timeout error
'------------------------------------------------------------------
Function ReStart_I2C() As Byte

    Dim count_timeout As Byte

    count_timeout = Delay_Timeout_I2C

    SSPCON2.RSEN = 1                  ' Initiate repeated START condition

    While SSPCON2.RSEN = 1 And count_timeout > 0
        --count_timeout
    Wend

    If count_timeout = 0 Then
        ReturnValue 1
    Else
        ReturnValue 0
    Endif

End Function

'------------------------------------------------------------------
' Function: WRITE_I2C

' Description:
' Writes one byte to the I2C bus.

' Arguments:
' - data : byte to transmit

' Notes:
' After sending the byte, the MSSP module stores the ACK status in
' ACKSTAT.

' ACKSTAT = 0 means that the slave acknowledged the byte.
' ACKSTAT = 1 means that the slave did not acknowledge the byte.

' This function is useful for normal writes and also for ACK polling
' with devices that may temporarily respond with NACK.

' Returns:
' - 0: ACK received
' - 1: NACK received or timeout
'------------------------------------------------------------------
Function WRITE_I2C(data As Byte) As Byte

    Dim count_timeout As Byte
     count_timeout = Delay_Timeout_I2C

    PIR1.SSPIF = 0                    ' Clear MSSP interrupt flag
    SSPBUF = data                     ' Load data into transmit buffer
    While SSPSTAT.BF = 1 And count_timeout > 0
        --count_timeout
    Wend

    ' Timeout
    If count_timeout = 0 Then
        ReturnValue 1
        Exit 'Function
    Endif

    count_timeout = Delay_Timeout_I2C

    While PIR1.SSPIF = 0 And count_timeout > 0
        --count_timeout
    Wend

	' Timeout
    If count_timeout = 0 Then
        ReturnValue 1
        Exit 'Function
    Endif

	' ACK/NACK
	If SSPCON2.ACKSTAT = 0 Then
		ReturnValue 0 ' ACK
	Else
		ReturnValue 1 ' NACK
	Endif

    ' Optional short delay after writing.
    ' Enable only if a specific device requires it.
    'WaitUs DELAY_END_WRITE_I2C

End Function

'------------------------------------------------------------------
' Function: READ_I2C

' Description:
' Reads one byte from the I2C bus and sends ACK or NACK afterwards.

' Arguments:
' - _ack : ACK  to continue reading more bytes
'          NACK to finish the read sequence

' Notes:
' In I2C master receiver mode, the master must send:

' - ACK after each byte if more bytes will be read.
' - NACK after the last byte before generating STOP.

' Example:

'     byte1 = READ_I2C(ACK)
'     byte2 = READ_I2C(NACK)
'     STOP_I2C()

' Clock stretching:
' If CLOCK_STRETCH_ENABLE_I2C is enabled, the function waits longer for BF to be
' set. This is useful with sensors that hold SCL low while preparing
' the measurement result.

' Returns:
' - received byte
'------------------------------------------------------------------
Function READ_I2C(_ack As Byte) As Byte

    Dim count_timeout As Byte
    count_timeout = Delay_Timeout_I2C

    SSPCON2.RCEN = 1                  ' Enable reception mode
    If CLOCK_STRETCH_TIMEOUT_MS_I2C = 0 Then
        While SSPSTAT.BF = 0 And count_timeout > 0
            --count_timeout
        Wend
    Else
        count_timeout = CLOCK_STRETCH_TIMEOUT_MS_I2C
        While SSPSTAT.BF = 0 And count_timeout > 0
            WaitMs 1
            --count_timeout
        Wend
    Endif

    If _ack = ACK Then
        SSPCON2.ACKDT = 0             ' Prepare ACK
    Else
        SSPCON2.ACKDT = 1             ' Prepare NACK
    Endif

    count_timeout = Delay_Timeout_I2C
    SSPCON2.ACKEN = 1                 ' Send ACK/NACK sequence
    While SSPCON2.ACKEN = 1 And count_timeout > 0
        --count_timeout
    Wend

    ReturnValue SSPBUF ' Return: read received byte from buffer

End Function


