ATmega8 + PCF8574: 8-битный расширитель портов на I2C интерфейсе

разделы: AVR , Arduino , I2C , HD44780 , дата: 24 октября 2017г.

Статья правилась 5-го августа 2022г. Было испрвлено неверное определение: "сдвиговый регистр pcf8574" на правильное: "расширитель портов pcf8574". Добавлено оглавление. Также поправлена битая ссылка на datasheet.

Этот расширитель портов наиболее известен по китайским драйверам дисплея HD44780, которые можно приобрести на али или ибэе. Он довольно подробно был разобран здесь: "Сообщество EasyElectronics.ru: I2C расширитель портов PCF8574". Я в свою очередь, попытаюсь сосредоточиться на программировании микроконтроллера ATmega8 для работы с этим регистром. Впрочем, начну я все же с Arduino и имеющегося у меня зоопарка: ATmega328/MSP430G2553/STM32F103C8.

Расширитель портов PCF8574 может выпускаться разными фирмами, мне попались чипы с суффиксом "T", что обозначает производителя как "NXP Semiconductor". Руководство на pcf8574t можно скачать с официального сайта NXP: "PCF8574; PCF8574A Remote 8-bit I/O expander for I2C-bus with interrupt".

Содержание:

I. Общие сведения

  1. Немного справочной информации
  2. Подключение LCD HD44780 к Arduino через модуль драйвера на PCF8574
  3. Подключение LCD HD44780 к STM32duino через модуль драйвера на PCF8574
  4. Подключение LCD HD44780 к MSP430 Launchpad через модуль драйвера на PCF8574
  5. Arduino библиотека PCF8574

II. Работа PCF8574 + ATmega8

  1. Сканер I2C шины
  2. Бегущий огонь на расширителе портов PCF8574
  3. Управление семисегментным индикатором через расширитель портов PCF8574
  4. Чтение из расширителя портов PCF8574
  5. Подключение LCD HD44780 через расширитель портов PCF8574

Немного справочной информации

    Основные особенности расширителя портов PCF8574:
  1. - он 8-битный, псевдо-двунаправленный;
  2. - работает на "медленной" I2C шине - 100 kHz;
  3. - рабочее напряжение от 2.5 до 6.0 Вольт;
  4. - выпускается в двух вариантах: с суффиксом "A" и без него. Эти варианты различаются I2C адресам 0х3F для чипов с "A" и 0х27 для чипов без А.
  5. - на шину можно ставить до восьми чипов одного варианта или шестнадцать чипов обоих вариантов.
  6. - чипы не соединяются последовательно в "паровозик" как 74HC595, они независимы друг к другу.

На мой взгляд, штука идеальная для подключения дисплея HD44780. Если сравнивать с драйвером на 595-м регистре, то вариант с pcf8574 будет немного дороже, но разница чисто условная: 25р за готовую плату на pcf8574 и ~19р за самодельный вариант на 595-м(3р за чип + 11р за плату + 5р за подстроечный резистор). Сам дисплей HD44780 не слишком скоростной, необходимости в скоростях SPI интерфейса нет. С другой стороны, драйвер на 595-м собирается из рассыпухи "на коленках" за полчаса-час, а посылка из Китая идёт от пары недель в лучшем случае.

Но внешне, готовый вариант все же будет выглядеть более культурно:


дисплей HD44780 c драйвером pcf8574


дисплей HD44780 c самодельным драйвером на 74hc595

Распиновка чипов PCF8574:

Таблица I2C адресов:

Принципиальная схема драйвера дисплея HD44780 (взято с сайта SUNROM):

2) Подключение LCD HD44780 к Arduino через модуль драйвера на PCF8574

Для Arduino существует "стандарт де-факто" библиотека для подключения дисплея HD44780 через модуль драйвера на PCF8574: Arduino-LiquidCrystal-I2C-library.

Перед подключением модуля к дисплею состоит проверить его I2C сканером. И хотя I2C адрес устройства точно задаётся маркировкой микросхемы, и пинами A2, A1, A0, все же могут быть нюансы.

Небольшой HelloWorld для проверки библиотеки Arduino-LiquidCrystal-I2C-library:

#include <LiquidCrystal_I2C.h>

// Set the LCD address to 0x27 for a 16 chars and 2 line display
LiquidCrystal_I2C lcd(0x27, 16, 2);  // for PCF8574
//LiquidCrystal_I2C lcd(0x3f, 16, 2); //for PCF8574A

void setup()
{
    // initialize the LCD
    lcd.begin();

    // Turn on the blacklight and print a message.
    lcd.backlight();
    lcd.print("Hello, world!");
}

void loop()
{
    // set the cursor to column 0, line 1
    // (note: line 1 is the second row, since counting begins with 0):
    lcd.setCursor(0, 1);
    // print the number of seconds since reset:
    lcd.print(millis() / 1000);
}

По сравнению со штатной LiquidCrystal библиотекой, эта обладает дополнительным функционалом: через нее можно включать и выключать подсветку дисплея. Если нужно отрегулировать яркость подсветки, то можно напаять резистор вместо джампика J3 сбоку.

Результат работы:

3) Подключение LCD HD44780 к STM32duino через модуль драйвера на PCF8574

Подключение LCD HD44780 к 3.3 вольтовой логике несколько сложнее. Сам контроллер дисплея может работать от 3.3V уровней, но для выставления контраста дисплея нужна разность потенциалов между VDD и VEE около 4.5 Вольт.

К счастью, в STM2F103C8T6 I2C-выводы толерантны к 5-вольтовом уровням (пометка FT - five volt tolerance):

Значит, подключение к модулю будет таким:

Прошивка после компиляции занимает более 17 килобайт:

В работе все это выглядит так:

4) Подключение LCD HD44780 к MSP430 Launchpad через модуль драйвера на PCF8574

В MSP430G2553 нет пинов толерантных к напряжению +5V, поэтому ничего не остаётся как прибегнуть к преобразователю логических уровней. Для последовательных интерфейсов китайцы продают специальный модуль на мосфетах:

Про него не писал только ленивый, его я и буду использовать.

Схематично, подключение будет таким:

После компиляции прошивка весит чуть более трёх килобайт:

Работает это как-то так:

5) Arduino библиотека PCF8574

Попробуем разобраться с расширителем портов PCF8574 более подробно. В этом деле поможет одноимённая библиотека для Arduino. Она описана на сайте arduino.cc здесь: A class for PCF8574, скачать ее можно с гитхаба на странице автора: https://github.com/RobTillaart/Arduino/tree/master/libraries/PCF8574

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

Заголовочный файл:

//
//    FILE: PCF8574.H
//  AUTHOR: Rob Tillaart
//    DATE: 02-febr-2013
// VERSION: 0.1.9
// PURPOSE: I2C PCF8574 library for Arduino
//     URL: http://forum.arduino.cc/index.php?topic=184800
//
// HISTORY:
// see PCF8574.cpp file
//

#ifndef _PCF8574_H
#define _PCF8574_H

#if defined(ARDUINO) && ARDUINO >= 100
#include "Arduino.h"
#else
#include "WProgram.h"
#endif

#define PCF8574_LIB_VERSION "0.1.9"

#define PCF8574_OK          0x00
#define PCF8574_PIN_ERROR   0x81
#define PCF8574_I2C_ERROR   0x82


class PCF8574
{
public:
    explicit PCF8574(const uint8_t deviceAddress);

    void begin(uint8_t val=0xFF);

    uint8_t read8();
    uint8_t read(uint8_t pin);
    uint8_t value() const { return _dataIn; };

    void write8(const uint8_t value);
    void write(const uint8_t pin, const uint8_t value);
    uint8_t valueOut() const { return _dataOut; }

    //added 0.1.07/08 Septillion
    inline uint8_t readButton8(){ return PCF8574::readButton8(_buttonMask);}
    uint8_t readButton8(const uint8_t mask=0xFF);
    uint8_t readButton(const uint8_t pin);
    void setButtonMask(uint8_t mask);

    // rotate, shift, toggle expect all lines are output
    void toggle(const uint8_t pin);
    void toggleMask(const uint8_t mask);    // invertAll() = toggleMask(0xFF)
    void shiftRight(const uint8_t n=1);
    void shiftLeft(const uint8_t n=1);
    void rotateRight(const uint8_t n=1);
    void rotateLeft(const uint8_t n=1);

    int lastError();

private:
    uint8_t _address;
    uint8_t _dataIn;
    uint8_t _dataOut;
    uint8_t _buttonMask;
    int _error;
};

#endif
//
// END OF FILE
//

Файл с исходным кодом:

//
//    FILE: PCF8574.cpp
//  AUTHOR: Rob Tillaart
//    DATE: 02-febr-2013
// VERSION: 0.1.9
// PURPOSE: I2C PCF8574 library for Arduino
//     URL: http://forum.arduino.cc/index.php?topic=184800
//
// HISTORY:
// 0.1.9  fix warning about return in readButton8()
// 0.1.08 2016-05-20 Merged work of Septillion 
//        Fix/refactor ButtonRead8() - see https://github.com/RobTillaart/Arduino/issues/38
//        missing begin() => mask parameter
// 0.1.07 2016-05-02 (manually merged) Septillion
//        added dataOut so a write() doesn't read first,
//        possibly corrupting a input pin;
//        fixed shift comment, should read 1..7;
//        added begin() to be sure it's in a known state,
//        states could be different if uC is reset and the PCF8574 isn't;
//        added buttonRead() and buttonRead8()
//        which only effect the output while reading
// 0.1.06 (intermediate) added defined errors + refactor rotate
// 0.1.05 2016-04-30 refactor, +toggleMask, +rotLeft, +rotRight
// 0.1.04 2015-05-09 removed ambiguity in read8()
// 0.1.03 2015-03-02 address int -> uint8_t
// 0.1.02 replaced ints with uint8_t to reduce footprint;
//        added default value for shiftLeft() and shiftRight()
//        renamed status() to lastError();
// 0.1.01 added value(); returns last read 8 bit value (cached);
//        value() does not always reflect the latest state of the pins!
// 0.1.00 initial version
//

#include "PCF8574.h"

#include <Wire.h>

PCF8574::PCF8574(const uint8_t deviceAddress)
{
    _address = deviceAddress;
    _dataIn = 0;
    _dataOut = 0xFF;
    _buttonMask = 0xFF;
    _error = PCF8574_OK;
}

void PCF8574::begin(uint8_t val)
{
  Wire.begin();
  PCF8574::write8(val);
}

// removed Wire.beginTransmission(addr);
// with  @100KHz -> 265 micros()
// without @100KHz -> 132 micros()
// without @400KHz -> 52 micros()
// TODO @800KHz -> ??
uint8_t PCF8574::read8()
{
    if (Wire.requestFrom(_address, (uint8_t)1) != 1)
    {
        _error = PCF8574_I2C_ERROR;
        return _dataIn; // last value
    }
#if (ARDUINO <  100)
    _dataIn = Wire.receive();
#else
    _dataIn = Wire.read();
#endif
    return _dataIn;
}

void PCF8574::write8(const uint8_t value)
{
    _dataOut = value;
    Wire.beginTransmission(_address);
    Wire.write(_dataOut);
    _error = Wire.endTransmission();
}

uint8_t PCF8574::read(const uint8_t pin)
{
    if (pin > 7)
    {
        _error = PCF8574_PIN_ERROR;
        return 0;
    }
    PCF8574::read8();
    return (_dataIn & (1 << pin)) > 0;
}

void PCF8574::write(const uint8_t pin, const uint8_t value)
{
    if (pin > 7)
    {
        _error = PCF8574_PIN_ERROR;
        return;
    }
    if (value == LOW)
    {
        _dataOut &= ~(1 << pin);
    }
    else
    {
        _dataOut |= (1 << pin);
    }
    write8(_dataOut);
}

void PCF8574::toggle(const uint8_t pin)
{
    if (pin > 7)
    {
        _error = PCF8574_PIN_ERROR;
        return;
    }
    toggleMask(1 << pin);
}

void PCF8574::toggleMask(const uint8_t mask)
{
    _dataOut ^= mask;
    PCF8574::write8(_dataOut);
}

void PCF8574::shiftRight(const uint8_t n)
{
    if (n == 0 || n > 7) return;
    _dataOut >>= n;
    PCF8574::write8(_dataOut);
}

void PCF8574::shiftLeft(const uint8_t n)
{
    if (n == 0 || n > 7) return;
    _dataOut <<= n;
    PCF8574::write8(_dataOut);
}

int PCF8574::lastError()
{
    int e = _error;
    _error = PCF8574_OK;
    return e;
}

void PCF8574::rotateRight(const uint8_t n)
{
    uint8_t r = n & 7;
    _dataOut = (_dataOut >> r) | (_dataOut << (8-r));
    PCF8574::write8(_dataOut);
}

void PCF8574::rotateLeft(const uint8_t n)
{
    rotateRight(8- (n & 7));
}

//added 0.1.07/08 Septillion
uint8_t PCF8574::readButton8(const uint8_t mask)
{
    uint8_t temp = _dataOut;
    PCF8574::write8(mask | _dataOut);
    PCF8574::read8();
    PCF8574::write8(temp);
    return _dataIn;
}

//added 0.1.07 Septillion
uint8_t PCF8574::readButton(const uint8_t pin)
{
    if (pin > 7)
    {
        _error = PCF8574_PIN_ERROR;
        return 0;
    }
    uint8_t temp = _dataOut;
    PCF8574::write(pin, HIGH);
    uint8_t rtn = PCF8574::read(pin);
    PCF8574::write8(temp);
    return rtn;
}

//added 0.1.08 Septillion
void PCF8574::setButtonMask(uint8_t mask){
  _buttonMask = mask;
}

//
// END OF FILE
//

Для начала попробуем помигать светодиодом через расширитель портов pcf8574. Для этого загрузим из примеров библиотеки скетч PCF8574_test:

Этот скетч позволяет командой через UART установить на 4-м выводе регистра логическое состояние: высокое(команда H), низкое(команда L) или противоположное(команда T).

Высокий уровень на выводе регистра формируется подтяжкой к pullup резистору и в таком состоянии, через вывод может проходить только лишь 0.1 мА, что будет недостаточно даже для работы светодиода. Поэтому включать светодиод нужно будет при низком логическом уровне, или ставить транзисторный ключ:

Я лично использовал RGB-светодиод с общим анодом, он как раз включается выставлением земли на ножке соответствующего цвета.

При высоком логическом уровне на выводе, его можно беспрепятственно замыкать на землю. При этом я намерял мультиметром ток 112 мкА, что примерно соответствует величине указанной в документации - 100мкА. При низком логическом уровне замыкать вывод на питание не надо, это приведёт к короткому замыканию.

Ещё забавный пример в стиле ёлочной гирлянды можно составить на основе демо-примера PCF8574_test2:

#include "PCF8574.h"

// adjust addresses if needed
PCF8574 PCF_39(0x3F);  // for PCF8574A
//PCF8574 PCF_39(0x27);  // for PCF8574

void setup()
{
    PCF_39.begin();
}

void loop()
{
    PCF_39.write(0, 1);
    for (int i=0; i<7; i++)
    {
        PCF_39.rotateLeft();
        delay(100);
    }

    for (int i=0; i<7; i++)
    {
        PCF_39.rotateRight();
        delay(100);
    }

    for (int i=0; i<7; i++)
    {
        PCF_39.rotateLeft(3);
        delay(100);
    }

    for (int i=0; i<7; i++)
    {
        PCF_39.rotateRight(2);
        delay(100);
    }

    for (int i=0; i<255; i++)
    {
        PCF_39.toggleMask(i);
        delay(100);
    }
}

Здесь по умолчанию прописан I2C адрес 0x3F для чипа PCF8574A. К выводам расширителя портов нужно подключить светодиоды с токоограничивающими резисторами.

Если вспомнить спецификацию I2C протокола, за одну сессию в I2C устройство можно или писать или читать. Или/или Т.е. одни пины оставить на вход другие на выход не получиться. Согласно документации , чтобы настроить ножку на вход, нужно сначала записать в соответствующий разряд единицу. Тогда вывод через подтягивающий резистор соединяется с питанием, после чего его можно безбоязненно замыкать на землю. По умолчанию, при включении расширителя портов, его выводы подключены к подтягивающим резисторам. Т.е. его состояние при включении равняется 0xff.

Из примеров библиотеки можно загрузить скетч ReadButton. Каждый раз при замыкании вывода расширителя портов P0 c землёй, будет переключаться светодиод на плате Arduino. Кроме того, через UART выводится состояние расширителя портов. Лог работы выглядит как-то так:

Здесь младший бит - это переключатель, нолик в четвёртом разряде это подсветка дисплея, она аппаратно по умолчанию замкнута на землю.

6) Работа с расширителем портов PCF8574 в ATmega8. Сканер I2C

Для работы с I2C я буду использовать аппаратный I2C интерфейс ATmega8. Готовые функции для работы с ним взял из поста двухлетней давности: ATmega8 + аппаратный TWI модуль: "делаем распечатку памяти RTC DS1307". Я исправил ошибку в функции send_i2c() и немного "причесал" код, но все равно, на мой взгляд он еще очень сырой.

Первым делом нужно будет сделать I2C сканер, чтобы знать, что микроконтроллер видит чип PCF8574. Для сканера потребуется UART интерфейс, чтобы выдавать на него результаты сканирования. Как его настроить в Proteus я рассказывал в предыдущей статье: Чтение сдвигового регистра 74hc165 через программный SPI интерфейс. Я использовал проект оттуда в качестве заготовки. Хотя, технически, UART в Proteus не нужен т.к. есть хороший I2C отладчик который гораздо лучше чем UART - терминал. Но на реальном железе он все же пригодиться. Итак, делаем.

Схема в Proteus:

Программа:

#include <inttypes.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <util/delay.h>
#include <stdio.h>

//General Master staus codes
#define START       0x08    //START has been transmitted
#define REP_START   0x10
#define MT_DATA_ACK 0x28
#define MT_SLA_ACK  0x18

//Master Transmitter staus codes
#define MT_ADR_ACK  0x18    //SLA+W has been tramsmitted and ACK received
#define MT_ADR_NACK 0x20    //SLA+W has been tramsmitted and NACK received

#define MT_DATA_ACK 0x28    //Data byte has been tramsmitted and ACK received
#define MT_DATA_NACK 0x30   //Data byte has been tramsmitted and NACK received
#define MT_ARB_LOST 0x38    //Arbitration lost in SLA+W or data bytes

#define WRITE       0x00
#define READ        0x01

#define READ_END    0x01
#define READ_NOEND  0x00

#define ERROR       0x01
#define SUCCESS     0x00

static int uart_putchar(char c, FILE *stream);
static uint8_t send_i2c(uint8_t value);
static uint8_t start_i2c(uint8_t d_adr);
static inline void stop_i2c();

static FILE mystdout = FDEV_SETUP_STREAM(uart_putchar, NULL, _FDEV_SETUP_WRITE);

int main()
{
    // UART setup
    UBRRH=0; UBRRL=12;
    UCSRB=(1<<TXEN);
    UCSRC=(1<<URSEL)|(3<<UCSZ0); // 8n1   
    stdout = &mystdout;
    // I2C setup 
    TWBR = (F_CPU / 100000UL - 16)/2; // TWI bitrate
    // main loop
    uint8_t i;
    for(;;)
    {
        printf("start scanning... \n");
        for(i=0;i<256; i+=2)
        {
            if (start_i2c(i) != ERROR)
            {
                printf("address found: 0x%x\n", (int)i);
                _delay_ms(1000);
            }

            _delay_ms(10);
            stop_i2c();
            _delay_ms(10);
        }
        _delay_ms(5000);
    };
    return 0;
 }

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;
}
uint8_t send_i2c(uint8_t value)
{
     TWDR = value;
     TWCR = (1<<TWINT) | (1<<TWEN);
     // wail until transmission completed and ACK/NACK has been received
     while(!(TWCR & (1<<TWINT))) {};
     // check value of TWI Status Register. Mask prescaler bits.

     value = TWSR & 0xF8;
     return (value == MT_SLA_ACK || value == MT_DATA_ACK) ? SUCCESS : ERROR;
}

static uint8_t start_i2c(uint8_t d_adr)
{
    TWCR=(1<<TWINT) | (1<<TWSTA) | (1<<TWEN); // START
    while (!(TWCR & (1<<TWINT))) {};

    uint8_t twst; twst = (TWSR & 0xF8); // check value of TWI Status Register. Mask prescaler bits.
    if ((twst != START) && (twst != REP_START))
        return ERROR;
    uint8_t ret; ret=send_i2c(d_adr);
    return ret;
};

static inline void stop_i2c()
{
    TWCR=(1<<TWINT) | (1<<TWEN) | (1<<TWSTO);
}

Результат работы:

Здесь 4e это число 0x27 сдвинутое влево на один разряд, т.е. (0x27<<1).

Немного изменённый вариант для вывода полученного адреса на семисегментные индикаторы вместо UART'а, в железе выглядит так:

Удобная штукень, не требует компьютера чтобы проверить какую-нибудь железку на I2C на наличие отклика.

7) Работа с расширителем портов PCF8574 в ATmega8. Бегущий огонь на расширителе портов PCF8574

Теперь попробуем что-нибудь писать в регистр pcf8574.

Собираем в Proteus схему для бегущего огня:

Текст программы будет следующим:

#include <inttypes.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <util/delay.h>

//General Master staus codes
#define START       0x08    //START has been transmitted
#define REP_START   0x10
#define MT_DATA_ACK 0x28
#define MT_SLA_ACK  0x18

//Master Transmitter staus codes
#define MT_ADR_ACK  0x18    //SLA+W has been tramsmitted and ACK received
#define MT_ADR_NACK 0x20    //SLA+W has been tramsmitted and NACK received

#define MT_DATA_ACK 0x28    //Data byte has been tramsmitted and ACK received
#define MT_DATA_NACK 0x30   //Data byte has been tramsmitted and NACK received
#define MT_ARB_LOST 0x38    //Arbitration lost in SLA+W or data bytes

#define WRITE       0x00
#define READ        0x01

#define READ_END    0x01
#define READ_NOEND  0x00

#define ERROR       0x01
#define SUCCESS     0x00

#define I2C_ADR_PCF8574 0x27

static uint8_t send_i2c(uint8_t value);
static uint8_t start_i2c(uint8_t d_adr);
static inline void stop_i2c();
static uint8_t write_i2c(uint8_t ADR, uint8_t value);

int main()
{
    // I2C setup
    TWBR = (F_CPU / 100000UL - 16)/2; // TWI bitrate
    // main loop
    uint8_t i;
    for(;;)
    {
        for(i=0;i<8; i++)
        {
            write_i2c((I2C_ADR_PCF8574<<1), ~(1<<i));
            _delay_ms(1000);
        }
        _delay_ms(5000);
    };
   return 0;
 }

// I2C /////////////////////////////////////////////////////////////////////////
static uint8_t write_i2c(uint8_t ADR, uint8_t value) {
   uint8_t ret;
    if (start_i2c(ADR) != ERROR)
    {
        ret=send_i2c(value);
        stop_i2c();
    } else {
        stop_i2c();
        ret=ERROR;
    }
    return ret;
}

uint8_t send_i2c(uint8_t value)
{
     TWDR = value;
     TWCR = (1<<TWINT) | (1<<TWEN);
     // wail until transmission completed and ACK/NACK has been received
     while(!(TWCR & (1<<TWINT))) {};
     // check value of TWI Status Register. Mask prescaler bits.

     value = TWSR & 0xF8;
     return (value == MT_SLA_ACK || value == MT_DATA_ACK) ? SUCCESS : ERROR;
}

static uint8_t start_i2c(uint8_t d_adr)
{
     TWCR=(1<<TWINT) | (1<<TWSTA) | (1<<TWEN); // START
     while (!(TWCR & (1<<TWINT))) {};

     uint8_t twst; twst = (TWSR & 0xF8); // check value of TWI Status Register. Mask prescaler bits.
     if ((twst != START) && (twst != REP_START))
         return ERROR;
     return send_i2c(d_adr);
};

static inline void stop_i2c()
{
     TWCR=(1<<TWINT) | (1<<TWEN) | (1<<TWSTO);
}

8) Работа с расширителем портов PCF8574 в ATmega8. Управление семисегментным индикатором через расширитель портов PCF8574

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

Схема:

Программа:

#include <inttypes.h>
#include <avr/io.h>
#include <util/delay.h>

//General Master staus codes
#define START       0x08    //START has been transmitted
#define REP_START   0x10
#define MT_DATA_ACK 0x28
#define MT_SLA_ACK  0x18

//Master Transmitter staus codes
#define MT_ADR_ACK  0x18    //SLA+W has been tramsmitted and ACK received
#define MT_ADR_NACK 0x20    //SLA+W has been tramsmitted and NACK received

#define MT_DATA_ACK 0x28    //Data byte has been tramsmitted and ACK received
#define MT_DATA_NACK 0x30   //Data byte has been tramsmitted and NACK received
#define MT_ARB_LOST 0x38    //Arbitration lost in SLA+W or data bytes

#define WRITE       0x00
#define READ        0x01

#define READ_END    0x01
#define READ_NOEND  0x00

#define ERROR       0x01
#define SUCCESS     0x00

#define I2C_ADR_PCF8574 0x27

static uint8_t send_i2c(uint8_t value);
static uint8_t start_i2c(uint8_t d_adr);
static inline void stop_i2c();
static uint8_t write_i2c(uint8_t ADR, uint8_t value);
void seven_segment(uint8_t Data);

int main()
{
    // I2C setup
    TWBR = (F_CPU / 100000UL - 16)/2; // TWI bitrate
    // main loop
    for(;;)
    {
        write_i2c((I2C_ADR_PCF8574<<1), 0xff);
        uint8_t i;
        for(i=0;i<10;i++)
        {
            seven_segment(i);
            _delay_ms(1000);
        }
    };
    return 0;
 }

void seven_segment(uint8_t Data) {
    static const uint8_t seg[10]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
    if (Data < 10 )
       write_i2c((I2C_ADR_PCF8574<<1), ~seg[Data]);
};

static uint8_t write_i2c(uint8_t ADR, uint8_t value) {
   uint8_t ret;
    if (start_i2c(ADR) != ERROR)
    {
        ret=send_i2c(value);
        stop_i2c();
    } else {
        stop_i2c();
        ret=ERROR;
    }
    return ret;
}

uint8_t send_i2c(uint8_t value)
{
     TWDR = value;
     TWCR = (1<<TWINT) | (1<<TWEN);
     // wail until transmission completed and ACK/NACK has been received
     while(!(TWCR & (1<<TWINT))) {};
     // check value of TWI Status Register. Mask prescaler bits.

     value = TWSR & 0xF8;
     return (value == MT_SLA_ACK || value == MT_DATA_ACK) ? SUCCESS : ERROR;
}

static uint8_t start_i2c(uint8_t d_adr)
{
     TWCR=(1<<TWINT) | (1<<TWSTA) | (1<<TWEN); // START
     while (!(TWCR & (1<<TWINT))) {};

     uint8_t twst; twst = (TWSR & 0xF8); // check value of TWI Status Register. Mask prescaler bits.
     if ((twst != START) && (twst != REP_START))
         return ERROR;
     return send_i2c(d_adr);
};

static inline void stop_i2c()
{
     TWCR=(1<<TWINT) | (1<<TWEN) | (1<<TWSTO);
}

9) Работа с расширителем портов PCF8574 в ATmega8. Чтение из расширителя портов PCF8574

Для чтения из pcf8574, сначала в соответствующий разряд нужно записать единицу(1).

Сделаем простую схему опроса кнопок:

Здесь снова потребуется UART для вывода состояния регистра.

Программа:

#include <inttypes.h>
#include <avr/io.h>
#include <util/delay.h>
#include <stdio.h>

//General Master staus codes
#define START       0x08    //START has been transmitted
#define REP_START   0x10
#define MT_DATA_ACK 0x28
#define MT_SLA_ACK  0x18

//Master Transmitter staus codes
#define MT_ADR_ACK  0x18    //SLA+W has been tramsmitted and ACK received
#define MT_ADR_NACK 0x20    //SLA+W has been tramsmitted and NACK received

#define MT_DATA_ACK 0x28    //Data byte has been tramsmitted and ACK received
#define MT_DATA_NACK 0x30   //Data byte has been tramsmitted and NACK received
#define MT_ARB_LOST 0x38    //Arbitration lost in SLA+W or data bytes

#define WRITE       0x00
#define READ        0x01

#define READ_END    0x01
#define READ_NOEND  0x00

#define ERROR       0x01
#define SUCCESS     0x00

#define READ_END    0x01
#define READ_NOEND  0x00

#define I2C_ADR_PCF8574 0x27

static int uart_putchar(char c, FILE *stream);
static uint8_t send_i2c(uint8_t value);
static uint8_t start_i2c(uint8_t d_adr);
static inline void stop_i2c();
static uint8_t get_i2c(uint8_t END);
static uint8_t write_i2c(uint8_t ADR, uint8_t value);
static uint8_t read_i2c(uint8_t ADR);

static FILE mystdout = FDEV_SETUP_STREAM(uart_putchar, NULL, _FDEV_SETUP_WRITE);

int main()
{
    // GPIO setup 

    // UART setup
    UBRRH=0; UBRRL=12; // 9600, 2MHz
    UCSRB=(1<<TXEN);
    UCSRC=(1<<URSEL)|(3<<UCSZ0); // 8n1
    stdout = &mystdout;
    // I2C setup 
    TWBR = (F_CPU / 100000UL - 16)/2; // TWI bitrate
    // main loop
    for(;;)
    {
        printf("start session... \n");
        write_i2c((I2C_ADR_PCF8574<<1), 0xff); // setup to read
        uint8_t data;
        for(;;)
        {
            data=read_i2c(I2C_ADR_PCF8574<<1);
            printf("data is: %x\n",data);
            _delay_ms(1000);
        }
    };
    return 0;
 }

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 uint8_t write_i2c(uint8_t ADR, uint8_t value) {
   uint8_t ret;
    if (start_i2c(ADR) != ERROR)
    {
        ret=send_i2c(value);
        stop_i2c();
    } else {
       stop_i2c();
       ret=ERROR;
    }
    return ret;
}

static uint8_t read_i2c(uint8_t ADR) {
   uint8_t ret;
   start_i2c(ADR|READ);
   ret=get_i2c(READ_END);
   stop_i2c();

   return ret;
}
uint8_t send_i2c(uint8_t value)
{
     TWDR = value;
     TWCR = (1<<TWINT) | (1<<TWEN);
     // wail until transmission completed and ACK/NACK has been received
     while(!(TWCR & (1<<TWINT))) {};
     // check value of TWI Status Register. Mask prescaler bits.

     value = TWSR & 0xF8;
     return (value == MT_SLA_ACK || value == MT_DATA_ACK) ? SUCCESS : ERROR;
}

static uint8_t start_i2c(uint8_t d_adr)
{
     TWCR=(1<<TWINT) | (1<<TWSTA) | (1<<TWEN); // START
     while (!(TWCR & (1<<TWINT))) {};

     uint8_t twst; twst = (TWSR & 0xF8); // check value of TWI Status Register. Mask prescaler bits.
     if ((twst != START) && (twst != REP_START))
         return ERROR;
     return send_i2c(d_adr);
};

static inline void stop_i2c()
{
     TWCR=(1<<TWINT) | (1<<TWEN) | (1<<TWSTO);
}

static uint8_t get_i2c(uint8_t END)
{
     if (END)
         TWCR = (1<<TWINT)|(1<<TWEN);
     else
         TWCR = (1<<TWINT)|(1<<TWEN)|(1<<TWEA);

     while(!(TWCR & (1<<TWINT)));

     return TWDR; // return data
}

В процессе работы лог в I2C отладчике должен быть такого вида:

10) Работа с расширителем портов PCF8574 в ATmega8. Подключение LCD HD44780 через расширитель портов PCF8574

Ну и собственно, то ради чего все затевалось: подключение HD44780-совместимого дисплея к ATmega8 через расширитель PCF8574 по двум проводам I2C.

Схема в Proteus:

Текст программы:

#include <inttypes.h>
#include <avr/io.h>
#include <util/delay.h>

// LCD HD44780
#define LCD_RS  PD0
#define LCD_RW  PD1
#define LCD_E   PD2
#define LCD_BL  PD3
#define LCD_D4  PD4
#define LCD_D5  PD5
#define LCD_D6  PD6
#define LCD_D7  PD7

#define LCD_CLEAR       0x01
#define LCD_OFF         0x08
#define LCD_ON          0x0C
#define LCD_RETURN      0x02

//General Master staus codes
#define START       0x08    //START has been transmitted
#define REP_START   0x10
#define MT_DATA_ACK 0x28
#define MT_SLA_ACK  0x18

#define CMD 0 // command
#define DTA 1 // data

//Master Transmitter staus codes
#define MT_ADR_ACK  0x18    //SLA+W has been tramsmitted and ACK received
#define MT_ADR_NACK 0x20    //SLA+W has been tramsmitted and NACK received

#define MT_DATA_ACK 0x28    //Data byte has been tramsmitted and ACK received
#define MT_DATA_NACK 0x30   //Data byte has been tramsmitted and NACK received
#define MT_ARB_LOST 0x38    //Arbitration lost in SLA+W or data bytes

#define WRITE       0x00
#define READ        0x01

#define READ_END    0x01
#define READ_NOEND  0x00

#define ERROR       0x01
#define SUCCESS     0x00

#define I2C_ADR_PCF8574 0x27
// I2C
static uint8_t send_i2c(uint8_t value);
static uint8_t start_i2c(uint8_t d_adr);
static inline void stop_i2c();
static uint8_t write_i2c(uint8_t ADR, uint8_t value);
/// LCD
static int  send_lcd(uint8_t value, uint8_t mode);
static int print_lcd(char* str);
static int init_lcd();
static int print_number(int number);

int main()
{
    // I2C setup
    TWBR = (F_CPU / 100000UL - 16)/2; // TWI bitrate
    // main loop
    init_lcd();
    send_lcd(LCD_CLEAR,CMD);
    int j=0;
    for(;;)
    {
        send_lcd(0x80,CMD); // position on second line
        print_lcd("count is:");
        send_lcd(0xC8,CMD); // position on second line
        print_number(j++);

        _delay_ms(1000);
    };
    return 0;
 }
 // LCD //////////////////////////////////////////////
static int init_lcd()
{
    uint8_t LCD;
    // 4bit mode
    LCD=(1<<LCD_D5)|(1<<LCD_E); write_i2c((I2C_ADR_PCF8574<<1), LCD);
    LCD&=~(1<<LCD_E); write_i2c((I2C_ADR_PCF8574<<1), 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
    send_lcd(0x0c,CMD); // display ON, Blink OFF, Position OFF
    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;
};

// for 4bit mode
static int  send_lcd(uint8_t value, uint8_t mode)
{
    uint8_t LCD;

    LCD=(value & 0xF0)|(mode<<LCD_RS)|(1<<LCD_E)|(1<<LCD_BL); write_i2c((I2C_ADR_PCF8574<<1), LCD);
    LCD&=~(1<<LCD_E); write_i2c((I2C_ADR_PCF8574<<1), LCD);
    _delay_us(10);

    LCD=(value<<4)|(mode<<LCD_RS)|(1<<LCD_E)|(1<<LCD_BL); write_i2c((I2C_ADR_PCF8574<<1), LCD);
    LCD&=~(1<<LCD_E); write_i2c((I2C_ADR_PCF8574<<1), LCD);

    if (value == 0x01)
        _delay_ms(50);
    else
        _delay_us(50);

    return 0;
}
static int print_number(int number){  // display hex number on LCD followed by a space
    static const uint8_t symbol[16] ="0123456789ABCDEF";
    uint8_t n;
    char i;
    for(i=12;i>=0;i=i-4){
        n= (number>>i) & 0xf;
    if (n<16)
        send_lcd(symbol[n],DTA);
    };
    return 0;
}

// I2C /////////////////////////////////////////////////////////////////////////
static uint8_t write_i2c(uint8_t ADR, uint8_t value) {
   uint8_t ret;
    if (start_i2c(ADR) != ERROR)
    {
        ret=send_i2c(value);
        stop_i2c();
    } else {
       stop_i2c();
       ret=ERROR;
    }
    return ret;
}

uint8_t send_i2c(uint8_t value)
{
     TWDR = value;
     TWCR = (1<<TWINT) | (1<<TWEN);
     // wail until transmission completed and ACK/NACK has been received
     while(!(TWCR & (1<<TWINT)));
     // check value of TWI Status Register. Mask prescaler bits.

     value = TWSR & 0xF8;
     return (value == MT_SLA_ACK || value == MT_DATA_ACK) ? SUCCESS : ERROR;
}

static uint8_t start_i2c(uint8_t d_adr)
{
     TWCR=(1<<TWINT) | (1<<TWSTA) | (1<<TWEN); // START
     while (!(TWCR & (1<<TWINT)));

     uint8_t twst = (TWSR & 0xF8); // check value of TWI Status Register. Mask prescaler bits.

     return ((twst != START) && (twst != REP_START)) ? ERROR : send_i2c(d_adr);
};

static inline void stop_i2c()
{
     TWCR=(1<<TWINT) | (1<<TWEN) | (1<<TWSTO);
}
// END OF FILE ////////////////////////////////////////////////////////////

Код был протестированной на реальной ATmega8 с кварцем на 16MHz - работает.

Немного видоизменённый вариант с семисегментными индикаторами по SPI в железе выглядит так: