I²C Master driver for PIC24FJ + FreeRTOS


I have written an I²C driver for my PIC24FJ32GB002 which is running with FreeRTOS. It has an easy to use API and can be easily adapted to suit any other microcontroller. Also, thanks to the RTOS, many independent threads can be queued to access the I²C bus.

Another great feature is that when a thread is performing an I²C communication, which is really slow, it blocks letting another thread use the CPU. Yet this feature is currently disabled in my driver because at 400 kHz I²C speed it took the PIC24FJ32GB002 more time to dispatch a thread than to wait for the communications to end. Thus it is only useful for 100 kHz communications or for faster microcontrollers.

For simplicities sake, the driver does only detect and report comunication errors, but does no handle them. It is up to the user to do this.

I have been using this code for a while, and I am fairly pleased whith it. Yet there could still remain some hidden bugs or optimization possibilities. Any feedback is highly welcome.



Introduction



Inter Inter Circuit

Inter Inter Circuit (I²C) is a low speed serial communication bus widely supported by most sensors used on multirotors. All communications have to be started and controlled by a master, while the slave nodes only answer with a data byte or and acknowledge bit when polled.

The net is full of really great tutorials of how I²C works, even the PIC24FJ family datasheet features a great explanation. If you want to learn more about I²C I encourage you to read them.


PIC24FJ

The PIC24FJ is a 16bit microcontroller family by Microchip. It is not the fastest microcontroller, but therefore very versatile. They feature most of everydays microcontroller uses in modules, like serial communication, pulse width modulation (PWM) and many others. That means that code bit-banging belongs to the past.


FreeRTOS

FreeRTOS is a tiny yet powerfull free Real Time Operating System (RTOS) lightweight enough to run on most microcontrollers. It is highly configurable and fairly easy to set up. A quick online search should yield many how-tos, and the example codes shipped with the FreeRTOS code are also a great starting point.



API



Driver notes

The PIC24FJ32BG002 has two I²C interfaces, one is called I2C1 and the other I2C2. This driver is written so that the second interface acts as master, but this behavior can be easily changed by replacing "i2c2" in the code by "i2c1".

Configuration

All configuration is done with #defines in i2c2.h There you can select the baud-rate prescaler value (to generate the I²C clock), interrupt priority and module configuration.


Functions

  • i2c2_init()

Initializes al variables and hardware needed for I2C-2 to communicate as master.


  • i2c2_write_registers(uint8_t dir, uint8_t reg, unsigned char * buffer, uint8_t size )

Write I2C registers 
Returns 0: Success
Returns -1: Error
dir: Device direction without any bit shifting 
reg: Starting register from where to write on 
buffer: pointer to buffer which contains data to write 
size: size of buffer (number of bytes to send)


  • i2c2_read_registers(uint8_t dir, uint8_t reg, unsigned char * buffer, uint8_t size )

Read I2C registers
 

Returns 0: Success
Returns -1: Error
 

dir: Device direction without any bit shifting
reg: Starting register from where to read on 
buffer: pointer to buffer where received data will be stored
size: size of buffer (number of bytes to read)


Code



I still have no file hosting service. So please pardon me for posting the plain text. I am way to lazy to fix it now ;)

i2c2.h


   /*
    * i2c2.h - I2C driver for PIC24FJ + FreeRTOS
    * Copyright (C) 2014 Andres Gongora

    * <https://yalneb.blogspot.com>
    *
    * This program is free software: you can redistribute it and/or modify
    * it under the terms of the GNU General Public License as published by
    * the Free Software Foundation, either version 3 of the License, or
    * (at your option) any later version.
    *
    * This program is distributed in the hope that it will be useful,
    * but WITHOUT ANY WARRANTY; without even the implied warranty of
    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    * GNU General Public License for more details.
    *
    * You should have received a copy of the GNU General Public License
    * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    */

#ifndef I2C2_H_INCLUDED
#define I2C2_H_INCLUDED

#include <i2c.h>                //libreria para I2C
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
#include "stdint.h"



/**///////////////////////////////////////////////////////////////////////////////////////////////
//    CONFIGURATION
///////////////////////////////////////////////////////////////////////////////////////////////**/

#define I2C2_BRG     37            //BRG for I2C=100KHz. Depends on oscilaltor speed
#define I2C2_CON     0x9000        // Configuration. See I2CxCON register description.
#define MI2C2_IP     4                //ISR Priority interrupcion (Default=4)
#define I2C2_INT_WAIT_TIME 100



/**///////////////////////////////////////////////////////////////////////////////////////////////
//    CONSTANTS
///////////////////////////////////////////////////////////////////////////////////////////////**/

#define I2C2_STAT_MASK     0xC41B            #define I2C_STAT_IGNORE    0xFFFF            // Dont perform I2C_STAT check
#define I2C_STAT_START    I2C_STAT_IGNORE    // Start -> Ignore as previuos comunications could have ended with NACK   
#define I2C_STAT_RE        0x0008            // ACK
#define I2C_STAT_ACK    0x0008            // ACK           
#define I2C_STAT_NACK    0x0008            // NACK       
#define I2C_STAT_DIR    0x0008            // Device dir + Read/Write
#define I2C_STAT_REG    0x0008            // Register dir
#define I2C_STAT_TX        0x0008            // Send data
#define I2C_STAT_RX        0x000A            // Receive data
#define I2C_STAT_STOP    I2C_STAT_IGNORE    // Stop -> Ignore for error handling   

#define _ISR_PSV __attribute__((interrupt, auto_psv))       
#define _ISR_NO_PSV __attribute__((interrupt, no_auto_psv))   



/**///////////////////////////////////////////////////////////////////////////////////////////////
//    DECLARATIONS
///////////////////////////////////////////////////////////////////////////////////////////////**/

void i2c2_ini(void);
/*    Initializes al variables and hardware needed for I2C-1 comunications MASTER
*/


inline void i2c2_int(void);
/*    Interrupt handling
*/


   
short i2c2_write_registers(uint8_t dir, uint8_t reg, unsigned char * buffer, uint8_t size );
/*    Write I2C registers
   
    Returns 0: Success
    Negative: Error

    dir: Device direction without any bit shifting
    reg: Starting register from where to write on
    buffer: pointer to buffer which contains data to write
    size: size of buffer (number of bytes to send)
*/

   
short i2c2_read_registers(uint8_t dir, uint8_t reg, unsigned char * buffer, uint8_t size );
/*    Read I2C registers

    Returns 0: Success
    Negative: Error

    dir: Device direction without any bit shifting
    reg: Starting register from where to read on
    buffer: pointer to buffer where received data will be stored
    size: size of buffer (number of bytes to read)
*/

   
inline unsigned char i2c2_wait(uint16_t expected_stat);
/*    After each command given to the I2C interface, the program should wait for an interrupt.
    This function waits for ISR using bsem_I2C2_int.
    After unblocking from the semaphore this function checks the I2C_STAT register against expected_stat

    Return 0: OK
    Return -1: Error
*/

#endif//I2C2_H_INCLUDED



i2c2.c



   /*
    * i2c2.c - I2C driver for PIC24FJ + FreeRTOS
    * Copyright (C) 2014 Andres Gongora
    * <https://yalneb.blogspot.com>
    *
    * This program is free software: you can redistribute it and/or modify
    * it under the terms of the GNU General Public License as published by
    * the Free Software Foundation, either version 3 of the License, or
    * (at your option) any later version.
    *
    * This program is distributed in the hope that it will be useful,
    * but WITHOUT ANY WARRANTY; without even the implied warranty of
    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    * GNU General Public License for more details.
    *
    * You should have received a copy of the GNU General Public License
    * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    */
//### INCLUDE ##################################################################################

#include "i2c2.h"                //cabecera

#include <i2c.h>                //libreria para I2C
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
#include "stdint.h"


//### VARIABLES ################################################################################
SemaphoreHandle_t msem_i2c2_mutex;    // Semaphore for read/write operations
SemaphoreHandle_t bsem_i2c2_int;    // Wait for ISR semaphore




//##### Functions ##############################################################################

/**///////////////////////////////////////////////////////////////////////////////////////////////
//    vI2C2_ini
///////////////////////////////////////////////////////////////////////////////////////////////**/
void i2c2_ini(void)
{
    IdleI2C2();                    //Wait in case module is busy

    // Configure pin direction
    //WARNING: Make sure JTAG is disabled
    _TRISB2=1;    // I2C2 is on Port B
    _TRISB3=1;   

   
    // Periferico
    uint8_t dummy = I2C2RCV;    //Vaciar basura del registro de entrada   
    I2C2CON = 0;                //Configurar dispositivo inicialmente apagado (MSB=0)
//    I2C2STAT = 0;
    I2C2BRG = I2C2_BRG;            //Baud rate (ver #defines)
    I2C2CON = I2C2_CON;            //Habilitar dispositivo (MSB=1)
    while(I2C2STATbits.TRSTAT || (I2C2CON & 0x1F));                               

           
    // Enable ONLY when expected -> I2C2_wait
    // Interrupt enable
//    _MI2C2IP = MI2C2_IP;        //Prioridad interrupcion (Default=4)   
//    _MI2C2IF = 0;                //Borrar posible basura de IF
//    _MI2C2IE = 1;


    // Create semaphore
    msem_i2c2_mutex = xSemaphoreCreateMutex();    // Only one task can be reading/writing at a time
//    vSemaphoreCreateBinary(bsem_i2c2_int);        // To wait for ISR
//    xSemaphoreTake(bsem_i2c2_int,(TickType_t)2);// Initialize it locked
}//I2C2_ini




/**///////////////////////////////////////////////////////////////////////////////////////////////
//    sI2C2_write_registers
///////////////////////////////////////////////////////////////////////////////////////////////**/   
short i2c2_write_registers(uint8_t dir, uint8_t reg, unsigned char * buffer, uint8_t size )
{
    int i=0;

    // Mutex (get)
    xSemaphoreTake(msem_i2c2_mutex, portMAX_DELAY);    // Wait for I2C to become free

    // Start
    StartI2C2();
    if(i2c2_wait(I2C_STAT_START) != 0) return -1;

    // Send device dir
    I2C2TRN = (dir <<1);                            // Send device dir + write command (0)
    if(i2c2_wait(I2C_STAT_DIR) != 0) return -1;

    // Send register dir
    I2C2TRN = reg;                                    // Send register address
    if(i2c2_wait(I2C_STAT_REG) != 0) return -1;

    // Send data
    for(i=0; i < size; i++)               
    {
        I2C2TRN = buffer[i];   
        if(i2c2_wait(I2C_STAT_TX) != 0) return -1;   
    }

    // Stop
    StopI2C2();
    if(i2c2_wait(I2C_STAT_STOP) != 0) return -1;

    // Mutex (release)
    xSemaphoreGive(msem_i2c2_mutex);                // Release mutex
    return 0;

}


/**///////////////////////////////////////////////////////////////////////////////////////////////
//    sI2C2_read_registers
///////////////////////////////////////////////////////////////////////////////////////////////**/   
short i2c2_read_registers(uint8_t dir, uint8_t reg, unsigned char * buffer, uint8_t size )
{
    int i=0;

    // Check that input data is valid
    if(size <= 0) return -1;

    // Mutex (get)
    xSemaphoreTake(msem_i2c2_mutex, portMAX_DELAY);    // Wait for I2C to become free
    IdleI2C2();    //Just in case...

    // Start
    StartI2C2();
    if(i2c2_wait(I2C_STAT_START) != 0) return -1;

    // Send device dir
    I2C2TRN = (dir <<1);                            // Send device dir + write command (0)
    if(i2c2_wait(I2C_STAT_DIR) != 0)     return -1;

    // Send register dir
    I2C2TRN = reg;                                    // Send register address
    if(i2c2_wait(I2C_STAT_REG) != 0)     return -1;

    // Send restart
    I2C2CONbits.RSEN = 1;
    if(i2c2_wait(I2C_STAT_RE) != 0)    return -1;

    // Send device dir +  read command
    I2C2TRN = (dir <<1) + 1;                        // Send device dir + read command (1)
    if(i2c2_wait(I2C_STAT_DIR) != 0)     return -1;

    // Receive data
    // All but last byte
    for(i=0; i < size-1; i++)                        // For all but the last byte           
    {
        I2C2CONbits.RCEN = 1;                         // Enable reception
        if(i2c2_wait(I2C_STAT_RX) != 0)    return -1;
        AckI2C2();                                    // Send ACK
        buffer[i] = I2C2RCV;                        // Save data   
        if(i2c2_wait(I2C_STAT_ACK) != 0)     return -1;
    }
    // Last byte
    I2C2CONbits.RCEN = 1;                             // For the last byte
    if(i2c2_wait(I2C_STAT_RX) != 0)     return -1;
    NotAckI2C2();                                    // Send NACK
    buffer[size-1] = I2C2RCV;                        // Save data
    if(i2c2_wait(I2C_STAT_NACK) != 0) return -1;

    // Stop
    StopI2C2();
    if(i2c2_wait(I2C_STAT_STOP) != 0) return -1;

    // Mutex (release)
    xSemaphoreGive(msem_i2c2_mutex);                // Release mutex
    return 0;
}


/**///////////////////////////////////////////////////////////////////////////////////////////////
//    i2c2_wait
///////////////////////////////////////////////////////////////////////////////////////////////**/
inline unsigned char i2c2_wait(uint16_t expected_stat)
{
    uint8_t dummy;

    //--- WITH ISR -------------------------------------------------------------------------------
/*    // Wait for ISR
    if( bsem_i2c2_int != NULL)
    {
        if( xSemaphoreTake(bsem_i2c2_int, I2C2_INT_WAIT_TIME) == pdTRUE )
        {
            while(I2C2STATbits.TRSTAT );
   
            // ISR arrived and unlocked semaphore
            if((I2C2STAT == expected_stat) || (expected_stat == I2C_STAT_IGNORE))
            {
                return 0;            // Everything went as expected
            }
        }
    }*/
 
// --- WITHOUT ISR ---------------------------------------------------------------------------   
    // This PIC is not fast enough to exploit the CPU while I2C is busy. Task yielding takes
    // longer than waiting while I2C is busy.

    // Wait while busy
//    TickType_t i2c2_started_at = xTaskGetTickCount();
    while((I2C2STATbits.TRSTAT || (I2C2CON & 0x1F)));// && (i2c2_started_at == xTaskGetTickCount()));

    // ISR arrived and unlocked semaphore
    if((I2C2STAT == expected_stat) || (expected_stat == I2C_STAT_IGNORE))
        return 0;            // Everything went as expected
           

    // --- ERROR ---------------------------------------------------------------------------------
    // If execution comes to this point something went wrong
    // Either waiting for the semaphore timed out
    // or I2C-STAT after INT does not match the expected value
//    I2C2STAT = 0;                        // Eliminate all error flags (which are writable)
    dummy = I2C2RCV;                    // Empty reception register to avoid further error
    StopI2C2();                            // Send a stop

    IdleI2C2();
//    _MI2C2IE = 0;                        // Disable INT

//    i2c2_ini();                            // Restart module
//    I2C2CON = 0;                        // Turn off
    I2C2STAT = 0;
//    I2C2BRG = I2C2_BRG;                    // Baud rate
    I2C2CON = I2C2_CON;                    // Configure
    while(I2C2STATbits.TRSTAT || (I2C2CON & 0x1F));               

    xSemaphoreGive(msem_i2c2_mutex);    // Release mutex
   
    return -1;                            // Signal error   
}


/**///////////////////////////////////////////////////////////////////////////////////////////////
//    I2C ISR
///////////////////////////////////////////////////////////////////////////////////////////////**/
void _ISR _MI2C2Interrupt(void)
{
    _MI2C2IF = 0;        //Disable interrupt flag
    while(I2C2STATbits.TRSTAT );
    i2c2_int();       
}



/**///////////////////////////////////////////////////////////////////////////////////////////////
//    i2c2_int
///////////////////////////////////////////////////////////////////////////////////////////////**/
inline void i2c2_int (void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
  
    // Unblock I2C2 interrupt semaphore
    xSemaphoreGiveFromISR( bsem_i2c2_int, &xHigherPriorityTaskWoken );
       
    // If xHigherPriorityTaskWoken was set to true then yield to higher priority task
      if( xHigherPriorityTaskWoken != pdFALSE )
        taskYIELD();
}


4 comments :

  1. hi,
    many thanks for this great work.
    can you also specify the source or article relating this source code for further information ?

    ReplyDelete
  2. @Sabotage

    There is no original source but me. I wrote it from scratch using as reference only the PIC24-family manual and the help section for FreeRTOS. Both of which can be found at their respective homepages and I highly recommend.

    The reason there are code snippets written in Spanish is because I reused code I wrote several years ago, before I started commenting in English only.

    ReplyDelete
  3. The use of un-protected "while" in any RTOS code is a bad idea when using I2C.. Especially in interrupt routines...
    This could lead to system hangs....

    ReplyDelete
    Replies
    1. Oh. I was not aware of this. Any suggestion on how to aproach it instead? I started quite recently to play around with FreeRTOS

      Delete