Создание контроллера вторичных часов “Воронеж”

Отладочный стенд.

Многие из вас на предприятиях и иных объектах бывшего СССР встречали зелёные сегментные часы. Были самостоятельные устройства, типа “Электроника-7”, но так же существовали и вторичные, как в предыдущем посте, только более сложного строения — часы семейства “Воронеж”. Они также включались в единую часовую сеть предприятия и показывали то время, которое передавалось от главных первичных часов по проводам.

Решил разобраться, как же это работало, и самостоятельно собрать первичные часы. Из-за отсутствия документации, информации по данной теме, а также живых первичных часов — это превратилось в настоящий квест, реверс-инжиниринг, навязчивую идею, боль страдание и бессонные ночи с кодом и осциллографом в обнимку.

Тем не менее, я смог их победить, и могу теперь рассказать, как же они работают и поделиться кодом своего проекта.

Как всё начиналось

Если вы помните, в моём посте “Охота на блошках 2.0 — Уделка” приводил фотографию разбитых вторичных часов. Меня поправили и сказали, что это обычные часы “Электроника-7”, и ценности они не представляют. Но хотелось приключений, и будучи в Санкт-Петербурге, я купил первичные часы и вторичные часы “Воронеж”, в надежде, что их можно будет поженить (как же сильно я ошибался).

Достаточно быстро я выяснил, что протоколы у них несовместимы и работать вместе они не будут. В результате, для начала разобрался как же работают первичные часы “старого типа”, что вылилось в пост “Разбираемся с работой часовых сетей в лабораторных условиях”, а потом приступил к часам “Воронеж”. И, как оказалось — это было верное решение. На ловца и зверь бежит!

Поиск информации и разъёмов

Шильдик

Если начать искать информацию по запросу “вторичные часы Воронеж” или по маркировке “ВЧЦ1-С2ПГ12К-80”, то, за исключением ЖЖ sfrolov, информации практически нет. Более того, встречаешь форумы, где Сергей спрашивает о протоколе часов, но без ответа.

В результате одним из кладезей информации стал его пост: “Вторичные часы «Воронеж”. Сергей, также позднее сделал собственный контроллер этих часов. Но, как я понял по форумам, где люди спрашивали о таймингах, Сергей не был готов делиться информацией. Поэтому я решил двигаться самостоятельно, хотя не скрою, что Сергей мне косвенно помог, своим постом, и тем, что в личной беседе сказал, что его посылки сигналов в посте отличаются от тех, что посылают оригинальные первичные часы.
Как я уже сказал, на ловца и зверь бежит. Для моего прошлого поста, уже в Москве я докупал вторичные часы. С продавцом разговорились, и оказалось, что у него есть первичные часы для “Воронежа”. К сожалению, мне не удалось уговорить продавца, чтобы я смог снять живые сигналы с его часов, предлагая деньги. Однако, продавец всё же, отфотографировал паспорт часов, где были бесценные эпюры и описание их работы

В общем, я разобрал все эти фотографии, немного обработал и сделал отдельную pdf, чтобы другие люди, которые пойдут по моим стопам, смогли её использовать в своих целях. Фактически, то что я делал дальше, основывалось только на этих данных.

Последнее, чего мне не хватало для полного счастья, чтобы начать работать с часами — это разъёма питания. Он оказался достаточно хитрым, хотя мне казалось что где-то я видел такие разъёмы, но на других приборах.

Не хотелось делать времянку из автомобильных клемм (да и опасно это с 220 вольтами). Поэтому долго искал варианты. Тут я просто кинул клич среди друзей, в том числе написал BootSector и он даже нашёл у себя в закромах таковой разъём, но что-то мы так и не состыковались. В одном из чатов, на помощь пришёл sfrolov, поняв, для чего я ищу. И дал ссылку на “Мешок”, где я и заказал жмень таких разъёмов.

Жмень разъёмов.

Внутри разъём оказался очень интересно устроен — сама прижимная клемма выполнена из нескольких наборных пластин, так что их нужно спаивать между собой. Если кому нужен такой разъём, у меня ещё есть в запасе.
В результате я получил вот такой аккуратный провод.

Пардон за жёлтую термоусадку, но чёрной не было, а дело было в ночи. В результате, всё готово, чтобы приступить к экспериментам.

Формат посылки

Располагая документацией, всё стало немного веселее. Если кому интересно, настоятельно рекомендую читать её всю (и по несколько раз, до просветления), но я приведу главные тезисы.

Перевожу с инженерно-советского на русский: каждые 20 мс у нас идут синхроимпульсы (СИ), между ними идут шесть пачек тактовых импульсов (ТИ), с интервалом 33,(3) мс и после них идёт двоичный код посылки десятков часов, единиц часов, десятков минут, единиц минут, десяток секунд, единиц минут. Единственное, что я не понял — это информационный импульс (ИИ). После 15 прочтений документации, яснее не стало. Проще взглянуть на картинку эпюр сигнала.

Если вы ещё ничего не поняли, не печальтесь, я тоже ничего не понимал долгое время.

Что из этого графика у нас есть: это формы сигналов, общая длительность посылки (20 мс), и то что сигнал у нас двухполярный (минус, ноль и плюс).

Что у нас нету: амплитуды сигнала, длительность одиночного импульса, и что же такое, ИИ? Будем честны, на последний вопрос у меня до сих пор нет толкового ответа.
Из графика выше, совершенно непонятно какое время выводится и документация, в сущности не даёт ответа на вопрос. Принципиальной электрической схемы устройства нет.
Поэтому придётся обратиться к посту sfrolov и взять оттуда картинку. Ниже процитирую информацию, которая оказалась мне полезной.

Сначала идёт длинный импульс сброса номера позиции отрицательной полярности (1). Такие же импульсы, но короткие (2) сбрасывают счетчик номера цифры.
Импульсы (3) как-то связаны с импульсами сброса (2) и должны идти друг за другом. Наверно чтобы отделить длинные импульсы от коротких. Номер цифры задается количеством счетных импульсов (4). Сколько импульсов придёт, такая цифра и отобразится. На осциллограмме можно увидеть, что закодировано время 22:09.
Если импульсов (2) не ставить, то на следующей позиции номер цифры будет прибавляться к предыдущей. То есть если предыдущая цифра была „3“, а на следующую позицию пришло еще два импульса (4), то зажжется цифра 5.
После пачки импульсов необходимо выдержать паузу, по окончании которой предыдущая цифра сменится той, которая пришла. То есть пока обрабатывается текущая цифра, на экране отображается предыдущая.

Внимательный тот, кто заметит, что на картинке в посте одни тайминги, а на видео там же другие. Однако, буду лукавить, если скажу, что не пробовал повторить этот сигнал, и да, я его повторил, но результат мне не понравился. Поэтому мы пойдём другим путём.

Из картинки выше, самое ценное, что сигнал ИИ (по паспорту), он же (2) (по картинке) идёт перед посылкой импульсов. Дальше уже дело техники сформировать данную посылку.

Электрическая часть

Одна из самых сложных задач было сформировать сигнал трёх напряжений: отрицательной полярности, нуля и положительной. Плюс, мне было неизвестно рабочее напряжение посылки. Эмпирическим путём было установлено, что мои часы работают, начиная от шести вольт. Поэтому я выбрал рабочее напряжение 12 вольт.
Когда только принёс часы домой, очень хотелось проверить, работают ли они вообще, поэтому провёл весьма варварский эксперимент: поочерёдно подключал то плюс, то минус, и получил такую картину.

Из чего стало понятно, что мне не требуется хитрого многополярного питания, а достаточно, как и в прошлой статье использовать Н-мост, например, драйвер двигателя L9110S. И в начале отладки, я даже использовал ту же плату, которой тактировал часы “Стрела”. Единственное, что на плате драйвера надо снять конденсатор.

Если этого не сделать, то на выходе будет получаться очень забавная арабская вязь.

Арабская вязь.

Однако, после шести сгоревших драйверов (на каждой платке их два), я понял что нужно что-то делать. Они горели как свечки. Оторвался провод питания — сгораем. Подключаем по USB, вместе с внешним блоком питания — горим с пламенем (я ни разу до этого не видел таких фейерверков). Поэтому, я решил отказаться от китайских плат, и купил себе нормальные микросхемы IR4427 с адекватным током работы и потолком напряжения выше 12 вольт. И всё стало сильно стабильнее.

Жаль, что микросхема pin to pin несовместима с L9110S, так бы поставил прямо на ту же плату, но увы.

Макетка с IR4427.

В качестве часов я выбрал модуль часов реального времени DS3231, и для отображения готовый модуль дисплея для Arduino. За основу потом взял Arduino Uno, её же питал от 12 В, и с неё же брал эти 12 В для драйвера.
На самом деле, работать с часами и дисплеем просто, примеров в интернете вагон, эта часть кода совершенно неинтересна. А вот сделать правильный сигнал — это важно, и совершенно не принципиально на какой плате вы это реализуете.

Его величество код!

Код, радикально (полностью) я переделывал три раза. Это выстраданная глава. В первую очередь, расскажу то, что может быть полезно и в других задачах. Скажу сразу, что имел глупость использовать среду Arduino, где очень многое скрыто под капотом. В какой-то момент я даже думал плюнуть и переписать на тёплом ламповом си, где всё прозрачно, но пока сдержался, потому что сама отладка тоже отняла слишком много времени.

▍ Отладочный код

Итак, наша задача — это формировать сигнал, с заданными длительностями. В качестве отладки сделал такую структуру.

struct pulse_package { uint8_t direction; uint16_t	delay_t;
};

Первая переменная — направление передачи, которое может принимать три значения:

#define POS_SIG 3
#define NEG_SIG 2
#define ZER_SIG 0

Где, 3 и 2 — соответствующие пины, нуль — отсутствие пина. delay_t — это переменная длительностей в микросекундах, соответственно она будет передана в функцию delayMicroseconds(), которая на вход принимает переменную типа uint16_t и со значением до 16383 (это важно, чтобы не выйти за рамки допустимых значений).Меня в этой структуре смущает, что она не выровнена в памяти (элемент занимает 3 байта), и занимает драгоценную оперативную память, но я прикинул, что её должно хватить. По хорошему, стоит подобные структуры делать константой и размещать в памяти программ.

Сама отправка сигнала осуществлялась функцией, которая на вход принимала указатель на массив структур.

void send_to_voronezh(struct pulse_package *to_send) { for (int i = 0; to_send[i].delay_t != 0 ; i++) { switch(to_send[i].direction) { case POS_SIG: set_posi_sig(); break; case NEG_SIG: set_nego_sig(); break; case ZER_SIG: set_zero_sig(); break; default: break; } delayMicroseconds(to_send[i].delay_t); }
}

О функциях set_posi_sig(), set_nego_sig(), set_zero_sig() я расскажу чуть позднее, но и так понятно что тут происходит.

Формирование самого массива структур делаю прямо в функции loop():

void loop() { static struct pulse_package pulse_package_send [] = { {NEG_SIG,	LONG_NEG},	//Синхроимпульс №1
// {ZER_SIG,	LONG_AFTER_LONG_NEG},	//нулевой импульс {POS_SIG,	SHORT_P},	//импульс №3 {NEG_SIG,	SHORT_P},	//импульс №2 //1 {POS_SIG,	SHORT_P},	//импульс №4 {ZER_SIG,	LAST_LONG-SHORT_P},	//нулевой импульс между посылками
.... }; send_to_voronezh(pulse_package_send);
}

Как видно, можно легко, в текстовом виде сделать сигнал с заданными таймингами, практически любой длительности (сколько позволит память вашего контроллера). Это не прям Real Time Output, но лучше, чем ничего.
В результате на выходных пинах контроллера 2 и 3 (простите меня за ардуиновские обозначения), мы получим, например, вот такой сигнал:

Могу сразу сказать, что это неработающая осциллограмма, но принцип уже хорошо виден. Жёлтый график — это сигналы отрицательной полярности, синей — положительной. Тут попытка вывести 22:09.

Вывод в порт осуществлялся следующими инлайновыми функциями (для экономии тактов процессора, они нам ещё пригодятся).

inline void set_zero_sig() __attribute__((always_inline));
inline void set_posi_sig() __attribute__((always_inline));
inline void set_nego_sig() __attribute__((always_inline)); void set_zero_sig() { digitalWrite(POS_SIG, HIGH); digitalWrite(NEG_SIG, HIGH);
} void set_posi_sig() { digitalWrite(POS_SIG, LOW);
} void set_nego_sig() { digitalWrite(NEG_SIG, LOW);
}

Чем удобно вынести всё это в отдельные функции? А тем, что всё это можно заменить printf с печатью сигнала на экране, и получить готовый консольный осциллограф (берите на заметку те, у кого нет осциллографа). Для примера одна функция, остальные аналогичные:

void set_zero_sig() { if ( -1 == signal_stage) { printf("- \n"); } if ( 1 == signal_stage) { printf(" -\n"); } signal_stage = 0;
}

В результате можно получить такой симпатичный график, который удобно пролистать.

Смех, смехом, но именно таким образом я потом отлаживал реализацию с помощью таймеров. Оказалось очень удобно, и легко отлаживать. Всё проще, чем каждый раз компилировать, отключать контроллер, подключать его обратно и смотреть осциллографом.
Отлаживать удобнее всего оказалось на 00:00:00, что логично, так как нет лишних импульсов. Самая беда начинается на цифре восемь и девять. Ниже покажу видео.

Сформированный программой сигнал.

И да, этот сигнал оказался рабочим, хотя период между синхроимпульсами составляет всего 12 мс (вместо положенных 20).

Часы стабильно показывают время.

На фото видно, что ещё используется L9110S (он сгорел, буквально через пару часов), а также пока что не подключен дисплей и часы.

Очень важный момент, выше — это отладочный код, который не попал в основную программу, но дал понимание, куда надо идти.

▍ Подключаем дисплей, клавиатуру и часы

Подключение дисплея 1602, клавиатуры на АЦП и часов достаточно простое и примитивное занятие — это примеры из интернета, и я даже не буду заострять на них внимание. Но важно другое, что суммарно опрос клавиатуры, запись значения в дисплей, получение времени с DS3231 занимали 16 мс! Что, ну никак не укладывается в последовательное выполнение программы. Поэтому необходимо было создать отдельный “поток” программы, который бы выполнял код. Поток, здесь скорее в кавычках, так как это не полноценные потоки в понимании, с переключением контекста, хотя на AVR я такое реализовывал, и это вполне реально. В этом случае, одну из функций нужно вынести в отдельный таймер.
Долго думал, что же вынести, всё же решил вынести самое критичное по таймингам — это функцию вывода сигнала. И в результате код был полностью переписан. Это была уже третья итерация. Промежуточную можете посмотреть по коммитам на гите.

Как вы помните, во многопоточном программировании, если вы работаете с разделяемой переменной, вам необходимо её ограждать мьютексами, либо семафорами, либо другим иным способом синхронизации потоков. Тут полностью такая же ситуация. В данном случае, разделяемая переменная у нас — это структура содержащая дату и время. Разделение доступа к переменный осуществляется очень просто, в качестве мьютекса у нас выступает запрещение прерываний на время записи в переменную. Таким образом, пока идёт запись структуры, эта операция будет полностью атомарной. После разрешения прерываний, если прерывание поступило в этот момент, оно будет обработано.

int settime () { tmElements_t l_time = {0};
... RTC.read(l_time); //read time from DS3231 noInterrupts(); m_time = l_time; //send time to output function interrupts();

Так делается в двух местах программы.

Функцию вывода сигнала отлаживал очень долго, и очень пригодился вариант с помощью printf. Вообще, в больших программах, можно сделать это всё дефайнами и делать универсальный код для отладки на компе и работе в контроллере.
Как выше было сказано, для вывода, я сделал отдельную функцию таймера. Она вышла просто монструозной, и кто сможет разобраться, что в ней происходит, тому дам конфетку. Приводить её сюда целиком не буду, однако обращу внимание на некоторые моменты. Таймер дёргается с интервалом 50 мкс (то есть, я могу делать импульсы не короче этого периода). Для того, чтобы минимизировать время выполнения функции, и не мешать основному коду программы, везде, где только было можно, использовал “запрещённый” оператор goto. Но если вы откроете код ядра линукс, особенно драйвера, то увидите, что он используется там постоянно. В общем, это можно делать, но с умом.

ISR(TIMER1_COMPA_vect) {
... if (_delay >= TIMER_PERIOD) { _delay-=TIMER_PERIOD; goto exit_isr; } else { _delay = 0; } if (0 == pkg_stage) { SET_BIT(pkg_stage, H_SYNC_PULSE); //h-sync pulse stage numbers[0]	= m_time.Hour / 10;
... numbers[5]	= m_time.Second % 10; //first pulse set_nego_sig(); _delay = PULSE_DURATION; goto exit_isr; } else { set_zero_sig(); if (CHK_BIT (pkg_stage, H_SYNC_PULSE)) { CLR_BIT(pkg_stage, H_SYNC_PULSE); SET_BIT(pkg_stage, L_SYNC_PULSE); //l-sync pulse stage set_posi_sig(); _delay = PULSE_DURATION; goto exit_isr;
...
exit_isr: return;
}

Таким образом, функция таймера — это машина состояний, которая даёт нужный сигнал, в зависимости от того, каком состоянии сейчас находится. Могу честно сказать, что я треснул умом, пока сделал её.

Отладка.

Какая же форма сигнала правильная?

Скажу сразу, что я до сих пор не знаю ответа на этот вопрос. Ниже покажу, те формы, которые оказались рабочими.
Изначально, я использовал большие паузы между посылками, и в целом это было хорошим решением, но мне хотелось большего, и проверить в каких режимах будет работать, и может подобрать более подходящий.

Форма сигнала с большими паузами. Тут СИ, ТИ и ИИ.

В результате я решил добиваться формы эпюр, которые были изображены в паспорте. Как видно, что там сигналы переходят друг в друга, без нулевой паузы между ними. Изначально, все мои сигналы следовали друг за другом с определённой задержкой, но я отказался от такого решения, потому что работает и без неё. Плюс, изначально сигнал синхроимпульса был длиннее, чем остальные сигналы, как оказалось, что и это не важно. На данный момент начало сигнала выглядит таким образом (пока не смотрите на время развёртки — это не принципиально).

Начало посылки.

1 это синхроимпульс (СИ), один на каждую пачку посылок, далее он без паузы переходит в 3 тактовый импульс (ТИ), который, также без пауз, переходит в 2 — информационный импульс (ИИ). После чего, также без пауз идёт посылка часов, уже разделённая паузами (тут 2 десятка часов и 1 единица). В посылке единиц часов всё тоже самое, но без СИ.

Вот график, как выглядит такая посылка целиком.

Полная форма сигнала. Время: 16:49:38.

Теперь главный вопрос: какие длительности импульсов и нужна ли пауза после посылки каждой цифры? Если без паузы, то длительность импульса я взял 150 мкс, и следующий импульс начинал после истечения времени всех пульсов, но это привело к забавным артефактам. Проиллюстрирую в видео.

Как видно, такое решение нельзя назвать рабочим. В результате, на данный момент я остановился на предельном значении импульса, равное 50 мкс, и после посылки делаю паузу, равную 2700 мкс, за вычетом отправленных импульсов и пауз между ними. В результате сейчас всё выглядит следующим образом.Да, работает не идеально, и есть ещё куда расти.

Резюмируя

Да, я смог докопаться и понять алгоритм работы часов. Практически каждый день, на протяжении месяца я ковырялся с этими часами, всё свободное время я думал, как улучшить код, как сделать его оптимальнее и элегантнее. В качестве награды — часы наконец-то показывают время.

На фото видно, что секунды светятся ярко, а остальные цифры имеют отдельные яркие точки, но остальные точки тусклые. Это не ошибка фотосъёмки, это так и выглядит.

Но, вы сами видите на видео, что в режим работы часов я так и не попал. Вам на фотографиях кажется, что яркость часов недостаточная — это не кажется, они в действительности светят странно, каждый отдельный сегмент имеет разную яркость. Я пробовал разные тайминги, разные режимы работы (даже те, о которых тут не рассказал), игрался с ними по-всякому — всё равно свечение сегментов неяркое и есть блики. Допускаю также, что конденсаторы внутри требуют замены, но, вероятнее всего, я делаю что-то не так. Пока рано делать выводы.

P.S.

Если у кого есть первичные часы ПЧЦ1-БИТ-М и есть возможность снять с них осциллограмму (в Москве могу сам приехать), буду очень признателен! Пока что я в тупике.

Ссылки:

Читайте так же: