Деталюги ЖАЛКОО...
Ежли уж с точки зрения оптимальности — лучше используя внутренний R-C генератор освободить под сегменты порт PB.
НО... тогда с «типовой адуриной» нестыковка — надо будет «сторонних разработчиков» использовать, а у меня относительно оных СОМНЕНИЯ гложуть...
Посему пока потеоретизирую относительно самого дисплейчика на основе динамической развертки.
Приму аки должные исходные данные -
сегментные ключи, активный уровень равен логической единице;
позиционные ключи — активный уровень равен логическому нулю.
Для начала без кноп.
Спойлер
Собственно сама идея динамической развертки основана на физиологии глаза сущности вида ХОМО ХАПИЕНС — смена картинок пошустрее, чем 50 раз в секунду вызывает видимость единого изображения.
Ёжли у нас 4 позиции то каждая из оных должна появляться в 4 раза быстрее.
Предпочтительно количество позиций(знакомест) иметь кратным двойке — меньше проблем с математикой относительно наших МК.
Но тут есть гвоздь (ныне не так уж заметный) — лампы накаливания и газосветки — они также на 50 Гц пульсации дают (по крайней мере в постСССР и там, где частота в электросети 50 Гц. Посему забираемся повыше — 60-120 Гц на полный цикл вывода нашего индикатора.
Поскольку ранее МК были не столь шустрыми и с точки зрения “поменьше событий, отвлекающих МК от более важных дел» берем за основу 60 Гц — 0,0166666.... секунды на полную развертку строки индикации. Как-то те «6 в периоде» не есть хорошо — округлим до 0,016 получая в итоге 62,5 Гц на полный кадр.
Делаем буфер отображения размером под количество знакомест (у нас 4) и каждые 0,004 секунды сбрасываем одну из сегментных комбинаций плюс активацию соответствующего оной комбинации позиционного ключа на дисплейчик.
Вроде бы все - «проще не бывает» как обычно заявляют начинаюшши КОТЯТКИ...
Потом … начинают выползать всякопакостны «нюенсы» -
то картинка «дергается, подсвечивается, нестабильна, мерцает»,
то... оказывается МК больше ничего делать не успевает... То ешшо какие матюки «аффтарам идей»...
Разберем ту индикацию более внимательно. Я буду брать за основу «чистый ассемблер» — то, с чем ВСЕГДА можно результат получить. Ну и при возможности чего из средств референса ардуино IDE. Для полноценного повторения алгоритма под «чистым СИ» явно потребуются ассемблерные вставки — а сие уже не моё — может кто из наших КОТОГУРУ подбросит...
Итак...
обзываем:
kadr — одна (или несколько) строк развертки позиций дисплея.
Собственно дисплей может быть и «многострочный»...
line — интервал развертки всех позиций в одной строке дисплея
простейший дисплейчик — однострочный (состоит из N*point)
point – интервал активности одного знакоместа текущей строки развертки.
(набирается из 1 step или N*step).
step - это еще одно «внутреннее деление» - из некоторого количества таких шажков состоит
интервал отображения каждой позиции (зарезервировано под ШИМ алгоритмы).
v_ram — участок в ОЗУ, предназначенный для хранения сегментной комбинации для каждого
из знакомест. Размер определяется количеством позиций ( N*point).
v_ram_tmp — участок ОЗУ, предназначенный для хранения предподготовленных данных,
которые после обнаружения соответствующего запроса будут перегружены в v_ram.
v_flags – байт флагов обеспечения программного автомата.
Это минимум для начала. Далее будем к оным возвращаться.
Собственно такое построение делается для независимости обработчика дисплея(дисплея/клавиатуры) от течения основной программы в МК.
Ну и базовый момент всей идеи АППАРАТНЫЙ ТАЙМЕР...
(Или источник независимых (асинхронных) от основной программы аппаратных прерываний).
Основные минимальные требования к которому:
1. разрядность, обеспечивающая наши потребности;
2. наличие автоперезагрузки заранее заданным коэффициентом деления;
3. наличие аппаратного прерывания минимум по переполнению.
Тут появилась необходимость в константе досчета (tim_step) одиночного цикла таймера.
Однако это при «линейном» (или ШИМ) варианте. Если за основу берется BAM — там несколько иное распределение будет.
Примитив — алгоритм развертки одной строки:
Читаем текущее значение сегментов в порт вывода сегментов;
Активируем текущий сегментный ключ порта позиционных ключей;
Ждем интервал (у нас 0,004S) свечения текущего знакоместа;
Декрементируем (или инкрементируем) и проверяем на исчерпание счетчик позиций -
если не исчерпан — повтор от начала,
если исчерпан — перезагрузка счетчика и повтор от начала...
Добавилися
v_point_cnt – счетчик позиций дисплейчика (пока он считает step - интервалы)
Однако светится-то ОДНО знакоместо....
Надо добавить:
v_ram_ptr – указатель начального адреса видеопамяти
и... как-то определиться с активацией соответствующего позиционного ключа и адреса сегментных данных в видеопамяти...
В зависимости от имеющегося набора команд и функций компилятора решение может варьироваться.
Однако наиболее удачно использование для указания адреса сегментного кода содержимого указателя
v_ram_ptr + смещение равное текущему содержимому счетчика позиций v_point_cnt.
Но тут ужшш...
Проще всего автоинкремент указателя — но там в конце цикла потребуется его перезагрузка константой.
Ежли счетчик декрементный — возможны проблемы с направлением вывода (слева направо или справа налево — что также заслуживает отдельного рассмотрения).
Альтернатива — вводим дополнительный байтик в ОЗУ с указателем смещения
v_point_ptr и инкрементируем/декрементируем как пожелаем. Ну и про его начальное значение не забываем в таком случае.
Далее вопрос про активацию дисплейного ключа...
Да наиболее универсальную и быструю.
Для таковой создаем байтик буфера
v_point_keyptr – собственно регистр или ячейка в ОЗУ
и начальную маску из бинарного кода B11111110 (или 01111111)
v_point_keymask
Для упрощения считаем, что и сегментный (v_seg_port) и позиционный (v_point_port) порты только нашим дисплеем и занимаются и настроены на вывод данных. Для усложнений относительно разнонастроенного порта, обслуживающего позиционные ключи позднее можно будет добавку дописать.
В итоге алгоритм несколько видоизменяется:
hard_init:
загружаем начальные значения в
v_point_cnt = N - количество позиций (режим предвывода с последующим декрементом
счетчика)
v_point_keyptr = v_point_keymask
v_ram_ptr = адрес метки начала массива v_ram
собственно заполняем массив сегментными комбинациями
инициализируем и запускаем таймер...
а программа обработки прерывания получает вид:
irq_displey:
v_seg_port = содержимому ячейки с адресом v_ram_ptr + v_point_cnt - загрузка
сегментного кода в сегментный порт
(или более упрощенный постинкремент/постдекремент указателя после
текущей загрузки)
v_point_port = содержимому v_point_keyptr - загрузка позиционного кода в
позиционный порт
Текущая позиция дисплея активирована
далее делаем обработку счетчика позиций
v_point_cnt = v_point_cnt — 1 (можно и версию «инкремента до совпадения» - но то
зависит от специфики команд ядра — например у ПИКовых таковое имеется)
если счетчик не исчерпан -
v_point_keyptr = v_point_keyptr сдвинутое влево/вправо — при условии, что это простой кольцевой сдвиг (ассемблер), если это СИ помним, что двигаем 0 в массиве окружающих единиц! Направление сдвига определяем выбранным направлением развертки дисплея и согласуем с выборкой/размещением данных в v_ram.
reti - выход из обработчика прерывания.
Если счетчик исчерпан -
v_point_cnt = N (исходному значению)
v_point_keyptr = v_point_keymask (исходному значению)
(если использовалось инкрементирование/декрементирование указателя начала массива видеопамяти v_ram_ptr то его также придется перезагружать исходным значением).
reti - выход из обработчика прерывания.
Вроде... работает...
Но как-то не комфортно...
Паразитная «ПОДСВЕТКА» в тех местах, где сегменты неактивны.
Особо действует на нерву/становится заметной ежли активны (высвечены) не все позиции на нашем дисплейчике.
ЗАСАДКА...
Дело в том, что смена комбинации сегменты/позиция происходит еще в старой позиции дисплея. А тут минимум несколько команд. Добавим еще и интервал времени на распространение сигнала как в сегментных так и в позиционных ключах. Оный надо высчитывать в каждом конкретном случае по даташитам применяемых внешних элементов. Да длину шлейфов... МНДЯаа...
Определяем таковой интервад по максимальной задержке распространения в самых медленных из имеющегося в схеме — оптронах типа 4N33 как 100 микросекунд...
Добавим на всяк случай еще микросекунд с 50... итогом «темная область» 150 микросекунд от сигнала «все выключено» или по сегментам или по позициям перед последующей сменой активных данных является ОБЯЗАТЕЛЬНЫМ дополнением для любого светодиодного индикатора на основе динамической развертки.
Вот только … Вставлять такой цикл задержки в само прерывание... Это уж слишком затратное по ресурсам МК дело.
Хотя... В такой ситуации для нашего предыдущего алгоритма добавим
v_cnt_mrak – счетчик циклов задержки (обычно 1 одноцикловая команда =1мкС)
v_mrak – собственно константа.
Можно и по другому — главное смысл —
сгенерировать задержку в 100-150 микросекунд.
А также добавляем константу
v_blank равную пассивному коду то-ли для позиционных ключей, то-ли для сегментных.
Для сегментных вроде удобнее, для позиционных — простое инверсное состояние к тому, что ранее было выведено или «общая заглушка».
Тогда вариант предшествующей тест-прожки будет иметь вид:
hard_init:
загружаем начальные значения в
v_point_cnt = N - количество позиций (режим предвывода с последующим декрементом
счетчика)
v_point_keyptr = v_point_keymask
v_ram_ptr = адрес начала массива v_ram
собственно заполняем массив сегментными комбинациями
инициализируем и запускаем таймер...
irq_displey:
v_point_port = v_blank гашение по позиционным ключам
v_cnt_mrak=v_mrak
timm:
v_cnt_mrak=v_cnt_mrak-1
v_cnt_mrak=0?
Если нет — повтор от timm отработать задержку времени
v_seg_port = содержимому ячейки с адресом v_ram_ptr + v_point_cnt - загрузка
сегментного кода в сегментный порт
(или более упрощенный постинкремент/постдекремент указателя после
текущей загрузки)
v_point_port = содержимому v_point_keyptr - загрузка позиционного кода в
позиционный порт
Текущая позиция активирована
далее делаем обработку счетчика позиций
v_point_cnt = v_point_cnt — 1 (можно и версию «инкремента до совпадения» - но то
зависит от специфики оманд ядра — например у ПИКовых таковое имеется)
если счетчик не исчерпан -
v_point_keyptr = v_point_keyptr сдвинутое влево/вправо — при условии, что это простой кольцевой сдвиг (ассемблер), если это СИ помним, что двигаем 0 в массиве окружающих единиц! Направление сдвига определяем выбранным направлением развертки дисплея и согласуем с выборкой/размещением данных в v_ram.
reti - выход из обработчика прерывания.
Если счетчик исчерпан -
v_point_cnt = N (исходному значению)
v_point_keyptr = v_point_keymask (исходному значению)
(если использовалось инкрементирование/декрементирование указателя начала массива видеопамяти v_ram_ptr то его также придется перпзагружать исходным значением).
reti - выход из обработчика прерывания.
Ну индикация фиксированного содержимого из буфера получилась.
Теперь бы добавить изменения в то сообщение. Да так, чтоб не было «нежданчиков» - ситуаций, когда замена содержимого попадает в произвольное место всей строки...
Уж больно неприятно такие относительно редкие моменты попадаются. Ощущения от них...
ФЕЕЕЭЭЭ....
Причина довольно банальна — часть строчки глаз запомнил от старого варианта, а часть пошла новая. Это при некоторых обстоятельствах приводит к ощущению «ярких вспышек» в произвольных точках дисплея и иным весьма неприятным ощущениям, портящим впечатление от работы индикатора.
Для того, чтоб подобное не происходило, замена данных (и/или их предварительная подготовка) производится независимо от работы программы обеспечения регенерации дисплея в отдельном буфере данных (v_ram_tmp), а замена содержимого буфера отображения (v_ram ) на новое производится перед началом следующего цикла развертки сканера строк дисплея.
При этом используется флаг запроса обновления данных (он же является флагом запрета работы основной программы с содержимым v_ram_tmp на время перезагрузки данных).
Программа основного задания подготавливает новые данные (сегментные коды) и устанавливает активный флаг
v_flags_quest (из регистра флагов v_flags).
По заверщении цикла обработки строки программа регенерации дополнительно опрашивает статус v_flags_quest.
Если обнаруживается, что
v_flags_quest = 1 (true)
производится перегрузка данных из
v_ram_tmp
в
v_ram
по завершении которой программа обработки/регенерации дисплея сбрасывает
v_flags_quest = 0 (false)
и выходит из прерывания.В принципе вполне работоспособный вариант.
В некоторых случаях можно тем и ограничиться и перейти к блоку «побочного эффекта» - обработчика клавиатуры.
Однако «по максимуму» лишние фрагменты разной длины в прерывании — штука весьма нежелательная.
Под ассемблером та проблема легко решается «условным возвратом» из прерывания через подстановку
адреса дополнительных фрагментов через стек с последующим reti.
По завершении «хвоста» командой ret проводится возврат в точку вызова прерывания по таймеру.
А вот как такой фокус без ассемблерных вставок в СИ соорудить?
Да еще не вникая в глубь специфики Сишного аппаратного стека?
Тем более, что в приличном модуле обработчика такие приемы есть жизненная необходимость на каждом шаге вывода позиции.
Иногда и по несколько раз за интервал отображения одной из позиций.
Ибо по условному возврату из прерывания разрешается работа других используемых основной программой прерываний при том, что «служебные хвосты» обработчика дисплея/клавиатуры могут достигать до половины тайм-аута индикации одной позиции дисплея.
Второй ГВОЗДЬ в отношении Сишного варианта -
В подобных алгоритмах практически всегда используются жестко определенные инлайновые таблицы в ПЗУ.
Это или готовые вектора переходов или поля указателей векторов/смещений по отношению к базовым указателям.
Работать в подобном режиме при помощи PROGMEM практически невозможно (см. ранее опубликованную «капельку дегтю»).
Третье касается только ардуиноIDE, но весьма существенно для «любителей плотной упаковки» с максимальным
использованием аппаратных ресурсов МК.
В самой IDE (описано в референсе) определен ряд функций, использующих аппаратные ресурсы — это и внутренний системный таймер и иные добавки, явно использующие аппаратные блоки — тот же вывод analogWrite() - PWM к примеру...
А описания всех таких моментов в отдельном виде не предоставлено — приходится по крупинкам собирать в разных источниках.
Вот и получается — полноценную прикладную программу с максимальным использованием аппаратных ресурсов и особенностей малолапых МК можно относительно легко написать только под «чистым ассемблером». А в более сложных случаях предпочтение комбинированному проектированию с использованием прикладных простых периферийных блоков и центрального МК «абстрактной обработки».
Ладно... влезать в глубины опционов компилятора и ассемблерных вставок не слишком хочется, а результат хош и «не очень» получить желаем...
Тогда есть еще один вариант...
Используем ту же динамическую развертку, но с dark_point - «темным/теневым позиционным фрагментом».
Собственно закладываем в цикл развертки отдельный участок, который занят не индикацией, а обработкой служебных подпрограмм самого дисплея.
Для наблюдателя это будет выражаться в некоторой потере яркости равноценной тому же дисплею с увеличенным количеством знакомест.
При том, что сам dark_point по длительности может быть равен паре обычных знакомест.
Тут главное не протупить с максимальной длительностью служебных подпрограмм, которые в том темном/теневом участке выполняться будут — предпочтительно не более ¾ его длительности. Зато можно уже вместо таблиц векторов вычислительные алгоритмы поставить будет.
Примерно так:
Планируем 4-х позиционку, добавляем еще два шага
итогом = 6 позиционный дисплей НО только с 4-мя действительными знакоместами.
0,016/6 = ~0,0026S примерно 64 Гц...
И в том дисплейчике у нас два служебных окна (или одно цельное — как работу прерываний по таймеру организовать).
А это уже минимум 2000 однотактовых команд (при 0,000001S на такт/команду) для математики и обработки...
Плюс такого алгоритма — используется только прерывание по таймеру и математика.
Минус — на время «темных дыр» работа иных прерываний запрещена — что соответственно вводит свои ограничения относительно основной программы устройства.
(Хотя... алгоритм на аппаратной начинке АВРкина Т1 мне гораздо приятнее как по возможностям, так и по ощущениям от его работы).
В связи с вышеизложенными размышлениями вероятнее всего под имеющиеся условия лучше подойдет версия с «теневым» обработчиком. Вроде у адуринок имеется библиотечка timer own... надо над тем дополнительно поразмышлять...