I2C-сниффер

в 12:49, , рубрики: arduino, Atmega, ATmega8, avr, I2C, twi, программный i2c

Добрый день! Как-то возникла на работе проблема — имеется устройство, работающее по I2С и протокол которого необходимо было понять. Следовательно, нужен сниффер под интерфейс I2С, который бы выводил все, что приходит-уходит по I2C, на порт UART и далее через преобразователь на COM-порт компьютера.

Начало

Под рукой валялись только пара атмег8 и я решил почему бы не использовать их. Далее, встал вопрос схемы сниффера.

Было 2 варианта — включать сниффер параллельно, либо в разрыв цепи. Очевидно, что первый вариант выглядит гораздо проще, что на деле оказалось совсем не так. Но обо всем по порядку.

Вкратце, о самом интерфейсе. В I2C (TWI по-атмеловски) используется два провода — SCL и SDA. Первый отвечает за тактирование сигнала, второй за передачу непосредственно информации. Также интерфейс располагает состояниями СТАРТ и СТОП.

Так вот, первая моя мысль была — взять щуп и с одной стороны подключить его к ноге внешнего прерывания на atmega8 с другой на линию SDA и ловить передний фронт, и по прошедшему времени определять 0 или 1. Очевидно, что работать это должно было очень плохо, так как не отрабатывался корректно сигнал СТОП.

Вторая мысль была сделать все то же, но прерывание ловить на линии SCL, и по прерыванию читать линию SDA, подключенную на обычную цифровую ногу. Здесь все выглядело уже более жизнеспособно, кроме того же состояния СТОП, но я решил попробовать собрать на макетке и посмотреть что же будет.

Заранее извиняюсь, если найдете очевидные промахи в коде ниже, так как отвергнутые варианты кода восстанавливаю по памяти на коленках.

Код обработчика прерывания выглядел следующим образом:

ISR(INT0_vect)
{
	cli();
	if (bitIsHigh(PINB, 0))
		uart_send_char('1');
	else
		uart_send_char('0');
	sei();
}

В порт потекли нули и единички, но сразу стало очевидно, что данные некорректные — их было гораздо меньше ожидаемых и они менялись при повторении одного и того же запроса. В процессе выяснения причин все свелось к тому, что данные теряли из-за обращения к uart интерфейсу, который, к слову, работал на максимальной стабильной скорости 38 кбит/с, при этом сам I2C работал на 100 кбит/с. Поднимать скорость работы UART не представлялось возможным ввиду отсутствия кристалла необходимой частоты, чтобы перевести uart на допустимую скорость. Следовательно нужно было убирать работу с uart из прерывания. Получил что-то следующее:

static uint8_t data = 0;
static uint8_t idx = 7;

ISR(INT0_vect)
{
	cli();
	data |= bitIsHigh(PINB, 0) << (idx--);
	if (!idx)
	{
		uart_send_char(data);
		data = 0;
		idx = 7;
	}
	sei();
}

Работать все стало стабильнее, вот только данные все равно не несли никакого смысла. После нескольких часов проработки алгоритма, включения обработки СТОП и пр. все же решено было пойти другим путем.

На верном пути

Сколько бы я ни пытался реализовать сниффер по параллельной схеме включения, ничего из этого не вышло. Исходя из этого оставался только один вариант — нужно было включать микроконтроллер в разрыв, то есть он должен быть одновременно мастером для ответного устройства и подчиненным для оригинального мастера. Звучит, наверное, запутанно, но на деле все не так.

Так как atmega8 на борту имеет только один аппаратный I2C, то очевидно, что для работы нужно написать программную поддержку протокола.

В итоге получился следующий код:

ISR(TWI_vect)
{
	cli();
	
	uint8_t status = TWSR;
	uint8_t b;
	char s[4];
	s[3] = 0;
	_delay_ms(1);

	switch (status & I2C_STATUS_MASK)
	{
		case I2C_STATUS_SR_RX_ADR_ACK:/* case I2C_STATUS_SR_RX_ADR_NACK:*/
			uart_send_str("-AW:");
			uart_send_int( TWDR );
			i2csoft_start();
			i2csoft_open_write(I2C_ADDRESS);
			break;
		
		case I2C_STATUS_SR_RX_DATA_ACK:/* case I2C_STATUS_SR_RX_DATA_NACK:*/
			b = TWDR;
			sprintf(s, " %.2X", b);
			uart_send_str(s);
			i2csoft_write_byte(b);
			break;

		case I2C_STATUS_SR_RX_STOP_RESTART:
			uart_send_str("En");
			_delay_ms(10);
			do
			{
				_delay_us(5);
				i2csoft_start();
			}
			while (!i2csoft_open_read(I2C_ADDRESS));
			break;
			
		case I2C_STATUS_BUS_ERROR:
			uart_send_str("Bn");
			break;

		case TW_ST_SLA_ACK:	
			uart_send_str("-AR:");
			uart_send_int( TWDR );
			b = i2csoft_read_byte();
			sprintf(s, " %.2X", b);
			uart_send_str(s);
			TWDR = b;
			break;
			
        case TW_ST_DATA_ACK: 
			b = i2csoft_read_byte();
			sprintf(s, " %.2X", b);
			uart_send_str(s);
			TWDR = b;
            break;
 
        case TW_ST_DATA_NACK: 
        case TW_ST_LAST_DATA:
			b = i2csoft_read_byte(false);
			uart_send_str("Ln");
			break;
			
		default:
			uart_send_char('U');
			uart_send_int(status);
			uart_send_char(' ');
			break;
	}
	TWCR |= (1<<TWINT);
	sei();
}

Мастер устройство подключается на аппаратный I2C атмеги, исполняющее устройство подключается на любые 2 цифровые ноги, одна из которых работает в режиме SCL, другая — SDA. Все, что делает приведенный выше код — это получает по I2C прерывание и вызывает аналогичное состояние на программном интерфейсе, при этом в uart записывается служебная информация, помогающая понять что происходит. Выставленные задержки подбирались для конкретного устройства и могут несколько отличаться для других. В итоге получаем вполне добротный сниффер.

Если кому-нибудь будет интересно, то исходники можно взять с гитхаба.

Автор: Neomer

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js