Связь двух микроконтроллеров на примере подключения 4-х разрядного семисегментного индикатора к Arduino через вспомогательный микроконтроллер ATtyny13a

разделы: Arduino , AVR , UART , I2C , дата: 29 января 2018г.

Если под вашу задачу требуется большее число пинов/портов/мегагерц/памяти, чем имеется в используемом вами микроконтроллере, то в ответ на эту проблему обычно советуют взять микроконтроллер "покрупнее". Ответ не лишенный смысла, однако мне удалось найти задачку, от которой так просто не отмахнешься. Героем сегодняшней статьи будет 4-х разрядный семисегментный индикатор с динамической индикацией.

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

В модуле меня привлекла его компактность. К примеру, для приборной панели паяльной станции, где место сильно ограничено, это то что надо. После некоторого размышления я решил, что в целом модуль неплох, но... для него требуется отдельный управляющий микроконтроллер, сопроцессор, на котором будет крутиться динамическая индикация.

Индикатор не содержит подтягивающих резисторов(!), возможно здесь используются сдвиговые регистры с подтяжкой? Так или иначе, я замерял потребление модуля через EnargyTrace и получил значение около 23mA при питании 3.3 Вольт, что для такой "гирлянды" вполне нормально.

Китайские ATtiny13a в SO-8 корпусе стоят около 15₽, они имеют пять рабочих выводов, три из которых нужно будет отдать на индикатор, остаются два вывода для организации линии связи, что более чем достаточно, но простенький SPI сюда не посадишь, т.к. тот SPI который будет использоваться для управления индикатором, работает мастером, а для связи с "главным" микроконтроллером нужен будет слейв( запускать слейв на главном микроконтроллере - это не вариант). К сожалению или к счастью(смотря как посмотреть), АTtiny13a не поддерживает аппаратно абсолютно никаких протоколов.

Т.о. перед нами стоит задача на ATtiny13a организовать c использованием не более двух пинов скоростную и надежную линию для приема двухбайтного числа от главного микроконтроллера, и отобразить его на 4-х разрядном семисегментном индикаторе. В идеале было бы использование аппаратного протокола главным микроконтроллером и его программной реализации на ATtiny13a. Также хотелось бы, что чтобы код реализации протоколов занимал минимально возможное место на флеше, чтобы его потом можно было использовать в других более сложных проектах.

    Оглавление статьи:
  1. Счетчик на ATiny13a и 4-х разрядном семисегментном индикаторе
  2. Простой протокол на счетчике импульсов
  3. Пакетная передача данных с использованием буфера
  4. Программный UART для ATtiny13a
  5. Программный I2C Slave на ATtiny13a

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

Полные исходники вместе со сборочными файлами и скомпилированными прошивками можно скачать по ссылке к конце статьи.

1) Счетчик на ATiny13a и 4-х разрядном семисегментном индикаторе

Первым делом, нам нужен будет драйвер индикатора, с помощью которого мы будем отображать ту или иную цифру. А для тестирования драйвера сделаем простой счетчик на таймере.

Так как подключение и управление 4-х разрядным семисегментным индикатором подробно рассматривалась в статье про сдвиговые регистры, я не буду переливать из пустого в порожнее, и просто приведу готовый код, с небольшими ремарками

Создаём каталоги проекта:

$ mkdir -p ./01_driver/{inc,src}
$ cd ./01_driver

Создаем Makefile:

MCU=attiny13a
OBJCOPY=avr-objcopy
CC=avr-gcc
CFLAGS=-mmcu=$(MCU) -Os -DF_CPU=1200000UL -Wall -I ./inc
LDFLAGS=-mmcu=$(MCU) -Wall -Os  -Werror
OBJ=main.o led.o
TARGET=led
.PHONY: all clean

%.o:	./src/%.c
	$(CC)  -c -o $@ $< $(CFLAGS)
all:	$(OBJ)
	$(CC) $(LDFLAGS) -o $(TARGET).elf  $(OBJ)
	$(OBJCOPY) -O ihex $(TARGET).elf $(TARGET).hex
install:
	avrdude  -p$(MCU) -c usbasp -p t13   -U flash:w:./$(TARGET).hex:i
clean:
	@rm -v $(TARGET).elf $(TARGET).hex $(OBJ)

В директории inc создаем заголовочный файл led.h с объявлением одной-единственной функции:

#ifndef __LED_H__
#define __LED_H__

#define SCLK (1<<PB2)
#define RCLK (1<<PB4)
#define DIO  (1<<PB3)

extern void show_led(uint16_t num);
#endif

в директории src, в файле led.c размещаем код драйвера:

#include <avr/io.h>
#include <led.h>

uint8_t reg=0;

static void spi_transmit(uint8_t data) {
    uint8_t i;

    for (i=0; i<8; i++)
    {
        PORTB=(data & 0x80) ? PORTB | DIO : PORTB & ~DIO;

        data=(data<<1);

        PORTB |= SCLK;
        PORTB &= ~SCLK;
    }
}

static void to_led(uint8_t value, uint8_t reg) {
static uint8_t digit[10] = {
      0b11000000, // 0
      0b11111001, // 1
      0b10100100, // 2
      0b10110000, // 3
      0b10011001, // 4
      0b10010010, // 5
      0b10000010, // 6
      0b11111000, // 7
      0b10000000, // 8
      0b10010000, // 9
};
    PORTB &= ~RCLK;
    spi_transmit(digit[value%10]);
    spi_transmit(1<<reg);
    PORTB |= RCLK;

}

void show_led(uint16_t num) {
        switch (reg) {
        case 0:
            to_led((uint8_t)(num%10),0);
            break;
        case 1:
            if (num>=10)
                to_led((uint8_t)((num%100)/10),1);
            break;
        case 2:
            if (num>=100)
                to_led((uint8_t)(num/100),2);
            break;
        }

        reg = (reg ==  2) ? 0 : reg+1;
}

Этот драйвер мы в дальнейшем трогать не будем, работать мы будем с функцией main.c Для счетчика она выглядит следующим образом:

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <led.h>

volatile uint16_t count;
register unsigned char part asm("r4");

ISR(TIM0_COMPA_vect)
{
    if (part == 9)
    {
        part=0;
        count++;
    }

    part++;
}

int main(void) {
    // GPIO setup
    DDRB |= (SCLK | RCLK | DIO);
    // Timer setup
    TCCR0A=(1<<WGM01);          // set CTC mode
    TCCR0B=(1<<CS02)|(1<<CS00); // prescaller 1/1024
    OCR0A=117;                  // freq 10Hz
    TIMSK0=(1<<OCIE0A);         // enable interrupt
    // init variables
    count=0; // start nmber
    part=1;
    // let's go...
    sei();
    for (;;)
    {
        show_led(count);
        _delay_ms(5);
    }
    return 0;
}

Т.е. линия тактирования SCLK подключается на PB2, защелка RCLK подключается на PB4, и линия данных DIO на PB3. Пины PB0 и PB1 у нас остаются зарезервированными для соединения с главным микроконтроллером. Частота обновления индикатора задается командой _delay_ms(5), и в данном случае она составляет 1000/5 = 200 Гц. Если величину задержки увеличить хотя бы до 10мс, то на индикаторе появится четко выраженное мерцание. И дальше - больше.

В работе конструкция выглядит примерно так:

Прошивка занимает 422 байта флеш-памяти микроконтроллера. Предполагается что микроконтроллер работает на частоте 1.2 МГц.

2) Простой протокол на счетчике импульсов

Моей первой идеей было сделать счетчик импульсов. У нас имеется два прерывания: PCINT0 на PB0 и INT0 на PB1. На первом прерывании пусть будет команда начала отчета, а на втором сам счетчик. Вроде бы проще некуда.

Для ATtiny13a функция main.c будет выглядеть так:

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <led.h>

#define PULSE PB1
#define LATCH PB0

volatile uint16_t count;
volatile uint16_t value;

ISR(PCINT0_vect) {
    if (PINB & (1<<LATCH)) {
        GIMSK &=~(1<<INT0);
        count=value;
    } else {
        GIMSK|=(1<<INT0);
        value=0;
    }
}

ISR(INT0_vect) {
    value++;
}

int main(void) {
    // GPIO setup
    DDRB |= (SCLK | RCLK | DIO); // LATCH & PULSE as INPUT
    // external interrupt
    MCUCR|=(1<<ISC01)|(1<<ISC00); // IRQ on rising edge
    GIMSK|=(1<<PCIE);
    PCMSK|=(1<<PCINT0);
    // init variables
    count=0; // start nmber
    value=0;
    // let's go...
    sei();
    for (;;)
    {
        show_led(count);
        _delay_ms(5);
    }
    return 0;
}

Здесь, в прерывании по PCINT, при прижимании PB0 к земле мы запускаем счетчик, а при высоком урове PB0 счетчик отключается. В прерывании по вектору INT0 находится сам счетчик. Прошивка для "весит" 482 байта.

Для Arduino скетч будет таким:

#define PULSE 3
#define LATCH 2

void send_number(uint16_t value);

void setup() {
    // put your setup code here, to run once:
    pinMode(PULSE,OUTPUT);
    pinMode(LATCH,OUTPUT);
    digitalWrite(PULSE, LOW);
    digitalWrite(LATCH, HIGH);
}

void loop() {
    static uint16_t num;
    num=(num == 573) ? 219 : 573;
    send_number(num);
    delay(1000);
}

void send_number(uint16_t num)
{
    digitalWrite(LATCH, LOW); // START
    delayMicroseconds(50);
    for(int i=0; i<num;i++)
    {
        digitalWrite(PULSE, HIGH);
        digitalWrite(PULSE, LOW);
        delayMicroseconds(50);
    }
    digitalWrite(LATCH, HIGH);
}

Здесь пин 2 Arduino соединяется с PB0 ATtyny13a, а пин 3 Arduino c PB1 ATtiny13a. Также для обоих микроконтроллеров будет не лишним соединить землю.

Arduino посылает на ATtiny13a два попеременно сменяющих друг-друга числа: 573 и 219. Если все сделано как надо, числа будут отображаться на индикаторе.

Ок. Теперь посмотрим что с у нас со скоростью. Скетч в Arduino приведем к следующему виду:

#define PULSE 3
#define LATCH 2

void send_number(uint16_t value);

void setup() {
    Serial.begin(9600);
    // put your setup code here, to run once:
    pinMode(PULSE,OUTPUT);
    pinMode(LATCH,OUTPUT);
    digitalWrite(PULSE, LOW);
    digitalWrite(LATCH, HIGH);
}

void loop() {
    static long previous;
    static uint16_t num;
    num=(num == 573) ? 219 : 573;
    previous=millis();
    send_number(num);
    Serial.print("latency: ");
    Serial.print(millis()-previous);
    Serial.println(" ms");
    delay(1000);
}

void send_number(uint16_t num)
{
    digitalWrite(LATCH, LOW); // START
    delayMicroseconds(50);
    for(int i=0; i<num;i++)
    {
        digitalWrite(PULSE, HIGH);
        digitalWrite(PULSE, LOW);
        delayMicroseconds(50);
    }
    digitalWrite(LATCH, HIGH);
}

Откроем монитор последовательного порта, нам должен будет открыться такой лог:

Т.е. мы имеем 35мс для ~500, значит если мы захочем послать число 2000, то задержка составит ~140мс, а для 8000 - 560мс, целых полсекунды(!).

Если мы попытаемся отправлять числа быстрее, т.е. уменьшить величину задержки в команде delayMicroseconds(50), то числа будут приниматься некорректно, внешние прерывания ATtiny13a будут не успевать обрабатывать входящие прерывания, Arduino все-таки работает на частоте 16 МГц, а "тинька" на 1.2 МГц.

Поэтому увеличить скорость протокола можно будет подняв рабочую частоту ATtiny13a до 9.6 МГц, добавив для этого в начало функции main() две строки:

    CLKPR =(1<<CLKPCE);
    CLKPR =0; // set 9.6 MHz CPU freq

А вот чтобы заставить Arduino передавать в десять раз быстрее, придется прибегнуть к "хардкорному" программированию, родные функции digitalWrite() слишком медлительны для этого. Т.о. функция void send_number(uint16_t num) будет выглядеть как-то так:

void send_number(uint16_t num)
{
    digitalWrite(LATCH, LOW); // START
    delayMicroseconds(10);
    for(int i=0; i<num;i++)
    {
      PORTD |= (1<<PD3);
      PORTD &= ~(1<<PD3);

      for(uint8_t j=0;j<20;j++) // DEALY
          asm volatile("nop");
    }
    digitalWrite(LATCH, HIGH); // STOP
}

Теперь глянем, что у нас у со скоростью:

Скорость выросла в пятнадцать раз, и с таким темпами уже вполне возможно передавать температуру паяльника, т.е. число в пределах 500. А вот если нужно будет передавать число с большим значением, то придется придумать что-то похитрее.

3) Пакетная передача данных с использованием буфера

Мы можем существенно увеличить скорость передачи, если число которое нужно будет передать разложим на множители.

Например, если нам нужно передавать температуру паяльника, т.е. положительное число в пределах от нуля до 500, то мы можем разложить одно число на два с каким-то общим основанием: n=n/основание + n%основание. Вследствие отсутствия аппаратного умножения на ATtiny13a, в качестве основания нас будут интересовать только степени двойки. К примеру, для числа 500 и основания 32, разложение будет таким: 500 = 15*32+20. Тогда передача числа в таком формате займет 15+20=32 импульсов вместо 500.

Однако у нам нет способа обозначить начало передачи. Для разделения чисел можно к одному прибавлять минимальное заведомо большое число, т.е. само основание. А затем на принимающей стороне вычитать его. Тогда передача числа 500 будет занимать 15+32+20=67 импульсов вместо 500. Т.е. мы увеличиваем скорость почти на порядок, и один пакет мы сможем отправлять со скоростью 0.2 мс, тогда пропускная способность нашего протокола составит ~10Кбайт/c(это только теоретически. На самом деле скорость будет существенно ниже).

Ок, посмотрим как все это будет работать на практике. Код для ATtiny13a у меня вышел таким:

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <led.h>

#define PULSE PB1
#define LATCH PB0

volatile uint16_t count;
volatile uint8_t  bf[2];
register uint8_t  idx   asm("r4");
register uint8_t  value asm("r5");


ISR(PCINT0_vect) {
    if (PINB & (1<<LATCH)) {
        GIMSK &=~(1<<INT0);

        bf[idx]=value;
        idx=(idx) ? 0 : 1;

        if ((bf[0] >=32) && (bf[1]<32))
            count=((bf[0]-32)<<5)+bf[1];
        else if ((bf[1] >=32) && (bf[0]<32))
            count=((bf[1]-32)<<5)+bf[0];

    } else {
        GIMSK|=(1<<INT0);
        value=0;
    }
}

ISR(INT0_vect) {
    value++;
}

int main(void) {
    CLKPR =(1<<CLKPCE);
    CLKPR =0; // set 9.6 MHz CPU freq

    // GPIO setup
    DDRB |= (SCLK | RCLK | DIO); // LATCH & PULSE as INPUT
    // external interrupt
    MCUCR|=(1<<ISC01)|(1<<ISC00); // IRQ on rising edge
    GIMSK|=(1<<PCIE);
    PCMSK|=(1<<PCINT0);
    // init variables
    count=0; // start nmber
    value=0;
    idx=0;
    // let's go...
    sei();
    for (;;)
    {
        show_led(count);
        _delay_ms(5);
    }
    return 0;
}

Прошивка занимает 548 байт. Скетч для Arduino выглядит так:

#define PULSE 3
#define LATCH 2

void send_number(uint16_t value);

void setup() {
    Serial.begin(9600);
    // put your setup code here, to run once:
    pinMode(PULSE,OUTPUT);
    pinMode(LATCH,OUTPUT);
    digitalWrite(PULSE, LOW);
    digitalWrite(LATCH, HIGH);
}


void loop() {
  static long previous;
  static uint16_t num;
  num=(num == 573) ? 219 : 573;
  Serial.print("latency: ");
  previous=millis();
  for(byte i=0;i<20;i++) {
    send_number((num/32)+32);
    send_number(num%32);
  }
  Serial.print(millis()-previous);
  Serial.println(" ms");

  delay(1000);
}

void send_number(uint16_t num)
{
    digitalWrite(LATCH, LOW); // START
    delayMicroseconds(10);
    for(int i=0; i<num;i++)
    {
      PORTD |= (1<<PD3);
      PORTD &= ~(1<<PD3);

      for(uint8_t j=0;j<20;j++) // DEALY
          asm volatile("nop");
    }
    digitalWrite(LATCH, HIGH); // STOP
}

Т.к. время передачи числа меньше чем одна миллисекунда, здесь в цикле передается двадцать чисел, после чего замеряется потраченное время. В мониторе последовательного порта получаем такой лог:

Делим полученные цифры на двадцать, и имеем где-то 0.5 мс на передачу одного числа. Прогресс, хотя и не такой сильный как я ожидал ;)

Протокол еще можно улучшать, видоизменять под свои задачи, но в целом, думаю, что здесь все понятно.

4) Программный UART для ATtiny13a

Другим вариантом решения задачи связи двух микроконтроллеров является использование UART-протокола. Т.к. у нас связь полудуплексная, то с помощью UART мы обойдемся всего одной линией, а т.к. Arduino поддерживает UART аппаратно, время затрачиваемое главным микроконтроллером на обслуживание нашей периферии с индикатором, сведется к минимуму. Минусом же будет то, протокол основан на временных задержках и если кто-то начнет "тормозить" то "кина, увы не будет". Поэтому потолком для программных UART'ов считается скорость 9600 bod, т.е. около одного Кбайт/c или передача одно одного байта за одну миллисекунду. Нам же нужно передавать пакет их двух байтов. Т.е. в сравнении с протоколом на счетчике импульсов имеем более медленный протокол с одной стороны, но аппаратную поддержку протокола с другой. Чтож, посмотрим кто кого победит.

Для начала можно попытаться сделать UART приемник для одного байта, т.е. числа в диапазоне от нуля до 256.

Вариант прошивки с UART приемником на прерывании у меня получился таким:

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <led.h>
/*
    PB1 - Rx UART
*/

register uint8_t  k        asm("r4");
register uint8_t  rx_data  asm("r5");
register uint8_t  value    asm("r6");

ISR(INT0_vect){
    // START
    GIMSK &= ~(1<<INT0); // disable
    // cleaning  variables
    k=0; rx_data=0;
    // timer enable
    TCNT0=0;
    TCCR0A=(1<<WGM01);  // set CTC mode
    TCCR0B=(1<<CS01);   // prescaller 1/8
    OCR0A=114;          // rate 9600
    TIMSK0=(1<<OCIE0A); // enable interrupt
}

ISR(TIM0_COMPA_vect){
    if ((PINB & (1<<PB1))) rx_data |=0x80;

    if (k < 8) { // receive data 
        rx_data >>=1;
    } else { // STOP 
        value=rx_data;
        TCCR0B=0; OCR0A=0; TIMSK0=0;
        TIFR0 |= (1 << OCF0A); // Очищаем флаг прерывания (важно!)
        GIFR  |= (1 << INTF0);
        GIMSK |= (1<<INT0); // enable interrupt
    }

    k++;
}
int main(void) {
    // set CPUfreq = 9.6 MHz
    CLKPR =(1<<CLKPCE);
    CLKPR =0; // set 9.6 MHz CPU freq
    // disable ADC
    PRR = (1<<PRADC);
    // ext. irq init
    MCUCR=(1<<ISC01); // falling edge 
    GIMSK=(1<<INT0);
    // GPIO setup
    DDRB |= (SCLK | RCLK | DIO);
    value=0;
    // let's go...
    sei();
    for (;;)
    {
        show_led(value);
        _delay_ms(5);
    }
    return 0;
}

Прошивка весит 476 байт.

Скетч для Arduino будет таким:

void setup() {
    Serial.begin(9600);
    // put your setup code here, to run once:
}

void loop() {
    static uint16_t num;
    num=(num == 43) ? 218 : 43;
    Serial.write(num);
    delay(1000);
}

Между Arduino и ATtiny13a соединяются "земля" между собой и пин 1(Tx) для Arduino соединяется с PB1 ATtiny13a. На индикаторе должны попеременно отображаться цифры 43 и 218.

Если все работает как надо, то код можно немного доработать для передачи двухбайтного числа. Для обозначения начала пакета будем использовать тот же прием, что и в прошлый раз, т.е. разложение на множители. Код для ATtiny13a:

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <led.h>
/*
    PB1 - Rx UART
*/
volatile uint8_t bf[2];
volatile uint16_t  value;
register uint8_t  k        asm("r4");
register uint8_t  rx_data  asm("r5");
register uint8_t  idx      asm("r6");

ISR(INT0_vect){
    // START
    GIMSK &= ~(1<<INT0); // disable
    // cleaning  variables
    k=0; rx_data=0;
    // timer enable
    TCNT0=0;
    TCCR0A=(1<<WGM01);  // set CTC mode
    TCCR0B=(1<<CS01);   // prescaller 1/8
    OCR0A=114;          // rate 9600
    TIMSK0=(1<<OCIE0A); // enable interrupt
}

ISR(TIM0_COMPA_vect){
    if ((PINB & (1<<PB1))) rx_data |=0x80;

    if (k < 8) { // receive data
        rx_data >>=1;
    } else {
        // STOP
        bf[idx]=rx_data;
        idx=(idx) ? 0 : 1;

        if ((bf[0] >=32) && (bf[1]<32))
            value=((bf[0]-32)<<5)+bf[1];
        else if ((bf[1] >=32) && (bf[0]<32))
            value=((bf[1]-32)<<5)+bf[0];

        TCCR0B=0; OCR0A=0; TIMSK0=0;
        TIFR0 |= (1 << OCF0A); // Очищаем флаг прерывания (важно!)
        GIFR  |= (1 << INTF0);
        GIMSK |= (1<<INT0); // enable interrupt
    }

    k++;
}
int main(void) {
    // set CPUfreq = 9.6 MHz
    CLKPR =(1<<CLKPCE);
    CLKPR =0; // set 9.6 MHz CPU freq
    // ext. irq init
    MCUCR=(1<<ISC01); // falling edge for ext. iqr & power_down sleep mode && sleep mode
    GIMSK=(1<<INT0);
    // GPIO setup
    DDRB |= (SCLK | RCLK | DIO);
    value=0; idx=0;
    // let's go...
    sei();
    for (;;)
    {
        show_led(value);
        _delay_ms(5);
    }
    return 0;
}

Прошивка в этот раз потянула 594 байта.

код для Arduino:

void setup() {
    Serial.begin(9600);
}


void loop() {
    static uint16_t num;
    num=(num == 573) ? 219 : 573;
    Serial.write((num/32)+32);
    Serial.write(num%32);
    delay(1000);
}

Замеры показали время на передачу одного пакета в районе 1.4 мс, т.е. 0.7 мс на передачу одного байта.

В данном случае сложно сказать какой протокол лучше. Для передачи небольших значений вполне сгодится счетчик импульсов, для полноценной линии связи лучше все-таки использовать UART. Если большой поток данных, то можно поднять частоту простым изменением коэффициентов. Для счетчика импульсов это недоступно. Через UART можно посылать пакеты с контролем приема и целостности данных. Одним словом, счетчик импульсов это совсем для чего-то простого. А UART протокол штука более универсальная. С него когда-то начинались локальные сети.

Однако повышать скорость программного UART свыше 9600 является решением сомнительным. Если нужна большая скорость, нужно использовать синхронные протоколы.

5) Программный I2C Slave на ATtiny13a

Протокол I2C работает на частоте 100 кГц, т.е. он вдвое быстрее чем UART на скорости 9600 bod. Протокол является двунаправленным, синхронным, высоконадежным протоколом с обратной связью. На одну I2C шину можно посадить сразу несколько устройств. Протокол I2C аппаратно поддерживается Arduino, для ATtiny13a же придется использовать программную реализацию. Готовый драйвер слейва I2C можно скачать с гитхаба здесь: lnx13/twi-slave-software-emulation

Данный драйвер написан для ATtiny13 "без A", но у меня на "A" он без проблем работал, даже mcu-id в Makefile менять не пришлось. Драйвер занимает немного менее 700 байт на флеше, после добавления кода драйвера индикатора, вся прошивка будет "весить" около 970 байт. Т.е. в размер флеш памяти микроконтроллера мы отлично вписываемся.

Драйвер слейва I2C состоит из файлов TWI.h, TWI.c, main.c и makefile. В файл main.c мы поместим драйвер 4-х разрядного семисегментного индикатора, а в остальных нужно будет поправить по одной, две строке.

В TWI.c нужно будет вырезать или закомментировать объявление массива для входящих данных:

//unsigned char incomingBuffer[8]; //!< Incoming buffer

Объявление этого массива нужно будет перенести в TWI.h, чтобы мы имели доступ к нему из main.c:

unsigned char incomingBuffer[8]; //

makefile можно вообще не трогать, или внести незначительные изменения.

а) Переменную MCU можно поменять на с суффиксом "а":

MCU=attiny13a

б) таргет writeflash можно поменять под свой программатор, в моем случае это USBasp:

writeflash: hex
    avrdude  -pattiny13a -c usbasp -p $(PROGRAMMER_MCU) -U flash:w:$(HEXROMTRG):i
#   $(AVRDUDE) -c $(AVRDUDE_PROGRAMMERID)   \
#    -p $(PROGRAMMER_MCU) -P $(AVRDUDE_PORT) -e        \
#    -U flash:w:$(HEXROMTRG)

Файл main.c для нашей задачи будет выглядеть так:

//#include <avr/io.h> //for debug
#include <avr/interrupt.h>
#include <util/delay.h>
#include "TWI.h"

#define SCLK (1<<PB2)
#define RCLK (1<<PB4)
#define DIO  (1<<PB3)

uint8_t reg=0;

static void spi_transmit(uint8_t data) {
    uint8_t i;

    for (i=0; i<8; i++)
    {
        PORTB=(data & 0x80) ? PORTB | DIO : PORTB & ~DIO;

        data=(data<<1);

        PORTB |= SCLK;
        PORTB &= ~SCLK;
    }
}

static void to_led(uint8_t value, uint8_t reg) {
static uint8_t digit[10] = {
      0b11000000, // 0
      0b11111001, // 1
      0b10100100, // 2
      0b10110000, // 3
      0b10011001, // 4
      0b10010010, // 5
      0b10000010, // 6
      0b11111000, // 7
      0b10000000, // 8
      0b10010000, // 9
};
    PORTB &= ~RCLK;
    spi_transmit(digit[value%10]);
    spi_transmit(1<<reg);
    PORTB |= RCLK;

}

static void show_led(uint16_t num) {
        switch (reg) {
        case 0:
            to_led((uint8_t)(num%10),0);
            break;
        case 1:
            if (num>=10)
                to_led((uint8_t)((num%100)/10),1);
            break;
        case 2:
            if (num>=100)
                to_led((uint8_t)(num/100),2);
            break;
        }

        reg=(reg ==  2) ? 0 : reg+1;
}

#define InvBit(reg, bit)   reg ^= (1<<(bit))
#define ClearBit(reg, bit)       reg &= (~(1<<(bit)))
#define SetBit(reg, bit)          reg |= (1<<(bit)) 
#define BitIsSet(reg, bit)       ((reg & (1<<bit)) != 0)


int main() {
    CLKPR =(1<<CLKPCE);
    CLKPR =0; // set 9.6 MHz CPU freq
    DDRB |= (SCLK | RCLK | DIO);

    sei();   //!< Enable global interrupts.

    twi_slave_init();
    twi_slave_enable();

    //SetBit(DDRB, DDB4); //for debug
    uint16_t count;
    for(;;) {
        if (reg==0) count=incomingBuffer[1]+(uint16_t)((incomingBuffer[2])<<8);
        show_led(count);
        _delay_ms(5);
    }
}

Данный слейв I2C работает следующим образом. При начале сессии, т.е. при состоянии START индекс массива устанавливается в ноль,при поступлении данных массив индекс прибавляет к себе по единице. Следить за предотвращением выхода индекса за пределы массива, должен сам программист.

 if (reg==0) count=incomingBuffer[1]+(uint16_t)((incomingBuffer[2])<<8);

При записи, данные записываются в буфер начиная с ячейки 1. В нулевую ячейку записывается I2C адрес, т.е. (0x5D<<1)=0x0BA.

Теперь компилируем:

$ make all

avr-gcc -DF_CPU=9600000 -I.  -mmcu=attiny13 -Os -fpack-struct -fshort-enums -funsigned-bitfields -funsigned-char -Wa,-ahlms=main.lst -c main.c -o main.o
avr-gcc -DF_CPU=9600000 -I.  -mmcu=attiny13 -Os -fpack-struct -fshort-enums -funsigned-bitfields -funsigned-char -Wa,-ahlms=TWI.lst -c TWI.c -o TWI.o
avr-gcc -Wl,-Map,twiLib.out.map -mmcu=attiny13 -lm  -o twiLib.out main.o TWI.o    

Делаем hex файл:

$make hex 

avr-objcopy -j .text                    \
    -j .data                       \
    -O ihex twiLib.out twiLib.hex
avr-objcopy -j .eeprom                  \
    --change-section-lma .eeprom=0 \
    -O ihex twiLib.out twiLib.ee.hex

Прошиваем:

avrdude  -pattiny13a -c usbasp -p attiny13 -U flash:w:twiLib.hex :i

avrdude: warning: cannot set sck period. please check for usbasp firmware update.
avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.00s

avrdude: Device signature = 0x1e9007 (probably t13)
avrdude: NOTE: "flash" memory has been specified, an erase cycle will be performed
         To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: warning: cannot set sck period. please check for usbasp firmware update.
avrdude: reading input file "twiLib.hex"
avrdude: input file twiLib.hex auto detected as Intel Hex
avrdude: writing flash (968 bytes):

Writing | ################################################## | 100% 0.74s

avrdude: 968 bytes of flash written
avrdude: verifying flash memory against twiLib.hex:
avrdude: load data flash data from input file twiLib.hex:
avrdude: input file twiLib.hex auto detected as Intel Hex
avrdude: input file twiLib.hex contains 968 bytes
avrdude: reading on-chip flash data:

Reading | ################################################## | 100% 0.49s

avrdude: verifying ...
avrdude: 968 bytes of flash verified

avrdude: safemode: Fuses OK (E:FF, H:FF, L:6A)

avrdude done.  Thank you.

Получили 968 байт прошивки. Места остается еще на пару строчек кода ;)

На PB0 и PB1 ставятся подтягивающие к питанию резисторы на 4.7К, после чего PB0 соединяется с A5(SCL) Arduino, а PB1 соединяется с A4(SDA) Arduino.

Проверочный скетч для Arduino будет выглядеть например так:

 #include <Wire.h>

void setup() {
  // put your setup code here, to run once:
   Wire.begin();
   Serial.begin(9600);
   while (!Serial);             // Leonardo: wait for serial monitor
   Serial.println("\ntest I2C...");

}

void loop() {
  // put your main code here, to run repeatedly:
   delay(1000);
   static long previous,current;
   previous=millis();
   for(int i=0;i<10;i++) {
      Wire.beginTransmission(0x5D);
      Wire.write(0x3d);
      Wire.write(0x02);
      Wire.endTransmission();
   }
   current=millis();
   Serial.print("latency: ");
   Serial.print(current-previous);
   Serial.println(" ms");

   delay(1000);
   Wire.beginTransmission(0x5D);
   Wire.write(0xdb);
   Wire.write(0x0);
   Wire.endTransmission();
}

На индикаторе должны ежесекундно сменяться цифры 573 и 219. А мониторе последовательного порта будет такая картинка:

Делим 3 на 10, и получаем расход 0.3 мс на каждую I2C сессию. И 0.1 мс на передачу одного байта, т.е. как и должно быть на 100 кГц шине. По-моему, это отлично)

На этом всё, надеюсь не утомил. Полные исходники можно скачать отсюда: protocols.zip