Всем привет!

Перебирая старые запасы на работе, я наткнулся на вакуумные индикаторы, среди них были несколько ИВ-11 и ИВ-6. Выкидывать такую винтажную красоту было жалко, родилась идея сделать стильные настольные часы, но с современной начинкой.

Найденные лампы
Найденные лампы

В интернете полно схем на трансформаторах или микросхемах вроде К176ИЕ18, К176ИЕ13, но найти их сейчас сложно, а мотать трансформатор — долго, да еще и проволоку искать.
Поэтому было принято решение сделать все с доступными компонентами.

Требования к питанию индикаторов:
Для ИВ-11: накал — 1,5 В, анодное напряжение — около 50В для динамической индикации.
Для ИВ-6: накал желательно 1,2 В (можно просто добавить резистор в цепь накала).

Поиск по маркетплейсам привёл к простому и дешёвому решению:
XL6009 (повышающий DC‑DC) — для получения стабильных 45 В из 5 В USB.
MP2307 (понижающий DC‑DC) — для точной регулировки напряжения накала 1,5 В
В качестве «мозга» проекта я выбрал STM32F401CC на плате Black Pill.

Во‑первых, она просто валялась без дела.
Во‑вторых, у него есть аппаратный RTC, что избавляет от покупки отдельного модуля DS3231.
Управлять индикаторами будем с помощью 8-канального высоковольтного транзисторного драйвера верхнего ключа TD62783AP. Если заказываете с AliExpress — берите китайский аналог KID65783AP. Оригинальный TD62783AP там часто подделывают, и он может не работать.

Необходимые модули
Необходимые модули

С компонентами определились - пора проектировать плату. Схему и печатную плату я собрал в Altium Designer.

Схема
Схема
3D вид платы
3D вид платы

Как-то так, отправляем гербер в поднебесную, теперь осталась только ждать.
А пока ждем можно и написать прошивку, использую STM32CubeIDE. За основу я взял библиотеку segment_lcd с сайта MicroTechnics, она будет перебирать сегменты, в код добавил много комментарий, если кто то захочет в нем ковыряться.

Код целиком
#include "main.h"
#include "segment_lcd.h"
#include "stdio.h"
#include "button.h"

/* Private variables ---------------------------------------------------------*/
RTC_HandleTypeDef hrtc;               // Идентификатор RTC
TIM_HandleTypeDef htim3;
TIM_HandleTypeDef htim10;             // Идентификатор для обработки кнопок

RTC_TimeTypeDef sTime = {0};          // Структура для хранения времени
RTC_DateTypeDef DateToUpdate = {0};   // Структура для хранения даты

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);        // Настройка тактирования мк
static void MX_GPIO_Init(void);       // Инициализация портов
static void MX_RTC_Init(void);        // Инициализация RTC
static void MX_TIM10_Init(void);      // Инициализация таймера кнопок

int main(void)
{
  HAL_Init();                                       // Сброс периферийных устройств
  SystemClock_Config();                             // Настройка системной тактовой частоты

  // Периферия
  MX_GPIO_Init();                                   // Настройка пинов
  MX_RTC_Init();                                    // Настройка часов

  MX_TIM10_Init();                                  // Настройка таймера для кнопок


  HAL_PWR_EnableBkUpAccess();														// Разблокируем доступ к резервной области


//КАЛИБРОВКА=============================================================
//    HAL_RTCEx_SetSmoothCalib(&hrtc,
//  		  RTC_SMOOTHCALIB_PERIOD_32SEC,
//  		  RTC_SMOOTHCALIB_PLUSPULSES_SET,
//  		  270);
//=======================================================================


  HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);   // Получаем текущее время из RTC
  HAL_TIM_Base_Start_IT(&htim10);                   // Запускаем таймер кнопок в режиме прерываний

  while (1)
  {
	HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);                     // Получаем текущее время из RTC
	HAL_RTC_GetDate(&hrtc, &DateToUpdate, RTC_FORMAT_BIN);              // Получаем текущую дату из RTC

	BUTTON_Process();
	if (BUTTON_GetAction(BUTTON_SETTINGS) == BUTTON_SHORT_PRESS)        // КОРОТКОЕ НАЖАТИЕ: увеличение минут
		{
		 sTime.Minutes++;                                               // Увеличиваем минуты на 1
		 sTime.Seconds = 0;                                             // Сбрасываем секунды в 0
		 if(sTime.Minutes >=60)                                         // Если достигли 60 минут
	         {
			 sTime.Seconds = 0;                                         // Сбрасываем секунды в 0
	         sTime.Minutes = 0;											// Сбрасываем минуты в 0
	         }
	         HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);			// Записываем новое время в RTC
			 }

	if (BUTTON_GetAction(BUTTON_SETTINGS) == BUTTON_LONG_PRESS)         // ДЛИННОЕ НАЖАТИЕ: увеличение часов
	    {
	     sTime.Hours++;													// Увеличиваем часы на 1
	     if(sTime.Hours >=24)											// Если достигли 24 часов
	         {
	         sTime.Hours = 0;											// Сбрасываем часы в 0
	         }
	         HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);			// Записываем новое время в RTC
	    	 }
	         BUTTON_ResetActions();										// Сбрасываем флаги действий кнопок

	  // Обновление 7-сегментного индикатора
      SEG_LCD_Process(); 															// Динамическая индикация (опрос разрядов)
      HAL_Delay(1);																	// Небольшая задержка для стабильности (также земедляят перебор
      // Формирование строки для вывода на индикатор
      char str[DIGITS_NUM + 2];														// Буфер для строки (4 символа + точка + запас)

      // Мигание точки с периодом 2 секунды (проверка четности секунд)
      if(sTime.Seconds % 2 == 0)													// Четные секунды - показываем точку
      {
    	  snprintf(str, DIGITS_NUM +2, "%02d.%02d", sTime.Hours, sTime.Minutes );   // Формат: "ЧЧ.ММ" (например "12.30")
      }
      else																			// Нечетные секунды - без точки
      {
    	  snprintf(str, DIGITS_NUM +2, "%02d%02d", sTime.Hours, sTime.Minutes );
      }
      // Отправляем строку в драйвер индикатора
      SEG_LCD_WriteString(str);
  }
}

void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  // Включаем тактирование блока питания и доступ к резервной области
  __HAL_RCC_PWR_CLK_ENABLE();   													// Включаем тактирование питания
   // HAL_PWR_EnableBkUpAccess(); 													// Разблокируем домен резервного питания
  __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE2); 					// Настраиваем осцилляторы
  __HAL_RCC_RTC_ENABLE();															// Включаем тактирование RTC

  // Настройка генераторов HSE и LSE
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE|RCC_OSCILLATORTYPE_LSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;											// Внешний кварц 25 МГц
  RCC_OscInitStruct.LSEState = RCC_LSE_ON;											// Внешний кварц 32.768 кГц для RTC
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;										// Включаем PLL
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;								// Источник PLL - HSE
  RCC_OscInitStruct.PLL.PLLM = 25;													// Делитель: 25 МГц / 25 = 1 МГц
  RCC_OscInitStruct.PLL.PLLN = 168;													// Множитель: 1 МГц * 168 = 168 МГц
  RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;										// Делитель для системной шины: 168/2 = 84 МГц
  RCC_OscInitStruct.PLL.PLLQ = 4;													// Делитель для USB/SDIO

  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  // Настройка делителей для шин AHB/APB
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;							// Источник SYSCLK - PLL
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;								// HCLK = SYSCLK (84 МГц)
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;									// PCLK1 = HCLK/2 (42 МГц)
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;									// PCLK2 = HCLK (84 МГц)

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

static void MX_RTC_Init(void)
{
  RTC_TimeTypeDef sTime = {0};														// Временная структура для начальной установки
  RTC_DateTypeDef sDate = {0};														// Временная структура для начальной установки

  hrtc.Instance = RTC;
  hrtc.Init.HourFormat = RTC_HOURFORMAT_24;											// 24-часовой формат
  hrtc.Init.AsynchPrediv = 127;														// Асинхронный делитель (для LSE)
  hrtc.Init.SynchPrediv = 255;														// Синхронный делитель
  hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;											// Отключаем выход RTC
  hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
  hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;

  if (HAL_RTC_Init(&hrtc) != HAL_OK)
  {
    Error_Handler();
  }

  HAL_PWR_EnableBkUpAccess();														// Разблокируем доступ к резервной области

  //КАЛИБРОВКА=============================================================
    HAL_RTCEx_SetSmoothCalib(&hrtc,
  		  RTC_SMOOTHCALIB_PERIOD_32SEC,
  		  RTC_SMOOTHCALIB_PLUSPULSES_RESET,
  		  -142);
          //136
  //=======================================================================

  if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0) != 0x32F2)								// Проверяем маркер инициализации в backup-регистре
  {
    // Первый запуск - устанавливаем корректное время и дату
    sTime.Hours = 12;
    sTime.Minutes = 0;
    sTime.Seconds = 0;
    sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
    sTime.StoreOperation = RTC_STOREOPERATION_RESET;
    HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);

    sDate.WeekDay = RTC_WEEKDAY_THURSDAY;
    sDate.Month = RTC_MONTH_FEBRUARY;
    sDate.Date = 12;
    sDate.Year = 26;  // 2026 год
    HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN);

    // Сохраняем маркер инициализации
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, 0x32F2);
  }


}


static void MX_TIM10_Init(void)
{
  htim10.Instance = TIM10;
  htim10.Init.Prescaler = 8400-1;														// 84 МГц / 8400 = 10 кГц
  htim10.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim10.Init.Period = 10;
  htim10.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim10.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim10) != HAL_OK)
  {
    Error_Handler();
  }
}

static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  // Включаем тактирование портов
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOH_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();

  // Устанавливаем начальное состояние выходов (все выключены)
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15
                          |GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6, GPIO_PIN_RESET);
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8|GPIO_PIN_12|GPIO_PIN_10|GPIO_PIN_11, GPIO_PIN_RESET);

  // Настройка пина кнопки (PA2) как вход
  GPIO_InitStruct.Pin = GPIO_PIN_2;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_NOPULL;												// Без подтяжки (если есть внешняя)
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  // Настройка пинов сегментов и разрядов на PORTB как выходы
  GPIO_InitStruct.Pin = GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15
                          |GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;                                      // Двухтактный выход
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;									   // Низкая скорость (для индикаторов)
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  // Настройка пинов сегментов на PORTA как выходы
  GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_12|GPIO_PIN_10|GPIO_PIN_11;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

}


void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == htim10.Instance)										  // Если прерывание от таймера 10 - обрабатываем кнопки
  {
    BUTTON_TimerProcess();
  }
}

void Error_Handler(void)
{
  __disable_irq();
  while (1)
  {
  }

}

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

    HAL_RTCEx_SetSmoothCalib(&hrtc,
  		  RTC_SMOOTHCALIB_PERIOD_32SEC,
  		  RTC_SMOOTHCALIB_PLUSPULSES_SET,
  		  270);

Кнопка всего одна, длительное нажатие регулирует часы, короткое минуты. Прошиваем, собираем на макете и вроде все работает, В поддельных stm32 часто не работает RTC этот момент тоже стоит проверить.

Макетная плата
Макетная плата

Пока занимался кодом пришли платы:

Шелкография может не совпадать с финальной версией
Шелкография может не совпадать с финальной версией

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

Все (почти) компоненты на месте
Все (почти) компоненты на месте

Теперь нужен корпус. Я перенёс 3D-модель платы в Fusion 360 - так проще точно соблюдать отступы под отверстия, кнопку и разъём Type-C.

В корпусе предусмотрено:

  • Отверстие под кнопку.

  • Вырез под USB Type-C.

  • Отверстия для крепления платы.

Корпус
Корпус
Процесс печати
Процесс печати

Осталось собрать все в кучу и готово!

Готовое изделие
Готовое изделие

Ночью смотрятся тоже весьма не плохо.

Спасибо за внимание! Все файлы проекта (Gerber, схема в Altium, исходники прошивки, 3D-модель корпуса и HEX-файл) лежат в открытом репозитории на GitHub - ссылка в конце статьи.

Если у вас есть вопросы - пишите в комментариях, постараюсь ответить.

GitHub