Вначале я хотел дополнить предыдущую статью входным регистром 74hc165, но потом понял понял, что он заслуживает "свои пять минут славы". Сложности возникают при подключении входного регистра совместно с выходным 74hc595 на один SPI порт. Кроме того, как оказалось, организация работы по SPI в ATmega8 имеет свои интересные особенности.
Итак, сдвиговый регистр 74hc165 преобразует параллельную шину в последовательную, работает только на вход, и имеет разрядность 8-бит. Их так же можно подключать цепочкой из n-элементов, которая даст 8^n - входов.
Руководство на SN74HC165N можно скачать например с сайта Texas Instruments.
Сдвиговый регистр может работать на питании от 2-х до 6-и Вольт, и он имеет комплементарный выход.
Предельные рабочие частоты зависят от уровня питающего напряжения: от 6MHz при двух Вольтах до 62 MHz при шести Вольт.
Распиновка микросхемы выглядит следующим образом:
Здесь A,B,C,D,E,F,H,G - входы параллельного интерфейса, SH/LD - защёлка, CLK - линия тактирования, QH и QH - комплементарный выход, SER - используется для соединения регистров в цепочку.
Для начала, попробуем в Proteus подключить регистр используя bit-banging:
В Proteus немного другие обозначения выводов микросхемы 74hc165, но номера пинов совпадают один в один, и просто сопоставив их, все сразу станет ясно.
Для вывода считываемых значений, и во избежании усложнения схемы и исходного кода пришлось пришлось подключать UART-терминал. UART-протокол неприятен тем, что для своей работы требует точного соблюдения временных задержек, что не всегда корректно работает в симуляторах типа Proteus. При частоте внутреннего резонатора 2 MHz, у меня получилось заставить его работать:
Но не факт, что в другой версии Proteus это будет работать(я использую версию 8.5).
Работа со сдвиговым регистром 74hc165 через bit-banging выглядит так:
#include <inttypes.h> #include <avr/io.h> #include <avr/interrupt.h> #include <avr/sleep.h> #include <util/delay.h> #include <stdio.h> #define CLK PD2 // clock #define SI PD3 // data #define E PD4 // Enter #define PORT PORTD #define DDR DDRD #define PIN PIND static int uart_putchar(char c, FILE *stream) { if (c == '\n') uart_putchar('\r', stream); loop_until_bit_is_set(UCSRA, UDRE); UDR = c; return 0; } static FILE mystdout = FDEV_SETUP_STREAM(uart_putchar, NULL, _FDEV_SETUP_WRITE); int main() { char k; int j; // GPIO setup DDR|=(1<<CLK)|(1<<E); // output DDR &= ~(1<<SI); // input // UART setup UBRRH=0; UBRRL=12; UCSRB=(1<<TXEN); UCSRC=(1<<URSEL)|(3<<UCSZ0); // 8n1 stdout = &mystdout; // main loop while (1) { j=0; PORT &= ~(1<<E); _delay_ms(1); PORT |= (1<<E); for(k=7;k>=0;k--) { _delay_ms(1); j|=((PIN & (1<<SI))<<k); PORT&=~(1<<CLK); _delay_ms(1); PORT|=(1<<CLK); } j=j>>3; printf("count is: %d\n", j); _delay_ms(1000); }; return 0; }
Здесь сначала "дёргается" защёлка, которая фиксирует состояние регистра от изменений, после считывается состояние старшего H-входа, для считывания следующего - "дёргается" линия тактирования, и т.д. Но не все так гладко. По непонятной мне причине, алгоритм "наматывает" три лишних бита, которые приходится удалять командой: j=j>>3. В остальном программа работает без косяков. Результат работы:
Сдвиговые регистры 74HC165 можно соединять каскадом:
показать код#include <inttypes.h> #include <avr/io.h> #include <avr/interrupt.h> #include <avr/sleep.h> #include <util/delay.h> #include <stdio.h> #define CLK PD2 // clock #define SI PD3 // data #define E PD4 // Enter #define PORT PORTD #define DDR DDRD #define PIN PIND static int uart_putchar(char c, FILE *stream) { if (c == '\n') uart_putchar('\r', stream); loop_until_bit_is_set(UCSRA, UDRE); UDR = c; return 0; } static FILE mystdout = FDEV_SETUP_STREAM(uart_putchar, NULL, _FDEV_SETUP_WRITE); int main() { char k; unsigned long j; // GPIO setup DDR|=(1<<CLK)|(1<<E); // output DDR &= ~(1<<SI); // input // UART setup UBRRH=0; UBRRL=12; UCSRB=(1<<TXEN); UCSRC=(1<<URSEL)|(3<<UCSZ0); // 8n1 stdout = &mystdout; // main loop while (1) { j=0; PORT &= ~(1<<E); _delay_ms(1); PORT |= (1<<E); for(k=0;k<16;k++) { _delay_ms(1); j|=((PIN & (1<<SI))<<k); PORT&=~(1<<CLK); _delay_ms(1); PORT|=(1<<CLK); } //j=j>>3; printf("count is: %d\n", (int)(j>>3)); _delay_ms(1000); }; return 0; }
Здесь нижний U2 - получается младшим регистром, а U3 - старшим.
В исходном коде прочитанное значение уже не надо сдвигать на три разряда влево, но баг всплывает в другом месте , старшие три бита U3 не читаются. Из Proteus мне сложно судить о природе багов, на реальном железе я не проверял, кроме того есть другой вариант программной реализации считывающего SPI интерфейса, который был в своё время проверен на "железе", но он отказывается работать в Proteus с 74hc165. В принципе, я предполагал работать с 74hc165 через аппаратный SPI.
Теперь нужно подключить сдвиговый регистр к SPI - порту ATmega8:
Управляющая программа для 74hc165, в случае использования аппаратного SPI интерфейса, будет выглядеть так:
#include <inttypes.h> #include <avr/io.h> #include <avr/interrupt.h> #include <avr/sleep.h> #include <util/delay.h> #include <stdio.h> #define CLK PB5 // clock #define SI PB4 // data #define E PB2 // Enter or CS(chip selector) or SS(slave selector) #define PORT PORTB #define DDR DDRB static int uart_putchar(char c, FILE *stream); static FILE mystdout = FDEV_SETUP_STREAM(uart_putchar, NULL, _FDEV_SETUP_WRITE); char SPI_SlaveReceive(); int main() { char j; // GPIO setup DDR|=(1<<CLK)|(1<<E); // output DDR &= ~(1<<SI); // input // SPI setup SPCR=(1<<SPE)|(1<<MSTR)|(1<<SPR0); // enable SPI, MASTER mode, prescaler 1/16 // UART setup UBRRH=0; UBRRL=12; UCSRB=(1<<TXEN); UCSRC=(1<<URSEL)|(3<<UCSZ0); // 8n1 stdout = &mystdout; // main loop while (1) { PORT &= ~(1<<E); _delay_ms(1); PORT |= (1<<E); j=SPI_SlaveReceive(); printf("count is: %x\n", j); _delay_ms(1000); }; return 0; } char SPI_SlaveReceive() { SPDR=0; /* Wait for reception complete */ while (!(SPSR & (1<<SPIF))); /* Return data register */ return SPDR; } static int uart_putchar(char c, FILE *stream) { if (c == '\n') uart_putchar('\r', stream); loop_until_bit_is_set(UCSRA, UDRE); UDR = c; return 0; }
Все самое интересное тут кроется в функции SPI_SlaveReceive(), а именно: команда SPDR=0 перед циклом while. В функции SPI_SlaveReceive() мы вроде как хотим прочитать порт, и передавать туда ничего не планировали. Однако, в SPI приём и передача происходят одновременно, и чтобы в режиме мастера прочитать порт, нам нужно записать в него какое-нибудь число, чтобы SPI-модуль микроконтроллера "прокрутил" линую тактирования восемь раз. В Arduino этот парадокс имеет похожий вид: uint8_t value = SPI.transfer(0).
SPI-модуль в AVR-микроконтроллерах имеет также своё прерывание, но, например, в книге Юрия Ревича "Практическое программирование микроконтроллеров AVR на языке ассемблера", автор говорит что ему в литературе ни разу не попадались примеры использования этого прерывания.
Помнится, моей первой мыслью было: "Если уж приходиться писать в SPI порт чтобы прокрутить сигналы тактирования в режиме мастера, до почему бы не совместить 74hc165 и 74hc595 регистры? Один работает на вход, дугой на выход, никто никому не мешает, да?". Посмотрим, что из этого получается.
Здесь за ненадобностью убрана часть связанная с UART, а DS линия 74hc595 регистра соединена с выходом 74hc165 QH(В Proteus QH обозначена как SO).
Исходник:
#include <avr/io.h> #include <util/delay.h> #define CLK PB5 // clock #define SI PB4 // data #define E PB2 // Enter #define PORT PORTB #define DDR DDRB int main() { char j; // GPIO setup DDR|=(1<<CLK)|(1<<E); // output DDR &= ~(1<<SI); // input // SPI setup SPCR=(1<<SPE)|(1<<MSTR)|(1<<SPR0); // enable SPI, MASTER mode, prescaler 1/16 // main loop while (1) { PORT &= ~(1<<E); _delay_ms(1); PORT |= (1<<E); SPDR=0; while (!(SPSR & (1<<SPIF))); j=SPDR; _delay_ms(100); }; return 0; }
Т.к. 595-й регистр не подключён к MOSI - линии микроконтроллера, он игнорирует все что пишет микроконтроллер в SPDR. И т.к. он напрямую подключён к выходу 165-го регистра, 595-й передаёт все, что принимает от 165-го регистра. Практически это означает, что сегментами семисегментного индикатора можно управлять с помощью переключателей на 165-м. Это не весть что, но вдруг когда-нибудь пригодится.
Теперь разделим линии данных: MISO оставим на 165-м регистре, а к 595-му протянем MOSI. На сам регистр 74hc595 поставим дисплей HD44780 что бы он отображал коды нажатых клавиш.
Здесь линии данных у каждого регистра свои, в линия SS/CS общая.
Управляющая программа получается такой:
#include <avr/io.h> #include <util/delay.h> /// SPI #define CLK PB5 // clock #define SI PB4 // data #define DS PB3 #define E PB2 // Enter #define PORT PORTB #define DDR DDRB /// LCD #define LCD_RS PD2 #define LCD_E PD3 #define LCD_D4 PD4 #define LCD_D5 PD5 #define LCD_D6 PD6 #define LCD_D7 PD7 #define CMD 0 // command #define DTA 1 // data #define LCD_CLEAR 0x01 #define LCD_OFF 0x08 #define LCD_ON 0x0C #define LCD_RETURN 0x02 char SPI_SlaveReceive(); void SPI_MasterTransmit(uint8_t Data); static int init_lcd(); static int print_lcd(char* str); static int send_lcd(uint8_t value, uint8_t mode); static int print_number(int number); int main() { char j; //uint8_t i=0; // GPIO setup DDR|=(1<<CLK)|(1<<DS)|(1<<E); // output DDR &= ~(1<<SI); // input // SPI setup SPCR=(1<<SPE)|(1<<MSTR)|(1<<SPR0); // enable SPI, MASTER mode, prescaler 1/16 // init LCD init_lcd(); send_lcd(LCD_CLEAR,CMD); send_lcd(0x80,CMD); // position on second line print_lcd("value is:"); // main loop while (1) { PORT &= ~(1<<E); _delay_ms(1); PORT |= (1<<E); j=SPI_SlaveReceive(); send_lcd(0xC8,CMD); // position on second line print_number((int)j); _delay_ms(500); }; return 0; } void SPI_MasterTransmit(uint8_t Data) { PORT &= ~(1<<E); SPDR=Data; while(!(SPSR & (1<<SPIF))); PORT|=(1<<E); _delay_us(10); PORT&=~(1<<E); }; char SPI_SlaveReceive() { SPDR=0; /* Wait for reception complete */ while (!(SPSR & (1<<SPIF))); /* Return data register */ return SPDR; } static int init_lcd() { uint8_t LCD=0; //LCD_PORT|=0xff; // pin 2,3,4,5,6,7 in OUTPUT mode _delay_ms(1); // 4bit mode //LCD&=0b11; // clear LCD=(1<<LCD_D5); SPI_MasterTransmit(LCD); LCD|=(1<<LCD_E); SPI_MasterTransmit(LCD); _delay_ms(1); LCD&=~(1<<LCD_E); SPI_MasterTransmit(LCD); _delay_ms(50); send_lcd(0x28,CMD); // mode: 4bit, 2 lines send_lcd(LCD_OFF,CMD); send_lcd(LCD_CLEAR,CMD); send_lcd(0x06,CMD); // seek mode: right send_lcd(0x0f,CMD); // display ON, Blink ON, Position ON return 0; } // for 4bit mode static int send_lcd(uint8_t value, uint8_t mode) { uint8_t LCD; LCD=0x00; // clear LCD|=(value&0xF0)|(mode<<LCD_RS); SPI_MasterTransmit(LCD); LCD|=(1<<LCD_E); SPI_MasterTransmit(LCD); _delay_us(1); LCD&=~(1<<LCD_E); SPI_MasterTransmit(LCD); _delay_us(10); LCD&=0x03; // clear SPI_MasterTransmit(LCD); LCD|=(value<<4)|(mode<<LCD_RS); SPI_MasterTransmit(LCD); LCD|=(1<<LCD_E); SPI_MasterTransmit(LCD); _delay_us(1); LCD&=~(1<<LCD_E); SPI_MasterTransmit(LCD); if (value == 0x01) _delay_ms(50); else _delay_us(50); return 0; } static int print_lcd(char* str) { uint8_t i=0; while(str[i] !=0 && i<255) send_lcd(str[i++],DTA); return i; }; static int print_number(int number){ // display hex number on LCD followed by a space static const uint8_t symbol[] ="0123456789ABCDEF"; uint8_t n; char i; for(i=12;i>=0;i=i-4){ n= (number>>i) & 0xf; send_lcd(symbol[n],DTA); }; return 0; }
К сожалению, такая схема корректно работает только в Proteus. На реальном железе, при нажатии на тактовые кнопки экран экран дисплея засыпает "кракозябрами".
Чтобы избежать ситуации, когда устройства на одной шине мешают работе друг-друга, следует на них выводить индивидуальные SS/CS линии, т.е. использовать канонический вариант подключения на SPI шину различных устройств. Как-то так:
Алгоритм программы остаётся неизменным, только небольшие изменения, в данном случае, в функции SPI_MasterTransmit(uint8_t Data):
#include <avr/io.h> #include <util/delay.h> /// SPI #define CLK PB5 // clock #define SI PB4 // data #define DS PB3 #define E PB2 // Enter #define EE PB1 // Enter #define PORT PORTB #define DDR DDRB /// LCD #define LCD_RS PD2 #define LCD_E PD3 #define LCD_D4 PD4 #define LCD_D5 PD5 #define LCD_D6 PD6 #define LCD_D7 PD7 #define CMD 0 // command #define DTA 1 // data #define LCD_CLEAR 0x01 #define LCD_OFF 0x08 #define LCD_ON 0x0C #define LCD_RETURN 0x02 uint8_t SPI_SlaveReceive(); void SPI_MasterTransmit(uint8_t Data); static int init_lcd(); static int print_lcd(char* str); static int send_lcd(uint8_t value, uint8_t mode); static int print_number(int number); // display hex number on LCD followed by a space int main(void) { uint8_t j; //uint8_t i=0; // GPIO setup DDR|=(1<<CLK)|(1<<DS)|(1<<E)|(1<<EE); // output DDR &= ~(1<<SI); // input // SPI setup SPCR=(1<<SPE)|(1<<MSTR)|(1<<SPR0); // enable SPI, MASTER mode, prescaler 1/16 // init LCD init_lcd(); send_lcd(LCD_CLEAR,CMD); // main loop for (;;) { j=0; PORT &= ~(1<<E); _delay_ms(1); PORT |= (1<<E); j=SPI_SlaveReceive(); send_lcd(0x80,CMD); // position on second line print_lcd("count is:"); send_lcd(0xC8,CMD); // position on second line print_number((int)j); _delay_ms(1000); } return 0; } void SPI_MasterTransmit(uint8_t Data) { PORT &= ~(1<<EE); SPDR=Data; while(!(SPSR & (1<<SPIF))); PORT|=(1<<EE); _delay_us(10); PORT&=~(1<<EE); }; uint8_t SPI_SlaveReceive() { SPDR=0; while (!(SPSR & (1<<SPIF))); // Wait for reception complete return SPDR; // Return data register } static int init_lcd() { uint8_t LCD=0; //LCD_PORT|=0xff; // pin 2,3,4,5,6,7 in OUTPUT mode _delay_ms(1); // 4bit mode //LCD&=0b11; // clear LCD=(1<<LCD_D5); SPI_MasterTransmit(LCD); LCD|=(1<<LCD_E); SPI_MasterTransmit(LCD); _delay_ms(1); LCD&=~(1<<LCD_E); SPI_MasterTransmit(LCD); _delay_ms(50); send_lcd(0x28,CMD); // mode: 4bit, 2 lines send_lcd(LCD_OFF,CMD); send_lcd(LCD_CLEAR,CMD); send_lcd(0x06,CMD); // seek mode: right send_lcd(0x0f,CMD); // display ON, Blink ON, Position ON return 0; } // for 4bit mode static int send_lcd(uint8_t value, uint8_t mode) { uint8_t LCD; LCD=0x00; // clear LCD|=(value&0xF0)|(mode<<LCD_RS); SPI_MasterTransmit(LCD); LCD|=(1<<LCD_E); SPI_MasterTransmit(LCD); _delay_us(1); LCD&=~(1<<LCD_E); SPI_MasterTransmit(LCD); _delay_us(10); LCD&=0x03; // clear SPI_MasterTransmit(LCD); LCD|=(value<<4)|(mode<<LCD_RS); SPI_MasterTransmit(LCD); LCD|=(1<<LCD_E); SPI_MasterTransmit(LCD); _delay_us(1); LCD&=~(1<<LCD_E); SPI_MasterTransmit(LCD); if (value == 0x01) _delay_ms(50); else _delay_us(50); return 0; } static int print_lcd(char* str) { uint8_t i=0; while(str[i] !=0 && i<255) send_lcd(str[i++],DTA); return i; }; static int print_number(int number){ // display hex number on LCD followed by a space static const uint8_t symbol[] ="0123456789ABCDEF"; uint8_t n; char i; for(i=12;i>=0;i=i-4){ n= (number>>i) & 0xf; send_lcd(symbol[n],DTA); }; return 0; }
Впрочем, в Proteus данный исходник также работает без проблем.
На непаячных макетках вся эта конструкция выглядит так: