Архитектура AVR в примерах/Управление ЖК-дисплеем на основе PCD8544
В этой работе мы рассмотрим управление жидкокристаллическим дисплеем на основе ИС PCD8544[1] на примере дисплея, примененного в Nokia 5110, используя встроенный в МК ATmega8 порт последовательного периферийного интерфейса (англ. SPI.)[2]
Перед началом
[править]- NB
-
- ИС PCD8544 ориентирована на использование напряжения (2.7 ÷ 3.3) V. Без принятия особых мер, использование дисплея в схеме с напряжением питания вне этого диапазона (в т. ч. принятом в Arduino 5 V) может привести к повреждению дисплея.
- Ввиду сложности монтажа самого дисплея, может иметь смысл использовать готовый модуль для монтажа на макетной панели.
Для данной работы нам потребуется присоединить к простейшему устройству дисплей Nokia 5110 следующим образом.
- Питание — «общий» к выводу Gnd и + Uи. п. к выводу Vcc дисплея.
- Линии данных:
- последовательный периферийный интерфейс: выводы PB2, MOSI, SCk (16, 17, 19; D 10, D 11, D 13) МК ATmega8 — к выводам CE, SDIn, SClk дисплея;
- управление: выводы PC4, PC5 (27, 28; AI 4, AI 5) МК — к выводам Data (Command), Res дисплея;
- отметим, что эти входы дисплея могут быть соединены с МК как непосредственно (при обязательном нахождении напряжений в допустимом для дисплея диапазоне), так и через ограничивающие резисторы номиналом порядка 10 kΩ.
Растровое изображение
[править]
/*** bitm8544.c — Send a bitmap to a PCD8544-based LCD via SPI -*- C -*- */
#include <assert.h> /* for _Static_assert () */
#include <avr/interrupt.h> /* for sei (), ISR () */
#include <avr/io.h> /* for DDRB, PORTB, etc. */
#include <avr/pgmspace.h> /* for PROGMEM, etc. */
#include <avr/sleep.h> /* for sleep_enable (), etc. */
#include <avr/wdt.h> /* for wdt_enable (), etc. */
#include <stdbool.h> /* for bool */
#include <stdint.h> /* for int16_t, etc. */
/* FIXME: huh? */
typedef int16_t ptrdiff_t;
#define LCD_WIDTH (84)
#define LCD_HEIGHT (48)
#define PCD8544_CMD_FUNC(pd_p, vrt_p, ext_p) \
(0x20 | ((pd_p) ? 4 : 0) | ((vrt_p) ? 2 : 0) | ((ext_p) ? 1 : 0))
/* extended mode commands */
#define PCD8544_EXT_BIAS(bias) \
(0x10 | (007 & (bias)))
#define PCD8544_EXT_V_OP(v_op) \
(0x80 | (077 & (v_op)))
/* normal mode commands */
#define PCD8544_CMD_DISP(mem_p, inv_p) \
(0x08 | ((mem_p) ? 4 : 0) | ((inv_p) ? 1 : 0))
#define PCD8544_CMD_SET_X(x) \
(0x80 | (127 & (x)))
#define PCD8544_CMD_SET_Y(y) \
(0x40 | (007 & (y)))
/* Source: [[commons:File:Wikimedia Community Logo optimized.svg]]
* Command:
* $ convert -ordered-dither c5x5b,2 -resize 82x46 \
* -bordercolor white -border 1x1 \
* -transpose \
* commons/b/b4/Wikimedia_Community_Logo_optimized.svg \
* xbm:community_logo_xp.xbm
*/
/* NB: ensure the bitmap ends up in the flash */
#define static static const PROGMEM
#include "community_logo_xp.xbm"
#undef static
static volatile bool seen_spi_interrupt_p = 0;
ISR (SPI_STC_vect) {
seen_spi_interrupt_p = 1;
}
static void
spi_send (char c)
{
/* NB: assumes interrupts are enabled */
SPDR = c;
while (! seen_spi_interrupt_p) {
/* sleep until the next event */
sleep_cpu ();
}
seen_spi_interrupt_p = 0;
/* . */
}
static void
spi_send_P (const char *s, size_t len)
{
for (ptrdiff_t i = 0; i < len; i++) {
spi_send (pgm_read_byte (s + i));
}
/* . */
}
int
main ()
{
/* expecting to finish in less than 100 ms */
wdt_enable (WDTO_2S);
/* Set up Port B:
– PB2 (D 10) is /CE (/SS)
– PB3 (D 11) is MOSI
– PB5 (D 13) is SCk */
/* raise /CE */
PORTB |= (1 << PB2);
DDRB |= ((1 << DDB5)
| (1 << DDB3)
| (1 << DDB2));
/* Set up Port C:
– PC4 (AI 4) is Data (/Command)
– PC5 (AI 5) is /Reset */
DDRC |= ((1 << DDC5)
| (1 << DDC4));
/* Set up the SPI master */
SPCR = ((1 << SPIE)
| (1 << SPE)
| (1 << MSTR)
| (1 << SPR1)
| (1 << SPR0));
/* NB: ignoring the datasheet recommendation! */
sleep_enable ();
/* enable interrupts */
sei ();
/* lower /CE */
PORTB &= ~ ((1 << PB2));
/* lower /Reset for 8 SPI ticks */
PORTC &= ~ ((1 << PC5));
/* wait for a complete transmission */
spi_send (42);
/* raise /Reset back */
PORTC |= (1 << PC5);
/* initialize PCD8544 */
_Static_assert (community_logo_xp_height <= LCD_WIDTH,
"The height of the bitmap exceeds LCD width");
static const PROGMEM char init_s[] = {
PCD8544_CMD_FUNC(0, 1, 1), /* ¬ power down, vert., extended */
PCD8544_EXT_BIAS(4),
PCD8544_EXT_V_OP(0x2f),
PCD8544_CMD_FUNC(0, 1, 0), /* ¬ power down, vert., normal */
PCD8544_CMD_DISP(1, 0), /* access memory, ¬ inverse */
PCD8544_CMD_SET_Y(0),
/* ensure the bitmap is X-centered centered by setting X */
PCD8544_CMD_SET_X(((LCD_WIDTH - community_logo_xp_height)
>> 1))
};
/* lower /Command (Data) */
PORTC &= ~ ((1 << PC4));
spi_send_P (init_s, sizeof (init_s));
/* send the bitmap */
_Static_assert (community_logo_xp_width == LCD_HEIGHT,
"The width of the bitmap has to match LCD height");
/* raise Data (/Command) */
PORTC |= (1 << PC4);
spi_send_P (community_logo_xp_bits,
sizeof (community_logo_xp_bits));
/* raise /CE back */
PORTB |= (1 << PB2);
/* halt */
wdt_disable ();
cli ();
sleep_enable ();
sleep_cpu ();
/* not reached */
/* . */
return 0;
}
/*** bitm8544.c ends here */
Сборка
[править]Внесем в созданный ранее
Makefile
следующие зависимости:default: bitm8544.hex bitm8544 bitm8544: bitm8544.c
Создадим файл
bitm8544.c
приведенного выше содержания, а также файл с целевым растромcommunity_logo_xp.xbm
, содержание которого можно найти в приложении к данной работе.Соберем рассматриваемый пример выполнив команду
make
:$ make bitm8544.hex avr-gcc -O2 -Wall -std=gnu11 -mmcu=atmega8 -DF_CPU=7372800 bitm8544.c -o bitm8544 avr-objcopy -O ihex bitm8544 bitm8544.hex $
- NB
При использовании микроконтроллера, отличного от ATmega8, или же кварцевого резонатора на частоту, отличную от 7.3728 MHz следует явно указать параметры сборки
MCU
илиF_CPU
, соответственно.Так, для платы Arduino Uno R3, поставляемой с МК ATmega328P и кварцевым резонатором на 20 MHz, команда сборки может быть следующей:
$ make MCU=atmega328p F_CPU=20000000 bitm8544.hex
Впрочем, на деле данный пример никак не использует значение тактовой частоты процессора (передаваемое через определение препроцессора
F_CPU
), и должен сохранить работоспособность вне зависимости от ее конкретного значения.
Удостоверимся в отсутствии ошибок сборки и в появлении файла
bitm8544.hex
, содержащего результирующий машинный код в формате Intel hex.
Загрузка и проверка работоспособности
[править]Загрузку кода в МК выполним аналогично ранее рассмотренному примеру.
Проверим работоспособность кода и устройства, для чего:
- подключим питание;
- сбросим МК;
- пронаблюдаем появление включенного в код выше изображения логотипа сообщества Викимедиа в центре дисплея.
Привет, мир!
[править]
/*** char8544.c — Send a bitmap to a PCD8544-based LCD via SPI -*- C -*- */
#include <assert.h> /* for _Static_assert () */
#include <avr/interrupt.h> /* for sei (), ISR () */
#include <avr/io.h> /* for DDRB, PORTB, etc. */
#include <avr/pgmspace.h> /* for PROGMEM, etc. */
#include <avr/sleep.h> /* for sleep_enable (), etc. */
#include <avr/wdt.h> /* for wdt_enable (), etc. */
#include <stdbool.h> /* for bool */
#include <stdint.h> /* for int16_t, etc. */
/* NB: ensure bitmap ends up in the flash */
#define static static const PROGMEM
#include "misc_fixed.xbm"
#undef static
#define font_bits \
(misc_fixed_bits)
#define font_coding_offset (-32)
#define font_glyph_spacing (1)
#define font_glyph_height (misc_fixed_width)
#define font_glyph_width (5)
#define font_glyphs \
(misc_fixed_height / font_glyph_width)
/* FIXME: huh? */
typedef int16_t ptrdiff_t;
#define LCD_WIDTH (84)
#define LCD_HEIGHT (48)
#define PCD8544_CMD_FUNC(pd_p, vrt_p, ext_p) \
(0x20 | ((pd_p) ? 4 : 0) | ((vrt_p) ? 2 : 0) | ((ext_p) ? 1 : 0))
/* extended mode commands */
#define PCD8544_EXT_BIAS(bias) \
(0x10 | (007 & (bias)))
#define PCD8544_EXT_V_OP(v_op) \
(0x80 | (077 & (v_op)))
/* normal mode commands */
#define PCD8544_CMD_DISP(mem_p, inv_p) \
(0x08 | ((mem_p) ? 4 : 0) | ((inv_p) ? 1 : 0))
#define PCD8544_CMD_SET_X(x) \
(0x80 | (127 & (x)))
#define PCD8544_CMD_SET_Y(y) \
(0x40 | (007 & (y)))
static volatile bool seen_spi_interrupt_p = 0;
ISR (SPI_STC_vect) {
seen_spi_interrupt_p = 1;
}
static void
spi_send (char c)
{
/* NB: assumes interrupts are enabled */
SPDR = c;
while (! seen_spi_interrupt_p) {
/* sleep until the next event */
sleep_cpu ();
}
seen_spi_interrupt_p = 0;
/* . */
}
static void
spi_send_P (const char *s, size_t len)
{
for (ptrdiff_t i = 0; i < len; i++) {
spi_send (pgm_read_byte (s + i));
}
/* . */
}
static void
pcd8544_draw_char (unsigned char c)
{
_Static_assert (font_glyph_height == 8,
"The height of the font has to be 8");
ptrdiff_t offset
= (font_glyph_width * ((ptrdiff_t)c + font_coding_offset));
spi_send_P (font_bits + offset, font_glyph_width);
_Static_assert (font_glyph_spacing < INT8_MAX,
"Glyph spacing has to be less than INT8_MAX");
for (int8_t i = 0; i < font_glyph_spacing; i++) {
spi_send (0);
}
/* . */
}
static void
pcd8544_draw_text_P (const char *s)
{
/* raise Data (/Command) */
PORTC |= (1 << PC4);
const char *p;
for (p = s; ; p++) {
char c = pgm_read_byte (p);
if (c == '\0') {
break;
}
pcd8544_draw_char (c);
}
/* . */
}
int
main ()
{
/* expecting to finish in less than 100 ms */
wdt_enable (WDTO_2S);
/* Set up Port B:
– PB2 (D 10) is /CE (/SS)
– PB3 (D 11) is MOSI
– PB5 (D 13) is SCk */
/* raise /CE */
PORTB |= (1 << PB2);
DDRB |= ((1 << DDB5)
| (1 << DDB3)
| (1 << DDB2));
/* Set up Port C:
– PC4 (AI 4) is Data (/Command)
– PC5 (AI 5) is /Reset */
DDRC |= ((1 << DDC5)
| (1 << DDC4));
/* Set up the SPI master */
SPCR = ((1 << SPIE)
| (1 << SPE)
| (1 << MSTR)
| (1 << SPR1)
| (1 << SPR0));
/* NB: ignoring the datasheet recommendation! */
sleep_enable ();
/* enable interrupts */
sei ();
/* lower /CE */
PORTB &= ~ ((1 << PB2));
/* lower /Reset for 8 SPI ticks */
PORTC &= ~ ((1 << PC5));
/* wait for a complete transmission */
spi_send (42);
/* raise /Reset back */
PORTC |= (1 << PC5);
/* initialize PCD8544 */
static const PROGMEM char init_s[] = {
PCD8544_CMD_FUNC(0, 0, 1), /* ¬ power down, horiz, extended */
PCD8544_EXT_BIAS(4),
PCD8544_EXT_V_OP(0x2f),
PCD8544_CMD_FUNC(0, 0, 0), /* ¬ power down, horiz, normal */
PCD8544_CMD_DISP(1, 0), /* access memory, ¬ inverse */
PCD8544_CMD_SET_Y(0),
PCD8544_CMD_SET_X(0)
};
/* lower /Command (Data) */
PORTC &= ~ ((1 << PC4));
spi_send_P (init_s, sizeof (init_s));
/* draw the text */
static const char PROGMEM text[]
= (" \317\360\350\342\345\362, \354\350\360! "
" "
"\253\300\360\365\350\362\345\352\362\363\360\340 "
" AVR "
" \342 \357\360\350\354\345\360\340\365\273"
"(\302\350\352\350\342\345\360\361\350\362\345\362)");
pcd8544_draw_text_P (text);
/* raise /CE back */
PORTB |= (1 << PB2);
/* halt */
wdt_disable ();
cli ();
sleep_enable ();
sleep_cpu ();
/* not reached */
/* . */
return 0;
}
/*** char8544.c ends here */
Сборка
[править]Внесем в созданный ранее
Makefile
следующие зависимости:default: char8544.hex char8544 char8544: char8544.c
Создадим файл
char8544.c
приведенного выше содержания, а также файл шрифтаmisc_fixed.xbm
, содержание которого можно найти в приложении к данной работе.Соберем рассматриваемый пример выполнив команду
make
:$ make char8544.hex avr-gcc -O2 -Wall -std=gnu11 -mmcu=atmega8 -DF_CPU=7372800 char8544.c -o char8544 avr-objcopy -O ihex char8544 char8544.hex $
Удостоверимся в отсутствии ошибок сборки и в появлении файла
char8544.hex
, содержащего результирующий машинный код в формате Intel hex.
Загрузка и проверка работоспособности
[править]Загрузку кода в МК выполним аналогично ранее рассмотренному примеру.
Проверим работоспособность кода и устройства, для чего:
- подключим питание;
- сбросим МК;
- пронаблюдаем появление включенного в код выше текста «Привет, мир!» на дисплее.
Исследование
[править]Попробуйте изменить код для вывода на дисплей иного изображения. Указание: воспользуйтесь программой
convert
из пакета ImageMagick, подобно:$ convert -ordered-dither c5x5b,2 -resize 82x46 \ -bordercolor white -border 1x1 \ -transpose \ Wikimedia_Community_Logo_optimized.svg \ community_logo_xp.xbm
Указанное в примере вывода изображения условие возможности
spi_send_P
для «битового массива» в целом несколько более строго, нежели действительно необходимо. Исправьте это условие (в форме_Static_assert
перед последним вызовомspi_send_P
) так, чтобы ошибка на этапе сборки диагностировалась для изображений с шириной вне диапазона (41 ÷ 48) пикселей, и удостоверьтесь в правильности работы такого варианта кода используя изображения:- соответствующее новому (но не старому) условию;
- не соответствующее ни одному из этих условий.
Предложенный код вывода изображения предполагает, что ширина транспонированного изображения в пикселях (или, что то же, — высота исходного) равна высоте самого дисплея. Несложно дополнить код возможностью вывода изображений меньшего размера одним из следующих двух способов:
- дополнением нулями до нужной высоты — подобно тому, как в функции
pcd8544_draw_char
кода знакогенератора ширина каждого глифа увеличивается на font_glyph_spacing пикселей; - пропуском позиций видеопамяти, не покрываемых изображением, — через периодическую переустановку координат командами дисплея
PCD8544_CMD_SET_X
,PCD8544_CMD_SET_Y
.
Реализуйте оба подхода и включите в код подходящее изображение. Сравните объемы результирующего кода. Убедитесь в том, что результат работы программы в обоих случаях совершенно одинаков (если дисплей, конечно, действительно «пуст» перед началом работы кода.)
- дополнением нулями до нужной высоты — подобно тому, как в функции
В предложенном варианте знакогенератора, функция
pcd8544_draw_text_P
не обрабатывает никаких управляющих кодов, включая код разрыва строки'\n'
(ASCII LF, 10.) Поэтому, чтобы обеспечить «читаемый» результат, каждая строка включенного в код примера текста дополнена до ширины дисплея (14 символов.)Обработку кода разрыва строки можно опять-таки выполнить одним из двух способов:
- дополняя выводимую строку нулями до полной ширины;
- переустанавливая текущие координаты соответствующими командами дисплея.
Ясно, что в обоих случаях потребуется следить за текущей позицией по-горизонтали (которая, в свою очередь, может считаться как в символах, так и в пикселях.) Может показаться, кроме того, что во втором случае требуется учитывать также позицию по-вертикали, однако в данном знакогенераторе присутствует особенность, делающая это необязательным.
Попробуйте реализовать оба подхода. Измените выводимый текст для использования разрывов строк (
\n
) вместо дополнения пробелами и сравните объемы результирующего кода. Удостоверьтесь, что результат работы программы не изменился по отношению к данному здесь варианту. Добавьте к тексту еще несколько (неполных) строк, чтобы различия между подходами проявились в результирующем изображении.Функция
pcd8544_draw_text_P
кода знакогенератора выводит на дисплей текст, хранящийся во flash-памяти МК, что подходит прежде всего для не меняющихся в процессе работы программы сообщений. Реализуйте функциюpcd8544_draw_text
, выводящую текст, хранящийся в ОЗУ. Удостоверьтесь в правильности работы функции, объявив в коде буфер подходящего размера и заполнив его функциейsprintf
, подобно:char buf[12]; sprintf_P (buf, sizeof (buf), PSTR (" \316\362\342\345\362: %d "), 42); pcd8544_draw_text (buf);
Примечания
[править]- ↑ PCD8544: 48 × 84 pixels matrix LCD. Проверено 19 июня 2013.
- ↑ Serial Peripheral Interface – SPI. ATmega8, ATmega8L: 8-bit Atmel with 8 KBytes In-System Programmable Flash. Проверено 29 апреля 2013.