读取时出现I2C奇怪的延迟问题

时间:2017-06-28 14:56:39

标签: c avr i2c

我一直试图让我的ATTINY85进行bit-bang I2C(读/写)。我有以下配置:

PB0 = SDA
PB1 = LED
PB2 = SCL

我能够毫无问题地编写,但是只有在读取循环中有'delay()'函数时,才能读取,所以这么好:

char i2c_read(void)
{
    uint8_t B = 0;
    DDRB &= 0b11111110; // switch PB0 to input

    for ( int bit = 0; bit < 0x08; bit++ )
    {        
        delay(); // <--!!!!!!!!! the root of all evil

        SIGNAL_HIGH( PORT_SCL );

        B <<= 1;

        if( PINB & (1 << PB0 ) )
        {
            B |= 1;              
        } 
        else
        {
            B |= 0;              
        }

        SIGNAL_LOW( PORT_SCL );
    }

    DDRB |= 0b00000001; // switch PB0 as output

    i2c_nack();

    return B;
}

如果我删除延迟(),I2C不再工作,我无法从设备读取(设备没有响应)。似乎逻辑,但我想删除延迟()的原因是因为它实际上不是'真正的'延迟,它只是打开和关闭不同引脚(PB1)上的LED,I2C线路在PB0和PB2上

_delay_ms太慢了,所以我只是打开和关闭PB1引脚,以便稍微延迟,这是唯一的工作方式。这是我的延迟功能的内容,如果我这样离开它,一切都很好:

void delay()
{
    LED_ON();
    LED_OFF();
}

void LED_ON( void )
{
    PORTB |= 0b00000010; // PB1
}

void LED_OFF( void )
{
    PORTB &= 0b11111101; // PB1
}

我怀疑我可能“钉”了一个完美的延迟,这会产生另一个设备所需的适当信号长度,所以我尝试使用for循环和示波器进行相同的延迟:

void delay()
{
   for( int i=0; i<20; i++){ }
}

没有运气,I2C读取停止工作..

然后我决定将LED切换到另一个PIN并完全离开PB1以查看它是否与延迟相关或引脚/电路相关:

void delay()
{
    LED_ON();
    LED_OFF();
}

void LED_ON( void )
{
    PORTB |= 0b00001000; // PB3
}


void LED_OFF( void )
{
    PORTB &= 0b11110111; // PB3
}

奇怪的是,I2C再次停止工作!它只有在我把PB1设为高/低时才有效。我仍然无法理解我是否碰巧确定了所需的完美延迟,而恰好是转动PB1比转动PB3花费更少的时间,或者它与电路本身有关并且LED做了某种拉动 - 在I2C上启动/下拉功能(原谅我的无知,我是初学者),但PB1根本没有连接到I2C线路。

任何人都可以详细说明为什么只有当我打开/关闭PB1而不是真正的延迟时才能正常工作?谢谢!

完整来源:

#define PORT_SDA PB0
#define PORT_SCL PB2

#define SIGNAL_HIGH(PORT) PORTB |=  ( 1 << PORT )
#define SIGNAL_LOW(PORT)  PORTB &= ~( 1 << PORT )

void delay();
void LED_ON(void);
void LED_OFF(void);

void i2c_init(void);
void i2c_start(void);
char i2c_read(void);
void i2c_stop(void);
void i2c_nack(void);
void i2c_ack(void);
void i2c_ack_slave(void);
void i2c_write(uint8_t byte);

void i2c_init()
{
    DDRB = 0b00000010; // TODO: should be removed once the weird delay issue is solved
    DDRB |= ( 1 << PORT_SDA );
    DDRB |= ( 1 << PORT_SCL );
}

void i2c_start( void )
{
    SIGNAL_LOW(  PORT_SCL );
    SIGNAL_HIGH( PORT_SDA );
    SIGNAL_HIGH( PORT_SCL );
    SIGNAL_LOW(  PORT_SDA );
    SIGNAL_LOW(  PORT_SCL );
}

void i2c_stop( void )
{
    SIGNAL_LOW(  PORT_SCL );
    SIGNAL_LOW(  PORT_SDA );
    SIGNAL_HIGH( PORT_SCL );
    SIGNAL_HIGH( PORT_SDA );
}

void i2c_ack(void)
{
   SIGNAL_LOW(  PORT_SDA );
   SIGNAL_HIGH( PORT_SCL );
   SIGNAL_LOW(  PORT_SCL );
   SIGNAL_HIGH( PORT_SDA );
}

void i2c_nack(void)
{
   SIGNAL_HIGH( PORT_SDA );
   SIGNAL_HIGH( PORT_SCL );
   SIGNAL_LOW(  PORT_SCL );
}

void i2c_ack_slave(void)
{
    SIGNAL_HIGH( PORT_SCL );
    SIGNAL_LOW( PORT_SCL );
}

void i2c_write(uint8_t byte)
{
    uint8_t bit;

    for ( bit = 0; bit < 0x08; bit++ )
    {
        if( ( byte << bit ) & 0x80 )
            SIGNAL_HIGH( PORT_SDA );
        else
            SIGNAL_LOW( PORT_SDA );

        SIGNAL_HIGH( PORT_SCL );
        SIGNAL_LOW( PORT_SCL );
    }

    // Clear both lines (needed?)
    SIGNAL_LOW( PORT_SCL );
    SIGNAL_LOW( PORT_SDA );

    i2c_ack();
}

char i2c_read(void)
{
    uint8_t B = 0;
    DDRB &= 0b11111110; // switch PB0 to input

    for ( int bit = 0; bit < 0x08; bit++ )
    {        
        delay(); // <-- the root of all evil

        SIGNAL_HIGH( PORT_SCL );

        B <<= 1;

        if( PINB & (1 << PB0 ) )
        {
            B |= 1;              
        } 
        else
        {
            B |= 0;              
        }

        SIGNAL_LOW( PORT_SCL );
    }

    DDRB |= 0b00000001; // switch PB0 as output

    i2c_nack();

    return B;
}


void delay()
{
    LED_ON();
    LED_OFF();
}


void LED_ON( void )
{
    PORTB |= 0b00000010;
}


void LED_OFF( void )
{
    PORTB &= 0b11111101;
}

1 个答案:

答案 0 :(得分:1)

I2c定义了许多信号的最小时序 - 这里重要的是SCL的高电平和低电平时间 - SCL在下一次转换到相反状态之前应该保持稳定的时间。 这些时序典型值约为5μs,具体数据应从数据表中获取。

read循环结束时的循环需要2到3条指令,具体取决于编译器的作用。 AVR指令,取决于你的时钟速率,大约需要200ns,所以(没有延迟)SCL低约600ns,给予或接受 - 这太短了,至少显然对于你的特定“其他终端设备”。

在被调用函数中插入函数调用和端口访问时,插入了足够的指令以使SCL保持低电平足够长的时间以正常工作。

在你的代码中,HIGH时间并不是问题所在,因为你让AVR在SCL为高电平时执行更多指令 - 显然,有足够的时间让你的SCL保持足够长的时间。

您在延迟功能中切换端口引脚的事实与此无关 - 唯一的相关性是您需要在SCL较低时花一些时间。显然,你目前所做的是浪费一个端口引脚只是花一些时间等待 - 使用delay_us,试验延迟而不是那个。但请查看“另一端”的数据表,了解所需的确切时间,4-5μs应该没问题。

为什么你的延迟循环不起作用?它最有可能被编译器优化掉,它认识到你没有在那个空循环中做一些相关的事情。

理想情况下,您应该尝试在SCL的HIGH阶段的中间读取SDA - 使用8位的非滚动循环和一些delay_us传播应该可以正常工作。