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

ATmega8 + Proteus: входной сдвиговый регистр 74HC165, совместная работа с 74hc595

разделы: AVR , SPI , Proteus , HD44780 , дата: 13 октября 2017г.

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

Вначале я хотел дополнить предыдущую статью входным регистром 74hc165, но потом понял понял, что он заслуживает "свои пять минут славы". Сложности возникают при подключении входного регистра совместно с выходным 74hc595 на один SPI порт. Кроме того, как оказалось, организация работы по SPI в ATmega8 имеет свои интересные особенности.

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

Руководство на SN74HC165N можно скачать например с сайта Texas Instruments.

    Оглавление:
  1. Чтение сдвигового регистра 74hc165 через программный SPI интерфейс;
  2. Чтение сдвигового регистра 74hc165 через аппаратный SPI ATmega8;
  3. Ретранслятор параллельной шины;
  4. LCD дисплей HD44780 и сдвиговый регистр 74HC165(вариант для Proteus);
  5. LCD дисплей HD44780 и сдвиговый регистр 74HC165(вариант для реального устройства);

1) Чтение сдвигового регистра 74hc165 через программный SPI интерфейс

Сдвиговый регистр может работать на питании от 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.

2) Чтение сдвигового регистра 74hc165 через аппаратный SPI ATmega8

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

3) Ретранслятор параллельной шины

Помнится, моей первой мыслью было: "Если уж приходиться писать в 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-м. Это не весть что, но вдруг когда-нибудь пригодится.

4) LCD дисплей HD44780 и сдвиговый регистр 74HC165(вариант для Proteus)

Теперь разделим линии данных: 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. На реальном железе, при нажатии на тактовые кнопки экран экран дисплея засыпает "кракозябрами".

5) LCD дисплей HD44780 и сдвиговый регистр 74HC165(вариант для реального устройства)

Чтобы избежать ситуации, когда устройства на одной шине мешают работе друг-друга, следует на них выводить индивидуальные 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 данный исходник также работает без проблем.

На непаячных макетках вся эта конструкция выглядит так: