Нашли в сети статью одного автора о том, как он усовершенствовал свою версию часов из вольтметра — перевели её для вас.

В далёком 2019 году я собрал свои первые простые часы с вольтметром, получилась вот такая штука:

Первая версия. Корпус, к слову, из вишни
Первая версия. Корпус, к слову, из вишни

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

Потом со временем понял, что таких поделок много, и выглядят они плюс-минус как моя. То есть коряво и довольно кустарно. Так что я решил сделать что-то красивое и современное, и задокументировать весь процесс создания. Он под катом.

Разработка началась с создания грубого макета в программе для 3D-моделирования. Я использовал Rhino3D.

Макет нового дизайна
Макет нового дизайна

Для новой версии часов я использовал три обычных панельных вольтметра, купленных на Amazon (ссылка, около 9 долларов за штуку). Я их разобрал, тщательно измерил циферблаты, а затем распечатал на самоклеящейся бумаге новые наклейки. Если вам нужны мои шаблоны в PDF — они здесь.

Новые наклейки
Новые наклейки

Обратите внимание, что на новом часовом циферблате 13 делений (от 0 до 12) а на минутном и секундном — 61 деление (от 00 до 60). Это потому, что я хотел реализовать непрерывное движение каждой стрелки. Иными словами — в 11:30 часовая стрелка должна была не просто стоять на отметке 11, а двигаться к двенадцатому делению, даже если бы она его не достигла.

Помимо кучи других проблем, у дешевых вольтметров Baomain 65C5, которые я купил, откровенно уродский пластиковый фланец. Я подумал, что будет круто его скрыть и использовать декоративный утопленный узор, чтобы передняя панель выглядела поинтереснее. Благодаря этой детали я смог вырезать переднюю и заднюю панели на станке с ЧПУ, а не делать корпус целиком вручную (как я сделал в первой, вишнёвой версии).

Получилось куда красивее
Получилось куда красивее

В качестве основного материала для корпуса я в этот раз взял кленовую доску, которую распиливают, строгают и обрабатывают обычными инструментами, а затем доводят до совершенства на станке с ЧПУ:

Обработанные передняя и задняя панели
Обработанные передняя и задняя панели

Что делать, если станка с ЧПУ у вас нет? Не печальтесь — проще всего будет собрать панель из двух склеенных частей, вырезав каждую из них по распечатанному (бумажному) шаблону. Чтобы идеально выровнять изгибы, можно использовать шлифовальный станок.

А вот изогнутая боковая стенка потребовала немного иного подхода. Чтобы добиться цельного вида, мне надо было согнуть плоский деревянный лист по шаблону. Для соблюдения нужного радиуса без использования станка для сгибания паром я решил сделать ряд надрезов с внутренней стороны. 

Вот так боковушки сгибаются вручную
Вот так боковушки сгибаются вручную

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

Склеиваем корпус часов с помощью внешнего шаблона (из фанеры)
Склеиваем корпус часов с помощью внешнего шаблона (из фанеры)

В общем, вот так выглядит готовая деталь после шлифовки и покрытия нитроцеллюлозным лаком:

Вроде, круто вышло.

Что внутри

Внутрянка, само собой, будет вам гораздо менее интересна, чем корпус. Но я всё равно расскажу.

На сборку ушло около часа: я взял почтенный микроконтроллер AVR128DB28, запитал его от сетевого адаптера и подключил к кварцевому резонатору на 8 МГц (ECS-80-18-4X-CKM). Если что, кварцевый резонатор на 32,768 кГц тоже подойдет. Панели подключены к трем цифровым выводам (PC0, PC1, PC2). Два входных контакта (PD6 и PD7) я соединил с двумя небольшими кнопками на задней панели — они нужны для установки времени.

Схема
Схема

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

Вот код.

Скрытый текст
/*

  Meter clock, version 2
  ----------------------

  Context: https://lcamtuf.substack.com/p/a-nicer-voltmeter-clock

  MCU: AVR128DA28

  Pinout: PC0, PC1, PC2 - PWM outputs to meters (other side to gnd)
          PA0, PA1      - 8 MHz crystal + 18 pF caps to gnd
          PD6, PD7      - time adjustment buttons (other side to gnd)

  The only other MCU connections are power supply pins and the UPDI
  programming header.

  Meter faces: https://lcamtuf.coredump.cx/soft/embedded/meter_clock2.pdf

  Complaints to: <lcamtuf@coredump.cx>

 */

#define F_CPU 8000000

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>

/* User-friendly typedefs */

typedef int8_t   s8;
typedef uint8_t  u8;
typedef int16_t  s16;
typedef uint16_t u16;
typedef int32_t  s32;
typedef uint32_t u32;

/* Configure clock. External 8 MHz crystal on PA0, PA1 */

static void setup_clock() {

  CCP = 0xd8;                       /* Unlock register access   */
  CLKCTRL.XOSCHFCTRLA = 0b10000001; /* Enable external clock    */

  while(!(CLKCTRL.MCLKSTATUS & 0b10000));

  CCP = 0xd8;                       /* Unlock register access   */
  CLKCTRL.MCLKCTRLA = 3;            /* Switch to external clock */

  while(!(CLKCTRL.MCLKSTATUS & 1));

}

/* Configure ports. */

static void setup_ports() {

  PORTA.DIR = 0b11111111;
  PORTC.DIR = 0b11111111;
  PORTD.DIR = 0b00111111; /* PA6, PA7: buttons */
  PORTF.DIR = 0b11111111;

  /* Pull-up for buttons */
  PORTD.PIN6CTRL = 0b00001000;
  PORTD.PIN7CTRL = 0b00001000;

  /* Slew rate limit for PWM output */
  PORTC.PORTCTRL = 1;

}

/* Configure tick update interrupt to run at 10 Hz */

static void setup_timer() {

  TCA0.SINGLE.INTCTRL  = 0b00000001; /* OVF interrupt every PER cycles     */
  TCA0.SINGLE.CTRLA    = 0b00001100; /* Clock prescaler / 256 (31.250 kHz) */
  TCA0.SINGLE.PER      = 3125 - 1;   /* Effective frequency 10 Hz          */
  TCA0.SINGLE.CTRLA   |= 1;          /* Timer enable                       */

}

/* Update time, handling wrap-around. */

static volatile u8  cur_hr, cur_min;   /* Hour (0-11) and minute (0-59)     */
static volatile u16 cur_secx10;        /* Tenth of a second counter (0-599) */

ISR(TCA0_OVF_vect) {

  cur_secx10++;

  if (cur_secx10 == 600) {

    cur_secx10 = 0;
    cur_min++;

    if (cur_min == 60) {

      cur_min = 0;
      cur_hr++;

      if (cur_hr == 12)  cur_hr = 0;

    }

  }

  TCA0.SINGLE.INTFLAGS = 1; /* Acknowledge interrupt */

}

/* Main entry point */

int main(void) {

  setup_clock();
  setup_ports();
  setup_timer();
  sei();

  /* Main PWM loop. Synchronous counter running from 0 to 599 at several hundred kHz. */

  u16 duty_ctr = 0; /* PWM counter        */
  u8  key_last = 0; /* Previous key state */

  u16 adj_minx10, adj_hrx10, adj_secx10; /* Computed duty cycles for meters */

  while (1) {

    /* Compute duty cycles. The minute gauge has divisions from 0 to 60. One minute corresponds
       to a duty cycle step of 10, so we multiply the minute counter accordingly, and then add
       another value between 0 and 9 based on the state of the second counter.

       For the hour gauge, we have divisions from 0 to 12, and one hour corresponds to an
       increment of 50. We multiply the hour counter by 50 and add another 0-49 depending on
       the minute counter.

       This is also where you can incorporate fudge factors if the meters aren't precise. */

    if (!duty_ctr) {
      adj_minx10 = cur_min * 10 + cur_secx10 / 60,
      adj_hrx10  = cur_hr  * 50 + adj_minx10 / 12,
      adj_secx10 = cur_secx10;
    }

    /* PWM actuation */

    u8 pcval = 0;

    if (adj_secx10 > duty_ctr) pcval  = 0b100;
    if (adj_minx10 > duty_ctr) pcval |= 0b010;
    if (adj_hrx10  > duty_ctr) pcval |= 0b001;

    PORTC.OUT = pcval;
    duty_ctr++;

    /* After 600 cycles, we reset the PWM counter and check for keypresses. */

    if (duty_ctr == 600) {

      duty_ctr = 0;

      u8 pd = (PORTD.IN >> 6);

      /* Both inputs are high: reset state and continue */
      if (pd == 0b11) { key_last = 0; continue; }

      /* At least one button pressed. If this is a continuation of a previous keypress, bail out. */
      if (key_last) continue;
      key_last = 1;

      /* Key 1 advances the minute dial while zeroing the seconds. */
      if (!(pd & 0b10)) {
        cur_secx10 = 0;
        if (++cur_min == 60) cur_min = 0;
      }

      /* Key 0 advances the hour dial without messing up any of the other dials. */
      if (!(pd & 0b01)) {
        if (++cur_hr == 12) cur_hr = 0;
      }

    }

  }

}

Основная идея заключается в том, чтобы с помощью прерывания таймера, синхронизированного с кварцевым резонатором, продвигать счётчик с частотой 10 Гц. После этого основной цикл обработки событий вычисляет соответствующий коэффициент заполнения и вручную переключает выходные контакты. Несмотря на то, что в микросхеме есть аппаратный модуль ШИМ, задача настолько проста, что использование схемы ШИМ ничего нам не даст.

А вот видео с «перелистыванием», снятое примерно в 11:59:59