ATmega8 + PCD8544: работа с графическим дисплеем от телефонов Nokia 5110/3310

разделы: AVR , SPI , PCD8544 , дата: 23 ноября 2017г.

Дисплей от мобильного телефона Nokia 5110/3310 - это довольно известный графический дисплей начального уровня на управляющем контроллере pcd8544. Цена на него колеблется в пределах ста рублей, что сделало его широко распространенным "народным" дисплеем, наряду со знакогенерирующим LCD1602. Несмотря на широкое распространение, имеющаяся информация по этому дисплею или противоречива, или обрывочна(например). Ниже приводится лог недельного изучения девайса с примерами кода от простого к сложному. Вначале я пытался использовать Proteus для большей наглядности, но его возможностей, увы, хватило только на три примера. Поэтому остальные примеры идут для реальной связки ATmega8+дисплей. Так или иначе примеры с Proteus работают и на настоящем устройстве. Ко всем примерами, исходники к ним, скомпилированные прошивками и сборочные Makefile можно скачать здесь https://gitlab.com/flank1er/pcd8544_atmega8.

Должен сразу сказать, что для русификации дисплея я использовал кодировку CP866, и сделал я это удобным для Linux-пользователей способом. В Windows это получится использовать разве что из CYGWIN (понадобится утилита iconv).

Также должен упомянуть, что у меня были сложности с управлением дисплеем через аппаратный SPI. Она заключается в том, что и USBasp, и дисплей используют SPI порт ATmega8, и чтобы загрузить прошивку в микроконтроллер, дисплей приходится отключать. Это просто неудобно. Поэтому почти во всех примерах используется подключение дисплея по программному SPI. Подключение через аппаратный SPI используется только во втором и в последнем (финишном) примере (там не сложно, лишь нужно поменять пару строчек в коде и переподключиться к нужным пинам).

В целом, говоря про графические дисплеи, скажу, что эти штуки быстро "съедают" оперативную и флеш память и легко занимают всю пропускную способность SPI шины ;)

    Содержание статьи:
  1. Основные характеристики дисплея на контроллере pcd8544;
  2. Подключение дисплея к Arduino;
  3. Пишем HelloWorld для ATmega8+PCD8544 в Proteus;
  4. Переподключение дисплея на аппаратный SPI в Proteus;
  5. Функции масштабирование шрифта в два и три раза;
  6. Создание проекта для реального микроконтроллера ATmega8, чтение таблицы ASCII из флеш-памяти;
  7. Добавляем кириллицу в кодировке CP866
  8. Добавление фреймбуфера. Программа рисования фрактала.
  9. Делаем скринсейвер "сквозь звездное небо"
  10. Функции рисования прямых линий, и окружностей
  11. Интерфейс для метеостанции
  12. Интерфейс для FM-радиоприемника
  13. Финальная версия библиотеки на аппаратном SPI

1) Основные характеристики дисплея на контроллере pcd8544

    Они следующие:
  • Напряжение питания дисплея: от 2.7 до 3.3 Вольт, пины данных толерантны к 5 Вольт;
  • Разрешение дисплея 48x84 пикселов;
  • Управление осуществляется через SPI интерфейс(три провода). Всего в управлении задействовано 5 линий;
  • Предельная частота SPI шины 4MBit/s;
  • Дисплей имеет шесть строк(банков) на каждой их которых может разместиться 12 символов. Итого экран имеет текстовую емкость 6 х 12 = 72 символа. Графическая емкость дисплея равна 48 х 84 = 4032 бита/пикселов, или 504 байта;
  • Базовый шрифт для дисплея имеет размерность 5x7 пикселов, знакоместо для одной литеры имеет графическую размерность 7(ш)x8(в).
  • Дисплей имеет два набора команд: базовый и расширенный. Расширенный набор команд используется для конфигурации дисплея, его настройки. Для вывода картинки на экран используется базовый набор команд.

Нужно осознавать, что дисплеи покупаемые на али или ибэе, это не оригинальные дисплеи на контроллере pcd8544, а совместимые с ним noname девайсы(подробности здесь). По крайней мере в продаже на mouser я их не нашел. Это не плохо, но параметры устройства могут не соответствовать документации на оригинальный чип, что как бы уже нехорошо.

Далее немного справочной информации.

Datasheet на дисплей можно скачать например отсюда: https://www.sparkfun.com/datasheets/LCD/Monochrome/Nokia5110.pdf

Распиновка. Модули с экраном от Nokia 5110/3310 продаются в двух вариантах: красные и синие. Синий модуль имеет следующий ряд контактов:

Здесь:
Gnd - это "земля",
BL - подсветка +3.3 Вольта,
Vcc - питание +3.3 Вольта,
Clk - линия тактирования,
Din - линия данных "Data input",
DC или D/C- выбор между регистрами данных или команд, аналогичен RS в HD44780,
CE или SCE - Chip Enable, он же CS/SS, Chip Select,
RST - это линия сброса контроллера PCD8544.

Мой экземпляр экрана имеет красную расцветку, от синего он отличается тем, что подсветка LIGHT включается подключением к "земле", а не к питанию. В остальном все тоже самое.

Стрелочкой обозначен верх дисплея.

Адресация видеопамяти:

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

Также хочется сказать пару слов о подключении дисплея с 3.3 вольтовой логикой к микроконтроллеру ATmega8. Все знают, что в частности Аrduino - это 5 вольтовая логика. Однако микроконтроллеры ATmega могут работать на 3.3 Вольтах.

Я лично использовал такую самодельную DevBoard:

Линейный стабилизатор LM1117 3.3 позволяет переключать внешнее питание с 5 Вольт на 3.3Вольт. Однако при подключении программатора USBasp линейный стабилизатор не задействуется, вместо этого на самом программаторе переключается джампик питания микроконтроллера на 3.3V.

На моей DevBoard установлен кварц на 16MHz, хотя по документации на ATmega8, при напряжении питания 3.3Вольт частота clkcpu не должна превышать 10МГц. И хотя при комнатной температуре это будет работать скорее всего без проблем, технически это является overclocking'ом.

2) Подключение экрана Nokia 5110 к Arduino

Для проверки дисплея на работоспособность, можно попробовать подключить его к Arduino.

На официальном сайте Arduino имеется небольшой скетч - HelloWorld, с помощью которого можно проверить работоспособность дисплея. Этот скетч не требует установки каких-либо библиотек для своей работы.

#define PIN_SCE   7
#define PIN_RESET 6
#define PIN_DC    5
#define PIN_SDIN  4
#define PIN_SCLK  3

#define LCD_C     LOW
#define LCD_D     HIGH

#define LCD_X     84
#define LCD_Y     48

static const byte ASCII[][5] =
{
 {0x00, 0x00, 0x00, 0x00, 0x00} // 20  
,{0x00, 0x00, 0x5f, 0x00, 0x00} // 21 !
,{0x00, 0x07, 0x00, 0x07, 0x00} // 22 "
,{0x14, 0x7f, 0x14, 0x7f, 0x14} // 23 #
,{0x24, 0x2a, 0x7f, 0x2a, 0x12} // 24 $
,{0x23, 0x13, 0x08, 0x64, 0x62} // 25 %
,{0x36, 0x49, 0x55, 0x22, 0x50} // 26 &
,{0x00, 0x05, 0x03, 0x00, 0x00} // 27 '
,{0x00, 0x1c, 0x22, 0x41, 0x00} // 28 (
,{0x00, 0x41, 0x22, 0x1c, 0x00} // 29 )
,{0x14, 0x08, 0x3e, 0x08, 0x14} // 2a *
,{0x08, 0x08, 0x3e, 0x08, 0x08} // 2b +
,{0x00, 0x50, 0x30, 0x00, 0x00} // 2c ,
,{0x08, 0x08, 0x08, 0x08, 0x08} // 2d -
,{0x00, 0x60, 0x60, 0x00, 0x00} // 2e .
,{0x20, 0x10, 0x08, 0x04, 0x02} // 2f /
,{0x3e, 0x51, 0x49, 0x45, 0x3e} // 30 0
,{0x00, 0x42, 0x7f, 0x40, 0x00} // 31 1
,{0x42, 0x61, 0x51, 0x49, 0x46} // 32 2
,{0x21, 0x41, 0x45, 0x4b, 0x31} // 33 3
,{0x18, 0x14, 0x12, 0x7f, 0x10} // 34 4
,{0x27, 0x45, 0x45, 0x45, 0x39} // 35 5
,{0x3c, 0x4a, 0x49, 0x49, 0x30} // 36 6
,{0x01, 0x71, 0x09, 0x05, 0x03} // 37 7
,{0x36, 0x49, 0x49, 0x49, 0x36} // 38 8
,{0x06, 0x49, 0x49, 0x29, 0x1e} // 39 9
,{0x00, 0x36, 0x36, 0x00, 0x00} // 3a :
,{0x00, 0x56, 0x36, 0x00, 0x00} // 3b ;
,{0x08, 0x14, 0x22, 0x41, 0x00} // 3c <
,{0x14, 0x14, 0x14, 0x14, 0x14} // 3d =
,{0x00, 0x41, 0x22, 0x14, 0x08} // 3e >
,{0x02, 0x01, 0x51, 0x09, 0x06} // 3f ?
,{0x32, 0x49, 0x79, 0x41, 0x3e} // 40 @
,{0x7e, 0x11, 0x11, 0x11, 0x7e} // 41 A
,{0x7f, 0x49, 0x49, 0x49, 0x36} // 42 B
,{0x3e, 0x41, 0x41, 0x41, 0x22} // 43 C
,{0x7f, 0x41, 0x41, 0x22, 0x1c} // 44 D
,{0x7f, 0x49, 0x49, 0x49, 0x41} // 45 E
,{0x7f, 0x09, 0x09, 0x09, 0x01} // 46 F
,{0x3e, 0x41, 0x49, 0x49, 0x7a} // 47 G
,{0x7f, 0x08, 0x08, 0x08, 0x7f} // 48 H
,{0x00, 0x41, 0x7f, 0x41, 0x00} // 49 I
,{0x20, 0x40, 0x41, 0x3f, 0x01} // 4a J
,{0x7f, 0x08, 0x14, 0x22, 0x41} // 4b K
,{0x7f, 0x40, 0x40, 0x40, 0x40} // 4c L
,{0x7f, 0x02, 0x0c, 0x02, 0x7f} // 4d M
,{0x7f, 0x04, 0x08, 0x10, 0x7f} // 4e N
,{0x3e, 0x41, 0x41, 0x41, 0x3e} // 4f O
,{0x7f, 0x09, 0x09, 0x09, 0x06} // 50 P
,{0x3e, 0x41, 0x51, 0x21, 0x5e} // 51 Q
,{0x7f, 0x09, 0x19, 0x29, 0x46} // 52 R
,{0x46, 0x49, 0x49, 0x49, 0x31} // 53 S
,{0x01, 0x01, 0x7f, 0x01, 0x01} // 54 T
,{0x3f, 0x40, 0x40, 0x40, 0x3f} // 55 U
,{0x1f, 0x20, 0x40, 0x20, 0x1f} // 56 V
,{0x3f, 0x40, 0x38, 0x40, 0x3f} // 57 W
,{0x63, 0x14, 0x08, 0x14, 0x63} // 58 X
,{0x07, 0x08, 0x70, 0x08, 0x07} // 59 Y
,{0x61, 0x51, 0x49, 0x45, 0x43} // 5a Z
,{0x00, 0x7f, 0x41, 0x41, 0x00} // 5b [
,{0x02, 0x04, 0x08, 0x10, 0x20} // 5c ¥
,{0x00, 0x41, 0x41, 0x7f, 0x00} // 5d ]
,{0x04, 0x02, 0x01, 0x02, 0x04} // 5e ^
,{0x40, 0x40, 0x40, 0x40, 0x40} // 5f _
,{0x00, 0x01, 0x02, 0x04, 0x00} // 60 `
,{0x20, 0x54, 0x54, 0x54, 0x78} // 61 a
,{0x7f, 0x48, 0x44, 0x44, 0x38} // 62 b
,{0x38, 0x44, 0x44, 0x44, 0x20} // 63 c
,{0x38, 0x44, 0x44, 0x48, 0x7f} // 64 d
,{0x38, 0x54, 0x54, 0x54, 0x18} // 65 e
,{0x08, 0x7e, 0x09, 0x01, 0x02} // 66 f
,{0x0c, 0x52, 0x52, 0x52, 0x3e} // 67 g
,{0x7f, 0x08, 0x04, 0x04, 0x78} // 68 h
,{0x00, 0x44, 0x7d, 0x40, 0x00} // 69 i
,{0x20, 0x40, 0x44, 0x3d, 0x00} // 6a j 
,{0x7f, 0x10, 0x28, 0x44, 0x00} // 6b k
,{0x00, 0x41, 0x7f, 0x40, 0x00} // 6c l
,{0x7c, 0x04, 0x18, 0x04, 0x78} // 6d m
,{0x7c, 0x08, 0x04, 0x04, 0x78} // 6e n
,{0x38, 0x44, 0x44, 0x44, 0x38} // 6f o
,{0x7c, 0x14, 0x14, 0x14, 0x08} // 70 p
,{0x08, 0x14, 0x14, 0x18, 0x7c} // 71 q
,{0x7c, 0x08, 0x04, 0x04, 0x08} // 72 r
,{0x48, 0x54, 0x54, 0x54, 0x20} // 73 s
,{0x04, 0x3f, 0x44, 0x40, 0x20} // 74 t
,{0x3c, 0x40, 0x40, 0x20, 0x7c} // 75 u
,{0x1c, 0x20, 0x40, 0x20, 0x1c} // 76 v
,{0x3c, 0x40, 0x30, 0x40, 0x3c} // 77 w
,{0x44, 0x28, 0x10, 0x28, 0x44} // 78 x
,{0x0c, 0x50, 0x50, 0x50, 0x3c} // 79 y
,{0x44, 0x64, 0x54, 0x4c, 0x44} // 7a z
,{0x00, 0x08, 0x36, 0x41, 0x00} // 7b {
,{0x00, 0x00, 0x7f, 0x00, 0x00} // 7c |
,{0x00, 0x41, 0x36, 0x08, 0x00} // 7d }
,{0x10, 0x08, 0x08, 0x10, 0x08} // 7e ←
,{0x78, 0x46, 0x41, 0x46, 0x78} // 7f
};

void LcdCharacter(char character)
{
  LcdWrite(LCD_D, 0x00);
  for (int index = 0; index < 5; index++)
  {
    LcdWrite(LCD_D, ASCII[character - 0x20][index]);
  }
  LcdWrite(LCD_D, 0x00);
}

void LcdClear(void)
{
  for (int index = 0; index < LCD_X * LCD_Y / 8; index++)
  {
    LcdWrite(LCD_D, 0x00);
  }
}

void LcdInitialise(void)
{
  pinMode(PIN_SCE, OUTPUT);
  pinMode(PIN_RESET, OUTPUT);
  pinMode(PIN_DC, OUTPUT);
  pinMode(PIN_SDIN, OUTPUT);
  pinMode(PIN_SCLK, OUTPUT);
  digitalWrite(PIN_RESET, LOW);
  digitalWrite(PIN_RESET, HIGH);
  LcdWrite(LCD_C, 0x21 );  // LCD Extended Commands.
  LcdWrite(LCD_C, 0xBA );  // Set LCD Vop (Contrast). Здесь константа в оригинале была B1 (c)flanker 
  LcdWrite(LCD_C, 0x04 );  // Set Temp coefficent. //0x04
  LcdWrite(LCD_C, 0x14 );  // LCD bias mode 1:48. //0x13
  LcdWrite(LCD_C, 0x20 );  // LCD Basic Commands
  LcdWrite(LCD_C, 0x0C );  // LCD in normal mode.
}

void LcdString(char *characters)
{
  while (*characters)
  {
    LcdCharacter(*characters++);
  }
}

void LcdWrite(byte dc, byte data)
{
  digitalWrite(PIN_DC, dc);
  digitalWrite(PIN_SCE, LOW);
  shiftOut(PIN_SDIN, PIN_SCLK, MSBFIRST, data);
  digitalWrite(PIN_SCE, HIGH);
}

void setup(void)
{
  LcdInitialise();
  LcdClear();
  LcdString("Hello World!");
}

void loop(void)
{
}

При успешном подключении, на экране дисплея появится строка: Hello World!

Данный скетч я буду использовать как образец для написания кода под ATmega8.

3)Пишем HelloWorld для ATmega8+PCD8544 в Proteus

В Proteus 8.5 дисплей на контролере pcd8544 уже имеется в библиотеке, так что ничего искать и скачивать не надо, нужно просто добавить его в проект:

После статьи о сдвиговых регистрах, у меня в Proteus остался проект с ATmega8 настроенный на работу на 2МГц, его я и буду продолжать использовать:

Код для микроконтроллера я пишу в самом Proteus, и вариант приведенного выше кода для ATmega8 будет выглядеть так:

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

#define PIN_SCE   PD7
#define PIN_RESET PD6
#define PIN_DC    PD5
#define PIN_SDIN  PD4
#define PIN_SCLK  PD3

#define LCD_C     0x00
#define LCD_D     0x01

#define LCD_X     84
#define LCD_Y     48

#define PORT_PCD8544 PORTD
#define DDR_PCD8544  DDRD

static const uint8_t ASCII[][5] =
{
 {0x00, 0x00, 0x00, 0x00, 0x00} // 20  
,{0x00, 0x00, 0x5f, 0x00, 0x00} // 21 !
,{0x00, 0x07, 0x00, 0x07, 0x00} // 22 "
,{0x14, 0x7f, 0x14, 0x7f, 0x14} // 23 #
,{0x24, 0x2a, 0x7f, 0x2a, 0x12} // 24 $
,{0x23, 0x13, 0x08, 0x64, 0x62} // 25 %
,{0x36, 0x49, 0x55, 0x22, 0x50} // 26 &
,{0x00, 0x05, 0x03, 0x00, 0x00} // 27 '
,{0x00, 0x1c, 0x22, 0x41, 0x00} // 28 (
,{0x00, 0x41, 0x22, 0x1c, 0x00} // 29 )
,{0x14, 0x08, 0x3e, 0x08, 0x14} // 2a *
,{0x08, 0x08, 0x3e, 0x08, 0x08} // 2b +
,{0x00, 0x50, 0x30, 0x00, 0x00} // 2c ,
,{0x08, 0x08, 0x08, 0x08, 0x08} // 2d -
,{0x00, 0x60, 0x60, 0x00, 0x00} // 2e .
,{0x20, 0x10, 0x08, 0x04, 0x02} // 2f /
,{0x3e, 0x51, 0x49, 0x45, 0x3e} // 30 0
,{0x00, 0x42, 0x7f, 0x40, 0x00} // 31 1
,{0x42, 0x61, 0x51, 0x49, 0x46} // 32 2
,{0x21, 0x41, 0x45, 0x4b, 0x31} // 33 3
,{0x18, 0x14, 0x12, 0x7f, 0x10} // 34 4
,{0x27, 0x45, 0x45, 0x45, 0x39} // 35 5
,{0x3c, 0x4a, 0x49, 0x49, 0x30} // 36 6
,{0x01, 0x71, 0x09, 0x05, 0x03} // 37 7
,{0x36, 0x49, 0x49, 0x49, 0x36} // 38 8
,{0x06, 0x49, 0x49, 0x29, 0x1e} // 39 9
,{0x00, 0x36, 0x36, 0x00, 0x00} // 3a :
,{0x00, 0x56, 0x36, 0x00, 0x00} // 3b ;
,{0x08, 0x14, 0x22, 0x41, 0x00} // 3c <
,{0x14, 0x14, 0x14, 0x14, 0x14} // 3d =
,{0x00, 0x41, 0x22, 0x14, 0x08} // 3e >
,{0x02, 0x01, 0x51, 0x09, 0x06} // 3f ?
,{0x32, 0x49, 0x79, 0x41, 0x3e} // 40 @
,{0x7e, 0x11, 0x11, 0x11, 0x7e} // 41 A
,{0x7f, 0x49, 0x49, 0x49, 0x36} // 42 B
,{0x3e, 0x41, 0x41, 0x41, 0x22} // 43 C
,{0x7f, 0x41, 0x41, 0x22, 0x1c} // 44 D
,{0x7f, 0x49, 0x49, 0x49, 0x41} // 45 E
,{0x7f, 0x09, 0x09, 0x09, 0x01} // 46 F
,{0x3e, 0x41, 0x49, 0x49, 0x7a} // 47 G
,{0x7f, 0x08, 0x08, 0x08, 0x7f} // 48 H
,{0x00, 0x41, 0x7f, 0x41, 0x00} // 49 I
,{0x20, 0x40, 0x41, 0x3f, 0x01} // 4a J
,{0x7f, 0x08, 0x14, 0x22, 0x41} // 4b K
,{0x7f, 0x40, 0x40, 0x40, 0x40} // 4c L
,{0x7f, 0x02, 0x0c, 0x02, 0x7f} // 4d M
,{0x7f, 0x04, 0x08, 0x10, 0x7f} // 4e N
,{0x3e, 0x41, 0x41, 0x41, 0x3e} // 4f O
,{0x7f, 0x09, 0x09, 0x09, 0x06} // 50 P
,{0x3e, 0x41, 0x51, 0x21, 0x5e} // 51 Q
,{0x7f, 0x09, 0x19, 0x29, 0x46} // 52 R
,{0x46, 0x49, 0x49, 0x49, 0x31} // 53 S
,{0x01, 0x01, 0x7f, 0x01, 0x01} // 54 T
,{0x3f, 0x40, 0x40, 0x40, 0x3f} // 55 U
,{0x1f, 0x20, 0x40, 0x20, 0x1f} // 56 V
,{0x3f, 0x40, 0x38, 0x40, 0x3f} // 57 W
,{0x63, 0x14, 0x08, 0x14, 0x63} // 58 X
,{0x07, 0x08, 0x70, 0x08, 0x07} // 59 Y
,{0x61, 0x51, 0x49, 0x45, 0x43} // 5a Z
,{0x00, 0x7f, 0x41, 0x41, 0x00} // 5b [
,{0x02, 0x04, 0x08, 0x10, 0x20} // 5c ?
,{0x00, 0x41, 0x41, 0x7f, 0x00} // 5d ]
,{0x04, 0x02, 0x01, 0x02, 0x04} // 5e ^
,{0x40, 0x40, 0x40, 0x40, 0x40} // 5f _
,{0x00, 0x01, 0x02, 0x04, 0x00} // 60 `
,{0x20, 0x54, 0x54, 0x54, 0x78} // 61 a
,{0x7f, 0x48, 0x44, 0x44, 0x38} // 62 b
,{0x38, 0x44, 0x44, 0x44, 0x20} // 63 c
,{0x38, 0x44, 0x44, 0x48, 0x7f} // 64 d
,{0x38, 0x54, 0x54, 0x54, 0x18} // 65 e
,{0x08, 0x7e, 0x09, 0x01, 0x02} // 66 f
,{0x0c, 0x52, 0x52, 0x52, 0x3e} // 67 g
,{0x7f, 0x08, 0x04, 0x04, 0x78} // 68 h
,{0x00, 0x44, 0x7d, 0x40, 0x00} // 69 i
,{0x20, 0x40, 0x44, 0x3d, 0x00} // 6a j 
,{0x7f, 0x10, 0x28, 0x44, 0x00} // 6b k
,{0x00, 0x41, 0x7f, 0x40, 0x00} // 6c l
,{0x7c, 0x04, 0x18, 0x04, 0x78} // 6d m
,{0x7c, 0x08, 0x04, 0x04, 0x78} // 6e n
,{0x38, 0x44, 0x44, 0x44, 0x38} // 6f o
,{0x7c, 0x14, 0x14, 0x14, 0x08} // 70 p
,{0x08, 0x14, 0x14, 0x18, 0x7c} // 71 q
,{0x7c, 0x08, 0x04, 0x04, 0x08} // 72 r
,{0x48, 0x54, 0x54, 0x54, 0x20} // 73 s
,{0x04, 0x3f, 0x44, 0x40, 0x20} // 74 t
,{0x3c, 0x40, 0x40, 0x20, 0x7c} // 75 u
,{0x1c, 0x20, 0x40, 0x20, 0x1c} // 76 v
,{0x3c, 0x40, 0x30, 0x40, 0x3c} // 77 w
,{0x44, 0x28, 0x10, 0x28, 0x44} // 78 x
,{0x0c, 0x50, 0x50, 0x50, 0x3c} // 79 y
,{0x44, 0x64, 0x54, 0x4c, 0x44} // 7a z
,{0x00, 0x08, 0x36, 0x41, 0x00} // 7b {
,{0x00, 0x00, 0x7f, 0x00, 0x00} // 7c |
,{0x00, 0x41, 0x36, 0x08, 0x00} // 7d }
,{0x10, 0x08, 0x08, 0x10, 0x08} // 7e <
,{0x78, 0x46, 0x41, 0x46, 0x78} // 7f >
};

void pcd8544_init(void);
void pcd8544_send(uint8_t dc, uint8_t data);
void pcd8544_print_string(char *str);
void pcd8544_send_char(uint8_t ch);
void pcd8544_clear(void);

int main(void)
{
   pcd8544_init();
   pcd8544_clear();
   pcd8544_print_string("Hello World!");

   for (;;){
      _delay_ms(1000);
   }

   return 0;
}

void pcd8544_init(void)
{
   // GPIO Setup
   DDR_PCD8544 |= (1<<PIN_SCE) | (1<<PIN_RESET) | (1<<PIN_DC) | (1<<PIN_SDIN) | (1<<PIN_SCLK);
   PORT_PCD8544&=~(1<<PIN_RESET);
   PORT_PCD8544|=(1<<PIN_RESET);
   pcd8544_send(LCD_C, 0x21 );  // LCD Extended Commands.
   pcd8544_send(LCD_C, 0xBA );  // Set LCD Vop (Contrast).
   pcd8544_send(LCD_C, 0x04 );  // Set Temp coefficent. //0x04
   pcd8544_send(LCD_C, 0x14 );  // LCD bias mode 1:48. //0x13
   pcd8544_send(LCD_C, 0x20 );  // LCD Basic Commands
   pcd8544_send(LCD_C, 0x0C );  // LCD in normal mode.
}

void pcd8544_send(uint8_t dc, uint8_t data)
{
   uint8_t i;
   if (dc == LCD_D)
      PORT_PCD8544 |= (1<<PIN_DC);
   else
      PORT_PCD8544 &= ~(1<<PIN_DC);

   PORT_PCD8544&=~(1<<PIN_SCE);
   for (i=0; i<8; i++)
   {
      PORT_PCD8544=(data & 0x80) ? PORT_PCD8544 | (1<<PIN_SDIN) : PORT_PCD8544 & ~(1<<PIN_SDIN);

      data=(data<<1);

      PORT_PCD8544|=(1<<PIN_SCLK);
      PORT_PCD8544&=~(1<<PIN_SCLK);
   }
   PORT_PCD8544|=(1<<PIN_SCE);
}

void pcd8544_print_string(char *str)
{
   while (*str)
   {
      pcd8544_send_char(*str++);
   }
}

void pcd8544_send_char(uint8_t ch)
{
   int i;
   if (ch >= 0x20 && ch <= 0x80)
   {
      pcd8544_send(LCD_D, 0x00);
      for (i = 0; i < 5; i++)
      {
         pcd8544_send(LCD_D,  ASCII[ch - 0x20][i]);
      }
      pcd8544_send(LCD_D, 0x00);
   }
}

void pcd8544_clear(void)
{
   int i;
   for (i=0; i < LCD_X * LCD_Y / 8; i++)
   {
      pcd8544_send(LCD_D, 0x00);
   }
}

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

Начало положено, начнём добавлять функционал. Что мы можем сделать на этом этапе? Первое, что хотелось бы сделать, это чтение ASCII таблицы из флеш-памяти, и поддержку кириллицы. Но Proteus такие штуки не осилит. Поэтому начнем с малого, установка курсора на определенную позицию экрана, сейчас это нам вполне по силам.

Добавляем функцию установки курсора:

void pcd8544_set_cursor(uint8_t x, uint8_t y) {
    x=x%12; y=y%6;
    pcd8544_send(LCD_C, 0x40+y);
    pcd8544_send(LCD_C, 0x80+x*7);
} 

Чтобы каждый раз не вызывать эту функцию, добавим функцию печати строки с определенной позиции:

void pcd8544_print_at(char *str, uint8_t x, uint8_t y)
{
    pcd8544_set_cursor(x,y);

    while (*str)
    {
        pcd8544_send_char(*str++);
    }
    break;
}

Теперь можно написать на дисплее уже что-то более осмысленное:

int main(void)
{
   pcd8544_init();
   pcd8544_clear();
   pcd8544_print_at("Nokia 3310",1,0);
   pcd8544_print_at("Connecting",1,2);
   pcd8544_print_at("People",6,3);

   for (;;){
      _delay_ms(1000);
   }

   return 0;
}

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

#define PIN_SCE   PD7
#define PIN_RESET PD6
#define PIN_DC    PD5
#define PIN_SDIN  PD4
#define PIN_SCLK  PD3

#define LCD_C     0x00
#define LCD_D     0x01

#define LCD_X     84
#define LCD_Y     48

#define PORT_PCD8544 PORTD
#define DDR_PCD8544  DDRD

static const uint8_t ASCII[][5] =
{
 {0x00, 0x00, 0x00, 0x00, 0x00} // 20  
,{0x00, 0x00, 0x5f, 0x00, 0x00} // 21 !
,{0x00, 0x07, 0x00, 0x07, 0x00} // 22 "
,{0x14, 0x7f, 0x14, 0x7f, 0x14} // 23 #
,{0x24, 0x2a, 0x7f, 0x2a, 0x12} // 24 $
,{0x23, 0x13, 0x08, 0x64, 0x62} // 25 %
,{0x36, 0x49, 0x55, 0x22, 0x50} // 26 &
,{0x00, 0x05, 0x03, 0x00, 0x00} // 27 '
,{0x00, 0x1c, 0x22, 0x41, 0x00} // 28 (
,{0x00, 0x41, 0x22, 0x1c, 0x00} // 29 )
,{0x14, 0x08, 0x3e, 0x08, 0x14} // 2a *
,{0x08, 0x08, 0x3e, 0x08, 0x08} // 2b +
,{0x00, 0x50, 0x30, 0x00, 0x00} // 2c ,
,{0x08, 0x08, 0x08, 0x08, 0x08} // 2d -
,{0x00, 0x60, 0x60, 0x00, 0x00} // 2e .
,{0x20, 0x10, 0x08, 0x04, 0x02} // 2f /
,{0x3e, 0x51, 0x49, 0x45, 0x3e} // 30 0
,{0x00, 0x42, 0x7f, 0x40, 0x00} // 31 1
,{0x42, 0x61, 0x51, 0x49, 0x46} // 32 2
,{0x21, 0x41, 0x45, 0x4b, 0x31} // 33 3
,{0x18, 0x14, 0x12, 0x7f, 0x10} // 34 4
,{0x27, 0x45, 0x45, 0x45, 0x39} // 35 5
,{0x3c, 0x4a, 0x49, 0x49, 0x30} // 36 6
,{0x01, 0x71, 0x09, 0x05, 0x03} // 37 7
,{0x36, 0x49, 0x49, 0x49, 0x36} // 38 8
,{0x06, 0x49, 0x49, 0x29, 0x1e} // 39 9
,{0x00, 0x36, 0x36, 0x00, 0x00} // 3a :
,{0x00, 0x56, 0x36, 0x00, 0x00} // 3b ;
,{0x08, 0x14, 0x22, 0x41, 0x00} // 3c <
,{0x14, 0x14, 0x14, 0x14, 0x14} // 3d =
,{0x00, 0x41, 0x22, 0x14, 0x08} // 3e >
,{0x02, 0x01, 0x51, 0x09, 0x06} // 3f ?
,{0x32, 0x49, 0x79, 0x41, 0x3e} // 40 @
,{0x7e, 0x11, 0x11, 0x11, 0x7e} // 41 A
,{0x7f, 0x49, 0x49, 0x49, 0x36} // 42 B
,{0x3e, 0x41, 0x41, 0x41, 0x22} // 43 C
,{0x7f, 0x41, 0x41, 0x22, 0x1c} // 44 D
,{0x7f, 0x49, 0x49, 0x49, 0x41} // 45 E
,{0x7f, 0x09, 0x09, 0x09, 0x01} // 46 F
,{0x3e, 0x41, 0x49, 0x49, 0x7a} // 47 G
,{0x7f, 0x08, 0x08, 0x08, 0x7f} // 48 H
,{0x00, 0x41, 0x7f, 0x41, 0x00} // 49 I
,{0x20, 0x40, 0x41, 0x3f, 0x01} // 4a J
,{0x7f, 0x08, 0x14, 0x22, 0x41} // 4b K
,{0x7f, 0x40, 0x40, 0x40, 0x40} // 4c L
,{0x7f, 0x02, 0x0c, 0x02, 0x7f} // 4d M
,{0x7f, 0x04, 0x08, 0x10, 0x7f} // 4e N
,{0x3e, 0x41, 0x41, 0x41, 0x3e} // 4f O
,{0x7f, 0x09, 0x09, 0x09, 0x06} // 50 P
,{0x3e, 0x41, 0x51, 0x21, 0x5e} // 51 Q
,{0x7f, 0x09, 0x19, 0x29, 0x46} // 52 R
,{0x46, 0x49, 0x49, 0x49, 0x31} // 53 S
,{0x01, 0x01, 0x7f, 0x01, 0x01} // 54 T
,{0x3f, 0x40, 0x40, 0x40, 0x3f} // 55 U
,{0x1f, 0x20, 0x40, 0x20, 0x1f} // 56 V
,{0x3f, 0x40, 0x38, 0x40, 0x3f} // 57 W
,{0x63, 0x14, 0x08, 0x14, 0x63} // 58 X
,{0x07, 0x08, 0x70, 0x08, 0x07} // 59 Y
,{0x61, 0x51, 0x49, 0x45, 0x43} // 5a Z
,{0x00, 0x7f, 0x41, 0x41, 0x00} // 5b [
,{0x02, 0x04, 0x08, 0x10, 0x20} // 5c ?
,{0x00, 0x41, 0x41, 0x7f, 0x00} // 5d ]
,{0x04, 0x02, 0x01, 0x02, 0x04} // 5e ^
,{0x40, 0x40, 0x40, 0x40, 0x40} // 5f _
,{0x00, 0x01, 0x02, 0x04, 0x00} // 60 `
,{0x20, 0x54, 0x54, 0x54, 0x78} // 61 a
,{0x7f, 0x48, 0x44, 0x44, 0x38} // 62 b
,{0x38, 0x44, 0x44, 0x44, 0x20} // 63 c
,{0x38, 0x44, 0x44, 0x48, 0x7f} // 64 d
,{0x38, 0x54, 0x54, 0x54, 0x18} // 65 e
,{0x08, 0x7e, 0x09, 0x01, 0x02} // 66 f
,{0x0c, 0x52, 0x52, 0x52, 0x3e} // 67 g
,{0x7f, 0x08, 0x04, 0x04, 0x78} // 68 h
,{0x00, 0x44, 0x7d, 0x40, 0x00} // 69 i
,{0x20, 0x40, 0x44, 0x3d, 0x00} // 6a j 
,{0x7f, 0x10, 0x28, 0x44, 0x00} // 6b k
,{0x00, 0x41, 0x7f, 0x40, 0x00} // 6c l
,{0x7c, 0x04, 0x18, 0x04, 0x78} // 6d m
,{0x7c, 0x08, 0x04, 0x04, 0x78} // 6e n
,{0x38, 0x44, 0x44, 0x44, 0x38} // 6f o
,{0x7c, 0x14, 0x14, 0x14, 0x08} // 70 p
,{0x08, 0x14, 0x14, 0x18, 0x7c} // 71 q
,{0x7c, 0x08, 0x04, 0x04, 0x08} // 72 r
,{0x48, 0x54, 0x54, 0x54, 0x20} // 73 s
,{0x04, 0x3f, 0x44, 0x40, 0x20} // 74 t
,{0x3c, 0x40, 0x40, 0x20, 0x7c} // 75 u
,{0x1c, 0x20, 0x40, 0x20, 0x1c} // 76 v
,{0x3c, 0x40, 0x30, 0x40, 0x3c} // 77 w
,{0x44, 0x28, 0x10, 0x28, 0x44} // 78 x
,{0x0c, 0x50, 0x50, 0x50, 0x3c} // 79 y
,{0x44, 0x64, 0x54, 0x4c, 0x44} // 7a z
,{0x00, 0x08, 0x36, 0x41, 0x00} // 7b {
,{0x00, 0x00, 0x7f, 0x00, 0x00} // 7c |
,{0x00, 0x41, 0x36, 0x08, 0x00} // 7d }
,{0x10, 0x08, 0x08, 0x10, 0x08} // 7e <
,{0x78, 0x46, 0x41, 0x46, 0x78} // 7f >
};

void pcd8544_init(void);
void pcd8544_send(uint8_t dc, uint8_t data);
void pcd8544_print_string(char *str);
void pcd8544_send_char(uint8_t ch);
void pcd8544_clear(void);
void pcd8544_set_cursor(uint8_t x, uint8_t y);
void pcd8544_print_at(char *str, uint8_t x, uint8_t y);

int main(void)
{
   pcd8544_init();
   pcd8544_clear();
   pcd8544_print_at("Nokia 3310",1,0);
   pcd8544_print_at("Connecting",1,2);
   pcd8544_print_at("People",6,3);

   for (;;){
      _delay_ms(1000);
   }

   return 0;
}

void pcd8544_init(void)
{
   // GPIO Setup
   DDR_PCD8544 |= (1<<PIN_SCE) | (1<<PIN_RESET) | (1<<PIN_DC) | (1<<PIN_SDIN) | (1<<PIN_SCLK);
   PORT_PCD8544&=~(1<<PIN_RESET);
   PORT_PCD8544|=(1<<PIN_RESET);
   pcd8544_send(LCD_C, 0x21 );  // LCD Extended Commands.
   pcd8544_send(LCD_C, 0xBA );  // Set LCD Vop (Contrast).
   pcd8544_send(LCD_C, 0x04 );  // Set Temp coefficent. //0x04
   pcd8544_send(LCD_C, 0x14 );  // LCD bias mode 1:48. //0x13
   pcd8544_send(LCD_C, 0x20 );  // LCD Basic Commands
   pcd8544_send(LCD_C, 0x0C );  // LCD in normal mode.
}

void pcd8544_send(uint8_t dc, uint8_t data)
{
   uint8_t i;
   if (dc == LCD_D)
      PORT_PCD8544 |= (1<<PIN_DC);
   else
      PORT_PCD8544 &= ~(1<<PIN_DC);

   PORT_PCD8544&=~(1<<PIN_SCE);
   for (i=0; i<8; i++)
   {
      PORT_PCD8544=(data & 0x80) ? PORT_PCD8544 | (1<<PIN_SDIN) : PORT_PCD8544 & ~(1<<PIN_SDIN);

      data=(data<<1);

      PORT_PCD8544|=(1<<PIN_SCLK);
      PORT_PCD8544&=~(1<<PIN_SCLK);
   }
   PORT_PCD8544|=(1<<PIN_SCE);
}

void pcd8544_print_string(char *str)
{
   while (*str)
   {
      pcd8544_send_char(*str++);
   }
}

void pcd8544_send_char(uint8_t ch)
{
   int i;
   if (ch >= 0x20 && ch <= 0x80)
   {
      pcd8544_send(LCD_D, 0x00);
      for (i = 0; i < 5; i++)
      {
     pcd8544_send(LCD_D,  ASCII[ch - 0x20][i]);
      }
      pcd8544_send(LCD_D, 0x00);
   }
}
void pcd8544_clear(void)
{
   int i;
   for (i=0; i < LCD_X * LCD_Y / 8; i++)
   {
      pcd8544_send(LCD_D, 0x00);
   }
}

void pcd8544_set_cursor(uint8_t x, uint8_t y) {
    x=x%12; y=y%6;
    pcd8544_send(LCD_C, 0x40+y);
    pcd8544_send(LCD_C, 0x80+x*7);
}

void pcd8544_print_at(char *str, uint8_t x, uint8_t y)
{
    pcd8544_set_cursor(x,y);
    while (*str)
    {
        pcd8544_send_char(*str++);
    }
}

Результат:

4) Переподключение дисплея на аппаратный SPI в Proteus

Теперь мы можем переподключить дисплей на аппаратную SPI шину ATmega8.

Для использования SPI, дисплей нужно будет переподключить следующим образом:

Обращаю внимание, что вывод D/C- дисплея я переподключил с PD5 на PD7. В этом нет особой необходимости, просто мне удобнее использовать крайние пины.

Именованные константы:

#define PIN_SCE   PD7
#define PIN_RESET PD6
#define PIN_DC    PD5
#define PIN_SDIN  PD4
#define PIN_SCLK  PD3

меняются на:

// PORT D
#define PIN_RESET PD6
#define PIN_DC    PD7
// PORT B
#define PIN_SCE   PB2
#define PIN_SDIN  PB3
#define PIN_SCLK  PB5

В функции инициализации дисплея void pcd8544_init(void), строка:

DDR_PCD8544 |= (1<<PIN_SCE) | (1<<PIN_RESET) | (1<<PIN_DC) | (1<<PIN_SDIN) | (1<<PIN_SCLK);

заменяется на:

// GPIO Setup
DDR_PCD8544 |=(1<<PIN_RESET) | (1<<PIN_DC);
DDRB |= (1<<PIN_SCE) | (1<<PIN_SDIN) | (1<<PIN_SCLK);

// SPI setup 
SPCR = (1<<SPE)|(1<<MSTR)|(1<<SPR0); // SPI mode 0, master, prescaler 1/16

Ну и последнее, в функции void pcd8544_send(uint8_t dc, uint8_t data), программный SPI:

PORT_PCD8544&=~(1<<PIN_SCE);
for (i=0; i<8; i++)
{
  PORT_PCD8544=(data & 0x80) ? PORT_PCD8544 | (1<<PIN_SDIN) : PORT_PCD8544 & ~(1<<PIN_SDIN);

  data=(data<<1);

  PORT_PCD8544|=(1<<PIN_SCLK);
  PORT_PCD8544&=~(1<<PIN_SCLK);
}
PORT_PCD8544|=(1<<PIN_SCE);

заменяется на аппаратный:

PORTB&=~(1<<PIN_SCE);

SPDR=data;
while(!(SPSR & (1<<SPIF)));

PORTB|=(1<<PIN_SCE);

Это все. Полный исходник под спойлером.

#include <avr/io.h>
#include <util/delay.h>
// PORT D
#define PIN_RESET PD6
#define PIN_DC    PD7
// PORT B
#define PIN_SCE   PB2
#define PIN_SDIN  PB3
#define PIN_SCLK  PB5
// LCD
#define LCD_C     0x00
#define LCD_D     0x01

#define LCD_X     84
#define LCD_Y     48

#define PORT_PCD8544 PORTD
#define DDR_PCD8544  DDRD

static const uint8_t ASCII[][5] =
{
 {0x00, 0x00, 0x00, 0x00, 0x00} // 20  
,{0x00, 0x00, 0x5f, 0x00, 0x00} // 21 !
,{0x00, 0x07, 0x00, 0x07, 0x00} // 22 "
,{0x14, 0x7f, 0x14, 0x7f, 0x14} // 23 #
,{0x24, 0x2a, 0x7f, 0x2a, 0x12} // 24 $
,{0x23, 0x13, 0x08, 0x64, 0x62} // 25 %
,{0x36, 0x49, 0x55, 0x22, 0x50} // 26 &
,{0x00, 0x05, 0x03, 0x00, 0x00} // 27 '
,{0x00, 0x1c, 0x22, 0x41, 0x00} // 28 (
,{0x00, 0x41, 0x22, 0x1c, 0x00} // 29 )
,{0x14, 0x08, 0x3e, 0x08, 0x14} // 2a *
,{0x08, 0x08, 0x3e, 0x08, 0x08} // 2b +
,{0x00, 0x50, 0x30, 0x00, 0x00} // 2c ,
,{0x08, 0x08, 0x08, 0x08, 0x08} // 2d -
,{0x00, 0x60, 0x60, 0x00, 0x00} // 2e .
,{0x20, 0x10, 0x08, 0x04, 0x02} // 2f /
,{0x3e, 0x51, 0x49, 0x45, 0x3e} // 30 0
,{0x00, 0x42, 0x7f, 0x40, 0x00} // 31 1
,{0x42, 0x61, 0x51, 0x49, 0x46} // 32 2
,{0x21, 0x41, 0x45, 0x4b, 0x31} // 33 3
,{0x18, 0x14, 0x12, 0x7f, 0x10} // 34 4
,{0x27, 0x45, 0x45, 0x45, 0x39} // 35 5
,{0x3c, 0x4a, 0x49, 0x49, 0x30} // 36 6
,{0x01, 0x71, 0x09, 0x05, 0x03} // 37 7
,{0x36, 0x49, 0x49, 0x49, 0x36} // 38 8
,{0x06, 0x49, 0x49, 0x29, 0x1e} // 39 9
,{0x00, 0x36, 0x36, 0x00, 0x00} // 3a :
,{0x00, 0x56, 0x36, 0x00, 0x00} // 3b ;
,{0x08, 0x14, 0x22, 0x41, 0x00} // 3c <
,{0x14, 0x14, 0x14, 0x14, 0x14} // 3d =
,{0x00, 0x41, 0x22, 0x14, 0x08} // 3e >
,{0x02, 0x01, 0x51, 0x09, 0x06} // 3f ?
,{0x32, 0x49, 0x79, 0x41, 0x3e} // 40 @
,{0x7e, 0x11, 0x11, 0x11, 0x7e} // 41 A
,{0x7f, 0x49, 0x49, 0x49, 0x36} // 42 B
,{0x3e, 0x41, 0x41, 0x41, 0x22} // 43 C
,{0x7f, 0x41, 0x41, 0x22, 0x1c} // 44 D
,{0x7f, 0x49, 0x49, 0x49, 0x41} // 45 E
,{0x7f, 0x09, 0x09, 0x09, 0x01} // 46 F
,{0x3e, 0x41, 0x49, 0x49, 0x7a} // 47 G
,{0x7f, 0x08, 0x08, 0x08, 0x7f} // 48 H
,{0x00, 0x41, 0x7f, 0x41, 0x00} // 49 I
,{0x20, 0x40, 0x41, 0x3f, 0x01} // 4a J
,{0x7f, 0x08, 0x14, 0x22, 0x41} // 4b K
,{0x7f, 0x40, 0x40, 0x40, 0x40} // 4c L
,{0x7f, 0x02, 0x0c, 0x02, 0x7f} // 4d M
,{0x7f, 0x04, 0x08, 0x10, 0x7f} // 4e N
,{0x3e, 0x41, 0x41, 0x41, 0x3e} // 4f O
,{0x7f, 0x09, 0x09, 0x09, 0x06} // 50 P
,{0x3e, 0x41, 0x51, 0x21, 0x5e} // 51 Q
,{0x7f, 0x09, 0x19, 0x29, 0x46} // 52 R
,{0x46, 0x49, 0x49, 0x49, 0x31} // 53 S
,{0x01, 0x01, 0x7f, 0x01, 0x01} // 54 T
,{0x3f, 0x40, 0x40, 0x40, 0x3f} // 55 U
,{0x1f, 0x20, 0x40, 0x20, 0x1f} // 56 V
,{0x3f, 0x40, 0x38, 0x40, 0x3f} // 57 W
,{0x63, 0x14, 0x08, 0x14, 0x63} // 58 X
,{0x07, 0x08, 0x70, 0x08, 0x07} // 59 Y
,{0x61, 0x51, 0x49, 0x45, 0x43} // 5a Z
,{0x00, 0x7f, 0x41, 0x41, 0x00} // 5b [
,{0x02, 0x04, 0x08, 0x10, 0x20} // 5c ?
,{0x00, 0x41, 0x41, 0x7f, 0x00} // 5d ]
,{0x04, 0x02, 0x01, 0x02, 0x04} // 5e ^
,{0x40, 0x40, 0x40, 0x40, 0x40} // 5f _
,{0x00, 0x01, 0x02, 0x04, 0x00} // 60 `
,{0x20, 0x54, 0x54, 0x54, 0x78} // 61 a
,{0x7f, 0x48, 0x44, 0x44, 0x38} // 62 b
,{0x38, 0x44, 0x44, 0x44, 0x20} // 63 c
,{0x38, 0x44, 0x44, 0x48, 0x7f} // 64 d
,{0x38, 0x54, 0x54, 0x54, 0x18} // 65 e
,{0x08, 0x7e, 0x09, 0x01, 0x02} // 66 f
,{0x0c, 0x52, 0x52, 0x52, 0x3e} // 67 g
,{0x7f, 0x08, 0x04, 0x04, 0x78} // 68 h
,{0x00, 0x44, 0x7d, 0x40, 0x00} // 69 i
,{0x20, 0x40, 0x44, 0x3d, 0x00} // 6a j 
,{0x7f, 0x10, 0x28, 0x44, 0x00} // 6b k
,{0x00, 0x41, 0x7f, 0x40, 0x00} // 6c l
,{0x7c, 0x04, 0x18, 0x04, 0x78} // 6d m
,{0x7c, 0x08, 0x04, 0x04, 0x78} // 6e n
,{0x38, 0x44, 0x44, 0x44, 0x38} // 6f o
,{0x7c, 0x14, 0x14, 0x14, 0x08} // 70 p
,{0x08, 0x14, 0x14, 0x18, 0x7c} // 71 q
,{0x7c, 0x08, 0x04, 0x04, 0x08} // 72 r
,{0x48, 0x54, 0x54, 0x54, 0x20} // 73 s
,{0x04, 0x3f, 0x44, 0x40, 0x20} // 74 t
,{0x3c, 0x40, 0x40, 0x20, 0x7c} // 75 u
,{0x1c, 0x20, 0x40, 0x20, 0x1c} // 76 v
,{0x3c, 0x40, 0x30, 0x40, 0x3c} // 77 w
,{0x44, 0x28, 0x10, 0x28, 0x44} // 78 x
,{0x0c, 0x50, 0x50, 0x50, 0x3c} // 79 y
,{0x44, 0x64, 0x54, 0x4c, 0x44} // 7a z
,{0x00, 0x08, 0x36, 0x41, 0x00} // 7b {
,{0x00, 0x00, 0x7f, 0x00, 0x00} // 7c |
,{0x00, 0x41, 0x36, 0x08, 0x00} // 7d }
,{0x10, 0x08, 0x08, 0x10, 0x08} // 7e <
,{0x78, 0x46, 0x41, 0x46, 0x78} // 7f >
};

void pcd8544_init(void);
void pcd8544_send(uint8_t dc, uint8_t data);
void pcd8544_print_string(char *str);
void pcd8544_send_char(uint8_t ch);
void pcd8544_clear(void);
void pcd8544_set_cursor(uint8_t x, uint8_t y);
void pcd8544_print_at(char *str, uint8_t x, uint8_t y);

int main(void)
{
   pcd8544_init();
   pcd8544_clear();
   pcd8544_print_at("Nokia 3310",1,0);
   pcd8544_print_at("Connecting",1,2);
   pcd8544_print_at("People",6,3);

   for (;;){
      _delay_ms(1000);
   }

   return 0;
}

void pcd8544_init(void)
{

   // GPIO Setup
   DDR_PCD8544 |=(1<<PIN_RESET) | (1<<PIN_DC);
   DDRB |= (1<<PIN_SCE) | (1<<PIN_SDIN) | (1<<PIN_SCLK);

   // SPI setup 
   SPCR = (1<<SPE)|(1<<MSTR)|(1<<SPR0); // master mode, 

   PORT_PCD8544&=~(1<<PIN_RESET);
   PORT_PCD8544|=(1<<PIN_RESET);
   pcd8544_send(LCD_C, 0x21 );  // LCD Extended Commands.
   pcd8544_send(LCD_C, 0xBA );  // Set LCD Vop (Contrast).
   pcd8544_send(LCD_C, 0x04 );  // Set Temp coefficent. //0x04
   pcd8544_send(LCD_C, 0x14 );  // LCD bias mode 1:48. //0x13
   pcd8544_send(LCD_C, 0x20 );  // LCD Basic Commands
   pcd8544_send(LCD_C, 0x0C );  // LCD in normal mode.
}

void pcd8544_send(uint8_t dc, uint8_t data)
{
   //uint8_t i;
   if (dc == LCD_D)
      PORT_PCD8544 |= (1<<PIN_DC);
   else
      PORT_PCD8544 &= ~(1<<PIN_DC);

   PORTB&=~(1<<PIN_SCE);

   SPDR=data;
   while(!(SPSR & (1<<SPIF)));

   PORTB|=(1<<PIN_SCE);
}

void pcd8544_print_string(char *str)
{
   while (*str)
   {
      pcd8544_send_char(*str++);
   }
}

void pcd8544_send_char(uint8_t ch)
{
   int i;
   if (ch >= 0x20 && ch <= 0x80)
   {
      pcd8544_send(LCD_D, 0x00);
      for (i = 0; i < 5; i++)
      {
        pcd8544_send(LCD_D,  ASCII[ch - 0x20][i]);
      }
      pcd8544_send(LCD_D, 0x00);
   }
}
void pcd8544_clear(void)
{
   int i;
   for (i=0; i < LCD_X * LCD_Y / 8; i++)
   {
      pcd8544_send(LCD_D, 0x00);
   }
}

void pcd8544_set_cursor(uint8_t x, uint8_t y) {
    x=x%12; y=y%6;
    pcd8544_send(LCD_C, 0x40+y);
    pcd8544_send(LCD_C, 0x80+x*7);
}

void pcd8544_print_at(char *str, uint8_t x, uint8_t y)
{
    pcd8544_set_cursor(x,y);
    while (*str)
    {
        pcd8544_send_char(*str++);
    }
}

5) Функции масштабирование шрифта в два и три раза

Пока отбросим аппаратный SPI в сторону и продолжим работать с программным.

Т.к. дисплей маленький, одной из самых востребованных функций является алгоритмическое увеличение шрифта.

Для увеличения шрифта в два раза добавим такую функцию:

void pcd8544_send_char_size2(uint8_t ch, uint8_t x, uint8_t y) {
    uint8_t s[5]; // source
    uint8_t r[20]; // result
    uint8_t i,j;
   // get littera
    if (ch >= 0x20 && ch <= 0x80)
    {
        for (i=0; i < 5; i++)
        {
            s[i]=ASCII[ch-0x20][i];
        }
    }
    // scale
    for(i=0;i<5;i++)
    {
        uint8_t b=0;
        uint8_t a=0;
        for(j=0;j<4;j++)
        {
            b=(s[i]>>j) & 0x01;
            a|=(b<<(j<<1)) | (b<<((j<<1)+1));
        }
        r[(i<<1)]=a;
        r[(i<<1)+1]=a;
    }

    for(i=0;i<5;i++)
    {
        uint8_t b=0;
        uint8_t a=0;
        for(j=0;j<4;j++)
        {
            b=(s[i]>>(j+4)) & 0x01;
            a|=(b<<(j<<1)) | (b<<((j<<1)+1));
        }
        r[(i<<1)+10]=a;
        r[(i<<1)+11]=a;
    }
    // print
    pcd8544_set_cursor(x,y);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    for(i=0;i<10;i++)
        pcd8544_send(LCD_D, r[i]);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);

    pcd8544_set_cursor(x,y+1);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    for(i=10;i<20;i++)
        pcd8544_send(LCD_D, r[i]);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
}

А увеличение в три раза будет делать такая функция:

void pcd8544_send_char_size3(uint8_t ch, uint8_t x, uint8_t y) {
    uint8_t s[5]; // source
    uint8_t r[45]; // result
    uint8_t i;
    // get littera
    if (ch >= 0x20 && ch <= 0x80)
    {
        for (i=0; i < 5; i++)
        {
            s[i]=ASCII[ch-0x20][i];
        }
    }
    // scale
    for(i=0;i<5;i++)
    {
        uint8_t b,a;
        b=(s[i] & 0x01);
        a=(b) ? 0x7 : 0;
        b=(s[i]>>1) & 0x01;
        if (b) a|=0x38;
        b=(s[i]>>2) & 0x01;
        a|=(b<<6)|(b<<7);

        r[i*3]=a;
        r[i*3+1]=a;
        r[i*3+2]=a;

        r[i*3+15]=b;
        r[i*3+16]=b;
        r[i*3+17]=b;
    }

    for(i=0;i<5;i++)
    {
        uint8_t b,a;
        b=(s[i]>>3) & 0x01;
        a=(b) ? 0x0e : 0;
        b=(s[i]>>4) & 0x01;
        if (b) a|=0x70;
        b=(s[i]>>5) & 0x01;
        a|=(b<<7);

        r[i*3+15]|=a;
        r[i*3+16]|=a;
        r[i*3+17]|=a;
     }

    for(i=0;i<5;i++)
    {
        uint8_t b,a;
        b=(s[i]>>5) & 0x01;
        a=(b) ? 0x3 : 0;
        b=(s[i]>>6) & 0x01;
        if (b) a|=0x1c;
        b=(s[i]>>7) & 0x01;
        if (b) a|=0xe0;

        r[i*3+30]=a;
        r[i*3+31]=a;
        r[i*3+32]=a;
     }

    // print
    pcd8544_set_cursor(x,y);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    for(i=0;i<15;i++)
        pcd8544_send(LCD_D, r[i]);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);

    pcd8544_set_cursor(x,y+1);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    for(i=15;i<30;i++)
        pcd8544_send(LCD_D, r[i]);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);

    pcd8544_set_cursor(x,y+2);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    for(i=30;i<45;i++)
        pcd8544_send(LCD_D, r[i]);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
}

Чтобы можно было печатать строки нужным размером шрифта, недавно введенную функцию void pcd8544_print_at() приведем к такому виду:

void pcd8544_print_at(char *str, uint8_t size,  uint8_t x, uint8_t y)
{
    uint8_t i=0;
    pcd8544_set_cursor(x,y);
    switch (size) {
      case 3:
      while (*str)
      {
         pcd8544_send_char_size3(*str++,x+i,y);
         i+=3;
      }
      break;
      case 2:
      while (*str)
      {
        pcd8544_send_char_size2(*str++,x+i,y);
        i+=2;
      }
      break;
      default:
      while (*str)
      {
        pcd8544_send_char(*str++);
      }
      break;
   }
}

И последнее, что нам нужно от шрифтов, это печать небольшого целого числа.

void pcd8544_print_uint8_at(uint8_t num, uint8_t size, uint8_t x, uint8_t y){
    uint8_t sym[3];
    int8_t i=2;
    do  {
      if (num == 0 && i<2)
        sym[i]=0x20; // space
      else
        sym[i]=0x30+num%10;

      num=num/10;
      i--;

    } while (i>=0);

    uint8_t j=0;
    for (i=0;i<3;i++)
    {
        if (!(i<2 && sym[i] == 0x20))
        {
            switch(size) {
            case 3:
                pcd8544_send_char_size3(sym[i],x+j*size,y);
                break;
            case 2:
                pcd8544_send_char_size2(sym[i],x+j*size,y);
                break;
            default:
                pcd8544_send_char(sym[i]);
                break;
            }
            j++;
        }
    }

Для теста, в функции main() сделаем печать шрифта крупным шрифтом:

int main(void)
{
   pcd8544_init();
   uint8_t i=0;
   for (;;){
      pcd8544_clear();
      pcd8544_print_at(">",3,0,2);
      pcd8544_print_uint8_at(i++,3,3,2);
      _delay_ms(1000);
   }

   return 0;
}

Результат работы, без анимации:

Полный листинг программы под спойлером:

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

#define PIN_SCE   PD7
#define PIN_RESET PD6
#define PIN_DC    PD5
#define PIN_SDIN  PD4
#define PIN_SCLK  PD3

#define LCD_C     0x00
#define LCD_D     0x01

#define LCD_X     84
#define LCD_Y     48

#define PORT_PCD8544 PORTD
#define DDR_PCD8544  DDRD

static const uint8_t ASCII[][5] =
{
 {0x00, 0x00, 0x00, 0x00, 0x00} // 20  
,{0x00, 0x00, 0x5f, 0x00, 0x00} // 21 !
,{0x00, 0x07, 0x00, 0x07, 0x00} // 22 "
,{0x14, 0x7f, 0x14, 0x7f, 0x14} // 23 #
,{0x24, 0x2a, 0x7f, 0x2a, 0x12} // 24 $
,{0x23, 0x13, 0x08, 0x64, 0x62} // 25 %
,{0x36, 0x49, 0x55, 0x22, 0x50} // 26 &
,{0x00, 0x05, 0x03, 0x00, 0x00} // 27 '
,{0x00, 0x1c, 0x22, 0x41, 0x00} // 28 (
,{0x00, 0x41, 0x22, 0x1c, 0x00} // 29 )
,{0x14, 0x08, 0x3e, 0x08, 0x14} // 2a *
,{0x08, 0x08, 0x3e, 0x08, 0x08} // 2b +
,{0x00, 0x50, 0x30, 0x00, 0x00} // 2c ,
,{0x08, 0x08, 0x08, 0x08, 0x08} // 2d -
,{0x00, 0x60, 0x60, 0x00, 0x00} // 2e .
,{0x20, 0x10, 0x08, 0x04, 0x02} // 2f /
,{0x3e, 0x51, 0x49, 0x45, 0x3e} // 30 0
,{0x00, 0x42, 0x7f, 0x40, 0x00} // 31 1
,{0x42, 0x61, 0x51, 0x49, 0x46} // 32 2
,{0x21, 0x41, 0x45, 0x4b, 0x31} // 33 3
,{0x18, 0x14, 0x12, 0x7f, 0x10} // 34 4
,{0x27, 0x45, 0x45, 0x45, 0x39} // 35 5
,{0x3c, 0x4a, 0x49, 0x49, 0x30} // 36 6
,{0x01, 0x71, 0x09, 0x05, 0x03} // 37 7
,{0x36, 0x49, 0x49, 0x49, 0x36} // 38 8
,{0x06, 0x49, 0x49, 0x29, 0x1e} // 39 9
,{0x00, 0x36, 0x36, 0x00, 0x00} // 3a :
,{0x00, 0x56, 0x36, 0x00, 0x00} // 3b ;
,{0x08, 0x14, 0x22, 0x41, 0x00} // 3c <
,{0x14, 0x14, 0x14, 0x14, 0x14} // 3d =
,{0x00, 0x41, 0x22, 0x14, 0x08} // 3e >
,{0x02, 0x01, 0x51, 0x09, 0x06} // 3f ?
,{0x32, 0x49, 0x79, 0x41, 0x3e} // 40 @
,{0x7e, 0x11, 0x11, 0x11, 0x7e} // 41 A
,{0x7f, 0x49, 0x49, 0x49, 0x36} // 42 B
,{0x3e, 0x41, 0x41, 0x41, 0x22} // 43 C
,{0x7f, 0x41, 0x41, 0x22, 0x1c} // 44 D
,{0x7f, 0x49, 0x49, 0x49, 0x41} // 45 E
,{0x7f, 0x09, 0x09, 0x09, 0x01} // 46 F
,{0x3e, 0x41, 0x49, 0x49, 0x7a} // 47 G
,{0x7f, 0x08, 0x08, 0x08, 0x7f} // 48 H
,{0x00, 0x41, 0x7f, 0x41, 0x00} // 49 I
,{0x20, 0x40, 0x41, 0x3f, 0x01} // 4a J
,{0x7f, 0x08, 0x14, 0x22, 0x41} // 4b K
,{0x7f, 0x40, 0x40, 0x40, 0x40} // 4c L
,{0x7f, 0x02, 0x0c, 0x02, 0x7f} // 4d M
,{0x7f, 0x04, 0x08, 0x10, 0x7f} // 4e N
,{0x3e, 0x41, 0x41, 0x41, 0x3e} // 4f O
,{0x7f, 0x09, 0x09, 0x09, 0x06} // 50 P
,{0x3e, 0x41, 0x51, 0x21, 0x5e} // 51 Q
,{0x7f, 0x09, 0x19, 0x29, 0x46} // 52 R
,{0x46, 0x49, 0x49, 0x49, 0x31} // 53 S
,{0x01, 0x01, 0x7f, 0x01, 0x01} // 54 T
,{0x3f, 0x40, 0x40, 0x40, 0x3f} // 55 U
,{0x1f, 0x20, 0x40, 0x20, 0x1f} // 56 V
,{0x3f, 0x40, 0x38, 0x40, 0x3f} // 57 W
,{0x63, 0x14, 0x08, 0x14, 0x63} // 58 X
,{0x07, 0x08, 0x70, 0x08, 0x07} // 59 Y
,{0x61, 0x51, 0x49, 0x45, 0x43} // 5a Z
,{0x00, 0x7f, 0x41, 0x41, 0x00} // 5b [
,{0x02, 0x04, 0x08, 0x10, 0x20} // 5c ?
,{0x00, 0x41, 0x41, 0x7f, 0x00} // 5d ]
,{0x04, 0x02, 0x01, 0x02, 0x04} // 5e ^
,{0x40, 0x40, 0x40, 0x40, 0x40} // 5f _
,{0x00, 0x01, 0x02, 0x04, 0x00} // 60 `
,{0x20, 0x54, 0x54, 0x54, 0x78} // 61 a
,{0x7f, 0x48, 0x44, 0x44, 0x38} // 62 b
,{0x38, 0x44, 0x44, 0x44, 0x20} // 63 c
,{0x38, 0x44, 0x44, 0x48, 0x7f} // 64 d
,{0x38, 0x54, 0x54, 0x54, 0x18} // 65 e
,{0x08, 0x7e, 0x09, 0x01, 0x02} // 66 f
,{0x0c, 0x52, 0x52, 0x52, 0x3e} // 67 g
,{0x7f, 0x08, 0x04, 0x04, 0x78} // 68 h
,{0x00, 0x44, 0x7d, 0x40, 0x00} // 69 i
,{0x20, 0x40, 0x44, 0x3d, 0x00} // 6a j 
,{0x7f, 0x10, 0x28, 0x44, 0x00} // 6b k
,{0x00, 0x41, 0x7f, 0x40, 0x00} // 6c l
,{0x7c, 0x04, 0x18, 0x04, 0x78} // 6d m
,{0x7c, 0x08, 0x04, 0x04, 0x78} // 6e n
,{0x38, 0x44, 0x44, 0x44, 0x38} // 6f o
,{0x7c, 0x14, 0x14, 0x14, 0x08} // 70 p
,{0x08, 0x14, 0x14, 0x18, 0x7c} // 71 q
,{0x7c, 0x08, 0x04, 0x04, 0x08} // 72 r
,{0x48, 0x54, 0x54, 0x54, 0x20} // 73 s
,{0x04, 0x3f, 0x44, 0x40, 0x20} // 74 t
,{0x3c, 0x40, 0x40, 0x20, 0x7c} // 75 u
,{0x1c, 0x20, 0x40, 0x20, 0x1c} // 76 v
,{0x3c, 0x40, 0x30, 0x40, 0x3c} // 77 w
,{0x44, 0x28, 0x10, 0x28, 0x44} // 78 x
,{0x0c, 0x50, 0x50, 0x50, 0x3c} // 79 y
,{0x44, 0x64, 0x54, 0x4c, 0x44} // 7a z
,{0x00, 0x08, 0x36, 0x41, 0x00} // 7b {
,{0x00, 0x00, 0x7f, 0x00, 0x00} // 7c |
,{0x00, 0x41, 0x36, 0x08, 0x00} // 7d }
,{0x10, 0x08, 0x08, 0x10, 0x08} // 7e <
,{0x78, 0x46, 0x41, 0x46, 0x78} // 7f >
};

void pcd8544_init(void);
void pcd8544_send(uint8_t dc, uint8_t data);
void pcd8544_print_string(char *str);
void pcd8544_send_char(uint8_t ch);
void pcd8544_clear(void);
void pcd8544_set_cursor(uint8_t x, uint8_t y);
void pcd8544_print_at(char *str, uint8_t size, uint8_t x, uint8_t y);
void pcd8544_send_char_size2(uint8_t ch, uint8_t x, uint8_t y);
void pcd8544_send_char_size3(uint8_t ch, uint8_t x, uint8_t y);
void pcd8544_print_uint8_at(uint8_t num, uint8_t size, uint8_t x, uint8_t y);

int main(void)
{
   pcd8544_init();
   uint8_t i=0;
   for (;;){
      pcd8544_clear();
      pcd8544_print_at(">",3,0,2);
      pcd8544_print_uint8_at(i++,3,3,2);
      _delay_ms(1000);
   }

   return 0;
}

void pcd8544_init(void)
{
   // GPIO Setup
   DDR_PCD8544 |= (1<<PIN_SCE) | (1<<PIN_RESET) | (1<<PIN_DC) | (1<<PIN_SDIN) | (1<<PIN_SCLK);
   PORT_PCD8544&=~(1<<PIN_RESET);
   PORT_PCD8544|=(1<<PIN_RESET);
   pcd8544_send(LCD_C, 0x21 );  // LCD Extended Commands.
   pcd8544_send(LCD_C, 0xBA );  // Set LCD Vop (Contrast).
   pcd8544_send(LCD_C, 0x04 );  // Set Temp coefficent. //0x04
   pcd8544_send(LCD_C, 0x14 );  // LCD bias mode 1:48. //0x13
   pcd8544_send(LCD_C, 0x20 );  // LCD Basic Commands
   pcd8544_send(LCD_C, 0x0C );  // LCD in normal mode.
}

void pcd8544_send(uint8_t dc, uint8_t data)
{
   uint8_t i;
   if (dc == LCD_D)
      PORT_PCD8544 |= (1<<PIN_DC);
   else
      PORT_PCD8544 &= ~(1<<PIN_DC);

   PORT_PCD8544&=~(1<<PIN_SCE);
   for (i=0; i<8; i++)
   {
      PORT_PCD8544=(data & 0x80) ? PORT_PCD8544 | (1<<PIN_SDIN) : PORT_PCD8544 & ~(1<<PIN_SDIN);

      data=(data<<1);

      PORT_PCD8544|=(1<<PIN_SCLK);
      PORT_PCD8544&=~(1<<PIN_SCLK);
   }
   PORT_PCD8544|=(1<<PIN_SCE);
}

void pcd8544_print_string(char *str)
{
   while (*str)
   {
      pcd8544_send_char(*str++);
   }
}

void pcd8544_send_char(uint8_t ch)
{
   int i;
   if (ch >= 0x20 && ch <= 0x80)
   {
      pcd8544_send(LCD_D, 0x00);
      for (i = 0; i < 5; i++)
      {
         pcd8544_send(LCD_D,  ASCII[ch - 0x20][i]);
      }
      pcd8544_send(LCD_D, 0x00);
   }
}
void pcd8544_clear(void)
{
   int i;
   for (i=0; i < LCD_X * LCD_Y / 8; i++)
   {
      pcd8544_send(LCD_D, 0x00);
   }
}

void pcd8544_set_cursor(uint8_t x, uint8_t y) {
    x=x%12; y=y%6;
    pcd8544_send(LCD_C, 0x40+y);
    pcd8544_send(LCD_C, 0x80+x*7);
}

void pcd8544_print_at(char *str, uint8_t size,  uint8_t x, uint8_t y)
{
    uint8_t i=0;
    pcd8544_set_cursor(x,y);
    switch (size) {
      case 3:
      while (*str)
      {
         pcd8544_send_char_size3(*str++,x+i,y);
         i+=3;
      }
      break;
      case 2:
      while (*str)
      {
        pcd8544_send_char_size2(*str++,x+i,y);
        i+=2;
      }
      break;
      default:
      while (*str)
      {
        pcd8544_send_char(*str++);
      }
      break;
   }
}

void pcd8544_send_char_size2(uint8_t ch, uint8_t x, uint8_t y) {
    uint8_t s[5]; // source
    uint8_t r[20]; // result
    uint8_t i,j;
   // get littera
    if (ch >= 0x20 && ch <= 0x80)
    {
        for (i=0; i < 5; i++)
        {
            s[i]=ASCII[ch-0x20][i];
        }
    }
    // scale
    for(i=0;i<5;i++)
    {
        uint8_t b=0;
        uint8_t a=0;
        for(j=0;j<4;j++)
        {
            b=(s[i]>>j) & 0x01;
            a|=(b<<(j<<1)) | (b<<((j<<1)+1));
        }
        r[(i<<1)]=a;
        r[(i<<1)+1]=a;
    }

    for(i=0;i<5;i++)
    {
        uint8_t b=0;
        uint8_t a=0;
        for(j=0;j<4;j++)
        {
            b=(s[i]>>(j+4)) & 0x01;
            a|=(b<<(j<<1)) | (b<<((j<<1)+1));
        }
        r[(i<<1)+10]=a;
        r[(i<<1)+11]=a;
    }
    // print
    pcd8544_set_cursor(x,y);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    for(i=0;i<10;i++)
        pcd8544_send(LCD_D, r[i]);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);

    pcd8544_set_cursor(x,y+1);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    for(i=10;i<20;i++)
        pcd8544_send(LCD_D, r[i]);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
}
void pcd8544_send_char_size3(uint8_t ch, uint8_t x, uint8_t y) {
    uint8_t s[5]; // source
    uint8_t r[45]; // result
    uint8_t i;
    // get littera
    if (ch >= 0x20 && ch <= 0x80)
    {
        for (i=0; i < 5; i++)
        {
            s[i]=ASCII[ch-0x20][i];
        }
    }
    // scale
    for(i=0;i<5;i++)
    {
        uint8_t b,a;
        b=(s[i] & 0x01);
        a=(b) ? 0x7 : 0;
        b=(s[i]>>1) & 0x01;
        if (b) a|=0x38;
        b=(s[i]>>2) & 0x01;
        a|=(b<<6)|(b<<7);

        r[i*3]=a;
        r[i*3+1]=a;
        r[i*3+2]=a;

        r[i*3+15]=b;
        r[i*3+16]=b;
        r[i*3+17]=b;
    }

    for(i=0;i<5;i++)
    {
        uint8_t b,a;
        b=(s[i]>>3) & 0x01;
        a=(b) ? 0x0e : 0;
        b=(s[i]>>4) & 0x01;
        if (b) a|=0x70;
        b=(s[i]>>5) & 0x01;
        a|=(b<<7);

        r[i*3+15]|=a;
        r[i*3+16]|=a;
        r[i*3+17]|=a;
     }

    for(i=0;i<5;i++)
    {
        uint8_t b,a;
        b=(s[i]>>5) & 0x01;
        a=(b) ? 0x3 : 0;
        b=(s[i]>>6) & 0x01;
        if (b) a|=0x1c;
        b=(s[i]>>7) & 0x01;
        if (b) a|=0xe0;

        r[i*3+30]=a;
        r[i*3+31]=a;
        r[i*3+32]=a;
     }

    // print
    pcd8544_set_cursor(x,y);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    for(i=0;i<15;i++)
        pcd8544_send(LCD_D, r[i]);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);

    pcd8544_set_cursor(x,y+1);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    for(i=15;i<30;i++)
        pcd8544_send(LCD_D, r[i]);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);

    pcd8544_set_cursor(x,y+2);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    for(i=30;i<45;i++)
        pcd8544_send(LCD_D, r[i]);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
}

void pcd8544_print_uint8_at(uint8_t num, uint8_t size, uint8_t x, uint8_t y){
    uint8_t sym[3];
    int8_t i=2;
    do  {
      if (num == 0 && i<2)
        sym[i]=0x20; // space
      else
        sym[i]=0x30+num%10;

      num=num/10;
      i--;

    } while (i>=0);

    uint8_t j=0;
    for (i=0;i<3;i++)
    {
        if (!(i<2 && sym[i] == 0x20))
        {
            switch(size) {
            case 3:
                pcd8544_send_char_size3(sym[i],x+j*size,y);
                break;
            case 2:
                pcd8544_send_char_size2(sym[i],x+j*size,y);
                break;
            default:
                pcd8544_send_char(sym[i]);
                break;
            }
            j++;
        }
    }
}

6) Создание проекта для реального микроконтроллера ATmega8, чтение таблицы ASCII из флеш-памяти

К сожалению, это все на был способен Proteus, поэтому переходим к реальному железу. Сейчас мы создадим проект и заодно перепишем код на хранение ascii-таблицы в флеш-памяти, с последующим чтением ее оттуда. Так следовало бы сделать с самого начала, если бы не Proteus :(

Итак, предполагаем, что используется Linux или CYGWIN в случае с Windows. Т.к. работа с флеш-памятью может меняться в зависимости от то ли версии avr-gcc то ли версии avr-libc, скажу что avr-libc у меня версии 1.8.1, а версия avr-gcc:

$ avr-gcc --version
avr-gcc (GCC) 4.9.2
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Создаем структуру каталогов будущего проекта:

$ mkdir -p ./06_flash_ascii/{inc,src}

Заходим:

$ cd ./06_flash_ascii

Создаем Makefile такого содержания:

MCU=atmega8
OBJCOPY=avr-objcopy
CC=avr-gcc
CFLAGS=-mmcu=$(MCU) -Os -fdata-sections -ffunction-sections -DF_CPU=16000000UL -Wall -I ./inc
LDFLAGS=-mmcu=$(MCU) -Wl,--gc-sections  -Werror
OBJ=main.o pcd8544_soft.o
TARGET=lcd
.PHONY: all install clean

%.o:    ./src/%.c
        $(CC)  -c -o $@ $< $(CFLAGS)
all:    $(OBJ)
        $(CC) $(LDFLAGS) -o $(TARGET).elf  $(OBJ)
        $(OBJCOPY) -O ihex $(TARGET).elf $(TARGET).hex
install:
        avrdude  -p$(MCU) -c usbasp -p m8   -U flash:w:./$(TARGET).hex:i
clean:
        @rm -v *.elf *.hex $(OBJ)

Не забывайте, что в Makefile отступы формируются табуляцией, а не пробелами, как будет если скопировать из браузера(ну нету в HTML табуляции).

Теперь создадим файл ./inc/ascii.h с нашей табличкой:

#ifndef __ASCII_H__
#define __ASCII_H__
#include <avr/pgmspace.h>

const uint8_t ASCII[][5] PROGMEM = {
 {0x00, 0x00, 0x00, 0x00, 0x00} // 20
,{0x00, 0x00, 0x5f, 0x00, 0x00} // 21 !
,{0x00, 0x07, 0x00, 0x07, 0x00} // 22 "
,{0x14, 0x7f, 0x14, 0x7f, 0x14} // 23 #
,{0x24, 0x2a, 0x7f, 0x2a, 0x12} // 24 $
,{0x23, 0x13, 0x08, 0x64, 0x62} // 25 %
,{0x36, 0x49, 0x55, 0x22, 0x50} // 26 &
,{0x00, 0x05, 0x03, 0x00, 0x00} // 27 '
,{0x00, 0x1c, 0x22, 0x41, 0x00} // 28 (
,{0x00, 0x41, 0x22, 0x1c, 0x00} // 29 )
,{0x14, 0x08, 0x3e, 0x08, 0x14} // 2a *
,{0x08, 0x08, 0x3e, 0x08, 0x08} // 2b +
,{0x00, 0x50, 0x30, 0x00, 0x00} // 2c ,
,{0x08, 0x08, 0x08, 0x08, 0x08} // 2d -
,{0x00, 0x60, 0x60, 0x00, 0x00} // 2e .
,{0x20, 0x10, 0x08, 0x04, 0x02} // 2f /
,{0x3e, 0x51, 0x49, 0x45, 0x3e} // 30 0
,{0x00, 0x42, 0x7f, 0x40, 0x00} // 31 1
,{0x42, 0x61, 0x51, 0x49, 0x46} // 32 2
,{0x21, 0x41, 0x45, 0x4b, 0x31} // 33 3
,{0x18, 0x14, 0x12, 0x7f, 0x10} // 34 4
,{0x27, 0x45, 0x45, 0x45, 0x39} // 35 5
,{0x3c, 0x4a, 0x49, 0x49, 0x30} // 36 6
,{0x01, 0x71, 0x09, 0x05, 0x03} // 37 7
,{0x36, 0x49, 0x49, 0x49, 0x36} // 38 8
,{0x06, 0x49, 0x49, 0x29, 0x1e} // 39 9
,{0x00, 0x36, 0x36, 0x00, 0x00} // 3a :
,{0x00, 0x56, 0x36, 0x00, 0x00} // 3b ;
,{0x08, 0x14, 0x22, 0x41, 0x00} // 3c <
,{0x14, 0x14, 0x14, 0x14, 0x14} // 3d =
,{0x00, 0x41, 0x22, 0x14, 0x08} // 3e >
,{0x02, 0x01, 0x51, 0x09, 0x06} // 3f ?
,{0x32, 0x49, 0x79, 0x41, 0x3e} // 40 @
,{0x7e, 0x11, 0x11, 0x11, 0x7e} // 41 A
,{0x7f, 0x49, 0x49, 0x49, 0x36} // 42 B
,{0x3e, 0x41, 0x41, 0x41, 0x22} // 43 C
,{0x7f, 0x41, 0x41, 0x22, 0x1c} // 44 D
,{0x7f, 0x49, 0x49, 0x49, 0x41} // 45 E
,{0x7f, 0x09, 0x09, 0x09, 0x01} // 46 F
,{0x3e, 0x41, 0x49, 0x49, 0x7a} // 47 G
,{0x7f, 0x08, 0x08, 0x08, 0x7f} // 48 H
,{0x00, 0x41, 0x7f, 0x41, 0x00} // 49 I
,{0x20, 0x40, 0x41, 0x3f, 0x01} // 4a J
,{0x7f, 0x08, 0x14, 0x22, 0x41} // 4b K
,{0x7f, 0x40, 0x40, 0x40, 0x40} // 4c L
,{0x7f, 0x02, 0x0c, 0x02, 0x7f} // 4d M
,{0x7f, 0x04, 0x08, 0x10, 0x7f} // 4e N
,{0x3e, 0x41, 0x41, 0x41, 0x3e} // 4f O
,{0x7f, 0x09, 0x09, 0x09, 0x06} // 50 P
,{0x3e, 0x41, 0x51, 0x21, 0x5e} // 51 Q
,{0x7f, 0x09, 0x19, 0x29, 0x46} // 52 R
,{0x46, 0x49, 0x49, 0x49, 0x31} // 53 S
,{0x01, 0x01, 0x7f, 0x01, 0x01} // 54 T
,{0x3f, 0x40, 0x40, 0x40, 0x3f} // 55 U
,{0x1f, 0x20, 0x40, 0x20, 0x1f} // 56 V
,{0x3f, 0x40, 0x38, 0x40, 0x3f} // 57 W
,{0x63, 0x14, 0x08, 0x14, 0x63} // 58 X
,{0x07, 0x08, 0x70, 0x08, 0x07} // 59 Y
,{0x61, 0x51, 0x49, 0x45, 0x43} // 5a Z
,{0x00, 0x7f, 0x41, 0x41, 0x00} // 5b [
,{0x02, 0x04, 0x08, 0x10, 0x20} // 5c ¥
,{0x00, 0x41, 0x41, 0x7f, 0x00} // 5d ]
,{0x04, 0x02, 0x01, 0x02, 0x04} // 5e ^
,{0x40, 0x40, 0x40, 0x40, 0x40} // 5f _
,{0x00, 0x01, 0x02, 0x04, 0x00} // 60 `
,{0x20, 0x54, 0x54, 0x54, 0x78} // 61 a
,{0x7f, 0x48, 0x44, 0x44, 0x38} // 62 b
,{0x38, 0x44, 0x44, 0x44, 0x20} // 63 c
,{0x38, 0x44, 0x44, 0x48, 0x7f} // 64 d
,{0x38, 0x54, 0x54, 0x54, 0x18} // 65 e
,{0x08, 0x7e, 0x09, 0x01, 0x02} // 66 f
,{0x0c, 0x52, 0x52, 0x52, 0x3e} // 67 g
,{0x7f, 0x08, 0x04, 0x04, 0x78} // 68 h
,{0x00, 0x44, 0x7d, 0x40, 0x00} // 69 i
,{0x20, 0x40, 0x44, 0x3d, 0x00} // 6a j 
,{0x7f, 0x10, 0x28, 0x44, 0x00} // 6b k
,{0x00, 0x41, 0x7f, 0x40, 0x00} // 6c l
,{0x7c, 0x04, 0x18, 0x04, 0x78} // 6d m
,{0x7c, 0x08, 0x04, 0x04, 0x78} // 6e n
,{0x38, 0x44, 0x44, 0x44, 0x38} // 6f o
,{0x7c, 0x14, 0x14, 0x14, 0x08} // 70 p
,{0x08, 0x14, 0x14, 0x18, 0x7c} // 71 q
,{0x7c, 0x08, 0x04, 0x04, 0x08} // 72 r
,{0x48, 0x54, 0x54, 0x54, 0x20} // 73 s
,{0x04, 0x3f, 0x44, 0x40, 0x20} // 74 t
,{0x3c, 0x40, 0x40, 0x20, 0x7c} // 75 u
,{0x1c, 0x20, 0x40, 0x20, 0x1c} // 76 v
,{0x3c, 0x40, 0x30, 0x40, 0x3c} // 77 w
,{0x44, 0x28, 0x10, 0x28, 0x44} // 78 x
,{0x0c, 0x50, 0x50, 0x50, 0x3c} // 79 y
,{0x44, 0x64, 0x54, 0x4c, 0x44} // 7a z
,{0x00, 0x08, 0x36, 0x41, 0x00} // 7b {
,{0x00, 0x00, 0x7f, 0x00, 0x00} // 7c |
,{0x00, 0x41, 0x36, 0x08, 0x00} // 7d }
,{0x10, 0x08, 0x08, 0x10, 0x08} // 7e ←
,{0x78, 0x46, 0x41, 0x46, 0x78} // 7f
};

#endif

А также заголовочный файл библиотеки pcd8544 ./scr/pcd8544_soft.h

#ifndef __PCD8544_SOFT_H__
#define __PCD8544_SOFT_H__

#define PIN_SCE   PD7
#define PIN_RESET PD6
#define PIN_DC    PD5
#define PIN_SDIN  PD4
#define PIN_SCLK  PD3

#define LCD_C     0x00
#define LCD_D     0x01

#define LCD_X     84
#define LCD_Y     48

#define PORT_PCD8544 PORTD
#define DDR_PCD8544  DDRD

extern void pcd8544_init(void);
extern void pcd8544_send(uint8_t dc, uint8_t data);
extern void pcd8544_print_string(char *str);
extern void pcd8544_send_char(uint8_t ch);
extern void pcd8544_clear(void);
extern void pcd8544_set_cursor(uint8_t x, uint8_t y);
extern void pcd8544_print_at(char *str, uint8_t size, uint8_t x, uint8_t y);
extern void pcd8544_send_char_size2(uint8_t ch, uint8_t x, uint8_t y);
extern void pcd8544_send_char_size3(uint8_t ch, uint8_t x, uint8_t y);
extern void pcd8544_print_uint8_at(uint8_t num, uint8_t size, uint8_t x, uint8_t y);

#endif

В функции pcd8544_send_char(uint8_t ch):

void pcd8544_send_char(uint8_t ch)
{
   int i;
   if (ch >= 0x20 && ch <= 0x80)
   {
      pcd8544_send(LCD_D, 0x00);
      for (i = 0; i < 5; i++)
      {
         pcd8544_send(LCD_D,  ASCII[ch - 0x20][i]);
      }
      pcd8544_send(LCD_D, 0x00);
   }
}

- нужно будет добавить чтение из флеш-памяти(тема рассматривалась здесь)

void pcd8544_send_char(uint8_t ch)
{
   int i;
   char * ptr= (char *)(&ASCII);
   if (ch >= 0x20 && ch <= 0x80)
   {
      pcd8544_send(LCD_D, 0x00);
      for (i = 0; i < 5; i++)
      {
            uint8_t c=ch - 0x20;
            c=pgm_read_byte(ptr+c*5+i);
            pcd8544_send(LCD_D,  c);
      }
      pcd8544_send(LCD_D, 0x00);
   }
}

Тоже самое нужно будет сделать в функциях void pcd8544_send_char_size2(uint8_t ch, uint8_t x, uint8_t y) и void pcd8544_send_char_size3(uint8_t ch, uint8_t x, uint8_t y).

Все функции мы скинем в библиотечный файл /src/pcd8544_soft.c

#include "ascii.h"
#include "pcd8544_soft.h"

void pcd8544_init(void)
{
   // GPIO Setup
   DDR_PCD8544 |= (1<<PIN_SCE) | (1<<PIN_RESET) | (1<<PIN_DC) | (1<<PIN_SDIN) | (1<<PIN_SCLK);
   PORT_PCD8544&=~(1<<PIN_RESET);
   PORT_PCD8544|=(1<<PIN_RESET);
   pcd8544_send(LCD_C, 0x21 );  // LCD Extended Commands.
   pcd8544_send(LCD_C, 0xBA );  // Set LCD Vop (Contrast).
   pcd8544_send(LCD_C, 0x04 );  // Set Temp coefficent. //0x04
   pcd8544_send(LCD_C, 0x14 );  // LCD bias mode 1:48. //0x13
   pcd8544_send(LCD_C, 0x20 );  // LCD Basic Commands
   pcd8544_send(LCD_C, 0x0C );  // LCD in normal mode.
}

void pcd8544_send(uint8_t dc, uint8_t data)
{
   uint8_t i;
   if (dc == LCD_D)
      PORT_PCD8544 |= (1<<PIN_DC);
   else
      PORT_PCD8544 &= ~(1<<PIN_DC);

   PORT_PCD8544&=~(1<<PIN_SCE);
   for (i=0; i<8; i++)
   {
      PORT_PCD8544=(data & 0x80) ? PORT_PCD8544 | (1<<PIN_SDIN) : PORT_PCD8544 & ~(1<<PIN_SDIN);

      data=(data<<1);

      PORT_PCD8544|=(1<<PIN_SCLK);
      PORT_PCD8544&=~(1<<PIN_SCLK);
   }
   PORT_PCD8544|=(1<<PIN_SCE);
}

void pcd8544_print_string(char *str)
{
   while (*str)
   {
      pcd8544_send_char(*str++);
   }
}

void pcd8544_send_char(uint8_t ch)
{
   int i;
   char * ptr= (char *)(&ASCII);
   if (ch >= 0x20 && ch <= 0x80)
   {
      pcd8544_send(LCD_D, 0x00);
      for (i = 0; i < 5; i++)
      {
            uint8_t c=ch - 0x20;
            c=pgm_read_byte(ptr+c*5+i);
            pcd8544_send(LCD_D,  c);
      }
      pcd8544_send(LCD_D, 0x00);
   }
}
void pcd8544_clear(void)
{
   int i;
   for (i=0; i < LCD_X * LCD_Y / 8; i++)
   {
      pcd8544_send(LCD_D, 0x00);
   }
}

void pcd8544_set_cursor(uint8_t x, uint8_t y) {
    x=x%12; y=y%6;
    pcd8544_send(LCD_C, 0x40+y);
    pcd8544_send(LCD_C, 0x80+x*7);
}

void pcd8544_print_at(char *str, uint8_t size,  uint8_t x, uint8_t y)
{
    uint8_t i=0;
    pcd8544_set_cursor(x,y);
    switch (size) {
      case 3:
      while (*str)
      {
         pcd8544_send_char_size3(*str++,x+i,y);
         i+=3;
      }
      break;
      case 2:
      while (*str)
      {
        pcd8544_send_char_size2(*str++,x+i,y);
        i+=2;
      }
      break;
      default:
      while (*str)
      {
        pcd8544_send_char(*str++);
      }
      break;
   }
}

void pcd8544_send_char_size2(uint8_t ch, uint8_t x, uint8_t y) {
    uint8_t s[5]; // source
    uint8_t r[20]; // result
    uint8_t i,j;
   // get littera
    char * ptr= (char *)(&ASCII);
    if (ch >= 0x20 && ch <= 0x80)
    {
        for (i=0; i < 5; i++)
        {
              uint8_t c=ch - 0x20;
              s[i]=pgm_read_byte(ptr+c*5+i);
        }
    }
    // scale
    for(i=0;i<5;i++)
    {
        uint8_t b=0;
        uint8_t a=0;
        for(j=0;j<4;j++)
        {
            b=(s[i]>>j) & 0x01;
            a|=(b<<(j<<1)) | (b<<((j<<1)+1));
        }
        r[(i<<1)]=a;
        r[(i<<1)+1]=a;
    }

    for(i=0;i<5;i++)
    {
        uint8_t b=0;
        uint8_t a=0;
        for(j=0;j<4;j++)
        {
            b=(s[i]>>(j+4)) & 0x01;
            a|=(b<<(j<<1)) | (b<<((j<<1)+1));
        }
        r[(i<<1)+10]=a;
        r[(i<<1)+11]=a;
    }
    // print
    pcd8544_set_cursor(x,y);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    for(i=0;i<10;i++)
        pcd8544_send(LCD_D, r[i]);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);

    pcd8544_set_cursor(x,y+1);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    for(i=10;i<20;i++)
        pcd8544_send(LCD_D, r[i]);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
}
void pcd8544_send_char_size3(uint8_t ch, uint8_t x, uint8_t y) {
    uint8_t s[5]; // source
    uint8_t r[45]; // result
    uint8_t i;
    // get littera
    char * ptr= (char *)(&ASCII);
    if (ch >= 0x20 && ch <= 0x80)
    {
        for (i=0; i < 5; i++)
        {
              uint8_t c=ch - 0x20;
              s[i]=pgm_read_byte(ptr+c*5+i);
        }
    }
    // scale
    for(i=0;i<5;i++)
    {
        uint8_t b,a;
        b=(s[i] & 0x01);
        a=(b) ? 0x7 : 0;
        b=(s[i]>>1) & 0x01;
        if (b) a|=0x38;
        b=(s[i]>>2) & 0x01;
        a|=(b<<6)|(b<<7);

        r[i*3]=a;
        r[i*3+1]=a;
        r[i*3+2]=a;

        r[i*3+15]=b;
        r[i*3+16]=b;
        r[i*3+17]=b;
    }

    for(i=0;i<5;i++)
    {
        uint8_t b,a;
        b=(s[i]>>3) & 0x01;
        a=(b) ? 0x0e : 0;
        b=(s[i]>>4) & 0x01;
        if (b) a|=0x70;
        b=(s[i]>>5) & 0x01;
        a|=(b<<7);

        r[i*3+15]|=a;
        r[i*3+16]|=a;
        r[i*3+17]|=a;
     }

    for(i=0;i<5;i++)
    {
        uint8_t b,a;
        b=(s[i]>>5) & 0x01;
        a=(b) ? 0x3 : 0;
        b=(s[i]>>6) & 0x01;
        if (b) a|=0x1c;
        b=(s[i]>>7) & 0x01;
        if (b) a|=0xe0;

        r[i*3+30]=a;
        r[i*3+31]=a;
        r[i*3+32]=a;
     }

    // print
    pcd8544_set_cursor(x,y);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    for(i=0;i<15;i++)
        pcd8544_send(LCD_D, r[i]);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);

    pcd8544_set_cursor(x,y+1);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    for(i=15;i<30;i++)
        pcd8544_send(LCD_D, r[i]);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);

    pcd8544_set_cursor(x,y+2);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    for(i=30;i<45;i++)
        pcd8544_send(LCD_D, r[i]);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
    pcd8544_send(LCD_D, 0x00);
}

void pcd8544_print_uint8_at(uint8_t num, uint8_t size, uint8_t x, uint8_t y){
    uint8_t sym[3];
    int8_t i=2;
    do  {
      if (num == 0 && i<2)
        sym[i]=0x20; // space
      else
        sym[i]=0x30+num%10;

      num=num/10;
      i--;

    } while (i>=0);

    uint8_t j=0;
    for (i=0;i<3;i++)
    {
        if (!(i<2 && sym[i] == 0x20))
        {
            switch(size) {
            case 3:
                pcd8544_send_char_size3(sym[i],x+j*size,y);
                break;
            case 2:
                pcd8544_send_char_size2(sym[i],x+j*size,y);
                break;
            default:
                pcd8544_send_char(sym[i]);
                break;
            }
            j++;
        }
    }
}

В итоге остается непосредственно сама программа в main.c

#include "avr/io.h"
#include "util/delay.h"
#include "pcd8544_soft.h"

int main(void)
{
   pcd8544_init();
   uint8_t i=0;
   for (;;){
      pcd8544_clear();
      pcd8544_print_at(">",3,0,2);
      pcd8544_print_uint8_at(i++,3,3,2);
      _delay_ms(1000);
   }

   return 0;
}

По-моему гораздо, лучше, чем все в одном файле.

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

Полные исходники к этому примеру можно посмотреть здесь:
https://gitlab.com/flank1er/pcd8544_atmega8/tree/master/06_flash_ascii

7) Добавляем кириллицу в кодировке CP866

Сложность с выводом русского текста на экран графического дисплея заключается в том, что тексты программ мы пишем в двухбайтовом юникоде. Не будем же вы кодировку юникода записывать в микроконтроллер? Поэтому вспоминаются былинные 8-битные кодировки: cp866, koi8-r и cp1251. И значит возникает необходимость преобразования одной кодировки в другую. Доходит до того, что это преобразование пытаются выполнять на стороне микроконтроллера. Нда..

Я выбрал кодировку CP866 из-за того, что там есть псевдографика. В свое время с ее использованием много чего делали: Лексикон, Дос Навигатор, Нортон Командер и т.д. Однако пока псевдографику трогать не будем, а будем заниматься непосредственно кириллицей. Кодовую таблицу CP866 можно посмотреть например здесь Таблица CP 866 DOS — Kvodo

Обращаю внимание, что строчными буквами "п" и "р" имеется разрыв на 48 символа псевдографики. В своей ASCII таблице мы все символы запишем единым массивом, соответственно индекс символа нужно будет корректировать.

Это значит, что вместо строки:

uint8_t c=ch - 0x20;

нужно будет использовать:

uint8_t c=(ch<0xe0) ? ch - 0x20 : ch - 0x50;

Для преобразования кодировок, в Makefile добавим новую цель:

prepare:
        cat main.c|iconv -f utf8 -t cp866 > cp866.c

Теперь для того чтобы перекодировать и сразу скомпилировать программу достаточно будет ввести команду:

$ make prepare && make all

Осталось табличный файл c ASCII дополнить русскими буквами:

#ifndef __ASCII_H__
#define __ASCII_H__
#include <avr/pgmspace.h>

// 96x5 = 480 bytes
const uint8_t ASCII[][5] PROGMEM = {
 {0x00, 0x00, 0x00, 0x00, 0x00} // 20
,{0x00, 0x00, 0x5f, 0x00, 0x00} // 21 !
,{0x00, 0x07, 0x00, 0x07, 0x00} // 22 "
,{0x14, 0x7f, 0x14, 0x7f, 0x14} // 23 #
,{0x24, 0x2a, 0x7f, 0x2a, 0x12} // 24 $
,{0x23, 0x13, 0x08, 0x64, 0x62} // 25 %
,{0x36, 0x49, 0x55, 0x22, 0x50} // 26 &
,{0x00, 0x05, 0x03, 0x00, 0x00} // 27 '
,{0x00, 0x1c, 0x22, 0x41, 0x00} // 28 (
,{0x00, 0x41, 0x22, 0x1c, 0x00} // 29 )
,{0x14, 0x08, 0x3e, 0x08, 0x14} // 2a *
,{0x08, 0x08, 0x3e, 0x08, 0x08} // 2b +
,{0x00, 0x50, 0x30, 0x00, 0x00} // 2c ,
,{0x08, 0x08, 0x08, 0x08, 0x08} // 2d -
,{0x00, 0x60, 0x60, 0x00, 0x00} // 2e .
,{0x20, 0x10, 0x08, 0x04, 0x02} // 2f /
,{0x3e, 0x51, 0x49, 0x45, 0x3e} // 30 0
,{0x00, 0x42, 0x7f, 0x40, 0x00} // 31 1
,{0x42, 0x61, 0x51, 0x49, 0x46} // 32 2
,{0x21, 0x41, 0x45, 0x4b, 0x31} // 33 3
,{0x18, 0x14, 0x12, 0x7f, 0x10} // 34 4
,{0x27, 0x45, 0x45, 0x45, 0x39} // 35 5
,{0x3c, 0x4a, 0x49, 0x49, 0x30} // 36 6
,{0x01, 0x71, 0x09, 0x05, 0x03} // 37 7
,{0x36, 0x49, 0x49, 0x49, 0x36} // 38 8
,{0x06, 0x49, 0x49, 0x29, 0x1e} // 39 9
,{0x00, 0x36, 0x36, 0x00, 0x00} // 3a :
,{0x00, 0x56, 0x36, 0x00, 0x00} // 3b ;
,{0x08, 0x14, 0x22, 0x41, 0x00} // 3c <
,{0x14, 0x14, 0x14, 0x14, 0x14} // 3d =
,{0x00, 0x41, 0x22, 0x14, 0x08} // 3e >
,{0x02, 0x01, 0x51, 0x09, 0x06} // 3f ?
,{0x32, 0x49, 0x79, 0x41, 0x3e} // 40 @
,{0x7e, 0x11, 0x11, 0x11, 0x7e} // 41 A
,{0x7f, 0x49, 0x49, 0x49, 0x36} // 42 B
,{0x3e, 0x41, 0x41, 0x41, 0x22} // 43 C
,{0x7f, 0x41, 0x41, 0x22, 0x1c} // 44 D
,{0x7f, 0x49, 0x49, 0x49, 0x41} // 45 E
,{0x7f, 0x09, 0x09, 0x09, 0x01} // 46 F
,{0x3e, 0x41, 0x49, 0x49, 0x7a} // 47 G
,{0x7f, 0x08, 0x08, 0x08, 0x7f} // 48 H
,{0x00, 0x41, 0x7f, 0x41, 0x00} // 49 I
,{0x20, 0x40, 0x41, 0x3f, 0x01} // 4a J
,{0x7f, 0x08, 0x14, 0x22, 0x41} // 4b K
,{0x7f, 0x40, 0x40, 0x40, 0x40} // 4c L
,{0x7f, 0x02, 0x0c, 0x02, 0x7f} // 4d M
,{0x7f, 0x04, 0x08, 0x10, 0x7f} // 4e N
,{0x3e, 0x41, 0x41, 0x41, 0x3e} // 4f O
,{0x7f, 0x09, 0x09, 0x09, 0x06} // 50 P
,{0x3e, 0x41, 0x51, 0x21, 0x5e} // 51 Q
,{0x7f, 0x09, 0x19, 0x29, 0x46} // 52 R
,{0x46, 0x49, 0x49, 0x49, 0x31} // 53 S
,{0x01, 0x01, 0x7f, 0x01, 0x01} // 54 T
,{0x3f, 0x40, 0x40, 0x40, 0x3f} // 55 U
,{0x1f, 0x20, 0x40, 0x20, 0x1f} // 56 V
,{0x3f, 0x40, 0x38, 0x40, 0x3f} // 57 W
,{0x63, 0x14, 0x08, 0x14, 0x63} // 58 X
,{0x07, 0x08, 0x70, 0x08, 0x07} // 59 Y
,{0x61, 0x51, 0x49, 0x45, 0x43} // 5a Z
,{0x00, 0x7f, 0x41, 0x41, 0x00} // 5b [
,{0x02, 0x04, 0x08, 0x10, 0x20} // 5c ¥
,{0x00, 0x41, 0x41, 0x7f, 0x00} // 5d ]
,{0x04, 0x02, 0x01, 0x02, 0x04} // 5e ^
,{0x40, 0x40, 0x40, 0x40, 0x40} // 5f _
,{0x00, 0x01, 0x02, 0x04, 0x00} // 60 `
,{0x20, 0x54, 0x54, 0x54, 0x78} // 61 a
,{0x7f, 0x48, 0x44, 0x44, 0x38} // 62 b
,{0x38, 0x44, 0x44, 0x44, 0x20} // 63 c
,{0x38, 0x44, 0x44, 0x48, 0x7f} // 64 d
,{0x38, 0x54, 0x54, 0x54, 0x18} // 65 e
,{0x08, 0x7e, 0x09, 0x01, 0x02} // 66 f
,{0x0c, 0x52, 0x52, 0x52, 0x3e} // 67 g
,{0x7f, 0x08, 0x04, 0x04, 0x78} // 68 h
,{0x00, 0x44, 0x7d, 0x40, 0x00} // 69 i
,{0x20, 0x40, 0x44, 0x3d, 0x00} // 6a j 
,{0x7f, 0x10, 0x28, 0x44, 0x00} // 6b k
,{0x00, 0x41, 0x7f, 0x40, 0x00} // 6c l
,{0x7c, 0x04, 0x18, 0x04, 0x78} // 6d m
,{0x7c, 0x08, 0x04, 0x04, 0x78} // 6e n
,{0x38, 0x44, 0x44, 0x44, 0x38} // 6f o
,{0x7c, 0x14, 0x14, 0x14, 0x08} // 70 p
,{0x08, 0x14, 0x14, 0x18, 0x7c} // 71 q
,{0x7c, 0x08, 0x04, 0x04, 0x08} // 72 r
,{0x48, 0x54, 0x54, 0x54, 0x20} // 73 s
,{0x04, 0x3f, 0x44, 0x40, 0x20} // 74 t
,{0x3c, 0x40, 0x40, 0x20, 0x7c} // 75 u
,{0x1c, 0x20, 0x40, 0x20, 0x1c} // 76 v
,{0x3c, 0x40, 0x30, 0x40, 0x3c} // 77 w
,{0x44, 0x28, 0x10, 0x28, 0x44} // 78 x
,{0x0c, 0x50, 0x50, 0x50, 0x3c} // 79 y
,{0x44, 0x64, 0x54, 0x4c, 0x44} // 7a z
,{0x00, 0x08, 0x36, 0x41, 0x00} // 7b {
,{0x00, 0x00, 0x7f, 0x00, 0x00} // 7c |
,{0x00, 0x41, 0x36, 0x08, 0x00} // 7d }
,{0x10, 0x08, 0x08, 0x10, 0x08} // 7e ←
,{0x78, 0x46, 0x41, 0x46, 0x78} // 7f
,{0x7e, 0x11, 0x11, 0x11, 0x7e}//A  0x80
,{0x7f, 0x49, 0x49, 0x49, 0x33}//Б  0x81
,{0x7f, 0x49, 0x49, 0x49, 0x36}//В  0x82
,{0x7f, 0x01, 0x01, 0x01, 0x03}//Г  0x83
,{0xe0, 0x51, 0x4f, 0x41, 0xff}//Д  0x84
,{0x7f, 0x49, 0x49, 0x49, 0x41}//E  0x85
,{0x77, 0x08, 0x7f, 0x08, 0x77}//Ж  0x86
,{0x41, 0x49, 0x49, 0x49, 0x36}//З  0x87
,{0x7f, 0x10, 0x08, 0x04, 0x7f}//И  0x88
,{0x7c, 0x21, 0x12, 0x09, 0x7c}//Й  0x89
,{0x7f, 0x08, 0x14, 0x22, 0x41}//K  0x8A
,{0x20, 0x41, 0x3f, 0x01, 0x7f}//Л  0x8B
,{0x7f, 0x02, 0x0c, 0x02, 0x7f}//M  0x8C
,{0x7f, 0x08, 0x08, 0x08, 0x7f}//H  0x8D
,{0x3e, 0x41, 0x41, 0x41, 0x3e}//O  0x8E
,{0x7f, 0x01, 0x01, 0x01, 0x7f}//П  0x8F
,{0x7f, 0x09, 0x09, 0x09, 0x06}//P  0x90
,{0x3e, 0x41, 0x41, 0x41, 0x22}//C  0x91
,{0x01, 0x01, 0x7f, 0x01, 0x01}//T  0x92
,{0x47, 0x28, 0x10, 0x08, 0x07}//У  0x93
,{0x1c, 0x22, 0x7f, 0x22, 0x1c}//Ф  0x94
,{0x63, 0x14, 0x08, 0x14, 0x63}//X  0x95
,{0x7f, 0x40, 0x40, 0x40, 0xff}//Ц  0x96
,{0x07, 0x08, 0x08, 0x08, 0x7f}//Ч  0x97
,{0x7f, 0x40, 0x7f, 0x40, 0x7f}//Ш  0x98
,{0x7f, 0x40, 0x7f, 0x40, 0xff}//Щ  0x99
,{0x01, 0x7f, 0x48, 0x48, 0x30}//Ъ  0x9A
,{0x7f, 0x48, 0x30, 0x00, 0x7f}//Ы  0x9B
,{0x00, 0x7f, 0x48, 0x48, 0x30}//Э  0x9C
,{0x22, 0x41, 0x49, 0x49, 0x3e}//Ь  0x9D
,{0x7f, 0x08, 0x3e, 0x41, 0x3e}//Ю  0x9E
,{0x46, 0x29, 0x19, 0x09, 0x7f}//Я  0x9F
,{0x20, 0x54, 0x54, 0x54, 0x78}//a  0xA0
,{0x3c, 0x4a, 0x4a, 0x49, 0x31}//б  0xA1
,{0x7c, 0x54, 0x54, 0x28, 0x00}//в  0xA2
,{0x7c, 0x04, 0x04, 0x04, 0x0c}//г  0xA3
,{0xe0, 0x54, 0x4c, 0x44, 0xfc}//д  0xA4
,{0x38, 0x54, 0x54, 0x54, 0x18}//e  0xA5
,{0x6c, 0x10, 0x7c, 0x10, 0x6c}//ж  0xA6
,{0x44, 0x44, 0x54, 0x54, 0x28}//з  0xA7
,{0x7c, 0x20, 0x10, 0x08, 0x7c}//и  0xA8
,{0x7c, 0x41, 0x22, 0x11, 0x7c}//й  0xA9
,{0x7c, 0x10, 0x28, 0x44, 0x00}//к  0xAA
,{0x20, 0x44, 0x3c, 0x04, 0x7c}//л  0xAB
,{0x7c, 0x08, 0x10, 0x08, 0x7c}//м  0xAC
,{0x7c, 0x10, 0x10, 0x10, 0x7c}//н  0xAD
,{0x38, 0x44, 0x44, 0x44, 0x38}//o  0xAE
,{0x7c, 0x04, 0x04, 0x04, 0x7c}//п  0xAF
,{0x7C, 0x14, 0x14, 0x14, 0x08}//p  0xE0
,{0x38, 0x44, 0x44, 0x44, 0x20}//c  0xE1
,{0x04, 0x04, 0x7c, 0x04, 0x04}//т  0xE2
,{0x0C, 0x50, 0x50, 0x50, 0x3C}//у  0xE3
,{0x30, 0x48, 0xfc, 0x48, 0x30}//ф  0xE4
,{0x44, 0x28, 0x10, 0x28, 0x44}//x  0xE5
,{0x7c, 0x40, 0x40, 0x40, 0xfc}//ц  0xE6
,{0x0c, 0x10, 0x10, 0x10, 0x7c}//ч  0xE7
,{0x7c, 0x40, 0x7c, 0x40, 0x7c}//ш  0xE8
,{0x7c, 0x40, 0x7c, 0x40, 0xfc}//щ  0xE9
,{0x04, 0x7c, 0x50, 0x50, 0x20}//ъ  0xEA
,{0x7c, 0x50, 0x50, 0x20, 0x7c}//ы  0xEB
,{0x7c, 0x50, 0x50, 0x20, 0x00}//ь  0xEC
,{0x28, 0x44, 0x54, 0x54, 0x38}//э  0xED
,{0x7c, 0x10, 0x38, 0x44, 0x38}//ю  0xEE
,{0x08, 0x54, 0x34, 0x14, 0x7c}//я  0xEF
};

#endif

Шрифты брал вроде бы на arduino.ru, кто их первоначальный автор, я не знаю.

Ну и теперь, мы можем вывести на экран что-то осмысленное. main.c:

#include "avr/io.h"
#include "util/delay.h"
#include <avr/pgmspace.h>
#include "pcd8544_soft.h"

#define LEN 72
const unsigned char mystr[LEN] PROGMEM = {"оператор Лапласа это векторный дифференциальный оператор второго порядка"};

int main(void)
{
   pcd8544_init();
   pcd8544_clear();
   char * ptr= (char *)(&mystr);
   uint8_t i; for(i=0;i<LEN;i++)
   {
        pcd8544_send_char(pgm_read_byte(ptr+i));
   }

   for (;;){
      _delay_ms(1000);
   }

   return 0;
}

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

Полные исходники к этому примеру можно посмотреть здесь:
https://gitlab.com/flank1er/pcd8544_atmega8/tree/master/07_cp866

8) Добавление фреймбуфера. Программа рисования фрактала.

Фреймбуфер - это буфер в оперативной памяти микроконтроллера, размер которого равен размеру видеопамяти дисплея.

Фреймбуфер нужен для того чтобы нарисовать на экране точку. Т.к. вся видеопамять дисплея pcd8544 упакована в 504 байта, для того чтобы нарисовать точку на экране, нужно определить номер байта к которому относится данная точка, прочитать этот байт, изменить в нем нужный бит относящийся к точке, и записать этот байт обратно. Однако с нашего дисплея ничего читать нельзя, дисплей доступен только для записи. Поэтому нужен фреймбуфер, чтобы поставить простую точку.

Микроконтроллер ATmega8 имеет 1 Kбайт оперативной памяти, и фреймбуфер будет занимать почти половину этой памяти. Но например такие микроконтроллеры как MSP430G2553 с 512 байтами SRAM, оказываются уже не у дел.

Для организации фреймбуфера, в файле pcd8544_soft.c объявим массив:

#define FB_LEN  504
static uint8_t fb[FB_LEN];
uint16_t pos;

Так же пригодиться переменная pos которая будет служить позицией виртуального курсора во фреймбуфере.

Для работы с фреймбуфер ом, понадобятся дополнительные функции для очистки содержимого фреймбуфера:

void pcd8544_clear_fb(void)
{
    for(pos=0;pos<FB_LEN; pos++)
        fb[pos]=0;
    pos=0;
}

и для вывода его содержимого на дисплей:

void pcd8544_display_fb() {
    int i;
    for(i=0;i<FB_LEN; i++)
        pcd8544_send(LCD_D,fb[i]);
}

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

Например в void pcd8544_send_char_fb(uint8_t ch) меняется всего одна строчка:

void pcd8544_send_char_fb(uint8_t ch)
{
    int i;
    char * ptr= (char *)(&ASCII);

    if (ch >= 0x20 && ch <= 0xf0 && pos <= (FB_LEN-7))
    {
        for (i=0; i < 5; i++)
        {
            uint8_t c=(ch<0xe0) ? ch - 0x20 : ch - 0x50;
            c=pgm_read_byte(ptr+c*5+i);
            fb[pos+i]|=c;
        }
        pos+=7;
    }
}

После завершения всей работы хорошо было бы проверить что все работает как надо. К примеру main.c из предыдущего примера у меня получился таким:

#include "avr/io.h"
#include "util/delay.h"
#include "pcd8544_soft.h"

int main(void)
{
   pcd8544_init();
   pcd8544_clear();
   uint8_t i=0;
   for (;;){
      pcd8544_clear_fb();
      pcd8544_print_at_fb(">",3,0,2);
      pcd8544_print_uint8_at_fb(i++,3,3,2);
      pcd8544_display_fb();
      _delay_ms(1000);
   }

   return 0;
}

Теперь для работы с дисплеем нужно сначала очищать содержимое фреймбуфера, а в конце выводить его содержимое на экран функцией pcd8544_display_fb();

Теперь мы можем добавить функцию для ради которой все и затевалось, т.е. для рисования точки:

void pcd8544_set_point(uint8_t x, uint8_t y) {
    if (x < LCD_X && y < LCD_Y)
    {
        uint16_t index = ((y>>3)*LCD_X)+x;
        fb[index]|=(1<<(y&0x07));
    }
}

Выглядит гадко, но это работает.

Ок. Теперь можно что-либо нарисовать. Я адаптировал программу из старой книжки по ZX Spectrum для отрисовки простенького фрактала:

#include "avr/io.h"
#include "util/delay.h"
#include "pcd8544_soft.h"

int main(void)
{
    int8_t x[17];
    int8_t y[17];
    pcd8544_init();
    pcd8544_clear();
    for (;;)
    {
        pcd8544_clear_fb();

        uint8_t i,j,k;
        for(i=1;i<=4;i++)
        {
            for(j=1;j<=4;j++)
            {
                k=4*i+j-4;
                x[k]=j-3;
                y[k]=i-3;
            }
        }

        x[2]=0;   y[2]=-3;
        x[8]=2;   y[8]=0;
        x[9]=-3;  y[9]=-1;
        x[15]=-1; y[15]=2;

        for(i=1;i<=16;i++)
        {
            for(j=1;j<=16;j++)
            {
                for(k=1;k<=16;k++)
                {
                    int8_t xx=(((x[i]<<4)+(x[j]<<2)+x[k])>>1);
                    int8_t yy=(((y[i]<<4)+(y[j]<<2)+y[k])>>1);
                    pcd8544_set_point(50+xx,28+yy);
                }
            }
        }

        pcd8544_display_fb();

        _delay_ms(1000);
   }

   return 0;
}

Результат работы выглядит так:

Полные исходники к этому примеру можно посмотреть здесь:
https://gitlab.com/flank1er/pcd8544_atmega8/tree/master/08_2_fractal

9) Делаем скринсейвер "сквозь звездное небо"

Фрактал штука интересная, но абсолютно бесполезная. Попробуем сделать что-нибудь более полезное. Допустим, что у нашего устройства имеется ждущий режим, во время которого на дисплее будет запускаться простая анимационная заставка.

На мой взгляд самая простая заставка это - "полет сквозь звездное небо".

Наиболее простой алгоритм этой заставки работает так: a) случайным образом формируется звездное небо для первой четверти координатной плоскости; б) картинка из первой четверти зеркалится на остальные четверти; в) при каждой итерации к х и у координатам "звезд" прибавляется по единице; г) при выходе "звезды" за пределы дисплея, случайно генерируется новая "звезда".

В итоге у меня получилась такая программа:

#include "avr/io.h"
#include "util/delay.h"
#include "stdlib.h"
#include "pcd8544_soft.h"

#define NUM 40
#define LCD_X 84
#define LCD_Y 48

uint8_t x[NUM];
uint8_t y[NUM];

int main(void)
{
    pcd8544_init();
    pcd8544_clear();
    uint8_t i,j;
    for(i=0;i<NUM;i++)
    {
        x[i]=rand()%(LCD_X/2);
        y[i]=rand()%(LCD_Y/2);
    }
    for (;;){
        for(i=0;i<NUM;i++)
        {
            if (x[i] == 0 || y[i] == 0)
            {
                x[i]=rand()%(LCD_X/2);
                y[i]=rand()%(LCD_Y/2);
            } else {
                x[i]=x[i]-1;
                y[i]=y[i]-1;
            }

        }

        pcd8544_clear_fb();
        for(i=0;i<4;i++)
        {
            for(j=0;j<NUM;j++)
                pcd8544_set_point(x[j],y[j]);
            for(j=0;j<NUM;j++)
                pcd8544_set_point(LCD_X-x[j],y[j]);
            for(j=0;j<NUM;j++)
                pcd8544_set_point(LCD_X-x[j],LCD_Y-y[j]);
            for(j=0;j<NUM;j++)
                pcd8544_set_point(x[j],LCD_Y-y[j]);

        }
        pcd8544_display_fb();
        _delay_ms(300);
   }

   return 0;
}

Результат:

исходники:
https://gitlab.com/flank1er/pcd8544_atmega8/tree/master/09_1_starsky

На мой взгляд, за счет симметрии выглядит несколько странно.

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

Программа:

#include "avr/io.h"
#include "util/delay.h"
#include "stdlib.h"
#include "pcd8544_soft.h"

#define NUM 120
#define LCD_X 84
#define LCD_Y 48

uint8_t x[NUM];
uint8_t y[NUM];

int main(void)
{
    pcd8544_init();
    pcd8544_clear();
    uint8_t i,j;
    for(i=0;i<NUM;i++)
    {
        x[i]=rand()%(LCD_X);
        y[i]=rand()%(LCD_Y);
    }
    for (;;)
    {
        for(i=0;i<NUM;i++)
        {
            if (x[i] == 0 || y[i] == 0 || x[i] == LCD_X || y[i] == LCD_Y) // border control
            {
                x[i]=rand()%(LCD_X);
                y[i]=rand()%(LCD_Y);
            } else if (x[i] < LCD_X/2 && y[i] < LCD_Y/2) {
                x[i]=x[i]-1;
                y[i]=y[i]-1;
            } else if (x[i] < LCD_X/2 && y[i] >= LCD_Y/2) {
                x[i]=x[i]-1;
                y[i]=y[i]+1;
            } else if (x[i] >=  LCD_X/2 && y[i] <LCD_Y/2) {
                x[i]=x[i]+1;
                y[i]=y[i]-1;
            } else if (x[i] >= LCD_X/2 && y[i] >= LCD_Y/2) {
                x[i]=x[i]+1;
                y[i]=y[i]+1;
            }
        }
        pcd8544_clear_fb();

        for(j=0;j<NUM;j++)
            pcd8544_set_point(x[j],y[j]);

        pcd8544_display_fb();
        _delay_ms(200);
   }

   return 0;
}

Результат:

Теперь выглядит получше, но все-равно не так как я ожидал. Звездное небо хорошо смотрится на черном фоне 14-дюймового ЭЛТ монитора, а здесь, не то не сё. Ну да ладно.

исходники:
https://gitlab.com/flank1er/pcd8544_atmega8/tree/master/09_2_starsky

10) Функции рисования прямых линий, и окружностей

Целочисленные алгоритмы рисования прямых линий и окружностей можно скопировать из википедии Реализации алгоритмов/Алгоритм Брезенхэма

Т.о. функция рисования прямой линии получается такой:

void pcd8544_draw_line(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2) {
    const int deltaX = myabs(x2 - x1);
    const int deltaY = myabs(y2 - y1);
    const int signX = x1 < x2 ? 1 : -1;
    const int signY = y1 < y2 ? 1 : -1;

    int error = deltaX - deltaY;

    pcd8544_set_point(x2,y2);
    while(x1 != x2 || y1 != y2)
    {
        pcd8544_set_point(x1,y1);
        const int error2 = error * 2;

        if(error2 > -deltaY)
        {
            error -= deltaY;
            x1 += signX;
        }
        if(error2 < deltaX)
        {
            error += deltaX;
            y1 += signY;
        }
    }
}

А функция рисования окружности такой:

void pcd8544_draw_circle(uint8_t x0, uint8_t y0, uint8_t radius) {
    int x = 0;
    int y = radius;
    int delta = 1 - 2 * radius;
    int error = 0;
    while(y >= 0)
    {
        pcd8544_set_point(x0 + x, y0 + y);
        pcd8544_set_point(x0 + x, y0 - y);
        pcd8544_set_point(x0 - x, y0 + y);
        pcd8544_set_point(x0 - x, y0 - y);
        error = 2 * (delta + y) - 1;
        if(delta < 0 && error <= 0) {
            ++x;
            delta += 2 * x + 1;
            continue;
        }
        error = 2 * (delta - x) - 1;
        if(delta > 0 && error > 0) {
            --y;
            delta += 1 - 2 * y;
            continue;
        }
        ++x;
        delta += 2 * (x - y);
        --y;
    }
}

Какого-то впечатляющего примера для проверки этих функций я не нашел, поэтому предлагаю проверить их сразу в деле.

11) Интерфейс для метеостанции

Теперь собственно о том, ради чего всё это делалось. Для начала попробуем сделать интерфейс для самодельной метеостанции. Допустим, что у нас имеется уличный датчик DS18B20, и комнатный DHT11. Информация с уличного датчика будет более приоритетна, поэтому она будет выводиться крупным шрифтом, а вся остальная сопутствующая информация - мелким.

Но для начала, чтобы рисовать интерфейсы, нам понадобиться еще одна функция для рисования иконок размером 7х8. Функция для рисования букв нам не подойдёт, т.к. она рисует растр 5x8, т.е. с межбуквенным интервалом по краям. В целях оптимизации иконки будем выводить построчно, т.е. в пределах одного банка.

void pcd8544_draw_icon_fb(char * img, uint8_t x,uint8_t y, uint8_t num) {
    pos=x+y*84;
    uint8_t i;
    for(i=0;i<(num*7);i++)
    {
        uint8_t c=pgm_read_byte(img+i);
        if ((pos+i)<504) fb[pos+i]|=c;
    }
    pos+=num;
}

Здесь y - это номер строки или банка, x - это номер пикселя в пределах от 0 до 83. num - это ширина изображения в знакоместах, т.е. в пикселях ширина будет равна num*7.

В итоге программа для отрисовки интерфейса у меня получилась такой:

#include "avr/io.h"
#include "util/delay.h"
#include <avr/pgmspace.h>
#include "pcd8544_soft.h"

#define myabs(n) ((n) < 0 ? -(n) : (n))

const uint8_t blob[7] PROGMEM={112,216,134,129,134,216,112};
const uint8_t degree[7] PROGMEM={0,0,6,9,9,6,0};
const uint8_t degree_big[7] PROGMEM={0,14,27,17,27,14,0};
const uint8_t termometer[7] PROGMEM={0,96,158,129,158,96,0};

int main(void)
{
    // LCD setup
    pcd8544_init();
    /// set variables
    int16_t temp=-117;
    int8_t t1,t2;
    char i=+1;
    // main loop
     for (;;){
        pcd8544_clear_fb();
        // PRINT TIME
        pcd8544_print_at_fb("10:32", FONT_SIZE_2,0,4);
        pcd8544_print_at_fb("07", FONT_SIZE_1,10,5);
        /// print temperature
        t1=temp/10;t2=temp%10;

        if (myabs(t1)>=10) {
            pcd8544_print_uint8_at_fb(myabs(t1),FONT_SIZE_3,4,1);
            if (t1>=0)
                pcd8544_print_at_fb("+", FONT_SIZE_3,1,1);
            else
                pcd8544_print_at_fb("-", FONT_SIZE_3,1,1);
        } else {
            if (t1>=0)
                pcd8544_print_at_fb("+", FONT_SIZE_3,4,1);
            else
                pcd8544_print_at_fb("-", FONT_SIZE_3,4,1);
            pcd8544_print_uint8_at_fb(myabs(t1),FONT_SIZE_3,7,1);
        }
        pcd8544_print_uint8_at_fb(myabs(t2),FONT_SIZE_2,10,2);
        // draw dot
        pcd8544_set_point(68,27);
        pcd8544_set_point(60,27);
        pcd8544_set_point(70,27);
        pcd8544_set_point(68,28);
        pcd8544_set_point(69,28);
        pcd8544_set_point(70,28);
        pcd8544_set_point(68,29);
        pcd8544_set_point(69,29);
        pcd8544_set_point(70,29);
        // draw blob
        char * ptr= (char *)(&blob);
        pcd8544_draw_icon_fb(ptr,0,0,1);
        // draw degree
        ptr= (char *)(&degree);
        pcd8544_draw_icon_fb(ptr,67,0,1);
        ptr= (char *)(&degree_big);
        pcd8544_draw_icon_fb(ptr,67,1,1);
        // draw termometer
        ptr= (char *)(&termometer);
        pcd8544_draw_icon_fb(ptr,48 ,0,1);
        // status bar
        pcd8544_print_at_fb("35%", FONT_SIZE_1,1,0);
        pcd8544_print_at_fb("26", FONT_SIZE_1,8,0);

        pcd8544_display_fb();

        if (temp >= 350)
            i=-1;
        else if (temp <= -350)
            i=1;
        temp+=i;

        _delay_ms(1000);
     }

   return 0;
}

Результат:

Полные исходники к этому примеру можно посмотреть здесь:
https://gitlab.com/flank1er/pcd8544_atmega8/tree/master/11_meteo

12) Интерфейс для FM-радиоприемника

Еще одна распространенная задача - дисплей для FM-приемника. Я не стал изобретать велосипед и попытался скопировать интерфейс со своего старенького плеера iRiver ifp-180tc. Там тоже монохромный дисплей, правда не квадратный, а прямоугольный.

В итоге у меня получилась такая программа:

#include "avr/io.h"
#include "util/delay.h"
#include <avr/pgmspace.h>
#include "pcd8544_soft.h"

const uint8_t pointer[7] PROGMEM={0x2,0x6,0xc,0x18,0xc,0x6,0x2};
const uint8_t stereo[7] PROGMEM={0x3c,0x66,0xc3,0x81,0xc3,0x66,0x3c};
static const uint8_t battery[14] PROGMEM={28,34,65,93,93,65,93, 93,65,93,93,65,127,0};

int main(void) {
    // LCD setup
    pcd8544_init();
    /// set variables
    uint16_t freq=1017;
    char i=1;
    uint8_t f1,f2;
    uint8_t j;
    // main loop
    for(;;)
    {
         pcd8544_clear_fb();
        /// print frequency
        f1=freq/10;f2=freq%10;
        if (f1>=100)
            pcd8544_print_uint8_at_fb(f1,FONT_SIZE_3,0,1);
        else
            pcd8544_print_uint8_at_fb(f1,FONT_SIZE_3,3,1);

        pcd8544_print_uint8_at_fb(f2,FONT_SIZE_2,9,2);

        // draw dot
        pcd8544_set_point(61,27);
        pcd8544_set_point(62,27);
        pcd8544_set_point(63,27);
        pcd8544_set_point(61,28);
        pcd8544_set_point(62,28);
        pcd8544_set_point(63,28);
        pcd8544_set_point(61,29);
        pcd8544_set_point(62,29);
        pcd8544_set_point(63,29);
        // status bar
        pcd8544_print_at_fb("MГц", FONT_SIZE_1,9,1);
        pcd8544_print_at_fb("ПМ", FONT_SIZE_1,3,0);
        pcd8544_print_at_fb("60dB", FONT_SIZE_1,6,0);
        pcd8544_print_at_fb("7-НАШЕ РАДИО", FONT_SIZE_1,0,4);
        //
        pcd8544_draw_line(0,46,83,46);
        for(j=0;j<14;j++)
            pcd8544_draw_line(j*6,44,j*6,47);
        pcd8544_draw_line(83,44,83,47);
        char * ptr= (char *)(&pointer);
        uint8_t p=(freq-880)/2-(freq-880)/10;
        pcd8544_draw_icon_fb(ptr,p,5,1);

        ptr= (char *)(&stereo);
        pcd8544_draw_icon_fb(ptr,0,0,1);
        pcd8544_draw_icon_fb(ptr,7,0,1);

        ptr= (char *)(&battery);
        pcd8544_draw_icon_fb(ptr,70,0,2);

        pcd8544_display_fb();

        if (freq >= 1080)
            i=-1;
        else if (freq <=880)
            i=1;
       freq+=i;

         _delay_ms(1000);
     }

   return 0;
}

Результат:

исходники:
https://gitlab.com/flank1er/pcd8544_atmega8/tree/master/12_radio

Когда происходит нажатие на кнопки регулировки громкости, то шкала FM диапазона на пару секунд меняется на шкалу регулировки громкости.

#include "avr/io.h"
#include "util/delay.h"
#include <avr/pgmspace.h>
#include "pcd8544_soft.h"

const uint8_t pointer[7] PROGMEM={0x2,0x6,0xc,0x18,0xc,0x6,0x2};
const uint8_t stereo[7] PROGMEM={0x3c,0x66,0xc3,0x81,0xc3,0x66,0x3c};
static const uint8_t battery[14] PROGMEM={28,34,65,93,93,65,93, 93,65,93,93,65,127,0};

int main(void) {
    // LCD setup
    pcd8544_init();
    /// set variables
    char volume=100;
    uint16_t freq=1017;
    char i=1;
    uint8_t f1,f2;
    // main loop
    for(;;)
    {
         pcd8544_clear_fb();
        /// print frequency
        f1=freq/10;f2=freq%10;
        if (f1>=100)
            pcd8544_print_uint8_at_fb(f1,FONT_SIZE_3,0,1);
        else
            pcd8544_print_uint8_at_fb(f1,FONT_SIZE_3,3,1);

        pcd8544_print_uint8_at_fb(f2,FONT_SIZE_2,9,2);

        // draw dot
        pcd8544_set_point(61,27);
        pcd8544_set_point(62,27);
        pcd8544_set_point(63,27);
        pcd8544_set_point(61,28);
        pcd8544_set_point(62,28);
        pcd8544_set_point(63,28);
        pcd8544_set_point(61,29);
        pcd8544_set_point(62,29);
        pcd8544_set_point(63,29);
        // status bar
        pcd8544_print_at_fb("MГц", FONT_SIZE_1,9,1);
        pcd8544_print_at_fb("ПМ", FONT_SIZE_1,3,0);
        pcd8544_print_at_fb("60dB", FONT_SIZE_1,6,0);
        pcd8544_print_at_fb("7-НАШЕ РАДИО", FONT_SIZE_1,0,4);
        //
        pcd8544_print_at_fb("гр", FONT_SIZE_1,0,5);
        pcd8544_print_uint8_at_fb(volume, FONT_SIZE_1,3,5);
        pcd8544_draw_line(33,45,83,45);
        //
        char * ptr= (char *)(&pointer);
        uint8_t p=33+(volume>>1);
        pcd8544_draw_icon_fb(ptr,p,5,1);

        ptr= (char *)(&stereo);
        pcd8544_draw_icon_fb(ptr,0,0,1);
        pcd8544_draw_icon_fb(ptr,7,0,1);

        ptr= (char *)(&battery);
        pcd8544_draw_icon_fb(ptr,70,0,2);

        pcd8544_display_fb();

        if (volume >= 100)
            i=-1;
        else if (volume <= 0)
            i=1;

        volume+=i;

         _delay_ms(1000);
     }

   return 0;
}

Результат:

Здесь можно видеть, что цифры налезают на статусную строку, хотя внизу есть пустое место. Вцелом, я бы для больших цифр сделал персональные шрифты, примерно как на HD44780.

исходники:
https://gitlab.com/flank1er/pcd8544_atmega8/tree/master/12_radio_volume

13) Финальная версия библиотеки на аппаратном SPI

Ну и в завершение, можно перевести библиотеку на аппаратный SPI. Напомню, что для корректной работы устройства при заливке прошивки через ISP-программатор, дисплей нужно отключать от микроконтроллера.

В библиотечном файле нужно будет задать переменную, в которой будет храниться номер CS пина:

static uint8_t CSpin;

Соответственно функция инициализации дополниться одним входным параметром:

void pcd8544_init(uint8_t cs)
{
   CSpin=cs;
   // GPIO Setup
   DDR_PCD8544 |= (1<<PIN_RESET) | (1<<PIN_DC);
   PORT_PCD8544&=~(1<<PIN_RESET);
   PORT_PCD8544|=(1<<PIN_RESET);
   pcd8544_send(LCD_C, 0x21 );  // LCD Extended Commands.
   pcd8544_send(LCD_C, 0xBA );  // Set LCD Vop (Contrast).
   pcd8544_send(LCD_C, 0x04 );  // Set Temp coefficent. //0x04
   pcd8544_send(LCD_C, 0x14 );  // LCD bias mode 1:48. //0x13
   pcd8544_send(LCD_C, 0x20 );  // LCD Basic Commands
   pcd8544_send(LCD_C, 0x0C );  // LCD in normal mode.
}

Инициализацию SPI я предпочитаю делать в main() функции, или через библиотеку SPI.

Функция void pcd8544_send(uint8_t dc, uint8_t data) примет в таком случае следующий вид:

void pcd8544_send(uint8_t dc, uint8_t data)
{
    if (dc == LCD_D)
        PORT_PCD8544 |= (1<<PIN_DC);
    else
        PORT_PCD8544 &= ~(1<<PIN_DC);

    PORT_PCD8544&=~(1<<CSpin);

    SPDR=data;
    while(!(SPSR & (1<<SPIF)));

    PORT_PCD8544|=(1<<CSpin);
}

И в принципе это все. Все исходники в примерам можно скачать с этого сайта www.count-zero.ru/files/pcd8544_atmega8.zip или c gitlab.com https://gitlab.com/flank1er/pcd8544_atmega8.

поделиться: