Индикатор громкости для гаражной колонки я собирал из того, что было под рукой: микрофонный модуль MAX4466 на аналоговом входе и экран 16x2. Сначала на экране жило честное число, которое дрожало между тридцатью и девяноста процентами, и это была одна из тех идей, которые работают технически и не работают по-человечески: число надо находить глазами, прочитывать и сравнивать, а пока читаешь — оно уже другое. Столбик читается иначе, периферийным зрением, не отрываясь от паяльника. Через неделю с числом на первую строку въехала шкала, ещё через несколько дней под ней поселилась гистограмма того, куда сигнал падает чаще всего. Обе картинки собраны из одних и тех же символов, и ниже — как именно.
По классике жанра сначала оговорю, что уже живёт в соседних уроках. Бар из пяти слотов с частичной заливкой разобран в уроке про классический прогресс-бар, спиннер загрузки с процентами на millis — в уроке про анимацию загрузки, а вертикальные столбики-история, где экран показывает временной ряд, — в уроке про индикатор уровня. Мой угол другой: горизонтальная шкала текущего значения с маркером пика и гистограмма-распределение, которая отвечает на вопрос «как часто сигнал бывает большим», а не «когда он был большим». Это разные вопросы, и железо под них тратится одно и то же.
Пара слов про вход, потому что у микрофонного модуля есть характер. Выход MAX4466 смещён на середину питания: тишина читается около 512, а звук качается вокруг неё в обе стороны — шкале это безразлично, она рисует то, что видит. В симуляторе сигнал изображает ручка макетки, на плате подставляйте что угодно аналоговое: LDR, газовый датчик, термистор — шкале всё равно, что мерить, меняется только смысл корзин. Единственное, что стоит знать заранее: у сигнала с сильной постоянкой есть мёртвые корзины, и пустые столбики по краям — не грабля, а честная картина диапазона.
План такой: сначала бюджет знакогенератора — восемь слотов, из которых шкала и маркер съедают пять, потом арифметика клеток с ловушкой на границе, потом корзины гистограммы и чтение их формы. В симуляторе движок команду createChar принимает, но слоты не рисует, поэтому стенд собран честно: шкала там из решёток и плюсов, гистограмма — плотностью символов, а на реальной панели каждая клетка — свой глиф из скетча для платы.
Восемь слотов на всё про всё
Знакогенератор HD44780 держит восемь своих глифов пять на восемь пикселей, и раскладку стоит придумать до кода, а не во время. У меня она такая: слоты с нулевого по третий — частичные заливки четвертями, от одной полоски до четырёх; четвёртый слот — маркер пика, штрих в верхней строке клетки, который въезжает поверх шкалы; пятый лежит про запас, а два последних всегда свободны для градуса или стрелки. Полный блок доставать не надо — под кодом 255 он уже лежит в постоянном знакогенераторе. Если захочется экономнее, один слот можно переписывать на лету под разные нужды — механика перезаписи разобрана в уроке про типы массивов глифов, там же про ловушки объявления байтовых массивов.
Арифметика клеток обходится без map, и вот почему. Красивое на бумаге map(raw, 0, 1023, 0, 16) при сырое 1023 возвращает ровно шестнадцать — а клеток шестнадцать только в счёте с единицы, их номера ноль-пятнадцать, и шестнадцатой не существует. Дальше значение уезжает в печать за краем, и грабля проявляется не каждый раз, а ровно на пике сигнала — самый обидный момент. Рабочие варианты: целочисленное деление raw на 64, что даёт ноль-пятнадцать честно, или map с потолком 1024, если map вам милее. Вторая ловушка тоньше: маркер пика рисуется после шкалы, отдельным вызовом setCursor — если рисовать его раньше, заливка клетки затрёт штрих, и пик исчезнет с экрана, оставшись только в переменной.
#include <Wire.h>#include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27, 16, 2);const int potPin = A0; // на плате - выход микрофонного модуля // Слоты: 0..3 - заливки четвертями, 4 - маркер пика (штрих сверху)byte zaliv1[8] = {0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000};byte zaliv2[8] = {0b11000, 0b11000, 0b11000, 0b11000, 0b11000, 0b11000, 0b11000, 0b11000};byte zaliv3[8] = {0b11100, 0b11100, 0b11100, 0b11100, 0b11100, 0b11100, 0b11100, 0b11100};byte zaliv4[8] = {0b11110, 0b11110, 0b11110, 0b11110, 0b11110, 0b11110, 0b11110, 0b11110};byte marker[8] = {0b11111, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000}; int pik = 0; // клетка пика, 1..12unsigned long last = 0; void setup() { lcd.init(); lcd.backlight(); lcd.createChar(0, zaliv1); lcd.createChar(1, zaliv2); lcd.createChar(2, zaliv3); lcd.createChar(3, zaliv4); lcd.createChar(4, marker);} void loop() { if (millis() - last < 200) return; last = millis(); int raw = analogRead(potPin); int chetv = raw * 48 / 1023; // 12 клеток по 4 четверти: диапазон 0..47 if (chetv > 47) chetv = 47; if (chetv / 4 + 1 > pik) pik = chetv / 4 + 1; lcd.setCursor(0, 0); for (int i = 0; i < 12; i++) { int ost = chetv - i * 4; // сколько четвертей осталось этой клетке if (ost >= 4) lcd.write(byte(255)); // полный блок из ROM else if (ost > 0) lcd.write(byte(ost - 1)); else lcd.print("-"); } if (pik > 0) { // маркер рисуется ПОСЛЕ шкалы lcd.setCursor(pik - 1, 0); lcd.write(byte(4)); } lcd.setCursor(12, 0); // проценты полем фиксированной ширины int proc = raw * 100 / 1023; if (proc < 100) lcd.print(" "); if (proc < 10) lcd.print(" "); lcd.print(proc); lcd.print("%");}Гистограмма: куда падают значения
Шкала отвечает на вопрос «сколько сейчас», гистограмма — на вопрос «как часто бывает сколько». Механика простая: диапазон сырья от нуля до 1023 режется на шестнадцать корзин по 64 отсчёта, каждый тик корзина выбранного значения получает плюс один, и экран показывает, где сигнал живёт. Счётчик корзины держу в byte с потолком девять: больше девяти попаданий в одну корзину картинку не меняет, а вот переполнение байта при долгой работе — реальная перспектива, и защита стоит одного сравнения. При тике в 200 миллисекунд корзина наливается до потолка за 1,8 секунды, то есть весь экран оживает на ваших глазах, а кнопка сброса возвращает всё в ноль для нового замера.
Форма читается как распечатка кардиограммы. Узкий горб — источник стабильный, микрофон слушает ровный шум; два горба — качели, где сигнал мечется между двумя режимами; полка у правого края — пики до предела, и это тот случай, когда усреднение в скетче скрыло бы от вас самое интересное. От вертикальной истории из урока про индикатор уровня эта картинка отличается осями: там время уходит влево и видно, что было минуту назад, здесь время свернуто в счётчик, и видно распределение за всё наблюдение. Проценты в правом углу первой строки живут в поле фиксированной ширины — механика ведущих пробелов разобрана в уроке про числа и нули.
Шестнадцать корзин — не догма, а следствие ширины строки. Если прибор смотрит только на нижнюю половину диапазона, шестнадцать корзин на полдиапазона — это тридцать две единицы на корзину, и горб можно растянуть: делите не raw на 64, а (raw - min) * 16 / (max - min), подставив свои границы. Вторая настройка — окно замера: тик 200 миллисекунд собирает картинку за секунды, тик в секунду растягивает её на минуты и подходит для суточных распределений. Меняется одна константа в условии millis, а смысл картинки меняется целиком: от «что происходит сейчас» до «как шёл весь день».
Что из этого ставить на свой прибор — вопрос не вкуса, а задачи, и моё правило простое. Шкала нужна, когда оператор реагирует на текущее значение: уровень сигнала, громкость, заполнение бака. Гистограмма нужна, когда важен характер: настройка чувствительности микрофона, поиск дребезга, оценка шума канала перед калибровкой. Число нужно для сверки с порогом и для протокола — и ровно поэтому оно у меня переехало из центра экрана в Serial: глаз читает столбики быстрее, чем цифру, а точную сверку делаешь раз в настройке, а не каждый час.
У гистограммы есть и неожиданное применение в отладке: она ловит то, что разовый замер прячет. Мой второй выпуск индикатора показал, что «ровный» аналоговый сигнал от блока питания дёргается между двумя корзинами каждые несколько секунд — дешёвый БП давал пульсацию, которую число с усреднением рисовало как стабильно сорок четыре процента. Куда расти дальше: бар загрузки с процентами на millis разобран в уроке про анимацию загрузки, а те же корзины на OLED рисуются пикселями без всяких слотов — приёмы заливки прямоугольников показаны в уроке про примитивы SSD1306.
Про цену кадра, чтобы никто не боялся частых тиков. Полный кадр этого стенда — две строки по шестнадцать клеток, те же 10-15 миллисекунд по шине, что и у любого текста 16x2; при тике 200 миллисекунд шина занята пятью-семью процентами времени, и соседям по I2C делать нечего. Я держал на одной шине с этим стендом DS3231 — часы даже не заметили соседа. Единственная дисциплина, которую надо соблюдать, — не ставить тик короче сотни миллисекунд: при 20 миллисекундах шина будет занята наполовину, а глаз всё равно не успеет прочитать изменения.
| Инструмент | Что показывает | Чего не показывает |
|---|---|---|
| Шкала из клеток | Мгновенное значение относительно границ диапазона | Как значение вело себя минуту назад |
| Маркер пика | Максимум за всё окно наблюдения | Когда пик случился и сколько длился |
| Гистограмма корзин | Как часто значения попадают в диапазоны | Порядок событий: была волна или рассыпанный шум |
| Число в процентах | Точное значение для сверки с порогом | Всё остальное: глаз читает его медленнее столбика |
- 1. Сколько своих глифов помещается в знакогенератор HD44780 одновременно?
- 2. Чем плоха запись map(raw, 0, 1023, 0, 16) для шкалы из 16 клеток?
- 3. Почему маркер пика рисуется после заливки шкалы, а не до?
- 4. Зачем счётчику корзины гистограммы потолок девять при типе byte?
- 5. Чем гистограмма-распределение отличается от вертикальной истории столбиков?
- Восемь слотов CGRAM — бюджет: четыре заливки, маркер пика, два слота в запасе
- map(raw, 0, 1023, 0, 16) на пике даёт 16 — несуществующую клетку; делите raw на 64
- Маркер пика рисуется после шкалы отдельным setCursor, иначе заливка его затирает
- Гистограмма — 16 корзин по 64 отсчёта, потолок счётчика 9, сброс кнопкой по фронту
- Шкала — для текущего значения, гистограмма — для характера сигнала, число — для сверки
Частые вопросы по теме урока
- Сколько своих глифов помещается в знакогенератор HD44780 одновременно?
- CGRAM держит восемь слотов 5x8. Раскладку надо придумать заранее: шкала съедает пять, остальное — под градус и стрелки. Восемь: слоты 0-7 по пять на восемь пикселей
- Чем плоха запись map(raw, 0, 1023, 0, 16) для шкалы из 16 клеток?
- Номера клеток — ноль-пятнадцать. Значение 16 уезжает за край: в невидимую зону DDRAM, откуда всплывает при скролле. На пике возвращает 16, а клеток с таким номером нет
- Почему маркер пика рисуется после заливки шкалы, а не до?
- Печать клетки кладёт символ поверх уже стоящего. Маркер рисуют последним отдельным setCursor, иначе пик исчезает с экрана. Заливка затёрла бы штрих, нарисованный раньше
- Зачем счётчику корзины гистограммы потолок девять при типе byte?
- Byte дотянул бы до 255, но картинку девятое попадание уже не меняет, а защита одним сравнением снимает вопрос переполнения. Потолок защищает от переполнения при долгой работе
- Чем гистограмма-распределение отличается от вертикальной истории столбиков?
- В истории ось времени уходит влево, в распределении время свернуто в счётчик корзины. Вопросы разные: что было и как часто. История показывает тренд во времени, распределение — частоту попаданий