Изучаем устройство OLED-экрана SSD1306 и дорабатываем звуковые индикаторы музыкального плеера PSG-файлов на чипе AY-3-8910.
Оглавление
В предыдущей статье я рассказал про свою версию музыкального chiptune-плеера на Arduino Pro Micro, воспроизводящего музыку на чипе AY-3-8910 из файлов в формате PSG, находящихся на SD-карте.
Про формат PSG
PSG-файл содержит последовательный дамп всех данных, которые выдаются в регистры чипа AY примерно 50 раз в секунду. С одной стороны, объем таких данных относительно велик по сравнению c форматами музыкальных редакторов-трекеров. С другой стороны, плеер такого формата очень прост в реализации, не требует больших объемов памяти для декодирования и значительных вычислительных мощностей. Имеется лишь небольшой буфер объемом пару сотен байт, используемый для считывания данных с SD-карты и передачу из него данных в регистры чипа AY без дополнительной обработки. Для преобразования прочих chiptune-файлов в этот формат можно использовать плеер AY_Emul Сергея Бульбы. На его же сайте можно найти огромные архивы с chiptune-музыкой.
Напомню, что за основу мной был взят другой плеер, в него добавлены:
кнопки для перехода на одну, 5 или 10 композиций назад/вперед
кнопка переключения между последовательным/случайным режимами выбора композиций
кнопка включения/выключения режима «демо» (когда играется только начальный фрагмент каждой композиции)
статическая память (SRAM) для хранения в ней имен всех файлов на SD-карте, а также списка неповторяющихся случайных чисел
Также расширена информация, показываемая на OLED-дисплее.
Индикатор громкости
Автором исходного плеера был реализован индикатор, который рисуется текстовыми символами:
Отклонение символов > [] < от краев и центра индикатора пропорционально громкости в соответствующем канале A/B/C синтезатора. Выглядит вполне изящно и наглядно. Но мне запомнились индикаторы в различных демосценах на ZX Spectrum, которые показываются столбиками разной высоты:
Захотелось реализовать что-то подобное. Чтобы понять, как это сделать, рассмотрим устройство экрана.
Экран SSD1306
Показ индикаторов громкости текстовыми символами выбран, в том числе, из-за особенностей дисплея. Здесь использована I2C-версия OLED-дисплея SSD1306 с разрешением 128х32 точки. Главная особенность таких экранов – данные для отрисовки возможно только передавать в них, но невозможно прочитать, что усложняет отрисовку графики поверх уже имеющегося изображения. Множество библиотек и программ, управляющих выводом данных на такие экраны, формируют изображение в памяти микроконтроллера, а затем копируют его в экран по шине I2C. Это повышает расход оперативной памяти, и передача большого объема данных в экран требует значительного времени.
На картинке схематично показан один ряд экрана высотой 8 пикселов и шириной 128 пикселов (столбцов). Экран монохромный, один байт соответствует вертикальному столбцу из 8 точек на экране. Младший бит соответствует верхнему пикселу, старший – нижнему пикселу в ряду. Подробнее устройство экрана рассмотрено, например, здесь и в datasheet.
Объем буфера для экрана 128х32 точки (4 ряда) потребует 512 байт оперативной памяти. Для чтения файлов с SD-карты используется библиотека, сама по себе занимающая в памяти значительный объем, и при общем объеме памяти в 2,5 килобайта свободного места больше почти не остается. Поэтому, здесь используется весьма полезная библиотека SSD1306Ascii. Она выводит только текст, передавая изображения символов из программной флеш-памяти (PROGMEM) напрямую в экран. Используется шрифт с размером букв 5х7 пикселов (в комплекте имеются и другие шрифты). В ряду высотой 8 пикселов как раз помещается буква плюс пустая строка в пиксел высотой для пропуска между строками текста.
В данном плеере верхние три строки обновляются только при переходе на следующий файл либо смене режима воспроизведения – перед этим вызывается полная очистка экрана (функция clear). Нижняя строка показывает, сколько процентов файла воспроизведено, и сами индикаторы громкости в каналах синтеза A/B/C. Только она обновляется 50 раз в секунду в той же процедуре обработки прерывания, которая выдает данные в регистры чипа AY для воспроизведения музыки. Для этого вызывается отдельная процедура - рассмотрим её подробнее.
Обновление индикаторов громкости
Для показа прогресса воспроизведения и индикаторов громкости вызывается процедура displayOLED. Рассмотрим её код:
void displayOLED() {
if (playbackFinished) return;
oled.setCursor(0, 24); // <= 1
oled.print(" "); // <= 2
//
float fprc = (fsize > 0) ? 1000.0 * float(floaded) / float(fsize) : 0;
int np = int(fprc + 0.5);
if (np > 1000) np = 1000;
sprintf(fperc, "%d.%d%%", np / 10, np % 10);
oled.setCursor(0, 24); // <= 1
oled.print(fperc);
//
oled.setCursor(32 + volumeA / 1.5, 24); // <= 3
oled.print(">");
oled.setCursor((122 - volumeC / 1.5), 24); // <= 3
oled.print("<");
oled.setCursor((80 + volumeB / 1.5), 24); // <= 3
oled.print("]");
oled.setCursor((75 - volumeB / 1.5), 24); // <= 3
oled.print("[");
}
Сначала обратим внимание на две строки, в которых устанавливается текущая позиция печати на экране: oled.setCursor(0, 24) - помечены в комментариях знаком <= 1. Здесь предыдущий автор явно хотел установить позицию x = 0, y = 24 в пикселах. В заголовочном файле библиотеки SSD1306Ascii.h можно найти описание этой функции:
/**
* @brief Set the cursor position.
*
* @param[in] col The column number in pixels.
* @param[in] row the row number in eight pixel rows.
*/
void setCursor(uint8_t col, uint8_t row);
Параметр row означает не координату по вертикали в пикселах, а индекс (начиная с нуля) ряда высотой 8-пикселов. То есть, для установки курсора в начало 4-й строки нужно вызвать oled.setCursor(0, 3).
После установки курсора в начало четвертой строки печатается строка из 22 пробелов (помечено в комментариях знаком <= 2). Это делается для очистки строки, т.к. заранее неизвестно, в какой позиции окажутся символы индикаторов громкости. Далее уже вычисляется и печатается прогресс воспроизведения в процентах, а также символы > [] < индикатора громкости в нужных координатах (в этих строках также нужно исправить 24 на 3 в параметрах вызова oled.setCursor – помечены знаком <= 3).
Но функция print при печати каждого символа, даже если это пробел, считывает данные из флеш-памяти, а также передает дополнительные данные, что требует времени. В заголовочном файле библиотеки можно обнаружить функцию clear с дополнительными параметрами:
/**
* @brief Clear a region of the display.
*
* @param[in] c0 Starting column.
* @param[in] c1 Ending column.
* @param[in] r0 Starting row;
* @param[in] r1 Ending row;
*/
void clear(uint8_t c0, uint8_t c1, uint8_t r0, uint8_t r1);
Из описания понятно, что эта функция очищает прямоугольную область экрана, начиная со столбца c0 и заканчивая столбцом c1, с ряда r0 до ряда r1, используя передачу нулевых байт в экран, без лишнего чтения данных. Здесь также используются именно номера рядов (строк), а не позиция по вертикали в пикселах.
В начале строки показывается прогресс в процентах, под которым не нужно каждый раз очищать область экрана. Звуковой индикатор показывается, начиная с позиции 32 по горизонтали, поэтому достаточно очищать прямоугольную область от этой позиции до конца строки:
oled.clear(32, 127, 3, 3);
Такая очистка производится гораздо быстрее, чем печать множества пробелов. В результате, отображение прогресса воспроизведения и индикаторов громкости теперь производится практически без мерцания.
Другой индикатор
Если изучить исходный код библиотеки, можно найти функцию, с помощью которой, в конечном итоге, вертикальные столбцы букв высотой в 8 пикселов (и объемом в один байт) передаются в экран для отрисовки:
/**
* @brief Write a byte to RAM in the display controller.
*
* @param[in] c The data byte.
* @note The byte will immediately be sent to the controller.
*/
void ssd1306WriteRam(uint8_t c);
С помощью этой функции мы можем отображать что-то, отличное от букв. Например, если передать байт 0xFF, на экране будет показана вертикальная черта высотой 8 пикселов и толщиной в один пиксел. Экран вытянут по горизонтали, поэтому попробуем нарисовать индикаторы громкости, расположенные не вертикально, а горизонтально слева направо. Как мы помним, индикаторы показываются, начиная с координаты 32 по горизонтали, до конца строки мы имеем 96 пикселов на 3 индикатора. Значит, каждый из них может иметь ширину до 32 пикселов при отображении максимальной громкости. Чтобы было похоже на то, что показано в начале статьи, попробуем рисовать вертикальные черточки через одну. При этом, пусть ширина индикатора будет тем больше, чем больше громкость в канале. Индикатор для одного канала при отображении максимальной громкости будет выглядеть примерно так:
Чтобы реализовать дополнительный метод отрисовки, я добавил класс SSD1306TextVol, унаследованный от класса SSD1306AsciiAvrI2c из библиотеки. В нем я добавил переменные для хранения текущего значения громкости в каждом из каналов A/B/C, а также метод drawVol для отрисовки громкости в трех каналах. Для отрисовки каждого из них вызывается внутренний метод drawMeter, а для сброса индикатора (например, при переходе к следующему файлу) – метод vreset.
При отрисовке индикатора в каждом канале сначала проверяется, изменилась ли громкость. Если не изменилась, то ничего и не делается. А если изменилась, то индикатор не перерисовывается весь, а в нем либо стираются лишние вертикальные черточки, либо дорисовываются недостающие.
Переключение режима индикатора
Я не стал убирать старый режим отрисовки индикаторов громкости, а добавил глобальную переменную nVolumeMode, значение которой определяет, какой режим сейчас включен. Теперь нужно добавить возможность переключения между режимами. Чтобы не изменять схему, я решил добавить для этого обработку одновременного нажатия кнопок 7 и 8. Отдельно нажатые кнопки переключают режимы по-старому («демо» и случайное/последовательное воспроизведение), а одновременно нажатые переключают режим индикатора.
Поскольку состояние нажатия всех восьми кнопок сразу я получаю из сдвигового регистра 74HC165, я могу обрабатывать их нажатие как независимо, так и одновременно. Для каждой из кнопок у меня имеется экземпляр класса CBtn, «привязанного» к соответствующему биту байта, приходящего из сдвигового регистра. Можно попробовать обрабатывать кнопки 7 и 8 примерно так:
byte inBtn = in_165_byte(); // сюда считывается состояние кнопок из входного сдвигового регистра
CBtn btn7(6, &inBtn), btn8(7, &inBtn); // кнопки 7 и 8 «привязаны» к битам 6 и 7
bool bPressed7 = btn7.pressed(), bPressed8 = btn8.pressed();
if (bPressed7 && bPressed8) {
// переключить режим индикатора громкости
}
else if (!bPressed7 && bPressed8) {
// переключить режим «демо»
}
else if (bPressed7 && !bPressed8) {
// переключить режим случайного/последовательного воспроизведения
}
Это даже как-то работает, но очень часто, помимо нажатия сразу двух кнопок, детектируется также нажатие какой-то одной из двух. Для правильной обработки как одновременного, так и независимого нажатия двух кнопок нужно проверять их нажатие с таймаутом. Это я реализовал в классе CBtn2. Работает он примерно так:
Сначала нужно из него вызвать функцию CheckPress. Она определяет состояние обеих кнопок – нажата или нет по соответствующему биту в байте, полученном из сдвигового регистра.
Если первая кнопка не была нажата, а её перевели в состояние «нажата» с таймаутом BTN_TIMEOUT_MS после предыдущего нажатия, бите 0 переменной m_press устанавливается в 1, а время нажатия запоминается в переменной m_nPressTime1. Аналогично для второй кнопки – если её перевели в состояние «нажата», это запоминается в бите 1 переменной m_press, а время нажатия запоминается в переменной m_nPressTime2. Здесь таймаут нужен для борьбы с дребезгом контактов (аналогично было сделано и в старом классе CBtn).
Если же наоборот, ранее кнопка была нажата, а теперь нет, соответствующий бит (0 или 1) в переменной m_press сбрасывается в 0.
После этого проверяется значение ещё одной переменной m_ret – если в ней не 0, а при этом обе кнопки отжаты, то переменной m_ret присваивается 0. Эта переменная нужна для того, чтобы одновременное нажатие кнопок детектировалось однократно (см. ниже).
Далее нужно вызвать функцию Pressed. В ней осуществляется проверка: если в переменной m_ret нулевое значение, и при этом в переменной m_pressed – не 0, это означает, что какая-то из кнопок была нажата. Если же в m_pressed значение 0, то ни одна из кнопок не нажата, и функция возвращает 0.
Если в переменной m_pressed и бит 0, и бит 1 установлены, значит, нажаты обе кнопки – функция возвращает значение 3.
Если в переменной m_pressed бит 0 установлен, а бит 1 нет, но при этом от времени нажатия первой кнопки прошло времени меньше, чем BTN_BOTH_MS (второй таймаут), то функция возвращает 0, как будто ни одна из кнопок не нажата. Если времени прошло больше, то функция возвращает 1 – нажата только первая кнопка.
Если в переменной m_pressed бит 1 установлен, а бит 0 нет, но при этом от времени нажатия второй кнопки прошло времени меньше, чем BTN_BOTH_MS, то функция возвращает 0, как будто ни одна из кнопок не нажата. Если времени прошло больше, то функция возвращает 2 – нажата только вторая кнопка.
Во всех случаях 5 – 8 возвращаемое значение запоминается в переменной m_ret. Это позволяет избежать многократного детектирования нажатия кнопок при повторном вызове Pressed().
Теперь обработка нажатия кнопок 7 и 8 выглядит так:
#define BTN1_MASK 0x01
#define BTN2_MASK 0x02
#define BTN12_MASK 0x03
CBtn2 btn78(6, 7, &inBtn);
void playNotes() {
inBtn = in_165_byte();
btn78.CheckPress();
const byte n78 = btn78.Pressed();
if (BTN12_MASK == n78) // нажаты обе кнопки 7 и 8 – переключить режим индикатора
bSwitchBars = true;
else if (BTN2_MASK == n78) { // нажата только кнопка 8 – переключить режим «демо»
demoMode = !demoMode;
showFile();
}
else if (BTN1_MASK == n78) { // нажата только кнопка 7 – переключить режим смены файлов
randMode = !randMode;
showFile();
}
// далее код не изменяется
}
В результате отдельное нажатие кнопок 7 и 8 обрабатывается не настолько быстро, как ранее, зато их одновременное нажатие обрабатывается надежно. При желании можно подобрать комфортное значение таймаута в константе BTN_BOTH_MS.
Индикатор нот
Здесь очень хотелось бы написать «индикатор спектра» и сделать что-то подобное этому. Для этого нужно было бы оцифровывать звуковой аналоговый выход чипа AY, добавить анализатор спектра на БПФ, но на это в Arduino Pro Micro уже не хватит свободной оперативной памяти.
Можно поступить по-другому. Индикатор громкости в предыдущих двух режимах показывает значения, передаваемые в регистры R8-R10, управляющие громкостью в каналах A/B/C чипа AY. Для задания высоты тона в младшие 6 регистров R0-R5 передаются 12-битные значения делителя базовой частоты (она равна тактовой частоте, делённой на 16).
Можно по значению делителя определять, к какой ближайшей ноте относится получаемая частота тона, и рисовать на индикаторе вертикальную черточку в соответствующей позиции по горизонтали. Высота черточки при этом пусть соответствует громкости.
При тактовой частоте 1.75 МГц самая низкая частота тона, соответствующая делителю 4095, равна 26.7 Гц, что примерно соответствует ноте «ля» субконтроктавы. Нужно построить таблицу значений делителя, которые являются пограничными между соседними нотами. Как мы помним, полная ширина индикатора составляет 96 пикселов – такое количество значений в таблице нам нужно получить.
Я написал вспомогательную программу, которая для всех значений делителей частоты от 4095 до 1 определяет ближайшую по частоте ноту и строит таблицу из 96 значений делителя, на которых ближайшая к значению делителя нота меняется. Полученные значения помещены в массив note_div в программной флеш-памяти (PROGMEM), чтобы не занимать оперативную память.
Массив делителей
const uint16_t note_div[] PROGMEM = {
4095, //26.7 Hz, A-1
3862, //28.3 Hz, A#-1
3645, //30.0 Hz, B-1
3441, //31.8 Hz, C0
3247, //33.7 Hz, C#0
3065, //35.7 Hz, D0
2893, //37.8 Hz, D#0
2731, //40.0 Hz, E0
2577, //42.4 Hz, F0
2433, //45.0 Hz, F#0
2296, //47.6 Hz, G0
2167, //50.5 Hz, G#0
2046, //53.5 Hz, A0
1931, //56.6 Hz, A#0
1822, //60.0 Hz, B0
1720, //63.6 Hz, C1
1623, //67.4 Hz, C#1
1532, //71.4 Hz, D1
1446, //75.6 Hz, D#1
1365, //80.1 Hz, E1
1288, //84.9 Hz, F1
1216, //89.9 Hz, F#1
1148, //95.3 Hz, G1
1083, //101.0 Hz, G#1
1023, //106.9 Hz, A1
965, //113.3 Hz, A#1
911, //120.1 Hz, B1
860, //127.2 Hz, C2
811, //134.9 Hz, C#2
766, //142.8 Hz, D2
723, //151.3 Hz, D#2
682, //160.4 Hz, E2
644, //169.8 Hz, F2
608, //179.9 Hz, F#2
574, //190.5 Hz, G2
541, //202.2 Hz, G#2
511, //214.0 Hz, A2
482, //226.9 Hz, A#2
455, //240.4 Hz, B2
430, //254.4 Hz, C3
405, //270.1 Hz, C#3
383, //285.6 Hz, D3
361, //303.0 Hz, D#3
341, //320.7 Hz, E3
322, //339.7 Hz, F3
304, //359.8 Hz, F#3
287, //381.1 Hz, G3
270, //405.1 Hz, G#3
255, //428.9 Hz, A3
241, //453.8 Hz, A#3
227, //481.8 Hz, B3
215, //508.7 Hz, C4
202, //541.5 Hz, C#4
191, //572.6 Hz, D4
180, //607.6 Hz, D#4
170, //643.4 Hz, E4
161, //679.3 Hz, F4
152, //719.6 Hz, F#4
143, //764.9 Hz, G4
135, //810.2 Hz, G#4
127, //861.2 Hz, A4
120, //911.5 Hz, A#4
113, //967.9 Hz, B4
107, //1022.2 Hz, C5
101, //1082.9 Hz, C#5
95, //1151.3 Hz, D5
90, //1215.3 Hz, D#5
85, //1286.8 Hz, E5
80, //1367.2 Hz, F5
76, //1439.1 Hz, F#5
71, //1540.5 Hz, G5
67, //1632.5 Hz, G#5
63, //1736.1 Hz, A5
60, //1822.9 Hz, A#5
56, //1953.1 Hz, B5
53, //2063.7 Hz, C6
50, //2187.5 Hz, C#6
47, //2327.1 Hz, D6
45, //2430.6 Hz, D#6
42, //2604.2 Hz, E6
40, //2734.4 Hz, F6
38, //2878.3 Hz, F#6
35, //3125.0 Hz, G6
33, //3314.4 Hz, G#6
31, //3528.2 Hz, A6
30, //3645.8 Hz, A#6
28, //3906.2 Hz, B6
26, //4206.7 Hz, C7
25, //4375.0 Hz, C#7
23, //4755.4 Hz, D7
22, //4971.6 Hz, D#7
21, //5208.3 Hz, E7
20, //5468.8 Hz, F7
19, //5756.6 Hz, F#7
17, //6433.8 Hz, G7
16, //6835.9 Hz, G#7
};
Для поиска индекса ближайшей ноты по значению делителя в класс SSD1306TextVol я добавил метод nearestNote. Поскольку массив note_div отсортирован, для быстрого поиска в нем ближайшего значения используется двоичный (или бинарный) поиск. Для чтения 16-битных значений делителя из массива в памяти PROGMEM вызывается pgm_read_word. Функция возвращает индекс найденной ноты в диапазоне от 0 до 95 – это и есть смещение по горизонтали от левого края индикатора.
Поскольку в каналах A/B/C могут звучать до трех нот одновременно, на индикаторе могут показываться три вертикальных черточки с разными смещениями по горизонтали. При их отрисовке проверяется, какие ноты с ненулевой громкостью были отрисованы до этого. Если среди текущих нот с ненулевой громкостью ранее отрисованных нет, они стираются с экрана записью нуля в нужную ячейку вызовом ssd1306WriteRam. Если какие-то две ноты в разных каналах оказались одинаковыми, отрисовывается максимум из их громкостей. Всё это реализовано в методе drawFreq, и результат его работы выглядит примерно так:
Добавляем плавности
Перемещение вертикальных черточек по горизонтали соответствует воспроизводимым нотам – чем выше нота, тем правее черточка. Но как следует рассмотреть это достаточно сложно – ноты меняются на экране очень быстро. Чтобы это выглядело более комфортно для восприятия, попробуем сделать так, чтобы вертикальная черточка для каждой ноты исчезала с экрана не мгновенно, а постепенно.
Самый простой метод – хранить в памяти массив громкостей для всех 96 нот, и после того, как очередные 3 поступивших ноты отрисованы, запоминать их в массиве, а по всем остальным «пробегаться» и уменьшать их «громкость» на экране, пока она не достигнет нуля. Но это затратно и по памяти, и по лишним вычислениям. Чтобы сэкономить память и не проверять постоянно все ноты, в большинстве которых почти всегда нулевые значения, я придумал примерно такой алгоритм:
Начало алгоритма такое же, как в предыдущем варианте: при поступлении трех нот в каналах A/B/C выбираются только несовпадающие, а если какие-то ноты совпадают, берется максимум громкости из двух. Это производится в методе getNotes – он возвращает количество несовпадающих нот с ненулевой громкостью, а также сами индексы нот и их громкости.
Имеется очередь некоторой максимальной глубины (задается в QUEUE_DEPTH) из индексов нот m_Qnote и соответствующих им громкостей m_Qvol. Также имеются переменные m_nQueueLen с текущей длиной очереди и m_nQueuePos с позицией в массиве, в которую будет записываться очередная поступившая нота. При старте в них нули (а также после сброса функцией vreset).
Для каждой из поступивших нот в каналах A/B/C проверяется, нет ли их уже в очереди – то есть, не были ли они ранее нарисованы на экране. Если какая-то из нот есть, и новая поступившая громкость больше, чем уже была, то значение громкости обновляется в очереди и на экране. Если же такой ноты в очереди нет, она добавляется в очередь вместе со значением громкости, и нота также отображается на экране.
При добавлении ноты в очередь делается проверка – не достигла ли длина очереди максимального значения QUEUE_DEPTH. Если достигла, с экрана стирается самый «старый» элемент.
По всей длине очереди для нот, не добавленных в очередь на текущем шаге, громкость уменьшается либо на 1, либо в 2 раза (задается с помощью #define SLOW_NOTE_FALL). Если громкость ноты не достигла нуля, она перерисовывается на экране, а если достигла, то нота с экрана стирается.
Если оказалось, что в результате уменьшения громкости нот длина очереди уменьшилась, значение переменной m_nQueueLen обновляется.
Всё это реализовано в методе drawFreq2. Вызовем его вместо drawFreq и посмотрим, что получилось:
На мой взгляд, это выглядит приятнее для глаз. Но на таком маленьком экране индикатор высотой 8 пикселов рассмотреть всё равно сложновато. Я добавил в коде #define FREQ_TWO_ROWS – если эту строку закомментировать, то индикатор частот показывается в одну строку, а если нет, то в две строки, т.е. высотой 16 пикселов. Во втором случае не показывается размер файла в килобайтах (не очень-то важная информация), а номер текущего файла показывается слева в третьем ряду экрана.
Ещё один индикатор
Дальше у меня возникла идея полноэкранного индикатора - разбить экран на 96 знакомест (4 ряда по 24 знака), в каждом из которых показывать громкость в виде каких-нибудь символов. Например, чем выше громкость, тем больше заполнено знакоместо. Индекс знакоместа (от 0 до 95), как и в предыдущем случае, соответствует играемой ноте.
Возможные значения громкости в канале чипа AY – от 0 до 15. Придумаем «шрифт», состоящий из 16 символов (первый символ – пробел, соответствующий нулевой громкости, здесь не показан):
Также допустимо значение 16, что означает включение огибающей громкости. Для него предусмотрим отдельный символ (показан справа внизу). Все символы поместим в массив из 17 байт vol_char в программной флеш-памяти (PROGMEM).
Массив символов
const byte vol_char[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x08, 0x00, 0x00,
0x00, 0x08, 0x00, 0x08, 0x00,
0x00, 0x08, 0x08, 0x08, 0x00,
0x00, 0x08, 0x14, 0x08, 0x00,
0x00, 0x08, 0x1C, 0x08, 0x00,
0x00, 0x1C, 0x14, 0x1C, 0x00,
0x00, 0x1C, 0x1C, 0x1C, 0x00,
0x00, 0x1C, 0x36, 0x1C, 0x00,
0x00, 0x1C, 0x3E, 0x1C, 0x00,
0x08, 0x1C, 0x3E, 0x1C, 0x08,
0x08, 0x3E, 0x3E, 0x3E, 0x08,
0x1C, 0x3E, 0x3E, 0x3E, 0x1C,
0x1C, 0x3E, 0x7F, 0x3E, 0x1C,
0x3E, 0x3E, 0x7F, 0x3E, 0x3E,
0x3E, 0x7F, 0x7F, 0x7F, 0x3E,
0x55, 0xAA, 0x55, 0xAA, 0x55,
};
В процедуре drawFreq2 вызываются ещё две: drawZero(byte note) для стирания с экрана ноты с индексом note и drawVal(byte note, byte vol) для отображения ноты note с громкостью vol. В предыдущем случае каждая из них вызывает ssd1306WriteRam для передачи в экран либо нулевого значения, либо соответствующего ненулевого для отрисовки вертикальной черты в соответствии с громкостью. Я добавил во все три функции параметр bool bFullScreen – если в нем передается значение true, то индикатор отрисовывается на полный экран.
В таком случае нужно индекс ноты пересчитать в координаты по горизонтали и вертикали. Со вторым всё просто: чтобы получить номер ряда (row), нужно разделить note на 4. Для получения индекса знакоместа по горизонтали (col) нужно взять остаток отделения note на 4, а для получения координаты X умножить на 5 (ширина одного символа) и прибавить 4. Последнее нужно для центрирования индикатора: полная ширина экрана 128, а 24 знакоместа по 5 пикселов шириной занимают только 120 пикселей.
Для отрисовки каждого символа из массива vol_char нужно передать в экран 5 байт. Сначала я рисовал их по одному, устанавливая позицию на экране с помощью setCursor, а затем передавая байт символа с помощью ssd1306WriteRam. Но это оказалось слишком медленно – при перерисовке индикатора 50 раз в секунду иногда возникали ситуации, когда воспроизведение мелодии начинало «притормаживать». Если вспомнить описание функции, в комментариях к ней написано: «The byte will immediately be sent to the controller». Это означает, что каждый байт немедленно отправляется в экран. Однако, есть способ отправки нескольких байт за раз.
Изучив исходный код библиотеки, можно найти еще одну функцию ssd1306WriteRamBuf:
/**
* @brief Write a byte to RAM in the display controller.
*
* @param[in] c The data byte.
* @note The byte may be buffered until a call to ssd1306WriteCmd
* or ssd1306WriteRam.
*/
void ssd1306WriteRamBuf(uint8_t c);
Она используется при печати текста в функции write самой библиотеки. Все байты символов накапливаются в буфере в памяти, не отображаясь сразу на экране. Если после этого вызвать функцию setRow или setCol, внутри будет вызвана функция ssd1306WriteCmd, которая и завершит отображение накопленных в буфере данных на экране. Этот подход я использовал при отрисовке символов из массива vol_char в функции drawVal:
#define FS_LEFT 4
void drawVal(byte note, byte vol, bool bFullScreen) {
byte val = vol;
if (val > 16) val = 16;
if (bFullScreen) {
val *= 5;
setCursor(FS_LEFT + (note % 24) * 5, note / 24);
for (byte i = 0; i < 5; i++) {
ssd1306WriteRamBuf(pgm_read_byte(&vol_char[val + i]));
m_col++;
}
setCol(m_col);
return;
}
// …
}
Отрисовка явно ускорилась – теперь музыка при воспроизведении не «тормозит».
Работу плеера со всеми режимами индикатора можно увидеть в видеоролике:
Заключение
В результате мы выяснили, что с помощью буквально трех функций библиотеки SSD1306Ascii можно быстро выводить в нужные места на экране практически любую информацию – главное, правильно сформировать нужные байты для изображения и задать позицию вывода.
При сборке проекта Arduino IDE выдает следующий результат:
Скетч использует 27454 байт (95%) памяти устройства. Всего доступно 28672 байт.
Глобальные переменные используют 1694 байт (66%) динамической памяти, оставляя 866 байт для локальных переменных. Максимум: 2560 байт.
Как видно, память устройства практически заполнена. В какой-то момент при разработке пришлось размер буфера для чтения с SD-карты уменьшить с 300 до 200 байт, иначе прошивка в устройство повторно отказывалась загружаться (видимо, портится загрузчик), и его пришлось сбрасывать в исходное состояние с пустым скетчем. Можно перенести буфер чтения во внешнюю память SRAM, либо перейти на Arduino Mega, если захочется как-то дальше развивать проект (например, есть мысль поставить экран побольше). Но это, как говорится, уже другая история.
Чтобы не загромождать основной код скетча, новый класс, «шрифт» полноэкранного индикатора и таблицы констант note_div я вынес в отдельный файл aymeter.h. Ещё я добавил запоминание в EEPROM текущего режима индикатора и режима выбора файлов (случайный/последовательный), а также состояние режима «демо» - после включения устройства все режимы восстанавливаются. Итоговый код обновлён в репозитории.
Ссылки по теме
Комментарии (26)
ponikrf
29.03.2024 11:10Взял бы аппаратный проигрыватель на AY-3-8910, да все как-то никто не доведет его до конечного продукта. Ждем ждем...
voldemar_d Автор
29.03.2024 11:10Есть вот такое устройство:
https://habr.com/ru/companies/ruvds/articles/743024/
В статье автор справедливо рассуждает о себестоимости и стоимости такого устройства. Если его довести до промышленного образца - вряд ли кто его купит. За такие же деньги можно взять смартфон и поставить в него программу ZXTune, у которой море возможностей, вплоть до воспроизведения chiptune-мелодий прямо с онлайн-ресурса.
ponikrf
29.03.2024 11:10Да я бы наверное уже и сам бы занялся, но вот после того, как порубили 2 площадки lcsc и jlcpcb перестал заниматься электроникой.
У меня еще проект был по созданию простых дешевых датчиков температуры влажности на rs485. Так по тем же причинам их не доделал, хотя все уже закупил всместе с корпусами, осталось только платы заказать.
А сейчас уже и возвращаться к этому не хочется. Эх.
NutsUnderline
29.03.2024 11:10+2Это все отговорки :) Заказ плат менее удобный но процесс налажен. Смысла правда в этом.. как раз вот электроникой баловаться.. а музыку можно слушать и более простыми методами
ponikrf
29.03.2024 11:10Это все отговорки :) Заказ плат менее удобный но процесс налажен.
Ну это сейчас он налажен. А когда обрубили свифт, совсем непонятно что было делать. А потом уже ушел в другие проекты.
а музыку можно слушать и более простыми методами
Можно, но у меня зачем то DJ-CD дека стоит) Да и винил проигрыватель есть) Вопрос тут явно не про проще
NutsUnderline
29.03.2024 11:10Понятно дело - чтобы немного скретчей на ночь послушать :) У взрослых -взрослые игрушки, за соотв. цену...
voldemar_d Автор
29.03.2024 11:10Видел у одного человека в гараже, где он машины ремонтирует, и виниловый проигрыватель, и ламповый усилитель, и кучу пластинок, да и CD-плеер с кучей дисков. Эстет :)
voldemar_d Автор
29.03.2024 11:10Не так давно в какой-то теме это уже обсуждали, но я забыл, где - не подскажете, на каких ресурсах сейчас можно платы штучно (ну или хотя бы несколько штук, а не 1000) заказать из РФ, и чтобы они не космических денег стоили?
voldemar_d Автор
29.03.2024 11:10Одно дело - "сам занялся и для себя сделал", а другое - "наладил серийный выпуск". ИМХО, тут даже в ноль сложно выйти. Есть некий сайт, где человек продает готовые платы с набором деталей, чтобы каждый желающий сам спаять себе мог что-нибудь (например, у него там есть музыкальный звонок на чипе AY). Но даже если такой набор для самостоятельной сборки продавать, он в несколько тысяч рублей выйдет, особенно с готовым корпусом, экраном, микроконтроллером и платой.
Zara6502
29.03.2024 11:10когда человек хочет, есть деньги, но не так много времени чтобы сидеть, читать, разбираться, паять, отлаживать, то мне кажется отдать разом 100уе за готовый продукт и просто им пользоваться - весьма разумное решение. Другое дело что таких людей может быть мало чтобы сама затея окупалась даже по нулям.
Как пример разные современные моды для старых приставок и компов, когда сам мод стоит столько же как ретро железка.
voldemar_d Автор
29.03.2024 11:10Вы читали раздел "Экономика" в статье по ссылке выше? Даже в конечную цену 100 у.е. ещё уложиться надо. А если это на себе тянет один человек - ещё и выжить при этом.
Zara6502
29.03.2024 11:10никто не говорил что будет легко, ну и я ваши слова "Но даже если такой набор для самостоятельной сборки продавать, он в несколько тысяч рублей выйдет" комментировал, а не всю экономику проектов.
Я, например, имею массу свободного времени, но оно абсолютно никак не монетизируется, я мог бы легко наладить выпуск и продавать. Другое дело что я одну детальку буду припаивать месяц и испорчу 30 плат. Так что "экономика" она не прямая как лом. Даже в той статье получение 29 тысяч рублей для меня больше месячной зарплаты. Всё очень индивидуально.
voldemar_d Автор
29.03.2024 11:10Другое дело что я одну детальку буду припаивать месяц и испорчу 30 плат
Это что, несущественное препятствие?
Даже если налаживать выпуск готовых наборов - как минимум, надо заказать несколько плат и купить деталей под несколько наборов. Это уже может вылиться в приличную сумму. Чтобы купить детали с приличной скидкой, надо их купить не на 10 наборов, а хотя бы штук на 100. Что-то мне подсказывает, что найти даже 10 покупателей на такие наборы будет не очень просто.
я мог бы легко наладить выпуск и продавать
Так попробуйте, вдруг получится.
Zara6502
29.03.2024 11:10Это что, несущественное препятствие?
мне кажется очевидным что оно как раз существенное, именно поэтому я платы и не собираю.
Так попробуйте, вдруг получится.
Читайте выше.
voldemar_d Автор
29.03.2024 11:10+1Я так и не пойму, что Вы сказать хотели. Если просто "всё очень индивидуально" - с этим я не спорю.
Zara6502
29.03.2024 11:10Но даже если такой набор для самостоятельной сборки продавать, он в несколько тысяч рублей выйдет, особенно с готовым корпусом, экраном, микроконтроллером и платой.
когда человек хочет, есть деньги, но не так много времени чтобы сидеть, читать, разбираться, паять, отлаживать, то мне кажется отдать разом 100уе за готовый продукт и просто им пользоваться - весьма разумное решение
Мне кажется тут всё однозначно. Вы написали про несколько тысяч, я написал про тех кто их готов потратить. Всё. Но вы зачем-то стали мне рассказывать про экономику и сложности, хотя к моим словам это не имеет отношения.
voldemar_d Автор
29.03.2024 11:10Речь же не только о покупателе, а о том, кто готов такой набор сделать и продавать. Сложности будут не у покупателя. Я не против, если кто-то готов рискнуть деньгами и создать такой набор на ровном месте. Я вот не готов.
Zara6502
29.03.2024 11:10вы упорно переводите тему.
я комментирую конкретный слова с конкретным смыслом, зачем указывать на какие-то неточности моих слов добавляя новые вводные?
NutsUnderline
"Анализатор спектра" нет особого смысля привязывать к нотам, сколь я помню - просто весь спектр частот делили на полосы, и уж точно не кто не помышлял чето там оцифровывать, да и не нужно - эффект был в результате такой же. Только называть это было ошибочно принято "эквалайзером".
Увидев ноты я ожидал увидеть что будет классическое G#3
voldemar_d Автор
В общем-то, да, особого смысла к нотам привязывать нет, всё равно никто не поймёт, глядя на полоски или символы на экране, что это именно какие-то ноты. Можно считать, что мне просто так захотелось :-)
Эквалайзер и анализатор спектра (ну или индикатор частот) - совершенно разные вещи, у кого это было принято так называть?
Насчет именно нот - нашёл недавно запись демо, которого у меня на Спектруме не было:
https://www.youtube.com/watch?v=Lfc3dSbIB1k
Тут прямо ноты на клавиатуре подсвечиваются (жаль, гаснут мгновенно, не всегда разглядеть успеваешь), да ещё и отдельный 32-символьный индикатор для частоты генератора шума есть. Но такое на Arduino реализовать, кмк, вряд ли получится - нужно что-то заметно помощнее, особенно если в цвете всё рисовать.
Да легко можно такое сделать, но как это на экране показывать? Если 50 раз в секунду будет 3 ноты одновременно меняться, что там можно будет разглядеть? Подкиньте разумную идею, мне сходу что-то ничего в голову не приходит.
voldemar_d Автор
Оцифровка и показ спектра с помощью FFT всё-таки имеет смысл. Показ индикатора на основе только делителя частоты тона никак не покажет действие генератора огибающей и генератора шума.
NutsUnderline
Все верно, но визуально все это дело скорее замылится, на таком то экранчике, тут же просто красоту навести все таки. Не знаю часто ли вообще применяют правильные анализаторы спектра настоящие звукачи, в плане музыки, я как то больше наблюдаю применение фильтров и эквализациии на слух.
voldemar_d Автор
Применяют, но не на экране с разрешением 128х32 :)
Плюс, в профессиональных анализаторах есть всякие специальные режимы, когда пики некоторых частот не сразу опадают и т.д. Конечно, здесь всё это просто "для красоты".
NutsUnderline
я уже не помню, пиковые зацепилки я вроде и на спектрмуме видел, причем пиксельные, но может уже и не на спектруме