读取时 I2C 奇怪的延迟问题

I2C bizarre delay issue when reading

我一直在努力让我的 ATTINY85 成为 bit-bang I2C (read/write)。我有以下配置:

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;
}

如果我删除 delay(),I2C 将不再工作并且我无法从设备读取(设备没有响应)。似乎合乎逻辑,但我想删除 delay() 的原因是因为它实际上不是 'true' 延迟,它只是打开和关闭位于不同引脚 (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
}

我怀疑我可能 'nailed' 一个完美的延迟,它产生了其他设备期望的适当信号长度,所以我尝试使用 for 循环和示波器进行相同的延迟:

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

运气不好,I2C 读取停止工作..

然后我决定将 LED 切换到另一个 PIN,让 PB1 完全独立,看看它是否与延迟相关或 pin/circuit 相关:

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

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


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

奇怪的是,I2C 又停止工作了!它只有在我输入 PB1 high/low 时才有效。我仍然不明白我是否只是碰巧确定了所需的完美延迟,而恰好是打开 PB1 比打开 PB3 花费的时间更少,或者它与电路本身和 LED 做某种 pull-up/pull-down I2C 上的功能(原谅我的无知,我是初学者),但是 PB1 又一次根本没有连接到 I2C 线路。

谁能解释一下为什么它只在我打开 PB1 on/off 时起作用,而不是真正的延迟?谢谢!

完整来源:

#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;
}

I2c 定义了一些信号的最小时序 - 这里重要的是 SCL 的高电平和低电平时间 - 在允许下一次转换到相反状态之前 SCL 应该稳定的时间量。 这些时序通常为 ~5µs,具体数字应取自数据sheet。

read 循环末尾的循环大约需要 2 到 3 条指令,具体取决于编译器的作用。 AVR 指令,根据您的时钟速率,大约需要 200ns,因此(没有延迟)SCL 低大约 600ns,给予或接受 - 这太短了,至少对于您的特定 "other-end-device" 显然是这样。

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

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

您在延迟功能中切换端口引脚的事实与此处无关 - 唯一相关的是您需要在 SCL 较低时花费一些时间。显然,您目前所做的是浪费端口引脚,只是花一些时间等待 - 使用 delay_us,尝试延迟而不是延迟。但是检查 "the other end's" 数据 sheet 以获得所需的确切时间,4-5µs 应该没问题。

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

理想情况下,您应该尝试在 SCL 的高电平阶段的中间读取 SDA - 一个 8 位的展开循环和一些 delay_us 散布在其中应该可以完美地工作。