Bit-banging AVR: делаем сканер TWI шины

разделы: I2C , дата: 26 сентября 2015г.


bit-banging - это тоже, что
дерганье за ниточки

Используя познания из предыдущего поста: "Введение в Bit-banging: "режимы работы GPIO микроконтроллеров AVR, организация последовательной шины" можно сделать что нибудь полезное. "Софтовая" реализация I2C шины будет не зависеть от модели микроконтроллера, будет работать на тех пинах которые вы зададите, и на мой взгляд, что самое важное, может служить примером протокола для взаимодействия между несколькими различными микроконтроллерами.

Иллюстрация I2C протокола, взятая из AppNoteAVR315: Using the TWI module as I2C master представлена ниже:


Waveform showing the clock and data timing for an I2C message.

    Основные моменты:
  1. Есть две линии: SCL служит для тактирования, SDA служит для передачи данных.
  2. Скважность импульсов не играет роли. Один такт может длиться 5мкс, другой 5мс.
  3. Высокий уровень формируется pull-up режимом, низкий - земля.
  4. Данные передаются сессиями.
  5. Каждая отдельная сессия работает только на прием или только на передачу.
  6. В начале и в конце сессии соответственно формируются сигналы START и STOP. Между этими сигналами линия считается занятой.
  7. Только при START и STOP, SDA линия меняет уровень при высоком уровне SCL.
  8. START формируется последовательностью: a) падением SDA при высоком уровне SCL, б) падением SCL.
  9. STOP формируется последовательностью: a) поднятием SCL при низком SDA, б) поднятем SDA при высоком SCL.
  10. При передаче данных уровень SDA меняется только при низком SCL.
  11. При чтении данных, SDA уровень считывается только при высоком SCL.
  12. После формирования START, передается адрес устройства, с которым хотят связаться. Младший бит адреса указывает режим сессии.
  13. Младший бит равный нулю в адресе устойства означает, что сессия будет направлена на запись, т.е. передачу в адресуемое устройство.
  14. Младший бит равный единице в адресе устойства означает, что сессия будет направлена на чтение, т.е. будет происходить чтение с адресуемого устройства.
  15. Байты передаются побитно, начиная от старшего разряда к младшему.
  16. После передачи каждого байта, принимающая сторона должна формировать ответ, означающий, что передача прошла успешно: ACK.
  17. При чтении с устройства, не сформировав ответ, мы дадим понять устройству, что прием закончен, и мы получили все, что нужно. Это называется NACK.

Несмотря на то, что с первого взгляда может показаться сложновато, то обстоятельство, что время каждого такта может быть произвольным, делает этот протокол доволько удобным и простым в отладке. Можно прикрутить тактовую кнопку к протоколу, пару светодиодов на линии SDA и SCL, и олаживать его в ручном режиме без всякого осцилографа.

Сейчас можно написать пробную программу, для опроса I2C шины, на ниличие полюченных устройств. Алгоритм такой: в цикле от нуля до 255 будем выдавать последовательности: START -- ADDRESS -- STOP. Если на адрес будем получать ACK, значит такое устройство подключено к шине.

Сканнер I2C шины писал для модуля DS1307. На нем установленно два устройства: сам ds1307 и eeprom AT24. Даннай модуль сам формирует линию. Если подключить его к питанию и мультиметром замерить напряжение на контактах SDA и SCL, то там будет высокий уровень. Эти контакты находятся в pull-up режиме. Тогда замыкая SDA или SC на землю, мы будем иметь низкий уровень, а "отпуская" их линия будет возваращаться в высокий.

Тогда, если при инициализации запишем нули в PORT, переключать состояния линий можно будет регистром DDR. Записывая в DDR ноль будем "прижимать" линию за землю, это будет низкий уровень. Записывая в DDR единицу будем "отпускать" линию, это будет высокий уровень.

За основу я взял программу из поста: "UART+AVR: функция форматного вывода printf()", добавил bit-banging и получилась такая штука

/* for ATmega8/168/328
site: http://countzero.weebly.com

avr-gcc -mmcu=atmega8 -Wall -Os -o i2c.elf i2c.c
avr-objcopy -O ihex i2c.elf i2c.hex
avrdude  -patmega8 -carduino -P/dev/ttyUSB0 -b19200 -D -Uflash:w:./i2c.hex:i
*/

#define F_CPU 16000000UL
#define LEN 32

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <compat/deprecated.h>
#include <stdio.h>

// Clock Line Port
#define SCLPORT PORTD
#define SCLDDR  DDRD
#define SCLPIN  PIND
// Data Line Port
#define SDAPORT	PORTD
#define SDADDR  DDRD
#define SDAPIN	PIND

#define SCL PD2
#define SDA PD3

#define QDEL  _delay_us(5)                // i2c quarter-bit delay
#define HDEL  _delay_us(10)               // i2c half-bit delay

#define SDA_I2C_LO      sbi(SDADDR, SDA)
#define SDA_I2C_HI      cbi(SDADDR, SDA)

#define SCL_I2C_LO      sbi(SCLDDR, SCL);
#define SCL_I2C_HI      cbi(SCLDDR, SCL);

#define SCL_TOGGLE_I2C  HDEL; SCL_I2C_HI; HDEL; SCL_I2C_LO;
#define INIT_I2C	cbi(SDAPORT, SDA); cbi(SCLPORT,SCL);

char buffer[LEN];
register unsigned char IT asm("r16");
volatile unsigned char done;
volatile unsigned char IDX;

static inline void clearStr(char* str)
{
        for(IT=0;IT<LEN;IT++)
                str[IT]=0;
}

static uint8_t uart_putchar(char c, FILE *stream)
{
	   if (c == '\n')
		           uart_putchar('\r', stream);
	      loop_until_bit_is_set(UCSRA, UDRE);
	         UDR = c;
		    return 0;
}

ISR(USART_RXC_vect)
{
        char bf= UDR;
        buffer[IDX]=bf;
        IDX++;

        if (bf == ':' || IDX >= LEN)
        {
                IDX=0;
                done=1;
        }
}

static void blink13(uint8_t count)
{
        PORTB |= (1<<PB5);
        count =(count <<1);count--; //count=(count*2)-1;
        for (IT=0;IT<count;IT++)
        {
                _delay_ms(500);
                PORTB ^= (1<<PB5);
        };
};

static uint8_t start_i2c()
{
	uint8_t ret; ret=64; // attempts
	uint8_t valueSDA, valueSCL;
	do {
		SDA_I2C_HI;
		SCL_I2C_HI;
		QDEL;

		valueSDA=SDAPIN & (1<<SDA);
		valueSCL=SCLPIN & (1<<SCL);
		ret--;
	} while ((valueSDA == 0 || valueSCL == 0) && ret >0);

	if (ret == 0)
		return 0;

	SDA_I2C_LO;
	QDEL;
	SCL_I2C_LO;
	QDEL;
	return ret;
}

static uint8_t stop_i2c()
{
	SDA_I2C_LO; SCL_I2C_LO; HDEL;

	SCL_I2C_HI; QDEL;
	SDA_I2C_HI; QDEL;
	if ((SDAPIN & (1<<SDA)) == 0 || (SCLPIN & (1<<SCL)) == 0)
	       	return 1;
	else
		return 0;
}

static uint8_t send_i2c(uint8_t value)
{
	uint8_t bits; bits=8;
	while (bits > 0)
	{
		bits--;
		SCL_I2C_LO;
		QDEL;
		while((SCLPIN &(1<<SCL)) != 0){};
		if (value & (1<<bits))
			SDA_I2C_HI;
		else
			SDA_I2C_LO;
		SCL_TOGGLE_I2C;
	}
	// get ACK
	QDEL; SDA_I2C_HI;
	QDEL; SCL_I2C_HI;

	uint8_t ack;
	ack = (SDAPIN & (1<<SDA));

	QDEL; SCL_I2C_LO;
	HDEL;

	return ack;
};

static FILE mystdout = FDEV_SETUP_STREAM(uart_putchar, NULL, _FDEV_SETUP_WRITE);

int main(void)
{
        // USART init
        UBRRL=103;
        UCSRB=(1<<TXEN)|(1<<RXEN)|(1<<RXCIE);
        UCSRC=(1<<URSEL)|(3<<UCSZ0);

        DDRB |= (1<<PB5); //  pinMode(13,OUTPUT);

        blink13(3); //ready indication
        IDX=0;
        done=0;
        sei();

	stdout = &mystdout;
	printf("Start I2C\n");
	INIT_I2C;
	uint8_t k;
	for(k=1; k < 256;k++)
	{
		if (start_i2c())
		{
			uint8_t ask;
			ask=send_i2c(k);

			if (ask == 0)
				printf("Found device:  %d\n", k);

			stop_i2c();
		} else
			printf("Failure start I2C on: %d\n", k);

		_delay_ms(300);
	};

	for (;;){};

        return 0;
}

Результат работы программы:

Найденое устройство 160 это 0xA0 EEPROM AT24

Устройство 208 это 0xD0 RTC DS1307

Часто, в скетчах Arduino можно увидеть адрес DS1307 как 0x68, но это адрес без младшего бита R/W. Т.е. 0x68<<1 = 0xD0. Достаточно будет обраться к официальной документации что бы развеять сомнения: DS1307 64 x 8, Serial, I2C Real-Time Clock