Архитектура AVR в примерах/Управление ЖК-дисплеем на основе PCD8544

В этой работе мы рассмотрим управление жидкокристаллическим дисплеем на основе ИС PCD8544[1] на примере дисплея, примененного в Nokia 5110, используя встроенный в МК ATmega8 порт последовательного периферийного интерфейса (англ. SPI.)[2]

Перед началом

править
NB
  • ИС PCD8544 ориентирована на использование напряжения (2.7 ÷ 3.3) V. Без принятия особых мер, использование дисплея в схеме с напряжением питания вне этого диапазона (в т. ч. принятом в Arduino 5 V) может привести к повреждению дисплея.
  • Ввиду сложности монтажа самого дисплея, может иметь смысл использовать готовый модуль для монтажа на макетной панели.

Для данной работы нам потребуется присоединить к простейшему устройству дисплей Nokia 5110 следующим образом.

  • Питание — «общий» к выводу Gnd и + Uи. п. к выводу Vcc дисплея.
  • Линии данных:
    • последовательный периферийный интерфейс: выводы PB2, MOSI, SCk (16, 17, 19; D 10, D 11, D 13) МК ATmega8 — к выводам CE, SDIn, SClk дисплея;
    • управление: выводы PC4, PC5 (27, 28; AI 4, AI 5) МК — к выводам Data (Command), Res дисплея;
    • отметим, что эти входы дисплея могут быть соединены с МК как непосредственно (при обязательном нахождении напряжений в допустимом для дисплея диапазоне), так и через ограничивающие резисторы номиналом порядка 10 kΩ.

Растровое изображение

править

/*** bitm8544.c — Send a bitmap to a PCD8544-based LCD via SPI  -*- C -*- */
#include <assert.h>             /* for _Static_assert () */
#include <avr/interrupt.h>      /* for sei (), ISR () */
#include <avr/io.h>             /* for DDRB, PORTB, etc. */
#include <avr/pgmspace.h>       /* for PROGMEM, etc. */
#include <avr/sleep.h>          /* for sleep_enable (), etc. */
#include <avr/wdt.h>            /* for wdt_enable (), etc. */
#include <stdbool.h>            /* for bool */
#include <stdint.h>             /* for int16_t, etc. */

/* FIXME: huh? */
typedef int16_t ptrdiff_t;

#define LCD_WIDTH   (84)
#define LCD_HEIGHT  (48)

#define PCD8544_CMD_FUNC(pd_p, vrt_p, ext_p) \
  (0x20 | ((pd_p)  ? 4 : 0) | ((vrt_p) ? 2 : 0) | ((ext_p) ? 1 : 0))
/* extended mode commands */
#define PCD8544_EXT_BIAS(bias) \
  (0x10 | (007 & (bias)))
#define PCD8544_EXT_V_OP(v_op) \
  (0x80 | (077 & (v_op)))
/* normal mode commands */
#define PCD8544_CMD_DISP(mem_p, inv_p) \
  (0x08 | ((mem_p) ? 4 : 0) | ((inv_p) ? 1 : 0))
#define PCD8544_CMD_SET_X(x) \
  (0x80 | (127 & (x)))
#define PCD8544_CMD_SET_Y(y) \
  (0x40 | (007 & (y)))

/* Source:  [[commons:File:Wikimedia Community Logo optimized.svg]]
 * Command:
 *    $ convert -ordered-dither c5x5b,2 -resize 82x46 \
 *          -bordercolor white -border 1x1 \
 *          -transpose \
 *          commons/b/b4/Wikimedia_Community_Logo_optimized.svg \
 *          xbm:community_logo_xp.xbm 
 */
/* NB: ensure the bitmap ends up in the flash */
#define static  static const PROGMEM
#include "community_logo_xp.xbm"
#undef  static

static volatile bool seen_spi_interrupt_p   = 0;
ISR (SPI_STC_vect) {
  seen_spi_interrupt_p  = 1;
}

static void
spi_send (char c)
{
  /* NB: assumes interrupts are enabled */
  SPDR  = c;
  while (! seen_spi_interrupt_p) {
    /* sleep until the next event */
    sleep_cpu ();
  }
  seen_spi_interrupt_p = 0;

  /* . */
}

static void
spi_send_P (const char *s, size_t len)
{
  for (ptrdiff_t i = 0; i < len; i++) {
    spi_send (pgm_read_byte (s + i));
  }

  /* . */
}

int
main ()
{
  /* expecting to finish in less than 100 ms */
  wdt_enable (WDTO_2S);

  /* Set up Port B:
     – PB2 (D 10) is /CE (/SS)
     – PB3 (D 11) is MOSI
     – PB5 (D 13) is SCk */
  /* raise /CE */
  PORTB   |=  (1    << PB2);
  DDRB    |= ((1    << DDB5)
              | (1  << DDB3)
              | (1  << DDB2));

  /* Set up Port C:
     – PC4 (AI 4) is Data (/Command)
     – PC5 (AI 5) is /Reset */
  DDRC    |= ((1    << DDC5)
              | (1  << DDC4));

  /* Set up the SPI master */
  SPCR     = ((1    << SPIE)
              | (1  << SPE)
              | (1  << MSTR)
              | (1  << SPR1)
              | (1  << SPR0));

  /* NB: ignoring the datasheet recommendation! */
  sleep_enable ();

  /* enable interrupts */
  sei ();

  /* lower /CE */
  PORTB &= ~ ((1    << PB2));

  /* lower /Reset for 8 SPI ticks */
  PORTC &= ~ ((1    << PC5));
  /* wait for a complete transmission */
  spi_send (42);
  /* raise /Reset back */
  PORTC |=    (1    << PC5);

  /* initialize PCD8544 */
  _Static_assert (community_logo_xp_height <= LCD_WIDTH,
                  "The height of the bitmap exceeds LCD width");
  static const PROGMEM char init_s[] = {
    PCD8544_CMD_FUNC(0, 1, 1),  /* ¬ power down,  vert., extended */
    PCD8544_EXT_BIAS(4),
    PCD8544_EXT_V_OP(0x2f),
    PCD8544_CMD_FUNC(0, 1, 0),  /* ¬ power down,  vert., normal */
    PCD8544_CMD_DISP(1, 0),     /* access memory, ¬ inverse */
    PCD8544_CMD_SET_Y(0),
    /* ensure the bitmap is X-centered centered by setting X */
    PCD8544_CMD_SET_X(((LCD_WIDTH - community_logo_xp_height)
                       >> 1))
  };
  /* lower /Command (Data) */
  PORTC &= ~ ((1    << PC4));
  spi_send_P (init_s, sizeof (init_s));

  /* send the bitmap */
  _Static_assert (community_logo_xp_width == LCD_HEIGHT,
                  "The width of the bitmap has to match LCD height");
  /* raise Data (/Command) */
  PORTC |=    (1    << PC4);
  spi_send_P (community_logo_xp_bits,
              sizeof (community_logo_xp_bits));

  /* raise /CE back */
  PORTB |=    (1    << PB2);

  /* halt */
  wdt_disable ();
  cli ();
  sleep_enable ();
  sleep_cpu ();

  /* not reached */
  /* . */
  return 0;
}
/*** bitm8544.c ends here */

Сборка

править
  1. Внесем в созданный ранее Makefile следующие зависимости:

    default: bitm8544.hex bitm8544
    
    bitm8544: bitm8544.c
    
  2. Создадим файл bitm8544.c приведенного выше содержания, а также файл с целевым растром community_logo_xp.xbm, содержание которого можно найти в приложении к данной работе.

  3. Соберем рассматриваемый пример выполнив команду make:

    $ make bitm8544.hex 
    avr-gcc -O2 -Wall -std=gnu11 -mmcu=atmega8 -DF_CPU=7372800   bitm8544.c   -o bitm8544
    avr-objcopy -O ihex bitm8544 bitm8544.hex
    $ 
    
    NB

    При использовании микроконтроллера, отличного от ATmega8, или же кварцевого резонатора на частоту, отличную от 7.3728 MHz следует явно указать параметры сборки MCU или F_CPU, соответственно.

    Так, для платы Arduino Uno R3, поставляемой с МК ATmega328P и кварцевым резонатором на 20 MHz, команда сборки может быть следующей:

    $ make MCU=atmega328p F_CPU=20000000 bitm8544.hex 
    

    Впрочем, на деле данный пример никак не использует значение тактовой частоты процессора (передаваемое через определение препроцессора F_CPU), и должен сохранить работоспособность вне зависимости от ее конкретного значения.

  4. Удостоверимся в отсутствии ошибок сборки и в появлении файла bitm8544.hex, содержащего результирующий машинный код в формате Intel hex.

Загрузка и проверка работоспособности

править
  1. Загрузку кода в МК выполним аналогично ранее рассмотренному примеру.

  2. Проверим работоспособность кода и устройства, для чего:

    1. подключим питание;
    2. сбросим МК;
    3.  
      пронаблюдаем появление включенного в код выше изображения логотипа сообщества Викимедиа в центре дисплея.


Привет, мир!

править

/*** char8544.c — Send a bitmap to a PCD8544-based LCD via SPI  -*- C -*- */
#include <assert.h>             /* for _Static_assert () */
#include <avr/interrupt.h>      /* for sei (), ISR () */
#include <avr/io.h>             /* for DDRB, PORTB, etc. */
#include <avr/pgmspace.h>       /* for PROGMEM, etc. */
#include <avr/sleep.h>          /* for sleep_enable (), etc. */
#include <avr/wdt.h>            /* for wdt_enable (), etc. */
#include <stdbool.h>            /* for bool */
#include <stdint.h>             /* for int16_t, etc. */

/* NB: ensure bitmap ends up in the flash */
#define static  static const PROGMEM
#include "misc_fixed.xbm"
#undef  static
#define font_bits \
  (misc_fixed_bits)
#define font_coding_offset  (-32)
#define font_glyph_spacing  (1)
#define font_glyph_height   (misc_fixed_width)
#define font_glyph_width    (5)
#define font_glyphs \
  (misc_fixed_height / font_glyph_width)

/* FIXME: huh? */
typedef int16_t ptrdiff_t;

#define LCD_WIDTH   (84)
#define LCD_HEIGHT  (48)

#define PCD8544_CMD_FUNC(pd_p, vrt_p, ext_p) \
  (0x20 | ((pd_p)  ? 4 : 0) | ((vrt_p) ? 2 : 0) | ((ext_p) ? 1 : 0))
/* extended mode commands */
#define PCD8544_EXT_BIAS(bias) \
  (0x10 | (007 & (bias)))
#define PCD8544_EXT_V_OP(v_op) \
  (0x80 | (077 & (v_op)))
/* normal mode commands */
#define PCD8544_CMD_DISP(mem_p, inv_p) \
  (0x08 | ((mem_p) ? 4 : 0) | ((inv_p) ? 1 : 0))
#define PCD8544_CMD_SET_X(x) \
  (0x80 | (127 & (x)))
#define PCD8544_CMD_SET_Y(y) \
  (0x40 | (007 & (y)))

static volatile bool seen_spi_interrupt_p   = 0;
ISR (SPI_STC_vect) {
  seen_spi_interrupt_p  = 1;
}

static void
spi_send (char c)
{
  /* NB: assumes interrupts are enabled */
  SPDR  = c;
  while (! seen_spi_interrupt_p) {
    /* sleep until the next event */
    sleep_cpu ();
  }
  seen_spi_interrupt_p = 0;

  /* . */
}

static void
spi_send_P (const char *s, size_t len)
{
  for (ptrdiff_t i = 0; i < len; i++) {
    spi_send (pgm_read_byte (s + i));
  }

  /* . */
}

static void
pcd8544_draw_char (unsigned char c)
{
  _Static_assert (font_glyph_height == 8,
                  "The height of the font has to be 8");
  ptrdiff_t offset
    = (font_glyph_width * ((ptrdiff_t)c + font_coding_offset));
  spi_send_P (font_bits + offset, font_glyph_width);
  _Static_assert (font_glyph_spacing < INT8_MAX,
                  "Glyph spacing has to be less than INT8_MAX");
  for (int8_t i = 0; i < font_glyph_spacing; i++) {
    spi_send (0);
  }

  /* . */
}

static void
pcd8544_draw_text_P (const char *s)
{
  /* raise Data (/Command) */
  PORTC |=    (1    << PC4);
  const char *p;
  for (p = s; ; p++) {
    char c  = pgm_read_byte (p);
    if (c == '\0') {
      break;
    }
    pcd8544_draw_char (c);
  }

  /* . */
}

int
main ()
{
  /* expecting to finish in less than 100 ms */
  wdt_enable (WDTO_2S);

  /* Set up Port B:
     – PB2 (D 10) is /CE (/SS)
     – PB3 (D 11) is MOSI
     – PB5 (D 13) is SCk */
  /* raise /CE */
  PORTB   |=  (1    << PB2);
  DDRB    |= ((1    << DDB5)
              | (1  << DDB3)
              | (1  << DDB2));

  /* Set up Port C:
     – PC4 (AI 4) is Data (/Command)
     – PC5 (AI 5) is /Reset */
  DDRC    |= ((1    << DDC5)
              | (1  << DDC4));

  /* Set up the SPI master */
  SPCR     = ((1    << SPIE)
              | (1  << SPE)
              | (1  << MSTR)
              | (1  << SPR1)
              | (1  << SPR0));

  /* NB: ignoring the datasheet recommendation! */
  sleep_enable ();

  /* enable interrupts */
  sei ();

  /* lower /CE */
  PORTB &= ~ ((1    << PB2));

  /* lower /Reset for 8 SPI ticks */
  PORTC &= ~ ((1    << PC5));
  /* wait for a complete transmission */
  spi_send (42);
  /* raise /Reset back */
  PORTC |=    (1    << PC5);

  /* initialize PCD8544 */
  static const PROGMEM char init_s[] = {
    PCD8544_CMD_FUNC(0, 0, 1),  /* ¬ power down,  horiz, extended */
    PCD8544_EXT_BIAS(4),
    PCD8544_EXT_V_OP(0x2f),
    PCD8544_CMD_FUNC(0, 0, 0),  /* ¬ power down,  horiz, normal */
    PCD8544_CMD_DISP(1, 0),     /* access memory, ¬ inverse */
    PCD8544_CMD_SET_Y(0),
    PCD8544_CMD_SET_X(0)
  };
  /* lower /Command (Data) */
  PORTC &= ~ ((1    << PC4));
  spi_send_P (init_s, sizeof (init_s));

  /* draw the text */
  static const char PROGMEM text[]
    =  (" \317\360\350\342\345\362, \354\350\360! "
        "              "
        "\253\300\360\365\350\362\345\352\362\363\360\340  "
        "      AVR     "
        "   \342 \357\360\350\354\345\360\340\365\273"
        "(\302\350\352\350\342\345\360\361\350\362\345\362)");
  pcd8544_draw_text_P (text);

  /* raise /CE back */
  PORTB |=    (1    << PB2);

  /* halt */
  wdt_disable ();
  cli ();
  sleep_enable ();
  sleep_cpu ();

  /* not reached */
  /* . */
  return 0;
}
/*** char8544.c ends here */

Сборка

править
  1. Внесем в созданный ранее Makefile следующие зависимости:

    default: char8544.hex char8544
    
    char8544: char8544.c
    
  2. Создадим файл char8544.c приведенного выше содержания, а также файл шрифта misc_fixed.xbm, содержание которого можно найти в приложении к данной работе.

  3. Соберем рассматриваемый пример выполнив команду make:

    $ make char8544.hex 
    avr-gcc -O2 -Wall -std=gnu11 -mmcu=atmega8 -DF_CPU=7372800   char8544.c   -o char8544
    avr-objcopy -O ihex char8544 char8544.hex
    $ 
    
  4. Удостоверимся в отсутствии ошибок сборки и в появлении файла char8544.hex, содержащего результирующий машинный код в формате Intel hex.

Загрузка и проверка работоспособности

править
  1. Загрузку кода в МК выполним аналогично ранее рассмотренному примеру.

  2. Проверим работоспособность кода и устройства, для чего:

    1. подключим питание;
    2. сбросим МК;
    3.  
      пронаблюдаем появление включенного в код выше текста «Привет, мир!» на дисплее.

Исследование

править
  1. Попробуйте изменить код для вывода на дисплей иного изображения. Указание: воспользуйтесь программой convert из пакета ImageMagick, подобно:

    $ convert -ordered-dither c5x5b,2 -resize 82x46 \
          -bordercolor white -border 1x1 \
          -transpose \
          Wikimedia_Community_Logo_optimized.svg \
          community_logo_xp.xbm 
    
  2. Указанное в примере вывода изображения условие возможности spi_send_P для «битового массива» в целом несколько более строго, нежели действительно необходимо. Исправьте это условие (в форме _Static_assert перед последним вызовом spi_send_P) так, чтобы ошибка на этапе сборки диагностировалась для изображений с шириной вне диапазона (41 ÷ 48) пикселей, и удостоверьтесь в правильности работы такого варианта кода используя изображения:

    1. соответствующее новому (но не старому) условию;
    2. не соответствующее ни одному из этих условий.
  3. Предложенный код вывода изображения предполагает, что ширина транспонированного изображения в пикселях (или, что то же, — высота исходного) равна высоте самого дисплея. Несложно дополнить код возможностью вывода изображений меньшего размера одним из следующих двух способов:

    1. дополнением нулями до нужной высоты — подобно тому, как в функции pcd8544_draw_char кода знакогенератора ширина каждого глифа увеличивается на font_glyph_spacing пикселей;
    2. пропуском позиций видеопамяти, не покрываемых изображением, — через периодическую переустановку координат командами дисплея PCD8544_CMD_SET_X, PCD8544_CMD_SET_Y.

    Реализуйте оба подхода и включите в код подходящее изображение. Сравните объемы результирующего кода. Убедитесь в том, что результат работы программы в обоих случаях совершенно одинаков (если дисплей, конечно, действительно «пуст» перед началом работы кода.)

  4. В предложенном варианте знакогенератора, функция pcd8544_draw_text_P не обрабатывает никаких управляющих кодов, включая код разрыва строки '\n' (ASCII LF, 10.) Поэтому, чтобы обеспечить «читаемый» результат, каждая строка включенного в код примера текста дополнена до ширины дисплея (14 символов.)

    Обработку кода разрыва строки можно опять-таки выполнить одним из двух способов:

    1. дополняя выводимую строку нулями до полной ширины;
    2. переустанавливая текущие координаты соответствующими командами дисплея.

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

    Попробуйте реализовать оба подхода. Измените выводимый текст для использования разрывов строк (\n) вместо дополнения пробелами и сравните объемы результирующего кода. Удостоверьтесь, что результат работы программы не изменился по отношению к данному здесь варианту. Добавьте к тексту еще несколько (неполных) строк, чтобы различия между подходами проявились в результирующем изображении.

  5. Функция pcd8544_draw_text_P кода знакогенератора выводит на дисплей текст, хранящийся во flash-памяти МК, что подходит прежде всего для не меняющихся в процессе работы программы сообщений. Реализуйте функцию pcd8544_draw_text, выводящую текст, хранящийся в ОЗУ. Удостоверьтесь в правильности работы функции, объявив в коде буфер подходящего размера и заполнив его функцией sprintf, подобно:

       char buf[12];
       sprintf_P (buf, sizeof (buf),
                  PSTR (" \316\362\342\345\362: %d "), 42);
       pcd8544_draw_text (buf);
    

Примечания

править
  1. PCD8544: 48 × 84 pixels matrix LCD. Проверено 19 июня 2013.
  2. Serial Peripheral Interface – SPI. ATmega8, ATmega8L: 8-bit Atmel with 8 KBytes In-System Programmable Flash. Проверено 29 апреля 2013.