Зеркало сайта: vivacious-stockings-frog.cyclic.app

ATmega8 + Proteus: работа со сдвиговыми регистром 74HC595

разделы: AVR , SPI , Proteus , HD44780 , дата: 26 сентября 2017г.

    Сдвиговые регистры, оглавление:
  1. ATmega8 + Proteus: работа со сдвиговыми регистром 74HC595
  2. ATmega8 + Proteus: входной сдвиговый регистр 74HC165, совместная работа с 74hc595
  3. ATmega8 + Arduino + Proteus: 8-битный сдвиговой регистр на I2C интерфейсе PCF8574

Изучение модуля USI MSP430 странным образом(на самом деле закономерным) вывела меня на такую штуку, как сдвиговый регистр. Имея о них лишь общее представление, мне пришлось срочно разбираться c этой, довольно обширной темой. Итак.

Сдвиговый регистр, он же расширитель портов, он же шинный преобразователь, преобразует сигнал последовательной шины в параллельный или/и обратно.

В рамках этой статью я рассмотрю работу с популярным 8-и битовыми сдвиговым регистром на SPI интерфейсе 74HC595.

В качестве практических примеров, я рассмотрю подключение светодиодной гирлянды, семисегментных индикаторов и дисплея с параллельной шиной HD44780.

В качестве микроконтроллера я буду использовать ATmega8, а в качестве среды моделирования Proteus 8.5.

Кроме этого, я затрону организацию SPI интерфейса у ATmega8.

    Оглавление:
  1. Сдвиговый регистр 74HC595 c SPI интерфейсом;
  2. Управление гирляндой светодиодов;
  3. SPI интерфейс в микроконтроллере ATmega8;
  4. Подключение семисегментного индикатора через сдвиговый регистр 74HC595;
  5. Подключение жидко-кристаллического дисплея HD44780 через сдвиговый регистр 74HC595;

1) Сдвиговый регистр 74HC595 c SPI интерфейсом

Это один из самых простых регистров, который преобразует последовательную шину в параллельную. Он позволяет получить из трех выводов микроконтроллера - 8^n.

    Описание
  • Микросхема принимает на вход последовательность 8-битных данных, которые затем преобразует в логические состояния на 8-пиновом выходе.
  • Микросхема работает только на выход, т.е. мы можем с ее помощью управлять светодиодами или дисплеем HD44780, но не сможем с нее получать данные с датчиков например.
  • Выходы могут принимать состояния: логический ноль, логическую единицу, высокоимпедансное состояние - HiZ.
  • Микросхемы можно соединять каскадом для получения 16-битного выхода, 24-битного, и т.д.
  • Питание микросхемы 74HC595N может варьироваться от двух до шести Вольт.
  • Сдвиговый регистр 74HC595N может работать на частотах до 100MHz.

Микросхема часто используется как драйвер семисегментных индикаторов или дисплея HD44780. Документацию на чип можно скачать например отсюда.

Распиновка микросхемы выглядит следующим образом:

Здесь, Q0 - Q7 - это цифровые выходы. MR - это reset. OE - переводит выводы в HiZ режим. Q'7 - это бит переполнения, используется для соединения регистров каскадом. DS - линия передачи данных, SH - линия тактирования, ST - защелка(latch), но мне привычнее такие штуки называть Enter'ом.

В рабочем состоянии, OE должен быть соединен с землей, а MR подтянут к питанию. Ведущий микроконтроллер может менять состояние DS при низком уровне линии тактирования - SH. Чип считывает состояние линии DS при растущем фронте на линии тактирования SH.

Прием данных сдвиговым регистром происходит при низком уровне защелки - ST. При этом принимаемые данные идут во внутренний (теневой) регистр(на самом деле там одна цепочка триггеров). При выставлении защелки ST в высокий уровень, содержимое теневого регистра записывается в регистр вывода, и выходы Q0 - Q7 принимают состояние в соответствии с его содержимым.

Данные посылаются старшим вперед.

Временная диаграмма сигналов:

2) Управление гирляндой светодиодов

Для знакомства с работой сдвигового регистра 74HC595 , в Proteus соберем такую схему:

    Всего в передаче данных задействовано три пина.
  • на PD2 - линия тактирования, SH;
  • на PD3 - линия передачи данных, DS;
  • на PD4 - линия синхронизации данных, ST.

Этими тремя пинами мы можем управлять теперь восемью светодиодами.

Составим программу бегущих огней:

#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.

3) SPI интерфейс в микроконтроллере ATmega8

То, что мы используем 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.

4) Подключение семисегментного индикатора через сдвиговый регистр 74HC595

Сдвиговый регистр часто используется как драйвер семисегментного индикатора. Наличие бита переноса позволяет составлять каскад из нескольких разрядов.

Вместо светодиодов подключим пока один индикатор с общим катодом:

Для работы с ним запустим такую программу:

#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 рассматривалось здесь пару лет назад.

Результат работы программы должен выглядеть как-то так:

5) Подключение жидко-кристаллического дисплея HD44780 через сдвиговый регистр 74HC595

Широко известный в узких кругах дисплей с параллельной шиной, также можно подключать через сдвиговый регистр.

Для начала следует проверить работу симуляции дисплея в 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. Мне лично этот фокус показался бесполезным, но упомянуть о нем считаю нужным.