Изучение модуля USI MSP430 странным образом(на самом деле закономерным) вывела меня на такую штуку, как сдвиговый регистр. Имея о них лишь общее представление, мне пришлось срочно разбираться c этой, довольно обширной темой. Итак.
Сдвиговый регистр, он же расширитель портов, он же шинный преобразователь, преобразует сигнал последовательной шины в параллельный или/и обратно.
В рамках этой статью я рассмотрю работу с популярным 8-и битовыми сдвиговым регистром на SPI интерфейсе 74HC595.
В качестве практических примеров, я рассмотрю подключение светодиодной гирлянды, семисегментных индикаторов и дисплея с параллельной шиной HD44780.
В качестве микроконтроллера я буду использовать ATmega8, а в качестве среды моделирования Proteus 8.5.
Кроме этого, я затрону организацию SPI интерфейса у ATmega8.
Это один из самых простых регистров, который преобразует последовательную шину в параллельную. Он позволяет получить из трех выводов микроконтроллера - 8^n.
Микросхема часто используется как драйвер семисегментных индикаторов или дисплея HD44780. Документацию на чип можно скачать например отсюда.
Распиновка микросхемы выглядит следующим образом:
Здесь, Q0 - Q7 - это цифровые выходы. MR - это reset. OE - переводит выводы в HiZ режим. Q'7 - это бит переполнения, используется для соединения регистров каскадом. DS - линия передачи данных, SH - линия тактирования, ST - защелка(latch), но мне привычнее такие штуки называть Enter'ом.
В рабочем состоянии, OE должен быть соединен с землей, а MR подтянут к питанию. Ведущий микроконтроллер может менять состояние DS при низком уровне линии тактирования - SH. Чип считывает состояние линии DS при растущем фронте на линии тактирования SH.
Прием данных сдвиговым регистром происходит при низком уровне защелки - ST. При этом принимаемые данные идут во внутренний (теневой) регистр(на самом деле там одна цепочка триггеров). При выставлении защелки ST в высокий уровень, содержимое теневого регистра записывается в регистр вывода, и выходы Q0 - Q7 принимают состояние в соответствии с его содержимым.
Данные посылаются старшим вперед.
Временная диаграмма сигналов:
Для знакомства с работой сдвигового регистра 74HC595 , в Proteus соберем такую схему:
Этими тремя пинами мы можем управлять теперь восемью светодиодами.
Составим программу бегущих огней:
#include <inttypes.h> #include <avr/io.h> #include <avr/interrupt.h> #include <avr/sleep.h> #include <util/delay.h> #define CLK PD2 // clock #define DS PD3 // data #define E PD4 // Enter #define PORT PORTD #define DDR DDRD int main() { DDR|=(1<<CLK)|(1<<DS)|(1<<E); // output // Write your code here while (1) { char i,k; int j; for(i=0;i<8;i++) { j=(1<<i); PORT &= ~(1<<E); for(k=0;k<8;k++) { PORT&=~(1<<CLK); _delay_ms(1); PORT=(j & 0x80) ? PORT | (1<<DS) : PORT & ~(1<<DS); PORT|=(1<<CLK); _delay_ms(1); j=j<<1; } PORT|=(1<<E); _delay_ms(1000); } }; return 0; }
Результат работы:
Теперь, если мы хотим соединить регистры каскадом, чтобы получить 16 выводов, всего-то нужно лишь немного видоизменить принципиальную схему:
Для работы видоизмененной схемы в коде нужно будет поменять всего три константы:
показать код#include <inttypes.h> #include <avr/io.h> #include <avr/interrupt.h> #include <avr/sleep.h> #include <util/delay.h> #define CLK PD2 // clock #define DS PD3 // data #define E PD4 // Enter #define PORT PORTD #define DDR DDRD int main() { DDR|=(1<<CLK)|(1<<DS)|(1<<E); // output // Write your code here while (1) { char i,k; int j; for(i=0;i<16;i++) { j=(1<<i); PORT &= ~(1<<E); for(k=0;k<16;k++) { PORT&=~(1<<CLK); _delay_ms(1); PORT=(j & 0x8000) ? PORT | (1<<DS) : PORT & ~(1<<DS); PORT|=(1<<CLK); _delay_ms(1); j=j<<1; } PORT|=(1<<E); _delay_ms(1000); } }; return 0; }
Все это хорошо, но: "При чем тут SPI, и всякие шины?" - спросите вы?
А вот при чем. Эта короткая программка является программной реализацией протокола SPI.
То, что мы используем SPI, проверяется очень просто - переключением в коде на использование аппаратного SPI модуля.
В руководстве на ATmega8, SPI модуль описан следующей блок-схемой:
В блок-схеме можно увидеть внутренний сдвиговый регистр, блок контроля с флаговым и регистром управления, и блок тактирования.
Передача данных по SPI между двумя устройствами происходит по такой схеме:
Здесь мастером(ведущим) выступает микроконтроллер ATmega8, а ведомым в нашем случае выступает 74hc595. При передаче старший бит мастера записывается в младший бит слейва, и через восемь тактов, они обмениваются одним байтом.
В работе SPI модуля в ATmega8 задействовано всего три регистра: SPCR - регистр управления, SPSR - флаговый регистр, SPDR - регистр данных.
Регистр управления выглядит так:
Здесь SPIE - включает прерывания по завершении приема или передачи байта из SPDR, SPE - включает модуль SPI, DORD - переключает направление в сдвиговом регистре, MSTR - определяет режим работы микроконтроллера: ведущий или ведомый, CPOL - переключает полярность линии тактирования, CPHA - переключает фазу линии тактирования. Оставшиеся два бита SPR0, SPR1 и SPI2X из SPSR, устанавливают предделитель для линии тактирования.
Регистр SPSR имеет три служебных бита:
Здесь нам будет интересен флаг вызова перерывания SPIF.
В руководстве на ATmega8 имеются примеры работы с SPI-модулем на ассемблере и Си. В последнем случае пример выглядит так:
Для проверки программы нужно будет переподключить сдвиговый регистр к SPI порту ATmega8:
Тестовая программа с использованием SPI будет выглядеть так:
#include <inttypes.h> #include <avr/io.h> #include <avr/interrupt.h> #include <avr/sleep.h> #include <util/delay.h> #define CLK PB5 // clock #define DS PB3 // data #define E PB2 // Enter #define PORT PORTB #define DDR DDRB void SPI_MasterTransmit(char Data); int main() { // SPI setup DDR|=(1<<CLK)|(1<<DS)|(1<<E); // output SPCR=(1<<SPE)|(1<<MSTR)|(1<<SPR0); // enable SPI, MASTER mode, prescaler 1/16 while (1) { char i,j; for(i=0;i<8;i++) { j=(1<<i); PORT &= ~(1<<E); SPI_MasterTransmit(j); PORT|=(1<<E); _delay_ms(1000); } }; return 0; } void SPI_MasterTransmit(char Data) { SPDR=Data; while(!(SPSR & (1<<SPIF))); };
И если все сделать правильно, "бегущий огонь" должен работать через SPI.
Сдвиговый регистр часто используется как драйвер семисегментного индикатора. Наличие бита переноса позволяет составлять каскад из нескольких разрядов.
Вместо светодиодов подключим пока один индикатор с общим катодом:
Для работы с ним запустим такую программу:
#include <inttypes.h> #include <avr/io.h> #include <avr/interrupt.h> #include <avr/sleep.h> #include <util/delay.h> #define CLK PB5 // clock #define DS PB3 // data #define E PB2 // Enter #define PORT PORTB #define DDR DDRB void SPI_MasterTransmit(uint8_t Data); int main() { // SPI setup DDR|=(1<<CLK)|(1<<DS)|(1<<E); // output SPCR=(1<<SPE)|(1<<MSTR)|(1<<SPR0); // enable SPI, MASTER mode, prescaler 1/16 while (1) { char i; for(i=0;i<10;i++) { PORT &= ~(1<<E); SPI_MasterTransmit(i); PORT|=(1<<E); _delay_ms(1000); } }; return 0; } void SPI_MasterTransmit(uint8_t Data) { static const uint8_t seg[10]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; SPDR=(Data < 10)? seg[Data] : Data; while(!(SPSR & (1<<SPIF))); };
Результат работы должен быть как на гифке сверху.
Семисегментные индикаторы тоже можно подключать каскадом:
Здесь верхний сдвиговый регистр подключен к крайне правому сегменту, он является младшим регистром. Нижний сдвиговый регистр подключается к левому сегменту, он является старшим регистром. Текст программы при этом получается такой:
#include <inttypes.h> #include <avr/io.h> #include <avr/interrupt.h> #include <avr/sleep.h> #include <util/delay.h> #define CLK PB5 // clock #define DS PB3 // data #define E PB2 // Enter #define PORT PORTB #define DDR DDRB void SPI_MasterTransmit(uint8_t Data); int main() { // SPI setup DDR|=(1<<CLK)|(1<<DS)|(1<<E); // output SPCR=(1<<SPE)|(1<<MSTR)|(1<<SPR0); // enable SPI, MASTER mode, prescaler 1/16 while (1) { char i; for(i=0;i<100;i++) { SPI_MasterTransmit(i); _delay_ms(1000); } }; return 0; } void SPI_MasterTransmit(uint8_t Data) { PORT &= ~(1<<E); static const uint8_t seg[10]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; if (Data <100) { SPDR=seg[Data/10]; while(!(SPSR & (1<<SPIF))); SPDR=seg[Data%10]; while(!(SPSR & (1<<SPIF))); } PORT|=(1<<E); _delay_us(10); PORT&=~(1<<E); };
Кроме такого способа подключения семисегментнных индикаторов, который называют статическим, существует еще динамический способ, когда, допустим, один сдвиговый регистр подключается к сегментам индикаторов соединенных параллельно, а другой регистр с высокой скоростью неуловимой глазом, переключает общий анод или катод элементов. Такие штуки продают на Али уже в сборе со сдвиговыми регистрами 74hc595:
На скорую руку я набросал в Proteus схему такого индикатора:
Здесь индикатор с общим анодом, следовательно сегмент будет загораться при подаче логического нуля. Чтобы обеспечить совместимость с программой для управления индикаторами с общим катодом, я поставил на вход логические инверторы. Хотя, вместо этого можно было бы поменять значения в массиве seg[10], но здесь я хотел показать как делать логические инверторы без использования корпусных микросхем.
Чтобы обеспечить динамическую индикацию, нужно постоянно обновлять информацию на дисплее. Я не нашел ничего проще, чем использовать прерывание по таймеру:
#include <inttypes.h> #include <avr/io.h> #include <avr/interrupt.h> #include <avr/sleep.h> #include <util/delay.h> #define CLK PB5 // clock #define DS PB3 // data #define E PB2 // Enter #define PORT PORTB #define DDR DDRB void SPI_MasterTransmit(uint8_t Data); volatile uint8_t spi_data; static const uint8_t seg[10]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; ISR(TIMER0_OVF_vect) { uint8_t t=spi_data; SPI_MasterTransmit(0x80|t/1000); t=t%1000; SPI_MasterTransmit(0x40|(t/100)); t=t%100; SPI_MasterTransmit(0x20|(t/10)); SPI_MasterTransmit(0x10|(t%10)); } int main() { // TIMER0 Setup TIMSK =(1<<TOIE0); // timer0 enable TCCR0 = (1<<CS00); // prescaler 1/1 // SPI setup DDR|=(1<<CLK)|(1<<DS)|(1<<E); // output SPCR=(1<<SPE)|(1<<MSTR)|(1<<SPR0); // enable SPI, MASTER mode, prescaler 1/16 sei(); for(;;) { for(spi_data=0;spi_data<1000;spi_data++) { _delay_ms(10); } }; return 0; } void SPI_MasterTransmit(uint8_t Data) { PORT &= ~(1<<E); SPDR=((Data & 0xf0) >> 4); while(!(SPSR & (1<<SPIF))); SPDR=seg[(Data & 0x0f)]; while(!(SPSR & (1<<SPIF))); PORT|=(1<<E); _delay_us(10); PORT&=~(1<<E); };
Это программа только для Proteus. Для реального устройства должны быть изменены значения временных задержек. В главном цикле должно стоять 1000ms вместо десяти, и прерывание по таймеру можно запускать не так часто. Напомню, что прерывание в ATmega8 по таймеру TIMER0 рассматривалось здесь пару лет назад.
Результат работы программы должен выглядеть как-то так:
Широко известный в узких кругах дисплей с параллельной шиной, также можно подключать через сдвиговый регистр.
Для начала следует проверить работу симуляции дисплея в Proteus. Для этого составляется такая схема:
В качестве управляющей программы служит следующая программа:
#include <avr/io.h> #include <util/delay.h> #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 LCD_PORT DDRD #define LCD PORTD #define CMD 0 // command #define DTA 1 // data #define LCD_CLEAR 0x01 #define LCD_OFF 0x08 #define LCD_ON 0x0C #define LCD_RETURN 0x02 // for 4bit mode static int send_lcd(uint8_t value, uint8_t mode) { LCD&=0x03; // clear LCD|=(value&0xF0)|(mode<<LCD_RS); LCD|=(1<<LCD_E); _delay_us(1); LCD&=~(1<<LCD_E); _delay_us(10); LCD&=0x03; // clear LCD|=(value<<4)|(mode<<LCD_RS); LCD|=(1<<LCD_E); _delay_us(1); LCD&=~(1<<LCD_E); 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 init_lcd() { 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); LCD|=(1<<LCD_E); _delay_ms(1); LCD&=~(1<<LCD_E); _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; } int main(void) { init_lcd(); send_lcd(LCD_CLEAR,CMD); for(;;) { _delay_ms(1000); send_lcd(0x80,CMD); // position on second line print_lcd("Hello"); send_lcd(0xC0,CMD); // position on second line print_lcd(" World!"); }; return 0; }
Это модифицированная версия программы из примера двухлетней давности: "ATmega8: простая программа управления ЖК-дисплеем HD44780. В отличии от оригинала, здесь строб подается отдельно от данных, что наверно более корректно.
Теперь рисуем схему подключения дисплея через сдвиговый регистр:
Управляющую программу я сделал по принципу минимального изменения предыдущего кода, чтобы было понятно, как это делается:
#include <avr/io.h> #include <util/delay.h> #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 LCD_PORT DDRD //define LCD PORTD #define CMD 0 // command #define DTA 1 // data #define LCD_CLEAR 0x01 #define LCD_OFF 0x08 #define LCD_ON 0x0C #define LCD_RETURN 0x02 #define CLK PB5 // clock #define DS PB3 // data #define E PB2 // Enter #define PORT PORTB #define DDR DDRB void SPI_MasterTransmit(char Data); // 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 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; } int main(void) { // SPI setup DDR|=(1<<CLK)|(1<<DS)|(1<<E); // output SPCR=(1<<SPE)|(1<<MSTR)|(1<<SPR0); // enable SPI, MASTER mode, prescaler 1/16 init_lcd(); send_lcd(LCD_CLEAR,CMD); for(;;) { _delay_ms(1000); //send_lcd(LCD_RETURN,CMD); send_lcd(0x80,CMD); // position on second line print_lcd("Hello"); send_lcd(0xC0,CMD); // position on second line print_lcd(" World!"); }; return 0; } void SPI_MasterTransmit(char Data) { PORT &= ~(1<<E); SPDR=Data; while(!(SPSR & (1<<SPIF))); PORT|=(1<<E); _delay_us(10); PORT&=~(1<<E); };
В отличие от работы через параллельную шину, здесь нельзя прочитать порт, поэтому пришлось водить дополнительную переменную LCD, с которой и осуществляются все операции. Остальное думаю понятно.
В заключение хочу сказать, что кроме SPI, сдвиговый регистр возможно подключать через OneWire интерфейс по одному проводу через RC-цепочки. Изначально это задумывалось для микроконтроллеров в корпусах с малым количеством пинов, на вроде ATtiny13. Но это можно использовать также для SoC c малым количеством выводов, например: ESP8266 или RT5350F. Мне лично этот фокус показался бесполезным, но упомянуть о нем считаю нужным.