В предыдущих частях статьи нами более или менее подробно были рассмотрены основные принципы работы четырёх модулей:
1. OLED дисплей на 0.96 дюйма (128×64 пикселей), контроллер SSD1306.
2. TFT дисплей на 1.8 дюйма (128×160 пикселей), контроллер ST7735.
3. TFT дисплей на 2.8 дюйма (240×320 пикселей), контроллер ILI9341.
4. TFT дисплей на 3.5 дюйма (320×480 пикселей), контроллер ILI9481.
Мы выяснили, как их запустить и активизировать один пиксель или, говоря иначе, вывести на дисплей точку.
Осталось сделать завершающий шаг – научится прорисовывать на экране базовые графические элементы (геометрические фигуры, иконки, текстовые символы), чем мы сегодня и займёмся.
Содержание статьи / Table Of Contents
↑ Договоримся о командах
Для начала небольшое отступление. Проведя ревизию четырёх предыдущих частей статьи, я пришёл к выводу, что один важный, на мой взгляд, вопрос был освещён не совсем чётко. Это минимальный набор команд, необходимый и достаточный для полноценной работы модуля.Для каких-то контроллеров я прошёлся по этой теме мимоходом, где-то не упомянул совсем, а то и вовсе рассмотрел в коде необязательные к применению команды.
Далее я вношу ясность в этот вопрос и привожу для каждого из четырёх модулей макроопределения обязательных и, в то же время, достаточных команд. Попутно замечу, что в примерах, выложенных в архив текущей части статьи, использованы только эти команды.
SSD1306:
/* Команды включения модуля */
#define SSD1306_CHARGEPUMP 0x8D
#define CHARGEPUMP_ENABLE 0x14
#define SSD1306_DISPLAYON 0xAF
/* Команды настройки модуля */
#define SSD1306_MEMORYMODE 0x20
#define HORIZONTAL_ADDRESSING 0x00
#define SSD1306_SEGREMAP 0xA0
#define SEGREMAP_OFF 0x00
#define SEGREMAP_ON 0x01
#define SSD1306_COMSCANINC 0xC0
#define SSD1306_COMSCANDEC 0xC8
/* Команды рисования точки */
#define SSD1306_COLUMNADDR 0x21
#define SSD1306_PAGEADDR 0x22
ST7735:
/* Команды включения и сброса модуля */
#define ST7735_RESET_SW 0x01
#define ST7735_SLPOUT 0x11
#define ST7735_DISPON 0x29
/* Команды настройки модуля */
#define ST7735_MADCTL 0x36
#define MADCTL_MY 0x80
#define MADCTL_MX 0x40
#define MADCTL_MV 0x20
#define MADCTL_ML 0x10
#define MADCTL_RGB 0x00
#define MADCTL_BGR 0x08
#define MADCTL_MH 0x04
#define ST7735_COLMOD 0x3A
#define COLMOD_16_BIT 0x05
/* Команды рисования точки */
#define ST7735_CASET 0x2A
#define ST7735_RASET 0x2B
#define ST7735_RAMWR 0x2C
ILI9341:
/* Команды включения и сброса модуля */
#define ILI9341_SOFTRESET 0x01
#define ILI9341_SLEEPOUT 0x11
#define ILI9341_DISPLAYON 0x29
/* Команды настройки модуля */
#define ILI9341_MADCTL 0x36
#define MADCTL_MY 0x80
#define MADCTL_MX 0x40
#define MADCTL_MV 0x20
#define MADCTL_ML 0x10
#define MADCTL_BGR 0x08
#define MADCTL_RGB 0x00
#define MADCTL_MH 0x04
#define ILI9341_COLMOD 0x3A
#define COLMOD_PARAMETR 0x55
/* Команды рисования точки */
#define ILI9341_COLADDRSET 0x2A
#define ILI9341_PAGEADDRSET 0x2B
#define ILI9341_MEMORYWRITE 0x2C
ILI9481:
#define ILI9481_MADCTL 0x36
#define MADCTL_MY 0x80
#define MADCTL_MX 0x40
#define MADCTL_MV 0x20
#define MADCTL_ML 0x10
#define MADCTL_BGR 0x08
#define MADCTL_RGB 0x00
#define MADCTL_MHF 0x02
#define MADCTL_MVF 0x01
#define ILI9481_COLMOD 0x3A
#define COLMOD_PARAMETER 0x55
/* Команды рисования точки */
#define ILI9481_COLADDRSET 0x2A
#define ILI9481_PAGEADDRSET 0x2B
#define ILI9481_MEMORYWRITE 0x2C
Вот теперь можно с чистой совестью поговорить о графике. :yahoo:
Сразу создадим два файла – graphics.с и graphics.h – и будем вносить вновь написанные нами функции в первый, а макроопределения, прототипы функций и объявления глобальных переменных во второй.
В предыдущих частях статьи мы создали одинаковую (по названию и входным параметрам) для всех рассматриваемых модулей функцию draw_pixel(), отображающую на дисплее точку. Каждый последующий графический объект будет рисоваться нами попиксельно при помощи указанной функции. Чтобы она понимала, куда именно и какого цвета точку выводить, объявим глобальные переменные для координат Х, Y и цвета пикселя, и пропишем функции, определяющие их текущие значения.
unsigned int currentX, currentY, currentColor;
void setCursor(uint16_t x, uint16_t y)
{
currentX = x;
currentY = y;
}
void setColor(uint16_t color)
{
currentColor = color;
}
Кроме того, договоримся о паре общих условий:
1. Выводить любой объект на дисплей мы будем сверху-вниз и/или слева-направо.
2. Чтобы примеры из текста статьи работали на каждом из рассмотренных нами модулей, включая монохромный SSD1306, используем в них белый цвет. Естественно, обладатели полноцветных дисплеев могут заменить оговорённый на любой другой, доступный цвет.
↑ Рисуем на дисплее геометрические фигуры
Рассмотрим лишь четыре фигуры – вертикальную и горизонтальную линии, рамку и прямоугольник. Всё, что имеет более сложные формы, гораздо проще не рисовать мучаясь попиксельно, а скопировать на дисплей из заранее подготовленного массива данных, о чём – чуть позже.Вертикальная линия заданной высоты рисуется по следующему алгоритму:
1. Рисуем точку в месте и цветом, в соответствии с текущими значениями переменных currentX, currentY и currentColor.
2. Сдвигаем курсор на один пиксель вниз, т.е. инкрементируем значение currentY.
3. Вычитаем из значения высоты единицу, используя его как счётчик количества повторений.
4. Повторяем пункты 1 – 3, пока значение высоты не обратится в ноль.
void drawVerticalLine(uint16_t_hight)
{
while(_hight--)
{
draw_pixel(currentX, currentY, currentColor);
currentY++;
}
}
Тогда, чтобы на дисплее появилась вертикальная черта белого цвета высотой 12 пикселей с начальными координатами X = 3, Y = 7, нужно написать следующее:
setCursor(3, 7);
setColor(WHITE);
drawVerticalLine(12);
С горизонтальной линией всё – то же самое, только оперируем значениями длины и currentX.
void drawHorizontalLine(uint16_t _length)
{
while(_length--)
{
draw_pixel(currentX, currentY, currentColor);
currentX++;
}
}
Для изображения рамки, как и прямоугольника, потребуется два параметра – её ширина и высота. Запоминаем координаты начальной точки во вспомогательные переменные startX и startX, а затем, используя две предыдущие функции рисуем:
1. Из начальной точки – вертикальную линию вниз высотою, равной высоте рамки.
2. От конца вертикальной, отменив последний инкремент координаты Y, – горизонтальную линию вправо длиною, равной ширине рамки.
3. Вернувшись в начальную точку – горизонтальную линию вправо.
4. От конца предыдущей горизонтальной, отменив последний инкремент координаты X, –вертикальную линию вниз.
void drawRectangle(uint16_t _width, uint16_t _hight)
{
uint16_t startX = currentX, startY = currentY;
drawVerticalLine(_hight);
currentY--;
drawHorizontalLine(_width);
currentX = startX; currentY = startY;
drawHorizontalLine(_width);
currentX--;
drawVerticalLine(_hight);
}
Теперь прямоугольник. Запоминаем начальное значение координаты Y и:
1. Рисуем вертикальную линию высотою, равной высоте прямоугольника.
2. Инкрементируем значение currentX.
3. Возвращаемся в начальное значение координаты Y.
4. Декрементируем значение ширины прямоугольника.
5. Повторяем пункты 1 – 4, пока значение ширины не обратится в ноль.
void fillRectangle(uint16_t _width, uint16_t _hight)
{
uint16_t startY = currentY;
while(_width--)
{
drawVerticalLine(_hight);
currentX++;
currentY = startY;
}
}
↑ Делаем монохромные иконки
Переделаем немного функцию drawVerticalLine(), чтобы она рисовала линию не задаваемой, а фиксированной высоты – 8 пикселей. Тогда, значение высоты можно, не передавая как параметр, объявить внутри функции. Изменим заодно тип переменной _hight c uint16_t на uint8_t, поскольку для хранения числа 8 достаточно одного байта, и получим следующее:void drawVerticalLine(void)
{
uint8_t _hight = 8;
while(_hight--)
{
draw_pixel(currentX, currentY, currentColor);
currentY++;
}
}
Теперь передадим нашей функции на вход однобайтное число и сделаем так, чтобы она выводила на дисплей точку только, если старший бит принимаемого в качестве параметра числа не равен нулю:
#define MSbit 0x80
void drawVerticalLine(uint8_t _byte)
{
uint8_t _hight = 8;
while(_hight--)
{
if((_byte & MSbit) != 0)
{
draw_pixel(currentX, currentY, currentColor);
}
currentY++;
}
}
На этом этапе, если передать в функцию число, к примеру, 255 (11111111 в двоичном выражении) или 128 (10000000), то на экране вы получите полноценную вертикальную линию высотой 8 пикселей, поскольку старший бит обоих чисел равен единице, т.е. не равен нулю. По обратной причине, проверив число 127 (01111111) наша функция не нарисует ни одной точки. Заметьте, что текущая координата Y инкрементируется в любом случае, не зависимо от того, рисуется точка или нет.
Продолжим переделку и заставим функцию проверять на равенство нулю все биты входящего числа, двигая по очереди каждый последующий бит на место старшего. Для этого нужно просто сдвигать наше число влево на одну позицию.
Изменим названия на более подходящие и получим функцию, печатающую однобайтное число.
Применим функцию drawByte() на практике. Для этого возьмём знакомую всем иконку.
Поскольку функция выводит 8–битное число на дисплей сверху-вниз, начиная со старшего бита, то и рисунок мы разобьём на вертикальные 8–пиксельные отрезки шириною в один пиксель.
Теперь переведём полученные отрезки в числа, руководствуясь двумя правилами:
1. Верхний пиксель отрезка соответствует старшему биту числа.
2. Если пиксель чёрного цвета, бит равен 1, в противном случае – 0.
В итоге получим следующие 20 чисел, в двоичном и шестнадцатиричном исчислении:
11111111 – 0xff
11111111 – 0xff
11000011 – 0xс3
11111111 – 0xff
11111111 – 0xff
11000011 – 0xс3
11111111 – 0xff
11111111 – 0xff
11000011 – 0xс3
11111111 – 0xff
11111111 – 0xff
11000011 – 0xс3
11000011 – 0xс3
11000011 – 0xс3
11000011 – 0xс3
11000011 – 0xс3
11111111 – 0xff
11111111 – 0xff
00111100 – 0х3с
00111100 – 0х3с
Запишем числа в массив, назвав его charge, и выведем изображение на дисплей по следующему алгоритму:
1. Определяем цвет иконки и координаты её верхней левой точки.
2. Запоминаем стартовое значение координаты Y во вспомогательную переменную.
3. Рисуем очередной байт из массива.
4. Сдвигаемся на один пиксель вправо, инкрементируя значение currentX.
5. Возвращаемся в начальное значение координаты Y.
6. Повторяем пункты 3 – 5 двадцать раз.
uint8_t charge[20] =
{
0xff, 0xff, 0xс3, 0xff, 0xff, 0xс3, 0xff, 0xff, 0xс3, 0xff, 0xff, 0xс3,
0xс3, 0xс3, 0xс3, 0xс3, 0xff, 0xff, 0x3с, 0x3с
};
setCursor(3, 9);
setColor(WHITE);
uint16_t startY = currentY;
for(uint8_t imageByteNum = 0; imageByteNum < 20; imageByteNum ++)
{
drawByte(charge[imageByteNum]);
currentX++;
currentY = startY;
}
Остановимся на массиве charge. При такой записи микроконтроллер (МК) будет хранить его в ОЗУ. Если объявить, как и планируется в дальнейшем, массив глобальным, то 20 байт оперативной памяти будут заняты на протяжении всего времени исполнения программы. Представим, что надо вывести рисунок, полностью покрывающий дисплей размером 320х480 пикселей. А это – уже 320*480/8/1000 = 19.2 килобайт. Даже, если в вашем распоряжении окажется МК с таким запасом ОЗУ, держать там постоянно такую кучу данных просто не разумно. Поэтому, будем хранить рисунок во флэш-памяти.
В первую очередь следует подключить библиотеку . После этого добавим перед именем массива ключевое слово const, a после него - PROGMEM:
const uint8_t charge[20] PROGMEM =
{
0xff, 0xff, 0xс3, 0xff, 0xff, 0xс3, 0xff, 0xff, 0xс3, 0xff, 0xff, 0xс3,
0xс3, 0xс3, 0xс3, 0xс3, 0xff, 0xff, 0x3с, 0x3с
};
и наши данные отправятся в программную память.А извлекать их оттуда и выводить на дисплей можно следующим образом:
drawByte(pgm_read_byte(&charge[imageByteNum]));
Пойдём дальше. Я намеренно привёл в качестве примера иконку с высотой 8 пикселей, чтобы максимально упростить код и сделать понятной базовую логику отображения рисунка на экране. Однако, в реальности высота изображения часто не равна 8 и не всегда кратна этому числу. Поэтому, чтобы функция drawIcon(), к созданию которой мы движемся, могла рисовать иконки любого размера, увеличим в нашем примере высоту до 10 пикселей
и посмотрим, как это отразится на алгоритме программы.
Вновь разобьём рисунок на вертикальные полосы шириной в один пиксель и попробуем отсчитывать 8–пиксельные отрезки. Сразу обращу ваше внимание на тот факт, что в нашем случае общее количество пикселей – 200 – кратно 8. Но, очень часто бывает и наоборот. И тогда, как бы мы ни крутили рисунок, нам не удастся получить целое число отрезков длиною в 8 пикселей: всегда будет оставаться как минимум один отрезок меньшей длины. Поэтому, давайте сразу договоримся: если нам в будущем встретится такой «обрубок», то при переводе его в число будем дополнять недостающие биты нулями.
Приступим. Первый отрезок, как мы и договорились ранее, начнётся с верхнего левого угла:
А откуда и куда считать второй и последующий отрезки? Тут есть два варианта. Первый – продолжать двигаться вниз и по достижении нижнего края иконки перейти к верхнему пикселю следующей полосы.
Такой путь даёт минимум «неполноценных» отрезков и, как следствие, требует меньше памяти для хранения массива данных, но, зато, усложняет программный код, выводящий рисунок на дисплей. Пожертвуем некоторым количество флэш-памяти в пользу простого и понятного кода и выберем второй вариант, в связи с чем проделаем небольшую подготовительную работу:
1. Помимо вертикальных разобьём иконку ещё и на горизонтальные полосы высотой 8 пикселей.
2. Если общая высота рисунка не кратна 8, как в нашем случае, мысленно дополним её до нужного размера неокрашенными пикселями.
В итоге имеем следующее:
Теперь будем по очереди «сканировать» слева-направо горизонтальные полосы, переводя, как ранее, содержимое 8–пиксельных отрезков в числа.
Для первой полосы получим следующие 20 чисел в шестнадцатиричном исчислении:
0xff, 0xff, 0xc0, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xff, 0xff, 0x1e, 0x1e,
а для второй:
0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x00, 0x00
Занесём данные в массив
const uint8_t charge[40] PROGMEM =
{
0xff, 0xff, 0xc0, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xc0,
0xc0, 0xc0, 0xc0, 0xc0, 0xff, 0xff, 0x1e, 0x1e,
0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0,
0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x00, 0x00
};
и попробуем написать функцию, которая выведет эту и любую другую иконку на экран модуля и будет иметь следующий синтаксис:
drawIcon(имя массива, ширина рисунка в пикселях, высота рисунка пикселях)
Для начала прикинем, что изменится в алгоритме по сравнению с предыдущим случаем.
1. Поскольку мы будем рисовать 8-пиксельные линии уже в два, а в более общем случае – в несколько рядов, необходимо запомнить стартовые значения обеих координат.
uint16_t startX = currentX, startY = currentY;
2. По той же причине, что и в п.1 нам понадобится не один, а два цикла, вложенных друг в друга.
3. Если внутренний цикл из п.2 будет, по-прежнему, считать от 0 до значения ширины рисунка в пикселях, то внешний должен отсчитывать количество 8-пиксельных горизонтальных полос, на которые мы разбили иконку. Но, мы то собираемся подавать на вход функции реальную высоту изображения в пикселях (в данном случае – 10), поэтому необходимо, во-первых, перевести высоту в количество вмещаемых ею 8-пиксельных отрезков, и, во-вторых, программно учесть добавленные нами пиксели. Делается это так:
а) Значение высоты делим на 8.
б) Выделяем целую часть частного, явно приведя его к целочисленному значению.
в) Умножаем целую часть на 8 и сравниваем с первоначальным значением высоты.
г) Если два сравниваемых числа не равны, добавляем к целому от частного единицу.
uint16_t bytesPerHight = (uint16_t)(_hight / 8);
if ((bytesPerHight * 8) != _hight) bytesPerHight += 1;
4. В связи с тем, что циклов у нас два, а массив по-прежнему одномерный, понадобится вспомогательная переменная imageByteNum для учёта номера извлекаемого из массива байта.
Теперь составим сам алгоритм. По сути, нам нужно повторить в обратном порядке вышеприведённую последовательность «сканирования», т.е. взять первые 20 элементов массива и вывести их на дисплей в один ряд, а затем вернуться в начальную точку, но на 8 пикселей ниже, и вывести следующие 20 байт.
1. Запоминаем стартовое значение координат в переменные startX и startY.
2. Обнуляем счётчик imageByteNum.
3. Определяем количество 8-пиксельных отрезков в высоте – bytesPerHight.
4. Запускаем внешний цикл, который bytesPerHight–раз повторяет следующее:
а) запускает второй – внутренний – цикл,
б) возвращает курсор в стартовое положение, сместив на вниз на 8 пикселей.
5. Внутренний цикл столько раз, сколько пикселей в ширине рисунка, извлекает очередной байт из массива и в соответствии с его значением рисует на экране 8-пиксельный отрезок справа от предыдущего.
Так и запишем:
void drawIcon(const uint8_t *image, uint16_t width, uint16_t hight)
{
uint16_t startX = currentX, startY = currentY;
uint16_t imageByteNum = 0;
uint16_t bytesPerHight = (uint16_t)(hight / 8);
if ((bytesPerHight * 8) != hight) bytesPerHight += 1;
for(uint16_t imageHigt = 0; imageHigt < bytesPerHight; imageHigt++)
{
for(uint16_t imageWidth = 0; imageWidth < width; imageWidth++)
{
drawByte(pgm_read_byte(&image[imageByteNum]));
currentX++;
currentY = startY;
imageByteNum++;
}
startY += 8;
currentY = startY;
currentX = startX;
}
}
А потом нарисуем нашу иконку:
setCursor(3, 9);
setColor(WHITE);
drawIcon(charge, 20, 10);
И, вроде бы, всё здорово, но, есть один момент. В реальном проекте может использоваться несколько рисунков, причём разных по величине, и держать все их параметры в голове, чтобы в нужный момент вписать в функцию drawIcon() – удовольствие то ещё. Поэтому, в таких ситуациях я поступаю очень просто: записываю все имена и размеры на бумажке, а потом … теряю её и судорожно ищу.
Избавимся от подобных «радостей»: запишем параметры рисунка в глобальные переменные, объединив их для удобства в структуру.
Пропишем прототип tImage структуры:
typedef struct {
const uint8_t *data; // имя массива, в котором хранятся числовые значения нашего рисунка
uint16_t width; // ширина рисунка в пикселях
uint16_t hight; // высота рисунка в пикселях
} tImage;
Массив с числовыми данными переименуем в image_data_ charge:
const uint8_t image_data_charge[40] PROGMEM =
{
0xff, 0xff, 0xc0, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xc0, 0xff, 0xff,
0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xff, 0xff, 0x1e, 0x1e,
0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0,
0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x00, 0x00
};
Создадим (инициализируем) нашу конкретную структуру charge:
tImage charge = {image_data_charge, 20, 10};
Внесём небольшие изменения в код, чтобы изображение выводилось на дисплей следующей записью:
drawIcon(имя структуры) ;
и пусть функция сама разбирается, где находится иконка и какие у неё размеры.
void drawIcon(tImage structureName)
{
uint16_t startX = currentX, startY = currentY;
uint16_t imageByteNum = 0;
uint16_t bytesPerHight = (uint16_t)(structureName.hight / 8);
if ((bytesPerHight * 8) != structureName.hight) bytesPerHight += 1;
for(uint16_t imageHigt = 0; imageHigt < bytesPerHight; imageHigt++)
{
for(uint16_t imageWidth = 0; imageWidth < structureName.width; imageWidth++)
{
drawByte(pgm_read_byte(&structureName.data[imageByteNum]));
currentX++;
currentY = startY;
imageByteNum++;
}
startY += 8;
currentY = startY;
currentX = startX;
}
}
Ну, вот теперь функция приобрела законченный вид, а её применение стало простым и коротким:
setCursor(3, 9);
setColor(WHITE);
drawIcon(charge);
Осталось ответить на вопрос, в каком файле мы будем хранить всё это добро? Можно, конечно, в файлах graphics. Но, ведь количество и состав рисунков может меняться от проекта к проекту и каждый раз лезть в этот файлы, чтобы рано или поздно что-то там напортить – неоправданный риск. Чтобы избежать его, мы пропишем в файле graphics.h только прототип структуры, а затем создадим ещё один файл – charge.c – и занесём туда массив и структуру.
charge.c
const uint8_t image_data_charge[40] PROGMEM =
{
0xff, 0xff, 0xc0, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xc0, 0xff, 0xff,
0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xff, 0xff, 0x1e, 0x1e,
0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0,
0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x00, 0x00
};
tImage charge = {image_data_charge, 20, 10};
Не знаю, как вы, уважаемые читатели, но меня при взгляде на последний рисунок охватила бы смутная тревога. Ну хорошо, если иконка небольшая, я могу ещё без ущерба для собственного здоровья и всего, что находится в пределах досягаемости, «разбить её на горизонтальные полосы», потом «сканировать» их и «перевести в числа», при этом «мысленно добавляя недостающие пиксели». А как быть с рисунком в 400х320 пикселей? Это ж – более 19 тысяч 8–пиксельных отрезков и ещё бог знает сколько «обрубков»! Да меня где-нибудь на второй тысяче увезут в «Курушасай» – местный аналог «Кащенко».
К счастью, о нашем с вами здоровье побеспокоились и создали программы, с помощью которых всю эту работу можно проделать буквально одним нажатием клавиши мышки. Их несколько, но я остановил свой выбор на lcd-image-converter, поскольку она – простая, интуитивно понятная и, с другой стороны, её возможности обеспечивают, в общем то, все мои потребности в графике при реализации проектов с использованием МК.
Скачаем из архива файл charge.png с иконкой, которую мы рассмотрели выше и сохраним на своём компьютере. Открываем lcd-image-converter и жмём «Файл\Открыть»:
В открывшемся окне поиска находим наш файл и выбираем его, нажав кнопку «Открыть»:
Теперь нужно настроить параметры преобразования рисунка в массив данных, чтобы они соответствовали коду, написанному нами выше. Делать это придётся только один раз. Жмём «Настройки\Преобразование»
В поле «Предустановка» открывшегося окна можете внести своё имя шаблона настроек. Настройки во вкладке «Подготовка» нужно выставить так, как это показано на рисунке ниже:
Переходим во вкладку «Изображение», устанавливаем нижеприведённые параметры, после чего жмём кнопу «ОК»:
и отвечаем «Yes» на вопрос о сохранении изменений:
Осуществим, наконец, преобразование, нажав «Файл\Преобразовать»:
Осталось определить место хранения выходного файла, не меняя его названия, и нажать кнопку «Сохранить»:
Выходим из lcd-image-converter, открываем сформированный ею файл charge.c и смотрим, что получилось:
Почти похоже на то, что прописали выше мы, не правда ли? Чтобы избавится от этого досадного «почти» нужно, во-первых, внести кое-какие изменения в файл image.tmpl, который находится в той же папке, что и lcd-image-converter и, во-вторых, осуществить дополнительные настройки в самой программе. Скачайте из архива уже изменённый файл image.tmpl и замените им действующий.
Открываем lcd-image-converter и сразу переходим в «Настройки\Преобразование»
Там переходим во вкладку «Шаблоны», жмём верхнюю кнопку «Обзор», находим папку lcd-image-converter, а в ней только что заменённый файл image.tmpl. Выбрав его жмём «ОК» и подтверждаем изменения в настройках.
Теперь, после преобразования файла charge.png мы получим файл charge.c нужного содержания, который можно напрямую подключить к проекту:
Для полноты эксперимента преобразуем ещё пару более крупных иконок - android.png и youtube.png - из архива, чтобы в дальнейшем вывести их на дисплей.
↑ Текстовые символы и строки
Собственно говоря, шрифты в нашем случае это – те же иконки и работать с ними мы будем точно так же.Повторите вышеописанную процедуру только для файла font.tmpl из архива и нижней кнопки «Обзор».
Далее выбираем «Новый шрифт» либо «Файл\Новый шрифт»
Программа предложит ввести имя шрифта, которое заодно станет и именем выходного файла. Давайте, для примера, введём Arial и нажмём «ОК».
В появившемся окне «Настройка шрифта» выберем раздел «Исходный шрифт», а в нём установим следующие параметры шрифта – семейство Arial, стиль Normal, размер 14:
Добавим кириллицу. Переходим в раздел «Символы», набираем в окне «Фильтр» слово «cyrillic» и выбираем из появившегося списка доступных шрифтов «Cyrillic», после чего в поле справа появится искомый шрифт. Нажав и удерживая клавишу «Alt» (или «Ctrl»), выбираем с помощью левой клавиши мыши нужные нам символы, причём ряд символов можно выбрать, нажав на кнопку графы со старшими значениями юникода (в нашем случае это – «410», «420», «430» и «440»). Жмём «Добавить выбранные».
Теперь переходим в раздел «Параметры» и устанавливаем параметры согласно рисунка ниже:
Выбор моноширинного шрифта обусловлен тем, что в этом случае получается самый простой код. На данном этапе мы выбрали все символы ASCII и кириллицу. Однако, чтобы не загромождать рисунки при последующем повествовании, с помощью окна сверху-справа оставим, опять же к примеру, только три символа – цифру ‘1’, заглавную латинскую букву ‘F’ и прописную русскую ‘ж’ – удалив всё остальное. Жмём «ОК», после чего программа перейдёт в основное окно:
Если изображение символа в центральном поле окна недостаточно крупное, можно нажать кнопку «Лупа» и в поле слева установить подходящий масштаб. Выбрав символ в графе «Предпросмотр» правого поля, вы можете посмотреть, как в реальности он будет выглядеть на дисплее вашего модуля. В крайней слева графе этого же поля представлен юникод символа. В окошке снизу отображены размеры иконок символов. Жмём «Файл\Преобразовать» и сохраняем результат в выбранном месте, нажав соответствующую кнопку.
Откроем файл Arial.c и посмотрим, что получилось.
Как видите, в отличие от предыдущих случаев данные по всем трём символам собраны в одном массиве, но, для удобства визуально разбиты на отдельные блоки. Сам массив стал безразмерным. Всё это влечёт за собой небольшие изменения в коде по сравнению с функцией drawIcon():
1. Чтобы можно было использовать в одном проекте шрифты разных типов и размеров объявим в файле graphics.h глобальную структуру tImage с именем currentFont.
2. Пропишем функцию устанавливающую текущий используемый шрифт
void setFont(tImage FontName)
{
currentFont.data = FontName.data;
currentFont.width = FontName.width;
currentFont.hight = FontName.hight;
}
3. Чтобы вычленить из массива первый элемент нужного нам блока, стартовое значение переменной imageByteNum будет равно не нулю, а произведению трёх величин:• порядкового номера BlockNum выбранного блока данных в массиве, который будет передаваться в качестве параметра функции,
• количества байтов в высоте иконки символа,
• ширины иконки символа в пикселях
uint16_t imageByteNum = BlockNum*bytesPerHight*currentFont.width;
Тогда функция печати символа printChar() будет выглядеть следующим образом:
void printChar(uint8_t BlockNum)
{
uint16_t startX = currentX, startY = currentY;
uint16_t bytesPerHight = (uint16_t)(currentFont.hight / 8);
if ((bytesPerHight * 8) != currentFont.hight) bytesPerHight += 1;
uint16_t imageByteNum = BlockNum*bytesPerHight*currentFont.width;
for(uint16_t imageHigt = 0; imageHigt < bytesPerHight; imageHigt++)
{
for(uint16_t imageWidth = 0; imageWidth < currentFont.width; imageWidth++)
{
drawByte(pgm_read_byte(¤tFont.data[imageByteNum]));
currentX++;
currentY = startY;
imageByteNum++;
}
startY += 8;
currentY = startY;
currentX = startX;
}
}
И код
#include "Arial.c"
setColor(WHITE);
setFont(Arial);
setCursor(300, 15);
printChar(0);
setCursor(300, 35);
printChar(1);
setCursor(300, 55);
printChar(2);
выведет в указанные места дисплея символы из массива. Использование функции printChar() в том виде, как есть, иногда даже оправданно, особенно, если символов в вашем проекте немного, а с памятью МК – напряжёнка. Однако, в целом, она имеет по крайней мере один недостаток – необходимость помнить порядковый номер блока данных в массиве для каждого символа. А если массивов несколько, а если количество и порядок размещения символов в массивах разное? Короче, перед нами опять маячит перспектива записи на бумажке с последующей её утерей. Поэтому, для случаев, когда надо выводить на дисплей целые строки, создадим на базе printChar() функцию соответствующего вида и содержания:
printString("строка");
Но, прежде, всё с той же целью – минимизации кода – договоримся вот о чём:
1. Какой-бы шрифт мы не преобразовывали количество символов будет одинаковым – 194, из них 128 – символы ASCII, 66 – буквы кириллицы в обоих регистрах.
2. Порядок расположения блоков данных в массиве тоже будет единым – сначала ASCII-символы, после них заглавные, а затем прописные буквы русского алфавита, кроме Ё и ё, которые будут в конце списка.
Пройдём полный цикл преобразования шрифтов, чтобы запомнить для использования без изменений в дальнейшем.
1. Запускаем lcd-image-converter и жмём «Новый шрифт».
2. Выберем для разнообразия шрифт «ArialNarrow» размером 14. Вводим имя и жмём «ОК»
3. В разделе «Исходный шрифт» выставляем выбранные нами параметры и, кроме того, в окне справа вверху выделяем и удаляем все символы, после чего там останется затемнённая надпись «Выбранные символы». Переходим в раздел «Символы».
4. В разделе «Символы» начинаем формировать наш список символов. Сначала, как и договорились, вносим ASCII-символы, для чего в поле «Фильтр» выбираем «Basic Latin», затем в поле справа с помощью кнопок юникода (от 0 до 70) выделяем все 8 рядов символов и жмём кнопку «Добавить выбранные».
5. Очередь за кириллицей. Находим через фильтр «Cyrillic», соблюдая оговорённый порядок выделяем ряды с 410 по 440 и жмём «Добавить выбранные».
6. И только после этого отдельно добавляем обе буквы «Ё».
В результате в правом верхнем окне отразится в нужном порядке весь список выбранных нами символов, а надпись слева внизу подтвердит, что их количество верное – 194.
7. Осталось перейти в раздел «Параметры», установить моноширинный размер и нажать «ОК».
8. После перехода программы в основное окно жмём «Файл\Преобразовать» и сохраняем выходной файл.
Начнём писать нашу функцию c printChar() в основе, чтобы вывести на экран надпись «Hello Мир»:
void printString(char *myString)
{
printChar(*myString);
}
printString("Hello Мир");
Попытаемся понять, что в реальности происходит, когда исполняется функция такого вида. Если откомпилировать следующий код:
void printString(char *myString);
char hello[] = "Hello Мир";
int main(void)
{
printString(hello);
while(1)
{
}
}
void printString(char *myString)
{
}
и открыть карту памяти, то в «AVR Studio», например, можно увидеть следующую картину:И когда внутри функции printString() мы пишем
printChar(*myString);
то фактически говорим МК: «Вот тебе адрес ячейки памяти (в нашем случае – 0х100), в которой хранится числовое значение (0x48) первой буквы строки «Н». Иди к этой ячейке, возьми её содержимое и передай в функцию printChar()». А что сделает последняя? Правильно, она примет число, вычислит с его помощью порядковый номер блока данных символа, пойдёт в массив из подключённого к проекту файла (теперь уже – «ArialNarrow14.c») и выведет нужное количество байтов в виде вертикальных отрезков в заданном месте дисплея.
Далее нужно дать МК адрес следующего символа строки:
myString++;
и повторять всё, пока не наступит конец
void printString(char *myString)
{
while(*myString != '\0')
{
printChar(*myString);
myString++;
}
}
Теперь разберёмся с самими числовыми значениями. Откроем таблицу ASCII-символов и убедимся, что первые шесть символов строки «Hello Мир», включая символ пробела, отражаются в памяти МК теми же числами, что и их порядковый номер в таблице в шестнадцатиричном исчислении.
А почему для букв «М», «и» и «р» в памяти проставлены числа 0хСС, 0хE8 и 0хF0? В какой таблице можно проверить их истинность и всегда ли мы будем при одной и той же строке иметь дело с именно этими числам?
Отвечать придётся с конца: нет, не всегда, во всяком случае – для букв кириллицы. Это зависит от того, в какой системе кодировки символов работает ваш IDE. Мой «AVR Studio» использует windows 1251 и, если обратиться к соответствующей таблице, то найдутся ответы и на два оставшихся вопроса.
Все дальнейшее будет объясняться применительно именно к этой системе кодов. Однако, я постараюсь излагать текст и код достаточно подробно и просто, чтобы желающие могли без особых усилий написать программу и для других систем.
Оформим список выбранных нами символов из файла «ArialNarrow14.c» в виде таблицы и, для наглядности, разместим её рядом с предыдущей.
В правой таблице числами сверху и справа от неё указаны старшие и младшие значения порядкового номера блока данных символа в массиве. Как видите, для первых 128 символов этот номер совпадает с кодом windows 1251. Это означает, что числовые значения первых шести символов строки «Hello Мир» мы будем передавать в функцию printChar() как есть. Дальше не всё так радужно. Если мы попытаемся напечатать русскую букву «А» (код – 0xC0), то получим на экране «Ё», поскольку именно она располагается в массиве под этим номером. А с буквами от русской «В» и дальше – совсем беда. Добравшись, к примеру, до русской буквы «М» из строки «Hello Мир» микроконтроллер возьмёт её код из левой таблицы – 0xС8 (или 200 в десятичном исчислении) – и пойдёт в массив искать двухсотый блок данных. А у нас их там – всего 194.
Решение лежит на поверхности – для всех используемых символов привести в соответствие их код из левой таблицы с номером из правой. Прикинем, что для этого нужно:
1. Для первых 128 символов, включая символ «DELETE», отправляем в функцию printChar() сам код.
2. Для печати буквы «Ё» меняем код (0xA8) на номер в массиве (0xC0), а для для «ё» – 0xB8 на 0xC1.
3. Все остальные символы кириллицы «А» до «я» сдвинуты в массиве вверх на 64 (0x40 в шестнадцатиричном выражении) позиции относительно значений их кодов. Значит, в этом случае мы передаём в функцию printChar() разницу между кодом и числом 0x40.
Введём макроопределения.
#define DELETE_CODE 0x7F
#define RUS_YO_CODE 0xA8
#define RUS_YO_NUMBER 0xC0
#define RUS_yo_CODE 0xB8
#define RUS_yo_NUMBER 0xC1
#define SUBTRUCT 0x40
Вот такие забавные обозначения для «Ё» и «ё» я встретил в интернете. Да и ладно, как пример сойдёт.
Объявим переменную BlockNum и функция printString() на этом этапе станет такой:
Это – ещё не всё. Если пройтись по строчкам функции printChar(), то станет видно, что после печати первой буквы «H» нашей строки курсор окажется на один пиксель ниже её левого нижнего угла, то есть в итоге мы получим вертикальную надпись. Это, в общем то, тоже интересный и иногда нужный вариант, но нас, всё же, сейчас интересует горизонтальная строка. Добавим ещё две переменные – startY и startX – и будем с их помощью устанавливать курсор после печати каждого символа на один пиксель правее его верхнего правого угла.
Вот теперь всё.
↑ Подыдожим коды для всех модулей!
Соберём воедино всё, что мы прошли в этой и предыдущих частях статьи, и выведем на экраны всех четырёх модулей разные варианты изображений.↑ SSD1306:
#include "softSPI.h"
#include "ssd1306.h"
#include "graphics.h"
#include "charge.c"
#include "ArialNarrow14.c"
int main(void)
{
ssd1306_init();
setColor(WHITE);
setCursor(54, 35);
drawIcon(charge);
setCursor(2, 17);
drawHorizontalLine(124);
setCursor(2, 62);
drawHorizontalLine(124);
setCursor(40, 26);
drawVerticalLine(30);
setCursor(88, 26);
drawVerticalLine(30);
setCursor(10, 29);
drawRectangle(20, 20);
setCursor(98, 29);
fillRectangle(20, 20);
setFont(ArialNarrow14);
setCursor(25, 0);
printString("Журнал");
ssd1306_draw_display();
while(1)
{
}
}
↑ ST7735:
#include "softSPI.h"
#include "st7735.h"
#include "graphics.h"
#include "charge.c"
#include "android.c"
#include "ArialNarrow14.c"
int main(void)
{
st7735_init();
st7735_fill_screen(BLACK);
setCursor(100, 140);
setColor(YELLOW);
drawIcon(charge);
setCursor(0, 30);
setColor(RED);
drawIcon(android);
setCursor(4, 4);
setColor(WHITE);
drawRectangle(120, 152);
setColor(GREEN);
setFont(ArialNarrow14);
setCursor(20, 6);
printString("ДАТАГОР");
while(1)
{
}
}
↑ ILI9341:
#include "parallel_8.h"
#include "ili9341.h"
#include "graphics.h"
#include "charge.c"
#include "android.c"
#include "youtube.c"
#include "Verdana20.c"
int main(void)
{
ili9341_init();
ili9341_fill_screen(BLUE);
setCursor(17, 17);
setColor(YELLOW);
fillRectangle(204, 204);
setCursor(17, 17);
setColor(BLACK);
drawIcon(youtube);
setCursor(260, 120);
setColor(YELLOW);
drawIcon(charge);
setCursor(300, 80);
setColor(GREEN);
drawIcon(android);
setFont(Verdana20);
setColor(WHITE);
setCursor(80, 250);
printString("практической");
while(1)
{
}
}
↑ ILI9481:
#include "parallel_8.h"
#include "ili9481.h"
#include "graphics.h"
#include "android.c"
#include "youtube.c"
#include "Verdana20.c"
int main(void)
{
ili9481_init();
ili9481_fill_screen(BLACK);
setColor(WHITE);
setCursor(15, 25);
fillRectangle(204, 204);
setColor(RED);
setCursor(15, 25);
drawIcon(youtube);
setColor(GREEN);
setCursor(280, 70);
drawIcon(android);
setFont(Verdana20);
setCursor(280, 40);
printString("Hello!");
setColor(YELLOW);
setCursor(120, 270);
printString("электроники");
while(1)
{
}
}
Приношу извинения за качество фотографий: никак не могу избавиться от бликов и размытости. В реальности все графические объекты, кроме youtube и android, выглядят замечательно. Две упомянутые иконки имеют небольшие огрехи, особенно Android. Этот товарищ местами похож на побитого молью бородавочника. Но, к нашему коду это не имеет никакого отношения. Если открыть иконки в графическом редакторе и увеличить, то помимо чёрного и белого цвета можно увидеть вкрапления серого разных оттенков. И только создателю lcd-image-converter известно, как именно программа трактует тот или иной оттенок. Там, где серый цвет вместо чёрного ошибочно засчитан белым, мы получим «проплешину», в противном случае – «бородавку».
Выход из ситуации очень простой и такой же нудный – в том же редакторе изменить цвет всех «неправильных» пикселей. Хотя, я не исключаю, что уже есть программы, позволяющие делать это автоматом.
↑ Файлы
Файлы обновлены 19-07-2017.🎁programmnyy-kod-vseh-indikatorov.7z 173.18 Kb ⇣ 179
🎁ikonki.7z 7.13 Kb ⇣ 132
🎁fayly-tmpl.7z 419 b ⇣ 149
↑ Заключение
На этом наш цикл статей, посвящённый средствам визуализации для микроконтроллеров, заканчивается.Хочу поблагодарить всех читателей, оставивших тёплые отзывы по каждой части. Особая признательность Игорю Котову за его тяжкий и нужный редакторский труд.
Всем удачи! :bye:
Камрад, рассмотри датагорские рекомендации
🌼 Полезные и проверенные железяки, можно брать
Опробовано в лаборатории редакции или читателями.