Pages

Interface DS1307 RTC with PIC16f877 through I2C

Previous post introduce you the I2C communication protocol in briefly and present a complete set of functions to perform I2C communication as a 'master' (from hi tech C compiler). This post will be dedicated to introduce a RTC(Real Time Clock) IC DS1307 and a simple code to perform read/write operations on RTC through I2C. This code also implemented using Hi tech C compiler and PIC16f877 as the MCU.

What is RTC? yaaa RTC stands for Real Time Clock. Most of the devices those maintaining a clock for their functions use these kind of ICs.Also DS1307 has its own capability to use backup battery(3V coin) power when main power source shutdown.
 
You can download  Datasheet for DS1307.

The features can be listed bellow,

  • Real-time clock (RTC) counts seconds, minutes, hours, date of the month, month, day of the week, and year with leap-year compensation valid up to 2100
  • 56-byte, battery-backed, nonvolatile (NV) RAM for data storage
  • Two-wire serial interface (I2C)
  • Programmable square wave output signal
  • Automatic power-fail detects and switches circuitry
  • Consumes less than 500nA in battery backup mode with oscillator running
  • Optional industrial temperature range: -40°C to +85°C
  • Available in 8-pin DIP or SOIC

Most significant thing in the IC is the battery backup power because if it a real time clock it should be run always to get the real time from it. Therefore you do non need to worry about clock it will run independently from your MCU.


Source code for DS1307 RTC

So let's move to source code. This code uses the functions implemented on previous post. Therefore it is better to download those codes and create a header file for those functions so you can re-use them latter. Don't worry a complete source code can be found at the end of this post. Here DS1307 act as a slave on the bus and our MCU act as the master on the bus.

Data Write - Slave Receiver Mode
First let's try to write a function to write a byte to DS1307's EEPROM. I refer following diagram which I grab from DS1307's datasheet to code this function.


So lets implement this diagram into code!

void ds1307Write(unsigned char address, unsigned char data){
    i2CStart();//start the i2c bus
    i2CSend(208);//send DS1307 write command, DS1307 address + direction bit(R/nW) 1101000 + 0 = 11010000 = 208
    i2CSend(address);//send the address that you need to write data on.
    i2CSend(data);//write data on the address
    i2CStop();//close the I2C bus
}

Code explained

i2CStart();//start the i2c bus

This is the function call which we create in previous post to send start condition through I2C bus. So this code is to send the start condition to slave device.


i2CSend(208);//send DS1307 write command, DS1307 address + direction bit(R/nW) 1101000 + 0 = 11010000 = 208

If you can remember, I2C communication can be done by sending 7 bits wide address and R/W bit immediately following start condition on the bus. So this is the sending of address and the direction bit to the DS1307. The address for DS1307 is 1101000 (in binary) then the direction bit R/W (1 for read 0 for write). Then whole byte is the concatenated bits of address and R/W bit,

1101000 + 0 = 11010000 = 208 (in decimal)


i2CSend(address);//send the address that you need to write data on.

Then you need to send address which you want to write data on EEPROM of DS1307.


i2CSend(data);//write data on the address

Then the data to be written on above specified address.


i2CStop();//close the I2C bus

Then stop the I2C communication.


Data Read—Slave Transmitter Mode
Now the read function. This will be some what complicated, because you need to point the address where you want to read from before initiate the read operation. I refer following diagram from datasheet to implement the function. This function will return a byte at once which is located on address specified.



unsigned char ds1307Read(unsigned char address){
    unsigned char data;

    i2CStart();//send start signal on I2C bus
    i2CSend(208);//send DS1307 dummy write command to get the pointer to our location
                //DS1307 address + direction bit(R/nW) 1101000 + 0 = 11010000 = 208
    i2CSend(address);//set the address pointer
    i2CRestart();//send stop+start signal to bus
    i2CSend(209);//send read command to DS1307
    data = i2CRead();//read the buffer for received data
    i2CStop();//close the i2c bus
    return data;
}

Code explained
Initially send the start condition to the bus as explained in previous function to initiate the I2C bus for communication.


i2CSend(208);//send DS1307 dummy write command to get the pointer to our location
             //DS1307 address + direction bit(R/nW) 1101000 + 0 = 11010000 = 208

Then we send a dummy write command to get the EEPROM's pointer to the location that we need to read from. Therefore this byte will send to the DS3107 with the write operation direction bit (same as previous function). But we do not write anything on it.


i2CSend(address);//set the address pointer

Then send the address that we need to point on to set the EEPROM's pointer to the location.


i2CRestart();//send stop+start signal to bus

As I explained earlier we are not going to write anything, only purpose to issue write operation is set the pointer to the location.Therefore send the 'repeated start' condition to the bus.


i2CSend(209);//send read command to DS1307

Now we can send the read operation to DS1307. The value can be calculated as follows,
DS1307 address (1101000) + direction bit (1 for read) = 1101001 = 209 (in decimal


data = i2CRead();//read the buffer for received data

Now you can read data from DS1307 and hold these data on "data" variable. If you have any doubt about these function which are calling in this post please refer previous post. Because, none of the function that we calling here are inbuilt functions.


i2CStop();//close the i2c bus
return data;


Then close the I2C bus by sending stop condition and return the data which read from DS1307's EEPROM

DS1307's memory
Now you have functions for read and write operations. Now let's try to extract data from DS1307. We will need its memory map to do that. Memory map is,


Now let's try to read seconds from the memory as you can see here the Seconds data contain in 3 parts. Those are CH bit, 10 Seconds and Seconds. If you read the datasheet, you can find the functionality of CH bit. It is the clock enable bit. If you set CH = 0 the clock is enabled and running, if you set CH = 1 the clock will be disable. I will explain 10 Seconds and Second thing from a example,

Let's say the seconds on the clock is 36, then,
10 Seconds  = 3 (011)
Seconds = 6 (110)
and let CH = 0

Now the byte on the 0th position is 0 + 011 + 110 = 0011110 (plus mean concatenate). So I think you got the trick or still complicated...! Don't worry I have created functions to get and set Seconds, Minutes, Hours (in 12 hour or 24 hour mode), and Day. You can code your own functions for other things (Date, month and year). Only thing is to understand the memory map of the IC and code it.

You can download a complete source code and test program at the end of this tutorial.
The complete code is,

/**
 * Function will read a byte from the @address location of the memory
 * @param address - address that you need to read
 * @return received data byte from DS1307
 */
unsigned char ds1307Read(unsigned char address){
    unsigned char data;

    i2CStart();//send start signal on I2C bus
    i2CSend(208);//send DS1307 dummy write command to get the pointer to our location
                //DS1307 address + direction bit(R/nW) 1101000 + 0 = 11010000 = 208
    i2CSend(address);//set the address pointer
    i2CRestart();//send stop+start signal to bus
    i2CSend(209);//send read commad to DS1307
    data = i2CRead();//read the buffer for received data
    i2CStop();//close the i2c bus
    return data;
}

/**
 * Write to DS1307's eeprom
 * @param address - address to write data (0 - 7)
 * @param data - 8bit data to be written
 */
void ds1307Write(unsigned char address, unsigned char data){
    i2CStart();//start the i2c bus
    i2CSend(208);//send DS1307 write command, DS1307 address + direction bit(R/nW) 1101000 + 0 = 11010000 = 208
    i2CSend(address);//send the address that you need to write data on.
    i2CSend(data);//write data on the address
    i2CStop();//close the I2C bus
}

/**
 * get seconds from DS1307's eeprom
 * @return seconds on DS1307
 */
unsigned char ds1307GetSec(){
    unsigned char sec10;
    unsigned char sec1;

    unsigned char bcdSec = ds1307Read(0);//get the 0th location data(CH + 10Sec + 1Sec)[in BCD format]

    bcdSec = bcdSec & 0b1111111;//ignore CH bit (ignore MSB)
    sec10 = bcdSec >> 4;//shift 4 bits right to ignore 1sec position(ignore 4 LSB)
    sec1 = bcdSec & 0b1111;//ignore 4 MSBs

    return sec10 * 10 + sec1;//return the seconds
}

/**
 * get minutes from DS1307's eeprom
 * @return minutes on DS1307
 */
unsigned char ds1307GetMin(){
    unsigned char min10;
    unsigned char min1;

    unsigned char bcdMin = ds1307Read(1);//get the 1st loaction's data(0 + 10Min + 1Min)[in BCD format]

    bcdMin = bcdMin & 0b1111111;//ignore MSB
    min10 = bcdMin >> 4;//shift 4 bit right to ignore 1min position(ignore 4 LSB)
    min1 = bcdMin &0b1111;

    return min10 * 10 + min1;//return the minnutes
}

/**
 * This function will return the hours in 24 hour format by reading from DS1307
 * @return hours from DS1307 in 24 hours format
 */
unsigned char ds1307GetHours(){
    unsigned char hour10;
    unsigned char hour1;
    unsigned char retHour;

    unsigned char bcdHour = ds1307Read(2);//get the 1st loaction's data(0 + 12 or 24 hour mode + 10hour(first bit) or PM/AM + 10hour(second bit) + 1hour)[in BCD format]
    bcdHour = bcdHour & 0b1111111;//ignore MSB(7th bit)
    if(bcdHour > 63){//is in 12 hours mode?
        bcdHour = bcdHour & 0b111111;//ignore MSB(6th bit)

        hour10 = (bcdHour & 0b11111) >> 4;//get the hour10 position by ignoring MSB(5th bit) and shift 4 bits to right
        hour1 = bcdHour & 0b1111;//get hour1 position by getting only 4 LSBs
        retHour = hour10 * 10 + hour1;//calculate the hours using hour10 and hour1

        if(bcdHour > 31){//is PM?
            if(retHour != 12)
                retHour = retHour + 12;
        }
    }else{
        bcdHour = bcdHour & 0b111111;//ignore MSB(6th bit)
        hour10 = bcdHour >> 4;//shift 4 bit to right to get 5th and 4th bits
        hour1 = bcdHour & 0b1111;//get 4 LSBs

        retHour = hour10 * 10 + hour1;//calculate the hours using hour10 and hour1
    }

    return retHour;
}

/**
 * reads the Day value from DS1307 memory
 * @return the number for the specific day (1=SUN, 2=MON,... 7=SAT)
 */
unsigned char ds1307GetDay(){
    return ds1307Read(3);//read value on 3rd location of DS1307 and return it
}

/**
 * set seconds to DS1307 with CH bit
 * @param newSec - seconds to be set (0 - 79)
 * @param chBit - Clock enable bit (0 = clock enable, 1 = clock disable)
 */
void ds1307SetSecond(unsigned char newSec, unsigned char chBit){
    unsigned char bcdNum;
    if(newSec > 79)
        return;//to avoid writing to CH when writing to second feild

    ds1307separateDigits(newSec);
    bcdNum = digitPlaceVal[1] << 4;//shift 4 bits left
    bcdNum = bcdNum | digitPlaceVal[0];//ORing with placeValue1

    //add CH bit
    if(chBit == 1)
        bcdNum = bcdNum | 0b10000000;

    ds1307Write(0, bcdNum);//write to sec
}

/**
 * Set DS1307 minutes
 * @param newMin - minutes to be set(0 - 127)
 */
void ds1307SetMinutes(unsigned char newMin){
    unsigned char bcdNum;
    if(newMin > 127)
        return;//to avoid overlimit

    ds1307separateDigits(newMin);
    bcdNum = digitPlaceVal[1] << 4;//shift 4 bits left
    bcdNum = bcdNum | digitPlaceVal[0];//ORing with placeValue1

    ds1307Write(1, bcdNum);//write to min
}

/**
 * Set DS1307 hours in 12 hour mode
 * @param newHour - hours in 12 hour mode (0 - 19)
 * @param pm_nAm - AM/PM bit (1 for PM, 0 for AM)
 */
void ds1307SetHours12(unsigned char newHour, unsigned char pm_nAm){
    unsigned char bcdNum;
    if(newHour > 19)
        return;//avoid overlimit

    ds1307separateDigits(newHour);
    bcdNum = digitPlaceVal[1] << 4;//place hour's placeValue10
    bcdNum = bcdNum | digitPlaceVal[0];//ORing with placeValue1

    bcdNum = bcdNum | 0b1000000;//set 12 hour mode
    if(pm_nAm)//PM?
        bcdNum = bcdNum | 0b100000;//set PM

    ds1307Write(2, bcdNum);//write to hours
}

/**
 * Set DS1307 hours in 24 hour mode
 * @param newHour - hours in 24 hour mode (0 - 29)
 */
void ds1307SetHour24(unsigned char newHour){
    unsigned char bcdNum;
    if(newHour > 29)
        return;//avoid overlimit

    ds1307separateDigits(newHour);
    bcdNum = digitPlaceVal[1] << 4;//place hour's placeValue10
    bcdNum = bcdNum | digitPlaceVal[0];//ORing with placeValue1

    ds1307Write(2, bcdNum);//write to hours
}

/**
 * Set day to DS1307 (range 0-7)
 * @param newDay - day to be set (1 = SUN, 2 = MON,...., 7 = SAT)
 */
void ds1307SetDay(unsigned char newDay){
    if(newDay > 7)
        return;//avoid overlimit

    ds1307Write(3, newDay);//write to day
}

/**
 * This function accept 2 digit number and separate it into 10 and 1 place value and assign to digit array
 *
digit[0] = place value 1
 *
digit[1] = place value 10
 * @param num2 - 2 digit number(0 - 99)
 */
void ds1307separateDigits(unsigned char num2){
    digitPlaceVal[1] = num2 / 10;
    digitPlaceVal[0] = num2 - (digitPlaceVal[1] * 10);
}


Download
A complete source code and test program.

Hi-tech C code for I2C communication

Before move to code I think it is better to write a brief introduction to I2C communication.

I²C (pronounced I-squared-C) created by Philips Semiconductors and commonly written as 'I2C' stands for Inter-Integrated Circuit and allows communication of data between I2C devices over two wires. It sends information serially using one line for data (SDA) and one for clock (SCL).There needs to be a third wire which is just the ground or 0 volts. There may also be a 5volt wire is power is being distributed to the devices. Both SCL and SDA lines are "open drain" drivers. What this means is that the chip can drive its output low, but it cannot drive it high. For the line to be able to go high you must provide pull-up resistors to the 5v supply. I2C bus can have one or more I2C devices connected.



Master and Slave
The devices on the I2C bus are either masters or slaves. The master is always the device that drives the SCL clock line. Slaves are the devices that respond to the master. A slave cannot initiate a transfer over the I2C bus, only a master can do that. There can be multiple slaves on the I2C bus. Also both slaves and masters can receive/send data over I2C. But always master initiate the data transfer over I2C bus.

Always I2C communication start with the start condition and terminate with the stop condition. Between these conditions you can transfer data between master and slave(s).

Addressing
Each slave device on the I2C bus should have a unique address. Then master can address these salve device individually for better communication between master and slave device. Always master initiate data transfer with the address to select a slave device on the bus for further communication.
Address byte contain 7bit address and data direction bit (R/W). This will initiate the specific slave device to be read or write operations.


START (S) and STOP (P) bits
START (S) and STOP (P) bits are unique signals that can be generated on the bus but only by a bus master.
This can be done at any time so you can force a restart if anything goes wrong even in the middle of communication.
START and STOP bits are defined as rising or falling edges on the data line while the clock line is kept high.
 And this is the only occurrence of falling or rising SDA line while SCL line in high. Therefore this can be captured by slave devices as start/stop condition.
Also microchip device support restart condition, which is simply send stop(P) condition and start(S) condition sequentially by the master.



Data
All data blocks are composed of 8 bits (1 Byte). The initial block has 7 address bits followed by a direction bit (Read or Write). Acknowledge bits are squeezed in between each block. Each data byte is transmitted MSB first including the address byte.

Acknowledge
The acknowledge bit (generated by the receiving device) indicates to the transmitter that the the data transfer was ok. Note that the clock pulse for the acknowledge bit is always created by the bus master.
The acknowledge data bit is generated by either the master or slave depending on the data direction. For the master writing to a slave the acknowledge is generated by the slave. For the master receiving data from a slave the master generates the acknowledge bit.

This acknowledge bit can  acknowledge(ACK) or not-acknowledge(NACK) depending on operations on the I2C bus.
  • To acknowledge the receiving device must put the SDA line into low with the next clock pulse.
  • To not-acknowledge  the receiving device must high SDA line with the next clock pulse (simply do nothing on the SDA line, then it will remain high) 
 When data receiving by the master not-acknowledge bit must generate by the master to indicate to slave, "there no need to send more data to me(master)". Then master must stop the I2C bus by sending STOP(P) condition.


Data transfer from master to slave



Data transfer from slave to master




I think now you have a brief idea about I2C communication. So let's move to code. This code was written in Hitech C for most popular PIC16f877 microcontroller which is developed by microchip Inc using its inbuilt MSSP(Master Synchronous Serial Port) unit.


Source code for complete I2C (Master) communication with PIC16f877


/**
 * Initialize the I2C module as a master mode

 * SCL - RC3
 * SDA - RC4
 * 100kHz I2C click at 4Mhz crystal
 */
void initializeI2C(){
    TRISC3 = 1;//make SCL as input
    TRISC4 = 1;//make SDA as intput

    //----------configs for SSPSTAT register------------
    SSPSTAT = 0b10000000;//SMP = 1 [Slew rate control disabled for standard speed mode (100 kHz and 1 MHz)]
    //--------------------------------------------------

    //---------- configs for SSPCON register------------
    //Synchronous Serial Port Mode Select bits
    //configs as I2C Master mode, clock = FOSC / (4 * (SSPADD+1))
    SSPM0 = 0;
    SSPM1 = 0;
    SSPM2 = 0;
    SSPM3 = 1;

    SSPEN = 1;//Enables the serial port and configures the SDA and SCL pins as the source of the serial port pins

    SSPOV = 0;//No overflow

    WCOL = 0;//No collision
    //--------------------------------------------------

    //---------- configs for SSPCON2 register-----------
    SSPCON2 = 0;//initially no operations on the bus
    //--------------------------------------------------

    SSPADD = 40;// 100kHz clock speed at 4Mhz cystal

    //----------PIR1-----------
    SSPIF = 0;//clear  Master Synchronous Serial Port (MSSP) Interrupt Flag bit

    //-----------PIR2-----------
    BCLIF = 0;//clear Bus Collision Interrupt Flag bit
}

/**
 * wait until I2C bus become idel
 */
void waitUntilIdel(){
    while(R_W || SEN || RSEN || PEN || RCEN || ACKEN){
        __delay_ms(1);
    }
}

/**
 * Send 8 bit(1 byte) through I2C bus
 * @param data - 8 bit data t be sent
 */
void i2CSend(unsigned char data){
    waitUntilIdel();
    SSPBUF = data;
    //while(BF) continue;// wait until complete this bit transmision
    waitUntilIdel();//wait until any pending transaction
}

/**
 * Send start condition to I2C bus
 */
void i2CStart(){
    waitUntilIdel();
    SEN = 1;//Initiate Start condition on SDA and SCL pins. Automatically cleared by hardware
    waitUntilIdel();
}

/**
 * Send stop condition to I2C bus
 */
void i2CStop(){
    waitUntilIdel();
    PEN = 1;//Initiate Stop condition on SDA and SCL pins. Automatically cleared by hardware.
    waitUntilIdel();
}

/**
 * Send restart condition to I2C bus
 */
void i2CRestart(){
    waitUntilIdel();
    RSEN = 1;//Initiate Repeated Start condition on SDA and SCL pins. Automatically cleared by hardware.
    waitUntilIdel();
}

/**
 * read the I2C bus
 * @return Read data from I2C slave
 */
unsigned char i2CRead(){
    unsigned char temp;

    waitUntilIdel();

    //configure MSSP for data recieving
    RCEN = 1;//Enables Receive mode for I2C

    waitUntilIdel();

    while(!BF);//wait for buffer full
    temp = SSPBUF;//read the buffer
    waitUntilIdel();//wait for any transaction
    return temp;//return the bufferd byte
}

/**
 * send acknowledge condition
 */
void i2CAck(){
    waitUntilIdel();
    ACKDT = 0;//Acknowledge Data bit(0 = Acknowledge)
    ACKEN = 1;//Initiate Acknowledge sequence on SDA and SCL pins, and transmit ACKDT data bit.Automatically cleared by hardware.
    waitUntilIdel();
}

/**
 * send not acknowledge condition
 */
void i2CNAck(){
    waitUntilIdel();
    ACKDT = 1;//Acknowledge data bit(1 = Not Acknowledge)
    ACKEN = 1;//Initiate Acknowledge sequence on SDA and SCL pins, and transmit ACKDT data bit.Automatically cleared by hardware.
    waitUntilIdel();
}


I think comments on each code line is better enough to understand these functions.
The MSSP module has three associated registers. These include a status register (SSPSTAT) and two control registers (SSPCON and SSPCON2). Above functions will configure these registers to use I2C communication in Master mode.

You can download complete source code with test program.

Next post will be a practical usage of I2C communication which use the RTC (Real Time Clock) IC DS1307.

Proteus VSM viewer to debug MPLABX projects

Hi all, Before few months earlier I heard Microchip released a new IDE to implement firmwares for their products. That is MPLABX. MPlabX has very nice features and tools to assist while you are coding, compiling, debugging, etc... If you familiar with Netbeans, it will be the great advantage to use MPLABX. Because MPLAB X is based on the Oracle Sponsored open-source NetBeans platform. Therefore supports many third-party tools, and is compatible with many NetBeans plug-ins. So try out it your self to feel the difference from MpLab8.

If you prefer to download new version of MPLABX click here

This post is not to present features of the MPLABX. So this article will introduce the way to work MPLABX with Proteus VSM viewer for debugging purposes. MPLAB8 has its inbuilt ability to work with Proteus VSM viewer. But there are a few more steps to integrate it with the MPLABX IDE. When you are working with Proteus VSM viewer in MPLABX, you will see how much easier to debug your code.

Step 1 - The first step is to install VSM viewer in MPLABX IDE.Open Plugins window by selecting Tools->Plugins from the menu bar. Then click on Available plugins, then you will see the Proteus VSM viewer from the list under 'MPLAB DBCore' category. (If proteus vsm viewer not listed on the list just press on Reload Catalog button to reload the list).

Then click on Install to install the plugin to IDE, the install wizard will guide you to install it successfully. Also you can update or remove you existing plugins from this window.

Step 2 - Create your project from MPLABX and select Proteus VSM viewer as debugging tool when it asked.As showed in following figure.

OR you can set Proteus VSM as the debugging tool for you existing project. Just goto project Properties by right click on your project. Then click on 'Config: [default]' category. Then select Proteus VSM from the list box positioned at right side of the window as shown below.

Step 3 - Now you can code your project and compile your project in debug mode. To do that right click on your project from the Projects window then select Debug from the drop down menu. Now this will lead to create files and folder hierarchy for debugging by IDE, if your code compile successfully. (This will terminate debugging session until we still do not create a ISIS design for our project. Just don't care those!)

Step 4 - Create the design file file for your project from the Proteus ISIS and select the .cof file which is located at \dist\default\debug folder as the program file for your micocontroller(in ISIS design). Then save the design in your project folder.

Step 5 - Now we should add our design file to MPLabX project. Go to project properties window and click on Proteus VSM Viewer category under Config: [default] category then select your design file from Design File Name and press on apply (you can let other configurations as it is under default environment).

Now you can set break points in your source code and Debug your project. Then MPLabX will automatically run the ISIS design simultaneously with your code.

Oh I forgot to say, it is better to only open your ISIS design from the Proteus ISIS while you debug and enable Remote Debug Monitor from Proteus ISIS (Debug->Use Remote Debug Monitor)

Then you can open both windows simultaneously and debug you project as you can see...