MPLAB Xpress: Altitude Click demo

Mar 25, 2016 0 comments

The Altitude Click manufactured by MikroElektronika is an altimeter/barometer implemented with an MPL3115A2 sensor from NXP. It provides 20-bit resolution measurements of altitude and atmospheric pressure, with a 20kPa to 110 kPa range. Altitude measurement has a 30cm precision (1.5Pa). It also has an on-board thermometer with a 12-bit resolution. Communication is done via I2C interface, and a programmable interrupt system can be used.


Altitude click from MikroElektronika

In this story I will show you how to use the Altitude click with an MPLAB Xpress Evaluation Board, with code in XC8. In this example we read the pressure and temperature and we determine the altitude, and the results are displayed using a terminal program running on the PC (I use putty).

To create the code I will follow the same steps as in my previous SHT1x example, and I will use the MPLAB Xpress Code Configurator to configure the UART and I2C interfaces.

In this part we go through all the steps required to create a new PIC16F18855 project and we are now in front of the MCC window. First, we configure the system clock to 1MHZ and we set in CONFIG4 register the option “Low voltage programming enable”. Then, from the “Device resources” area we add the EUSART and MSSP2 peripherals.

Then we configure EUSART: baud rate is set to 19200, and we check “Enable transmit” and “Redirect STDIO to EUSART”. There is nothing special here, this configuration is used in almost all projects that use PC terminal to display messages.

MCC: EUSART configuration


To configure I2C communication we set MSSP2 to I2C master mode, Enable I2C, and we leave everything else to default settings.

MCC: MSSP2 configuration

We have configured the peripherals and now it’s time to configure the pin module. In the right pin view window we set EUSART RX to pin RC1 and EUSART TX on pin RC0 by placing the required locks. Then we assign I2C2 SCL line to pin RC4, and I2C2 SDA lline to pin RC3, by clicking on both input and output locks, like in the picture below.

MCC: I2C2 and EUSART pin assignements

Back to the pin manager we leave only the TX pin set as output and we uncheck everything else.

MCC: pin configuration




At this stage we get two warnings: On pin “RC3” TRIS bit is “input” but function “SDA2” requires it to be “output” and On pin “RC4” TRIS bit is “input” but function “SCL2” requires it to be “output”. All you have to do is ignore these warnings, the code will run fine.

I know that ignoring compiler warnings is a big no-no, but this is not the case. The explanation here lies in the way the Peripheral Pin Select (PPS) module works: for I2C2 module we are allowed to have different pins for SDA input and SDA output, and these pins are controlled by different registers. Input pin is controlled by register SSP2DATPPS, while output pin is given by register RC3PPS. Same happens with the SCL2 line, with input pin controlled by SSP2CLKPPS and output pin given by register RC4PPS. For more information see pages 241–250 of PIC16F18855 datasheet Rev.C.

Finally, we go to the Interrupt module and we enable I2C2 interrupts. We get here a notice that we have to enable the global and the peripheral interrupts in the main code — this warning should be respected, or else the code won’t work.

MCC: Interrupt configuration

With this we are done with MPLAB Xpress Code Configurator and we can click on “Generate” button to create the code and return to MPLAB Xpress window.

Before going any further let’s take a quick look at the pin_manager.c file:

RC3PPS = 0x0017;   //RC3->MSSP2:SDA2;
RC4PPS = 0x0016;   //RC4->MSSP2:SCL2;
SSP2DATPPSbits.SSP2DATPPS = 0x0013;   //RC3->MSSP2:SDA2;
SSP2CLKPPSbits.SSP2CLKPPS = 0x0014;   //RC4->MSSP2:SCL2;

We see here the values for the SSP2DATPPS , SSP2CLKPPS, RC3PPS and RC4PPS registers I mentioned before. If you are curious about what those values mean, look into the PIB16F18855 datasheet, tables 13–2 and 13–3.

In this blog post I will try to use the I2C communication functions generated by the MPLAB Xpress Code Configurator. These functions are:
/**
  Section: Interface Routines
*/

//Initializes the MSSP instance
void I2C2_Initialize(void);

//Handles one i2c master write transaction with the supplied parameters.
void I2C2_MasterWrite(
                                uint8_t *pdata,
                                uint8_t length,
                                uint16_t address,
                                I2C2_MESSAGE_STATUS *pstatus);

// Handles one i2c master read transaction with the supplied parameters.
void I2C2_MasterRead(
                                uint8_t *pdata,
                                uint8_t length,
                                uint16_t address,
                                I2C2_MESSAGE_STATUS *pstatus);

//Inserts a list of i2c transaction requests into the i2c transaction queue.
void I2C2_MasterTRBInsert(
                                uint8_t count,
                                I2C2_TRANSACTION_REQUEST_BLOCK *ptrb_list,
                                I2C2_MESSAGE_STATUS *pflag);

//This function populates a trb supplied by the calling function
//with the parameters supplied by the calling function.
void I2C2_MasterReadTRBBuild(
                                I2C2_TRANSACTION_REQUEST_BLOCK *ptrb,
                                uint8_t *pdata,
                                uint8_t length,
                                uint16_t address);

//This function populates a trb supplied by the calling function
//with the parameters supplied by the calling function.
void I2C2_MasterWriteTRBBuild(
                                I2C2_TRANSACTION_REQUEST_BLOCK *ptrb,
                                uint8_t *pdata,
                                uint8_t length,
                                uint16_t address);

//This function returns the empty status of the Master queue.
bool I2C2_MasterQueueIsEmpty(void);

//This function returns the full status of the Master queue.

bool I2C2_MasterQueueIsFull(void);

void I2C2_BusCollisionISR( void );

void I2C2_ISR ( void );
We find here Transaction Block Requests, defined as a structure:
typedef struct
{
    uint16_t  address;          // Bits <10:1> are the 10 bit address.
                                // Bits <7:1> are the 7 bit address
                                // Bit 0 is R/W (1 for read)
    uint8_t   length;           // the # of bytes in the buffer
    uint8_t   *pbuffer;         // a pointer to a buffer of length bytes
} I2C2_TRANSACTION_REQUEST_BLOCK;
We also notice status messages defined as:
typedef enum
{
    I2C2_MESSAGE_COMPLETE,
    I2C2_MESSAGE_FAIL,
    I2C2_MESSAGE_PENDING,
    I2C2_STUCK_START,
    I2C2_MESSAGE_ADDRESS_NO_ACK,
    I2C2_DATA_NO_ACK,
    I2C2_LOST_STATE
} I2C2_MESSAGE_STATUS;

If you ever programmed in Arduino IDE or in MikroC for PIC, you are probably used to work with a nicer set of I2C functions. In fact, I find myself easy to switch between Arduino and MikroC as most functions are quite similar.

in XC8 things are tougher. The I2C communication functions are more complex, and we have those TRB’s and status messages that have no correspondence in other popular compilers. Besides that examples in the i2c2.h file, documentation on using I2C is quite scarce, which doesn’t make things easy.

Finally, I ended up by using the provided I2C2 functions as a basis to create my own functions for reading and writing to the MPL315A2 sensor.

Writing was the easy part: we send the device address, then the register where we want to write then the data to be written. The function is:
// Writes one byte to the register indicated by reg_address of the I2C slave
uint8_t I2C2_write (uint8_t device_address, uint8_t reg_address, uint8_t wr_data){
    I2C2_MESSAGE_STATUS status = I2C2_MESSAGE_PENDING;
    uint8_t         write_buffer[2];
    write_buffer[0] = reg_address;
    write_buffer[1] = wr_data;
    I2C2_MasterWrite(&write_buffer, 2, device_address, &status);  // pointer la valoarea wr_data
    while(status == I2C2_MESSAGE_PENDING);
    return (status == I2C2_MESSAGE_COMPLETE);
}
This function takes as input arguments the device_address, the register address and the data to be written. The device_address is in 7-bit format while the register_address and wr_data are of byte format.

What happens in this function is that we create a buffer containing the register address and the data to be written, then we pass that array to the I2C2_MasterWrite function, indicating the device address and the number of bytes to be written — which is 2 in our case. We then check the status messages to see if the message is still pending or was sent successfully.

Reading a byte from an I2C slave is a bit more complex: first we have to perform a write cycle, sending the address of the register to be read, then we perform a read cycle. My initial attempt was to use the I2C2_MasterRead function, but it didn’t work:
// Wrong I2C read function
// Inserts a stop command in the middle of I2C transaction
uint8_t I2C2_bad_read (uint8_t device_address, uint8_t reg_address, uint8_t *pData)
{
    I2C2_MESSAGE_STATUS status = I2C2_MESSAGE_PENDING;
    I2C2_MasterWrite(&reg_address, 1, device_address, &status);
    while(status == I2C2_MESSAGE_PENDING);      // blocking
    I2C2_MasterRead(&rd_data, 1, device_address, &status);
    while(status == I2C2_MESSAGE_PENDING);      // blocking
    return (status == I2C2_MESSAGE_COMPLETE);
}


What happens with this function is that it inserts a STOP command after the write completes, and then issues a new START for the read phase (waveform taken with an 8 channel logic analyzer from Saleae).

XC8 — I2C write and read


To fix this issue I wrote another function, inspired from Microchip’s code examples:

// Reads one byte from the register indicated by reg_address of the I2C slave
uint8_t I2C2_read (uint8_t device_address, uint8_t reg_address, uint8_t *pData)
{
    I2C2_MESSAGE_STATUS status = I2C2_MESSAGE_PENDING;
    static I2C2_TRANSACTION_REQUEST_BLOCK trb[2];
    // Checks if there is any more space in TRB queue
    while(I2C2_MasterQueueIsFull() == true);    // blocking
    I2C2_MasterWriteTRBBuild(&trb[0], &reg_address, 1, device_address);
    // Checks if there is any more space in TRB queue
    while(I2C2_MasterQueueIsFull() == true);    // blocking
    I2C2_MasterReadTRBBuild(&trb[1], pData, 1, device_address);
    I2C2_MasterTRBInsert(2, &trb[0], &status);
    while(status == I2C2_MESSAGE_PENDING);      // blocking
    return (status == I2C2_MESSAGE_COMPLETE);
}

In this function I first build a TRB for writing, which sends the device_address and the register address. Then I create another TRB which reads one byte from the device address. Both TRB’s are combined and sent together using I2C2_MasterTRBInsert. The result is:

New I2C read

The signal diagram shows the write of the device address and register, followed by a repeated start, then by the read command and we see that the slave is sending data back. The NACK before the end of communication tells the slave to stop sending and to wait for STOP.

In both working functions I made some simplifying assumptions, such as not checking for all possible status messages, or by not checking the queue status always. There is also no retry if communication fails. While there is only one master and only one slave there should be no problem. However, one should regard these functions as “education grade”, and if you wish to use them in a production environment you have to cover all the possible situations.

Finally, we have working I2C read and write functions, and we can go to the main code:

 


Comments

Related Posts

{{posts[0].title}}

{{posts[0].date}} {{posts[0].commentsNum}} {{messages_comments}}

{{posts[1].title}}

{{posts[1].date}} {{posts[1].commentsNum}} {{messages_comments}}

{{posts[2].title}}

{{posts[2].date}} {{posts[2].commentsNum}} {{messages_comments}}

{{posts[3].title}}

{{posts[3].date}} {{posts[3].commentsNum}} {{messages_comments}}

Recent Comments

Contact Form