Привет, мир.
Долго я ходил вокруг да около: «наверное, надо написать статью» и «а зачем ещё одна, все и так всё знают про это». В конце концов подумал, что время пришло, решимости поприбавилось, звёзды сошлись и встал с подходящей ноги.
Сразу, на всякий случай: на плате вы можете увидеть мой старый сайт, который уже не существует, но уверяю вас, проектировал и собирал сам. Просто переосмыслил концепцию, можно сказать.
Так же: это — «расширенная и дополненная» версия моей же статьи с моего сайта.
Итак, часы. Делались они ещё в 2019-м году. С тех пор исправно ходят, показывают время, дату и температуру (про неё отдельно по ходу объяснений). Пару раз только заменил батарейку, которую на картинках не видно — она с нижней стороны платы в стандартном держателе для CR2032.
Почему выбрал VFD и конкретно ИВ-18? Во‑первых, с детства нравились ВЛИ, особенно на всяческих аудио‑/видеомагнитофонах («Маяк-232-стерео», «Электроника ВМ-12» и подобные). Во‑вторых, у меня как раз появилась пара таких «трубок» и, коль скоро с ними «вживую» дела иметь ещё не приходилось, то интересно было их «запустить». В‑третьих, к тому моменту была уже освоена «технология» работы с газоразрядными индикаторами (они же — nixie, ИН-12 и подобные) и выглядело для меня всё как «ну, не должно быть сильно сложнее».
Поразмыслив малость о концепции и о том, что хочу получитьна выходе, решил, что «базовые» требования будут примерно такие же, как и в сделанных до этого часах на ИН-12:
-
Часы должны работать от любого USB2.0, даже от самого минимально‑стандартного, с максимальным током на выходе в 0.5A и не перегружать его, чтобы «подключил к любому USB в пределах видимости и работает»
-
Сохранять ход времени при обесточивании
-
Иметь возможность более‑менее удобной настройки даты/времени не только с помощью программатора, правя код и перепрошивая
-
«Пищалку» для экспериментов с таймерами/ШИМ
-
И, вспоминая свой первый и единственный модем Acorp Sprinter 56K, возможность поуправлять часами с компьютера с помощью AT‑команд
Плюс ко всему этому: схема должна быть «самодостаточная», то есть, не зависеть от любых модулей. Конкретно для часов — повышающего DC‑DC преобразователя для анодов и сеток индикатора. Причин две: во‑первых, хотелось сделать всё самому — так интереснее и полезнее в качестве получения новых знаний, во‑вторых, если рассчитывать на какой‑то один из готовых и закладывать в схему — то надо поискать подходящий, работающий с нужными требованиями по току и напряжению, плюс габариты этого модуля и «дизайн»: на момент рисования схемы ещё не было полного понимания, как же хочется оформить готовые часы. Забегая несколько вперёд — как на титульной картинке.
Полистав популярный китайский магазин «всего и вся» и посмотрев схемы готовых DC‑DC преобразователей, даташиты используемых микросхем и прочего
на предмет сбора информации о том, что вообще используют для таких целей, нашёл подходящий вариант — NE555.
В итоге из главного «нам понадобятся»:
-
NE555 в качестве ШИМ‑контроллера для DC‑DC преобразователя
-
MAX6921AWI для коммутации анодов и сеток индикатора
-
Микросхема RTC DS3231 для, собственно, даты и времени
-
Микроконтроллер STM32F030F4P6 — для управления почти всем этим
Теперь, когда определился с основыми компонентами, можно остановиться на каждом из них подробнее.
Поначалу решил описывать всё идя от питания, как логично было бы начать. Но в тот момент, когда только начал проектировать схему, я исходил от имеющихся микроконтроллеров — STM32F030. Отсюда и основные мотивы выбора всего, связанного, в первую очередь, с управлением ИВ-18.
Общее питание
Как уже сказано выше, необходим только стандартный минимально возможный источник питания, который есть везде. Поэтому — USB. Из того, что стоит пояснить по плате — поставил разъём mini usb. Когда‑то закупился большим их количеством и куда‑то тратить нужно. Да и footprint уже имеется готовый — рисовать своё не надо.
Фрагмент схемы питания с минимальной обвязкой
Индикатор ИВ-18 и повышающий преобразователь
У ВЛИ всех типов и размеров есть несколько важных особенностей. Так как по внутреннему устройству они — суть те же радиолампы, только ориентированные на «видимость» во время работы, то для работы им нужны основное (анодное) напряжение, управляющее напряжение на сетки и напряжение накала.
Краткий принцип работы любого ВЛИ и короткая ремарка про ИВ-18
Для простоты допустим, что у нас есть, например, ИВ-3 — 7-сегментный индикатор с отдельной «точкой». Каждый сегмент и «точка» — это аноды. Ещё есть сетка, одна на все сегменты и «точку». Чтобы «активировать» какой‑либо сегмент (‑ы) (хоть один, хоть все разом), нам нужно на соответствующий вывод подать то самое анодное напряжение. И чтобы «зажечь» (или включить) их — подать это же (как правило) напряжение на сетку. При этом накал подключается к источнику накального напряжения — для нашего примера (ИВ-3) — это 0.85±0.15В. Один конец — это «плюс» накала, второй — общий (GND), к этому же GND подключается вся остальная схема.
ИВ-18 — это «сборка» из восьми подобных индикаторов, в одной колбе. Плюс два «служебных» сегмента на конце трубки (слева, если смотреть сегментами к себе) — это «кружок» и «тире» (или «минус»). Их я никуда не подключал (не придумал, как использовать), поэтому дальше по тексту они не упоминаются.
Из даташита (или, скорее, «этикетки» или «справочного листка») самые важные параметры:
-
Напряжение накала:
5 В -
Ток накала:
85±10мА -
Напряжение анода-сегмента:
50В -
Напряжение сетки импульсное:
50В
Накал
Первое и важное: напряжение и ток накала очень хорошо подходят под те самые «5В/0.5A любого стандартного USB2.0». То есть, накал индикатора подключил напрямую к USB, снабдив минимальным резистором последовательно для токоограничения на всякий случай.
Схема подключения накала ИВ-18 к USB
Чтобыне вешать что‑то «костылями» сверху/сбоку или не переделывать плату, решил добавить ещё резистор R13, чтобы организовать делитель, если для имеющихся ламп впоследствии 5В окажется слишком много.
По итогу оказалось, что конкретно для моего экземпляра индикатора резисторы не понадобились вообще. R12 заменил просто перемычкой, R13 не запаян вовсе.
Внимание
В отличие от «обычной» радиолампы, свечение накала не должно быть видно. Или видно совершенно минимально, на границе видимости глазами и только в темноте. Иначе нить накала перегорит очень быстро, особенно, если лампа уже была в употреблении какое-то время до этого. Особенно это касается «малогабаритных» ВЛИ с низкими напряжением и током накала типа того же ИВ-3 из спойлера про принцип работы.
Итого — с накалом всё достаточно прозаично. Дальше нужно организовать то самое анодное напряжение. И здесь ещё одно предупреждение, которое я считаю обязательным.
Анодное напряжение
Внимание: высокое напряжение
Если вы носите искусственный водитель ритма (кардиостимулятор) или имеете какие-то другие проблемы с сердцем, то работа с любым повышающим преобразователем, включая описанный ниже, может быть смертельно опасна для жизни! Не касайтесь токоведущих элементов при подключённом питании, особенно — выходных цепей преобразователя. Так же, выходной конденсатор будет сохранять свой заряд некоторое время после обесточивания схемы. Автор не несёт ответственности за ваши действия, всё написанное дальше описывается «как есть», на ваш страх и риск.
Повышающий DC-DC преобразователь построен на базе таймера NE555. Так как минимальное напряжение питания у него — 4.5В, то он без проблем будет работать от того же USB. И, если все номиналы подобраны верно и компоненты исправны, то «схема работает сразу, дополнительных регулировок не требует», как писали раньше в технической литературе.
По итогу, получился преобразователь с простой пороговой обратной связью на стабилитроне (предел на выходе ~51В благодаря D2 и сбросу скважности через Q2). Точной стабилизации напряжения нет, но для питания ВЛИ она и не нужна — главное, не дать выходу уйти выше предела. Индуктивность (L1) можно поставить практически любую в диапазоне 100..300мкГн «не глядя», скорее всего, заработает без изменения остальных номиналов. Сейчас физически установлена на 200мкГн.
Важно отметить, что тут нет и защиты от короткого замыкания, поэтому выход преобразователя коротить на GND лучше не надо, особенно, если питание идёт не от стандартного USB 0.5A. Стандартный USB-порт компьютера, вероятно, уйдёт в защиту раньше. Но если используется какой-то адаптер-зарядник — возможны варианты и что выйдет из строя первым, дроссель L1 или MOSFET Q1, — зависит от их запаса прочности.
Джампер XS1 стоит для отладки и быстрого отключения преобразователя при экспериментах, если что-то пойдёт не так.
Можно задаться так же вопросом, почему я не взял что-то популярное, вроде MC34063? Короткий ответ: за давностью лет уже не помню, почему именно отказался от него. Из обрывочных воспоминаний было что-то про подбор индуктивности под расчёт по даташиту (на NE555 ставится «любая из диапазона», на MC34063 — конкретный номинал по формуле), но врать не буду.
Осциллограммы вывода OUT (NE555), стока (drain, Q1) и общих пульсаций на выходе преобразователя
Уже в процессе подготовки этой статьи решил снять осциллограммы с реальной собранной платы — для иллюстрации, что «там происходит?».
Тут видно (Mi: -4.160В) отрицательный выброс напряжения при закрытии нижнего ключа выходного каскада NE555 и влияние паразитной ёмкости разводки и ёмкости затвора Q1.
Видно, как Q1 открывается и сток падает к земле (нижняя полка), потом закрывается и дроссель выбрасывает напряжение вверх (верхние импульсы).
Скорее всего, эти пульсации — результат работы переключения сегментов ВЛИ, а не самого преобразователя. Однако, утверждать наверняка не буду. Но, чтобы проверить, можно остановить вывод цифр на ВЛИ или остановить таймер.
Само собой, такие уровни пульсаций для ВЛИ вообще не критичны и можно ими пренебречь в принципе. А замеры провёл уже сейчас для дополнительной иллюстрации материала и себе — сравнить с будущими вариантами преобразователей в более «компактном» исполнении.
ИВ-18
Теперь из той же «этикетки» нужна цоколёвка индикатора. Они бывают двух типов: Тип 1 и Тип 2. Отличаются эти типы количеством «цифровых» разрядов — у типа 1 их 9, вместо 8 у типа 2. При этом на самом баллоне индикатора обычно не указано ничего, кроме наименования и даты производства (ИВ-18, 89 01 в моём случае) — поэтому, для определения конкретного нужно или посчитать количество разрядов, или посмотреть на «цоколь», где будут видны неподключённые выводы 6, 7 и 8 — всё ровно так, как указано в «назначении выводов». Кстати, вживую тип 1 ощущается и выглядит существенно длиннее (что логично).
Кратко про динамическую индикацию
Если читатель только начал погружаться в мир ВЛИ и динамической индикации, возможно, этот блок будет полезен.
Самое главное для начала, мне кажется, для понимания принципа, стоит проговорить явно: каждые одноимённые сегменты в индикаторе соединены параллельно. Это указано в «назначении выводов») как «… с 1-го по 8-й разряды». Например: разряд 8 (самый дальний от выводов), сегмент а — «верхняя горизонтальная черта» цифр (символов, знаков) 2, 3, 5, 6, 7, 8, 9 — исходя из таблицы — это вывод 12. Дальше нам нужно «выбрать» разряд, который мы хотим отобразить. Для разряда 8 это вывод 18 («сетка 8-го разряда»). То есть, подав анодное напряжение на вывод 12 и на вывод 18 — засветится один сегмент.
«Перебирая» таким образом сетки по очереди с достаточной частотой (от, примерно, 10 Гц), за счёт инерционности зрения, всё это будет выглядеть как одновременное свечение всех сегментов а.
Развивая идею для цифры 1 — нам нужны сегмент в и сегмент е. Это выводы 10 и 4 соответственно. Выставляем на этих выводах те самые 40В и перебираем (подаём по очереди напряжение 40В) сетки с 1-й по 8-ю (выводы 15, 22, 16, 21, 17, 20, 19, 18 — как в «назначении выводов»).
Дальше развивая последовательность с уточнением: выводы для каждой цифры приведены в том же порядке, в котором они будут перечисляться ниже в примере.
Например, для времени 14–23-25 получается последовательность:
-
Цифра 1 в разряде 8:
-
сегмент в — вывод 10, сегмент е — вывод 4
-
сетка разряда 8 — вывод 18
-
-
Подали напряжение на все три вывода (10, 4, 18)
-
Подождали 1 мс
-
Сняли напряжение со всех трёх выводов
-
Цифра 4 в разряде 7:
-
сегмент б — вывод 11, сегмент г — вывод 9, сегмент в — вывод 10, сегмент е — вывод 4
-
сетка разряда 7 — вывод 19
-
-
Подали напряжение на все пять выводов (11, 9, 10, 4, 19)
-
Подождали 1 мс
-
Сняли напряжение со всех пяти выводов
-
Символ — («минус«) в разряде 6:
-
сегмент г — вывод 9
-
сетка разряда 6 — вывод 20
-
-
…здесь так же остальные разряды
-
Переход на пункт 1
Следующая задача: нужно коммутировать как-то 17 выводов к ИВ-18 (аноды-сегменты и сетки). Беглый поиск выдаёт два варианта подобной коммутации: ULN2803, TD62783 и, если поискать чуть подольше, третий — MAX6921.
Очень кратко про сравнение ULN2803 vs TD62783
Для ULN2803 управление инвертированное (нужно подать 0 для включения — выход уходит в hi-Z, и через подтягивающий резистор сегмент получает +40В). В «активном» состоянии выхода (подали 1) сегмент притягивается к GND и гаснет. То есть, нужны ещё подтягивающие резисторы на каждый сегмент (или сетку). И, самое главное — при включении питания все сегменты вспыхивают до инициализации МК, так как на входах микросхемы — что попало.
И ещё про вывод COM. Микросхема создавалась далеко не для ВЛИ, а для управления соленоидами (реле, например) и моторами — то есть, индуктивной нагрузкой. COM используется для встроенных гасящих диодов на каждом из каналов. Применительно к ВЛИ — можно, в общем-то, COM не подключать, но «спокойнее» читаться будет, делая «по даташиту» — подключая COM к тому же питанию, что и нагрузку (здесь — ВЛИ).
Для TD62783 управление прямое (нужно подать логическую 1 для включения). При подаче питания на выходах — hi-Z, ничего не вспыхивает, выводы «выключены». Это уже удобнее и нагляднее. И сама микросхема — уже специализированная, как раз для управления ВЛИ.
Главный недостаток первых двух вариантов — это нужно три подобных микросхемы, потому что выводов 17, то есть (3 x 8) 24 вывода. А так как микроконтроллер — маленький (TSSOP-20), то логично напрашивается управление по SPI, что уже влечёт за собой использование сдвиговых регистров перед непосредственно коммутирующими микросхемами. То есть, ещё три микросхемы. А это уже — очень много места на плате, во-первых. А во-вторых — неоправданно усложняется разводка платы.
Поэтому остановился на MAX6921AWI. Микросхема спроектирована специально для подобных многовыводных ВЛИ, как следует из описания из даташита.
The MAX6921/MAX6931 are 20-output, 76V, vacuumfluorescent display (VFD) tube drivers that interface a multiplexed VFD tube to a VFD controller, such as the MAX6850–MAX6853, or to a microcontroller.
Собственно, единственный минус этой микросхемы — это цена и относительная редкость. На тот момент (2019 год) она стоила почти ровно в 10 раз дороже, чем TD62783, но этим можно пренебречь ради удобства и минимума дополнительных компонентов.
Вывод BLANK. На момент проектирования схемы я недостаточно внимательно прочитал про этот вывод. К моменту, когда разобрался, плата уже была разведена и отправлена в производство, так что переделывать не стал: BLANK напрямую соединён с GND, микросхема всегда включена на полную. Однако, можно было бы его подключить к какому-нибудь выходу таймера с ШИМ и менять яркость свечения индикатора — что вообще классический подход к такому в мире ВЛИ. Ну и просто включать/выключать программно. При выключении (BLANK — активный, логическая 1), кстати, микросхема переходит в shutdown-режим — значительно (по меркам времён активного применения ВЛИ) сокращает энергопотребление и сохраняет состояние сдвигового регистра.
Основное в коде
Ссылка на репозиторий, конечно, будет в конце. А здесь — небольшие отрывки для иллюстрации вышесказанного.
// Маски сетокstatic uint8_t VFD_GRIDS[] = { 0, // это "заполняющее" значение, чтобы удобнее было обращаться по номеру разряда (сетки) 0b00000001, //1 0b00000010, //2 0b00000100, //3 0b00001000, //4 0b00010000, //5 0b01000000, //6 0b00100000, //7 0b10000000, //8};
Сетки 6 и 7 как будто переставлены местами. Если посмотреть на массив: для разряда 6 стоит маска 0b01000000 (бит 6), а для разряда 7 — 0b00100000 (бит 5). Правильный порядок был бы обратный, но я при пайке перепутал два вывода ВЛИ и перепаивать не стал — лишний раз гнуть ноги индикатора не хотелось. Поэтому скомпенсировал это в коде — неудобно, но ладно. Один раз и для себя же.
// Маски сегментов#define VFD_SEG_A 0b00000010#define VFD_SEG_B 0b00000100#define VFD_SEG_C 0b00001000#define VFD_SEG_D 0b00010000#define VFD_SEG_E 0b00100000#define VFD_SEG_F 0b01000000#define VFD_SEG_G 0b10000000// А тут "собираю" каждую цифру из сегментов#define VFD_DIGIT_0 VFD_SEG_A | VFD_SEG_B | VFD_SEG_C | VFD_SEG_D | VFD_SEG_E | VFD_SEG_F#define VFD_DIGIT_1 VFD_SEG_B | VFD_SEG_C#define VFD_DIGIT_2 VFD_SEG_A | VFD_SEG_B | VFD_SEG_D | VFD_SEG_E | VFD_SEG_G#define VFD_DIGIT_3 VFD_SEG_A | VFD_SEG_B | VFD_SEG_C | VFD_SEG_D | VFD_SEG_G#define VFD_DIGIT_4 VFD_SEG_B | VFD_SEG_C | VFD_SEG_F | VFD_SEG_G#define VFD_DIGIT_5 VFD_SEG_A | VFD_SEG_C | VFD_SEG_D | VFD_SEG_F | VFD_SEG_G#define VFD_DIGIT_6 VFD_SEG_A | VFD_SEG_C | VFD_SEG_D | VFD_SEG_E | VFD_SEG_F | VFD_SEG_G#define VFD_DIGIT_7 VFD_SEG_A | VFD_SEG_B | VFD_SEG_C#define VFD_DIGIT_8 VFD_SEG_A | VFD_SEG_B | VFD_SEG_C | VFD_SEG_D | VFD_SEG_E | VFD_SEG_F | VFD_SEG_G#define VFD_DIGIT_9 VFD_SEG_A | VFD_SEG_B | VFD_SEG_C | VFD_SEG_D | VFD_SEG_F | VFD_SEG_G// Символ "минус" для разделения часов, минут и секунд#define VFD_CHAR_MINUS VFD_SEG_G// Символ "градус" (кружок) для температуры#define VFD_CHAR_DEGREE VFD_SEG_A | VFD_SEG_B | VFD_SEG_F | VFD_SEG_G// Символ "С" (градусов Цельсия) для той же температуры#define VFD_CHAR_C VFD_SEG_A | VFD_SEG_E | VFD_SEG_D | VFD_SEG_F// И массив готовых цифрstatic uint8_t VFD_DIGITS[] = {VFD_DIGIT_0, VFD_DIGIT_1, VFD_DIGIT_2, VFD_DIGIT_3, VFD_DIGIT_4, VFD_DIGIT_5, VFD_DIGIT_6, VFD_DIGIT_7, VFD_DIGIT_8, VFD_DIGIT_9};
В коде сегменты обозначены латиницей (VFD_SEG_A..G) — тут я использовал уже стандартную нотацию. В тексте статьи и на цоколёвке ИВ-18 — кириллица (а..ж).
Маска начинается с 0b00000010 (VFD_SEG_A). Бит 0 — это точка (сегмент и). Его в объявлении сначала не дописал, а потом он так и остался «хардкодом» (дальше в коде).
// Буфер, который шлём по SPIuint8_t vfdBuf[3]; void showDigit(const uint8_t grid, const uint8_t digit, const uint8_t showDot) { vfdBuf[2] = VFD_GRIDS[grid]; vfdBuf[1] = VFD_DIGITS[digit]; vfdBuf[0] = showDot; HAL_SPI_Transmit(&hspi1, &vfdBuf[0], sizeof(vfdBuf), 1);}void showMinus(const uint8_t grid) { vfdBuf[2] = VFD_GRIDS[grid]; vfdBuf[1] = VFD_CHAR_MINUS; HAL_SPI_Transmit(&hspi1, &vfdBuf[0], sizeof(vfdBuf), 1);}static inline void showDegree(const uint8_t grid) { vfdBuf[2] = VFD_GRIDS[grid]; vfdBuf[1] = VFD_CHAR_DEGREE; HAL_SPI_Transmit(&hspi1, &vfdBuf[0], sizeof(vfdBuf), 1);}static inline void showCelsius(const uint8_t grid) { vfdBuf[2] = VFD_GRIDS[grid]; vfdBuf[1] = VFD_CHAR_C; HAL_SPI_Transmit(&hspi1, &vfdBuf[0], sizeof(vfdBuf), 1);}
Заполняем буферы нужными данными и отправляем по SPI.
Нужно отправить три полных байта (24 бита) вместо положенных 20 — SPI в STM32 работает с целыми байтами, а 4 старших «лишних» бита MAX6921 просто «пропустит»: в момент LOAD защёлкнутся только последние 20, а более ранние данные вытолкнутся из сдвигового регистра.
Ещё момент. В showDigit есть vfdBuf[0] = showDot, а в showMinus, showDegree, showCelsius — нет. По коду это значит, что в этих функциях бит 0 остаётся от предыдущего вызова — это баг (хотя и рабочий — минус/градус всегда вызываются там, где точка не нужна, и она там не зажигается, но только потому, что до этого последний showDigit был с showDot = 0). Это, конечно, не правильно и следует явно обнулять ненужное.
Так же, для чистоты и правильности кода следовало бы объявить vfdBuf как static, но, так как она используется только в одном месте, то работает.
#define MODE_SHOW_TIME 1#define MODE_SHOW_DATE 2#define MODE_SHOW_TEMP_DAY 3static volatile uint8_t blinkers;static volatile uint8_t currentMode = MODE_SHOW_TIME;int main(void) { // тут стандартная инициализация всего и вся uint8_t h; uint8_t l; while (1) { switch (currentMode) { case MODE_SHOW_TIME: h = ds3231_Time.hour / 10; l = ds3231_Time.hour % 10; showDigit(8, h, 0); HAL_Delay(1); showDigit(7, l, 0); HAL_Delay(1); if (blinkers != 0) { showMinus(6); HAL_Delay(1); } h = ds3231_Time.minutes / 10; l = ds3231_Time.minutes % 10; showDigit(5, h, 0); HAL_Delay(1); showDigit(4, l, 0); HAL_Delay(1); if (blinkers != 0) { showMinus(3); HAL_Delay(1); } h = ds3231_Time.seconds / 10; l = ds3231_Time.seconds % 10; showDigit(2, h, 0); HAL_Delay(1); showDigit(1, l, 0); HAL_Delay(1); break; case MODE_SHOW_DATE: h = ds3231_Date.date / 10; l = ds3231_Date.date % 10; showDigit(8, h, 0); HAL_Delay(1); if (blinkers != 0) { showDigit(7, l, 0); } else { showDigit(7, l, 1); } HAL_Delay(1); h = ds3231_Date.month / 10; l = ds3231_Date.month % 10; showDigit(6, h, 0); HAL_Delay(1); if (blinkers != 0) { showDigit(5, l, 0); } else { showDigit(5, l, 1); } HAL_Delay(1); showDigit(4, 2, 0); HAL_Delay(1); showDigit(3, 0, 0); HAL_Delay(1); h = ds3231_Date.year / 10; l = ds3231_Date.year % 10; showDigit(2, h, 0); HAL_Delay(1); showDigit(1, l, 0); HAL_Delay(1); break; case MODE_SHOW_TEMP_DAY: h = ds3231_Temp.temp_1 / 10; l = ds3231_Temp.temp_1 % 10; showDigit(8, h, 0); HAL_Delay(1); showDigit(7, l, 0); HAL_Delay(1); showDegree(6); HAL_Delay(1); showCelsius(5); HAL_Delay(1); showDigit(1, ds3231_Date.day, 0); HAL_Delay(1); break; } }}
Про volatile на всякий случай
Переменные меняются в прерывании таймера и читаются в основном цикле (потоке) main. Без volatile компилятор может закэшировать их в регистре и не увидеть обновление.
blinkers — просто маленький счётчик (инкрементируется и обнуляется в прерывании таймера, ниже по тексту). Чтобы мигать разделителями: «минусами» для ЧЧ‑ММ‑СС и «точками» (тот самый сегмент и) для ДД.ММ.ГГГГ, чтобы показать, что часы «идут», а не зависли. Ну и красиво.
currentMode — точно так же переключается в том же прерывании таймера, так же можно переключать вручную кнопкой BTN_3 (тоже ниже по тексту).
HAL_Delay(1). Блокирующая задержка на каждый символ (разряд) в 1мс, то есть 8мс на полный «кадр» — для глаза это совершенно незаметно. Подобная (блокирующая) задержка тут не несёт каких‑то дополнительных проблем, потому что главный цикл больше ничего и не делает, а чтение DS3231 происходит в том же прерывании таймера (ниже по тексту). И ради «абсолютной правильности и красоты» не стал переусложнять код. Опять же, делалось для себя, а сам с собой я как‑нибудь договорюсь.
Отображение года — последние две цифры, то есть, 20YY. Вряд ли мне понадобится выставлять 19YY или 21YY.
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { switch (GPIO_Pin) { case BTN_1_Pin: // TODO: select next position within current settings mode (see MODE_SETTINGS_* and selectedPos in this file). break; case BTN_2_Pin: // TODO: change value at selectedPos (increment / decrement depending on long/short press, with debounce). break; case BTN_3_Pin: currentMode++; // TODO: extend upper bound to MODE_SETTINGS_LAST_POSITION when settings UI (BTN_1 / BTN_2 handling) is implemented. if (currentMode > MODE_SHOW_TEMP_DAY) { currentMode = MODE_SHOW_TIME; } break; }}
Нажатия кнопок обрабатываются по прерыванию. В изначальном плане BTN_1 и BTN_2 должны были управлять режимом настройки даты и времени — навигация по позициям и изменение значений, а BTN_3 — переключать режимы отображения и долгим нажатием входить в настройку. Но руки до этого так и не дошли. В текущем виде работает только BTN_3 для переключения режимов, а время и дата выставляются через закомментированный вызов DS3231_SetDateTime() в начале main() (раскомментировать, прошить, выставить, закомментировать обратно, прошить заново — не самый удобный процесс, но и не самый частый, в общем-то).
#define MODE_TIMEOUT_TIME 45 // таймаут отображения времени (45 "тиков" таймера)#define MODE_TIMEOUT_DATE 5 // таймаут отображения даты (5 "тиков")#define MODE_TIMEOUT_OTHER 5 // таймаут отображения всего остального (номер дня недели и температура, 5 "тиков")static volatile uint8_t modeTimeout = 0;static volatile uint8_t halfSeconds = 0;void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { blinkers ^= 1 << 1; modeTimeout++; halfSeconds++; if (halfSeconds == 2) { DS3231_ReadData(); halfSeconds = 0; } switch (currentMode) { case MODE_SHOW_TIME: if (modeTimeout == MODE_TIMEOUT_TIME) { modeTimeout = 0; currentMode = MODE_SHOW_DATE; } break; case MODE_SHOW_DATE: if (modeTimeout == MODE_TIMEOUT_DATE) { modeTimeout = 0; currentMode = MODE_SHOW_TEMP_DAY; } break; case MODE_SHOW_TEMP_DAY: if (modeTimeout == MODE_TIMEOUT_OTHER) { modeTimeout = 0; currentMode = MODE_SHOW_TIME; } break; }}
То самое прерывание таймера. Таймер «тикает» с частотой 2Гц, то есть, каждые полсекунды (500мс) переключаются мигающие элементы («точки» или «минусы») и каждую секунду опрашивается DS3231. То есть, время отображается примерно 22 секунды, и примерно по три секунды на дату и всё остальное.
blinkers задумывался как битовая маска на все 8 разрядов индикатора: каждый бит отвечает за мигание своего разряда в режиме настройки времени и даты. До реализации этого, как выше упоминал, руки не дошли, но тип переменной и маска через 1 << n остались.
Плюс, чтобы не загромождать код, я убрал тут #ifdef’ы, в которых была отправка даты/времени по UART (они есть в репозитории).
Про температуру. Это — температура внутреннего сенсора в DS3231, который им самим используется для термостабилизации хода часов. А так как чип греется при работе выше, чем температура окружающего воздуха, то она скорее символическая, нежели реальная — зато можно оценить температуру кристалла внутри корпуса. И для «пересчёта» в комнатную можно смело отнимать пару градусов.
Выглядит сейчас это вот так. Само собой, вживую всё гораздо «статичнее» — без «мерцания» и «ряби».
Весь остальной код — самый «стандартный» для любого подобного проекта и отдельного упоминания, как по мне, не стоит.
Фото плат
Самих плат, традиционно после заказа, осталось ещё 4 штуки
В начале упоминал AT‑команды. До реализации этого тоже так и не дошёл, но на плате разведён и распаян CH340. Возможно, таки доберусь до этого тоже, вместе со всеми режимами.
Корпус. Так и не придумал за это время, что бы сюда подошло лучше всего. Однако, в таком виде, по‑моему, тоже смотрится хорошо и решил так и оставить. Хотя индикатор, конечно, «висит» только на своих жёстких выводах и, по‑хорошему, нужно с этим что‑то сделать. Но, если не тормошить, не поднимать за него и не гнуть выводы — всё хорошо.
Полные исходники и схема — в репозитории на Github. Единственное, чего нет — это разводки платы — где‑то потерялись. Если найду, то доложу в репозиторий же.
Ещё несколько фото
На этом, пожалуй, с этими часами всё. Если кому это пригодится — буду рад. Следующая статья, если кому‑то будет интересно, может называться «Плеер psg‑файлов на AY-3-8910 + STM32».
Спасибо, что дочитали.
ссылка на оригинал статьи https://habr.com/ru/articles/1025908/