AVR I2C (TWI) with a DS1307 RTC: TWBR, TWCR and TWSR Explained in C

I2C is a two-wire bus that lets one microcontroller talk to many chips using only a data line (SDA) and a clock line (SCL). Real-time clocks, EEPROMs like the 24C32, port expanders, OLED displays and most digital sensors use it. Atmel calls its I2C-compatible hardware the Two-wire Serial Interface (TWI), which is why the registers start with TW.

In this tutorial you will write a small polled TWI master driver for the ATmega328P and use it with the DS1307 real-time clock, found on the popular Tiny RTC modules. You will set the time once, then read hours, minutes and seconds every second and print them over the USART.

I2C Bus Basics

Both lines are open drain: devices can only pull them low, and pull-up resistors (typically 4.7 kΩ to VCC) pull them high. A transaction begins with a START condition (SDA falls while SCL is high), followed by a 7-bit slave address and a read/write bit. Each byte is followed by an acknowledge bit from the receiver: ACK (low) means continue, NACK (high) means stop or no device. The transaction ends with a STOP condition (SDA rises while SCL is high).

The DS1307 has the fixed 7-bit address 0x68. Shifted left with the R/W bit, the address byte is 0xD0 for a write and 0xD1 for a read. To read registers you first write the starting register address, then send a repeated START and read.

Set register pointer and read 3 bytes from the DS1307:
START | 0xD0 (A) | 0x00 (A) | REP START | 0xD1 (A) | sec (A) | min (A) | hour (N) | STOP
A = ACK, N = NACK sent by the master on the last byte

TWI Registers and Bit Rate

RegisterPurpose
TWBRBit rate register, sets the SCL frequency
TWSRStatus code in bits 7..3; prescaler bits TWPS1, TWPS0 in bits 1..0
TWCRControl: TWINT, TWEA, TWSTA, TWSTO, TWEN, TWIE
TWDRData register for the address byte or data byte
SCL frequency = F_CPU / (16 + 2 x TWBR x Prescaler)

For 100 kHz at 16 MHz, prescaler 1:
TWBR = (16 000 000 / 100 000 - 16) / 2 = (160 - 16) / 2 = 72

Every TWI operation follows the same pattern: write TWCR with TWINT = 1 (which clears the flag and starts the action) plus TWEN and the action bits, wait until hardware sets TWINT again, then check the status code in TWSR masked with 0xF8.

TWSR & 0xF8Meaning (master mode)
0x08START transmitted
0x10Repeated START transmitted
0x18SLA+W sent, ACK received
0x20SLA+W sent, NACK received (no device)
0x28Data byte sent, ACK received
0x40SLA+R sent, ACK received
0x50Data byte received, ACK returned
0x58Data byte received, NACK returned

Wiring the DS1307

On the ATmega328P, SDA is PC4 (DIP pin 27, Uno A4) and SCL is PC5 (pin 28, Uno A5). On the ATmega16/32, SDA is PC1 and SCL is PC0. Connect the module's VCC to 5 V, since the DS1307 needs about 4.5 V to 5.5 V on its main supply, and connect GND. Most modules already carry 4.7 kΩ pull-ups and a CR2032 backup battery holder; if yours has no pull-ups, add them on SDA and SCL. The DS1307 needs its 32.768 kHz crystal, which is on the module.

TWI Driver and DS1307 Example

The program reuses the uart_init() and printf setup from the USART tutorial; only the TWI and RTC parts are shown in full.

Polled TWI master driver and DS1307 time read/write (ATmega328P, 16 MHz, 100 kHz I2C)
#define F_CPU 16000000UL

#include <avr/io.h>
#include <util/delay.h>
#include <stdio.h>
#include <stdint.h>

#define DS1307_ADDR 0x68
#define TW_STATUS   (TWSR & 0xF8)

/* ---------- USART for printing (see USART tutorial) ---------- */
static int uart_putchar(char c, FILE *s)
{
    if (c == '\n') uart_putchar('\r', s);
    while (!(UCSR0A & (1 << UDRE0)));
    UDR0 = c;
    return 0;
}
static FILE uart_out = FDEV_SETUP_STREAM(uart_putchar, NULL, _FDEV_SETUP_WRITE);

/* ---------- TWI master ---------- */
static void twi_init(void)
{
    TWSR = 0;                    /* prescaler = 1 */
    TWBR = 72;                   /* 100 kHz SCL at 16 MHz */
    TWCR = (1 << TWEN);          /* enable TWI */
}

static uint8_t twi_start(uint8_t addr_rw)
{
    TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN);   /* send START */
    while (!(TWCR & (1 << TWINT)));
    if (TW_STATUS != 0x08 && TW_STATUS != 0x10) return 1;

    TWDR = addr_rw;                                     /* SLA+R/W */
    TWCR = (1 << TWINT) | (1 << TWEN);
    while (!(TWCR & (1 << TWINT)));
    if (TW_STATUS != 0x18 && TW_STATUS != 0x40) return 2; /* no ACK */
    return 0;
}

static void twi_stop(void)
{
    TWCR = (1 << TWINT) | (1 << TWSTO) | (1 << TWEN);
    while (TWCR & (1 << TWSTO));          /* wait until STOP is sent */
}

static uint8_t twi_write(uint8_t data)
{
    TWDR = data;
    TWCR = (1 << TWINT) | (1 << TWEN);
    while (!(TWCR & (1 << TWINT)));
    return (TW_STATUS == 0x28) ? 0 : 1;
}

static uint8_t twi_read(uint8_t ack)
{
    TWCR = (1 << TWINT) | (1 << TWEN) | (ack ? (1 << TWEA) : 0);
    while (!(TWCR & (1 << TWINT)));
    return TWDR;
}

/* ---------- DS1307 helpers ---------- */
static uint8_t bcd2bin(uint8_t b) { return (b >> 4) * 10 + (b & 0x0F); }
static uint8_t bin2bcd(uint8_t d) { return ((d / 10) << 4) | (d % 10); }

/* Set time in 24-hour mode; writing seconds with bit 7 = 0 starts the clock */
static void rtc_set_time(uint8_t h, uint8_t m, uint8_t s)
{
    if (twi_start(DS1307_ADDR << 1) == 0) {
        twi_write(0x00);                 /* register pointer = seconds */
        twi_write(bin2bcd(s) & 0x7F);    /* CH bit = 0 */
        twi_write(bin2bcd(m));
        twi_write(bin2bcd(h) & 0x3F);    /* bit 6 = 0: 24-hour mode */
    }
    twi_stop();
}

static uint8_t rtc_get_time(uint8_t *h, uint8_t *m, uint8_t *s)
{
    if (twi_start(DS1307_ADDR << 1)) { twi_stop(); return 1; }
    twi_write(0x00);                                 /* point to seconds */
    if (twi_start((DS1307_ADDR << 1) | 1)) { twi_stop(); return 1; }
    *s = bcd2bin(twi_read(1) & 0x7F);                /* ACK */
    *m = bcd2bin(twi_read(1) & 0x7F);                /* ACK */
    *h = bcd2bin(twi_read(0) & 0x3F);                /* NACK last byte */
    twi_stop();
    return 0;
}

int main(void)
{
    uint8_t h, m, s;

    UBRR0 = 103;                                     /* 9600 baud */
    UCSR0B = (1 << TXEN0);
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
    stdout = &uart_out;

    twi_init();
    /* Run once to set the clock, then comment out and reflash */
    rtc_set_time(10, 30, 0);

    while (1) {
        if (rtc_get_time(&h, &m, &s) == 0)
            printf("%02u:%02u:%02u\n", h, m, s);
        else
            printf("DS1307 not responding\n");
        _delay_ms(1000);
    }
}

Understanding the DS1307 Data

The DS1307 stores time in BCD (binary coded decimal): 45 seconds is stored as 0x45, not 0x2D. The helper functions convert both ways. Register 0x00 holds seconds, with bit 7 as the Clock Halt (CH) bit; on a brand-new chip CH is usually set, so the clock does not run until you write seconds with bit 7 cleared. Register 0x01 is minutes, 0x02 hours (bit 6 selects 12-hour mode), 0x03 day of week, 0x04 date, 0x05 month and 0x06 year. The chip auto-increments its register pointer, which is why three consecutive reads return seconds, minutes and hours.

When reading, the master sends ACK after every byte except the last, where it sends NACK to tell the slave to release the bus before the STOP. Setting TWEA in twi_read() controls this.

Troubleshooting

  • Program hangs in a while loop: the bus is stuck, usually due to missing pull-ups or SDA and SCL swapped.
  • Status 0x20 after the address: no device acknowledged. Check address 0x68, wiring and power.
  • Time reads but does not advance: the CH bit is still set, or the crystal is missing or faulty.
  • Time resets after power cut: backup battery missing or flat. Some cheap modules also include a charging circuit that is unsuitable for a non-rechargeable CR2032; check your module.

Summary

The AVR TWI is controlled by writing TWCR with TWINT set, waiting for TWINT, and checking TWSR & 0xF8 against the expected status. TWBR = 72 gives 100 kHz at 16 MHz. The DS1307 at address 0x68 stores BCD time from register 0x00, needs CH cleared to run, and is read with a register-pointer write followed by a repeated START.