Thursday, June 30, 2016

I2C on the C2000 LaunchPad

Alright, let's see what the (only?) peripheral that doesn't have APIs in the peripheral driver library offers us - yeah, we're forced to combine the nasty software driver model with the direct register access model :(. Running short on any examples or whatsoever (literally the only example is a messy I2C EEPROM controlling program, but believe me, it's highly over-complicated), I had a fear that it will be long, complex and difficult task to use I2C without APIs; the C2000 isn't a simple MCU after all, there's so much to configure if we go by the manual way. However, after spending more than a day studying the I2C Module Reference Guide, I can now say that while it was a tough experience, I had no reason to fear.

While the first steps were indeed hard, I think I got everything right. My task is to read the object temperature my IR sensor is reading - this is similar to reading from an EEPROM: I first need to send the register address to read from, then using a repeated start condition, I need to read 3 bytes from the slave.

First of all, forget about that EEPROM example TI provided. It just doesn't worth the time trying to work upon that...

As usual, let's start from our code skeleton from a previous article. As I found out, in order to use the direct register access model, one needs to have a header linker file in addition to the main linker command file to link the peripheral structures to the proper locations within the memory map. Luckily, all we need to do is copying 'c:\ti\controlSUITE\device_support\f2802x\v230\f2802x_headers\cmd\F2802x_Headers_nonBIOS.cmd' to the project folder, next to the main linker command file, F28027.cmd. The compiler will automatically find and use it. Actually, it seems like a good idea to add the header linker file to the code skeleton as well, so I'll update the previous article with this information.

Back to the source code, we will need a GPIO handle in order to configure the pins; while at it, also add the I2C header (do note that since there are no APIs for it, there's no handle either):
#include "f2802x_common/include/gpio.h"
#include "f2802x_common/include/i2c.h"

GPIO_Handle myGpio;

void setup_handles() {
    ...
    myGpio = GPIO_init((void *) GPIO_BASE_ADDR, sizeof(GPIO_Obj));
}

Looking up the C2000 LaunchPad User's Guide to find out where to mux SDA and SCL, notice that we can reach them through either GPIO28/29 or GPIO32/33. Well, since GPIO28/29 is used by the on-board FTDI module on the LaunchPad, and we don't want to disable it, let's use GPIO32/33.
Notice the odd way the pins are wired on the LaunchPad:


Right, the SPI and the I2C pins are wired together by default, supposedly to retain compatibility with some booster packs. This isn't a problem unless one wants to use both I2C and SPI at the same time; even then, there some on-board markings (JP4-JP11) marking where to cut the line on the PCB in order to separate the two modules. Like said, this isn't a problem for us; we can simply connect our device to GPIO32/SDA (J6.7 or J2.6) and GPIO33/SCL (J6.8 or J2.7), configure I2C by software, and it's ready to go.

Back again to the software side, let's do the necessary muxing. F2802x Peripheral Driver Library User's Guide and System Control and Interrupts Reference Guide to the rescue! Notice that I also enabled the weak (~200 uA) internal pull-up; while this is not mandatory, it strengthens the pull-up a bit (which comes good on 3.3V). Of course, we still need to use external pull-up resistors (according to my testing, around 2K7, maybe even a little less).
GPIO_setPullUp(myGpio, GPIO_Number_32, GPIO_PullUp_Enable);
GPIO_setPullUp(myGpio, GPIO_Number_33, GPIO_PullUp_Enable);

GPIO_setQualification(myGpio, GPIO_Number_32, GPIO_Qual_ASync);
GPIO_setQualification(myGpio, GPIO_Number_33, GPIO_Qual_ASync);

GPIO_setMode(myGpio, GPIO_Number_32, GPIO_32_Mode_SDAA);
GPIO_setMode(myGpio, GPIO_Number_33, GPIO_33_Mode_SCLA);

We can now enable the clock  for the I2C peripheral:
CLK_enableI2cClock(myClk);

From now on, we need to switch to the direct register access model, and begin configuring the I2C peripheral, based on the I2C Module Reference Guide. While I provide a starting guide below, I highly recommend reading through this document for a detailed explanation of the various modes and registers.

Begin by configuring the I2C module clocks. Detailed information can be found in the reference guide, so I'll only cover the basics here. There are two clocks to configure.
In order to configure them, we need to put the I2C module to reset state by setting the IRS bit in the I2CMDR register to 0:
I2caRegs.I2CMDR.bit.IRS = 0;

The first clock to configure is the module clock. To meet all of the I2C protocol timing specifications, the module clock must be between 7 - 12 MHz. The formula to calculate it: module clock = CPU clock / (IPSC + 1).  So, since we configured the CPU to run at 60 MHz, setting IPSC to 5 results in a module clock of 10 MHz, which is perfect.
I2caRegs.I2CPSC.all = 5;

The master clock is more interesting - this is what appears on the SCL pin when the bus isn't idle. There are two values that we need to calculate: the high-time and the low-time duration of the clock signal. These can be calculated using the Tmod * (ICCH + d) and Tmod * (ICCL + d) formulas respectively, where Tmod = 1/module clock, and d = 5 (as described on page 34).
Since I wanted a compromise between stability and speed, I chosen both high-time and low-time to be 12.5 us, resulting in 25 us periodic time, equivalent to 40 kHz I2C frequency. I wanted to include oscilloscope shots here, but unfortunately, my ghetto scope is barely enough to see that the waveform is "about right". :(
I2caRegs.I2CCLKL = 120;
I2caRegs.I2CCLKH = 120;

Now that the clocks are configured, we can put the I2C module out of reset:
I2caRegs.I2CMDR.bit.IRS = 1;

The bus is now configured and ready to operate. Do note that there are dozens of additional options and modes available, but for standard I2C operations, the above configuration is enough. Time to read something!

First, we need to load the slave address (in my case, 0x5A) into the I2CSAR slave address register, and wait until a STOP condition presents on the bus by waiting for the STP bit of the I2CMDR status register (meaning the bus is not occupied). After that, we configure the module for master mode by setting the MST bit (beware that it resets after each stop condition!), and generate a START condition by setting the STT bit (which resets once the START condition is issued) of the status register.
// load slave address
I2caRegs.I2CSAR = 0x5A;

// wait for STOP condition
while (I2caRegs.I2CMDR.bit.STP != 0);

// master mode
I2caRegs.I2CMDR.bit.MST = 1;

// generate START condition
I2caRegs.I2CMDR.bit.STT = 1;

Now, we can send (write) the register address from which we want to read from to the slave device.
We begin by enabling repeat mode, in which a data byte is transmitted each time the I2CDXR register is written to, until a STOP or REPEATED START condition is generated, both of which ends repeat mode. We set the TRX bit to switch into transmit mode, and wait for the XRDY (transmit mode ready) bit of the status register, which means that the data transmit register is ready to accept new data.
// repeat mode
I2caRegs.I2CMDR.bit.RM = 1;

// transmit mode
I2caRegs.I2CMDR.bit.TRX = 1;

// wait for XRDY flag to transmit data
while (I2caRegs.I2CSTR.bit.XRDY != 1);

We can now load the byte we want to send, and wait for the transmission to complete by waiting either for an ACK or NACK (error) from the slave... note that for sending multiple bytes, this part can be put into a loop.
// load data into the transmit register
I2caRegs.I2CDXR = byte;

// wait for XRDY flag to transmit data or ARDY if we get NACKed
while (I2caRegs.I2CSTR.bit.XRDY != 1 || I2caRegs.I2CSTR.bit.ARDY != 1);

// if a NACK is received, clear the NACK bit and stop the transfer
if (I2caRegs.I2CSTR.bit.NACK == 1) {
    I2caRegs.I2CMDR.bit.STP = 1;
    I2caRegs.I2CSTR.all = I2C_CLR_NACK_BIT;
    return I2C_NACK_ERROR;
}

Okay, hopefully, the slave device responded with an ACK, and we can continue our read operation.
Now that the slave device knows what to send (what we want to read), we need to issue a REPEATED START condition by setting the STT bit and switch into non-repeat mode for reading (clear RM bit). In this mode, the I2C Data Count Register (I2CCNT) determines how many bytes to receive, so we set that as well (my slave device sends 3 bytes). Finally, we set the STP bit to automatically generate a STOP condition when the data counter counts down to 0 (this is why the data counter is useful), and switch to receive mode.
// issue repeated start
I2caRegs.I2CMDR.bit.STT = 1;

// non-repeat mode
I2caRegs.I2CMDR.bit.RM = 0;

// set data count
I2caRegs.I2CCNT = 3;

// generate STOP condition when the data counter counts down to 0
I2caRegs.I2CMDR.bit.STP = 1;

// receiver mode
I2caRegs.I2CMDR.bit.TRX = 0;

The last step is to, well, read the bytes the slave device sent us. To do so, we wait until the RRDY bit is set, which indicates that the data receive register (I2CDRR) is ready to be read, and then read it.
Since I'm reading 3 bytes, I put this part into a loop:
for (i = 0; i < 3; ++i) {
    // wait until the data receive register is ready to be read
    while (I2caRegs.I2CSTR.bit.RRDY != 1);

    data[i] = I2caRegs.I2CDRR;
}

That's it! Since we set the STP bit, a STOP condition will be automatically issued after reading the last byte, ending the transfer.

Once again, I'd like to highlight that I didn't use the full potential of the hardware at all; just like the I2C reference suggests, the MCU supports FIFO mode and interrupts for I2C as well, however, I didn't need any of these: I simply needed to poll my IR sensor periodically to read the object temperature. I think that for this task, using FIFO or interrupts would be an overkill.

I'd like to revisit this at some point to fill in any of the NACK error cases I might have missed, as the documentations doesn't really describe when and where to check for them... once I'm done with that, I might release the API I put together, for much easier usage of I2C on the C2000. Until then, simply concating the code snippers above should work.

8 comments:

  1. Hey! Great series on the f28027 by the way. I ran into an error with this project not working. I believe it was CLK_enableSciaClock(myClk) needing to be changed to CLK_enableI2cClock(myClk).

    ReplyDelete
  2. Hey, thanks for notifying me - fixed the typo :) I'd like to write a few more articles BTW - I just don't really have time at the moment to do it.

    ReplyDelete
  3. Understood Daniel! Since I fixed that issue I'm now trying to get interrupts working for i2c. Unfortunately, a lot of the material out there doesn't use the updated libraries and I'm struggling to get them working. Once I do get i2c interrupts going I'll be sure to forward you the code.

    ReplyDelete
  4. Thank you. Your code was really helpful in debugging my program.

    ReplyDelete
  5. could you please provide the direct register access model for this function: CLK_enableI2cClock(myClk)

    ReplyDelete
  6. Bonjour les gars, si vous avez besoin d'embaucher un vrai hacker pour surveiller / pirater le téléphone de votre partenaire à distance, échanger et doubler votre argent en quelques jours / semaines, ou pirater une base de données, le tout avec confidentialité garantie, contactez easybinarysolutions@gmail.com, ou whatsapp: +1 3478577580, ils sont efficaces et confidentiels.

    ReplyDelete
  7. Join the world’s largest community of ethical hackers and start hacking today! Be challenged and earn rewarding bounties. Learn more! https://www.hackerone.com/for-hackers/how-to-start-hacking

    ReplyDelete