3
\$\begingroup\$

I am trying to implement I2C using an AVR controller without the designated internal TWI registers.

The I2C protocol requires that the Master device drives the SDA and SCL lines to begin with in order to address and begin a communication transaction. The problem I am having is that after an address is sent over the serial line, you must let the data line "float" (that is, be pulled high by the pull-up resistor) in order for the slave to drive the line low for an ACK signal.

I am able to successfully successfully address the I2C device, but when I attempt to let the line "float" I am having a bit of trouble. I was under the impression that in a input HIGH-Z state (tri-state that is - DDRX 0, PORTX 1 for a particular pin) would essentially be modeled like:

enter image description here

However, the state of the SDA line remains high even though the I2C device is responding with an ACK bit by driving the line low. I know this to be true because I set a sizable delay after setting the pin as input. During this delay, the line remains high. However, if I remove the wire tying the MCU to the SDA line then it will drop to LOW due to the slave device.

In short, how do you handle "letting go" of a line to ensure its potential is driven by the devices on your I2C data line rather than being influenced by the MCU?

/* Pin and direction register manipulations */ 
#define I2C_CLKDEL              10 //10uS
#define I2C_PORT                PORTB
#define SDA                     PB0 
#define SCL                     PB1
#define set_high(port, pin)     (port    |=  (1<<pin))
#define set_low(port, pin)      (port    &=  ~(1<<pin))
#define set_in(portDDR, pin)    (portDDR &=  ~(1<<pin))
#define set_out(portDDR, pin)   (portDDR |=  (1<<pin))

void clkStrobe(void){
    set_high(I2C_PORT, SCL); 
    _delay_us(I2C_CLKDEL); 
    set_low(I2C_PORT, SCL); 
    _delay_us(I2C_CLKDEL);
}

uint8_t sendByte(uint8_t byte){ //MSB first
    uint8_t count = 8, ack; 
    set_low(PORTB, SCL);  

    while( count-- ){
        if( byte & 0x80 )
            set_high(PORTB, SDA);
        else
            set_low(PORTB, SDA); 
        byte <<= 1;
        _delay_ms(I2C_CLKDEL); 
        clkStrobe();  
    }
    //set as input and read in the I2C port data
    set_in(I2C_PORT, SDA);
        _delay_ms(1000); 
    ack = PINB;  
    set_out(I2C_PORT, SDA); 
    clkStrobe();    //clock in the ACK bit 

    return (ack & (1<<SDA)); //return ACK bit 
}

A large delay was inserted after SDA on the MCU is set as input for testing. The line remains high. If the SDA line is removed from the MCU (wire no longer connected), the SDA line is driven low by the I2C device being addressed (device is sending ACK). Clearly I am doing something wrong here.

\$\endgroup\$
  • \$\begingroup\$ Set the DDR bit for the pin to 0 when you're expecting an ACK. \$\endgroup\$ – Matt Young Mar 3 '14 at 18:10
  • \$\begingroup\$ That is exactly what I do - set the pin as INPUT (which is a high-Z tri-state configuration). However, the line remains high regardless of this. I posted the code. \$\endgroup\$ – sherrellbc Mar 3 '14 at 18:15
  • \$\begingroup\$ Where? There is no reference to the DDRB register in that code. \$\endgroup\$ – Matt Young Mar 3 '14 at 18:18
  • \$\begingroup\$ Updated with #define statements. \$\endgroup\$ – sherrellbc Mar 3 '14 at 18:23
  • \$\begingroup\$ Possibly a clocking issue? Are you sure you're sending a 9th clock cycle onto SCL? \$\endgroup\$ – Dzarda Mar 3 '14 at 18:28
2
\$\begingroup\$

For set_in, you’re passing I2C_PORT, which is PORTB, but you need to pass in DDRB.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.