Если под вашу задачу требуется большее число пинов/портов/мегагерц/памяти, чем имеется в используемом вами микроконтроллере, то в ответ на эту проблему обычно советуют взять микроконтроллер "покрупнее". Ответ не лишенный смысла, однако мне удалось найти задачку, от которой так просто не отмахнешься. Героем сегодняшней статьи будет 4-х разрядный семисегментный индикатор с динамической индикацией.
Я уже упоминал о нем в статье про сдвиговые регистры, но тогда у меня не было на руках самой железки, и соответственно говорил я лишь теоретически. Сами ардуинщики об индикаторе отзываются не очень лестно, т.к. применение этого индикатора ограниченное из-за того, что вследствие динамической индикации его нужно постоянно обновлять, что накладывает серьезное ограничение на основную программу. Теоретически эту задачу можно было бы "скинуть" в прерывание таймера, но решение это спорное.
В модуле меня привлекла его компактность. К примеру, для приборной панели паяльной станции, где место сильно ограничено, это то что надо. После некоторого размышления я решил, что в целом модуль неплох, но... для него требуется отдельный управляющий микроконтроллер, сопроцессор, на котором будет крутиться динамическая индикация.
Индикатор не содержит подтягивающих резисторов(!), возможно здесь используются сдвиговые регистры с подтяжкой? Так или иначе, я замерял потребление модуля через EnargyTrace и получил значение около 23mA при питании 3.3 Вольт, что для такой "гирлянды" вполне нормально.
Китайские ATtiny13a в SO-8 корпусе стоят около 15₽, они имеют пять рабочих выводов, три из которых нужно будет отдать на индикатор, остаются два вывода для организации линии связи, что более чем достаточно, но простенький SPI сюда не посадишь, т.к. тот SPI который будет использоваться для управления индикатором, работает мастером, а для связи с "главным" микроконтроллером нужен будет слейв( запускать слейв на главном микроконтроллере - это не вариант). К сожалению или к счастью(смотря как посмотреть), АTtiny13a не поддерживает аппаратно абсолютно никаких протоколов.
Т.о. перед нами стоит задача на ATtiny13a организовать c использованием не более двух пинов скоростную и надежную линию для приема двухбайтного числа от главного микроконтроллера, и отобразить его на 4-х разрядном семисегментном индикаторе. В идеале было бы использование аппаратного протокола главным микроконтроллером и его программной реализации на ATtiny13a. Также хотелось бы, что чтобы код реализации протоколов занимал минимально возможное место на флеше, чтобы его потом можно было использовать в других более сложных проектах.
Т.к. подразумевается использование индикатора для отображения температуры паяльника, во всех примерах будут задействованы только три разряда индикатора.
Полные исходники вместе со сборочными файлами и скомпилированными прошивками можно скачать по ссылке к конце статьи.
Первым делом, нам нужен будет драйвер индикатора, с помощью которого мы будем отображать ту или иную цифру. А для тестирования драйвера сделаем простой счетчик на таймере.
Так как подключение и управление 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 МГц.
Моей первой идеей было сделать счетчик импульсов. У нас имеется два прерывания: 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. А вот если нужно будет передавать число с большим значением, то придется придумать что-то похитрее.
Мы можем существенно увеличить скорость передачи, если число которое нужно будет передать разложим на множители.
Например, если нам нужно передавать температуру паяльника, т.е. положительное число в пределах от нуля до 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 мс на передачу одного числа. Прогресс, хотя и не такой сильный как я ожидал ;)
Протокол еще можно улучшать, видоизменять под свои задачи, но в целом, думаю, что здесь все понятно.
Другим вариантом решения задачи связи двух микроконтроллеров является использование 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 является решением сомнительным. Если нужна большая скорость, нужно использовать синхронные протоколы.
Протокол 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