Linux中断对I2C总线的影响

    #kernel分析

    此处讨论的内核版本是linux-2.6.19 for mips-32。

    1. i2c driver

    PoE IC通过i2c与主IC(RTL8382M)通信,为标准i2c过程,代码如下:

    int32 IP80xI2cInit(void)
    {
    	osal_printf("IP808_i2c_init...\n");
    	_sda_output();
    	_scl_output();
    	return SYS_ERR_OK;
    }
    
    void _i2c_wait(void)
    {
    	IP80X_DELAY_US(50);
    }
    
    void i2c_start(void)
    {
    	_sda_high();
    	_scl_high();
    	_i2c_wait();
    	_sda_low();
    	_i2c_wait();
    	_scl_low();
    }
    
    void i2c_stop(void)
    {
    	_i2c_wait();
    	_scl_low();
    	_sda_low();
    	_i2c_wait();
    	_scl_high();
    	_i2c_wait();
    	_sda_high();
    }
    
    int32 _ack_get(void)
    {
    	_scl_low();
    	_sda_high();
    	_sda_input();
    	_i2c_wait();
    	_scl_high();
    	_i2c_wait();
    	if(_sda_read())
    	{
    		_sda_output();
    		i2c_stop();
    		osal_printf("_ack_get err...\n");
    		return SYS_ERR_FAILED;
    	}
    	_i2c_wait();
    	_scl_low();
    	_sda_output();
    	return SYS_ERR_OK;
    }
    
    void _ack_set(void)
    {
    	_scl_low();
    	_sda_low();
    	_i2c_wait(); 
    	_scl_high();
    	_i2c_wait(); 
    	_scl_low();
    }
    
    void _ack_no_set(void)
    {
    	_scl_low();
    	_sda_high();
    	_i2c_wait(); 
    	_scl_high();
    	_i2c_wait(); 
    	_scl_low();
    }
    
    void i2c_write(uint8 value)
    {
    	uint8 i=9;
    	while(--i)
    	{
    		_i2c_wait();
    		if((value & 0x80) != 0x80)
    			_sda_low();
    		else
    			_sda_high();
    		_i2c_wait();
    		_scl_high();
    		_i2c_wait();
    		_scl_low();
    		value <<= 1;
    	}
    	_sda_high();
    }
    
    uint8 i2c_read(void)
    {
    	uint8 value=0;
    	uint8 i=9;
    	_sda_input();
    	while(--i)
    	{
    		value <<= 1;
    		_i2c_wait();
    		_scl_high();
    		_i2c_wait();
    		if (_sda_read() == 1)
    			value |= 0x01;
    		_i2c_wait();
    		_scl_low();
    	}
    	_sda_output();
    	return value;
    }
    
    int32 ip80x_write(uint8 slave_addr, uint8 reg_addr, uint8 value)
    {
    	int32 ret;
    	i2c_start();							// start I2C
    	i2c_write(slave_addr << 1);				// slave address
    	ret = _ack_get();						// get ack
    	if(SYS_ERR_FAILED == ret)
    		return ret;
    	i2c_write(reg_addr);					// register address
    	ret = _ack_get();						// get ack
    	if(SYS_ERR_FAILED == ret)
    		return ret;
    	i2c_write(value);						// send data
    	ret = _ack_get();						// get ack
    	if(SYS_ERR_FAILED == ret)
    		return ret;
    	i2c_stop();								// stop I2C
    	return SYS_ERR_OK;
    }
    
    int32 ip80x_read(uint8 slave_addr, uint8 reg_addr, uint8 *value)
    {
    	uint32 ret;
    	i2c_start();							// start I2C
    	i2c_write(slave_addr << 1);				// slave address
    	ret = _ack_get();						// get ack
    	if(SYS_ERR_FAILED == ret)
    		return ret;
    	i2c_write(reg_addr);					// register address
    	ret = _ack_get();						// get ack
    	if(SYS_ERR_FAILED == ret)
    		return ret;
    	i2c_stop();								// stop I2C
    	i2c_start();
    	i2c_write((slave_addr << 1) | 0x01);	// slave address
    	ret = _ack_get();						// get ack
    	if(SYS_ERR_FAILED == ret)
    		return ret;
    	*value = i2c_read();					// read data
    	_ack_no_set();							// get ack
    	i2c_stop();								// stop I2C
    	return SYS_ERR_OK;
    }
    

    2. 问题引出

    以上代码在c51的机器上面运行正常,但是移植到linux-2.6.19 for mips-32平台就出现了问题。

    具体表现为随机打印’_ack_get err…‘错误,通讯周期越长越频繁。

    3. 解决思路

    1.既然只有部分通信失败,所以driver多半没有问题,这里面最有可能影响的就只有wait时间了,目前的值为50us,可能由于时间不合适导致。

    2.由于i2c通信过程是不可重入的,所以可能是读写代码没有加入临界区保护,因为此机器带有web,PoE内核线程polling的时候如果同时刷新页面i2c总线就会出现竞争,如果不加保护肯定会出现上一次通信被下一次打断。

    3.linux interrupt会导致通信被中断,尝试关中断,但是硬中断在linux里面好像是不能关闭的,可以参考c51,因为主ic集成了中断控制器,可以考虑直接操作寄存器禁止所有硬中断。

    4.又linux interrupt时间不知道多久,必需用示波器测量i2c失败过程波形图分析,而且不确定中断是否会导致i2c失败,因为i2c并没有严格限制最长时间。

    5.参考PoE IC,是否其针对i2c又特殊规定。

    4. 解决过程

    首先使用osal_sem_mutex_take函数和osal_sem_mutex_give函数给i2c读写代码加锁,需要说明的是,这两个函数相当于spinlock,竞争会导致忙等,同时关闭软中断,但是不能禁止硬中断,代码如下:

    osal_mutex_t poe_sem;
    
    poe_sem = osal_sem_mutex_create();
    
    int32 ip80x_reg_get(uint8 slave_addr, uint8 page, uint8 reg_addr, uint8 *value)
    {
    	int32 ret;
    
    	osal_sem_mutex_take(poe_sem,OSAL_SEM_WAIT_FOREVER);
    
    	ret = IP80xPageSet(page);
    	if(SYS_ERR_FAILED == ret)
    	{
    		osal_sem_mutex_give(poe_sem);
    		return ret;
    	}
    
    	ret = ip80x_read(slave_addr,reg_addr,value);
    
    	osal_sem_mutex_give(poe_sem);
    	return ret;
    }
    
    int32 ip80x_reg_set(uint8 slave_addr, uint8 page, uint8 reg_addr, uint8 value)
    {
    	int32 ret;
    
    	osal_sem_mutex_take(poe_sem,OSAL_SEM_WAIT_FOREVER);
    
    	ret = IP80xPageSet(page);
    	if(SYS_ERR_FAILED == ret)
    	{
    		osal_sem_mutex_give(poe_sem);
    		return ret;
    	}
    
    	ret = ip80x_write(slave_addr,reg_addr,value);
    
    	osal_sem_mutex_give(poe_sem);
    	return ret;
    }
    

    测试证明加锁无效,所以很有可能是系统某个地方引起的通讯中断。

    下面必需使用示波器测量i2c波形分析。

    图1

    图1

    图2

    图2

    图3

    图3

    图4

    图4

    分别测量clock和data input线,蓝色表示clock,黄色表示从PoE IC到host的data input。黄色线low表示PoE IC给出ack确认信号,蓝色一个上升沿表示host给PoE IC一个bit数据,长一点的高表示start信号。

    图1是正常波形,可以看到读出一个byte和写入4个byte的波形。

    图2为接图1之前的波形,正常应该是最左第一个ack表示host已经发出去了一个byte的数据,由于从后面data线有一个byte数据出现可以确定,前面27个clock上升沿加上后面有data的9个clock上升沿必定是一个完整的读周期,所以正常情况data应该每隔8个clock上升沿有一个low的ack回复,但是这里没见到。异常出现在AB测量点,clock会出现10ms左右的低电平,AB点前到前一个ack有三个clock上升沿加上AB点后的五个上升沿刚好为一个byte写入,但是第九个周期却没有data ack,因此问题应该出现在这里。

    图3可以看到这样的中断还有很多,似乎随机出现。

    图4为半个上升沿的时间,测量为700us。

    从波形图可以看出来,这些中断应该对i2c通信有影响,但是为什么会这样呢,正常来说10ms不应该影响正常通信才对,因为这很可能是系统中断,而之前用Microsemi的就不会出现这种情况,所以会不会是IC本身的限制,因此需要仔细查看PoE IC的datasheet看看是否有特殊规定。

    图5

    图5

    很明显了,clock处于低电平不能超过7mini-seconds,也就是7ms,否则就终止通信,所以也就是系统因为某些原因导致通信中断10ms,超过IC的规定,因此出现错误。

    从以上信息来看,系统导致通信中断,基本上就是硬件中断的时间太长,看datasheet可知RTL8382M集成中断控制器,并且有相应寄存器可以enable/disable,类似于c51的EA寄存器。接下来就禁止所有中断测试一下,代码如下:

    int32 ip80x_reg_get(uint8 slave_addr, uint8 page, uint8 reg_addr, uint8 *value)
    {
    	int32 ret,inter;
    
    	osal_sem_mutex_take(poe_sem,OSAL_SEM_WAIT_FOREVER);
    	inter = REG32(0xB8003000);
    	REG32(0xB8003000) = 0;
    
    	ret = IP80xPageSet(page);
    	if(SYS_ERR_FAILED == ret)
    	{
    		REG32(0xB8003000) = inter;
    		osal_sem_mutex_give(poe_sem);
    		return ret;
    	}
    
    	ret = ip80x_read(slave_addr,reg_addr,value);
    
    	REG32(0xB8003000) = inter;
    	osal_sem_mutex_give(poe_sem);
    	return ret;
    }
    
    int32 ip80x_reg_set(uint8 slave_addr, uint8 page, uint8 reg_addr, uint8 value)
    {
    	int32 ret,inter;
    
    	osal_sem_mutex_take(poe_sem,OSAL_SEM_WAIT_FOREVER);
    	inter = REG32(0xB8003000);
    	REG32(0xB8003000) = 0;
    
    	ret = IP80xPageSet(page);
    	if(SYS_ERR_FAILED == ret)
    	{
    		REG32(0xB8003000) = inter;
    		osal_sem_mutex_give(poe_sem);
    		return ret;
    	}
    
    	ret = ip80x_write(slave_addr,reg_addr,value);
    
    	REG32(0xB8003000) = inter;
    	osal_sem_mutex_give(poe_sem);
    	return ret;
    }
    

    REG32(0xB8003000) = 0;为禁止所有硬中断,结果证明确实为某一个外设的硬中断导致的,继续测试证明,具体为time/counter0和nic。

    中断时间过长的源头就是中断处理函数内容太多,跟踪代码得知:

    static struct irqaction timer_irqaction = {
    	.handler = timer_interrupt,
    	.flags = IRQF_DISABLED | IRQF_TIMER,
    	.name = "timer",
    };
    

    此为TC0 irq的irqaction,timer_interrupt为中断处理函数。

    osal_isr_register(RTK_DEV_NIC, _maple_nic_isr_handler, NULL);
    

    _maple_nic_isr_handler为RTK_DEV_NIC irq的中断处理函数。

    到此,timer_interrupt_maple_nic_isr_handler执行时间过长为导致i2c错误的原因,接下来就是简化中断执行的时间,尽量在最短的时间执行中断,避免对其他线程造成影响。