Добрый день! Меня зовут Евгений и мое хобби - электроника и программирование. Особый интерес для меня представляет поиск и использование скрытых или мало описанных возможностей программных и аппаратных модулей. И сегодня мы разберем вывод на монохромном текстовом LCD дисплее простой графики.
Сразу оговорюсь, что разбирать подключение дисплея к Ардуинке я здесь не буду - статей на эту тему полно. Мы будем рассматривать создание произвольной монохромной графики размером 40 на 8 пикселей, т.е. четверть экрана. Думаю, ни для кого не секрет, что первые 8 символов знакогенератора в таком дисплее можно подменить на свои. И если определить символы, у которых закрашена одна нижняя линия, две, три... восемь, то мы получим таким образом, базовый набор символов для вывода гистограмм. Но гистограмма, полученная таким образом будет содержать лишь 16 точек по оси х, чего иногда не хватает. И при этом она займет весь экран. Мы же рассмотрим вывод произвольных изображений в любую четверть экрана. Для этого создадим класс с двумерным массивом Data для переопределения символов, методы setPixel и getPixel для установки и получения точки в нашей картинке 40х8.
Помимо описанного выше, класс содержит указатель на дисплей, два метода graph для вывода графика функции в заданном интервале аргумента и вывода графика по рассчитанному массиву точек. В функции происходит автомасштабирование по осям в нужные нам интервалы от 0 до 39 по х и от 0 до 7 по у.
Сама же картинка – это просто вывод переопределенных символов с кодами от 0 до 7 как строки в любое место экрана.
Идея элементарная, но реализации интерфейса для ее осуществления я нигде не видел.
Поскольку у нас есть функция setPixel, то мы можем нарисовать любую картинку 40х8, например, логотип, стрелочки, красивые подсказки и т.п. Получилась полезная вещь в быту.
Спросите почему я задался такой целью? А потому, что OLED дисплей из Поднебесной едет уже 2-й месяц. И неизвестно, заработает ли чудо-юдо, наспех сварганенное с липкой вонючей канифолью маленькими желтыми ручками в подвале дяди Ляо… Да и просто интересно.
Собственно код с комментариями, который выводит то, что на картинке выше:
#include <LiquidCrystal_I2C.h>
//Создаем экземпляр дисплея
LiquidCrystal_I2C lcd(0x27, 16, 2);
//Битовая карта для рисования
class BitField
{
private:
//Данные переопределяемых символов - поле для рисования
byte Data[8][8];
//Ссылка на дисплей
LiquidCrystal_I2C* lcd;
public:
//Конструктор - передается ссылка на объект дисплея
BitField(LiquidCrystal_I2C& rLCD);
//Установка (с = true) / очистка точки
void setPixel(byte x, byte y, bool c);
//Возврат "цвета" точки (true - установлена)
bool getPixel(byte x, byte y);
//Рисуем график функции f в интервале от x0 до x1 без инверсии (c = true), начиная в строке 0, позиции 0 с автомасштабированием
void graph(float x0, float x1, float f(float), bool c = true, byte str = 0, byte row = 0);
//Рисуем график функции со значениями в массиве my без инверсии (c = true), начиная в строке 0, позиции 0 с автомасштабированием
void graph(int n, float my[], bool c = true, byte str = 0, byte row = 0);
//Очищаем поле для рисования без инверсии (c = true)
void clear(bool c = true);
}BF(lcd); //Экземпляр нашего класса для рисования
BitField::BitField(LiquidCrystal_I2C& rLCD)
{
lcd = &rLCD;
clear();
}
void BitField::clear(bool c)
{
//Очищаем поле
memset(Data, (c ? 0x00 : 0xFF), 64);
}
void BitField::setPixel(byte x, byte y, bool c)
{
//Устанавливаем точку: у нас по 5 бит на символ в ширину
byte*p = &(Data[x / 5][y]);
//символ начинается со старшего бита
byte s = 1 << (4 - (x % 5));
//Либо устанавливаем, либо сбрасываем бит
if (c) *p |= s; else *p &= ~s;
}
//Аналогично установке получаем точку
bool BitField::getPixel(byte x, byte y)
{
byte*p = &(Data[x / 5][y]);
byte s = 1 << (4 - (x % 5));
return (*p | s == 0) ? false : true;
}
void BitField::graph(float x0, float x1, float f(float), bool c, byte str, byte row)
{
float x, dx, my[64];
byte i;
//Очищаем поле для рисования
clear();
//Формируем 40 значений функции для заданного интервала
dx = (x1 - x0) / 39.0;
for(i = 0, x = x0; i < 40; x += dx, i++) my[i] = f(x);
//Рисуем с учетом масштабирования
graph(40, my, c, str, row);
}
void BitField::graph(int n, float my[], bool c, byte str, byte row)
{
//Если ничго не передано - ничего не рисуем
if (--n < 0) return;
//Теперь в n - индекс последней точки массива my
float y0, y1, ay, by;
byte yc, xp, yp, yy, ym;
byte i;
clear(c);
//Одна точка у нас гарантированно есть, устанавливаем максимальное и минимальное значение функции по ней
y0 = my[0];
y1 = y0;
//Находим максимальное и минимальное значение функции, начиная со второй точки
for(i = 1; i <= n; i ++)
{
if (y0 < my[i]) y0 = my[i];
if (y1 > my[i]) y1 = my[i];
}
//Дабы не вылететь
if(y0 == y1) y1 = y0 + 1E-6;
//Рассчитываем коэффициенты масштабирования функции так, чтобы все переданные my попали в интервал 0..7
ay = 7.0 / (y1 - y0);
by = -ay * y0;
//Пробегаем по 40 точкам х, рассчитываем и рисуем точки
//Отрисовываем первую точку, запоминаем ее как предыдущую
i = 0;
xp = i;
yp = round(ay * my[round((float)i * n / 39)]+ by);
setPixel(i, yp, c);
i++;
for(; i < 40; i++)
{
yc = round(ay * my[round((float)i * n / 39)]+ by);
setPixel(i, yc, c);
ym = max(yp, yc);
//"Соединяем" предыдущую точку графика с текущей
for(yy = min(yp, yc) + 1; yy < ym; yy++) setPixel(xp, yy, c);
//Запоминаем предыдущую точку
xp = i;
yp = yc;
}
//Подменяем символы
for(i = 0; i < 8; i++) lcd -> createChar(i, Data[i]);
//Инициализируем экран, чтобы перечитался знакогенератор (сначала придется выводить графику, а затем - остальное, чтобы не стерлось)
lcd -> clear();
//Выводим нашу картинку в заданную позицию экрана
lcd -> setCursor(row, str);
for(i = 0; i < 8; i++) lcd -> write(i);
}
//Тестовая функция
float f(float x)
{
return sin(x * x);
}
void setup(void)
{
//Инициализируем экран
lcd.setBacklight(1);
lcd.init();
//Выводим график (сначала его, поскольку он очищает экран!)
BF.graph(0, 4.5, f, true, 1, 3);
//Выводим остальной текст
lcd.setCursor(1, 0);
lcd.print("f(x)=sin(x*x)");
}
void loop()
{
}